import { type CommonReservedFields, type NoReservedFields, defaultGqlPermission, defaultPermission, } from "@tailor-platform/erp-kit/core"; import { db, type TailorAnyDBField, type TailorAnyDBType } from "@tailor-platform/sdk"; import { shiftPattern } from "./shiftPattern"; import { shiftSchedule } from "./shiftSchedule"; // Normalized shift classification (ADR-006): 通常/中抜け/通し/宿直/応援, tied to segment count validation const SHIFT_TYPES = ["NORMAL", "SPLIT", "THROUGH", "ON_CALL", "SUPPORT"] as const; const builtins = (params: { siteType?: TailorAnyDBType }) => { // Cross-module FK, optional: multi-site / 応援 slots carry the Site they are staffed at const siteId = db .uuid({ optional: true }) .description("Foreign key to the organization Site this shift is staffed at, if site-bound"); return { // Intra-module FK, required: a Shift always belongs to exactly one shiftSchedule period. The Shift // carries no lifecycle of its own — whether it is committed to workers is its ShiftSchedule's status. shiftScheduleId: db .uuid() .relation({ type: "n-1", toward: { type: shiftSchedule, as: "shiftSchedule" }, backward: "shifts", }) .description("Foreign key to the ShiftSchedule period this shift belongs to"), // The business date this slot belongs to; consistent with the first segment's start date: db .date() .description( "Business date this shift slot belongs to (used with the time-tracking holiday calendar)", ), // Normalized classification, consistent with segment count (SPLIT requires 2+ segments, etc.) shiftType: db .enum(SHIFT_TYPES) .description("Normalized shift classification: NORMAL / SPLIT / THROUGH / ON_CALL / SUPPORT"), // Withdrawal of a single slot, which the shift-schedule-level status cannot express ("the period is // confirmed but this one night shift is no longer needed"). Recorded rather than deleted, for // the same reason a ShiftPlacement is superseded rather than removed. cancelledAt: db .datetime({ optional: true }) .description("Timestamp this slot was withdrawn; null means the slot is live"), // Span envelope, derived from the earliest/latest embedded segment, not independently supplied plannedStartAt: db .datetime() .description( "Envelope start: earliest embedded segment start (derived, not independently supplied)", ), plannedEndAt: db .datetime() .description( "Envelope end: latest embedded segment end (derived, not independently supplied); may cross midnight", ), // Ordered embedded planned work intervals (ADR-022: formerly the independent // ShiftSegment entity). A 通常/通し/宿直 shift is one element; a 中抜け (SPLIT) // shift is two or more separated by a real, unpaid gap. Envelope derives from these. segments: db .object( { // 1-based order within the shift, contiguous and consistent with time order sequence: db.int().description("1-based ordering of this segment within the Shift"), // Datetimes (not minutes-from-midnight): a shift is date-instantiated and a segment may cross midnight plannedStartAt: db.datetime().description("Planned start of this segment"), plannedEndAt: db .datetime() .description( "Planned end of this segment; strictly after plannedStartAt, may cross midnight", ), // Intra-segment rest, distinct from the inter-segment 中抜け gap breakMinutes: db .int() .description( "Non-negative intra-segment break minutes, strictly less than the segment's gross span", ), }, { array: true }, ) .description("Ordered embedded planned work intervals (ADR-022); at least one"), // Intra-module FK, optional: only set when this shift was instantiated from a ShiftPattern shiftPatternId: db .uuid({ optional: true }) .relation({ type: "n-1", toward: { type: shiftPattern, as: "shiftPattern" }, backward: "shifts", }) .description("Foreign key to the ShiftPattern this shift was instantiated from, if any"), // Cross-module FK: relation only when the foreign type is injected; plain uuid at codegen siteId: params.siteType ? siteId.relation({ type: "n-1", toward: { type: params.siteType, as: "site" }, backward: "shifts", }) : siteId, }; }; type ReservedFields = keyof ReturnType | CommonReservedFields; export interface CreateShiftTypeParams> { fields?: NoReservedFields; siteType?: TailorAnyDBType; } export function createShiftType< const F extends Record = Record, >(params: CreateShiftTypeParams) { return db .table("Shift", { ...builtins(params), // Custom fields spread + timestamps ...((params.fields ?? {}) as F), ...db.fields.timestamps(), }) .permission(defaultPermission) .gqlPermission(defaultGqlPermission); } // Codegen instance with no cross-module types; real FKs injected via module.ts export const shift = createShiftType({});