import { type CommonReservedFields, type NoReservedFields, defaultGqlPermission, defaultPermission, } from "@tailor-platform/erp-kit/core"; import { db, type TailorAnyDBField, type TailorAnyDBType } from "@tailor-platform/sdk"; import { pipeline } from "./pipeline"; import { pipelineItemLifecycle } from "./pipelineItem.lifecycle.generated"; import { pipelineStage } from "./pipelineStage"; export const ITEM_PRIORITIES = ["LOW", "MEDIUM", "HIGH", "URGENT"] as const; export type ItemPriority = (typeof ITEM_PRIORITIES)[number]; export function isItemPriority(value: string | undefined): value is ItemPriority { return value !== undefined && ITEM_PRIORITIES.some((p) => p === value); } const builtins = (params: { userType?: TailorAnyDBType }) => { const assigneeId = db .uuid({ optional: true }) .description("Foreign key to User assigned to the item"); return { pipelineId: db .uuid() .relation({ type: "n-1", toward: { type: pipeline, as: "pipeline" }, backward: "items", }) .description("ID of the pipeline this item belongs to"), stageId: db .uuid() .relation({ type: "n-1", toward: { type: pipelineStage, as: "stage" }, backward: "items", }) .description("Current stage ID (changed via drag & drop)"), title: db.string().description("PipelineItem title"), description: db .string({ optional: true }) .description("Detailed description or notes for the item"), assigneeId: params.userType ? assigneeId.relation({ type: "n-1", toward: { type: params.userType, as: "assignee" }, backward: "assignedPipelineItems", }) : assigneeId, priority: db.enum(ITEM_PRIORITIES).description("Priority. LOW / MEDIUM / HIGH / URGENT"), dueDate: db.date({ optional: true }).description("Due date"), // Reorder and list queries both filter by stageId and order by position; // when items per stage grow large, add a composite (stageId, position) // index at the platform level to keep the renumbering writes cheap. position: db.int().description("Display order within the stage (top to bottom, 1-based)"), itemNumber: db .int() .description( "Sequential number within the pipeline. Auto-assigned like a GitHub issue number.", ), lifecycle: db .enum(pipelineItemLifecycle.states) .description( "PipelineItem lifecycle. DRAFT = visible only to the creator and hidden from operational lists, OPEN = published and in progress, CLOSED = explicitly closed. Independent of the PipelineStage category (DONE/CANCELED); transitioning to CLOSED only happens via an explicit ClosePipelineItem call. Approval workflows are not carried on the PipelineItem; the owning domain holds them as needed.", ), }; }; type ReservedFields = keyof ReturnType | CommonReservedFields; export interface CreatePipelineItemTypeParams> { fields?: NoReservedFields; /** Related User type (user-management module). The instance wired by the consuming app is injected here. */ userType?: TailorAnyDBType; } export function createPipelineItemType>( params: CreatePipelineItemTypeParams, ) { return db .table("PipelineItem", { ...builtins(params), // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- mirrors erp-kit's createXxxType factories; field bag is constrained but TS can't prove F is concrete ...((params.fields ?? {}) as F), ...db.fields.timestamps(), }) .description( "Generic work item. A unit of work placed on a PipelineStage within a Pipeline; a general-purpose progress-tracking entity that domain modules can link to via foreign key.", ) .permission(defaultPermission) .gqlPermission(defaultGqlPermission); } export const pipelineItem = createPipelineItemType({});