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(["Phone", "Email", "Text"]); function asRecord(raw: unknown): Record { return typeof raw === "object" && raw !== null ? (raw as Record) : {}; } 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 readBool(record: Record, ...keys: string[]): boolean { for (const key of keys) { const value = record[key]; if (typeof value === "boolean") return value; } return true; } function readPreferredContact(record: Record): 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, ...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 | 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"), areaId: readString(item, "areaId", "AreaId"), areaName: readString(item, "areaName", "AreaName"), technicians: mapRosterTechnicians(item.technicians ?? item.Technicians), }; } /** Form Area ids are strings; the API takes a positive integer id or null (Unassigned). */ export function toBackendAreaId(value: unknown): number | null { const parsed = Number(typeof value === "string" ? value.trim() : value); return Number.isInteger(parsed) && parsed > 0 ? parsed : null; } export function mapRosterTechnicianToBackend(technician: unknown): Record { const item = asRecord(technician); const payload: Record = { 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 { const item = asRecord(values); const techniciansRaw = item.technicians ?? item.Technicians; const payload: Record = { 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"), // The stored Maps URL is deprecated: reads keep it for legacy records, but // roster writes must not carry it back to the backend. notes: readString(item, "notes", "Notes"), technicians: Array.isArray(techniciansRaw) ? techniciansRaw.map(mapRosterTechnicianToBackend) : [], }; // The form always round-trips its Area (id or null = Unassigned). Callers // without an Area value omit it, which the API treats as "leave unchanged". if ("areaId" in item || "AreaId" in item) { payload.areaId = toBackendAreaId(item.areaId ?? item.AreaId); } const rowVersion = readString(item, "rowVersion", "RowVersion"); if (rowVersion) payload.rowVersion = rowVersion; return payload; } export interface VendorRosterAdditivePatch { rowVersion: string; addTechnicians: unknown[]; companyFields?: Record; } export function mapVendorRosterAdditivePatchToBackend( patch: VendorRosterAdditivePatch, ): Record { const item = asRecord(patch); const payload: Record = { rowVersion: readString(item, "rowVersion", "RowVersion"), addTechnicians: patch.addTechnicians.map((technician) => { const mapped = mapRosterTechnicianToBackend(technician); delete mapped.id; return mapped; }), }; if (patch.companyFields) { // SH-250: match the reconcile write path, which canonicalizes companyPhone. // Copying companyFields verbatim let additive patches send a different phone // shape than PUT for the same user input. const companyFields: Record = { ...patch.companyFields }; if (companyFields.companyPhone !== undefined) companyFields.companyPhone = toCanonicalPhone(companyFields.companyPhone); // An edited Area is sent as an id or an explicit null (Unassigned). if (companyFields.areaId !== undefined) companyFields.areaId = toBackendAreaId(companyFields.areaId); payload.companyFields = companyFields; } 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 code = readString(item, "code", "Code"); const kind: VendorRosterConflictKind = code === "duplicate_vendor_company_name" ? "duplicate" : blockedWorkOrders.length > 0 ? "open-work-orders" : "stale"; const message = readString(item, "message", "Message") || fallbackMessage; return { kind, message, blockedWorkOrders }; }