import type { Transaction } from "../generated/kysely-tailordb"; import type { Schema, TimeClockEventVoid } from "./types"; export type TimeClockEventVoidRow = TimeClockEventVoid; /** * The effective void state of a TimeClockEvent is decided by the highest-sequence * TimeClockEventVoid row for that target (ADR-023): VOID means currently voided, * UNVOID (or no record) means active. Returns the latest record, or undefined. */ export async function latestVoidRecord( db: Transaction, targetEventId: string, ): Promise { return db .selectFrom("TimeClockEventVoid") .selectAll() .where("targetEventId", "=", targetEventId) .orderBy("sequence", "desc") .limit(1) .executeTakeFirst(); } /** True when the latest void record for the target is a VOID (i.e. currently voided). */ export function isCurrentlyVoided(latest: TimeClockEventVoidRow | undefined): boolean { return latest?.action === "VOID"; } /** * Returns the subset of the given event ids that are currently voided (latest * record is VOID). Used by readers/derivation that must skip retracted punches. */ export async function selectVoidedEventIds( db: Transaction, eventIds: string[], ): Promise> { if (eventIds.length === 0) return new Set(); const rows = await db .selectFrom("TimeClockEventVoid") .select(["targetEventId", "action", "sequence"]) .where("targetEventId", "in", eventIds) .execute(); const latest = new Map(); for (const row of rows) { const current = latest.get(row.targetEventId); if (!current || row.sequence > current.sequence) { latest.set(row.targetEventId, { sequence: row.sequence, action: row.action }); } } const voided = new Set(); for (const [id, state] of latest) { if (state.action === "VOID") voided.add(id); } return voided; }