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' 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 NCRPage() { const { user } = useApp() const [tab, setTab] = useState<'register' | 'library'>('register') // Register const [ncrs, setNcrs] = useState([]) const [loading, setLoading] = useState(true) const [filter, setFilter] = useState('ALL') const [selected, setSelected] = useState(null) const [similar, setSimilar] = useState([]) const [resNote, setResNote] = useState('') const [resCategory, setResCategory] = useState('') const [users, setUsers] = useState([]) // Escalate modal const [escalateOpen, setEscalateOpen] = useState(false) const [escForm, setEscForm] = useState({ title: '', priority: 'MEDIUM', ownerId: '', dueDate: '' }) // Library const [resolutions, setResolutions] = useState([]) const [libSearch, setLibSearch] = useState('') const [libCategory, setLibCategory] = useState('ALL') useEffect(() => { loadNcrs() }, [filter]) useEffect(() => { if (tab === 'library') loadLibrary() }, [tab, libSearch, libCategory]) useEffect(() => { fetch('/api/users').then(r => r.ok && r.json()).then(d => d && setUsers(d.data || [])) }, []) async function loadNcrs() { setLoading(true) const params = filter !== 'ALL' ? `?status=${filter}` : '' const res = await fetch(`/api/ncrs${params}`) if (res.ok) { const { data } = await res.json(); setNcrs(data || []) } setLoading(false) } async function loadLibrary() { const params = new URLSearchParams() if (libCategory !== 'ALL') params.set('category', libCategory) if (libSearch) params.set('search', libSearch) const res = await fetch(`/api/resolutions?${params}`) if (res.ok) { const { data } = await res.json(); setResolutions(data || []) } } 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(ncr: any) { setSelected(ncr) setResNote(ncr.resolution || '') setResCategory(ncr.category || '') setSimilar([]) if (ncr.status !== 'RESOLVED') loadSimilar(ncr.description) } async function classify(severity: string) { const res = await fetch(`/api/ncrs/${selected.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ severity }), }) if (res.ok) { const { data } = await res.json() setSelected(data) loadNcrs() } } async function setStatus(status: string) { const res = await fetch(`/api/ncrs/${selected.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }), }) if (res.ok) { const { data } = await res.json() setSelected(data) loadNcrs() } } async function resolve() { if (!resNote.trim()) { showToast('Resolution notes required', 'error'); return } if (!resCategory) { showToast('Category required for filing', 'error'); return } const res = await fetch(`/api/ncrs/${selected.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ resolution: resNote, category: resCategory }), }) if (res.ok) { const { data } = await res.json() setSelected(data) showToast('Resolved and filed to resolutions library') loadNcrs() } } async function confirmSolution() { const res = await fetch(`/api/ncrs/${selected.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ confirmNotify: true }), }) if (res.ok) { const { data } = await res.json() setSelected(data) showToast(`${data.raisedBy?.name || 'Reporter'} notified — fix confirmed`) loadNcrs() } } 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] || 'MEDIUM', ownerId: '', dueDate: '', }) setEscalateOpen(true) } async function confirmEscalate() { if (!escForm.ownerId || !escForm.dueDate) { showToast('Owner and due date required', 'error'); return } const res = await fetch(`/api/ncrs/${selected.id}/escalate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(escForm), }) if (res.ok) { const { data } = await res.json() showToast(`${data.capa.ref} created and linked`) setEscalateOpen(false) setSelected(data.ncr) loadNcrs() } else { showToast('Escalation failed', 'error') } } const filters = ['ALL', 'OPEN', 'INVESTIGATING', 'ESCALATED', 'RESOLVED'] const kpi = { open: ncrs.filter(n => n.status === 'OPEN').length, inv: ncrs.filter(n => n.status === 'INVESTIGATING').length, esc: ncrs.filter(n => n.status === 'ESCALATED').length, res: ncrs.filter(n => n.status === 'RESOLVED').length, } return (
{(['register', 'library'] as const).map(t => ( ))}
{tab === 'register' ? ( <>
{filters.map(f => ( ))}
{loading ? (
Loading…
) : ncrs.length === 0 ? ( ) : ( {ncrs.map(n => ( openDetail(n)} style={{ cursor: 'pointer' }}> ))}
{n.ref} {n.description.length > 80 ? n.description.slice(0, 80) + '…' : n.description} {n.source || '—'} {n.severity ? {n.severity.charAt(0) + n.severity.slice(1).toLowerCase()} : Needs triage} {n.status.charAt(0) + n.status.slice(1).toLowerCase()} {n.status === 'RESOLVED' && ( {n.notified ? '●✓' : '●'} )} {new Date(n.createdAt).toLocaleDateString()} View
)}
) : (
setLibSearch(e.target.value)} placeholder="Search past fixes — e.g. 'caps', 'supplier', 'torque'…" style={{ flex: 1, minWidth: '200px', padding: '6px 10px', fontSize: '12px', border: '0.5px solid #ddd', borderRadius: '8px', outline: 'none', fontFamily: 'inherit' }}/>
{['ALL', ...CATEGORIES].map(c => ( ))}
{resolutions.length === 0 ? ( ) : resolutions.map(r => (
{r.title}
{r.category}
{r.resolution}
Filed from {r.linkedRef} · {new Date(r.createdAt).toLocaleDateString()}
))}
)} {/* Detail modal */} {selected && ( setSelected(null)} title={selected.ref} width={500}>
{selected.severity ? {selected.severity.charAt(0) + selected.severity.slice(1).toLowerCase()} : Needs triage} {' '}{selected.status.charAt(0) + selected.status.slice(1).toLowerCase()}

{selected.description}

Source: {selected.source || '—'} · Raised by {selected.raisedBy?.name} · {new Date(selected.createdAt).toLocaleDateString()}
{selected.capa && (
Escalated to {selected.capa.ref} — root cause investigation continues there.
)} {selected.status !== 'RESOLVED' && similar.length > 0 && (
Similar issue fixed before
{similar[0].resolution}
From {similar[0].linkedRef} · category: {similar[0].category}
useFix(similar[0])} style={{ fontSize: '11px', color: '#27500A', fontWeight: '500', cursor: 'pointer' }}>Use this fix
)} {!selected.severity && ( <>
Classify severity
{['OBSERVATION', 'MINOR', 'MAJOR'].map(s => ( classify(s)} style={{ flex: 1, justifyContent: 'center' }}>{s.charAt(0) + s.slice(1).toLowerCase()} ))}
)} {selected.status === 'RESOLVED' ? ( <>
Resolution {selected.category}
{selected.resolution}
Filed to resolutions library for future reference.
{selected.notified ? (
{selected.raisedBy?.name} notified — solution confirmed.
) : (
Admin review
QC has filed a fix. Confirm it's solved and notify {selected.raisedBy?.name} — they don't need to follow up themselves.
Confirm solution found — notify {selected.raisedBy?.name}
)} ) : ( <>