From 116f65ca4803d7bacda5f0bf09561955f37e34d4 Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Thu, 10 Sep 2026 15:41:01 -0300 Subject: [PATCH] feat(work-orders): consume completion freeze snapshot on the board Prefer FrozenSite, FrozenCompany, and FrozenPoc on Completed rows. --- .../comp-doc/use-comp-doc-dialog.tsx | 13 ++- .../tabs/slide-over-info-tab-display-view.tsx | 30 +++++-- .../list/table/cells/site-cell.tsx | 10 ++- .../list/table/cells/site-dialog.tsx | 4 + .../list/table/cells/use-site-dialog-state.ts | 36 ++++++-- .../list/table/cells/vendor-cell.tsx | 4 + .../list/table/cells/vendor-dialog-body.tsx | 9 +- .../cells/vendor-dialog-company-card.tsx | 39 ++++++-- .../list/table/cells/vendor-dialog.tsx | 4 + .../list/table/wo-table-row-cells.tsx | 23 +++-- .../list/table/wo-table-row-service-cells.tsx | 10 ++- .../_components/list/table/wo-table.tsx | 5 +- .../_hooks/create-comp-doc-dialog-actions.ts | 4 +- .../workorders/_hooks/use-comp-doc-dialog.ts | 27 ++++-- .../mappers/work-order-table-row-mapper.ts | 54 ++++++++++++ .../work-orders/types/work-order-board.ts | 10 ++- .../work-orders/types/work-order-table-row.ts | 32 +++++++ .../work-orders/utils/completion-doc-site.ts | 14 ++- .../utils/effective-completion-snapshot.ts | 55 ++++++++++++ .../use-comp-doc-dialog-address-seed.test.tsx | 41 +++++++++ .../workorders/use-site-dialog-state.test.tsx | 51 +++++++++-- .../vendor-dialog-frozen-company.test.tsx | 51 +++++++++++ .../work-order-table-row-mapper.test.ts | 26 ++++++ .../utils/completion-doc-site.test.ts | 55 ++++++++++++ .../effective-completion-snapshot.test.ts | 88 +++++++++++++++++++ 25 files changed, 643 insertions(+), 52 deletions(-) create mode 100644 src/domain/work-orders/utils/effective-completion-snapshot.ts create mode 100644 src/test/app/(protected)/workorders/vendor-dialog-frozen-company.test.tsx create mode 100644 src/test/domain/work-orders/utils/completion-doc-site.test.ts create mode 100644 src/test/domain/work-orders/utils/effective-completion-snapshot.test.ts diff --git a/src/app/(protected)/workorders/_components/comp-doc/use-comp-doc-dialog.tsx b/src/app/(protected)/workorders/_components/comp-doc/use-comp-doc-dialog.tsx index 365cf856..b6ff6147 100644 --- a/src/app/(protected)/workorders/_components/comp-doc/use-comp-doc-dialog.tsx +++ b/src/app/(protected)/workorders/_components/comp-doc/use-comp-doc-dialog.tsx @@ -12,6 +12,11 @@ import { useWorkOrderBoardDetail } from "@/domain/work-orders/use-cases/use-work import { useUploadCompletionDoc } from "@/domain/work-orders/use-cases/use-work-order-mutations"; import { resolveCompDocSite } from "@/domain/work-orders/utils/completion-doc-site"; import { DOC_STATE_CFG } from "@/domain/work-orders/utils/completion-doc-state"; +import { + effectiveCompany, + effectivePoc, + effectiveSite, +} from "@/domain/work-orders/utils/effective-completion-snapshot"; import { isWorkOrderDocumentsReadOnly } from "@/domain/work-orders/utils/work-order-media-limits"; import { useCompDocDialogSeed } from "./use-comp-doc-dialog-seed"; import { useCompDocNotRequiredActions } from "./use-comp-doc-not-required-actions"; @@ -61,7 +66,9 @@ export function useCompDocDialog({ onDateCompletedChange, }: UseCompDocDialogArgs) { const locId = String(row.locationId || ""); - const { data: locationDetail } = useLocationDetail(locId || undefined); + const { data: locationDetail } = useLocationDetail( + effectiveSite(row) != null ? undefined : locId || undefined, + ); const { data: boardDetail } = useWorkOrderBoardDetail(row.id, open); const uploadCompletionMutation = useUploadCompletionDoc(row.id); const pdfInputRef = useRef(null); @@ -93,8 +100,8 @@ export function useCompDocDialog({ setEditDateCompleted, }); - const amazonRep = info.pocName || ""; - const seaRep = info.tech || ""; + const amazonRep = effectivePoc(info).contacts[0]?.name || ""; + const seaRep = effectiveCompany(info).contactName || ""; const cfg = DOC_STATE_CFG[docState]; const isNotRequired = docState === "not-required"; const isGenerated = docState === "generated" || Boolean(signOffAttachment); diff --git a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-display-view.tsx b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-display-view.tsx index 2f497d10..edc03f69 100644 --- a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-display-view.tsx +++ b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-display-view.tsx @@ -13,6 +13,10 @@ import type { import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; import type { WOType } from "@/domain/work-orders/types/work-order-wizard"; import { formatApptRange, parseApptTime } from "@/domain/work-orders/utils/wizard-date-utils"; +import { + effectiveCompany, + effectivePoc, +} from "@/domain/work-orders/utils/effective-completion-snapshot"; import { DocBadge } from "./slide-over-doc-badge"; import { SlideOverInfoTabAssignedField } from "./slide-over-info-tab-assigned-field"; import { SlideOverInfoTabStatusField } from "./slide-over-info-tab-status-field"; @@ -39,6 +43,9 @@ export function SlideOverInfoTabDisplayView({ }: SlideOverInfoTabDisplayViewProps) { const appt = parseApptTime(source.apptTime); const apptDisplay = appt.start ? formatApptRange(appt.start, appt.end) : source.apptTime || "—"; + const company = effectiveCompany(source); + const poc = effectivePoc(source); + const primaryPoc = poc.contacts[0]; return (
@@ -51,12 +58,16 @@ export function SlideOverInfoTabDisplayView({ value={source.site} location={source.location} locationId={source.locationId} - pocName={source.pocName} - pocPhone={source.pocPhone} - pocNotes={source.pocNotes} - additionalContacts={source.additionalContacts} + pocName={primaryPoc?.name ?? ""} + pocPhone={primaryPoc?.phone ?? ""} + pocNotes={poc.notes ?? ""} + additionalContacts={poc.contacts.slice(1).map((contact) => ({ + name: contact.name ?? "", + phone: contact.phone ?? "", + }))} sites={sites} viewOnly + frozenSite={source.frozenSite} onSave={() => undefined} /> ) : ( @@ -123,11 +134,12 @@ export function SlideOverInfoTabDisplayView({ {isCompleted ? ( undefined} /> ) : ( @@ -142,8 +154,8 @@ export function SlideOverInfoTabDisplayView({ - {source.pocName || "—"} - {source.pocPhone ? ` · ${source.pocPhone}` : ""} + {primaryPoc?.name || "—"} + {primaryPoc?.phone ? ` · ${primaryPoc.phone}` : ""}
diff --git a/src/app/(protected)/workorders/_components/list/table/cells/site-cell.tsx b/src/app/(protected)/workorders/_components/list/table/cells/site-cell.tsx index 4c40ae06..ccc16b35 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/site-cell.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/site-cell.tsx @@ -3,6 +3,7 @@ import { MapPin } from "lucide-react"; import { formatSitePlace } from "@/domain/locations/mappers/location-mapper"; import type { WorkOrderAdditionalContact } from "@/domain/work-orders/types/work-order-additional-contact"; import type { LocationOption } from "@/domain/work-orders/types/work-order"; +import type { WorkOrderFrozenSite } from "@/domain/work-orders/types/work-order-table-row"; import { HighlightText } from "./highlight-text"; import { HoverChevron } from "./hover-chevron"; import { SiteDialog, type SitePatch } from "./site-dialog"; @@ -22,6 +23,7 @@ type SiteCellProps = { /** Inline create: editable POC + Site/POC required to confirm the modal. */ createMode?: boolean; viewOnly?: boolean; + frozenSite?: WorkOrderFrozenSite; onSave: (patch: SitePatch) => void; }; @@ -39,10 +41,15 @@ export function SiteCell({ q, createMode = false, viewOnly = false, + frozenSite, onSave, }: SiteCellProps) { const [open, setOpen] = useState(false); - const place = formatSitePlace(city, state, location); + const place = formatSitePlace( + frozenSite?.city ?? city, + frozenSite?.state ?? state, + frozenSite?.label ?? location, + ); return ( <> @@ -102,6 +109,7 @@ export function SiteCell({ sites={sites} createMode={createMode} viewOnly={viewOnly} + frozenSite={frozenSite} onSave={onSave} /> diff --git a/src/app/(protected)/workorders/_components/list/table/cells/site-dialog.tsx b/src/app/(protected)/workorders/_components/list/table/cells/site-dialog.tsx index 0843e331..239c883e 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/site-dialog.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/site-dialog.tsx @@ -6,6 +6,7 @@ import { Text } from "@/components/ui/text"; import { dialogBlurBackdropSlotProps } from "@/components/ui/dialog-blur-backdrop"; import type { WorkOrderAdditionalContact } from "@/domain/work-orders/types/work-order-additional-contact"; import type { LocationOption } from "@/domain/work-orders/types/work-order"; +import type { WorkOrderFrozenSite } from "@/domain/work-orders/types/work-order-table-row"; import { FieldLabel } from "@/app/(protected)/workorders/_components/list/table/cells/dialog-field-label"; import { SiteDialogDetailPanel } from "@/app/(protected)/workorders/_components/list/table/cells/site-dialog-detail-panel"; import { SiteDialogFooter } from "@/app/(protected)/workorders/_components/list/table/cells/site-dialog-footer"; @@ -27,6 +28,7 @@ type SiteDialogProps = { sites: LocationOption[]; createMode?: boolean; viewOnly?: boolean; + frozenSite?: WorkOrderFrozenSite; onSave: (patch: SitePatch) => void; }; @@ -42,6 +44,7 @@ export function SiteDialog({ sites, createMode = false, viewOnly = false, + frozenSite, onSave, }: SiteDialogProps) { const s = useSiteDialogState({ @@ -55,6 +58,7 @@ export function SiteDialog({ additionalContacts, sites, createMode, + frozenSite, onSave, }); diff --git a/src/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-state.ts b/src/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-state.ts index c9d16c1e..5800cbf4 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-state.ts +++ b/src/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-state.ts @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; import { buildSiteDialogPatch } from "@/app/(protected)/workorders/_components/list/table/cells/build-site-dialog-patch"; import type { SitePatch } from "@/app/(protected)/workorders/_components/list/table/cells/site-dialog-types"; import { useSiteDialogFormFields } from "@/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-form-fields"; @@ -6,6 +6,7 @@ import { useLocationDetail } from "@/domain/locations/use-cases/use-location-det import { formatLocationAddressPreview } from "@/domain/locations/mappers/location-mapper"; import type { WorkOrderAdditionalContact } from "@/domain/work-orders/types/work-order-additional-contact"; import type { LocationOption } from "@/domain/work-orders/types/work-order"; +import type { WorkOrderFrozenSite } from "@/domain/work-orders/types/work-order-table-row"; export type { SitePatch }; @@ -20,6 +21,7 @@ type UseSiteDialogStateArgs = { additionalContacts: WorkOrderAdditionalContact[]; sites: LocationOption[]; createMode: boolean; + frozenSite?: WorkOrderFrozenSite; onSave: (patch: SitePatch) => void; }; @@ -34,6 +36,7 @@ export function useSiteDialogState({ additionalContacts = [], sites, createMode, + frozenSite, onSave, }: UseSiteDialogStateArgs) { const fields = useSiteDialogFormFields({ @@ -49,23 +52,42 @@ export function useSiteDialogState({ }); const { locId, pocFilledFor, pn, pp, setPn, setPp, setPocFilledFor, siteMissing } = fields; const { - data: locationDetail, - isLoading: locationDetailLoading, - isError: locationDetailError, - } = useLocationDetail(open && locId ? locId : undefined); + data: liveLocationDetail, + isLoading: liveLocationDetailLoading, + isError: liveLocationDetailError, + } = useLocationDetail(frozenSite == null && open && locId ? locId : undefined); + const frozenLocationDetail = useMemo( + () => + frozenSite == null + ? undefined + : { + name: frozenSite.label, + address: frozenSite.address, + city: frozenSite.city, + state: frozenSite.state, + zipCode: frozenSite.zip, + phone: frozenSite.phone, + contact: undefined, + contactEmail: frozenSite.email, + }, + [frozenSite], + ); + const locationDetail = frozenLocationDetail ?? liveLocationDetail; + const locationDetailLoading = frozenSite == null && liveLocationDetailLoading; + const locationDetailError = frozenSite == null && liveLocationDetailError; const addressPreview = locationDetail ? formatLocationAddressPreview(locationDetail) : ""; const pocMissing = !pn.trim() || !pp.trim(); const canConfirm = !siteMissing && (!createMode || !pocMissing); useEffect(() => { - if (!open || !locId || !locationDetail || pocFilledFor === locId) { + if (frozenSite != null || !open || !locId || !locationDetail || pocFilledFor === locId) { return; } setPn((prev) => (prev.trim() ? prev : (locationDetail.contact ?? ""))); setPp((prev) => (prev.trim() ? prev : (locationDetail.phone ?? ""))); setPocFilledFor(locId); - }, [open, locId, locationDetail, pocFilledFor, setPn, setPp, setPocFilledFor]); + }, [open, locId, locationDetail, pocFilledFor, setPn, setPp, setPocFilledFor, frozenSite]); const attemptSave = () => { if (createMode && !canConfirm) { diff --git a/src/app/(protected)/workorders/_components/list/table/cells/vendor-cell.tsx b/src/app/(protected)/workorders/_components/list/table/cells/vendor-cell.tsx index b34cdd49..1cfac800 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/vendor-cell.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/vendor-cell.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order"; +import type { WorkOrderFrozenCompany } from "@/domain/work-orders/types/work-order-table-row"; import { HighlightText } from "./highlight-text"; import { HoverChevron } from "./hover-chevron"; import { VendorDialog, type VendorPatch } from "./vendor-dialog"; @@ -12,6 +13,7 @@ type VendorCellProps = { vendors: VendorDropdownItem[]; q?: string; viewOnly?: boolean; + frozenCompany?: WorkOrderFrozenCompany; onSave: (patch: VendorPatch) => void; }; @@ -23,6 +25,7 @@ export function VendorCell({ vendors, q, viewOnly = false, + frozenCompany, onSave, }: VendorCellProps) { const [open, setOpen] = useState(false); @@ -63,6 +66,7 @@ export function VendorCell({ techPhone={techPhone} vendors={vendors} viewOnly={viewOnly} + frozenCompany={frozenCompany} onSave={onSave} /> diff --git a/src/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-body.tsx b/src/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-body.tsx index 928bfe51..4195702c 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-body.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-body.tsx @@ -14,6 +14,7 @@ import type { CompanyOption, TechnicianOption, } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-options"; +import type { WorkOrderFrozenCompany } from "@/domain/work-orders/types/work-order-table-row"; type VendorDialogBodyProps = { selectedCompany: string; @@ -27,6 +28,7 @@ type VendorDialogBodyProps = { notes: string; resolvedCompany: string; selectedVendor: VendorDropdownItem | undefined; + frozenCompany?: WorkOrderFrozenCompany; saveError: string | null; onCompanyPick: (company: string) => void; onPick: (key: string) => void; @@ -51,6 +53,7 @@ export function VendorDialogBody({ notes, resolvedCompany, selectedVendor, + frozenCompany, saveError, onCompanyPick, onPick, @@ -85,7 +88,11 @@ export function VendorDialogBody({ {Boolean(resolvedCompany) && (
Company info - +
)} diff --git a/src/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-company-card.tsx b/src/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-company-card.tsx index b14f8a4a..fc711dc9 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-company-card.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-company-card.tsx @@ -1,16 +1,39 @@ -import { ExternalLink, Mail, MapPin } from "lucide-react"; +import { ExternalLink, Mail, MapPin, Phone, User } from "lucide-react"; import { Detail } from "@/app/(protected)/workorders/_components/wizard/wizard-labels"; import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order"; +import type { WorkOrderFrozenCompany } from "@/domain/work-orders/types/work-order-table-row"; import { formatVendorDistance } from "@/domain/work-orders/utils/vendor-dropdown-label"; type VendorDialogCompanyCardProps = { company: string; vendor: VendorDropdownItem | undefined; + frozenCompany?: WorkOrderFrozenCompany; }; -export function VendorDialogCompanyCard({ company, vendor }: VendorDialogCompanyCardProps) { - const distance = formatVendorDistance(vendor?.distanceMiles); - const email = vendor?.email?.trim() || ""; +function formatFrozenCompanyAddress(company: WorkOrderFrozenCompany): string { + const region = [company.city, company.state] + .map((part) => part?.trim()) + .filter(Boolean) + .join(", "); + + return [company.address?.trim(), region, company.zip?.trim()].filter(Boolean).join(", "); +} + +export function VendorDialogCompanyCard({ + company, + vendor, + frozenCompany, +}: VendorDialogCompanyCardProps) { + const isFrozen = frozenCompany != null; + const distance = isFrozen ? "" : formatVendorDistance(vendor?.distanceMiles); + const contactName = isFrozen ? (frozenCompany.contactName?.trim() ?? "") : ""; + const email = isFrozen ? (frozenCompany.email?.trim() ?? "") : (vendor?.email?.trim() ?? ""); + const address = isFrozen ? formatFrozenCompanyAddress(frozenCompany) : (vendor?.address ?? ""); + const mapsUrl = + (isFrozen ? frozenCompany.maps : undefined) || + (!isFrozen && address + ? `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(address)}` + : ""); return (
)}
- {Boolean(vendor?.address) && } + {Boolean(address) && } + {Boolean(contactName) && } + {Boolean(frozenCompany?.phone) && } {Boolean(email) && } - {vendor != null && Boolean(vendor.address) && ( + {Boolean(mapsUrl) && ( void; }; @@ -31,6 +33,7 @@ export function VendorDialog({ techPhone = "", vendors, viewOnly = false, + frozenCompany, onSave, }: VendorDialogProps) { const form = useVendorDialogForm({ @@ -79,6 +82,7 @@ export function VendorDialog({ notes={form.notes} resolvedCompany={form.resolvedCompany} selectedVendor={form.selectedVendor} + frozenCompany={frozenCompany} saveError={form.saveError} onCompanyPick={form.handleCompanyPick} onPick={form.handlePick} diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx index 01fea44a..2cc959c6 100644 --- a/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx +++ b/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx @@ -3,6 +3,10 @@ import type { LocationOption, WorkOrderUser } from "@/domain/work-orders/types/w import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; import { applyAssignedToTableSave } from "@/domain/work-orders/utils/work-order-feedback-toasts"; import { isWorkOrderCompletionSurfaceReadOnly } from "@/domain/work-orders/utils/work-order-lock"; +import { + effectivePoc, + effectiveSite, +} from "@/domain/work-orders/utils/effective-completion-snapshot"; import { saveExistingRowType } from "./save-existing-row-type"; import { AssignedToCell } from "./cells/assigned-to-cell"; import { DateCell } from "./cells/date-cell"; @@ -45,6 +49,9 @@ export function WoTableRowCells({ }: WoTableRowCellsProps) { const hasSchedule = Boolean(row.scheduledOn) || Boolean(row.targetWeek) || Boolean(row.scheduleEnd); + const frozenSite = effectiveSite(row); + const poc = effectivePoc(row); + const primaryPoc = poc.contacts[0]; return ( <> @@ -54,18 +61,22 @@ export function WoTableRowCells({ onClick={(e) => e.stopPropagation()} > ({ + name: contact.name ?? "", + phone: contact.phone ?? "", + }))} sites={sites} q={search} viewOnly={viewOnly} + frozenSite={frozenSite} onSave={(p) => onPatchRow({ site: p.site, diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-row-service-cells.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-row-service-cells.tsx index bb6d8d6c..0326f870 100644 --- a/src/app/(protected)/workorders/_components/list/table/wo-table-row-service-cells.tsx +++ b/src/app/(protected)/workorders/_components/list/table/wo-table-row-service-cells.tsx @@ -11,6 +11,7 @@ import { VendorCell } from "./cells/vendor-cell"; import { EMPTY_UPLIFT_SUMMARY } from "@/domain/work-orders/types/work-order-uplift"; import { canOpenUpliftsDialog } from "@/domain/work-orders/utils/uplift-display-utils"; import { applyVendorTableSave } from "@/domain/work-orders/utils/work-order-feedback-toasts"; +import { effectiveCompany } from "@/domain/work-orders/utils/effective-completion-snapshot"; import type { WoTableRowHandlers } from "./wo-table-row"; type WoTableRowServiceCellsProps = { @@ -36,6 +37,8 @@ export function WoTableRowServiceCells({ handlers, onPatchRow, }: WoTableRowServiceCellsProps) { + const company = effectiveCompany(row); + return ( <> applyVendorTableSave(handlers.onPatch, row.id, p)} /> diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table.tsx index 226dab4a..3fb69fba 100644 --- a/src/app/(protected)/workorders/_components/list/table/wo-table.tsx +++ b/src/app/(protected)/workorders/_components/list/table/wo-table.tsx @@ -7,6 +7,7 @@ import type { } from "@/domain/work-orders/types/work-order"; import type { WorkOrderTablePatchFn } from "@/domain/work-orders/use-cases/use-work-order-table-mutations"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; +import { effectiveSitePlace } from "@/domain/work-orders/utils/effective-completion-snapshot"; import { DuplicateWoNumberDialog } from "@/app/(protected)/workorders/_components/list/duplicate-wo-number-dialog"; import { WoTableSkeleton } from "@/app/(protected)/workorders/_components/loading/wo-table-skeleton"; import { WoDayPaginationFooter } from "./wo-day-pagination-footer"; @@ -136,7 +137,9 @@ export function WoTable({ allRows={tableData.rows} users={users} sites={sites} - sitePlace={resolveSitePlace(row.locationId, row.site, sitePlaceLookups)} + sitePlace={ + effectiveSitePlace(row) ?? resolveSitePlace(row.locationId, row.site, sitePlaceLookups) + } vendors={vendors} pmTypes={pmTypes} search={tableData.search} diff --git a/src/app/(protected)/workorders/_hooks/create-comp-doc-dialog-actions.ts b/src/app/(protected)/workorders/_hooks/create-comp-doc-dialog-actions.ts index 343f9a8f..4fb049dd 100644 --- a/src/app/(protected)/workorders/_hooks/create-comp-doc-dialog-actions.ts +++ b/src/app/(protected)/workorders/_hooks/create-comp-doc-dialog-actions.ts @@ -39,6 +39,7 @@ type CreateCompDocDialogActionsParams = { site: CompDocSite; locId: string; isMockLocation: boolean; + isSiteFrozen: boolean; locationDetail: Location | undefined; queryClient: QueryClient; onSave: (data: CompDocData, status: CompDocSaveStatus) => void | Promise; @@ -58,6 +59,7 @@ export function createCompDocDialogActions({ site, locId, isMockLocation, + isSiteFrozen, locationDetail, queryClient, onSave, @@ -79,7 +81,7 @@ export function createCompDocDialogActions({ const next = editAddress.trim(); const current = buildCompDocAddressLine(site); if (next === current) return; - if (isMockLocation || !locId) return; + if (isSiteFrozen || isMockLocation || !locId) return; if (!locationDetail) { throw new Error("Location details are still loading. Try saving again."); } diff --git a/src/app/(protected)/workorders/_hooks/use-comp-doc-dialog.ts b/src/app/(protected)/workorders/_hooks/use-comp-doc-dialog.ts index ad5c470b..07328e0c 100644 --- a/src/app/(protected)/workorders/_hooks/use-comp-doc-dialog.ts +++ b/src/app/(protected)/workorders/_hooks/use-comp-doc-dialog.ts @@ -15,6 +15,11 @@ import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-ta import { getMockWoWizardLocationDetail } from "@/domain/work-orders/mocks/wo-wizard-mock-data"; import { resolveCompDocSite } from "@/domain/work-orders/utils/completion-doc-site"; import { DOC_STATE_CFG, docStateOf } from "@/domain/work-orders/utils/completion-doc-state"; +import { + effectiveCompany, + effectivePoc, + effectiveSite, +} from "@/domain/work-orders/utils/effective-completion-snapshot"; type UseCompDocDialogParams = { row: WorkOrderTableRow; @@ -34,8 +39,9 @@ export function useCompDocDialog({ const queryClient = useQueryClient(); const locId = String(row.locationId || ""); const isMockLocation = locId.startsWith("mock-"); + const frozenSite = effectiveSite(row); const { data: apiLocationDetail } = useLocationDetail( - isMockLocation || !locId ? undefined : locId, + frozenSite != null || isMockLocation || !locId ? undefined : locId, ); const locationDetail = isMockLocation ? getMockWoWizardLocationDetail(locId) : apiLocationDetail; const site = useMemo(() => resolveCompDocSite(row, locationDetail), [row, locationDetail]); @@ -75,22 +81,32 @@ export function useCompDocDialog({ return; } - const awaitingLocation = Boolean(locId) && !isMockLocation && !locationDetail; + const awaitingLocation = + frozenSite == null && Boolean(locId) && !isMockLocation && !locationDetail; if (awaitingLocation) { return; } setEditAddress(buildCompDocAddressLine(resolveCompDocSite(row, locationDetail))); addressSeededRef.current = true; - }, [open, row, data?.notes, data?.generatedAt, locationDetail, locId, isMockLocation]); + }, [ + open, + row, + data?.notes, + data?.generatedAt, + locationDetail, + locId, + isMockLocation, + frozenSite, + ]); const handleAddressChange = (value: string) => { addressTouchedRef.current = true; setEditAddress(value); }; - const amazonRep = row.pocName || ""; - const seaRep = row.tech || ""; + const amazonRep = effectivePoc(row).contacts[0]?.name || ""; + const seaRep = effectiveCompany(row).contactName || ""; const cfg = DOC_STATE_CFG[docState]; const isNotRequired = docState === "not-required"; const isGenerated = docState === "generated"; @@ -129,6 +145,7 @@ export function useCompDocDialog({ site, locId, isMockLocation, + isSiteFrozen: frozenSite != null, locationDetail, queryClient, onSave, diff --git a/src/domain/work-orders/mappers/work-order-table-row-mapper.ts b/src/domain/work-orders/mappers/work-order-table-row-mapper.ts index da4cff15..d7203502 100644 --- a/src/domain/work-orders/mappers/work-order-table-row-mapper.ts +++ b/src/domain/work-orders/mappers/work-order-table-row-mapper.ts @@ -9,6 +9,9 @@ import { } from "@/domain/work-orders/types/work-order-board"; import type { CompletionDocStatus, + WorkOrderFrozenCompany, + WorkOrderFrozenPoc, + WorkOrderFrozenSite, WorkOrderTableRow, WorkOrderUpliftSummary, } from "@/domain/work-orders/types/work-order-table-row"; @@ -271,6 +274,54 @@ function readPrimaryDispatchStatus(item: Record): string { return readString(item, "primaryDispatchStatus", "PrimaryDispatchStatus"); } +function readFrozenSite(item: Record): WorkOrderFrozenSite | undefined { + const source = asRecord(item.frozenSite ?? item.FrozenSite); + if (Object.keys(source).length === 0) return undefined; + return { + code: readString(source, "code", "Code"), + label: readString(source, "label", "Label"), + address: readString(source, "address", "Address"), + city: readString(source, "city", "City"), + state: readString(source, "state", "State"), + zip: readString(source, "zip", "Zip"), + phone: readString(source, "phone", "Phone"), + email: readString(source, "email", "Email"), + maps: readString(source, "maps", "Maps"), + }; +} + +function readFrozenCompany(item: Record): WorkOrderFrozenCompany | undefined { + const source = asRecord(item.frozenCompany ?? item.FrozenCompany); + if (Object.keys(source).length === 0) return undefined; + return { + name: readString(source, "name", "Name"), + contactName: readString(source, "contactName", "ContactName"), + address: readString(source, "address", "Address"), + city: readString(source, "city", "City"), + state: readString(source, "state", "State"), + zip: readString(source, "zip", "Zip"), + phone: readString(source, "phone", "Phone"), + email: readString(source, "email", "Email"), + maps: readString(source, "maps", "Maps"), + }; +} + +function readFrozenPoc(item: Record): WorkOrderFrozenPoc | undefined { + const source = asRecord(item.frozenPoc ?? item.FrozenPoc); + if (Object.keys(source).length === 0) return undefined; + const rawContacts = source.contacts ?? source.Contacts; + const contacts = Array.isArray(rawContacts) + ? rawContacts.map(asRecord).map((contact) => ({ + name: readString(contact, "name", "Name"), + phone: readString(contact, "phone", "Phone"), + })) + : []; + return { + contacts, + notes: readString(source, "notes", "Notes"), + }; +} + export function mapWorkOrderTableRow(raw: unknown): WorkOrderTableRow { const item = asRecord(raw); const scheduledOn = toDateInputValue( @@ -364,6 +415,9 @@ export function mapWorkOrderTableRow(raw: unknown): WorkOrderTableRow { pocPhone: readString(item, "pocPhone", "PocPhone", "POCPhone"), pocNotes: readString(item, "pocNotes", "PocNotes", "POCNotes"), additionalContacts: readAdditionalContacts(item), + frozenSite: readFrozenSite(item), + frozenCompany: readFrozenCompany(item), + frozenPoc: readFrozenPoc(item), dueDate: toDateInputValue(readString(item, "dueDate", "DueDate")), rescheduleCount: readNumber(item, "rescheduleCount", "RescheduleCount") ?? 0, carriedOver: readNumber(item, "carriedOver", "CarriedOver") ?? 0, diff --git a/src/domain/work-orders/types/work-order-board.ts b/src/domain/work-orders/types/work-order-board.ts index cbd48acd..b8bdcaa0 100644 --- a/src/domain/work-orders/types/work-order-board.ts +++ b/src/domain/work-orders/types/work-order-board.ts @@ -1,6 +1,11 @@ import type { DateRangeKey } from "./work-order-advanced-filters"; import type { WorkOrderAdditionalContact } from "./work-order-additional-contact"; -import type { WorkOrderTableRow } from "./work-order-table-row"; +import type { + WorkOrderFrozenCompany, + WorkOrderFrozenPoc, + WorkOrderFrozenSite, + WorkOrderTableRow, +} from "./work-order-table-row"; import type { WizardWOStatus, WOType } from "./work-order-wizard"; /** Numeric WorkOrderType enum as expected by POST /workorders/board (no string enums). */ @@ -173,6 +178,9 @@ export interface WorkOrderBoardRowApi { pocPhone?: string | null; pocNotes?: string | null; additionalContacts?: WorkOrderAdditionalContact[] | null; + frozenSite?: WorkOrderFrozenSite | null; + frozenCompany?: WorkOrderFrozenCompany | null; + frozenPoc?: WorkOrderFrozenPoc | null; lifecycleStatus?: number | null; lifecycleStatusLabel?: string | null; legacyStatus?: string | null; diff --git a/src/domain/work-orders/types/work-order-table-row.ts b/src/domain/work-orders/types/work-order-table-row.ts index 92040fa2..ad6bb06e 100644 --- a/src/domain/work-orders/types/work-order-table-row.ts +++ b/src/domain/work-orders/types/work-order-table-row.ts @@ -13,6 +13,35 @@ export type WorkOrderUpliftSummary = { primaryStatus?: string; }; +export type WorkOrderFrozenSite = { + code?: string; + label?: string; + address?: string; + city?: string; + state?: string; + zip?: string; + phone?: string; + email?: string; + maps?: string; +}; + +export type WorkOrderFrozenCompany = { + name?: string; + contactName?: string; + address?: string; + city?: string; + state?: string; + zip?: string; + phone?: string; + email?: string; + maps?: string; +}; + +export type WorkOrderFrozenPoc = { + contacts: { name?: string; phone?: string }[]; + notes?: string; +}; + export interface WorkOrderTableRow { id: string | number; woNumber: string; @@ -49,6 +78,9 @@ export interface WorkOrderTableRow { pocPhone: string; pocNotes: string; additionalContacts: WorkOrderAdditionalContact[]; + frozenSite?: WorkOrderFrozenSite; + frozenCompany?: WorkOrderFrozenCompany; + frozenPoc?: WorkOrderFrozenPoc; dueDate: string; rescheduleCount: number; carriedOver: number; diff --git a/src/domain/work-orders/utils/completion-doc-site.ts b/src/domain/work-orders/utils/completion-doc-site.ts index b1b9dc18..8ac80886 100644 --- a/src/domain/work-orders/utils/completion-doc-site.ts +++ b/src/domain/work-orders/utils/completion-doc-site.ts @@ -1,15 +1,27 @@ import type { Location } from "@/domain/locations/types/location"; import type { CompDocSiteInfo } from "@/domain/work-orders/types/completion-doc"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; +import { effectiveSite } from "@/domain/work-orders/utils/effective-completion-snapshot"; function buildLabel(city?: string, state?: string): string { return [city, state].filter(Boolean).join(", "); } export function resolveCompDocSite( - row: Pick, + row: Pick, location?: Location | null, ): CompDocSiteInfo { + const frozen = effectiveSite(row); + if (frozen != null) { + const city = frozen.city ?? ""; + const state = frozen.state ?? ""; + const zip = frozen.zip ?? ""; + const label = buildLabel(city, state); + const street = frozen.address ?? ""; + const fullAddress = [street, label, zip].filter(Boolean).join(", ") || "—"; + return { street, city, state, zip, label, fullAddress }; + } + if (location?.address || location?.city) { const city = location.city ?? ""; const state = location.state ?? ""; diff --git a/src/domain/work-orders/utils/effective-completion-snapshot.ts b/src/domain/work-orders/utils/effective-completion-snapshot.ts new file mode 100644 index 00000000..59dc9827 --- /dev/null +++ b/src/domain/work-orders/utils/effective-completion-snapshot.ts @@ -0,0 +1,55 @@ +import type { + WorkOrderFrozenCompany, + WorkOrderFrozenPoc, + WorkOrderFrozenSite, + WorkOrderTableRow, +} from "@/domain/work-orders/types/work-order-table-row"; + +type SiteSnapshotRow = Pick; +type CompanySnapshotRow = Pick< + WorkOrderTableRow, + "status" | "company" | "tech" | "techPhone" | "frozenCompany" +>; +type PocSnapshotRow = Pick< + WorkOrderTableRow, + "status" | "pocName" | "pocPhone" | "pocNotes" | "frozenPoc" +> & { + additionalContacts?: WorkOrderTableRow["additionalContacts"]; +}; + +export function effectiveSite(row: SiteSnapshotRow): WorkOrderFrozenSite | undefined { + if (row.status === "Completed" && row.frozenSite != null) return row.frozenSite; + return undefined; +} + +export function effectiveSitePlace(row: SiteSnapshotRow): { + city: string; + state: string; +} | null { + const frozen = effectiveSite(row); + if (frozen == null) return null; + return { city: frozen.city ?? "", state: frozen.state ?? "" }; +} + +export function effectiveCompany(row: CompanySnapshotRow): WorkOrderFrozenCompany { + if (row.status === "Completed" && row.frozenCompany != null) return row.frozenCompany; + return { + name: row.company, + contactName: row.tech, + phone: row.techPhone, + }; +} + +export function effectivePoc(row: PocSnapshotRow): WorkOrderFrozenPoc { + if (row.status === "Completed" && row.frozenPoc != null) return row.frozenPoc; + return { + contacts: [ + { name: row.pocName, phone: row.pocPhone }, + ...(row.additionalContacts ?? []).map((contact) => ({ + name: contact.name, + phone: contact.phone, + })), + ], + notes: row.pocNotes, + }; +} diff --git a/src/test/app/(protected)/workorders/use-comp-doc-dialog-address-seed.test.tsx b/src/test/app/(protected)/workorders/use-comp-doc-dialog-address-seed.test.tsx index 80a9b011..3635974c 100644 --- a/src/test/app/(protected)/workorders/use-comp-doc-dialog-address-seed.test.tsx +++ b/src/test/app/(protected)/workorders/use-comp-doc-dialog-address-seed.test.tsx @@ -9,11 +9,16 @@ import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-ta const locationDetailState = vi.hoisted(() => ({ data: undefined as Location | undefined, })); +const updateLocation = vi.hoisted(() => vi.fn()); vi.mock("@/domain/locations/use-cases/use-location-detail", () => ({ useLocationDetail: () => ({ data: locationDetailState.data }), })); +vi.mock("@/domain/locations/api/locations-api", () => ({ + locationsApi: { update: updateLocation }, +})); + function baseRow(overrides: Partial = {}): WorkOrderTableRow { const defaults: WorkOrderTableRow = { id: 42, @@ -80,6 +85,7 @@ function wrapper({ children }: { children: ReactNode }) { describe("useCompDocDialog address seeding", () => { beforeEach(() => { locationDetailState.data = undefined; + updateLocation.mockReset(); }); it("seeds address from the first authoritative locationDetail without locking on the empty fallback", async () => { @@ -153,4 +159,39 @@ describe("useCompDocDialog address seeding", () => { expect(result.current.editAddress).toBe("Dispatcher typed address"); }); + + it("does not update the live location when a completed work order uses a frozen site", async () => { + const onSave = vi.fn().mockResolvedValue(undefined); + const row = baseRow({ + status: "Completed", + frozenSite: { + address: "100 Frozen Ave", + city: "Chicago", + state: "IL", + zip: "60601", + }, + }); + const { result } = renderHook( + () => + useCompDocDialog({ + row, + open: true, + onSave, + }), + { wrapper }, + ); + + await waitFor(() => { + expect(result.current.editAddress).toBe("100 Frozen Ave"); + }); + act(() => { + result.current.setEditAddress("Edited document address"); + }); + await act(async () => { + await result.current.handleSaveDraft(); + }); + + expect(updateLocation).not.toHaveBeenCalled(); + expect(onSave).toHaveBeenCalledWith(expect.any(Object), "No"); + }); }); diff --git a/src/test/app/(protected)/workorders/use-site-dialog-state.test.tsx b/src/test/app/(protected)/workorders/use-site-dialog-state.test.tsx index b3f72c52..d527414d 100644 --- a/src/test/app/(protected)/workorders/use-site-dialog-state.test.tsx +++ b/src/test/app/(protected)/workorders/use-site-dialog-state.test.tsx @@ -5,19 +5,31 @@ import type { Location } from "@/domain/locations/types/location"; import type { LocationOption } from "@/domain/work-orders/types/work-order"; const detailById = new Map(); +const requestedDetailIds: Array = []; +const EMPTY_CONTACTS: [] = []; vi.mock("@/domain/locations/use-cases/use-location-detail", () => ({ - useLocationDetail: (id: string | undefined) => ({ - data: id && detailById.has(id) ? detailById.get(id) : undefined, - isLoading: Boolean(id && !detailById.has(id)), - isError: false, - }), + useLocationDetail: (id: string | undefined) => { + requestedDetailIds.push(id); + return { + data: id && detailById.has(id) ? detailById.get(id) : undefined, + isLoading: Boolean(id && !detailById.has(id)), + isError: false, + }; + }, })); const SITES: LocationOption[] = [ { id: "loc-1", name: "BK5 · Dallas", siteCode: "BK5", city: "Dallas", state: "TX" }, { id: "loc-2", name: "SEA1 · Seattle", siteCode: "SEA1", city: "Seattle", state: "WA" }, ]; +const FROZEN_SITE = { + label: "BK5 · Frozen", + address: "100 Frozen Ave", + city: "Chicago", + state: "IL", + zip: "60601", +}; function locationDetail(id: string, contact: string, phone: string): Location { return { @@ -42,7 +54,7 @@ function renderSiteDialogState() { pocName: "", pocPhone: "", pocNotes: "", - additionalContacts: [], + additionalContacts: EMPTY_CONTACTS, sites: SITES, createMode: true, onSave: vi.fn(), @@ -53,6 +65,7 @@ function renderSiteDialogState() { describe("useSiteDialogState POC autofill", () => { beforeEach(() => { detailById.clear(); + requestedDetailIds.length = 0; }); it("preserves POC typed before deferred location detail resolves", () => { @@ -110,4 +123,30 @@ describe("useSiteDialogState POC autofill", () => { expect(result.current.pn).toBe("Contact B"); expect(result.current.pp).toBe("222-222-2222"); }); + + it("does not request live location detail when a frozen site is provided", () => { + const { result } = renderHook(() => + useSiteDialogState({ + open: true, + onOpenChange: vi.fn(), + value: "BK5", + locationId: "loc-1", + pocName: "Frozen POC", + pocPhone: "111", + pocNotes: "", + additionalContacts: EMPTY_CONTACTS, + sites: SITES, + createMode: false, + frozenSite: FROZEN_SITE, + onSave: vi.fn(), + }), + ); + + expect(requestedDetailIds).not.toContain("loc-1"); + expect(result.current.locationDetail).toMatchObject({ + address: "100 Frozen Ave", + city: "Chicago", + state: "IL", + }); + }); }); diff --git a/src/test/app/(protected)/workorders/vendor-dialog-frozen-company.test.tsx b/src/test/app/(protected)/workorders/vendor-dialog-frozen-company.test.tsx new file mode 100644 index 00000000..afe6f231 --- /dev/null +++ b/src/test/app/(protected)/workorders/vendor-dialog-frozen-company.test.tsx @@ -0,0 +1,51 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { VendorDialogCompanyCard } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-company-card"; +import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order"; + +const LIVE_VENDOR: VendorDropdownItem = { + id: 10, + companyName: "Live Vendor", + contactName: "Live Contact", + tradeSpecialties: "Live Trade", + address: "900 Live St", + distanceMiles: 12, + email: "live@example.com", +}; + +describe("VendorDialogCompanyCard completion snapshot", () => { + it("does not fill missing frozen fields from the live vendor registry", () => { + render( + , + ); + + expect(screen.getByText("Frozen Vendor")).toBeInTheDocument(); + expect(screen.queryByText("Live Contact")).not.toBeInTheDocument(); + expect(screen.queryByText("Live Trade")).not.toBeInTheDocument(); + expect(screen.queryByText("live@example.com")).not.toBeInTheDocument(); + expect(screen.queryByRole("link", { name: /open in google maps/i })).not.toBeInTheDocument(); + }); + + it("renders the complete frozen address instead of the live vendor address", () => { + render( + , + ); + + expect(screen.getByText("200 Vendor Ave, Chicago, IL, 60602")).toBeInTheDocument(); + expect(screen.queryByText("900 Live St")).not.toBeInTheDocument(); + }); +}); diff --git a/src/test/domain/work-orders/mappers/work-order-table-row-mapper.test.ts b/src/test/domain/work-orders/mappers/work-order-table-row-mapper.test.ts index 0b0e78c5..910e42d3 100644 --- a/src/test/domain/work-orders/mappers/work-order-table-row-mapper.test.ts +++ b/src/test/domain/work-orders/mappers/work-order-table-row-mapper.test.ts @@ -186,3 +186,29 @@ describe("mapWorkOrderTableRow carry-over fields", () => { expect(group.items.map((item) => item.id)).toEqual([42]); }); }); + +describe("mapWorkOrderTableRow completion snapshot", () => { + it("maps frozen site, company, and POC payloads", () => { + const row = mapWorkOrderTableRow({ + id: 191, + lifecycleStatus: 7, + frozenSite: { code: "BK5", label: "BK5 Chicago", city: "Chicago", state: "IL" }, + frozenCompany: { name: "Frozen Co", contactName: "Frozen Tech", phone: "111" }, + frozenPoc: { + contacts: [{ name: "Frozen POC", phone: "222" }], + notes: "Call first", + }, + }); + + expect(row.frozenSite).toMatchObject({ code: "BK5", city: "Chicago", state: "IL" }); + expect(row.frozenCompany).toMatchObject({ + name: "Frozen Co", + contactName: "Frozen Tech", + phone: "111", + }); + expect(row.frozenPoc).toEqual({ + contacts: [{ name: "Frozen POC", phone: "222" }], + notes: "Call first", + }); + }); +}); diff --git a/src/test/domain/work-orders/utils/completion-doc-site.test.ts b/src/test/domain/work-orders/utils/completion-doc-site.test.ts new file mode 100644 index 00000000..8abeae5d --- /dev/null +++ b/src/test/domain/work-orders/utils/completion-doc-site.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import { resolveCompDocSite } from "@/domain/work-orders/utils/completion-doc-site"; + +describe("resolveCompDocSite", () => { + it("prefers a completed work order snapshot over conflicting live location data", () => { + const result = resolveCompDocSite( + { + site: "BK5", + location: "Live City, WI", + status: "Completed", + frozenSite: { + address: "100 Frozen Ave", + city: "Chicago", + state: "IL", + zip: "60601", + }, + }, + { + address: "900 Live St", + city: "Milwaukee", + state: "WI", + zipCode: "53202", + }, + ); + + expect(result).toEqual({ + street: "100 Frozen Ave", + city: "Chicago", + state: "IL", + zip: "60601", + label: "Chicago, IL", + fullAddress: "100 Frozen Ave, Chicago, IL, 60601", + }); + }); + + it("keeps using the live location for an open work order", () => { + const result = resolveCompDocSite( + { + site: "BK5", + location: "Old City, IL", + status: "Scheduled", + frozenSite: { address: "100 Frozen Ave", city: "Chicago", state: "IL" }, + }, + { + address: "900 Live St", + city: "Milwaukee", + state: "WI", + zipCode: "53202", + }, + ); + + expect(result.street).toBe("900 Live St"); + expect(result.label).toBe("Milwaukee, WI"); + }); +}); diff --git a/src/test/domain/work-orders/utils/effective-completion-snapshot.test.ts b/src/test/domain/work-orders/utils/effective-completion-snapshot.test.ts new file mode 100644 index 00000000..688e886f --- /dev/null +++ b/src/test/domain/work-orders/utils/effective-completion-snapshot.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from "vitest"; +import { + effectiveCompany, + effectivePoc, + effectiveSite, + effectiveSitePlace, +} from "@/domain/work-orders/utils/effective-completion-snapshot"; + +describe("effective completion snapshot", () => { + it("prefers frozen values for a completed work order", () => { + const frozenSite = { code: "BK5", city: "Chicago", state: "IL" }; + expect(effectiveSite({ status: "Completed", frozenSite })).toEqual(frozenSite); + expect(effectiveSitePlace({ status: "Completed", frozenSite })).toEqual({ + city: "Chicago", + state: "IL", + }); + + expect( + effectiveCompany({ + status: "Completed", + company: "Live Co", + tech: "Live Tech", + techPhone: "222", + frozenCompany: { name: "Frozen Co", contactName: "Frozen Tech", phone: "111" }, + }), + ).toMatchObject({ name: "Frozen Co", contactName: "Frozen Tech", phone: "111" }); + + expect( + effectivePoc({ + status: "Completed", + pocName: "Live POC", + pocPhone: "222", + pocNotes: "live", + additionalContacts: [], + frozenPoc: { + contacts: [{ name: "Frozen POC", phone: "111" }], + notes: "frozen", + }, + }), + ).toEqual({ + contacts: [{ name: "Frozen POC", phone: "111" }], + notes: "frozen", + }); + }); + + it("uses live values while open or when a completed snapshot is missing", () => { + expect(effectiveSite({ status: "Scheduled", frozenSite: { city: "Old" } })).toBeUndefined(); + expect(effectiveSitePlace({ status: "Completed" })).toBeNull(); + expect( + effectiveCompany({ + status: "Scheduled", + company: "Live Co", + tech: "Live Tech", + techPhone: "222", + frozenCompany: { name: "Old Co" }, + }), + ).toMatchObject({ name: "Live Co", contactName: "Live Tech", phone: "222" }); + expect( + effectivePoc({ + status: "Completed", + pocName: "Live POC", + pocPhone: "222", + pocNotes: "live", + additionalContacts: [{ name: "Backup", phone: "333" }], + }), + ).toEqual({ + contacts: [ + { name: "Live POC", phone: "222" }, + { name: "Backup", phone: "333" }, + ], + notes: "live", + }); + }); + + it("treats missing additional contacts as an empty live contact list", () => { + expect( + effectivePoc({ + status: "Scheduled", + pocName: "Primary", + pocPhone: "111", + pocNotes: "", + }), + ).toEqual({ + contacts: [{ name: "Primary", phone: "111" }], + notes: "", + }); + }); +});