import { ALL_WIZARD_STATUSES, type WizardWOStatus, } from "@/domain/work-orders/types/work-order-wizard"; import type { WorkOrderTablePatch, WorkOrderTableRow, } from "@/domain/work-orders/types/work-order-table-row"; import { PENDING_UPLIFT_GAP, UPLIFT_PENDING_COMPLETED_TITLE, } from "@/domain/work-orders/utils/wo-closability"; /** AAP `ALL_STATUSES` — Canceled is kebab-only. */ export const BOARD_INLINE_STATUSES: readonly WizardWOStatus[] = ALL_WIZARD_STATUSES; /** AAP `CREATION_STATUSES` — wizard Step 3 and inline create. */ export const CREATION_INLINE_STATUSES: readonly WizardWOStatus[] = [ "Incomplete", "Pending", "Scheduled", ]; export const SCHEDULED_NEEDS_DATE_MSG = "Set a Schedule On date before marking this work order as Scheduled."; export const PAST_DUE_BADGE_TOOLTIP = "This WO missed its service window. Reschedule to unlock status updates."; export const PAST_DUE_POPOVER_BODY = "This work order is past due. A new date must be scheduled before the status can be updated."; export const PAST_DUE_RESCHEDULE_CTA = "Reschedule →"; export const COMPLETED_DOC_BLOCK_MSG = "Generate or mark the completion document as not required before completing this work order"; export const COMPLETED_MEDIA_BLOCK_MSG = "Upload at least one completion photo before completing this work order."; const COMPLETE_ACTION = "completing this work order"; const EMPTY_FIELD_TOKENS = new Set(["", "-", "—", "N/A", "n/a"]); const PRE_SCHEDULE_STATUSES = new Set(["Incomplete", "Pending", ""]); const FIELD_GAP_TO_AAP_LABEL: [string, string][] = [ ["Dispatcher", "Assigned To"], ["Company", "Vendor"], ["Completed Date", "Completed Date"], ["Service", "Service"], ]; export type ScheduleDateInput = { scheduledOn?: string; scheduleWeekOnly?: boolean; schedMode?: string; }; export type StatusOptionState = { disabled: boolean; title?: string; }; export function isLifecycleFieldEmpty(value?: string): boolean { if (value == null) { return true; } const trimmed = value.trim(); return EMPTY_FIELD_TOKENS.has(trimmed); } /** AAP `hasScheduleDate`: concrete day/range only — week-only does not count. */ export function hasConcreteScheduleDate(row: ScheduleDateInput): boolean { if (row.scheduleWeekOnly === true || row.schedMode === "week") { return false; } return !isLifecycleFieldEmpty(row.scheduledOn); } export function isPreScheduleStatus(status: string): boolean { return PRE_SCHEDULE_STATUSES.has(status); } export function formatAapCompletedBlockTitle(gaps: string[]): string | undefined { if (gaps.length === 0) { return undefined; } const fields = FIELD_GAP_TO_AAP_LABEL.filter(([gap]) => gaps.includes(gap)).map( ([, label]) => label, ); if (fields.length === 1 && fields[0] === "Service") { return `Select a service before ${COMPLETE_ACTION}.`; } if (fields.length > 0) { return `Fill in ${fields.join(", ")} before ${COMPLETE_ACTION}.`; } if (gaps.includes("WO number")) { return `Assign a real APM number (not SH) before ${COMPLETE_ACTION}.`; } if (gaps.includes(PENDING_UPLIFT_GAP)) { return UPLIFT_PENDING_COMPLETED_TITLE; } if (gaps.includes("Completion document")) { return COMPLETED_DOC_BLOCK_MSG; } if (gaps.includes("At least 1 photo")) { return COMPLETED_MEDIA_BLOCK_MSG; } return undefined; } export function getStatusOptionState( row: ScheduleDateInput, status: WizardWOStatus, closabilityGaps: string[], ): StatusOptionState { if (status === "Scheduled" && !hasConcreteScheduleDate(row)) { return { disabled: true, title: SCHEDULED_NEEDS_DATE_MSG }; } if (status === "Completed") { const title = formatAapCompletedBlockTitle(closabilityGaps); if (title) { return { disabled: true, title }; } } return { disabled: false }; } /** AAP `maybeAutoSchedule` — no dispatcher requirement. */ export function maybeAutoSchedulePatch( row: ScheduleDateInput & { status?: string }, ): WorkOrderTablePatch { const hasDate = hasConcreteScheduleDate(row); if (hasDate && isPreScheduleStatus(row.status ?? "")) { return { status: "Scheduled" }; } if (!hasDate && row.status === "Scheduled") { return { status: "Pending" }; } return {}; } function patchTouchesScheduleOrStatus(patch: WorkOrderTablePatch): boolean { return ( patch.status !== undefined || patch.scheduledOn !== undefined || patch.scheduleWeekOnly !== undefined || patch.targetWeek !== undefined ); } export function mergeAutoSchedulePatch( row: WorkOrderTableRow | undefined, patch: WorkOrderTablePatch, ): WorkOrderTablePatch { if (!row || !patchTouchesScheduleOrStatus(patch)) { return patch; } const merged = { status: patch.status ?? row.status, scheduledOn: patch.scheduledOn !== undefined ? patch.scheduledOn : row.scheduledOn, scheduleWeekOnly: patch.scheduleWeekOnly !== undefined ? patch.scheduleWeekOnly : row.scheduleWeekOnly, }; return { ...patch, ...maybeAutoSchedulePatch(merged) }; } /** Apply schedule fields and the AAP auto-status for that next schedule. */ export function withAutoScheduleStatus( current: T, schedulePatch: Partial & ScheduleDateInput, ): Partial { const next = { status: schedulePatch.status ?? current.status ?? "", scheduledOn: schedulePatch.scheduledOn !== undefined ? schedulePatch.scheduledOn : current.scheduledOn, scheduleWeekOnly: schedulePatch.scheduleWeekOnly !== undefined ? schedulePatch.scheduleWeekOnly : current.scheduleWeekOnly, }; return { ...schedulePatch, ...maybeAutoSchedulePatch(next) }; } /** AAP create: apply maybeAutoSchedule to the draft before POST. */ export function resolveCreationStatus(row: ScheduleDateInput & { status: string }): WizardWOStatus { const autoStatus = maybeAutoSchedulePatch(row).status; if (autoStatus === "Scheduled" || autoStatus === "Pending") { return autoStatus; } return (row.status || "Incomplete") as WizardWOStatus; }