import { type CommonReservedFields, type NoReservedFields, defaultGqlPermission, defaultPermission, } from "@tailor-platform/erp-kit/core"; import { db, type TailorAnyDBField, type TailorAnyDBType } from "@tailor-platform/sdk"; // Timecard lifecycle (ADR-014): OPEN -> SUBMITTED -> APPROVED -> LOCKED, with a // REOPENED-style side path back to OPEN from SUBMITTED, APPROVED, or LOCKED. // No timecard.lifecycle.generated.ts exists yet, so this enum is hardcoded to // match the exact strings used in the command docs' Process Flow sections // (OpenTimecard/SubmitTimecard/ApproveTimecard/LockTimecard/ReopenTimecard.md). const TIMECARD_STATUSES = ["OPEN", "SUBMITTED", "APPROVED", "LOCKED"] as const; const builtins = (params: { assignmentType?: TailorAnyDBType; userType?: TailorAnyDBType }) => { // Cross-module FK: relation only when the foreign type is injected; plain uuid at codegen const assignmentId = db .uuid() .description( "Foreign key to the workforce Assignment whose period this Timecard aggregates and signs off", ); const approvedBy = db .uuid({ optional: true }) .description( "Foreign key to the user-management User who approved this Timecard; set only once APPROVED", ); const lockedBy = db .uuid({ optional: true }) .description( "Foreign key to the user-management User (labor/HR operator) who closed (period close) the period; set only once LOCKED", ); return { // Cross-module FK (required): every Timecard references exactly one workforce Assignment assignmentId: params.assignmentType ? assignmentId.relation({ type: "n-1", toward: { type: params.assignmentType, as: "assignment" }, backward: "timecards", }) : assignmentId, periodStart: db .date() .description("Start date of the period this Timecard aggregates (on or before periodEnd)"), periodEnd: db.date().description("End date of the period this Timecard aggregates"), status: db .enum(TIMECARD_STATUSES) .description("Lifecycle status: OPEN, SUBMITTED, APPROVED, or LOCKED"), // Per-category minute totals rolled up from CalculatedTimeBlocks in the period. Keyed by the // strategy-defined category key (open string), NOT fixed JP columns — so any jurisdiction's // categories (the JP strategy's REGULAR/OVERTIME/NIGHT/HOLIDAY, or an app's own) are surfaced // without a schema change. categoryTotals: db .object( { category: db.string().description("Strategy-defined span-category key"), minutes: db.int().description("Total minutes for this category in the period"), }, { array: true }, ) .description( "Per-category minute totals rolled up from CalculatedTimeBlocks (category = strategy-defined key)", ), submittedAt: db .datetime({ optional: true }) .description("Timestamp the Timecard was submitted; present once SUBMITTED or later"), approvedAt: db .datetime({ optional: true }) .description("Timestamp the Timecard was approved; present once APPROVED or later"), // Cross-module FK (optional): set only once approved; never the owner of the Assignment (self-approval blocked) approvedBy: params.userType ? approvedBy.relation({ type: "n-1", toward: { type: params.userType, as: "approvedByUser" }, backward: "approvedTimecards", }) : approvedBy, lockedAt: db .datetime({ optional: true }) .description("Timestamp the period was closed (period close); present once LOCKED"), // Cross-module FK (optional): the labor/HR operator who closed the period; set only once LOCKED lockedBy: params.userType ? lockedBy.relation({ type: "n-1", toward: { type: params.userType, as: "lockedByUser" }, backward: "lockedTimecards", }) : lockedBy, historicalCorrection: db .bool() .description( "True if and only if at least one change (e.g. a TimeCorrectionLog entry) was applied after the Timecard first reached LOCKED", ), }; }; type ReservedFields = keyof ReturnType | CommonReservedFields; export interface CreateTimecardTypeParams> { fields?: NoReservedFields; assignmentType?: TailorAnyDBType; userType?: TailorAnyDBType; } export function createTimecardType>( params: CreateTimecardTypeParams, ) { return db .table("Timecard", { ...builtins(params), ...((params.fields ?? {}) as F), ...db.fields.timestamps(), }) .permission(defaultPermission) .gqlPermission(defaultGqlPermission); } // Codegen instance with no cross-module types; real FKs (assignmentId -> workforce // Assignment, approvedBy -> user-management User) injected via module.ts export const timecard = createTimecardType({});