mega-plant/lib/clock-rules.ts
David df53130861 Add Workforce pages, Plant Clock tab bar and oil & diesel book-out
- 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>
2026-10-22 11:39:18 +02:00

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;
}