'use client'; import { useEffect, useRef, useState } from 'react'; import { AlertTriangle, ArrowRight, CheckCircle2, LogIn, LogOut, RotateCcw } from 'lucide-react'; import { clockTime, duration } from '@/lib/clock-display'; import { clockRequestId } from '@/lib/clock-request'; import { BadgeCamera } from './BadgeCamera'; // Plant staff clock in and out of the Plant itself, not a site: scan = saved. // The server decides IN or OUT, so there is nothing to choose or confirm here. type Saved={id:string;employeeName:string;employeeNo:string;direction:'in'|'out';at:string;sessionMinutes:number}; type Banner= | {kind:'saved';scan:Saved} | {kind:'repeat';employeeName:string;direction:'in'|'out';at:string} | {kind:'error';message:string;retry:{employee_no:string;request_id:string}|null}; /** How long a result stays on screen before the scanner is ready-looking again. */ const BANNER_MS=5000; /** The camera reads a held badge many times a second; ignore the same QR this long after any outcome. */ const SAME_BADGE_MS=6000; export function PlantClock() { const [code,setCode]=useState(''); const [busy,setBusy]=useState(false); const [banner,setBanner]=useState(null); const [now,setNow]=useState(''); const gate=useRef(false); const lastBadge=useRef({value:'',until:0}); const bannerTimer=useRef>(undefined); useEffect(()=>{const update=()=>setNow(clockTime(new Date().toISOString()));update();const t=setInterval(update,1000);return()=>clearInterval(t);},[]); useEffect(()=>()=>clearTimeout(bannerTimer.current),[]); function show(next:Banner) { clearTimeout(bannerTimer.current);setBanner(next); // A failed save that can be retried stays until someone acts on it. if(!(next.kind==='error' && next.retry))bannerTimer.current=setTimeout(()=>setBanner(null),next.kind==='error'?BANNER_MS*2:BANNER_MS); } async function send(payload:{employee_no:string;request_id:string}) { gate.current=true;setBusy(true); try { const response=await fetch('/api/clock/scan',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}); const data=await response.json().catch(()=>({})); if(response.ok) { const scan=data as Saved; show({kind:'saved',scan});setCode(''); navigator.vibrate?.(120); } else if(data.repeat) { show({kind:'repeat',...data.repeat});setCode(''); } else { // 5xx and 423 (busy) are worth retrying with the SAME request id; anything else is a real answer. const retryable=response.status>=500||response.status===423; show({kind:'error',message:data.error||'Could not save the scan.',retry:retryable?payload:null}); navigator.vibrate?.([80,60,80]); } } catch { show({kind:'error',message:'Could not reach the server. Nothing was saved yet.',retry:payload}); } finally { gate.current=false;setBusy(false); lastBadge.current={value:payload.employee_no.toUpperCase(),until:Date.now()+SAME_BADGE_MS}; } } function scan(raw:string) { const badge=raw.trim(); if(!badge || gate.current)return; if(lastBadge.current.value===badge.toUpperCase() && Date.now()

MEGA PLANT

Scan to clock in or out

{now||'--:--'}

{busy && !banner &&

Saving…

} {banner?.kind==='saved' &&
{banner.scan.direction==='in'?:}

{banner.scan.employeeName}

Clocked {banner.scan.direction.toUpperCase()} at {clockTime(banner.scan.at)}{banner.scan.direction==='out'?` · ${duration(banner.scan.sessionMinutes)} today`:''}

} {banner?.kind==='repeat' &&

{banner.employeeName}

Already clocked {banner.direction.toUpperCase()} at {clockTime(banner.at)}. Not saved again — scan in a minute to clock {banner.direction==='in'?'OUT':'IN'}.

} {banner?.kind==='error' &&

{banner.message}

{banner.retry && }
} }/>
{e.preventDefault();lastBadge.current={value:'',until:0};scan(code);}}>
setCode(e.target.value)} placeholder="e.g. MCW067" maxLength={100} autoComplete="off" autoCapitalize="characters"/>
; }