import { readAdditionalContacts } from "@/domain/work-orders/mappers/additional-contacts-mapper"; import { mapApiStatusToWizard } from "@/domain/work-orders/mappers/wizard-draft-mapper"; import { mapUpliftSummary } from "@/domain/work-orders/mappers/work-order-uplift-mapper"; import { DOC_STATUS_API, LIFECYCLE_STATUS_API_TO_FE, WORK_ORDER_TYPE_API, WORK_ORDER_TYPE_API_TO_FE, } from "@/domain/work-orders/types/work-order-board"; import type { CompletionDocStatus, WorkOrderFrozenCompany, WorkOrderFrozenPoc, WorkOrderFrozenSite, WorkOrderTableRow, WorkOrderUpliftSummary, } from "@/domain/work-orders/types/work-order-table-row"; import { parseWorkOrderSeverity } from "@/domain/work-orders/types/work-order-severity"; import type { WOType } from "@/domain/work-orders/types/work-order-wizard"; import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils"; import { formatApptRange } from "@/domain/work-orders/utils/wizard-date-utils"; import { toDateInputValue } from "@/lib/time-utils"; function asRecord(raw: unknown): Record { return typeof raw === "object" && raw !== null ? (raw as Record) : {}; } function readId(record: Record): string | number { const value = record.id ?? record.Id; if (typeof value === "string" || typeof value === "number") { return value; } return ""; } function readString(record: Record, ...keys: string[]): string { for (const key of keys) { const value = record[key]; if (typeof value === "string") { return value; } if (typeof value === "number") { return String(value); } } return ""; } function readNumber(record: Record, ...keys: string[]): number | null { for (const key of keys) { const value = record[key]; if (typeof value === "number") { return value; } if (typeof value === "string" && value !== "") { const parsed = Number(value); if (!Number.isNaN(parsed)) { return parsed; } } } return null; } function readBool(record: Record, ...keys: string[]): boolean { for (const key of keys) { const value = record[key]; if (typeof value === "boolean") { return value; } } return false; } function readStringArray(record: Record, ...keys: string[]): string[] { for (const key of keys) { const value = record[key]; if (Array.isArray(value)) { return value .filter((item): item is string => typeof item === "string") .map((item) => item.trim()) .filter(Boolean); } if (typeof value === "string" && value.trim()) { try { const parsed: unknown = JSON.parse(value); if (Array.isArray(parsed)) { return parsed .filter((item): item is string => typeof item === "string") .map((item) => item.trim()) .filter(Boolean); } } catch { // ignore invalid JSON } } } return []; } function readRowVersion(record: Record, ...keys: string[]): string { for (const key of keys) { const value = record[key]; if (typeof value === "string" && value.length > 0) { return value; } } return ""; } const WO_TYPES_SET = new Set(["PM", "Reactive", "Emergency", "Overdue"]); function isLegacyAddOnType(record: Record): boolean { const numeric = readNumber(record, "workOrderType", "WorkOrderType") ?? readNumber(record, "type", "Type"); if (numeric === WORK_ORDER_TYPE_API.AddOn) { return true; } const raw = readString( record, "workOrderType", "WorkOrderType", "type", "Type", "woType", "WoType", "source", "Source", ); return raw === "AddOn" || raw === "Add-On"; } function readIsAddOn(record: Record): boolean { for (const key of ["isAddOn", "IsAddOn"] as const) { if (!(key in record)) continue; const value = record[key]; // Explicit boolean is authoritative; null/undefined fall through to legacy. if (typeof value === "boolean") { return value; } if (value == null) { break; } return Boolean(value); } return isLegacyAddOnType(record); } function readWoType(record: Record): WOType | "" { const numeric = readNumber(record, "workOrderType", "WorkOrderType") ?? readNumber(record, "type", "Type"); if (numeric === WORK_ORDER_TYPE_API.AddOn) { return ""; } if (numeric != null && WORK_ORDER_TYPE_API_TO_FE[numeric]) { return WORK_ORDER_TYPE_API_TO_FE[numeric] as WOType; } const raw = readString( record, "workOrderType", "WorkOrderType", "type", "Type", "woType", "WoType", "source", "Source", ); if (raw === "AddOn" || raw === "Add-On") { return ""; } if (WO_TYPES_SET.has(raw)) { return raw as WOType; } return ""; } function readDocStatus(record: Record): CompletionDocStatus { const numeric = readNumber(record, "docStatus", "DocStatus"); if (numeric === DOC_STATUS_API.Yes) return "Yes"; if (numeric === DOC_STATUS_API.No) return "No"; if (numeric === DOC_STATUS_API.NotNeeded) return "NN"; const raw = readString(record, "docStatus", "DocStatus", "completionDoc", "CompletionDoc"); if (raw === "Yes" || raw === "No" || raw === "NN") { return raw; } if (raw === "NotNeeded") { return "NN"; } return ""; } function readBoardStatus(record: Record): ReturnType { const label = readString(record, "lifecycleStatusLabel", "LifecycleStatusLabel"); if (label) { return mapApiStatusToWizard(label); } const lifecycle = readNumber(record, "lifecycleStatus", "LifecycleStatus"); if (lifecycle != null && LIFECYCLE_STATUS_API_TO_FE[lifecycle]) { return mapApiStatusToWizard(LIFECYCLE_STATUS_API_TO_FE[lifecycle]); } const legacy = readString(record, "legacyStatus", "LegacyStatus", "status", "Status"); return mapApiStatusToWizard(legacy); } function readFlagColor(record: Record): string | null { for (const key of ["flagColor", "FlagColor"]) { const value = record[key]; if (value == null) { continue; } if (typeof value === "string") { const trimmed = value.trim(); return trimmed || null; } } return null; } function readUpliftSummary(item: Record): WorkOrderUpliftSummary | undefined { const nested = asRecord(item.upliftSummary ?? item.UpliftSummary); const hasNested = Object.keys(nested).length > 0; const source = hasNested ? nested : item; // Board/search/detail contract (SH-188): pendingUpliftCount is authoritative when present. const boardCount = readNumber(item, "pendingUpliftCount", "PendingUpliftCount"); const nestedCount = readNumber(source, "pendingCount", "PendingCount"); const pendingCount = nestedCount ?? boardCount ?? 0; const hasUplift = readBool(source, "hasUplift", "HasUplift") || readBool(item, "hasUplift", "HasUplift") || pendingCount > 0; // No nested summary and no board count → omit (legacy payloads without the contract field). if (!hasNested && boardCount == null && !hasUplift) { return undefined; } const summary: WorkOrderUpliftSummary = { hasUplift: hasUplift || pendingCount > 0, pendingCount, }; const primaryStatus = readString(nested, "primaryStatus", "PrimaryStatus") || readString(item, "primaryStatus", "PrimaryStatus"); if (primaryStatus) { summary.primaryStatus = primaryStatus; } return summary; } function readHasPendingUplift( item: Record, upliftSummary: WorkOrderUpliftSummary | undefined, ): boolean | undefined { if ("hasPendingUplift" in item || "HasPendingUplift" in item) { return Boolean(item.hasPendingUplift ?? item.HasPendingUplift); } const boardCount = readNumber(item, "pendingUpliftCount", "PendingUpliftCount"); if (boardCount != null) { return boardCount > 0; } if (upliftSummary != null) { return upliftSummary.pendingCount > 0; } return undefined; } function readPrimaryDispatchStatus(item: Record): string { return readString(item, "primaryDispatchStatus", "PrimaryDispatchStatus"); } function readFrozenSite(item: Record): WorkOrderFrozenSite | undefined { const source = asRecord(item.frozenSite ?? item.FrozenSite); if (Object.keys(source).length === 0) return undefined; return { code: readString(source, "code", "Code"), label: readString(source, "label", "Label"), address: readString(source, "address", "Address"), city: readString(source, "city", "City"), state: readString(source, "state", "State"), zip: readString(source, "zip", "Zip"), phone: readString(source, "phone", "Phone"), email: readString(source, "email", "Email"), maps: readString(source, "maps", "Maps"), }; } function readFrozenCompany(item: Record): WorkOrderFrozenCompany | undefined { const source = asRecord(item.frozenCompany ?? item.FrozenCompany); if (Object.keys(source).length === 0) return undefined; return { name: readString(source, "name", "Name"), contactName: readString(source, "contactName", "ContactName"), address: readString(source, "address", "Address"), city: readString(source, "city", "City"), state: readString(source, "state", "State"), zip: readString(source, "zip", "Zip"), phone: readString(source, "phone", "Phone"), email: readString(source, "email", "Email"), maps: readString(source, "maps", "Maps"), }; } function readFrozenPoc(item: Record): WorkOrderFrozenPoc | undefined { const source = asRecord(item.frozenPoc ?? item.FrozenPoc); if (Object.keys(source).length === 0) return undefined; const rawContacts = source.contacts ?? source.Contacts; const contacts = Array.isArray(rawContacts) ? rawContacts.map(asRecord).map((contact) => ({ name: readString(contact, "name", "Name"), phone: readString(contact, "phone", "Phone"), notes: readString(contact, "notes", "Notes"), })) : []; return { contacts, notes: readString(source, "notes", "Notes"), }; } export function mapWorkOrderTableRow(raw: unknown): WorkOrderTableRow { const item = asRecord(raw); const scheduledOn = toDateInputValue( readString(item, "scheduledOn", "scheduledDate", "ScheduledDate", "ScheduledOn"), ); // FE day groups use MM/DD keys (week skeleton); ignore BE weekday labels like "monday". const { dayGroup, dayLabel } = deriveDayFields(scheduledOn); const woNumber = readString( item, "woNumber", "internalWONumber", "InternalWONumber", "number", "workOrderNumber", "WorkerOrderNumber", ); const site = readString( item, "siteCode", "SiteCode", "site", "Site", "locationCode", "LocationCode", ); const location = readString(item, "location", "Location", "locationName", "LocationName"); const dispatcherId = readString( item, "dispatcherId", "DispatcherId", "assigneeId", "AssigneeId", "employeeId", "EmployeeId", ); const dispatcherName = readString( item, "dispatcherName", "DispatcherName", "assignedTo", "AssignedTo", "employeeName", "EmployeeName", "assignee", "Assignee", ); const apptStart = readString(item, "apptStart", "ApptStart", "appointmentStart"); const apptEnd = readString(item, "apptEnd", "ApptEnd", "appointmentEnd"); const apptTime = readString(item, "apptTime", "ApptTime") || (apptStart ? formatApptRange(apptStart, apptEnd) : ""); const locationIdRaw = item.locationId ?? item.LocationId; const vendorIdNum = readNumber(item, "vendorId", "VendorId"); const primaryDispatchId = readNumber(item, "primaryDispatchId", "PrimaryDispatchId"); const boardUpliftSummary = readUpliftSummary(item); const hasPendingUplift = readHasPendingUplift(item, boardUpliftSummary); const upliftSummary = boardUpliftSummary == null ? undefined : mapUpliftSummary(item); const row: WorkOrderTableRow = { id: readId(item), woNumber, site: site || location.slice(0, 8), type: readWoType(item), severity: parseWorkOrderSeverity(item.severity ?? item.Severity), dispatcherId, dispatcherName, scheduledOn, completedDate: toDateInputValue( readString(item, "completedDate", "CompletedDate", "completedOn", "CompletedOn"), ), targetWeek: toDateInputValue(readString(item, "targetWeek", "TargetWeek")), dayGroup, dayLabel, location, locationId: typeof locationIdRaw === "string" || typeof locationIdRaw === "number" ? locationIdRaw : "", pm: readString(item, "pm", "PM", "problem", "Problem", "service", "Service", "trade", "Trade"), serviceNotes: readString(item, "serviceNotes", "ServiceNotes", "pmNote", "PmNote"), extraServices: readStringArray(item, "extraServices", "ExtraServices"), vendorId: vendorIdNum != null ? String(vendorIdNum) : "", company: readString(item, "company", "Company", "vendorName", "VendorName"), primaryDispatchStatus: readPrimaryDispatchStatus(item), tech: readString(item, "techName", "TechName", "tech", "Tech", "technician", "Technician"), techPhone: readString(item, "techPhone", "TechPhone"), apptStart, apptEnd, apptTime, status: readBoardStatus(item), docStatus: readDocStatus(item), pocName: readString(item, "pocName", "PocName", "POCName"), pocPhone: readString(item, "pocPhone", "PocPhone", "POCPhone"), pocNotes: readString(item, "pocNotes", "PocNotes", "POCNotes"), additionalContacts: readAdditionalContacts(item), frozenSite: readFrozenSite(item), frozenCompany: readFrozenCompany(item), frozenPoc: readFrozenPoc(item), dueDate: toDateInputValue(readString(item, "dueDate", "DueDate")), rescheduleCount: readNumber(item, "rescheduleCount", "RescheduleCount") ?? 0, carriedOver: readNumber(item, "carriedOver", "CarriedOver") ?? 0, isAddOn: readIsAddOn(item), avetaRequired: readBool(item, "avetaRequired", "AvetaRequired"), hasAvetaDocument: readBool(item, "hasAvetaDocument", "HasAvetaDocument"), originalWeek: readString(item, "originalWeek", "OriginalWeek"), originalDate: toDateInputValue(readString(item, "originalDate", "OriginalDate")), isPastDue: readBool(item, "isPastDue", "IsPastDue"), scheduleWeekOnly: readBool(item, "scheduleWeekOnly", "ScheduleWeekOnly"), mediaCount: readNumber(item, "mediaCount", "MediaCount") ?? 0, title: readString(item, "title", "Title", "workerOrderTitle", "WorkerOrderTitle"), priority: readString(item, "priority", "Priority"), primaryDispatchId, rowVersion: readRowVersion(item, "rowVersion", "RowVersion"), dispatchRowVersion: readRowVersion(item, "dispatchRowVersion", "DispatchRowVersion"), flagColor: readFlagColor(item), upliftSummary, hasPendingUplift, }; return row; } export function mapWorkOrderTableRows(rawItems: unknown[]): WorkOrderTableRow[] { return rawItems.map(mapWorkOrderTableRow); }