diff --git a/app/(clock)/clock/stock/page.tsx b/app/(clock)/clock/stock/page.tsx index e6aaf0a..16de27a 100644 --- a/app/(clock)/clock/stock/page.tsx +++ b/app/(clock)/clock/stock/page.tsx @@ -7,14 +7,19 @@ export const metadata = { title: 'Book out oil & diesel · Mega Plant' }; export default async function StockPage() { await requireClockUser(); const pb = await clockPB(); - const [items, assets, moves] = await Promise.all([ + const [items, assets, moves, operators, mechanics] = await Promise.all([ pb.collection('oil_items').getFullList({ sort: 'code' }), pb.collection('assets').getFullList({ sort: 'code', filter: 'is_active = true' }), pb.collection('oil_stock_movements').getFullList({ expand: 'item,asset' }), + pb.collection('operators').getFullList({ sort: 'name', fields: 'name,employee_code' }), + pb.collection('mechanics').getFullList({ sort: 'name', fields: 'name,employee_code' }), ]); + const employees = [...operators, ...mechanics].filter(e => String(e.employee_code || '').trim()) + .map(e => ({ name: String(e.name), code: String(e.employee_code) })).sort((a, b) => a.name.localeCompare(b.name)); const onHand = (id: string) => moves.filter(m => m.item === id).reduce((s, m) => s + (m.kind === 'used' ? -(m.litres || 0) : m.litres || 0), 0); return String(i.code).startsWith('FUEL')).map(i => ({ id: i.id, code: i.code, name: i.name, colour: i.colour || '#30638E', onHand: Math.round(onHand(i.id) * 100) / 100, tank: String(i.code).startsWith('FUEL') }))} + employees={employees} assets={assets.map(a => ({ id: a.id, code: a.code, name: a.name, qr: a.qr_code || '' }))} />; } diff --git a/app/api/clock/stock-out/route.ts b/app/api/clock/stock-out/route.ts index f5872e6..01938c4 100644 --- a/app/api/clock/stock-out/route.ts +++ b/app/api/clock/stock-out/route.ts @@ -35,7 +35,15 @@ export async function POST(request: Request) { if (!asset) return Response.json({ error: 'That vehicle is not in the plant register.' }, { status: 404 }); if (!String(item.code).startsWith('FUEL')) return Response.json({ error: 'Oil is booked on the Job cards tab.' }, { status: 400 }); - const who = String(user.user.name || user.user.email || user.user.id); + // Optional: who is fuelling. Looked up by employee number, never trusted as free text. + const employeeCode = String(form.get('employee_code') || '').trim(); + let who = String(user.user.name || user.user.email || user.user.id); + if (employeeCode) { + const filter = pb.filter('employee_code = {:c}', { c: employeeCode }); + const person = (await pb.collection('operators').getFullList({ filter }))[0] ?? (await pb.collection('mechanics').getFullList({ filter }))[0]; + if (!person) return Response.json({ error: 'That employee is not on the register.' }, { status: 404 }); + who = `${person.name} (${employeeCode})`; + } const data = new FormData(); data.set('item', item.id); data.set('asset', asset.id); data.set('kind', 'used'); data.set('litres', String(litres)); data.set('occurred_on', new Date().toISOString()); // full instant: `created` is not auto-filled here diff --git a/app/globals.css b/app/globals.css index 610e279..f40d212 100644 --- a/app/globals.css +++ b/app/globals.css @@ -148,6 +148,7 @@ body, .dark body { transition: background-color 0.2s ease, color 0.2s ease; } .plant-clock-warning { padding: 12px 14px; border-radius: 10px; border: 1px solid #EDAE4980; background: #EDAE4915; font-size: 14px; line-height: 1.6; overflow-wrap: anywhere; } .plant-camera { position: relative; overflow: hidden; display: grid; place-items: center; border-radius: 12px; min-height: min(62svh, 520px); background: #003D5B; color: white; } .plant-clock-banner { display: flex; align-items: center; gap: 12px; padding: 14px 16px; border-radius: 12px; box-shadow: 0 8px 24px rgb(0 0 0 / .25); } +.plant-camera-compact { min-height: min(34svh, 300px); } .plant-scan-frame { display: grid; place-items: center; width: 154px; height: 140px; border: 2px solid #EDAE49; border-radius: 16px; color: #EDAE49; } .plant-clock-status { display: inline-flex; align-items: center; border-radius: 7px; background: var(--color-bg); color: var(--color-text-muted); padding: 5px 9px; font-size: 12px; font-weight: 600; white-space: nowrap; } .plant-clock-status-in { background: #00798C15; color: var(--color-accent-ink); } diff --git a/components/BadgeCamera.tsx b/components/BadgeCamera.tsx index 547bd04..7dc95fc 100644 --- a/components/BadgeCamera.tsx +++ b/components/BadgeCamera.tsx @@ -4,7 +4,7 @@ import jsQR from 'jsqr'; import { Camera, ScanLine } from 'lucide-react'; /** Starts on mount: the clock IS the scanner, so there is nothing to open first. */ -export function BadgeCamera({onDecode,paused,overlay,hint='Hold the badge QR inside the frame'}:{onDecode:(code:string)=>void;paused:boolean;overlay?:ReactNode;hint?:string}) { +export function BadgeCamera({onDecode,paused,overlay,hint='Hold the badge QR inside the frame',compact=false}:{onDecode:(code:string)=>void;paused:boolean;overlay?:ReactNode;hint?:string;compact?:boolean}) { const video=useRef(null); const callback=useRef(onDecode);callback.current=onDecode; const pause=useRef(paused);pause.current=paused; @@ -45,7 +45,7 @@ export function BadgeCamera({onDecode,paused,overlay,hint='Hold the badge QR ins return ()=>{stopped=true;setLive(false);clearTimeout(timer);stream?.getTracks().forEach(track=>track.stop());}; },[enabled]); return
-
+
{enabled &&