mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 04:12:09 +00:00
Merge pull request #175 from Sea-Haven-Industries/feat/SH-191-completion-freeze
Some checks are pending
Some checks are pending
feat(work-orders): consume completion freeze snapshot on the board
This commit is contained in:
commit
99df031f68
32 changed files with 1059 additions and 168 deletions
|
|
@ -46,7 +46,7 @@ export function CompDocDetailsSection({
|
||||||
<CompDocDetailRow
|
<CompDocDetailRow
|
||||||
icon={MapPin}
|
icon={MapPin}
|
||||||
label="Site Code"
|
label="Site Code"
|
||||||
value={`${row.site} · Amazon.com Services LLC`}
|
value={`${site.code} · Amazon.com Services LLC`}
|
||||||
/>
|
/>
|
||||||
<CompDocEditableRow
|
<CompDocEditableRow
|
||||||
label="Address"
|
label="Address"
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export function CompDocPreviewDocument({
|
||||||
isGenerated={isGenerated}
|
isGenerated={isGenerated}
|
||||||
generatedAt={generatedAt}
|
generatedAt={generatedAt}
|
||||||
/>
|
/>
|
||||||
<CompDocPreviewVendorSafety row={row} />
|
<CompDocPreviewVendorSafety site={site} />
|
||||||
<CompDocPreviewProcedureNotes pm={row.pm} notes={notes} />
|
<CompDocPreviewProcedureNotes pm={row.pm} notes={notes} />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -72,10 +72,10 @@ export function CompDocPreviewServiceInfo({
|
||||||
margin: "2px 0",
|
margin: "2px 0",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Site ID — {row.site}
|
Site ID — {site.code}
|
||||||
</Text>
|
</Text>
|
||||||
<Text as="p" style={{ fontSize: 13, color: "#161E38", margin: "2px 0" }}>
|
<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>
|
||||||
<Text as="p" style={{ fontSize: 13, color: "#58595B", margin: "2px 0" }}>
|
<Text as="p" style={{ fontSize: 13, color: "#58595B", margin: "2px 0" }}>
|
||||||
{site.street},
|
{site.street},
|
||||||
|
|
|
||||||
|
|
@ -73,10 +73,10 @@ export function CompDocPreviewSummary({
|
||||||
margin: "2px 0",
|
margin: "2px 0",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Site ID — {row.site}
|
Site ID — {site.code}
|
||||||
</Text>
|
</Text>
|
||||||
<Text variant="body" sx={{ fontSize: 13, color: "#161E38", margin: "2px 0" }}>
|
<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>
|
||||||
<Text variant="body" sx={{ fontSize: 13, color: "#58595B", margin: "2px 0" }}>
|
<Text variant="body" sx={{ fontSize: 13, color: "#58595B", margin: "2px 0" }}>
|
||||||
{site.street},
|
{site.street},
|
||||||
|
|
@ -88,8 +88,8 @@ export function CompDocPreviewSummary({
|
||||||
|
|
||||||
<Text variant="body" sx={{ fontSize: 13, color: "#161E38", margin: "14px 0 6px" }}>
|
<Text variant="body" sx={{ fontSize: 13, color: "#161E38", margin: "14px 0 6px" }}>
|
||||||
<span style={{ fontWeight: 700 }}>Vendor / Technician: </span>
|
<span style={{ fontWeight: 700 }}>Vendor / Technician: </span>
|
||||||
{row.tech || "—"}
|
{site.tech || "—"}
|
||||||
{row.company ? ` · ${row.company}` : ""}
|
{site.company ? ` · ${site.company}` : ""}
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
import { Text } from "@/components/ui/text";
|
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";
|
import { SAFETY_BULLETS } from "@/domain/work-orders/utils/completion-doc-procedures";
|
||||||
|
|
||||||
type CompDocPreviewVendorSafetyProps = {
|
type CompDocPreviewVendorSafetyProps = {
|
||||||
row: WorkOrderTableRow;
|
site: CompDocSiteInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CompDocPreviewVendorSafety({ row }: CompDocPreviewVendorSafetyProps) {
|
export function CompDocPreviewVendorSafety({ site }: CompDocPreviewVendorSafetyProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Text as="p" style={{ fontSize: 13, color: "#161E38", margin: "14px 0 6px" }}>
|
<Text as="p" style={{ fontSize: 13, color: "#161E38", margin: "14px 0 6px" }}>
|
||||||
<span style={{ fontWeight: 700 }}>Vendor / Technician: </span>
|
<span style={{ fontWeight: 700 }}>Vendor / Technician: </span>
|
||||||
{row.tech || "—"}
|
{site.tech || "—"}
|
||||||
{row.company ? ` · ${row.company}` : ""}
|
{site.company ? ` · ${site.company}` : ""}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<ul style={{ listStyle: "disc", paddingLeft: 18, margin: "8px 0 0" }}>
|
<ul style={{ listStyle: "disc", paddingLeft: 18, margin: "8px 0 0" }}>
|
||||||
|
|
|
||||||
|
|
@ -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 { useUploadCompletionDoc } from "@/domain/work-orders/use-cases/use-work-order-mutations";
|
||||||
import { resolveCompDocSite } from "@/domain/work-orders/utils/completion-doc-site";
|
import { resolveCompDocSite } from "@/domain/work-orders/utils/completion-doc-site";
|
||||||
import { DOC_STATE_CFG } from "@/domain/work-orders/utils/completion-doc-state";
|
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 { isWorkOrderDocumentsReadOnly } from "@/domain/work-orders/utils/work-order-media-limits";
|
||||||
import { useCompDocDialogSeed } from "./use-comp-doc-dialog-seed";
|
import { useCompDocDialogSeed } from "./use-comp-doc-dialog-seed";
|
||||||
import { useCompDocNotRequiredActions } from "./use-comp-doc-not-required-actions";
|
import { useCompDocNotRequiredActions } from "./use-comp-doc-not-required-actions";
|
||||||
|
|
@ -61,7 +66,9 @@ export function useCompDocDialog({
|
||||||
onDateCompletedChange,
|
onDateCompletedChange,
|
||||||
}: UseCompDocDialogArgs) {
|
}: UseCompDocDialogArgs) {
|
||||||
const locId = String(row.locationId || "");
|
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 { data: boardDetail } = useWorkOrderBoardDetail(row.id, open);
|
||||||
const uploadCompletionMutation = useUploadCompletionDoc(row.id);
|
const uploadCompletionMutation = useUploadCompletionDoc(row.id);
|
||||||
const pdfInputRef = useRef<HTMLInputElement>(null);
|
const pdfInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
@ -93,8 +100,9 @@ export function useCompDocDialog({
|
||||||
setEditDateCompleted,
|
setEditDateCompleted,
|
||||||
});
|
});
|
||||||
|
|
||||||
const amazonRep = info.pocName || "";
|
const company = effectiveCompany(info);
|
||||||
const seaRep = info.tech || "";
|
const amazonRep = effectivePoc(info).contacts[0]?.name || "";
|
||||||
|
const seaRep = company.contactName || "";
|
||||||
const cfg = DOC_STATE_CFG[docState];
|
const cfg = DOC_STATE_CFG[docState];
|
||||||
const isNotRequired = docState === "not-required";
|
const isNotRequired = docState === "not-required";
|
||||||
const isGenerated = docState === "generated" || Boolean(signOffAttachment);
|
const isGenerated = docState === "generated" || Boolean(signOffAttachment);
|
||||||
|
|
@ -107,6 +115,9 @@ export function useCompDocDialog({
|
||||||
dateCompleted: editDateCompleted,
|
dateCompleted: editDateCompleted,
|
||||||
amazonRep,
|
amazonRep,
|
||||||
seaRep,
|
seaRep,
|
||||||
|
siteCode: site.code,
|
||||||
|
tech: company.contactName || "",
|
||||||
|
company: company.name || "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const { persist, syncDateIfChanged, refreshAuthoritativeState, handleSaveDraft, handleGenerate } =
|
const { persist, syncDateIfChanged, refreshAuthoritativeState, handleSaveDraft, handleGenerate } =
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,11 @@ import type {
|
||||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
import type { WOType } from "@/domain/work-orders/types/work-order-wizard";
|
import type { WOType } from "@/domain/work-orders/types/work-order-wizard";
|
||||||
import { formatApptRange, parseApptTime } from "@/domain/work-orders/utils/wizard-date-utils";
|
import { formatApptRange, parseApptTime } from "@/domain/work-orders/utils/wizard-date-utils";
|
||||||
|
import {
|
||||||
|
effectiveCompany,
|
||||||
|
effectivePoc,
|
||||||
|
effectiveSite,
|
||||||
|
} from "@/domain/work-orders/utils/effective-completion-snapshot";
|
||||||
import { DocBadge } from "./slide-over-doc-badge";
|
import { DocBadge } from "./slide-over-doc-badge";
|
||||||
import { SlideOverInfoTabAssignedField } from "./slide-over-info-tab-assigned-field";
|
import { SlideOverInfoTabAssignedField } from "./slide-over-info-tab-assigned-field";
|
||||||
import { SlideOverInfoTabStatusField } from "./slide-over-info-tab-status-field";
|
import { SlideOverInfoTabStatusField } from "./slide-over-info-tab-status-field";
|
||||||
|
|
@ -28,6 +33,153 @@ type SlideOverInfoTabDisplayViewProps = {
|
||||||
pmTypes: string[];
|
pmTypes: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FrozenPoc = ReturnType<typeof effectivePoc>;
|
||||||
|
|
||||||
|
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 <span style={{ color: "var(--color-text-muted)" }}>—</span>;
|
||||||
|
}
|
||||||
|
return <WoTableTypeLabel type={type as WOType} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SlideOverCompletedSite({
|
||||||
|
source,
|
||||||
|
poc,
|
||||||
|
sites,
|
||||||
|
}: {
|
||||||
|
source: WorkOrderTableRow;
|
||||||
|
poc: FrozenPoc;
|
||||||
|
sites: LocationOption[];
|
||||||
|
}) {
|
||||||
|
const primaryPoc = poc.contacts[0];
|
||||||
|
const frozenSite = effectiveSite(source);
|
||||||
|
return (
|
||||||
|
<SiteCell
|
||||||
|
value={frozenSite?.code ?? source.site}
|
||||||
|
location={frozenSite?.label ?? source.location}
|
||||||
|
locationId={source.locationId}
|
||||||
|
pocName={primaryPoc?.name ?? ""}
|
||||||
|
pocPhone={primaryPoc?.phone ?? ""}
|
||||||
|
pocNotes={poc.notes ?? ""}
|
||||||
|
additionalContacts={poc.contacts.slice(1).map((contact) => ({
|
||||||
|
name: contact.name ?? "",
|
||||||
|
phone: contact.phone ?? "",
|
||||||
|
notes: contact.notes ?? "",
|
||||||
|
}))}
|
||||||
|
sites={sites}
|
||||||
|
viewOnly
|
||||||
|
frozenSite={frozenSite}
|
||||||
|
onSave={() => undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SlideOverSiteField({
|
||||||
|
source,
|
||||||
|
isCompleted,
|
||||||
|
poc,
|
||||||
|
sites,
|
||||||
|
}: {
|
||||||
|
source: WorkOrderTableRow;
|
||||||
|
isCompleted: boolean;
|
||||||
|
poc: FrozenPoc;
|
||||||
|
sites: LocationOption[];
|
||||||
|
}) {
|
||||||
|
if (!isCompleted) {
|
||||||
|
return (
|
||||||
|
<span className="font-mono text-[12.5px] font-bold" style={{ color: "var(--foreground)" }}>
|
||||||
|
{source.site || "—"}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <SlideOverCompletedSite source={source} poc={poc} sites={sites} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SlideOverOpenService({ source }: { source: WorkOrderTableRow }) {
|
||||||
|
if (!source.pm) {
|
||||||
|
return <span style={{ color: "var(--color-text-muted)" }}>—</span>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center gap-1.5 text-[12.5px]">
|
||||||
|
<PmTypeIcon pmType={source.pm} size={14} />
|
||||||
|
{source.pm}
|
||||||
|
{Boolean(source.avetaRequired) && (
|
||||||
|
<AvetaBadge size={11} attached={Boolean(source.hasAvetaDocument)} />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SlideOverServiceField({
|
||||||
|
source,
|
||||||
|
isCompleted,
|
||||||
|
pmTypes,
|
||||||
|
}: {
|
||||||
|
source: WorkOrderTableRow;
|
||||||
|
isCompleted: boolean;
|
||||||
|
pmTypes: string[];
|
||||||
|
}) {
|
||||||
|
if (!isCompleted) return <SlideOverOpenService source={source} />;
|
||||||
|
return (
|
||||||
|
<PMTypeCell
|
||||||
|
value={source.pm}
|
||||||
|
pmTypes={pmTypes}
|
||||||
|
serviceNotes={source.serviceNotes}
|
||||||
|
extraServices={source.extraServices}
|
||||||
|
avetaRequired={source.avetaRequired}
|
||||||
|
avetaAttached={source.hasAvetaDocument}
|
||||||
|
woNumber={source.woNumber}
|
||||||
|
viewOnly
|
||||||
|
onSave={() => undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SlideOverVendorField({
|
||||||
|
source,
|
||||||
|
isCompleted,
|
||||||
|
vendors,
|
||||||
|
}: {
|
||||||
|
source: WorkOrderTableRow;
|
||||||
|
isCompleted: boolean;
|
||||||
|
vendors: VendorDropdownItem[];
|
||||||
|
}) {
|
||||||
|
if (!isCompleted) return <SlideOverInfoTabVendorField source={source} />;
|
||||||
|
const company = effectiveCompany(source);
|
||||||
|
return (
|
||||||
|
<VendorCell
|
||||||
|
vendorId={source.vendorId}
|
||||||
|
company={company.name ?? source.company}
|
||||||
|
tech={company.contactName ?? source.tech}
|
||||||
|
techPhone={company.phone ?? source.techPhone}
|
||||||
|
vendors={vendors}
|
||||||
|
viewOnly
|
||||||
|
frozenCompany={source.frozenCompany}
|
||||||
|
onSave={() => undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function SlideOverInfoTabDisplayView({
|
export function SlideOverInfoTabDisplayView({
|
||||||
source,
|
source,
|
||||||
isCancelled,
|
isCancelled,
|
||||||
|
|
@ -37,8 +189,7 @@ export function SlideOverInfoTabDisplayView({
|
||||||
vendors,
|
vendors,
|
||||||
pmTypes,
|
pmTypes,
|
||||||
}: SlideOverInfoTabDisplayViewProps) {
|
}: SlideOverInfoTabDisplayViewProps) {
|
||||||
const appt = parseApptTime(source.apptTime);
|
const poc = effectivePoc(source);
|
||||||
const apptDisplay = appt.start ? formatApptRange(appt.start, appt.end) : source.apptTime || "—";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={isCancelled ? { opacity: 0.6, pointerEvents: "none" } : undefined}>
|
<div style={isCancelled ? { opacity: 0.6, pointerEvents: "none" } : undefined}>
|
||||||
|
|
@ -46,34 +197,10 @@ export function SlideOverInfoTabDisplayView({
|
||||||
<span className="font-mono text-[12.5px] font-semibold">{source.woNumber || "—"}</span>
|
<span className="font-mono text-[12.5px] font-semibold">{source.woNumber || "—"}</span>
|
||||||
</SlideOverInfoRow>
|
</SlideOverInfoRow>
|
||||||
<SlideOverInfoRow label="Site">
|
<SlideOverInfoRow label="Site">
|
||||||
{isCompleted ? (
|
<SlideOverSiteField source={source} isCompleted={isCompleted} poc={poc} sites={sites} />
|
||||||
<SiteCell
|
|
||||||
value={source.site}
|
|
||||||
location={source.location}
|
|
||||||
locationId={source.locationId}
|
|
||||||
pocName={source.pocName}
|
|
||||||
pocPhone={source.pocPhone}
|
|
||||||
pocNotes={source.pocNotes}
|
|
||||||
additionalContacts={source.additionalContacts}
|
|
||||||
sites={sites}
|
|
||||||
viewOnly
|
|
||||||
onSave={() => undefined}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
className="font-mono text-[12.5px] font-bold"
|
|
||||||
style={{ color: "var(--foreground)" }}
|
|
||||||
>
|
|
||||||
{source.site || "—"}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</SlideOverInfoRow>
|
</SlideOverInfoRow>
|
||||||
<SlideOverInfoRow label="Type of WO">
|
<SlideOverInfoRow label="Type of WO">
|
||||||
{source.type ? (
|
<SlideOverTypeField type={source.type} />
|
||||||
<WoTableTypeLabel type={source.type as WOType} />
|
|
||||||
) : (
|
|
||||||
<span style={{ color: "var(--color-text-muted)" }}>—</span>
|
|
||||||
)}
|
|
||||||
</SlideOverInfoRow>
|
</SlideOverInfoRow>
|
||||||
<SlideOverInfoRow label="Assigned To">
|
<SlideOverInfoRow label="Assigned To">
|
||||||
<SlideOverInfoTabAssignedField
|
<SlideOverInfoTabAssignedField
|
||||||
|
|
@ -82,57 +209,19 @@ export function SlideOverInfoTabDisplayView({
|
||||||
/>
|
/>
|
||||||
</SlideOverInfoRow>
|
</SlideOverInfoRow>
|
||||||
<SlideOverInfoRow label="Schedule On">
|
<SlideOverInfoRow label="Schedule On">
|
||||||
<span className="text-[12.5px]">
|
<span className="text-[12.5px]">{formatScheduleOn(source)}</span>
|
||||||
{source.scheduleWeekOnly && source.targetWeek
|
|
||||||
? `Week of ${source.targetWeek}`
|
|
||||||
: source.scheduledOn || "—"}
|
|
||||||
</span>
|
|
||||||
</SlideOverInfoRow>
|
</SlideOverInfoRow>
|
||||||
<SlideOverInfoRow label="Due Date">
|
<SlideOverInfoRow label="Due Date">
|
||||||
<span className="text-[12.5px]">{source.dueDate || "—"}</span>
|
<span className="text-[12.5px]">{source.dueDate || "—"}</span>
|
||||||
</SlideOverInfoRow>
|
</SlideOverInfoRow>
|
||||||
<SlideOverInfoRow label="Appt Time">
|
<SlideOverInfoRow label="Appt Time">
|
||||||
<span className="text-[12.5px]">{apptDisplay}</span>
|
<span className="text-[12.5px]">{formatApptDisplay(source.apptTime)}</span>
|
||||||
</SlideOverInfoRow>
|
</SlideOverInfoRow>
|
||||||
<SlideOverInfoRow label="Service">
|
<SlideOverInfoRow label="Service">
|
||||||
{isCompleted ? (
|
<SlideOverServiceField source={source} isCompleted={isCompleted} pmTypes={pmTypes} />
|
||||||
<PMTypeCell
|
|
||||||
value={source.pm}
|
|
||||||
pmTypes={pmTypes}
|
|
||||||
serviceNotes={source.serviceNotes}
|
|
||||||
extraServices={source.extraServices}
|
|
||||||
avetaRequired={source.avetaRequired}
|
|
||||||
avetaAttached={source.hasAvetaDocument}
|
|
||||||
woNumber={source.woNumber}
|
|
||||||
viewOnly
|
|
||||||
onSave={() => undefined}
|
|
||||||
/>
|
|
||||||
) : source.pm ? (
|
|
||||||
<span className="inline-flex items-center gap-1.5 text-[12.5px]">
|
|
||||||
<PmTypeIcon pmType={source.pm} size={14} />
|
|
||||||
{source.pm}
|
|
||||||
{Boolean(source.avetaRequired) && (
|
|
||||||
<AvetaBadge size={11} attached={Boolean(source.hasAvetaDocument)} />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span style={{ color: "var(--color-text-muted)" }}>—</span>
|
|
||||||
)}
|
|
||||||
</SlideOverInfoRow>
|
</SlideOverInfoRow>
|
||||||
<SlideOverInfoRow label="Vendor">
|
<SlideOverInfoRow label="Vendor">
|
||||||
{isCompleted ? (
|
<SlideOverVendorField source={source} isCompleted={isCompleted} vendors={vendors} />
|
||||||
<VendorCell
|
|
||||||
vendorId={source.vendorId}
|
|
||||||
company={source.company}
|
|
||||||
tech={source.tech}
|
|
||||||
techPhone={source.techPhone}
|
|
||||||
vendors={vendors}
|
|
||||||
viewOnly
|
|
||||||
onSave={() => undefined}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<SlideOverInfoTabVendorField source={source} />
|
|
||||||
)}
|
|
||||||
</SlideOverInfoRow>
|
</SlideOverInfoRow>
|
||||||
<SlideOverInfoRow label="Status">
|
<SlideOverInfoRow label="Status">
|
||||||
<SlideOverInfoTabStatusField source={source} />
|
<SlideOverInfoTabStatusField source={source} />
|
||||||
|
|
@ -142,8 +231,7 @@ export function SlideOverInfoTabDisplayView({
|
||||||
</SlideOverInfoRow>
|
</SlideOverInfoRow>
|
||||||
<SlideOverInfoRow label="POC">
|
<SlideOverInfoRow label="POC">
|
||||||
<span className="text-[12.5px]" style={{ color: "var(--color-text-secondary)" }}>
|
<span className="text-[12.5px]" style={{ color: "var(--color-text-secondary)" }}>
|
||||||
{source.pocName || "—"}
|
{formatPocLine(poc)}
|
||||||
{source.pocPhone ? ` · ${source.pocPhone}` : ""}
|
|
||||||
</span>
|
</span>
|
||||||
</SlideOverInfoRow>
|
</SlideOverInfoRow>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { MapPin } from "lucide-react";
|
||||||
import { formatSitePlace } from "@/domain/locations/mappers/location-mapper";
|
import { formatSitePlace } from "@/domain/locations/mappers/location-mapper";
|
||||||
import type { WorkOrderAdditionalContact } from "@/domain/work-orders/types/work-order-additional-contact";
|
import type { WorkOrderAdditionalContact } from "@/domain/work-orders/types/work-order-additional-contact";
|
||||||
import type { LocationOption } from "@/domain/work-orders/types/work-order";
|
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 { HighlightText } from "./highlight-text";
|
||||||
import { HoverChevron } from "./hover-chevron";
|
import { HoverChevron } from "./hover-chevron";
|
||||||
import { SiteDialog, type SitePatch } from "./site-dialog";
|
import { SiteDialog, type SitePatch } from "./site-dialog";
|
||||||
|
|
@ -22,6 +23,7 @@ type SiteCellProps = {
|
||||||
/** Inline create: editable POC + Site/POC required to confirm the modal. */
|
/** Inline create: editable POC + Site/POC required to confirm the modal. */
|
||||||
createMode?: boolean;
|
createMode?: boolean;
|
||||||
viewOnly?: boolean;
|
viewOnly?: boolean;
|
||||||
|
frozenSite?: WorkOrderFrozenSite;
|
||||||
onSave: (patch: SitePatch) => void;
|
onSave: (patch: SitePatch) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -39,10 +41,15 @@ export function SiteCell({
|
||||||
q,
|
q,
|
||||||
createMode = false,
|
createMode = false,
|
||||||
viewOnly = false,
|
viewOnly = false,
|
||||||
|
frozenSite,
|
||||||
onSave,
|
onSave,
|
||||||
}: SiteCellProps) {
|
}: SiteCellProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const place = formatSitePlace(city, state, location);
|
const place = formatSitePlace(
|
||||||
|
frozenSite?.city ?? city,
|
||||||
|
frozenSite?.state ?? state,
|
||||||
|
frozenSite?.label ?? location,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -102,6 +109,7 @@ export function SiteCell({
|
||||||
sites={sites}
|
sites={sites}
|
||||||
createMode={createMode}
|
createMode={createMode}
|
||||||
viewOnly={viewOnly}
|
viewOnly={viewOnly}
|
||||||
|
frozenSite={frozenSite}
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { Text } from "@/components/ui/text";
|
||||||
import { dialogBlurBackdropSlotProps } from "@/components/ui/dialog-blur-backdrop";
|
import { dialogBlurBackdropSlotProps } from "@/components/ui/dialog-blur-backdrop";
|
||||||
import type { WorkOrderAdditionalContact } from "@/domain/work-orders/types/work-order-additional-contact";
|
import type { WorkOrderAdditionalContact } from "@/domain/work-orders/types/work-order-additional-contact";
|
||||||
import type { LocationOption } from "@/domain/work-orders/types/work-order";
|
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 { 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 { 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";
|
import { SiteDialogFooter } from "@/app/(protected)/workorders/_components/list/table/cells/site-dialog-footer";
|
||||||
|
|
@ -27,6 +28,7 @@ type SiteDialogProps = {
|
||||||
sites: LocationOption[];
|
sites: LocationOption[];
|
||||||
createMode?: boolean;
|
createMode?: boolean;
|
||||||
viewOnly?: boolean;
|
viewOnly?: boolean;
|
||||||
|
frozenSite?: WorkOrderFrozenSite;
|
||||||
onSave: (patch: SitePatch) => void;
|
onSave: (patch: SitePatch) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -42,6 +44,7 @@ export function SiteDialog({
|
||||||
sites,
|
sites,
|
||||||
createMode = false,
|
createMode = false,
|
||||||
viewOnly = false,
|
viewOnly = false,
|
||||||
|
frozenSite,
|
||||||
onSave,
|
onSave,
|
||||||
}: SiteDialogProps) {
|
}: SiteDialogProps) {
|
||||||
const s = useSiteDialogState({
|
const s = useSiteDialogState({
|
||||||
|
|
@ -55,6 +58,7 @@ export function SiteDialog({
|
||||||
additionalContacts,
|
additionalContacts,
|
||||||
sites,
|
sites,
|
||||||
createMode,
|
createMode,
|
||||||
|
frozenSite,
|
||||||
onSave,
|
onSave,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 { 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 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";
|
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 { formatLocationAddressPreview } from "@/domain/locations/mappers/location-mapper";
|
||||||
import type { WorkOrderAdditionalContact } from "@/domain/work-orders/types/work-order-additional-contact";
|
import type { WorkOrderAdditionalContact } from "@/domain/work-orders/types/work-order-additional-contact";
|
||||||
import type { LocationOption } from "@/domain/work-orders/types/work-order";
|
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 };
|
export type { SitePatch };
|
||||||
|
|
||||||
|
|
@ -20,6 +21,7 @@ type UseSiteDialogStateArgs = {
|
||||||
additionalContacts: WorkOrderAdditionalContact[];
|
additionalContacts: WorkOrderAdditionalContact[];
|
||||||
sites: LocationOption[];
|
sites: LocationOption[];
|
||||||
createMode: boolean;
|
createMode: boolean;
|
||||||
|
frozenSite?: WorkOrderFrozenSite;
|
||||||
onSave: (patch: SitePatch) => void;
|
onSave: (patch: SitePatch) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -34,6 +36,7 @@ export function useSiteDialogState({
|
||||||
additionalContacts = [],
|
additionalContacts = [],
|
||||||
sites,
|
sites,
|
||||||
createMode,
|
createMode,
|
||||||
|
frozenSite,
|
||||||
onSave,
|
onSave,
|
||||||
}: UseSiteDialogStateArgs) {
|
}: UseSiteDialogStateArgs) {
|
||||||
const fields = useSiteDialogFormFields({
|
const fields = useSiteDialogFormFields({
|
||||||
|
|
@ -49,23 +52,42 @@ export function useSiteDialogState({
|
||||||
});
|
});
|
||||||
const { locId, pocFilledFor, pn, pp, setPn, setPp, setPocFilledFor, siteMissing } = fields;
|
const { locId, pocFilledFor, pn, pp, setPn, setPp, setPocFilledFor, siteMissing } = fields;
|
||||||
const {
|
const {
|
||||||
data: locationDetail,
|
data: liveLocationDetail,
|
||||||
isLoading: locationDetailLoading,
|
isLoading: liveLocationDetailLoading,
|
||||||
isError: locationDetailError,
|
isError: liveLocationDetailError,
|
||||||
} = useLocationDetail(open && locId ? locId : undefined);
|
} = 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 addressPreview = locationDetail ? formatLocationAddressPreview(locationDetail) : "";
|
||||||
const pocMissing = !pn.trim() || !pp.trim();
|
const pocMissing = !pn.trim() || !pp.trim();
|
||||||
const canConfirm = !siteMissing && (!createMode || !pocMissing);
|
const canConfirm = !siteMissing && (!createMode || !pocMissing);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !locId || !locationDetail || pocFilledFor === locId) {
|
if (frozenSite != null || !open || !locId || !locationDetail || pocFilledFor === locId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setPn((prev) => (prev.trim() ? prev : (locationDetail.contact ?? "")));
|
setPn((prev) => (prev.trim() ? prev : (locationDetail.contact ?? "")));
|
||||||
setPp((prev) => (prev.trim() ? prev : (locationDetail.phone ?? "")));
|
setPp((prev) => (prev.trim() ? prev : (locationDetail.phone ?? "")));
|
||||||
setPocFilledFor(locId);
|
setPocFilledFor(locId);
|
||||||
}, [open, locId, locationDetail, pocFilledFor, setPn, setPp, setPocFilledFor]);
|
}, [open, locId, locationDetail, pocFilledFor, setPn, setPp, setPocFilledFor, frozenSite]);
|
||||||
|
|
||||||
const attemptSave = () => {
|
const attemptSave = () => {
|
||||||
if (createMode && !canConfirm) {
|
if (createMode && !canConfirm) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order";
|
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 { HighlightText } from "./highlight-text";
|
||||||
import { HoverChevron } from "./hover-chevron";
|
import { HoverChevron } from "./hover-chevron";
|
||||||
import { VendorDialog, type VendorPatch } from "./vendor-dialog";
|
import { VendorDialog, type VendorPatch } from "./vendor-dialog";
|
||||||
|
|
@ -12,6 +13,7 @@ type VendorCellProps = {
|
||||||
vendors: VendorDropdownItem[];
|
vendors: VendorDropdownItem[];
|
||||||
q?: string;
|
q?: string;
|
||||||
viewOnly?: boolean;
|
viewOnly?: boolean;
|
||||||
|
frozenCompany?: WorkOrderFrozenCompany;
|
||||||
onSave: (patch: VendorPatch) => void;
|
onSave: (patch: VendorPatch) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -23,6 +25,7 @@ export function VendorCell({
|
||||||
vendors,
|
vendors,
|
||||||
q,
|
q,
|
||||||
viewOnly = false,
|
viewOnly = false,
|
||||||
|
frozenCompany,
|
||||||
onSave,
|
onSave,
|
||||||
}: VendorCellProps) {
|
}: VendorCellProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
@ -63,6 +66,7 @@ export function VendorCell({
|
||||||
techPhone={techPhone}
|
techPhone={techPhone}
|
||||||
vendors={vendors}
|
vendors={vendors}
|
||||||
viewOnly={viewOnly}
|
viewOnly={viewOnly}
|
||||||
|
frozenCompany={frozenCompany}
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import type {
|
||||||
CompanyOption,
|
CompanyOption,
|
||||||
TechnicianOption,
|
TechnicianOption,
|
||||||
} from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-options";
|
} 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 = {
|
type VendorDialogBodyProps = {
|
||||||
selectedCompany: string;
|
selectedCompany: string;
|
||||||
|
|
@ -27,6 +28,7 @@ type VendorDialogBodyProps = {
|
||||||
notes: string;
|
notes: string;
|
||||||
resolvedCompany: string;
|
resolvedCompany: string;
|
||||||
selectedVendor: VendorDropdownItem | undefined;
|
selectedVendor: VendorDropdownItem | undefined;
|
||||||
|
frozenCompany?: WorkOrderFrozenCompany;
|
||||||
saveError: string | null;
|
saveError: string | null;
|
||||||
onCompanyPick: (company: string) => void;
|
onCompanyPick: (company: string) => void;
|
||||||
onPick: (key: string) => void;
|
onPick: (key: string) => void;
|
||||||
|
|
@ -51,6 +53,7 @@ export function VendorDialogBody({
|
||||||
notes,
|
notes,
|
||||||
resolvedCompany,
|
resolvedCompany,
|
||||||
selectedVendor,
|
selectedVendor,
|
||||||
|
frozenCompany,
|
||||||
saveError,
|
saveError,
|
||||||
onCompanyPick,
|
onCompanyPick,
|
||||||
onPick,
|
onPick,
|
||||||
|
|
@ -85,27 +88,41 @@ export function VendorDialogBody({
|
||||||
{Boolean(resolvedCompany) && (
|
{Boolean(resolvedCompany) && (
|
||||||
<div>
|
<div>
|
||||||
<FieldLabel>Company info</FieldLabel>
|
<FieldLabel>Company info</FieldLabel>
|
||||||
<VendorDialogCompanyCard company={resolvedCompany} vendor={selectedVendor} />
|
<VendorDialogCompanyCard
|
||||||
|
company={resolvedCompany}
|
||||||
|
vendor={selectedVendor}
|
||||||
|
frozenCompany={frozenCompany}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<FieldLabel>Technician (optional)</FieldLabel>
|
<FieldLabel>Technician (optional)</FieldLabel>
|
||||||
<WizardFieldSelect
|
{frozenCompany != null ? (
|
||||||
value={selectedKey}
|
<div className={DLG_INPUT_CLS} style={DLG_INPUT_STYLE}>
|
||||||
placeholder={selectedCompany ? "Select technician …" : "Pick a company first"}
|
{frozenCompany.contactName?.trim() || tech || "—"}
|
||||||
disabled={!selectedCompany}
|
</div>
|
||||||
icon={
|
) : (
|
||||||
<Wrench size={14} className="shrink-0" style={{ color: "var(--color-text-muted)" }} />
|
<WizardFieldSelect
|
||||||
}
|
value={selectedKey}
|
||||||
searchPlaceholder="Search technician…"
|
placeholder={selectedCompany ? "Select technician …" : "Pick a company first"}
|
||||||
options={technicianOptions.map((option) => ({
|
disabled={!selectedCompany}
|
||||||
key: option.key,
|
icon={
|
||||||
label: option.name,
|
<Wrench
|
||||||
}))}
|
size={14}
|
||||||
onPick={onPick}
|
className="shrink-0"
|
||||||
/>
|
style={{ color: "var(--color-text-muted)" }}
|
||||||
{companyVendorForAdd != null && (
|
/>
|
||||||
|
}
|
||||||
|
searchPlaceholder="Search technician…"
|
||||||
|
options={technicianOptions.map((option) => ({
|
||||||
|
key: option.key,
|
||||||
|
label: option.name,
|
||||||
|
}))}
|
||||||
|
onPick={onPick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{frozenCompany == null && companyVendorForAdd != null && (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<VendorDialogAddTechnician
|
<VendorDialogAddTechnician
|
||||||
companyName={anchorCompanyName}
|
companyName={anchorCompanyName}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,96 @@
|
||||||
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 { Detail } from "@/app/(protected)/workorders/_components/wizard/wizard-labels";
|
||||||
import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order";
|
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";
|
import { formatVendorDistance } from "@/domain/work-orders/utils/vendor-dropdown-label";
|
||||||
|
|
||||||
type VendorDialogCompanyCardProps = {
|
type VendorDialogCompanyCardProps = {
|
||||||
company: string;
|
company: string;
|
||||||
vendor: VendorDropdownItem | undefined;
|
vendor: VendorDropdownItem | undefined;
|
||||||
|
frozenCompany?: WorkOrderFrozenCompany;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function VendorDialogCompanyCard({ company, vendor }: VendorDialogCompanyCardProps) {
|
type CompanyCardModel = {
|
||||||
const distance = formatVendorDistance(vendor?.distanceMiles);
|
title: string;
|
||||||
const email = vendor?.email?.trim() || "";
|
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())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
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) && <Detail icon={MapPin} text={model.address} />}
|
||||||
|
{Boolean(model.contactName) && <Detail icon={User} text={model.contactName} />}
|
||||||
|
{Boolean(model.phone) && <Detail icon={Phone} text={model.phone} />}
|
||||||
|
{Boolean(model.email) && <Detail icon={Mail} text={model.email} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VendorDialogCompanyCard({
|
||||||
|
company,
|
||||||
|
vendor,
|
||||||
|
frozenCompany,
|
||||||
|
}: VendorDialogCompanyCardProps) {
|
||||||
|
const model = resolveCompanyCardModel(company, vendor, frozenCompany);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -22,19 +102,18 @@ export function VendorDialogCompanyCard({ company, vendor }: VendorDialogCompany
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<div style={{ fontSize: 13, fontWeight: 500, color: "var(--foreground)" }}>
|
<div style={{ fontSize: 13, fontWeight: 500, color: "var(--foreground)" }}>
|
||||||
{company || "—"}
|
{model.title || "—"}
|
||||||
</div>
|
</div>
|
||||||
{Boolean(distance) && (
|
{Boolean(model.distance) && (
|
||||||
<span className="shrink-0 text-[11px]" style={{ color: "var(--muted-foreground)" }}>
|
<span className="shrink-0 text-[11px]" style={{ color: "var(--muted-foreground)" }}>
|
||||||
{distance}
|
{model.distance}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{Boolean(vendor?.address) && <Detail icon={MapPin} text={vendor?.address ?? ""} />}
|
<CompanyCardDetails model={model} />
|
||||||
{Boolean(email) && <Detail icon={Mail} text={email} />}
|
{Boolean(model.mapsUrl) && (
|
||||||
{vendor != null && Boolean(vendor.address) && (
|
|
||||||
<a
|
<a
|
||||||
href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(vendor.address)}`}
|
href={model.mapsUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="inline-flex items-center gap-1.5 text-[12px] hover:underline"
|
className="inline-flex items-center gap-1.5 text-[12px] hover:underline"
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { VendorDialogBody } from "@/app/(protected)/workorders/_components/list/
|
||||||
import { VendorDialogFooter } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-footer";
|
import { VendorDialogFooter } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-footer";
|
||||||
import { useVendorDialogForm } from "@/app/(protected)/workorders/_components/list/table/cells/use-vendor-dialog-form";
|
import { useVendorDialogForm } from "@/app/(protected)/workorders/_components/list/table/cells/use-vendor-dialog-form";
|
||||||
import type { VendorPatch } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-types";
|
import type { VendorPatch } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-types";
|
||||||
|
import type { WorkOrderFrozenCompany } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
|
||||||
export type { VendorPatch } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-types";
|
export type { VendorPatch } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-types";
|
||||||
|
|
||||||
|
|
@ -19,6 +20,7 @@ type VendorDialogProps = {
|
||||||
techPhone?: string;
|
techPhone?: string;
|
||||||
vendors: VendorDropdownItem[];
|
vendors: VendorDropdownItem[];
|
||||||
viewOnly?: boolean;
|
viewOnly?: boolean;
|
||||||
|
frozenCompany?: WorkOrderFrozenCompany;
|
||||||
onSave: (patch: VendorPatch) => void;
|
onSave: (patch: VendorPatch) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -31,6 +33,7 @@ export function VendorDialog({
|
||||||
techPhone = "",
|
techPhone = "",
|
||||||
vendors,
|
vendors,
|
||||||
viewOnly = false,
|
viewOnly = false,
|
||||||
|
frozenCompany,
|
||||||
onSave,
|
onSave,
|
||||||
}: VendorDialogProps) {
|
}: VendorDialogProps) {
|
||||||
const form = useVendorDialogForm({
|
const form = useVendorDialogForm({
|
||||||
|
|
@ -74,11 +77,12 @@ export function VendorDialog({
|
||||||
technicianOptions={form.technicianOptions}
|
technicianOptions={form.technicianOptions}
|
||||||
anchorCompanyName={form.anchorCompanyName}
|
anchorCompanyName={form.anchorCompanyName}
|
||||||
companyVendorForAdd={viewOnly ? undefined : (form.companyVendor ?? undefined)}
|
companyVendorForAdd={viewOnly ? undefined : (form.companyVendor ?? undefined)}
|
||||||
tech={form.selected?.name ?? form.t}
|
tech={frozenCompany?.contactName ?? form.selected?.name ?? form.t}
|
||||||
phone={form.phone}
|
phone={frozenCompany?.phone ?? form.phone}
|
||||||
notes={form.notes}
|
notes={form.notes}
|
||||||
resolvedCompany={form.resolvedCompany}
|
resolvedCompany={form.resolvedCompany}
|
||||||
selectedVendor={form.selectedVendor}
|
selectedVendor={form.selectedVendor}
|
||||||
|
frozenCompany={frozenCompany}
|
||||||
saveError={form.saveError}
|
saveError={form.saveError}
|
||||||
onCompanyPick={form.handleCompanyPick}
|
onCompanyPick={form.handleCompanyPick}
|
||||||
onPick={form.handlePick}
|
onPick={form.handlePick}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,11 @@ import type { LocationOption, WorkOrderUser } from "@/domain/work-orders/types/w
|
||||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
import { applyAssignedToTableSave } from "@/domain/work-orders/utils/work-order-feedback-toasts";
|
import { applyAssignedToTableSave } from "@/domain/work-orders/utils/work-order-feedback-toasts";
|
||||||
import { isWorkOrderCompletionSurfaceReadOnly } from "@/domain/work-orders/utils/work-order-lock";
|
import { isWorkOrderCompletionSurfaceReadOnly } from "@/domain/work-orders/utils/work-order-lock";
|
||||||
|
import {
|
||||||
|
effectiveAdditionalContacts,
|
||||||
|
effectivePoc,
|
||||||
|
effectiveSite,
|
||||||
|
} from "@/domain/work-orders/utils/effective-completion-snapshot";
|
||||||
import { saveExistingRowType } from "./save-existing-row-type";
|
import { saveExistingRowType } from "./save-existing-row-type";
|
||||||
import { AssignedToCell } from "./cells/assigned-to-cell";
|
import { AssignedToCell } from "./cells/assigned-to-cell";
|
||||||
import { DateCell } from "./cells/date-cell";
|
import { DateCell } from "./cells/date-cell";
|
||||||
|
|
@ -45,6 +50,9 @@ export function WoTableRowCells({
|
||||||
}: WoTableRowCellsProps) {
|
}: WoTableRowCellsProps) {
|
||||||
const hasSchedule =
|
const hasSchedule =
|
||||||
Boolean(row.scheduledOn) || Boolean(row.targetWeek) || Boolean(row.scheduleEnd);
|
Boolean(row.scheduledOn) || Boolean(row.targetWeek) || Boolean(row.scheduleEnd);
|
||||||
|
const frozenSite = effectiveSite(row);
|
||||||
|
const poc = effectivePoc(row);
|
||||||
|
const primaryPoc = poc.contacts[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -54,18 +62,19 @@ export function WoTableRowCells({
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<SiteCell
|
<SiteCell
|
||||||
value={row.site}
|
value={frozenSite?.code ?? row.site}
|
||||||
location={row.location}
|
location={frozenSite?.label ?? row.location}
|
||||||
locationId={row.locationId}
|
locationId={row.locationId}
|
||||||
city={sitePlace?.city}
|
city={sitePlace?.city}
|
||||||
state={sitePlace?.state}
|
state={sitePlace?.state}
|
||||||
pocName={row.pocName}
|
pocName={primaryPoc?.name ?? ""}
|
||||||
pocPhone={row.pocPhone}
|
pocPhone={primaryPoc?.phone ?? ""}
|
||||||
pocNotes={row.pocNotes}
|
pocNotes={poc.notes ?? ""}
|
||||||
additionalContacts={row.additionalContacts}
|
additionalContacts={effectiveAdditionalContacts(row)}
|
||||||
sites={sites}
|
sites={sites}
|
||||||
q={search}
|
q={search}
|
||||||
viewOnly={viewOnly}
|
viewOnly={viewOnly}
|
||||||
|
frozenSite={frozenSite}
|
||||||
onSave={(p) =>
|
onSave={(p) =>
|
||||||
onPatchRow({
|
onPatchRow({
|
||||||
site: p.site,
|
site: p.site,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { VendorCell } from "./cells/vendor-cell";
|
||||||
import { EMPTY_UPLIFT_SUMMARY } from "@/domain/work-orders/types/work-order-uplift";
|
import { EMPTY_UPLIFT_SUMMARY } from "@/domain/work-orders/types/work-order-uplift";
|
||||||
import { canOpenUpliftsDialog } from "@/domain/work-orders/utils/uplift-display-utils";
|
import { canOpenUpliftsDialog } from "@/domain/work-orders/utils/uplift-display-utils";
|
||||||
import { applyVendorTableSave } from "@/domain/work-orders/utils/work-order-feedback-toasts";
|
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";
|
import type { WoTableRowHandlers } from "./wo-table-row";
|
||||||
|
|
||||||
type WoTableRowServiceCellsProps = {
|
type WoTableRowServiceCellsProps = {
|
||||||
|
|
@ -36,6 +37,8 @@ export function WoTableRowServiceCells({
|
||||||
handlers,
|
handlers,
|
||||||
onPatchRow,
|
onPatchRow,
|
||||||
}: WoTableRowServiceCellsProps) {
|
}: WoTableRowServiceCellsProps) {
|
||||||
|
const company = effectiveCompany(row);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<td
|
<td
|
||||||
|
|
@ -71,12 +74,13 @@ export function WoTableRowServiceCells({
|
||||||
>
|
>
|
||||||
<VendorCell
|
<VendorCell
|
||||||
vendorId={row.vendorId}
|
vendorId={row.vendorId}
|
||||||
company={row.company}
|
company={company.name ?? row.company}
|
||||||
tech={row.tech}
|
tech={company.contactName ?? row.tech}
|
||||||
techPhone={row.techPhone}
|
techPhone={company.phone ?? row.techPhone}
|
||||||
vendors={vendors}
|
vendors={vendors}
|
||||||
q={search}
|
q={search}
|
||||||
viewOnly={viewOnly}
|
viewOnly={viewOnly}
|
||||||
|
frozenCompany={row.status === "Completed" ? row.frozenCompany : undefined}
|
||||||
onSave={(p) => applyVendorTableSave(handlers.onPatch, row.id, p)}
|
onSave={(p) => applyVendorTableSave(handlers.onPatch, row.id, p)}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import type {
|
||||||
} from "@/domain/work-orders/types/work-order";
|
} from "@/domain/work-orders/types/work-order";
|
||||||
import type { WorkOrderTablePatchFn } from "@/domain/work-orders/use-cases/use-work-order-table-mutations";
|
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 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 { DuplicateWoNumberDialog } from "@/app/(protected)/workorders/_components/list/duplicate-wo-number-dialog";
|
||||||
import { WoTableSkeleton } from "@/app/(protected)/workorders/_components/loading/wo-table-skeleton";
|
import { WoTableSkeleton } from "@/app/(protected)/workorders/_components/loading/wo-table-skeleton";
|
||||||
import { WoDayPaginationFooter } from "./wo-day-pagination-footer";
|
import { WoDayPaginationFooter } from "./wo-day-pagination-footer";
|
||||||
|
|
@ -136,7 +137,9 @@ export function WoTable({
|
||||||
allRows={tableData.rows}
|
allRows={tableData.rows}
|
||||||
users={users}
|
users={users}
|
||||||
sites={sites}
|
sites={sites}
|
||||||
sitePlace={resolveSitePlace(row.locationId, row.site, sitePlaceLookups)}
|
sitePlace={
|
||||||
|
effectiveSitePlace(row) ?? resolveSitePlace(row.locationId, row.site, sitePlaceLookups)
|
||||||
|
}
|
||||||
vendors={vendors}
|
vendors={vendors}
|
||||||
pmTypes={pmTypes}
|
pmTypes={pmTypes}
|
||||||
search={tableData.search}
|
search={tableData.search}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ type CreateCompDocDialogActionsParams = {
|
||||||
site: CompDocSite;
|
site: CompDocSite;
|
||||||
locId: string;
|
locId: string;
|
||||||
isMockLocation: boolean;
|
isMockLocation: boolean;
|
||||||
|
isSiteFrozen: boolean;
|
||||||
locationDetail: Location | undefined;
|
locationDetail: Location | undefined;
|
||||||
queryClient: QueryClient;
|
queryClient: QueryClient;
|
||||||
onSave: (data: CompDocData, status: CompDocSaveStatus) => void | Promise<void>;
|
onSave: (data: CompDocData, status: CompDocSaveStatus) => void | Promise<void>;
|
||||||
|
|
@ -58,6 +59,7 @@ export function createCompDocDialogActions({
|
||||||
site,
|
site,
|
||||||
locId,
|
locId,
|
||||||
isMockLocation,
|
isMockLocation,
|
||||||
|
isSiteFrozen,
|
||||||
locationDetail,
|
locationDetail,
|
||||||
queryClient,
|
queryClient,
|
||||||
onSave,
|
onSave,
|
||||||
|
|
@ -79,7 +81,7 @@ export function createCompDocDialogActions({
|
||||||
const next = editAddress.trim();
|
const next = editAddress.trim();
|
||||||
const current = buildCompDocAddressLine(site);
|
const current = buildCompDocAddressLine(site);
|
||||||
if (next === current) return;
|
if (next === current) return;
|
||||||
if (isMockLocation || !locId) return;
|
if (isSiteFrozen || isMockLocation || !locId) return;
|
||||||
if (!locationDetail) {
|
if (!locationDetail) {
|
||||||
throw new Error("Location details are still loading. Try saving again.");
|
throw new Error("Location details are still loading. Try saving again.");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 { getMockWoWizardLocationDetail } from "@/domain/work-orders/mocks/wo-wizard-mock-data";
|
||||||
import { resolveCompDocSite } from "@/domain/work-orders/utils/completion-doc-site";
|
import { resolveCompDocSite } from "@/domain/work-orders/utils/completion-doc-site";
|
||||||
import { DOC_STATE_CFG, docStateOf } from "@/domain/work-orders/utils/completion-doc-state";
|
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 = {
|
type UseCompDocDialogParams = {
|
||||||
row: WorkOrderTableRow;
|
row: WorkOrderTableRow;
|
||||||
|
|
@ -34,8 +39,9 @@ export function useCompDocDialog({
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const locId = String(row.locationId || "");
|
const locId = String(row.locationId || "");
|
||||||
const isMockLocation = locId.startsWith("mock-");
|
const isMockLocation = locId.startsWith("mock-");
|
||||||
|
const frozenSite = effectiveSite(row);
|
||||||
const { data: apiLocationDetail } = useLocationDetail(
|
const { data: apiLocationDetail } = useLocationDetail(
|
||||||
isMockLocation || !locId ? undefined : locId,
|
frozenSite != null || isMockLocation || !locId ? undefined : locId,
|
||||||
);
|
);
|
||||||
const locationDetail = isMockLocation ? getMockWoWizardLocationDetail(locId) : apiLocationDetail;
|
const locationDetail = isMockLocation ? getMockWoWizardLocationDetail(locId) : apiLocationDetail;
|
||||||
const site = useMemo(() => resolveCompDocSite(row, locationDetail), [row, locationDetail]);
|
const site = useMemo(() => resolveCompDocSite(row, locationDetail), [row, locationDetail]);
|
||||||
|
|
@ -75,22 +81,33 @@ export function useCompDocDialog({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const awaitingLocation = Boolean(locId) && !isMockLocation && !locationDetail;
|
const awaitingLocation =
|
||||||
|
frozenSite == null && Boolean(locId) && !isMockLocation && !locationDetail;
|
||||||
if (awaitingLocation) {
|
if (awaitingLocation) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setEditAddress(buildCompDocAddressLine(resolveCompDocSite(row, locationDetail)));
|
setEditAddress(buildCompDocAddressLine(resolveCompDocSite(row, locationDetail)));
|
||||||
addressSeededRef.current = true;
|
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) => {
|
const handleAddressChange = (value: string) => {
|
||||||
addressTouchedRef.current = true;
|
addressTouchedRef.current = true;
|
||||||
setEditAddress(value);
|
setEditAddress(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const amazonRep = row.pocName || "";
|
const company = effectiveCompany(row);
|
||||||
const seaRep = row.tech || "";
|
const amazonRep = effectivePoc(row).contacts[0]?.name || "";
|
||||||
|
const seaRep = company.contactName || "";
|
||||||
const cfg = DOC_STATE_CFG[docState];
|
const cfg = DOC_STATE_CFG[docState];
|
||||||
const isNotRequired = docState === "not-required";
|
const isNotRequired = docState === "not-required";
|
||||||
const isGenerated = docState === "generated";
|
const isGenerated = docState === "generated";
|
||||||
|
|
@ -104,6 +121,9 @@ export function useCompDocDialog({
|
||||||
dateCompleted: editDateCompleted,
|
dateCompleted: editDateCompleted,
|
||||||
amazonRep,
|
amazonRep,
|
||||||
seaRep,
|
seaRep,
|
||||||
|
siteCode: site.code,
|
||||||
|
tech: company.contactName || "",
|
||||||
|
company: company.name || "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const previewSite = useMemo(
|
const previewSite = useMemo(
|
||||||
|
|
@ -129,6 +149,7 @@ export function useCompDocDialog({
|
||||||
site,
|
site,
|
||||||
locId,
|
locId,
|
||||||
isMockLocation,
|
isMockLocation,
|
||||||
|
isSiteFrozen: frozenSite != null,
|
||||||
locationDetail,
|
locationDetail,
|
||||||
queryClient,
|
queryClient,
|
||||||
onSave,
|
onSave,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ import {
|
||||||
} from "@/domain/work-orders/types/work-order-board";
|
} from "@/domain/work-orders/types/work-order-board";
|
||||||
import type {
|
import type {
|
||||||
CompletionDocStatus,
|
CompletionDocStatus,
|
||||||
|
WorkOrderFrozenCompany,
|
||||||
|
WorkOrderFrozenPoc,
|
||||||
|
WorkOrderFrozenSite,
|
||||||
WorkOrderTableRow,
|
WorkOrderTableRow,
|
||||||
WorkOrderUpliftSummary,
|
WorkOrderUpliftSummary,
|
||||||
} from "@/domain/work-orders/types/work-order-table-row";
|
} from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
|
@ -271,6 +274,55 @@ function readPrimaryDispatchStatus(item: Record<string, unknown>): string {
|
||||||
return readString(item, "primaryDispatchStatus", "PrimaryDispatchStatus");
|
return readString(item, "primaryDispatchStatus", "PrimaryDispatchStatus");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readFrozenSite(item: Record<string, unknown>): 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<string, unknown>): 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<string, unknown>): 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"),
|
||||||
|
notes: readString(contact, "notes", "Notes"),
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
return {
|
||||||
|
contacts,
|
||||||
|
notes: readString(source, "notes", "Notes"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function mapWorkOrderTableRow(raw: unknown): WorkOrderTableRow {
|
export function mapWorkOrderTableRow(raw: unknown): WorkOrderTableRow {
|
||||||
const item = asRecord(raw);
|
const item = asRecord(raw);
|
||||||
const scheduledOn = toDateInputValue(
|
const scheduledOn = toDateInputValue(
|
||||||
|
|
@ -364,6 +416,9 @@ export function mapWorkOrderTableRow(raw: unknown): WorkOrderTableRow {
|
||||||
pocPhone: readString(item, "pocPhone", "PocPhone", "POCPhone"),
|
pocPhone: readString(item, "pocPhone", "PocPhone", "POCPhone"),
|
||||||
pocNotes: readString(item, "pocNotes", "PocNotes", "POCNotes"),
|
pocNotes: readString(item, "pocNotes", "PocNotes", "POCNotes"),
|
||||||
additionalContacts: readAdditionalContacts(item),
|
additionalContacts: readAdditionalContacts(item),
|
||||||
|
frozenSite: readFrozenSite(item),
|
||||||
|
frozenCompany: readFrozenCompany(item),
|
||||||
|
frozenPoc: readFrozenPoc(item),
|
||||||
dueDate: toDateInputValue(readString(item, "dueDate", "DueDate")),
|
dueDate: toDateInputValue(readString(item, "dueDate", "DueDate")),
|
||||||
rescheduleCount: readNumber(item, "rescheduleCount", "RescheduleCount") ?? 0,
|
rescheduleCount: readNumber(item, "rescheduleCount", "RescheduleCount") ?? 0,
|
||||||
carriedOver: readNumber(item, "carriedOver", "CarriedOver") ?? 0,
|
carriedOver: readNumber(item, "carriedOver", "CarriedOver") ?? 0,
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@ export interface DocFields {
|
||||||
dateCompleted: string;
|
dateCompleted: string;
|
||||||
amazonRep: string;
|
amazonRep: string;
|
||||||
seaRep: string;
|
seaRep: string;
|
||||||
|
siteCode: string;
|
||||||
|
tech: string;
|
||||||
|
company: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DocState = "draft" | "generated" | "not-required";
|
export type DocState = "draft" | "generated" | "not-required";
|
||||||
|
|
@ -31,12 +34,15 @@ export interface CompletionDocStateConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CompDocSiteInfo {
|
export interface CompDocSiteInfo {
|
||||||
|
code: string;
|
||||||
street: string;
|
street: string;
|
||||||
city: string;
|
city: string;
|
||||||
state: string;
|
state: string;
|
||||||
zip: string;
|
zip: string;
|
||||||
label: string;
|
label: string;
|
||||||
fullAddress: string;
|
fullAddress: string;
|
||||||
|
tech: string;
|
||||||
|
company: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompDocSaveStatus = Exclude<CompletionDocStatus, "">;
|
export type CompDocSaveStatus = Exclude<CompletionDocStatus, "">;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
import type { DateRangeKey } from "./work-order-advanced-filters";
|
import type { DateRangeKey } from "./work-order-advanced-filters";
|
||||||
import type { WorkOrderAdditionalContact } from "./work-order-additional-contact";
|
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";
|
import type { WizardWOStatus, WOType } from "./work-order-wizard";
|
||||||
|
|
||||||
/** Numeric WorkOrderType enum as expected by POST /workorders/board (no string enums). */
|
/** Numeric WorkOrderType enum as expected by POST /workorders/board (no string enums). */
|
||||||
|
|
@ -173,6 +178,9 @@ export interface WorkOrderBoardRowApi {
|
||||||
pocPhone?: string | null;
|
pocPhone?: string | null;
|
||||||
pocNotes?: string | null;
|
pocNotes?: string | null;
|
||||||
additionalContacts?: WorkOrderAdditionalContact[] | null;
|
additionalContacts?: WorkOrderAdditionalContact[] | null;
|
||||||
|
frozenSite?: WorkOrderFrozenSite | null;
|
||||||
|
frozenCompany?: WorkOrderFrozenCompany | null;
|
||||||
|
frozenPoc?: WorkOrderFrozenPoc | null;
|
||||||
lifecycleStatus?: number | null;
|
lifecycleStatus?: number | null;
|
||||||
lifecycleStatusLabel?: string | null;
|
lifecycleStatusLabel?: string | null;
|
||||||
legacyStatus?: string | null;
|
legacyStatus?: string | null;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,35 @@ export type WorkOrderUpliftSummary = {
|
||||||
primaryStatus?: string;
|
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 }[];
|
||||||
|
notes?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export interface WorkOrderTableRow {
|
export interface WorkOrderTableRow {
|
||||||
id: string | number;
|
id: string | number;
|
||||||
woNumber: string;
|
woNumber: string;
|
||||||
|
|
@ -49,6 +78,9 @@ export interface WorkOrderTableRow {
|
||||||
pocPhone: string;
|
pocPhone: string;
|
||||||
pocNotes: string;
|
pocNotes: string;
|
||||||
additionalContacts: WorkOrderAdditionalContact[];
|
additionalContacts: WorkOrderAdditionalContact[];
|
||||||
|
frozenSite?: WorkOrderFrozenSite;
|
||||||
|
frozenCompany?: WorkOrderFrozenCompany;
|
||||||
|
frozenPoc?: WorkOrderFrozenPoc;
|
||||||
dueDate: string;
|
dueDate: string;
|
||||||
rescheduleCount: number;
|
rescheduleCount: number;
|
||||||
carriedOver: number;
|
carriedOver: number;
|
||||||
|
|
|
||||||
|
|
@ -75,11 +75,11 @@ export function buildDocHtml(
|
||||||
<div class="rule"></div>
|
<div class="rule"></div>
|
||||||
<h1>${esc(serviceTitle(row).toUpperCase())}</h1>
|
<h1>${esc(serviceTitle(row).toUpperCase())}</h1>
|
||||||
<p style="margin:2px 0;" class="wo">WO# ${esc(row.woNumber)}</p>
|
<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:14px 0 0;">Site ID — ${esc(fields.siteCode)}</p>
|
||||||
<p style="margin:2px 0;">Amazon.com Services LLC ${esc(row.site)}</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(fields.street)},</p>
|
||||||
<p style="margin:2px 0;" class="muted">${esc(cityStateZip)}</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 ? ` · ${esc(row.company)}` : ""}</p>
|
<p style="margin:14px 0 4px;"><b>Vendor / Technician:</b> ${esc(fields.tech || "—")}${fields.company ? ` · ${esc(fields.company)}` : ""}</p>
|
||||||
<ul style="padding-left:20px;">${safety}</ul>
|
<ul style="padding-left:20px;">${safety}</ul>
|
||||||
${completedBlock}
|
${completedBlock}
|
||||||
<h3 style="margin:16px 0 4px;">${esc(proc.heading)}:</h3>
|
<h3 style="margin:16px 0 4px;">${esc(proc.heading)}:</h3>
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,87 @@
|
||||||
import type { Location } from "@/domain/locations/types/location";
|
import type { Location } from "@/domain/locations/types/location";
|
||||||
import type { CompDocSiteInfo } from "@/domain/work-orders/types/completion-doc";
|
import type { CompDocSiteInfo } from "@/domain/work-orders/types/completion-doc";
|
||||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
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 {
|
function buildLabel(city?: string, state?: string): string {
|
||||||
return [city, state].filter(Boolean).join(", ");
|
return [city, state].filter(Boolean).join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveCompDocSite(
|
function toCompDocAddress(
|
||||||
row: Pick<WorkOrderTableRow, "site" | "location">,
|
street: string,
|
||||||
location?: Location | null,
|
city: string,
|
||||||
): CompDocSiteInfo {
|
state: string,
|
||||||
if (location?.address || location?.city) {
|
zip: string,
|
||||||
const city = location.city ?? "";
|
): CompDocAddress {
|
||||||
const state = location.state ?? "";
|
const label = buildLabel(city, state);
|
||||||
const zip = location.zipCode ?? "";
|
const fullAddress = [street, label, zip].filter(Boolean).join(", ") || "—";
|
||||||
const label = buildLabel(city, state);
|
return { street, city, state, zip, label, fullAddress };
|
||||||
const street = location.address ?? "";
|
}
|
||||||
const fullAddress = [street, label, zip].filter(Boolean).join(", ") || "—";
|
|
||||||
return { street, city, state, zip, label, fullAddress };
|
|
||||||
}
|
|
||||||
|
|
||||||
const locationParts = row.location.split(",").map((s) => s.trim());
|
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 ?? "",
|
||||||
|
location.zipCode ?? "",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRowLocationSite(row: Pick<WorkOrderTableRow, "location">): CompDocAddress {
|
||||||
|
const locationParts = row.location.split(",").map((part) => part.trim());
|
||||||
const city = locationParts[0] ?? "";
|
const city = locationParts[0] ?? "";
|
||||||
const state = locationParts[1] ?? "";
|
const state = locationParts[1] ?? "";
|
||||||
const label = buildLabel(city, state);
|
const label = buildLabel(city, state);
|
||||||
const street = row.location && !label ? row.location : "";
|
const street = row.location && !label ? row.location : "";
|
||||||
const fullAddress = [street, label].filter(Boolean).join(", ") || "—";
|
return toCompDocAddress(street, city, state, "");
|
||||||
|
}
|
||||||
return { street, city, state, zip: "", label, fullAddress };
|
|
||||||
|
export function resolveCompDocSite(
|
||||||
|
row: CompDocIdentityRow,
|
||||||
|
location?: Location | null,
|
||||||
|
): CompDocSiteInfo {
|
||||||
|
const frozen = effectiveSite(row);
|
||||||
|
if (frozen != null) {
|
||||||
|
return withCompDocIdentity(
|
||||||
|
toCompDocAddress(
|
||||||
|
frozen.address ?? "",
|
||||||
|
frozen.city ?? "",
|
||||||
|
frozen.state ?? "",
|
||||||
|
frozen.zip ?? "",
|
||||||
|
),
|
||||||
|
row,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (location?.address || location?.city) {
|
||||||
|
return withCompDocIdentity(resolveLiveLocationSite(location), row);
|
||||||
|
}
|
||||||
|
return withCompDocIdentity(resolveRowLocationSite(row), row);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import type { WorkOrderAdditionalContact } from "@/domain/work-orders/types/work-order-additional-contact";
|
||||||
|
import type {
|
||||||
|
WorkOrderFrozenCompany,
|
||||||
|
WorkOrderFrozenPoc,
|
||||||
|
WorkOrderFrozenSite,
|
||||||
|
WorkOrderTableRow,
|
||||||
|
} from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
|
||||||
|
type SiteSnapshotRow = Pick<WorkOrderTableRow, "status" | "frozenSite">;
|
||||||
|
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: 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 ?? [];
|
||||||
|
}
|
||||||
|
|
@ -9,11 +9,16 @@ import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-ta
|
||||||
const locationDetailState = vi.hoisted(() => ({
|
const locationDetailState = vi.hoisted(() => ({
|
||||||
data: undefined as Location | undefined,
|
data: undefined as Location | undefined,
|
||||||
}));
|
}));
|
||||||
|
const updateLocation = vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
vi.mock("@/domain/locations/use-cases/use-location-detail", () => ({
|
vi.mock("@/domain/locations/use-cases/use-location-detail", () => ({
|
||||||
useLocationDetail: () => ({ data: locationDetailState.data }),
|
useLocationDetail: () => ({ data: locationDetailState.data }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/domain/locations/api/locations-api", () => ({
|
||||||
|
locationsApi: { update: updateLocation },
|
||||||
|
}));
|
||||||
|
|
||||||
function baseRow(overrides: Partial<WorkOrderTableRow> = {}): WorkOrderTableRow {
|
function baseRow(overrides: Partial<WorkOrderTableRow> = {}): WorkOrderTableRow {
|
||||||
const defaults: WorkOrderTableRow = {
|
const defaults: WorkOrderTableRow = {
|
||||||
id: 42,
|
id: 42,
|
||||||
|
|
@ -80,6 +85,7 @@ function wrapper({ children }: { children: ReactNode }) {
|
||||||
describe("useCompDocDialog address seeding", () => {
|
describe("useCompDocDialog address seeding", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
locationDetailState.data = undefined;
|
locationDetailState.data = undefined;
|
||||||
|
updateLocation.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("seeds address from the first authoritative locationDetail without locking on the empty fallback", async () => {
|
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");
|
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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,19 +5,31 @@ import type { Location } from "@/domain/locations/types/location";
|
||||||
import type { LocationOption } from "@/domain/work-orders/types/work-order";
|
import type { LocationOption } from "@/domain/work-orders/types/work-order";
|
||||||
|
|
||||||
const detailById = new Map<string, Location>();
|
const detailById = new Map<string, Location>();
|
||||||
|
const requestedDetailIds: Array<string | undefined> = [];
|
||||||
|
const EMPTY_CONTACTS: [] = [];
|
||||||
|
|
||||||
vi.mock("@/domain/locations/use-cases/use-location-detail", () => ({
|
vi.mock("@/domain/locations/use-cases/use-location-detail", () => ({
|
||||||
useLocationDetail: (id: string | undefined) => ({
|
useLocationDetail: (id: string | undefined) => {
|
||||||
data: id && detailById.has(id) ? detailById.get(id) : undefined,
|
requestedDetailIds.push(id);
|
||||||
isLoading: Boolean(id && !detailById.has(id)),
|
return {
|
||||||
isError: false,
|
data: id && detailById.has(id) ? detailById.get(id) : undefined,
|
||||||
}),
|
isLoading: Boolean(id && !detailById.has(id)),
|
||||||
|
isError: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const SITES: LocationOption[] = [
|
const SITES: LocationOption[] = [
|
||||||
{ id: "loc-1", name: "BK5 · Dallas", siteCode: "BK5", city: "Dallas", state: "TX" },
|
{ id: "loc-1", name: "BK5 · Dallas", siteCode: "BK5", city: "Dallas", state: "TX" },
|
||||||
{ id: "loc-2", name: "SEA1 · Seattle", siteCode: "SEA1", city: "Seattle", state: "WA" },
|
{ 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 {
|
function locationDetail(id: string, contact: string, phone: string): Location {
|
||||||
return {
|
return {
|
||||||
|
|
@ -42,7 +54,7 @@ function renderSiteDialogState() {
|
||||||
pocName: "",
|
pocName: "",
|
||||||
pocPhone: "",
|
pocPhone: "",
|
||||||
pocNotes: "",
|
pocNotes: "",
|
||||||
additionalContacts: [],
|
additionalContacts: EMPTY_CONTACTS,
|
||||||
sites: SITES,
|
sites: SITES,
|
||||||
createMode: true,
|
createMode: true,
|
||||||
onSave: vi.fn(),
|
onSave: vi.fn(),
|
||||||
|
|
@ -53,6 +65,7 @@ function renderSiteDialogState() {
|
||||||
describe("useSiteDialogState POC autofill", () => {
|
describe("useSiteDialogState POC autofill", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
detailById.clear();
|
detailById.clear();
|
||||||
|
requestedDetailIds.length = 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("preserves POC typed before deferred location detail resolves", () => {
|
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.pn).toBe("Contact B");
|
||||||
expect(result.current.pp).toBe("222-222-2222");
|
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",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
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";
|
||||||
|
|
||||||
|
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(
|
||||||
|
<VendorDialogCompanyCard
|
||||||
|
company="Frozen Vendor"
|
||||||
|
vendor={LIVE_VENDOR}
|
||||||
|
frozenCompany={{ name: "Frozen Vendor" }}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<VendorDialogCompanyCard
|
||||||
|
company="Frozen Vendor"
|
||||||
|
vendor={LIVE_VENDOR}
|
||||||
|
frozenCompany={{
|
||||||
|
name: "Frozen Vendor",
|
||||||
|
address: "200 Vendor Ave",
|
||||||
|
city: "Chicago",
|
||||||
|
state: "IL",
|
||||||
|
zip: "60602",
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -186,3 +186,45 @@ describe("mapWorkOrderTableRow carry-over fields", () => {
|
||||||
expect(group.items.map((item) => item.id)).toEqual([42]);
|
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: "" }],
|
||||||
|
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",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
city: "Milwaukee",
|
||||||
|
state: "WI",
|
||||||
|
zipCode: "53202",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
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",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
effectiveAdditionalContacts,
|
||||||
|
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", notes: "gate hours" }],
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
contacts: [
|
||||||
|
{ name: "Live POC", phone: "222" },
|
||||||
|
{ 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", () => {
|
||||||
|
expect(
|
||||||
|
effectivePoc({
|
||||||
|
status: "Scheduled",
|
||||||
|
pocName: "Primary",
|
||||||
|
pocPhone: "111",
|
||||||
|
pocNotes: "",
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
contacts: [{ name: "Primary", phone: "111" }],
|
||||||
|
notes: "",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue