- Workforce: Employees register, per-employee dashboards, Attendance table - Plant Clock: scan-only clock, bottom tab bar (Employees / Oil & Diesel / Job cards) - Oil & Diesel stock: diesel tank (650 L, opening 589 L), level overwrite dialog, QR on each card, book-out screen with searchable vehicle picker, filterable "Booked out" table (today/week/month/all/range) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
23 lines
1.3 KiB
TypeScript
23 lines
1.3 KiB
TypeScript
import type { ClockEvent } from './clock';
|
|
|
|
export const workDate = (instant:string) => {
|
|
const date = new Date(instant);
|
|
if (!Number.isFinite(date.getTime())) throw new Error('Invalid scan time');
|
|
return new Intl.DateTimeFormat('en-CA',{timeZone:'Africa/Johannesburg',year:'numeric',month:'2-digit',day:'2-digit'}).format(date);
|
|
};
|
|
/** Same-day IN closes with OUT; anything else (first scan, new day, after an OUT) is IN. */
|
|
export function clockAction(last:ClockEvent|null, now:string):'in'|'out' {
|
|
return last?.direction==='in' && workDate(last.scanned_at)===workDate(now) ? 'out' : 'in';
|
|
}
|
|
|
|
/** An IN never closed on the same day is still missing its physical OUT. */
|
|
export function missedClockIns(events:ClockEvent[], now:string):ClockEvent[] {
|
|
const open=new Map<string,ClockEvent>();const missed:ClockEvent[]=[];
|
|
for (const event of [...events].sort((a,b)=>Date.parse(a.scanned_at)-Date.parse(b.scanned_at)||a.id.localeCompare(b.id))) {
|
|
const previous=open.get(event.employee);
|
|
if (event.direction==='in') { if(previous)missed.push(previous);open.set(event.employee,event); }
|
|
else if(previous && workDate(previous.scanned_at)===workDate(event.scanned_at)) open.delete(event.employee);
|
|
}
|
|
for(const event of open.values())if(workDate(event.scanned_at)<workDate(now))missed.push(event);
|
|
return missed;
|
|
}
|