import 'server-only'; import type PocketBase from 'pocketbase'; import { type ClockEmployee, type ClockEvent, type ClockStore } from './clock'; import { acquireEmployeeLock } from './clock-lock'; import { serverAuth } from './pb'; async function optional(read:()=>Promise):Promise { try{return await read();}catch(e){if((e as {status?:number}).status===404)return null;throw e;} } export async function clockPB() { const pb=await serverAuth(); if(!pb.authStore.isSuperuser)throw new Error('Clock database authentication unavailable.'); pb.beforeSend=(url,options)=>({url,options:{...options,signal:AbortSignal.timeout(15000)}}); return pb; } export async function clockEmployees(pb:PocketBase):Promise { const [operators,mechanics]=await Promise.all([ pb.collection('operators').getFullList({sort:'name',fields:'id,name,employee_code,asset,expand.asset.code',expand:'asset'}), pb.collection('mechanics').getFullList({sort:'name',fields:'id,name,employee_code,role'}), ]); return [...operators.map(e=>({id:`operators:${e.id}`,name:String(e.name||''),employeeNo:String(e.employee_code||''),role:'Operator',asset:String(e.expand?.asset?.code||'')})), ...mechanics.map(e=>({id:`mechanics:${e.id}`,name:String(e.name||''),employeeNo:String(e.employee_code||''),role:String(e.role||'Mechanic'),asset:''}))] .sort((a,b)=>a.name.localeCompare(b.name)); } export function clockStore(pb:PocketBase):ClockStore { return { employees:()=>clockEmployees(pb), latest:employee=>optional(()=>pb.collection('clock_events').getFirstListItem(pb.filter('employee = {:employee}',{employee}),{sort:'-scanned_at,-id',requestKey:null})), event:id=>optional(()=>pb.collection('clock_events').getOne(id,{requestKey:null})), lock:employee=>acquireEmployeeLock({ create:employee=>pb.collection('clock_scan_locks').create({employee}), find:employee=>optional(()=>pb.collection('clock_scan_locks').getFirstListItem<{id:string;created:string}>(pb.filter('employee = {:employee}',{employee}),{requestKey:null})), remove:id=>pb.collection('clock_scan_locks').delete(id), },employee), unlock:async id=>{ try{await pb.collection('clock_scan_locks').delete(id);} catch(e){if((e as {status:number}).status!==404)console.error('Clock lock release failed; lease will expire.',id);} }, create:event=>pb.collection('clock_events').create(event), }; } export async function clockRoster() { const pb=await clockPB(); const [employees,events]=await Promise.all([clockEmployees(pb),pb.collection('clock_events').getFullList({sort:'-scanned_at,-id'})]); // Actor IDs, GPS and request signatures are server audit details, not roster data. return {employees,events:events.map(({id,employee,employee_name,employee_no,site,direction,scanned_at,session_minutes,has_location})=> ({id,employee,employee_name,employee_no,site,direction,scanned_at,session_minutes,has_location})),asOf:new Date().toISOString()}; } export type ClockRoster = Awaited>;