'use client'; import { useEffect, useRef, useState } from 'react'; import { useRouter } from 'next/navigation'; import { AlertTriangle, CheckCircle2, Droplets, Play, Square, X } from 'lucide-react'; import { BadgeCamera } from './BadgeCamera'; import { SearchPicker } from './SearchPicker'; import { clockTime } from '@/lib/clock-display'; import { assetFromQr, oilCodeFromQr } from '@/lib/stock-out'; import { formatMinutes, JOB_TYPES, minutesBetween, type JobType } from '@/lib/job-cards'; type Asset = { id: string; code: string; name: string; qr?: string }; type Mechanic = { id: string; name: string; code: string }; type Oil = { id: string; code: string; name: string; colour: string; onHand: number }; type OpenJob = { id: string; asset: string; mechanic: string; startedAt: string }; const LAST_MECHANIC = 'mp:lastMechanic'; /** * Scan a vehicle's QR. No open job on it → START one (pick who is working). An open job → * FINISH it: odometer, kind of work and a note. The clock runs from the first scan. */ export function JobScanner({ assets, mechanics, oils, open }: { assets: Asset[]; mechanics: Mechanic[]; oils: Oil[]; open: OpenJob[] }) { const router = useRouter(); const [asset, setAsset] = useState(null); // Oil is its own path: scan the drum, say how many litres and which vehicle. No odometer, no photo. const [oil, setOil] = useState(null); const [oilAssetId, setOilAssetId] = useState(''); const [oilLitres, setOilLitres] = useState(''); const [mechanicId, setMechanicId] = useState(''); const [odometer, setOdometer] = useState(''); const [noMeter, setNoMeter] = useState(false); const [jobType, setJobType] = useState(''); const [note, setNote] = useState(''); const [busy, setBusy] = useState(false); const [message, setMessage] = useState<{ ok: boolean; text: string } | null>(null); const [unknownScan, setUnknownScan] = useState(''); const [now, setNow] = useState(() => new Date().toISOString()); const lastScan = useRef({ value: '', until: 0 }); useEffect(() => { const t = setInterval(() => setNow(new Date().toISOString()), 30000); return () => clearInterval(t); }, []); useEffect(() => { try { const m = localStorage.getItem(LAST_MECHANIC); if (m) setMechanicId(m); } catch { /* private mode */ } }, []); const job = asset ? open.find(j => j.asset === asset.id) ?? null : null; const assetById = (id: string) => assets.find(a => a.id === id); function choose(next: Asset | null) { setOil(null); setOilAssetId(''); setOilLitres(''); setAsset(next); setOdometer(''); setNoMeter(false); setJobType(''); setNote(''); setUnknownScan(''); } function onDecode(raw: string) { if (asset || oil || busy) return; if (lastScan.current.value === raw && Date.now() < lastScan.current.until) return; lastScan.current = { value: raw, until: Date.now() + 4000 }; const oilCode = oilCodeFromQr(raw); const oilFound = oilCode && oils.find(o => o.code.toUpperCase() === oilCode.toUpperCase()); if (oilFound) { setMessage(null); setUnknownScan(''); setOil(oilFound); navigator.vibrate?.(120); return; } const found = assetFromQr(raw, assets); if (found) { setMessage(null); choose(found); navigator.vibrate?.(120); } else setUnknownScan(raw.slice(0, 60)); } async function call(url: string, body: object) { setBusy(true); setMessage(null); try { const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || 'Could not save.'); return data; } catch (e) { setMessage({ ok: false, text: e instanceof Error ? e.message : 'Could not save.' }); return null; } finally { setBusy(false); } } async function start(e: React.FormEvent) { e.preventDefault(); if (!asset || !mechanicId) return; const data = await call('/api/clock/job-start', { asset_id: asset.id, mechanic_id: mechanicId }); if (!data) return; try { localStorage.setItem(LAST_MECHANIC, mechanicId); } catch { /* ignore */ } setMessage({ ok: true, text: `Job started on ${data.asset} for ${data.mechanic} at ${clockTime(data.at)}.${data.oil ? ` ${data.oil} oil booking${data.oil > 1 ? 's' : ''} from today added to the job card.` : ''}` }); choose(null); router.refresh(); navigator.vibrate?.(120); } async function bookOil(e: React.FormEvent) { e.preventDefault(); if (!oil || !oilAssetId) return; const data = await call('/api/clock/oil-out', { item_id: oil.id, asset_id: oilAssetId, litres: oilLitres.trim() }); if (!data) return; setMessage({ ok: true, text: `${data.litres} L of ${data.item} booked out to ${data.asset}. ${data.onJob ? 'Added to its open job card.' : 'It joins the job card if you start a job on that vehicle today.'}` }); choose(null); router.refresh(); navigator.vibrate?.(120); } async function finish(e: React.FormEvent) { e.preventDefault(); if (!job || !jobType) return; const data = await call('/api/clock/job-finish', { job_id: job.id, job_type: jobType, note, odometer, no_meter: noMeter }); if (!data) return; setMessage({ ok: true, text: `Job on ${data.asset} finished — ${formatMinutes(data.minutes)} on the job.` }); choose(null); router.refresh(); navigator.vibrate?.(120); } return

MEGA PLANT

Job cards

{message &&
{message.ok ? : }

{message.text}

} {oil ?

{oil.name}

{oil.onHand.toLocaleString('en-ZA', { maximumFractionDigits: 2 })} L in stock · booked out as soon as you save

setOilLitres(e.target.value)} placeholder="0"/> ({ id: a.id, label: a.code, sub: a.name }))}/> : !asset ? <> {/* Search sits ABOVE the camera so camera + search fit one screen and the list opens over the camera. */}
{ const found = assetById(id); if (found) { setMessage(null); choose(found); } }} options={assets.map(a => ({ id: a.id, label: a.code, sub: a.name }))}/>
{unknownScan &&

That QR is not a vehicle in the plant register or an oil label ({unknownScan}).

} : !job ?

{asset.code} · {asset.name}

No job open — this starts one, clock running from now.

({ id: m.id, label: m.name, sub: m.code }))}/> :

{asset.code} · {asset.name}

{job.mechanic} · started {clockTime(job.startedAt.replace(' ', 'T'))} · {formatMinutes(minutesBetween(job.startedAt, now))} so far

setOdometer(e.target.value)} placeholder="Current reading"/>

What kind of work was it?

{JOB_TYPES.map(t => )}