2026-10-21 04:39:15 +00:00
|
|
|
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);
|
|
|
|
|
};
|
2026-10-22 09:39:18 +00:00
|
|
|
/** 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';
|
2026-10-21 04:39:15 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-22 09:39:18 +00:00
|
|
|
/** An IN never closed on the same day is still missing its physical OUT. */
|
2026-10-21 04:39:15 +00:00
|
|
|
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); }
|
2026-10-22 09:39:18 +00:00
|
|
|
else if(previous && workDate(previous.scanned_at)===workDate(event.scanned_at)) open.delete(event.employee);
|
2026-10-21 04:39:15 +00:00
|
|
|
}
|
|
|
|
|
for(const event of open.values())if(workDate(event.scanned_at)<workDate(now))missed.push(event);
|
|
|
|
|
return missed;
|
|
|
|
|
}
|