import { ok, type ReadonlyDB } from "@tailor-platform/erp-kit/core"; import type { DB } from "../generated/kysely-tailordb"; import { classifyPlacementVariance, computePlannedMinutes, type ShiftSegmentInput, } from "../lib/_shiftVariance"; export interface ShiftVarianceActualBlock { startAt: Date; endAt: Date; minutes: number; } export interface ShiftVarianceActuals { assignmentId: string; blocks: ShiftVarianceActualBlock[]; } export interface ShiftVariancePlacement { shiftPlacementId: string; assignmentId: string; } export interface GetShiftVarianceInput { /** The Shift's planned envelope, read once by the caller from shiftSchedule's own Shift row. */ plannedStartAt: Date; plannedEndAt: Date; /** The Shift's segments, for the planned-minutes definition. */ segments: ShiftSegmentInput[]; /** The placements on the Shift (from ShiftPlacement), read once by the caller. */ placements: ShiftVariancePlacement[]; /** * Actual CalculatedTimeBlocks per placed Assignment. CalculatedTimeBlock is owned by * time-tracking; the app resolver composes that cross-module read and passes the actuals in. * A placement with no entry here is reported PENDING. */ actuals: ShiftVarianceActuals[]; } /** * Function: getShiftVariance * Description: Computes the planned-vs-actual variance report for a Shift from the planned facts, * placements, and actuals the caller has already read. This is a pure computation — it performs * no database reads (the caller owns the Shift / ShiftPlacement reads and the cross-module * CalculatedTimeBlock read, so nothing is re-queried here). It exists as a module query so the * scheduling domain's planned-minutes definition and variance-classification thresholds * (computePlannedMinutes / classifyPlacementVariance) are owned and unit-tested in the module * rather than embedded in the app resolver (issue #38). */ // oxlint-disable-next-line typescript/require-await -- pure computation; async only to match the query handler signature export async function run(_db: ReadonlyDB, input: GetShiftVarianceInput) { const plannedMinutes = computePlannedMinutes(input.segments); const actualsByAssignment = new Map( input.actuals.map((entry) => [entry.assignmentId, entry.blocks]), ); const placements = input.placements.map((placement) => { const blocks = actualsByAssignment.get(placement.assignmentId) ?? []; const { variance, actualMinutes, actualStartAt, actualEndAt } = classifyPlacementVariance({ plannedMinutes, plannedStartAt: input.plannedStartAt, plannedEndAt: input.plannedEndAt, blocks, }); return { shiftPlacementId: placement.shiftPlacementId, assignmentId: placement.assignmentId, variance, plannedMinutes, actualMinutes, actualStartAt, actualEndAt, }; }); return ok({ plannedStartAt: input.plannedStartAt, plannedEndAt: input.plannedEndAt, placements, }); }