mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-05 20:02:05 +00:00
Keep the slide-over uplift tab and the evidence upload while taking the current board, dashboard, and vendor changes from main.
208 lines
8.4 KiB
TypeScript
208 lines
8.4 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"),
|
|
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<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"),
|
|
// why: 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)
|
|
: [],
|
|
};
|
|
// why: 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<string, string>;
|
|
}
|
|
|
|
export function mapVendorRosterAdditivePatchToBackend(
|
|
patch: VendorRosterAdditivePatch,
|
|
): Record<string, unknown> {
|
|
const item = asRecord(patch);
|
|
const payload: Record<string, unknown> = {
|
|
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<string, unknown> = { ...patch.companyFields };
|
|
if (companyFields.companyPhone !== undefined)
|
|
companyFields.companyPhone = toCanonicalPhone(companyFields.companyPhone);
|
|
// why: 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 };
|
|
}
|