'use client'; import { useState, useRef } from 'react'; import { useRouter } from 'next/navigation'; import Link from 'next/link'; import { ArrowLeft, Camera, X } from 'lucide-react'; import { trpc } from '@/lib/trpc/client'; import { db } from '@/lib/queue/db'; import { runSync } from '@/lib/queue/sync'; // Resize to max 1600px on longest side and compress to JPEG q=0.8. function compressImage(file: File): Promise { return new Promise((resolve, reject) => { const img = new Image(); const url = URL.createObjectURL(file); img.onload = () => { URL.revokeObjectURL(url); const MAX = 1600; let { width, height } = img; if (width > MAX || height > MAX) { if (width >= height) { height = Math.round((height * MAX) / width); width = MAX; } else { width = Math.round((width * MAX) / height); height = MAX; } } const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); if (!ctx) return reject(new Error('Canvas context unavailable')); ctx.drawImage(img, 0, 0, width, height); canvas.toBlob( (blob) => (blob ? resolve(blob) : reject(new Error('Canvas toBlob failed'))), 'image/jpeg', 0.8, ); }; img.onerror = () => reject(new Error('Image load failed')); img.src = url; }); } export default function NewRequestPage() { const router = useRouter(); const fileRef = useRef(null); const [workstationId, setWorkstationId] = useState(''); const [description, setDescription] = useState(''); const [photoBlob, setPhotoBlob] = useState(null); const [photoPreview, setPhotoPreview] = useState(null); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const { data: workstations = [], isLoading: wsLoading } = trpc.workstation.list.useQuery( undefined, { staleTime: 60 * 60 * 1000 }, // 1h — serves from cache when offline ); async function handlePhotoChange(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; try { const compressed = await compressImage(file); if (photoPreview) URL.revokeObjectURL(photoPreview); setPhotoBlob(compressed); setPhotoPreview(URL.createObjectURL(compressed)); } catch { setError('Não foi possível processar a foto. Tenta de novo.'); } } function removePhoto() { if (photoPreview) URL.revokeObjectURL(photoPreview); setPhotoBlob(null); setPhotoPreview(null); if (fileRef.current) fileRef.current.value = ''; } async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!workstationId || description.trim().length < 3) return; setSubmitting(true); setError(null); try { const clientRequestId = crypto.randomUUID(); // Enqueue in IndexedDB immediately — returns control to the user // regardless of network state. The SyncProvider will drain the queue. await db.pending.add({ clientRequestId, workstationId, description: description.trim(), photoBlob: photoBlob ?? undefined, queuedAt: Date.now(), retries: 0, }); // Attempt immediate sync if online (fire-and-forget) if (navigator.onLine) runSync().catch(() => {}); router.push(`/maintenance/sent?cid=${clientRequestId}`); } catch (err) { setError(err instanceof Error ? err.message : 'Erro ao guardar pedido. Tenta de novo.'); setSubmitting(false); } } const descLen = description.length; const canSubmit = workstationId !== '' && descLen >= 3 && descLen <= 1000 && !submitting; return (

Novo pedido de manutenção

{/* Posto */}
{/* Foto */}
Foto (opcional) {photoPreview ? (
{/* eslint-disable-next-line @next/next/no-img-element */} Pré-visualização
) : ( )}
{/* Descrição */}