'use client'; import { useState, useRef } from 'react'; import { useRouter } from 'next/navigation'; import Link from 'next/link'; import { ArrowLeft, Camera, X, MapPin } from 'lucide-react'; import { useTranslations } from 'next-intl'; import { trpc } from '@/lib/trpc/client'; import { db } from '@/lib/queue/db'; import { runSync } from '@/lib/queue/sync'; 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 t = useTranslations('maintenance'); const router = useRouter(); const fileRef = useRef(null); const [description, setDescription] = useState(''); const [photoBlob, setPhotoBlob] = useState(null); const [photoPreview, setPhotoPreview] = useState(null); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); // Workstation is no longer chosen per-request: it comes from the operator's // active badge-in session. It still travels in the queued payload so offline // submissions remain self-contained even if the operator later changes posto. const { data: session, isLoading: sessionLoading } = trpc.operatorSession.current.useQuery(); const workstationId = session?.workstationId ?? ''; 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(t('photoError')); } } 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(); await db.pending.add({ clientRequestId, workstationId, description: description.trim(), photoBlob: photoBlob ?? undefined, queuedAt: Date.now(), retries: 0, }); if (navigator.onLine) runSync().catch(() => {}); router.push(`/maintenance/sent?cid=${clientRequestId}`); } catch (err) { setError(err instanceof Error ? err.message : t('saveError')); setSubmitting(false); } } const descLen = description.length; const canSubmit = workstationId !== '' && descLen >= 3 && descLen <= 1000 && !submitting; return (

{t('newTitle')}

{/* Workstation — read-only, from the active session */}
{t('workstationLabel')} {sessionLoading ? (

{t('workstationLoading')}

) : session ? (
{session.workstation.code} — {session.workstation.name} · {session.workstation.area}
) : (

{t('noSession')}

)}
{/* Photo */}
{t('photoLabel')} {photoPreview ? (
{/* eslint-disable-next-line @next/next/no-img-element */} {t('photoPreview')}
) : ( )}
{/* Description */}