'use client'; import { useEffect, useRef, useState } from 'react'; import { useRouter } from 'next/navigation'; import { AlertTriangle, Camera, CheckCircle2, Droplets, X } from 'lucide-react'; import { BadgeCamera } from './BadgeCamera'; import { SearchPicker } from './SearchPicker'; import { assetFromQr, oilCodeFromQr } from '@/lib/stock-out'; import { shrinkImage } from '@/lib/shrink-image'; type Item = { id: string; code: string; name: string; colour: string; onHand: number; tank: boolean }; type Asset = { id: string; code: string; name: string; qr?: string }; const fmt = (n: number) => n.toLocaleString('en-ZA', { maximumFractionDigits: 2 }); /** Scan the drum/tank QR, say how many litres and which vehicle, and it is booked out. */ type Employee = { name: string; code: string }; const LAST_EMPLOYEE = 'mp:lastFuelEmployee'; export function StockOut({ items, assets, employees }: { items: Item[]; assets: Asset[]; employees: Employee[] }) { const router = useRouter(); const [item, setItem] = useState(null); const [litres, setLitres] = useState(''); const [assetId, setAssetId] = useState(''); const [busy, setBusy] = useState(false); const [message, setMessage] = useState<{ ok: boolean; text: string } | null>(null); const [unknownScan, setUnknownScan] = useState(''); const [employee, setEmployee] = useState(''); const [odometer, setOdometer] = useState(''); const [photo, setPhoto] = useState(null); const [preview, setPreview] = useState(''); const lastScan = useRef({ value: '', until: 0 }); useEffect(() => { try { const e = localStorage.getItem(LAST_EMPLOYEE); if (e) setEmployee(e); } catch { /* private mode */ } }, []); function pick(next: Item | null) { setItem(next); setLitres(''); setAssetId(''); setUnknownScan(''); setPhoto(null); setPreview(''); setOdometer(''); } async function takePhoto(file: File | undefined) { if (!file) return; const small = await shrinkImage(file, 'meter.jpg'); setPhoto(small); setPreview(URL.createObjectURL(small)); } function onDecode(raw: string) { if (item || busy) return; if (lastScan.current.value === raw && Date.now() < lastScan.current.until) return; lastScan.current = { value: raw, until: Date.now() + 4000 }; const code = oilCodeFromQr(raw); const found = code && items.find(i => i.code.toUpperCase() === code.toUpperCase()); if (found) { setMessage(null); pick(found); navigator.vibrate?.(120); return; } // A vehicle's own QR books DIESEL from the main tank to that vehicle. const vehicle = assetFromQr(raw, assets); const tank = items.find(i => i.tank); if (vehicle && tank) { setMessage(null); pick(tank); setAssetId(vehicle.id); navigator.vibrate?.(120); return; } setUnknownScan(raw.slice(0, 60)); } async function submit(e: React.FormEvent) { e.preventDefault(); if (!item || busy) return; setBusy(true); setMessage(null); try { const body = new FormData(); body.set('item_id', item.id); body.set('asset_id', assetId); body.set('litres', litres.trim()); body.set('meter_photo', photo!); if (odometer.trim()) body.set('odometer', odometer.trim()); if (employee) body.set('employee_code', employee); const res = await fetch('/api/clock/stock-out', { method: 'POST', body }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || 'Could not save.'); setMessage({ ok: true, text: `${fmt(data.litres)} L of ${data.item} booked to ${data.asset}.` }); try { if (employee) localStorage.setItem(LAST_EMPLOYEE, employee); } catch { /* ignore */ } pick(null); router.refresh(); navigator.vibrate?.(120); } catch (err) { setMessage({ ok: false, text: err instanceof Error ? err.message : 'Could not save.' }); } finally { setBusy(false); } } const entered = Number(litres); const over = item && entered > item.onHand; return

MEGA PLANT

Book out diesel

{message &&
{message.ok ? : }

{message.text}

} {!item ? <> {/* Search above the camera: both fit one screen and the list opens over the camera. */}
{ const found = assets.find(a => a.id === id); const tank = items.find(i => i.tank); if (found && tank) { setMessage(null); pick(tank); setAssetId(found.id); } }} 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 the diesel tank label ({unknownScan}). Oil is booked on the Job cards tab.

} :

{item.name}

{fmt(item.onHand)} L in {item.tank ? 'the tank' : 'stock'}

({ id: e.code, label: e.name, sub: e.code }))}/> setLitres(e.target.value)} placeholder="0"/> {over &&

That is more than the {fmt(item.onHand)} L recorded. It will still be booked — check the stock level afterwards.

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

Photo of the meter

{ void takePhoto(e.target.files?.[0]); e.target.value = ''; }}/> {preview && Meter photo to upload} }
; } /** Type to filter by code or name, tap to choose. A native { setQuery(''); setOpen(true); }} onChange={e => { setQuery(e.target.value); setOpen(true); if (value) onChange(''); }} onBlur={() => setTimeout(() => setOpen(false), 150)} onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); if (matches[0]) { onChange(matches[0].id); setOpen(false); } } if (e.key === 'Escape') setOpen(false); }}/> {open && } ; }