- 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>
49 lines
3 KiB
TypeScript
49 lines
3 KiB
TypeScript
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<T>(read:()=>Promise<T>):Promise<T|null> {
|
|
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<ClockEmployee[]> {
|
|
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<ClockEvent>(pb.filter('employee = {:employee}',{employee}),{sort:'-scanned_at,-id',requestKey:null})),
|
|
event:id=>optional(()=>pb.collection('clock_events').getOne<ClockEvent>(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<ClockEvent>(event),
|
|
};
|
|
}
|
|
export async function clockRoster() {
|
|
const pb=await clockPB();
|
|
const [employees,events]=await Promise.all([clockEmployees(pb),pb.collection('clock_events').getFullList<ClockEvent>({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<ReturnType<typeof clockRoster>>;
|