shoc-frontend-new/src/domain/vendors/mappers/vendor-roster-mapper.ts
Alexandre Brandizzi ca7829036f
Some checks failed
CI / ci (push) Has been cancelled
CI / governance (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
feat(vendors): add company roster workflows (SH-198) (#64)
* feat(vendors): support company roster workflows

* fix(vendors): address roster review feedback

* test(vendors): preserve draft on roster load failure

* fix(vendors): preserve current roster selection
2026-08-04 09:53:48 -03:00

156 lines
6.3 KiB
TypeScript

import type {
VendorCompanyRoster,
VendorPreferredContact,
VendorRosterBlockedWorkOrder,
VendorRosterConflict,
VendorRosterConflictKind,
VendorRosterTechnician,
} from "@/domain/vendors/types/vendor";
import { toCanonicalPhone } from "@/domain/vendors/mappers/vendor-mapper";
const PREFERRED_CONTACT_VALUES = new Set<VendorPreferredContact>(["Phone", "Email", "Text"]);
function asRecord(raw: unknown): Record<string, unknown> {
return typeof raw === "object" && raw !== null ? (raw as Record<string, unknown>) : {};
}
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 readBool(record: Record<string, unknown>, ...keys: string[]): boolean {
for (const key of keys) {
const value = record[key];
if (typeof value === "boolean") return value;
}
return true;
}
function readPreferredContact(record: Record<string, unknown>): VendorPreferredContact | undefined {
const raw = record.preferredContact ?? record.PreferredContact;
if (typeof raw === "string") {
const normalized = raw.trim();
if (PREFERRED_CONTACT_VALUES.has(normalized as VendorPreferredContact)) {
return normalized as VendorPreferredContact;
}
const lowered = normalized.toLowerCase();
if (lowered === "phone" || lowered === "call") return "Phone";
if (lowered === "email" || lowered === "e-mail") return "Email";
if (lowered === "text" || lowered === "sms") return "Text";
}
return undefined;
}
function readId(record: Record<string, unknown>, ...keys: string[]): string | number | undefined {
for (const key of keys) {
const value = record[key];
if (typeof value === "string" && value.trim() !== "") return value;
if (typeof value === "number" && Number.isFinite(value)) return value;
}
return undefined;
}
function readCompanyId(record: Record<string, unknown>): string | number | null {
const value = readId(record, "companyId", "CompanyId");
return value ?? null;
}
export function mapRosterTechnician(raw: unknown): VendorRosterTechnician {
const item = asRecord(raw);
return {
id: readId(item, "id", "Id"),
contactName: readString(item, "contactName", "ContactName"),
phone: readString(item, "phone", "Phone"),
email: readString(item, "email", "Email"),
preferredContact: readPreferredContact(item),
tradeSpecialties: readString(item, "tradeSpecialties", "TradeSpecialties"),
isActive: readBool(item, "isActive", "IsActive"),
totalJobs: Number(item.totalJobs ?? item.TotalJobs ?? 0),
};
}
export function mapRosterTechnicians(raw: unknown): VendorRosterTechnician[] {
return Array.isArray(raw) ? raw.map(mapRosterTechnician) : [];
}
export function mapVendorCompanyRoster(raw: unknown): VendorCompanyRoster {
const item = asRecord(raw);
return {
companyId: readCompanyId(item),
rowVersion: readString(item, "rowVersion", "RowVersion"),
name: readString(item, "name", "Name", "companyName", "CompanyName"),
companyPhone: readString(item, "companyPhone", "CompanyPhone"),
email: readString(item, "email", "Email"),
address: readString(item, "address", "Address"),
city: readString(item, "city", "City"),
state: readString(item, "state", "State"),
zip: readString(item, "zip", "Zip", "zipCode", "ZipCode"),
googleMapsUrl: readString(item, "googleMapsUrl", "GoogleMapsUrl"),
notes: readString(item, "notes", "Notes"),
technicians: mapRosterTechnicians(item.technicians ?? item.Technicians),
};
}
export function mapRosterTechnicianToBackend(technician: unknown): Record<string, unknown> {
const item = asRecord(technician);
const payload: Record<string, unknown> = {
contactName: readString(item, "contactName", "ContactName"),
phone: toCanonicalPhone(item.phone ?? item.Phone),
email: readString(item, "email", "Email"),
tradeSpecialties: readString(item, "tradeSpecialties", "TradeSpecialties"),
isActive: readBool(item, "isActive", "IsActive"),
};
const preferredContact = readPreferredContact(item);
if (preferredContact) payload.preferredContact = preferredContact;
const id = readId(item, "id", "Id");
if (id !== undefined) payload.id = id;
return payload;
}
export function mapVendorRosterToBackend(values: unknown): Record<string, unknown> {
const item = asRecord(values);
const techniciansRaw = item.technicians ?? item.Technicians;
const payload: Record<string, unknown> = {
name: readString(item, "name", "Name", "companyName", "CompanyName"),
companyPhone: toCanonicalPhone(item.companyPhone ?? item.CompanyPhone),
email: readString(item, "email", "Email"),
address: readString(item, "address", "Address"),
city: readString(item, "city", "City"),
state: readString(item, "state", "State"),
zip: readString(item, "zip", "Zip", "zipCode", "ZipCode"),
googleMapsUrl: readString(item, "googleMapsUrl", "GoogleMapsUrl"),
notes: readString(item, "notes", "Notes"),
technicians: Array.isArray(techniciansRaw)
? techniciansRaw.map(mapRosterTechnicianToBackend)
: [],
};
const rowVersion = readString(item, "rowVersion", "RowVersion");
if (rowVersion) payload.rowVersion = rowVersion;
return payload;
}
function mapBlockedWorkOrder(raw: unknown): VendorRosterBlockedWorkOrder {
const item = asRecord(raw);
return {
workOrderId: readId(item, "workOrderId", "WorkOrderId", "id", "Id") ?? "",
workOrderNumber: readString(item, "workOrderNumber", "WorkOrderNumber"),
workOrderTitle: readString(item, "workOrderTitle", "WorkOrderTitle", "title", "Title"),
status: readString(item, "status", "Status"),
scheduledDate: readString(item, "scheduledDate", "ScheduledDate") || undefined,
};
}
export function mapRosterConflict(raw: unknown, fallbackMessage: string): VendorRosterConflict {
const item = asRecord(raw);
const blockedRaw = item.blockedWorkOrders ?? item.BlockedWorkOrders ?? item.openWorkOrders;
const blockedWorkOrders = Array.isArray(blockedRaw) ? blockedRaw.map(mapBlockedWorkOrder) : [];
const kind: VendorRosterConflictKind =
blockedWorkOrders.length > 0 ? "open-work-orders" : "stale";
const message = readString(item, "message", "Message") || fallbackMessage;
return { kind, message, blockedWorkOrders };
}