'use client'; import { useState, useRef } from 'react'; import Link from 'next/link'; import { Wrench, Camera, X, Loader2, ClipboardList, AlertTriangle } from 'lucide-react'; import { useTranslations } from 'next-intl'; import { trpc } from '@/lib/trpc/client'; import type { RouterOutputs } from '@/lib/trpc/server'; import { LanguageSwitcher } from '../language-switcher'; type Defect = RouterOutputs['qualityDefect']['queue'][number]; type DefectStatus = 'OPEN' | 'ACKNOWLEDGED' | 'CORRECTED'; type TFn = (key: string, values?: Record) => string; function timeAgo(date: Date | string, t: TFn): string { const diffMs = Date.now() - new Date(date).getTime(); const mins = Math.floor(diffMs / 60_000); if (mins < 1) return t('now'); if (mins < 60) return t('minutesAgo', { mins }); const hours = Math.floor(mins / 60); if (hours < 24) return t('hoursAgo', { hours }); return t('daysAgo', { days: Math.floor(hours / 24) }); } 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; }); } const STATUS_CLASS: Record = { OPEN: 'bg-orange-100 text-orange-700', ACKNOWLEDGED: 'bg-blue-100 text-blue-700', CORRECTED: 'bg-green-100 text-green-700', }; function Thumbnail({ photoKey, alt }: { photoKey: string | null; alt: string }) { const { data } = trpc.storage.signPhotoDownload.useQuery( { photoKey: photoKey! }, { enabled: !!photoKey, staleTime: 50_000 }, ); if (!photoKey) return null; if (!data?.url) return
; return ( // eslint-disable-next-line @next/next/no-img-element {alt} ); } // ── New-defect form ─────────────────────────────────────────────────────────── function NewDefectForm({ onCreated, t, }: { onCreated: () => void; t: ReturnType>; }) { const fileRef = useRef(null); const [workstationId, setWorkstationId] = useState(''); const [defectType, setDefectType] = useState(''); const [location, setLocation] = useState(''); const [rfsCode, setRfsCode] = 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 = [] } = trpc.workstation.list.useQuery(undefined, { staleTime: 60 * 60 * 1000, }); const signUpload = trpc.storage.signPhotoUpload.useMutation(); const create = trpc.qualityDefect.create.useMutation(); 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('form.photoError')); } } function removePhoto() { if (photoPreview) URL.revokeObjectURL(photoPreview); setPhotoBlob(null); setPhotoPreview(null); if (fileRef.current) fileRef.current.value = ''; } function reset() { setWorkstationId(''); setDefectType(''); setLocation(''); setRfsCode(''); setDescription(''); removePhoto(); } async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!workstationId || defectType.trim().length < 1 || description.trim().length < 3) return; setSubmitting(true); setError(null); try { let photoKey: string | undefined; if (photoBlob) { const signed = await signUpload.mutateAsync({ contentType: 'image/jpeg', byteSize: photoBlob.size, category: 'quality', }); const res = await fetch(signed.uploadUrl, { method: 'PUT', body: photoBlob, headers: { 'Content-Type': 'image/jpeg' }, }); if (!res.ok) throw new Error(`Photo PUT ${res.status}`); photoKey = signed.photoKey; } await create.mutateAsync({ workstationId, defectType: defectType.trim(), location: location.trim() || undefined, rfsCode: rfsCode.trim() || undefined, description: description.trim(), photoKey, }); reset(); onCreated(); } catch { setError(t('form.submitError')); } finally { setSubmitting(false); } } const canSubmit = workstationId !== '' && defectType.trim().length >= 1 && description.trim().length >= 3 && !submitting; return (

{t('newDefect')}