shoc-frontend-new/src/domain/work-orders/mappers/work-order-table-row-mapper.ts
arthur.bassi 0c2ef6ac5e merge origin/dev into feature/wo-multi-poc-contacts
Use the extracted inline Schedule On cell from dev and keep additional-contacts fixtures complete.
2026-08-18 13:19:45 -03:00

328 lines
10 KiB
TypeScript

import { readAdditionalContacts } from "@/domain/work-orders/mappers/additional-contacts-mapper";
import { mapApiStatusToWizard } from "@/domain/work-orders/mappers/wizard-draft-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,
WorkOrderTableRow,
} 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<string, unknown> {
return typeof raw === "object" && raw !== null ? (raw as Record<string, unknown>) : {};
}
function readId(record: Record<string, unknown>): string | number {
const value = record.id ?? record.Id;
if (typeof value === "string" || typeof value === "number") {
return value;
}
return "";
}
function readString(record: Record<string, unknown>, ...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<string, unknown>, ...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<string, unknown>, ...keys: string[]): boolean {
for (const key of keys) {
const value = record[key];
if (typeof value === "boolean") {
return value;
}
}
return false;
}
function readStringArray(record: Record<string, unknown>, ...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<string, unknown>, ...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<string, unknown>): 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<string, unknown>): 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<string, unknown>): 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<string, unknown>): 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<string, unknown>): ReturnType<typeof mapApiStatusToWizard> {
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, unknown>): 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;
}
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 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")),
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"),
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),
dueDate: toDateInputValue(readString(item, "dueDate", "DueDate")),
rescheduleCount: readNumber(item, "rescheduleCount", "RescheduleCount") ?? 0,
carriedOver: readNumber(item, "carriedOver", "CarriedOver") ?? 0,
isAddOn: readIsAddOn(item),
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),
};
return row;
}
export function mapWorkOrderTableRows(rawItems: unknown[]): WorkOrderTableRow[] {
return rawItems.map(mapWorkOrderTableRow);
}