From 116f65ca4803d7bacda5f0bf09561955f37e34d4 Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Thu, 10 Sep 2026 15:41:01 -0300 Subject: [PATCH 1/4] 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: "", + }); + }); +}); From 99c758872ace33e769ef42c9da2a3ce625589753 Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Thu, 10 Sep 2026 18:15:02 -0300 Subject: [PATCH 2/4] fix(work-orders): extract helpers to satisfy governance complexity --- .../tabs/slide-over-info-tab-display-view.tsx | 231 ++++++++++++------ .../cells/vendor-dialog-company-card.tsx | 92 +++++-- .../work-orders/utils/completion-doc-site.ts | 56 +++-- 3 files changed, 257 insertions(+), 122 deletions(-) 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 edc03f69..1109be91 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 @@ -32,6 +32,151 @@ type SlideOverInfoTabDisplayViewProps = { pmTypes: string[]; }; +type FrozenPoc = ReturnType; + +function formatScheduleOn(source: WorkOrderTableRow): string { + if (source.scheduleWeekOnly && source.targetWeek) { + return `Week of ${source.targetWeek}`; + } + return source.scheduledOn || "—"; +} + +function formatApptDisplay(apptTime: string): string { + const appt = parseApptTime(apptTime); + if (appt.start) return formatApptRange(appt.start, appt.end); + return apptTime || "—"; +} + +function formatPocLine(poc: FrozenPoc): string { + const primary = poc.contacts[0]; + const name = primary?.name || "—"; + if (!primary?.phone) return name; + return `${name} · ${primary.phone}`; +} + +function SlideOverTypeField({ type }: { type: WorkOrderTableRow["type"] }) { + if (!type) { + return —; + } + return ; +} + +function SlideOverCompletedSite({ + source, + poc, + sites, +}: { + source: WorkOrderTableRow; + poc: FrozenPoc; + sites: LocationOption[]; +}) { + const primaryPoc = poc.contacts[0]; + return ( + ({ + name: contact.name ?? "", + phone: contact.phone ?? "", + }))} + sites={sites} + viewOnly + frozenSite={source.frozenSite} + onSave={() => undefined} + /> + ); +} + +function SlideOverSiteField({ + source, + isCompleted, + poc, + sites, +}: { + source: WorkOrderTableRow; + isCompleted: boolean; + poc: FrozenPoc; + sites: LocationOption[]; +}) { + if (!isCompleted) { + return ( + + {source.site || "—"} + + ); + } + return ; +} + +function SlideOverOpenService({ source }: { source: WorkOrderTableRow }) { + if (!source.pm) { + return —; + } + return ( + + + {source.pm} + {Boolean(source.avetaRequired) && ( + + )} + + ); +} + +function SlideOverServiceField({ + source, + isCompleted, + pmTypes, +}: { + source: WorkOrderTableRow; + isCompleted: boolean; + pmTypes: string[]; +}) { + if (!isCompleted) return ; + return ( + undefined} + /> + ); +} + +function SlideOverVendorField({ + source, + isCompleted, + vendors, +}: { + source: WorkOrderTableRow; + isCompleted: boolean; + vendors: VendorDropdownItem[]; +}) { + if (!isCompleted) return ; + const company = effectiveCompany(source); + return ( + undefined} + /> + ); +} + export function SlideOverInfoTabDisplayView({ source, isCancelled, @@ -41,11 +186,7 @@ export function SlideOverInfoTabDisplayView({ vendors, pmTypes, }: 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 (
@@ -53,38 +194,10 @@ export function SlideOverInfoTabDisplayView({ {source.woNumber || "—"} - {isCompleted ? ( - ({ - name: contact.name ?? "", - phone: contact.phone ?? "", - }))} - sites={sites} - viewOnly - frozenSite={source.frozenSite} - onSave={() => undefined} - /> - ) : ( - - {source.site || "—"} - - )} + - {source.type ? ( - - ) : ( - — - )} + - - {source.scheduleWeekOnly && source.targetWeek - ? `Week of ${source.targetWeek}` - : source.scheduledOn || "—"} - + {formatScheduleOn(source)} {source.dueDate || "—"} - {apptDisplay} + {formatApptDisplay(source.apptTime)} - {isCompleted ? ( - undefined} - /> - ) : source.pm ? ( - - - {source.pm} - {Boolean(source.avetaRequired) && ( - - )} - - ) : ( - — - )} + - {isCompleted ? ( - undefined} - /> - ) : ( - - )} + @@ -154,8 +228,7 @@ export function SlideOverInfoTabDisplayView({ - {primaryPoc?.name || "—"} - {primaryPoc?.phone ? ` · ${primaryPoc.phone}` : ""} + {formatPocLine(poc)}
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 fc711dc9..1881b797 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 @@ -10,6 +10,16 @@ type VendorDialogCompanyCardProps = { frozenCompany?: WorkOrderFrozenCompany; }; +type CompanyCardModel = { + title: string; + distance: string; + contactName: string; + email: string; + address: string; + mapsUrl: string; + phone: string; +}; + function formatFrozenCompanyAddress(company: WorkOrderFrozenCompany): string { const region = [company.city, company.state] .map((part) => part?.trim()) @@ -19,21 +29,68 @@ function formatFrozenCompanyAddress(company: WorkOrderFrozenCompany): string { return [company.address?.trim(), region, company.zip?.trim()].filter(Boolean).join(", "); } +function liveMapsUrl(address: string): string { + if (!address) return ""; + return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(address)}`; +} + +function resolveFrozenCompanyCard( + company: string, + frozen: WorkOrderFrozenCompany, +): CompanyCardModel { + return { + title: company, + distance: "", + contactName: frozen.contactName?.trim() ?? "", + email: frozen.email?.trim() ?? "", + address: formatFrozenCompanyAddress(frozen), + mapsUrl: frozen.maps ?? "", + phone: frozen.phone ?? "", + }; +} + +function resolveLiveCompanyCard( + company: string, + vendor: VendorDropdownItem | undefined, +): CompanyCardModel { + const address = vendor?.address ?? ""; + return { + title: company, + distance: formatVendorDistance(vendor?.distanceMiles), + contactName: "", + email: vendor?.email?.trim() ?? "", + address, + mapsUrl: liveMapsUrl(address), + phone: "", + }; +} + +function resolveCompanyCardModel( + company: string, + vendor: VendorDropdownItem | undefined, + frozenCompany?: WorkOrderFrozenCompany, +): CompanyCardModel { + if (frozenCompany != null) return resolveFrozenCompanyCard(company, frozenCompany); + return resolveLiveCompanyCard(company, vendor); +} + +function CompanyCardDetails({ model }: { model: CompanyCardModel }) { + return ( + <> + {Boolean(model.address) && } + {Boolean(model.contactName) && } + {Boolean(model.phone) && } + {Boolean(model.email) && } + + ); +} + 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)}` - : ""); + const model = resolveCompanyCardModel(company, vendor, frozenCompany); return (
- {company || "—"} + {model.title || "—"}
- {Boolean(distance) && ( + {Boolean(model.distance) && ( - {distance} + {model.distance} )}
- {Boolean(address) && } - {Boolean(contactName) && } - {Boolean(frozenCompany?.phone) && } - {Boolean(email) && } - {Boolean(mapsUrl) && ( + + {Boolean(model.mapsUrl) && (
): CompDocSiteInfo { + const locationParts = row.location.split(",").map((part) => part.trim()); + const city = locationParts[0] ?? ""; + const state = locationParts[1] ?? ""; + const label = buildLabel(city, state); + const street = row.location && !label ? row.location : ""; + return toCompDocSite(street, city, state, ""); +} + export function resolveCompDocSite( 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 }; + return toCompDocSite( + frozen.address ?? "", + frozen.city ?? "", + frozen.state ?? "", + frozen.zip ?? "", + ); } - if (location?.address || location?.city) { - const city = location.city ?? ""; - const state = location.state ?? ""; - const zip = location.zipCode ?? ""; - const label = buildLabel(city, state); - const street = location.address ?? ""; - const fullAddress = [street, label, zip].filter(Boolean).join(", ") || "—"; - return { street, city, state, zip, label, fullAddress }; + return resolveLiveLocationSite(location); } - - const locationParts = row.location.split(",").map((s) => s.trim()); - const city = locationParts[0] ?? ""; - const state = locationParts[1] ?? ""; - const label = buildLabel(city, state); - const street = row.location && !label ? row.location : ""; - const fullAddress = [street, label].filter(Boolean).join(", ") || "—"; - - return { street, city, state, zip: "", label, fullAddress }; + return resolveRowLocationSite(row); } From c7aec49f0535a462b9a81883ced51d02de5dc497 Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Thu, 10 Sep 2026 18:47:32 -0300 Subject: [PATCH 3/4] fix(work-orders): keep completion freeze on live registry edits --- .../comp-doc/comp-doc-details-section.tsx | 2 +- .../comp-doc/comp-doc-preview-document.tsx | 2 +- .../comp-doc-preview-service-info.tsx | 4 +- .../comp-doc/comp-doc-preview-summary.tsx | 8 +-- .../comp-doc-preview-vendor-safety.tsx | 10 +-- .../comp-doc/use-comp-doc-dialog.tsx | 6 +- .../tabs/slide-over-info-tab-display-view.tsx | 9 ++- .../list/table/cells/vendor-dialog-body.tsx | 40 +++++++----- .../list/table/cells/vendor-dialog.tsx | 4 +- .../list/table/wo-table-row-cells.tsx | 6 +- .../workorders/_hooks/use-comp-doc-dialog.ts | 6 +- .../mappers/work-order-table-row-mapper.ts | 1 + .../work-orders/types/completion-doc.ts | 6 ++ .../work-orders/types/work-order-table-row.ts | 2 +- .../work-orders/utils/completion-doc-html.ts | 6 +- .../work-orders/utils/completion-doc-site.ts | 64 +++++++++++++++---- .../utils/effective-completion-snapshot.ts | 13 ++++ .../vendor-dialog-frozen-company.test.tsx | 39 +++++++++++ .../utils/completion-doc-site.test.ts | 7 ++ .../effective-completion-snapshot.test.ts | 32 +++++++++- 20 files changed, 208 insertions(+), 59 deletions(-) diff --git a/src/app/(protected)/workorders/_components/comp-doc/comp-doc-details-section.tsx b/src/app/(protected)/workorders/_components/comp-doc/comp-doc-details-section.tsx index 3a38f2e3..26ba67fd 100644 --- a/src/app/(protected)/workorders/_components/comp-doc/comp-doc-details-section.tsx +++ b/src/app/(protected)/workorders/_components/comp-doc/comp-doc-details-section.tsx @@ -46,7 +46,7 @@ export function CompDocDetailsSection({ - +
- Site ID — {row.site} + Site ID — {site.code} - Amazon.com Services LLC {row.site} + Amazon.com Services LLC {site.code} {site.street}, diff --git a/src/app/(protected)/workorders/_components/comp-doc/comp-doc-preview-summary.tsx b/src/app/(protected)/workorders/_components/comp-doc/comp-doc-preview-summary.tsx index c9716b8d..e07e3726 100644 --- a/src/app/(protected)/workorders/_components/comp-doc/comp-doc-preview-summary.tsx +++ b/src/app/(protected)/workorders/_components/comp-doc/comp-doc-preview-summary.tsx @@ -73,10 +73,10 @@ export function CompDocPreviewSummary({ margin: "2px 0", }} > - Site ID — {row.site} + Site ID — {site.code} - Amazon.com Services LLC {row.site} + Amazon.com Services LLC {site.code} {site.street}, @@ -88,8 +88,8 @@ export function CompDocPreviewSummary({ Vendor / Technician:  - {row.tech || "—"} - {row.company ? ` · ${row.company}` : ""} + {site.tech || "—"} + {site.company ? ` · ${site.company}` : ""} ); diff --git a/src/app/(protected)/workorders/_components/comp-doc/comp-doc-preview-vendor-safety.tsx b/src/app/(protected)/workorders/_components/comp-doc/comp-doc-preview-vendor-safety.tsx index 807eca19..dec94e83 100644 --- a/src/app/(protected)/workorders/_components/comp-doc/comp-doc-preview-vendor-safety.tsx +++ b/src/app/(protected)/workorders/_components/comp-doc/comp-doc-preview-vendor-safety.tsx @@ -1,18 +1,18 @@ import { Text } from "@/components/ui/text"; -import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; +import type { CompDocSiteInfo } from "@/domain/work-orders/types/completion-doc"; import { SAFETY_BULLETS } from "@/domain/work-orders/utils/completion-doc-procedures"; type CompDocPreviewVendorSafetyProps = { - row: WorkOrderTableRow; + site: CompDocSiteInfo; }; -export function CompDocPreviewVendorSafety({ row }: CompDocPreviewVendorSafetyProps) { +export function CompDocPreviewVendorSafety({ site }: CompDocPreviewVendorSafetyProps) { return ( <> Vendor / Technician:  - {row.tech || "—"} - {row.company ? ` · ${row.company}` : ""} + {site.tech || "—"} + {site.company ? ` · ${site.company}` : ""}
    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 b6ff6147..b11f6464 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 @@ -100,8 +100,9 @@ export function useCompDocDialog({ setEditDateCompleted, }); + const company = effectiveCompany(info); const amazonRep = effectivePoc(info).contacts[0]?.name || ""; - const seaRep = effectiveCompany(info).contactName || ""; + const seaRep = company.contactName || ""; const cfg = DOC_STATE_CFG[docState]; const isNotRequired = docState === "not-required"; const isGenerated = docState === "generated" || Boolean(signOffAttachment); @@ -114,6 +115,9 @@ export function useCompDocDialog({ dateCompleted: editDateCompleted, amazonRep, seaRep, + siteCode: site.code, + tech: company.contactName || "", + company: company.name || "", }; const { persist, syncDateIfChanged, refreshAuthoritativeState, handleSaveDraft, handleGenerate } = 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 1109be91..f74bcf78 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 @@ -16,6 +16,7 @@ import { formatApptRange, parseApptTime } from "@/domain/work-orders/utils/wizar import { effectiveCompany, effectivePoc, + effectiveSite, } from "@/domain/work-orders/utils/effective-completion-snapshot"; import { DocBadge } from "./slide-over-doc-badge"; import { SlideOverInfoTabAssignedField } from "./slide-over-info-tab-assigned-field"; @@ -71,10 +72,11 @@ function SlideOverCompletedSite({ sites: LocationOption[]; }) { const primaryPoc = poc.contacts[0]; + const frozenSite = effectiveSite(source); return ( ({ name: contact.name ?? "", phone: contact.phone ?? "", + notes: contact.notes ?? "", }))} sites={sites} viewOnly - frozenSite={source.frozenSite} + frozenSite={frozenSite} onSave={() => undefined} /> ); 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 4195702c..62e69494 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 @@ -98,21 +98,31 @@ export function VendorDialogBody({
    Technician (optional) - - } - searchPlaceholder="Search technician…" - options={technicianOptions.map((option) => ({ - key: option.key, - label: option.name, - }))} - onPick={onPick} - /> - {companyVendorForAdd != null && ( + {frozenCompany != null ? ( +
    + {frozenCompany.contactName?.trim() || tech || "—"} +
    + ) : ( + + } + searchPlaceholder="Search technician…" + options={technicianOptions.map((option) => ({ + key: option.key, + label: option.name, + }))} + onPick={onPick} + /> + )} + {frozenCompany == null && companyVendorForAdd != null && (
    ({ - name: contact.name ?? "", - phone: contact.phone ?? "", - }))} + additionalContacts={effectiveAdditionalContacts(row)} sites={sites} q={search} viewOnly={viewOnly} 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 07328e0c..01025099 100644 --- a/src/app/(protected)/workorders/_hooks/use-comp-doc-dialog.ts +++ b/src/app/(protected)/workorders/_hooks/use-comp-doc-dialog.ts @@ -105,8 +105,9 @@ export function useCompDocDialog({ setEditAddress(value); }; + const company = effectiveCompany(row); const amazonRep = effectivePoc(row).contacts[0]?.name || ""; - const seaRep = effectiveCompany(row).contactName || ""; + const seaRep = company.contactName || ""; const cfg = DOC_STATE_CFG[docState]; const isNotRequired = docState === "not-required"; const isGenerated = docState === "generated"; @@ -120,6 +121,9 @@ export function useCompDocDialog({ dateCompleted: editDateCompleted, amazonRep, seaRep, + siteCode: site.code, + tech: company.contactName || "", + company: company.name || "", }; const previewSite = useMemo( 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 d7203502..d1d4afc3 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 @@ -314,6 +314,7 @@ function readFrozenPoc(item: Record): WorkOrderFrozenPoc | unde ? rawContacts.map(asRecord).map((contact) => ({ name: readString(contact, "name", "Name"), phone: readString(contact, "phone", "Phone"), + notes: readString(contact, "notes", "Notes"), })) : []; return { diff --git a/src/domain/work-orders/types/completion-doc.ts b/src/domain/work-orders/types/completion-doc.ts index 0c6377f5..e68685a4 100644 --- a/src/domain/work-orders/types/completion-doc.ts +++ b/src/domain/work-orders/types/completion-doc.ts @@ -14,6 +14,9 @@ export interface DocFields { dateCompleted: string; amazonRep: string; seaRep: string; + siteCode: string; + tech: string; + company: string; } export type DocState = "draft" | "generated" | "not-required"; @@ -31,12 +34,15 @@ export interface CompletionDocStateConfig { } export interface CompDocSiteInfo { + code: string; street: string; city: string; state: string; zip: string; label: string; fullAddress: string; + tech: string; + company: string; } export type CompDocSaveStatus = Exclude; 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 ad6bb06e..d362f931 100644 --- a/src/domain/work-orders/types/work-order-table-row.ts +++ b/src/domain/work-orders/types/work-order-table-row.ts @@ -38,7 +38,7 @@ export type WorkOrderFrozenCompany = { }; export type WorkOrderFrozenPoc = { - contacts: { name?: string; phone?: string }[]; + contacts: { name?: string; phone?: string; notes?: string }[]; notes?: string; }; diff --git a/src/domain/work-orders/utils/completion-doc-html.ts b/src/domain/work-orders/utils/completion-doc-html.ts index 87d3b30c..a1114090 100644 --- a/src/domain/work-orders/utils/completion-doc-html.ts +++ b/src/domain/work-orders/utils/completion-doc-html.ts @@ -75,11 +75,11 @@ export function buildDocHtml(

    ${esc(serviceTitle(row).toUpperCase())}

    WO# ${esc(row.woNumber)}

    -

    Site ID — ${esc(row.site)}

    -

    Amazon.com Services LLC ${esc(row.site)}

    +

    Site ID — ${esc(fields.siteCode)}

    +

    Amazon.com Services LLC ${esc(fields.siteCode)}

    ${esc(fields.street)},

    ${esc(cityStateZip)}

    -

    Vendor / Technician: ${esc(row.tech || "—")}${row.company ? ` · ${esc(row.company)}` : ""}

    +

    Vendor / Technician: ${esc(fields.tech || "—")}${fields.company ? ` · ${esc(fields.company)}` : ""}

      ${safety}
    ${completedBlock}

    ${esc(proc.heading)}:

    diff --git a/src/domain/work-orders/utils/completion-doc-site.ts b/src/domain/work-orders/utils/completion-doc-site.ts index 5ff4d52b..1546fdcc 100644 --- a/src/domain/work-orders/utils/completion-doc-site.ts +++ b/src/domain/work-orders/utils/completion-doc-site.ts @@ -1,20 +1,53 @@ 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"; +import { + effectiveCompany, + effectiveSite, +} from "@/domain/work-orders/utils/effective-completion-snapshot"; + +type CompDocAddress = Pick< + CompDocSiteInfo, + "street" | "city" | "state" | "zip" | "label" | "fullAddress" +>; + +type CompDocIdentityRow = Pick & + Partial>; function buildLabel(city?: string, state?: string): string { return [city, state].filter(Boolean).join(", "); } -function toCompDocSite(street: string, city: string, state: string, zip: string): CompDocSiteInfo { +function toCompDocAddress( + street: string, + city: string, + state: string, + zip: string, +): CompDocAddress { const label = buildLabel(city, state); const fullAddress = [street, label, zip].filter(Boolean).join(", ") || "—"; return { street, city, state, zip, label, fullAddress }; } -function resolveLiveLocationSite(location: Location): CompDocSiteInfo { - return toCompDocSite( +function withCompDocIdentity(address: CompDocAddress, row: CompDocIdentityRow): CompDocSiteInfo { + const frozen = effectiveSite(row); + const company = effectiveCompany({ + status: row.status, + company: row.company ?? "", + tech: row.tech ?? "", + techPhone: row.techPhone ?? "", + frozenCompany: row.frozenCompany, + }); + return { + ...address, + code: frozen?.code ?? row.site, + tech: company.contactName ?? "", + company: company.name ?? "", + }; +} + +function resolveLiveLocationSite(location: Location): CompDocAddress { + return toCompDocAddress( location.address ?? "", location.city ?? "", location.state ?? "", @@ -22,30 +55,33 @@ function resolveLiveLocationSite(location: Location): CompDocSiteInfo { ); } -function resolveRowLocationSite(row: Pick): CompDocSiteInfo { +function resolveRowLocationSite(row: Pick): CompDocAddress { const locationParts = row.location.split(",").map((part) => part.trim()); const city = locationParts[0] ?? ""; const state = locationParts[1] ?? ""; const label = buildLabel(city, state); const street = row.location && !label ? row.location : ""; - return toCompDocSite(street, city, state, ""); + return toCompDocAddress(street, city, state, ""); } export function resolveCompDocSite( - row: Pick, + row: CompDocIdentityRow, location?: Location | null, ): CompDocSiteInfo { const frozen = effectiveSite(row); if (frozen != null) { - return toCompDocSite( - frozen.address ?? "", - frozen.city ?? "", - frozen.state ?? "", - frozen.zip ?? "", + return withCompDocIdentity( + toCompDocAddress( + frozen.address ?? "", + frozen.city ?? "", + frozen.state ?? "", + frozen.zip ?? "", + ), + row, ); } if (location?.address || location?.city) { - return resolveLiveLocationSite(location); + return withCompDocIdentity(resolveLiveLocationSite(location), row); } - return resolveRowLocationSite(row); + return withCompDocIdentity(resolveRowLocationSite(row), row); } diff --git a/src/domain/work-orders/utils/effective-completion-snapshot.ts b/src/domain/work-orders/utils/effective-completion-snapshot.ts index 59dc9827..b552b39c 100644 --- a/src/domain/work-orders/utils/effective-completion-snapshot.ts +++ b/src/domain/work-orders/utils/effective-completion-snapshot.ts @@ -1,3 +1,4 @@ +import type { WorkOrderAdditionalContact } from "@/domain/work-orders/types/work-order-additional-contact"; import type { WorkOrderFrozenCompany, WorkOrderFrozenPoc, @@ -48,8 +49,20 @@ export function effectivePoc(row: PocSnapshotRow): WorkOrderFrozenPoc { ...(row.additionalContacts ?? []).map((contact) => ({ name: contact.name, phone: contact.phone, + notes: contact.notes, })), ], notes: row.pocNotes, }; } + +export function effectiveAdditionalContacts(row: PocSnapshotRow): WorkOrderAdditionalContact[] { + if (row.status === "Completed" && row.frozenPoc != null) { + return row.frozenPoc.contacts.slice(1).map((contact) => ({ + name: contact.name ?? "", + phone: contact.phone ?? "", + notes: contact.notes ?? "", + })); + } + return row.additionalContacts ?? []; +} 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 index afe6f231..413eb2f1 100644 --- a/src/test/app/(protected)/workorders/vendor-dialog-frozen-company.test.tsx +++ b/src/test/app/(protected)/workorders/vendor-dialog-frozen-company.test.tsx @@ -1,5 +1,6 @@ import { render, screen } from "@testing-library/react"; import { describe, expect, it } from "vitest"; +import { VendorDialogBody } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-body"; import { VendorDialogCompanyCard } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-company-card"; import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order"; @@ -48,4 +49,42 @@ describe("VendorDialogCompanyCard completion snapshot", () => { expect(screen.getByText("200 Vendor Ave, Chicago, IL, 60602")).toBeInTheDocument(); expect(screen.queryByText("900 Live St")).not.toBeInTheDocument(); }); + + it("shows the frozen technician instead of a renamed live roster contact", () => { + render( + undefined} + onPick={() => undefined} + onPhoneChange={() => undefined} + onNotesChange={() => undefined} + onTechnicianCreated={() => undefined} + />, + ); + + expect(screen.getAllByText("Frozen Tech").length).toBeGreaterThan(0); + expect(screen.queryByText("Live Contact")).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: /\+ add technician/i })).not.toBeInTheDocument(); + }); }); 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 index 8abeae5d..4a2cb2f9 100644 --- a/src/test/domain/work-orders/utils/completion-doc-site.test.ts +++ b/src/test/domain/work-orders/utils/completion-doc-site.test.ts @@ -8,12 +8,16 @@ describe("resolveCompDocSite", () => { site: "BK5", location: "Live City, WI", status: "Completed", + company: "Live Co", + tech: "Live Tech", frozenSite: { + code: "FROZEN", address: "100 Frozen Ave", city: "Chicago", state: "IL", zip: "60601", }, + frozenCompany: { name: "Frozen Co", contactName: "Frozen Tech" }, }, { address: "900 Live St", @@ -24,12 +28,15 @@ describe("resolveCompDocSite", () => { ); expect(result).toEqual({ + code: "FROZEN", street: "100 Frozen Ave", city: "Chicago", state: "IL", zip: "60601", label: "Chicago, IL", fullAddress: "100 Frozen Ave, Chicago, IL, 60601", + tech: "Frozen Tech", + company: "Frozen Co", }); }); 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 index 688e886f..5044a09a 100644 --- a/src/test/domain/work-orders/utils/effective-completion-snapshot.test.ts +++ b/src/test/domain/work-orders/utils/effective-completion-snapshot.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { + effectiveAdditionalContacts, effectiveCompany, effectivePoc, effectiveSite, @@ -61,15 +62,42 @@ describe("effective completion snapshot", () => { pocName: "Live POC", pocPhone: "222", pocNotes: "live", - additionalContacts: [{ name: "Backup", phone: "333" }], + additionalContacts: [{ name: "Backup", phone: "333", notes: "gate hours" }], }), ).toEqual({ contacts: [ { name: "Live POC", phone: "222" }, - { name: "Backup", phone: "333" }, + { name: "Backup", phone: "333", notes: "gate hours" }, ], notes: "live", }); + expect( + effectiveAdditionalContacts({ + status: "Scheduled", + pocName: "Live POC", + pocPhone: "222", + pocNotes: "live", + additionalContacts: [{ name: "Backup", phone: "333", notes: "gate hours" }], + }), + ).toEqual([{ name: "Backup", phone: "333", notes: "gate hours" }]); + }); + + it("keeps extra-contact notes on the open work-order path", () => { + expect( + effectiveAdditionalContacts({ + status: "Scheduled", + pocName: "Primary", + pocPhone: "111", + pocNotes: "", + additionalContacts: [{ name: "Backup", phone: "333", notes: "gate hours" }], + frozenPoc: { + contacts: [ + { name: "Frozen", phone: "000" }, + { name: "Frozen Extra", phone: "999", notes: "ignored while open" }, + ], + }, + }), + ).toEqual([{ name: "Backup", phone: "333", notes: "gate hours" }]); }); it("treats missing additional contacts as an empty live contact list", () => { From 15ea87df4f0ac114b1b8b77a8b0243553865496d Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Mon, 14 Sep 2026 10:19:12 -0300 Subject: [PATCH 4/4] test(work-orders): expect frozen POC contact notes in mapper --- .../work-order-table-row-mapper.test.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) 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 910e42d3..9315fd9d 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 @@ -207,7 +207,23 @@ describe("mapWorkOrderTableRow completion snapshot", () => { phone: "111", }); expect(row.frozenPoc).toEqual({ - contacts: [{ name: "Frozen POC", phone: "222" }], + contacts: [{ name: "Frozen POC", phone: "222", notes: "" }], + notes: "Call first", + }); + }); + + it("maps frozen POC contact notes when the payload supplies them", () => { + const row = mapWorkOrderTableRow({ + id: 192, + lifecycleStatus: 7, + frozenPoc: { + contacts: [{ name: "Frozen POC", phone: "222", notes: "Gate code 9" }], + notes: "Call first", + }, + }); + + expect(row.frozenPoc).toEqual({ + contacts: [{ name: "Frozen POC", phone: "222", notes: "Gate code 9" }], notes: "Call first", }); });