import { useState, useEffect } from 'react' import Layout from '@/components/layout/Layout' import { Card, EmptyState, Btn, Modal, Field, Input, Select, Textarea, showToast, Tag, StatusDot, Table, StatCard } from '@/components/ui' import { useApp } from '@/lib/context' import { SHIPMENT_SEND_ROLES } from '@/lib/auth' const SEV_COLOR: Record = { OBSERVATION: 'gray', MINOR: 'amber', MAJOR: 'red' } const STATUS_COLOR: Record = { OPEN: 'blue', INVESTIGATING: 'amber', ESCALATED: 'red', RESOLVED: 'green' } const CAT_COLOR: Record = { Sealing: 'purple', Packaging: 'amber', Calibration: 'gray', Supplier: 'red', Process: 'green', Training: 'gray', Other: 'gray' } const PRIORITY_FROM_SEVERITY: Record = { OBSERVATION: 'LOW', MINOR: 'MEDIUM', MAJOR: 'HIGH' } const CATEGORIES = ['Sealing', 'Packaging', 'Calibration', 'Supplier', 'Process', 'Training', 'Other'] export default function EscapesPage() { const { user } = useApp() const [escapes, setEscapes] = useState([]) const [shipments, setShipments] = useState([]) const [loading, setLoading] = useState(true) const [selected, setSelected] = useState(null) const [similar, setSimilar] = useState([]) const [resNote, setResNote] = useState('') const [resCategory, setResCategory] = useState('') const [stdText, setStdText] = useState('') const [users, setUsers] = useState([]) // Report modal const [reportOpen, setReportOpen] = useState(false) const [reportForm, setReportForm] = useState({ shipmentId: '', contact: '', description: '' }) // Escalate modal const [escalateOpen, setEscalateOpen] = useState(false) const [escForm, setEscForm] = useState({ title: '', priority: 'MEDIUM', ownerId: '', dueDate: '' }) const canReport = user && (SHIPMENT_SEND_ROLES as readonly string[]).includes(user.role) useEffect(() => { load() }, []) async function load() { setLoading(true) const [er, sr] = await Promise.all([fetch('/api/escapes'), fetch('/api/shipments')]) if (er.ok) { const { data } = await er.json(); setEscapes(data || []) } if (sr.ok) { const { data } = await sr.json(); setShipments(data || []) } setLoading(false) } async function loadSimilar(description: string) { const res = await fetch(`/api/resolutions?similarTo=${encodeURIComponent(description)}`) if (res.ok) { const { data } = await res.json(); setSimilar(data || []) } } function openDetail(e: any) { setSelected(e) setResNote(e.resolution || '') setResCategory(e.category || '') setStdText('') setSimilar([]) if (e.status !== 'RESOLVED') loadSimilar(e.description) } async function submitReport() { if (!reportForm.shipmentId || !reportForm.description.trim()) { showToast('Shipment and description required', 'error'); return } const res = await fetch('/api/escapes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(reportForm), }) if (res.ok) { const { data } = await res.json() setReportOpen(false) setReportForm({ shipmentId: '', contact: '', description: '' }) showToast(`${data.ref} logged`) load() openDetail(data) } else { showToast('Failed to log issue', 'error') } } async function patch(body: any) { const res = await fetch(`/api/escapes/${selected.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) if (res.ok) { const { data } = await res.json() setSelected(data) load() return data } return null } async function classify(severity: string) { await patch({ severity }) } async function setStatus(status: string) { await patch({ status }) } async function resolve() { if (!resNote.trim()) { showToast('Resolution notes required', 'error'); return } if (!resCategory) { showToast('Category required for filing', 'error'); return } const data = await patch({ resolution: resNote, category: resCategory }) if (data) showToast('Resolved and filed to resolutions library') } async function addToStandard() { if (!stdText.trim()) { showToast('Describe the new check', 'error'); return } const data = await patch({ standardItem: stdText }) if (data) showToast('Shipping standard updated') } async function skipStandard() { await patch({ standardItem: '' }) } function useFix(r: any) { setResNote(`Reapplied previous fix: ${r.resolution}`) setResCategory(r.category) } function openEscalate() { setEscForm({ title: selected.description, priority: PRIORITY_FROM_SEVERITY[selected.severity] || 'HIGH', ownerId: '', dueDate: '', }) setEscalateOpen(true) } async function confirmEscalate() { if (!escForm.ownerId || !escForm.dueDate) { showToast('Owner and due date required', 'error'); return } const data = await patch({ escalate: true, capaForm: escForm }) if (data) { showToast(`${data.capa?.ref} created and linked`) setEscalateOpen(false) } } useEffect(() => { fetch('/api/users').then(r => r.ok && r.json()).then(d => d && setUsers(d.data || [])) }, []) const kpi = { total: escapes.length, open: escapes.filter(e => e.status === 'OPEN' || e.status === 'INVESTIGATING').length, res: escapes.filter(e => e.status === 'RESOLVED').length, std: escapes.filter(e => e.standardItemAdded && e.standardItemAdded !== '—').length, } return (

Client issues — quality escapes

Defects that passed QC and reached a client — investigated like an NCR, and can update the shipping standard

{canReport && shipments.length > 0 && setReportOpen(true)}>+ Report client issue}
Report access: Production leads · Logistics lead · Admin
{loading ? (
Loading…
) : escapes.length === 0 ? ( ) : ( {escapes.map((e: any) => ( openDetail(e)} style={{ cursor: 'pointer' }}> ))}
{e.ref} {e.shipment.product} — Batch {e.shipment.batch}
{e.shipment.client}
{e.description.length > 60 ? e.description.slice(0, 60) + '…' : e.description} {e.severity ? {e.severity.charAt(0) + e.severity.slice(1).toLowerCase()} : Needs triage} {e.status.charAt(0) + e.status.slice(1).toLowerCase()} {new Date(e.createdAt).toLocaleDateString()} View
)}
{/* Report modal */} setReportOpen(false)} title="Report client issue" width={460}>
This logs a quality escape — a defect that reached the client after passing QC — and links it to the shipment.
setReportForm(f => ({ ...f, contact: e.target.value }))} placeholder="e.g. Acme QA team"/>