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 PublishedShiftOrderByField = "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 ListPublishedShiftsInput extends PaginationInput { startDate: Date; endDate: Date; siteId?: string; } /** * Function: listPublishedShifts * Description: Returns published shifts for a date range and/or site, * paginated — the shiftSchedule view of the committed schedule once shifts have * been published, including both assigned and open shifts. */ export async function run(db: ReadonlyDB, input: ListPublishedShiftsInput) { if (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}`)); } // "Published" is now a property of the shiftSchedule the shift hangs off, not of the shift itself. let query = db .selectFrom("Shift") .innerJoin("ShiftSchedule", "ShiftSchedule.id", "Shift.shiftScheduleId") .selectAll("Shift") .where("ShiftSchedule.status", "=", "CONFIRMED") .where("Shift.cancelledAt", "is", null) .where("Shift.date", ">=", input.startDate) .where("Shift.date", "<=", input.endDate); if (input.siteId !== undefined) { query = query.where("Shift.siteId", "=", input.siteId); } // A shift-table view reads by date, so `date` stays the default rather than `id`; the `id` // tiebreaker makes the order total so pagination cannot repeat or skip same-date rows. const shifts = await query .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)); }