shoc-frontend-new/src/domain/work-orders/mappers/work-order-table-row-mapper.ts
Arthur Bassi 8a221c8819 fix(work-orders): consume board pendingUpliftCount for SH-188 gate
Map the authoritative board/search/detail pendingUpliftCount into the
Completed closability gate and align the pending-uplift tooltip with SH-188.
2026-08-12 09:55:34 -03:00

337 lines
11 KiB
TypeScript

import { mapApiStatusToWizard } from "@/domain/work-orders/mappers/wizard-draft-mapper";
import {
DOC_STATUS_API,
LIFECYCLE_STATUS_API_TO_FE,
WORK_ORDER_TYPE_API_TO_FE,
} from "@/domain/work-orders/types/work-order-board";
import type {
CompletionDocStatus,
WorkOrderTableRow,
WorkOrderUpliftSummary,
} from "@/domain/work-orders/types/work-order-table-row";
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", "Add-On", "Overdue"]);
function readWoType(record: Record<string, unknown>): WOType | "" {
const numeric =
readNumber(record, "workOrderType", "WorkOrderType") ?? readNumber(record, "type", "Type");
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 (WO_TYPES_SET.has(raw)) {
return raw as WOType;
}
if (raw === "AddOn") {
return "Add-On";
}
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;
}
function readUpliftSummary(item: Record<string, unknown>): 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(source, "primaryStatus", "PrimaryStatus", "status", "Status");
if (primaryStatus) {
summary.primaryStatus = primaryStatus;
}
return summary;
}
function readHasPendingUplift(
item: Record<string, unknown>,
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;
}
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 upliftSummary = readUpliftSummary(item);
const hasPendingUplift = readHasPendingUplift(item, upliftSummary);
const row: WorkOrderTableRow = {
id: readId(item),
woNumber,
site: site || location.slice(0, 8),
type: readWoType(item),
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"),
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"),
dueDate: toDateInputValue(readString(item, "dueDate", "DueDate")),
rescheduleCount: readNumber(item, "rescheduleCount", "RescheduleCount") ?? 0,
carriedOver: readNumber(item, "carriedOver", "CarriedOver") ?? 0,
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);
}