fix(work-orders): keep completion freeze on live registry edits

This commit is contained in:
Arthur Bassi 2026-09-10 18:47:32 -03:00
parent 1809694973
commit c7aec49f05
20 changed files with 208 additions and 59 deletions

View file

@ -46,7 +46,7 @@ export function CompDocDetailsSection({
<CompDocDetailRow
icon={MapPin}
label="Site Code"
value={`${row.site} · Amazon.com Services LLC`}
value={`${site.code} · Amazon.com Services LLC`}
/>
<CompDocEditableRow
label="Address"

View file

@ -43,7 +43,7 @@ export function CompDocPreviewDocument({
isGenerated={isGenerated}
generatedAt={generatedAt}
/>
<CompDocPreviewVendorSafety row={row} />
<CompDocPreviewVendorSafety site={site} />
<CompDocPreviewProcedureNotes pm={row.pm} notes={notes} />
</div>
<div

View file

@ -72,10 +72,10 @@ export function CompDocPreviewServiceInfo({
margin: "2px 0",
}}
>
Site ID — {row.site}
Site ID — {site.code}
</Text>
<Text as="p" style={{ fontSize: 13, color: "#161E38", margin: "2px 0" }}>
Amazon.com Services LLC {row.site}
Amazon.com Services LLC {site.code}
</Text>
<Text as="p" style={{ fontSize: 13, color: "#58595B", margin: "2px 0" }}>
{site.street},

View file

@ -73,10 +73,10 @@ export function CompDocPreviewSummary({
margin: "2px 0",
}}
>
Site ID — {row.site}
Site ID — {site.code}
</Text>
<Text variant="body" sx={{ fontSize: 13, color: "#161E38", margin: "2px 0" }}>
Amazon.com Services LLC {row.site}
Amazon.com Services LLC {site.code}
</Text>
<Text variant="body" sx={{ fontSize: 13, color: "#58595B", margin: "2px 0" }}>
{site.street},
@ -88,8 +88,8 @@ export function CompDocPreviewSummary({
<Text variant="body" sx={{ fontSize: 13, color: "#161E38", margin: "14px 0 6px" }}>
<span style={{ fontWeight: 700 }}>Vendor / Technician:&nbsp;</span>
{row.tech || "—"}
{row.company ? ` · ${row.company}` : ""}
{site.tech || "—"}
{site.company ? ` · ${site.company}` : ""}
</Text>
</>
);

View file

@ -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 (
<>
<Text as="p" style={{ fontSize: 13, color: "#161E38", margin: "14px 0 6px" }}>
<span style={{ fontWeight: 700 }}>Vendor / Technician:&nbsp;</span>
{row.tech || "—"}
{row.company ? ` · ${row.company}` : ""}
{site.tech || "—"}
{site.company ? ` · ${site.company}` : ""}
</Text>
<ul style={{ listStyle: "disc", paddingLeft: 18, margin: "8px 0 0" }}>

View file

@ -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 } =

View file

@ -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 (
<SiteCell
value={source.site}
location={source.location}
value={frozenSite?.code ?? source.site}
location={frozenSite?.label ?? source.location}
locationId={source.locationId}
pocName={primaryPoc?.name ?? ""}
pocPhone={primaryPoc?.phone ?? ""}
@ -82,10 +84,11 @@ function SlideOverCompletedSite({
additionalContacts={poc.contacts.slice(1).map((contact) => ({
name: contact.name ?? "",
phone: contact.phone ?? "",
notes: contact.notes ?? "",
}))}
sites={sites}
viewOnly
frozenSite={source.frozenSite}
frozenSite={frozenSite}
onSave={() => undefined}
/>
);

View file

@ -98,21 +98,31 @@ export function VendorDialogBody({
<div>
<FieldLabel>Technician (optional)</FieldLabel>
<WizardFieldSelect
value={selectedKey}
placeholder={selectedCompany ? "Select technician …" : "Pick a company first"}
disabled={!selectedCompany}
icon={
<Wrench size={14} className="shrink-0" style={{ color: "var(--color-text-muted)" }} />
}
searchPlaceholder="Search technician…"
options={technicianOptions.map((option) => ({
key: option.key,
label: option.name,
}))}
onPick={onPick}
/>
{companyVendorForAdd != null && (
{frozenCompany != null ? (
<div className={DLG_INPUT_CLS} style={DLG_INPUT_STYLE}>
{frozenCompany.contactName?.trim() || tech || "—"}
</div>
) : (
<WizardFieldSelect
value={selectedKey}
placeholder={selectedCompany ? "Select technician …" : "Pick a company first"}
disabled={!selectedCompany}
icon={
<Wrench
size={14}
className="shrink-0"
style={{ color: "var(--color-text-muted)" }}
/>
}
searchPlaceholder="Search technician…"
options={technicianOptions.map((option) => ({
key: option.key,
label: option.name,
}))}
onPick={onPick}
/>
)}
{frozenCompany == null && companyVendorForAdd != null && (
<div className="mt-2">
<VendorDialogAddTechnician
companyName={anchorCompanyName}

View file

@ -77,8 +77,8 @@ export function VendorDialog({
technicianOptions={form.technicianOptions}
anchorCompanyName={form.anchorCompanyName}
companyVendorForAdd={viewOnly ? undefined : (form.companyVendor ?? undefined)}
tech={form.selected?.name ?? form.t}
phone={form.phone}
tech={frozenCompany?.contactName ?? form.selected?.name ?? form.t}
phone={frozenCompany?.phone ?? form.phone}
notes={form.notes}
resolvedCompany={form.resolvedCompany}
selectedVendor={form.selectedVendor}

View file

@ -4,6 +4,7 @@ import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-ta
import { applyAssignedToTableSave } from "@/domain/work-orders/utils/work-order-feedback-toasts";
import { isWorkOrderCompletionSurfaceReadOnly } from "@/domain/work-orders/utils/work-order-lock";
import {
effectiveAdditionalContacts,
effectivePoc,
effectiveSite,
} from "@/domain/work-orders/utils/effective-completion-snapshot";
@ -69,10 +70,7 @@ export function WoTableRowCells({
pocName={primaryPoc?.name ?? ""}
pocPhone={primaryPoc?.phone ?? ""}
pocNotes={poc.notes ?? ""}
additionalContacts={poc.contacts.slice(1).map((contact) => ({
name: contact.name ?? "",
phone: contact.phone ?? "",
}))}
additionalContacts={effectiveAdditionalContacts(row)}
sites={sites}
q={search}
viewOnly={viewOnly}

View file

@ -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(

View file

@ -314,6 +314,7 @@ function readFrozenPoc(item: Record<string, unknown>): WorkOrderFrozenPoc | unde
? rawContacts.map(asRecord).map((contact) => ({
name: readString(contact, "name", "Name"),
phone: readString(contact, "phone", "Phone"),
notes: readString(contact, "notes", "Notes"),
}))
: [];
return {

View file

@ -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<CompletionDocStatus, "">;

View file

@ -38,7 +38,7 @@ export type WorkOrderFrozenCompany = {
};
export type WorkOrderFrozenPoc = {
contacts: { name?: string; phone?: string }[];
contacts: { name?: string; phone?: string; notes?: string }[];
notes?: string;
};

View file

@ -75,11 +75,11 @@ export function buildDocHtml(
<div class="rule"></div>
<h1>${esc(serviceTitle(row).toUpperCase())}</h1>
<p style="margin:2px 0;" class="wo">WO# ${esc(row.woNumber)}</p>
<p style="margin:14px 0 0;">Site ID — ${esc(row.site)}</p>
<p style="margin:2px 0;">Amazon.com Services LLC ${esc(row.site)}</p>
<p style="margin:14px 0 0;">Site ID — ${esc(fields.siteCode)}</p>
<p style="margin:2px 0;">Amazon.com Services LLC ${esc(fields.siteCode)}</p>
<p style="margin:2px 0;" class="muted">${esc(fields.street)},</p>
<p style="margin:2px 0;" class="muted">${esc(cityStateZip)}</p>
<p style="margin:14px 0 4px;"><b>Vendor / Technician:</b> ${esc(row.tech || "—")}${row.company ? ` &middot; ${esc(row.company)}` : ""}</p>
<p style="margin:14px 0 4px;"><b>Vendor / Technician:</b> ${esc(fields.tech || "—")}${fields.company ? ` &middot; ${esc(fields.company)}` : ""}</p>
<ul style="padding-left:20px;">${safety}</ul>
${completedBlock}
<h3 style="margin:16px 0 4px;">${esc(proc.heading)}:</h3>

View file

@ -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<WorkOrderTableRow, "site" | "location" | "status" | "frozenSite"> &
Partial<Pick<WorkOrderTableRow, "company" | "tech" | "techPhone" | "frozenCompany">>;
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<WorkOrderTableRow, "location">): CompDocSiteInfo {
function resolveRowLocationSite(row: Pick<WorkOrderTableRow, "location">): 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<WorkOrderTableRow, "site" | "location" | "status" | "frozenSite">,
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);
}

View file

@ -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 ?? [];
}

View file

@ -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(
<VendorDialogBody
selectedCompany="Frozen Vendor"
companyOptions={[{ key: "Frozen Vendor", label: "Frozen Vendor" }]}
selectedKey={String(LIVE_VENDOR.id)}
technicianOptions={[
{
key: String(LIVE_VENDOR.id),
label: LIVE_VENDOR.contactName,
name: LIVE_VENDOR.contactName,
company: LIVE_VENDOR.companyName,
phone: "",
vendor: LIVE_VENDOR,
},
]}
anchorCompanyName="Frozen Vendor"
companyVendorForAdd={LIVE_VENDOR}
tech="Frozen Tech"
phone="111"
notes=""
resolvedCompany="Frozen Vendor"
selectedVendor={LIVE_VENDOR}
frozenCompany={{ name: "Frozen Vendor", contactName: "Frozen Tech", phone: "111" }}
saveError={null}
onCompanyPick={() => 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();
});
});

View file

@ -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",
});
});

View file

@ -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", () => {