import { buildPaginatedResult, DEFAULT_PAGE_SIZE, err, ok, type PaginationInput, type ReadonlyDB, } from "@tailor-platform/erp-kit/core"; import type { DB } from "../generated/kysely-tailordb"; import { InvalidDateRangeError, InvalidPaginationError } from "../lib/errors.generated"; type OpenShiftOrderByField = "date" | "plannedStartAt" | "createdAt"; // The listing joins ShiftSchedule, so every sort column has to be qualified; callers still pass // the bare field name. const ORDER_BY_COLUMNS = { id: "Shift.id", date: "Shift.date", plannedStartAt: "Shift.plannedStartAt", createdAt: "Shift.createdAt", } as const; export interface ListOpenShiftsInput extends PaginationInput { startDate?: Date; endDate?: Date; siteId?: string; } /** * Function: listOpenShifts * Description: Returns live slots in a CONFIRMED shiftSchedule that no ACTIVE placement staffs — the * "unfilled demand slots" view driving open-shift advertising and shiftSchedule gap-filling. Open-ness * is an anti-join against ShiftPlacement and nothing else: ShiftPlacement is the single * authoritative record of who staffs a slot, so a released or superseded placement leaves the * slot open again, and a filled one takes it off this list. */ export async function run(db: ReadonlyDB, input: ListOpenShiftsInput) { if ( input.startDate !== undefined && input.endDate !== undefined && input.endDate < input.startDate ) { return err( new InvalidDateRangeError(`${input.startDate.toISOString()}..${input.endDate.toISOString()}`), ); } const limit = input.limit ?? DEFAULT_PAGE_SIZE; const offset = input.offset ?? 0; if (!Number.isInteger(limit) || limit < 1) { return err(new InvalidPaginationError(`limit=${input.limit}`)); } if (!Number.isInteger(offset) || offset < 0) { return err(new InvalidPaginationError(`offset=${input.offset}`)); } // Only a slot that is committed to workers (its shiftSchedule is CONFIRMED), still live (not // withdrawn), and carries no ACTIVE placement is genuinely awaiting fill. let query = db .selectFrom("Shift") .innerJoin("ShiftSchedule", "ShiftSchedule.id", "Shift.shiftScheduleId") .selectAll("Shift") .where("ShiftSchedule.status", "=", "CONFIRMED") .where("Shift.cancelledAt", "is", null) .where((eb) => eb.not( eb.exists( eb .selectFrom("ShiftPlacement") .select("ShiftPlacement.id") .whereRef("ShiftPlacement.shiftId", "=", "Shift.id") .where("ShiftPlacement.status", "=", "ACTIVE"), ), ), ); if (input.startDate !== undefined) { query = query.where("Shift.date", ">=", input.startDate); } if (input.endDate !== undefined) { query = query.where("Shift.date", "<=", input.endDate); } if (input.siteId !== undefined) { query = query.where("Shift.siteId", "=", input.siteId); } const shifts = await query // Open shifts are worked through chronologically, so `date` stays the default rather than // `id`; the `id` tiebreaker makes the order total so a page boundary inside one date cannot // repeat or skip a slot. .orderBy(ORDER_BY_COLUMNS[input.orderBy ?? "date"], input.orderDirection ?? "asc") .orderBy("Shift.id", "asc") .limit(limit + 1) .offset(offset) .execute(); return ok(buildPaginatedResult(shifts, limit)); }