mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-01 12:03:11 +00:00
feat(work-orders): slim completion-doc UI onto current dev
Keep SH-116 CompDocDialog wiring and completedDate board contract. Drop slide-over ownership (SH-112) so it stays in PR #32.
This commit is contained in:
parent
94bd10245b
commit
725efd2703
14 changed files with 1155 additions and 0 deletions
|
|
@ -0,0 +1,19 @@
|
|||
import type { ElementType } from "react";
|
||||
|
||||
type CompDocCoLineProps = {
|
||||
icon: ElementType;
|
||||
text: string;
|
||||
bold?: boolean;
|
||||
};
|
||||
|
||||
export function CompDocCoLine({ icon: Icon, text, bold }: CompDocCoLineProps) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-end gap-1.5 break-words"
|
||||
style={{ fontSize: 11, color: "#161E38", overflowWrap: "break-word" }}
|
||||
>
|
||||
<span style={{ fontWeight: bold ? 600 : 400 }}>{text}</span>
|
||||
<Icon size={12} className="shrink-0" style={{ color: "#1C75BC" }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import type { ElementType } from "react";
|
||||
|
||||
type CompDocDetailRowProps = {
|
||||
icon: ElementType;
|
||||
label: string;
|
||||
value: string;
|
||||
last?: boolean;
|
||||
};
|
||||
|
||||
export function CompDocDetailRow({ icon: Icon, label, value, last }: CompDocDetailRowProps) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-start gap-2.5"
|
||||
style={{
|
||||
padding: "10px 0",
|
||||
borderBottom: last ? "none" : "1px solid var(--color-border)",
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
size={15}
|
||||
className="shrink-0"
|
||||
style={{ color: "var(--muted-foreground)", marginTop: 2 }}
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<div style={{ fontSize: 11.5, color: "var(--muted-foreground)" }}>{label}</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
color: "var(--color-text-primary)",
|
||||
marginTop: 1,
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,398 @@
|
|||
import { useEffect, useMemo, useState, type MouseEvent } from "react";
|
||||
import { Dialog, DialogContent, DialogTitle } from "@mui/material";
|
||||
import { Ban, CheckCircle2, Download, FileText, Save, Sparkles, Undo2, X } from "lucide-react";
|
||||
import { toast } from "react-toastify";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { dialogBlurBackdropSlotProps } from "@/components/ui/dialog-blur-backdrop";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import { useLocationDetail } from "@/domain/locations/use-cases/use-location-detail";
|
||||
import type {
|
||||
CompDocData,
|
||||
CompDocSaveStatus,
|
||||
DocFields,
|
||||
DocState,
|
||||
} from "@/domain/work-orders/types/completion-doc";
|
||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||
import { getMockWoWizardLocationDetail } from "@/domain/work-orders/mocks/wo-wizard-mock-data";
|
||||
import { buildDocHtml, printDoc } from "@/domain/work-orders/utils/completion-doc-html";
|
||||
import { resolveCompDocSite } from "@/domain/work-orders/utils/completion-doc-site";
|
||||
import {
|
||||
DOC_STATE_CFG,
|
||||
docStateOf,
|
||||
fmtDocNow,
|
||||
} from "@/domain/work-orders/utils/completion-doc-state";
|
||||
import { serviceTitle } from "@/domain/work-orders/utils/completion-doc-service-title";
|
||||
import { CompDocPreview } from "./comp-doc-preview";
|
||||
import { CompDocSidebar } from "./comp-doc-sidebar";
|
||||
|
||||
const FOOTER_BTN_SX = {
|
||||
minHeight: 28,
|
||||
height: 28,
|
||||
fontSize: 12,
|
||||
px: 1.25,
|
||||
py: 0.5,
|
||||
gap: 0.75,
|
||||
} as const;
|
||||
|
||||
type CompDocDialogProps = {
|
||||
row: WorkOrderTableRow;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
data?: CompDocData;
|
||||
onSave: (data: CompDocData, status: CompDocSaveStatus) => void;
|
||||
onDateCompletedChange?: (date: string) => void;
|
||||
};
|
||||
|
||||
export function CompDocDialog({
|
||||
row,
|
||||
open,
|
||||
onOpenChange,
|
||||
data,
|
||||
onSave,
|
||||
onDateCompletedChange,
|
||||
}: CompDocDialogProps) {
|
||||
const locId = String(row.locationId || "");
|
||||
const isMockLocation = locId.startsWith("mock-");
|
||||
const { data: apiLocationDetail } = useLocationDetail(
|
||||
isMockLocation || !locId ? undefined : locId,
|
||||
);
|
||||
const locationDetail = isMockLocation ? getMockWoWizardLocationDetail(locId) : apiLocationDetail;
|
||||
|
||||
const site = useMemo(() => resolveCompDocSite(row, locationDetail), [row, locationDetail]);
|
||||
|
||||
const [notes, setNotes] = useState("");
|
||||
const [docState, setDocState] = useState<DocState>("draft");
|
||||
const [generatedAt, setGeneratedAt] = useState("");
|
||||
const [editDateCompleted, setEditDateCompleted] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setNotes(data?.notes ?? "");
|
||||
setDocState(docStateOf(row.docStatus));
|
||||
setGeneratedAt(data?.generatedAt ?? "");
|
||||
setEditDateCompleted(row.completedDate || "");
|
||||
}, [open, row.id, row.docStatus, row.completedDate, data?.notes, data?.generatedAt]);
|
||||
|
||||
const amazonRep = row.pocName || "";
|
||||
const seaRep = row.tech || "";
|
||||
const cfg = DOC_STATE_CFG[docState];
|
||||
const isNotRequired = docState === "not-required";
|
||||
const isGenerated = docState === "generated";
|
||||
|
||||
const docFields: DocFields = {
|
||||
street: site.street,
|
||||
city: site.city,
|
||||
state: site.state,
|
||||
zip: site.zip,
|
||||
dateCompleted: editDateCompleted,
|
||||
amazonRep,
|
||||
seaRep,
|
||||
};
|
||||
|
||||
const persist = (extra: Partial<CompDocData>, status: CompDocSaveStatus) => {
|
||||
onSave({ notes, generatedAt, ...extra }, status);
|
||||
};
|
||||
|
||||
function handleMarkNotRequired() {
|
||||
setDocState("not-required");
|
||||
persist({}, "NN");
|
||||
}
|
||||
|
||||
function handleUndo() {
|
||||
setDocState("draft");
|
||||
persist({}, "No");
|
||||
}
|
||||
|
||||
function handleSaveDraft() {
|
||||
persist({}, "No");
|
||||
if (editDateCompleted !== (row.completedDate || "")) {
|
||||
onDateCompletedChange?.(editDateCompleted);
|
||||
}
|
||||
toast(
|
||||
<div
|
||||
className="flex items-center gap-2"
|
||||
style={{
|
||||
background: "var(--foreground)",
|
||||
color: "#fff",
|
||||
padding: "10px 18px",
|
||||
borderRadius: 9999,
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
boxShadow: "0 10px 30px -10px rgba(0,0,0,.45)",
|
||||
}}
|
||||
>
|
||||
<CheckCircle2 size={16} /> Draft saved
|
||||
</div>,
|
||||
{
|
||||
position: "bottom-center",
|
||||
autoClose: 3000,
|
||||
hideProgressBar: true,
|
||||
closeButton: false,
|
||||
icon: false,
|
||||
className: "!min-h-0 !bg-transparent !p-0 !shadow-none",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function handleGenerate() {
|
||||
const ts = fmtDocNow();
|
||||
setGeneratedAt(ts);
|
||||
setDocState("generated");
|
||||
persist({ generatedAt: ts }, "Yes");
|
||||
if (editDateCompleted !== (row.completedDate || "")) {
|
||||
onDateCompletedChange?.(editDateCompleted);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDownloadPdf() {
|
||||
printDoc(buildDocHtml(row, { notes, generatedAt }, docFields));
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={() => onOpenChange(false)}
|
||||
maxWidth={false}
|
||||
slotProps={{
|
||||
...dialogBlurBackdropSlotProps,
|
||||
paper: {
|
||||
className: "flex h-[85vh] max-h-[85vh] flex-col overflow-hidden border border-border p-0",
|
||||
style: {
|
||||
borderRadius: 12,
|
||||
width: "min(1024px, calc(100vw - 32px))",
|
||||
maxWidth: "none",
|
||||
},
|
||||
onClick: (e: MouseEvent<HTMLDivElement>) => e.stopPropagation(),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
dividers={false}
|
||||
className="flex min-h-0 flex-1 flex-col gap-0 overflow-hidden p-0"
|
||||
sx={{
|
||||
p: 0,
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<DialogTitle className="sr-only">Completion Document — WO #{row.woNumber}</DialogTitle>
|
||||
<Text className="sr-only" variant="description">
|
||||
Generate, save or mark not-required the completion document for this work order.
|
||||
</Text>
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<div
|
||||
className="flex shrink-0 items-center justify-between"
|
||||
style={{
|
||||
height: 60,
|
||||
padding: "0 20px",
|
||||
borderBottom: "1px solid var(--color-border)",
|
||||
background: "var(--color-bg-surface)",
|
||||
}}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-2.5">
|
||||
<FileText size={18} className="shrink-0" style={{ color: "var(--color-primary)" }} />
|
||||
<div className="min-w-0">
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "var(--font-display)",
|
||||
fontSize: 16,
|
||||
fontWeight: 700,
|
||||
color: "var(--color-text-primary)",
|
||||
lineHeight: 1.1,
|
||||
}}
|
||||
>
|
||||
Completion Document
|
||||
</div>
|
||||
<div
|
||||
className="truncate"
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: "var(--muted-foreground)",
|
||||
marginTop: 2,
|
||||
}}
|
||||
>
|
||||
WO{" "}
|
||||
<span
|
||||
className="font-mono"
|
||||
style={{ color: "var(--color-primary)", fontWeight: 600 }}
|
||||
>
|
||||
#{row.woNumber}
|
||||
</span>
|
||||
{" · "}
|
||||
{serviceTitle(row)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<span
|
||||
className="inline-flex items-center gap-1.5"
|
||||
style={{
|
||||
height: 26,
|
||||
padding: "0 10px",
|
||||
borderRadius: 9999,
|
||||
background: cfg.pillBg,
|
||||
color: cfg.pillText,
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 9999,
|
||||
background: cfg.pillText,
|
||||
}}
|
||||
/>
|
||||
{cfg.pillLabel}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpenChange(false)}
|
||||
aria-label="Close"
|
||||
className="flex items-center justify-center rounded-md transition-colors hover:bg-[var(--color-bg-muted)]"
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
border: "1px solid var(--color-border)",
|
||||
color: "var(--color-text-muted)",
|
||||
}}
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-row">
|
||||
<CompDocPreview
|
||||
row={row}
|
||||
site={site}
|
||||
notes={notes}
|
||||
generatedAt={generatedAt}
|
||||
isGenerated={isGenerated}
|
||||
isNotRequired={isNotRequired}
|
||||
/>
|
||||
<CompDocSidebar
|
||||
row={row}
|
||||
site={site}
|
||||
docState={docState}
|
||||
generatedAt={generatedAt}
|
||||
notes={notes}
|
||||
editDateCompleted={editDateCompleted}
|
||||
amazonRep={amazonRep}
|
||||
seaRep={seaRep}
|
||||
isGenerated={isGenerated}
|
||||
onNotesChange={setNotes}
|
||||
onDateCompletedChange={setEditDateCompleted}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex shrink-0 items-center justify-between gap-3"
|
||||
style={{
|
||||
padding: "14px 20px",
|
||||
borderTop: "1px solid var(--color-border)",
|
||||
background: "var(--color-bg-surface)",
|
||||
}}
|
||||
>
|
||||
{docState === "draft" && (
|
||||
<>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="default"
|
||||
className="gap-1.5 whitespace-nowrap text-[12px]"
|
||||
sx={FOOTER_BTN_SX}
|
||||
style={{ color: "var(--muted-foreground)" }}
|
||||
onClick={handleMarkNotRequired}
|
||||
>
|
||||
<Ban size={14} /> Mark as not required
|
||||
</Button>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="default"
|
||||
className="gap-1.5 whitespace-nowrap text-[12px]"
|
||||
sx={FOOTER_BTN_SX}
|
||||
onClick={handleSaveDraft}
|
||||
>
|
||||
<Save size={14} /> Save draft
|
||||
</Button>
|
||||
<Button
|
||||
size="default"
|
||||
className="gap-1.5 whitespace-nowrap text-[12px]"
|
||||
sx={FOOTER_BTN_SX}
|
||||
style={{ background: "var(--color-primary)", color: "#fff" }}
|
||||
onClick={handleGenerate}
|
||||
>
|
||||
<Sparkles size={14} /> Generate
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{docState === "generated" && (
|
||||
<>
|
||||
<span
|
||||
className="inline-flex items-center gap-1.5 whitespace-nowrap"
|
||||
style={{ fontSize: 12, color: "var(--color-success)" }}
|
||||
>
|
||||
<CheckCircle2 size={14} /> Generated {generatedAt}
|
||||
</span>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<Button
|
||||
size="default"
|
||||
className="gap-1.5 whitespace-nowrap text-[12px]"
|
||||
sx={FOOTER_BTN_SX}
|
||||
style={{ background: "var(--color-primary)", color: "#fff" }}
|
||||
onClick={handleDownloadPdf}
|
||||
>
|
||||
<Download size={14} /> Download PDF
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{docState === "not-required" && (
|
||||
<>
|
||||
<span
|
||||
className="inline-flex items-center gap-1.5 whitespace-nowrap"
|
||||
style={{ fontSize: 12, color: "var(--muted-foreground)" }}
|
||||
>
|
||||
<Ban size={14} style={{ color: "var(--warning)" }} /> Marked{" "}
|
||||
<b style={{ fontWeight: 600 }}>not required</b> for this work order
|
||||
</span>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="default"
|
||||
className="gap-1.5 whitespace-nowrap text-[12px]"
|
||||
sx={FOOTER_BTN_SX}
|
||||
onClick={handleUndo}
|
||||
>
|
||||
<Undo2 size={14} /> Undo
|
||||
</Button>
|
||||
<Button
|
||||
size="default"
|
||||
disabled
|
||||
title="Marked not required — undo to generate"
|
||||
className="gap-1.5 whitespace-nowrap text-[12px]"
|
||||
sx={FOOTER_BTN_SX}
|
||||
style={{
|
||||
background: "var(--color-primary)",
|
||||
color: "#fff",
|
||||
opacity: 0.5,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
<Sparkles size={14} /> Generate
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,295 @@
|
|||
import { Ban, Building2, CheckCircle2, FileText, Mail, MapPin, Phone, Printer } from "lucide-react";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import type { CompDocSiteInfo } from "@/domain/work-orders/types/completion-doc";
|
||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||
import { SEAHAVEN_HEADER } from "@/domain/work-orders/utils/completion-doc-html";
|
||||
import { procedureFor, SAFETY_BULLETS } from "@/domain/work-orders/utils/completion-doc-procedures";
|
||||
import { serviceTitle } from "@/domain/work-orders/utils/completion-doc-service-title";
|
||||
import { CompDocCoLine } from "./comp-doc-co-line";
|
||||
import { CompDocProcedureList } from "./comp-doc-procedure-list";
|
||||
|
||||
type CompDocPreviewProps = {
|
||||
row: WorkOrderTableRow;
|
||||
site: CompDocSiteInfo;
|
||||
notes: string;
|
||||
generatedAt: string;
|
||||
isGenerated: boolean;
|
||||
isNotRequired: boolean;
|
||||
};
|
||||
|
||||
export function CompDocPreview({
|
||||
row,
|
||||
site,
|
||||
notes,
|
||||
generatedAt,
|
||||
isGenerated,
|
||||
isNotRequired,
|
||||
}: CompDocPreviewProps) {
|
||||
const proc = procedureFor(row.pm);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex min-h-0 flex-col"
|
||||
style={{
|
||||
width: "58%",
|
||||
borderRight: "1px solid var(--color-border)",
|
||||
background: "var(--color-bg-muted)",
|
||||
}}
|
||||
>
|
||||
<div className="flex shrink-0 items-center justify-center" style={{ padding: "12px 0" }}>
|
||||
<span
|
||||
className="inline-flex items-center gap-1.5"
|
||||
style={{
|
||||
height: 28,
|
||||
padding: "0 12px",
|
||||
borderRadius: 9999,
|
||||
background: "var(--color-bg-surface)",
|
||||
border: "1px solid var(--color-border)",
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: "var(--muted-foreground)",
|
||||
}}
|
||||
>
|
||||
{isGenerated ? (
|
||||
<CheckCircle2 size={13} style={{ color: "var(--color-success)" }} />
|
||||
) : (
|
||||
<FileText size={13} />
|
||||
)}
|
||||
{isGenerated ? "Generated document" : "Preview — not yet generated"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto"
|
||||
style={{ padding: "0 20px 20px" }}
|
||||
>
|
||||
<div
|
||||
className="relative break-words"
|
||||
style={{
|
||||
background: "#ffffff",
|
||||
borderRadius: 8,
|
||||
boxShadow: "0 1px 3px rgba(0,0,0,.08), 0 12px 32px -16px rgba(0,0,0,.25)",
|
||||
overflow: "hidden",
|
||||
fontFamily: "var(--font-sans)",
|
||||
overflowWrap: "break-word",
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
opacity: isNotRequired ? 0.5 : 1,
|
||||
filter: isNotRequired ? "grayscale(0.4)" : "none",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: 12,
|
||||
background: "linear-gradient(100deg,#27AAE1,#1C75BC)",
|
||||
}}
|
||||
/>
|
||||
<div style={{ padding: "0 28px 28px" }}>
|
||||
<div
|
||||
className="flex flex-wrap items-start justify-between gap-y-3"
|
||||
style={{ padding: "20px 0 12px" }}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src="/seahaven-logo.svg"
|
||||
alt="Seahaven Industries"
|
||||
style={{ height: 34, width: "auto", display: "block" }}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<CompDocCoLine icon={Building2} text={SEAHAVEN_HEADER.name} bold />
|
||||
<CompDocCoLine icon={MapPin} text={SEAHAVEN_HEADER.address} />
|
||||
<CompDocCoLine icon={Phone} text={SEAHAVEN_HEADER.phone} />
|
||||
<CompDocCoLine icon={Printer} text={SEAHAVEN_HEADER.fax} />
|
||||
<CompDocCoLine icon={Mail} text={SEAHAVEN_HEADER.email} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
height: 3,
|
||||
borderRadius: 2,
|
||||
background: "linear-gradient(90deg,#27AAE1,#1C75BC)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{isGenerated && (
|
||||
<div
|
||||
className="inline-flex items-center gap-1.5"
|
||||
style={{
|
||||
marginTop: 16,
|
||||
padding: "4px 10px",
|
||||
borderRadius: 9999,
|
||||
background: "var(--doc-yes-bg)",
|
||||
color: "var(--color-success)",
|
||||
fontSize: 11.5,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<CheckCircle2 size={13} /> Generated · {generatedAt} · v1
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Text
|
||||
as="h2"
|
||||
variant="heading"
|
||||
sx={{
|
||||
fontFamily: "var(--font-sans)",
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
letterSpacing: "0.02em",
|
||||
color: "#161E38",
|
||||
margin: "16px 0 4px",
|
||||
}}
|
||||
>
|
||||
{serviceTitle(row).toUpperCase()}
|
||||
</Text>
|
||||
<Text
|
||||
family="mono"
|
||||
variant="body"
|
||||
sx={{
|
||||
fontSize: 12.5,
|
||||
fontWeight: 600,
|
||||
color: "#1C75BC",
|
||||
margin: "2px 0",
|
||||
}}
|
||||
>
|
||||
WO# {row.woNumber}
|
||||
</Text>
|
||||
|
||||
<div style={{ marginTop: 14 }}>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: "#161E38",
|
||||
margin: "2px 0",
|
||||
}}
|
||||
>
|
||||
Site ID — {row.site}
|
||||
</Text>
|
||||
<Text variant="body" sx={{ fontSize: 13, color: "#161E38", margin: "2px 0" }}>
|
||||
Amazon.com Services LLC {row.site}
|
||||
</Text>
|
||||
<Text variant="body" sx={{ fontSize: 13, color: "#58595B", margin: "2px 0" }}>
|
||||
{site.street},
|
||||
</Text>
|
||||
<Text variant="body" sx={{ fontSize: 13, color: "#58595B", margin: "2px 0" }}>
|
||||
{site.label}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Text variant="body" sx={{ fontSize: 13, color: "#161E38", margin: "14px 0 6px" }}>
|
||||
<span style={{ fontWeight: 700 }}>Vendor / Technician: </span>
|
||||
{row.tech || "—"}
|
||||
{row.company ? ` · ${row.company}` : ""}
|
||||
</Text>
|
||||
|
||||
<ul style={{ listStyle: "disc", paddingLeft: 18, margin: "8px 0 0" }}>
|
||||
{SAFETY_BULLETS.map((b, i) => (
|
||||
<li
|
||||
key={i}
|
||||
style={{
|
||||
fontSize: 12.5,
|
||||
lineHeight: 1.5,
|
||||
margin: "5px 0",
|
||||
color: "#161E38",
|
||||
}}
|
||||
>
|
||||
{b}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<Text
|
||||
as="h3"
|
||||
variant="heading"
|
||||
sx={{
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
color: "#161E38",
|
||||
margin: "16px 0 4px",
|
||||
}}
|
||||
>
|
||||
{proc.heading}
|
||||
</Text>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{
|
||||
fontSize: 12.5,
|
||||
lineHeight: 1.5,
|
||||
color: "#58595B",
|
||||
margin: "0 0 8px",
|
||||
}}
|
||||
>
|
||||
{proc.intro}
|
||||
</Text>
|
||||
<CompDocProcedureList proc={proc} />
|
||||
|
||||
{notes.length > 0 && (
|
||||
<>
|
||||
<Text
|
||||
as="h3"
|
||||
variant="heading"
|
||||
sx={{
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
color: "#161E38",
|
||||
margin: "16px 0 4px",
|
||||
}}
|
||||
>
|
||||
PM Comments
|
||||
</Text>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{
|
||||
fontSize: 12.5,
|
||||
lineHeight: 1.5,
|
||||
color: "#161E38",
|
||||
margin: 0,
|
||||
whiteSpace: "pre-wrap",
|
||||
}}
|
||||
>
|
||||
{notes}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
height: 24,
|
||||
background: "linear-gradient(100deg,#1C75BC,#27AAE1)",
|
||||
clipPath: "polygon(0 45%,100% 0,100% 100%,0 100%)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isNotRequired && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-start justify-center">
|
||||
<div
|
||||
className="inline-flex items-center gap-2"
|
||||
style={{
|
||||
marginTop: "30%",
|
||||
transform: "rotate(-15deg)",
|
||||
border: "3px solid var(--warning)",
|
||||
color: "var(--warning)",
|
||||
borderRadius: 8,
|
||||
padding: "8px 18px",
|
||||
fontWeight: 800,
|
||||
fontSize: 22,
|
||||
letterSpacing: "0.06em",
|
||||
background: "rgba(255,255,255,0.65)",
|
||||
}}
|
||||
>
|
||||
<Ban size={20} /> NOT REQUIRED
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import type { Procedure } from "@/domain/work-orders/utils/completion-doc-procedures";
|
||||
|
||||
type CompDocProcedureListProps = {
|
||||
proc: Procedure;
|
||||
};
|
||||
|
||||
export function CompDocProcedureList({ proc }: CompDocProcedureListProps) {
|
||||
return (
|
||||
<ol style={{ listStyle: "decimal", paddingLeft: 22, margin: 0 }}>
|
||||
{proc.steps.map((st, i) => {
|
||||
const subSteps = st.sub ?? [];
|
||||
return (
|
||||
<li key={i} style={{ margin: "6px 0", fontSize: 13, lineHeight: 1.5 }}>
|
||||
{st.text}
|
||||
{subSteps.length > 0 && (
|
||||
<ol
|
||||
style={{
|
||||
listStyle: "lower-alpha",
|
||||
paddingLeft: 22,
|
||||
margin: "4px 0 0",
|
||||
}}
|
||||
>
|
||||
{subSteps.map((x, j) => (
|
||||
<li key={j} style={{ margin: "2px 0" }}>
|
||||
{x}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
import type { CSSProperties } from "react";
|
||||
import { Building2, MapPin, Users, Wrench } from "lucide-react";
|
||||
import type { CompDocSiteInfo, DocState } from "@/domain/work-orders/types/completion-doc";
|
||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||
import {
|
||||
DOC_STATE_CFG,
|
||||
docStatusCardDescription,
|
||||
} from "@/domain/work-orders/utils/completion-doc-state";
|
||||
import { CompDocDetailRow } from "./comp-doc-detail-row";
|
||||
|
||||
const COMP_DOC_INPUT_CLS = "w-full rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-1";
|
||||
|
||||
const COMP_DOC_INPUT_STYLE: CSSProperties = {
|
||||
fontSize: 13,
|
||||
border: "1px solid var(--color-border)",
|
||||
color: "var(--color-text-primary)",
|
||||
background: "var(--color-bg-surface)",
|
||||
};
|
||||
|
||||
type CompDocSidebarProps = {
|
||||
row: WorkOrderTableRow;
|
||||
site: CompDocSiteInfo;
|
||||
docState: DocState;
|
||||
generatedAt: string;
|
||||
notes: string;
|
||||
editDateCompleted: string;
|
||||
amazonRep: string;
|
||||
seaRep: string;
|
||||
isGenerated: boolean;
|
||||
onNotesChange: (value: string) => void;
|
||||
onDateCompletedChange: (value: string) => void;
|
||||
};
|
||||
|
||||
export function CompDocSidebar({
|
||||
row,
|
||||
site,
|
||||
docState,
|
||||
generatedAt,
|
||||
notes,
|
||||
editDateCompleted,
|
||||
amazonRep,
|
||||
seaRep,
|
||||
isGenerated,
|
||||
onNotesChange,
|
||||
onDateCompletedChange,
|
||||
}: CompDocSidebarProps) {
|
||||
const cfg = DOC_STATE_CFG[docState];
|
||||
const StateIcon = cfg.icon;
|
||||
|
||||
const editRow = (
|
||||
label: string,
|
||||
value: string,
|
||||
onChange: (v: string) => void,
|
||||
type = "text",
|
||||
placeholder = "",
|
||||
) => (
|
||||
<div style={{ padding: "8px 0", borderBottom: "1px solid var(--color-border)" }}>
|
||||
<div style={{ fontSize: 11.5, color: "var(--muted-foreground)", marginBottom: 4 }}>
|
||||
{label}
|
||||
</div>
|
||||
<input
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
readOnly={isGenerated}
|
||||
placeholder={placeholder}
|
||||
className={COMP_DOC_INPUT_CLS}
|
||||
style={{
|
||||
...COMP_DOC_INPUT_STYLE,
|
||||
...(isGenerated
|
||||
? {
|
||||
opacity: 0.6,
|
||||
cursor: "default",
|
||||
pointerEvents: "none" as const,
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex min-h-0 flex-col"
|
||||
style={{ width: "42%", background: "var(--color-bg-surface)" }}
|
||||
>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto" style={{ padding: 20 }}>
|
||||
<div
|
||||
className="flex items-start gap-3"
|
||||
style={{
|
||||
padding: 14,
|
||||
borderRadius: 10,
|
||||
background: cfg.cardBg,
|
||||
border: `1px solid ${cfg.cardBorder}`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex shrink-0 items-center justify-center"
|
||||
style={{
|
||||
width: 34,
|
||||
height: 34,
|
||||
borderRadius: 8,
|
||||
background: cfg.cardIconBg,
|
||||
border: `1px solid ${cfg.cardBorder}`,
|
||||
}}
|
||||
>
|
||||
<StateIcon size={17} style={{ color: cfg.iconColor }} />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div
|
||||
style={{
|
||||
fontSize: 13.5,
|
||||
fontWeight: 600,
|
||||
color: "var(--color-text-primary)",
|
||||
}}
|
||||
>
|
||||
{cfg.cardTitle}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12.5,
|
||||
lineHeight: 1.45,
|
||||
color: "var(--muted-foreground)",
|
||||
marginTop: 3,
|
||||
}}
|
||||
>
|
||||
{docStatusCardDescription(docState, generatedAt)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "var(--font-mono)",
|
||||
fontSize: 10,
|
||||
fontWeight: 600,
|
||||
letterSpacing: "0.1em",
|
||||
textTransform: "uppercase",
|
||||
color: "var(--muted-foreground)",
|
||||
}}
|
||||
>
|
||||
Work Order Details
|
||||
</div>
|
||||
<div style={{ marginTop: 6 }}>
|
||||
<CompDocDetailRow icon={Building2} label="WO #" value={`#${row.woNumber}`} />
|
||||
<CompDocDetailRow
|
||||
icon={MapPin}
|
||||
label="Site Code"
|
||||
value={`${row.site} · Amazon.com Services LLC`}
|
||||
/>
|
||||
<CompDocDetailRow icon={MapPin} label="Address" value={site.fullAddress} />
|
||||
{editRow("Date Work Completed", editDateCompleted, onDateCompletedChange, "date")}
|
||||
<CompDocDetailRow icon={Users} label="Amazon Representative" value={amazonRep || "—"} />
|
||||
<CompDocDetailRow
|
||||
icon={Wrench}
|
||||
label="Seahaven Representative"
|
||||
value={seaRep || "—"}
|
||||
last
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 18 }}>
|
||||
<label
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: "var(--color-text-primary)",
|
||||
}}
|
||||
>
|
||||
PM Comments{" "}
|
||||
<span style={{ fontWeight: 400, color: "var(--muted-foreground)" }}>
|
||||
— optional, included in the document
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
value={notes}
|
||||
onChange={(e) => onNotesChange(e.target.value)}
|
||||
readOnly={isGenerated}
|
||||
rows={3}
|
||||
placeholder="Add findings, parts used, access notes, or anything the site POC should see…"
|
||||
className="mt-2 w-full resize-none"
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontFamily: "var(--font-sans)",
|
||||
color: "var(--color-text-primary)",
|
||||
background: "var(--color-bg-surface)",
|
||||
border: "1px solid var(--color-border)",
|
||||
borderRadius: 8,
|
||||
padding: "10px 12px",
|
||||
lineHeight: 1.5,
|
||||
outline: "none",
|
||||
...(isGenerated
|
||||
? {
|
||||
opacity: 0.6,
|
||||
cursor: "default",
|
||||
pointerEvents: "none" as const,
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { useMemo, useState } from "react";
|
|||
import { Box } from "@mui/material";
|
||||
import { useNavigate } from "react-router";
|
||||
import { PageHeader } from "@/components/ui/page-header";
|
||||
import { CompDocDialog } from "@/app/(protected)/workorders/_components/comp-doc/comp-doc-dialog";
|
||||
import { MediaUploadModal } from "@/app/(protected)/workorders/_components/media/media-upload-modal";
|
||||
import { WorkOrdersAdvancedSearchSheet } from "@/app/(protected)/workorders/_components/list/advanced-search/work-orders-advanced-search-sheet";
|
||||
import { WorkOrdersAdvancedFiltersBanner } from "@/app/(protected)/workorders/_components/list/work-orders-advanced-filters-banner";
|
||||
|
|
@ -19,6 +20,7 @@ import {
|
|||
import { useWorkOrderUsers } from "@/domain/work-orders/use-cases/use-work-orders-list";
|
||||
import { isWoTableMockEnabled } from "@/domain/work-orders/mocks/is-wo-table-mock-enabled";
|
||||
import { getMockPmTypes } from "@/domain/work-orders/mocks/wo-table-mock-data";
|
||||
import type { CompDocData } from "@/domain/work-orders/types/completion-doc";
|
||||
import { EMPTY_MEDIA, type MediaFile } from "@/domain/work-orders/types/work-order-media";
|
||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||
import { useWorkOrderTableMutations } from "@/domain/work-orders/use-cases/use-work-order-table-mutations";
|
||||
|
|
@ -28,7 +30,9 @@ export default function WorkOrdersListPage() {
|
|||
const navigate = useNavigate();
|
||||
const { user } = useAuthContext();
|
||||
const [wizardOpen, setWizardOpen] = useState(false);
|
||||
const [docRow, setDocRow] = useState<WorkOrderTableRow | null>(null);
|
||||
const [mediaRow, setMediaRow] = useState<WorkOrderTableRow | null>(null);
|
||||
const [completionDocs, setCompletionDocs] = useState<Record<string, CompDocData>>({});
|
||||
const [mediaFiles, setMediaFiles] = useState<Record<string, MediaFile[]>>({});
|
||||
const filters = useWorkOrdersListFilters(user?.id);
|
||||
const tableData = useWorkOrdersTableData(filters);
|
||||
|
|
@ -55,6 +59,11 @@ export default function WorkOrdersListPage() {
|
|||
const totalCount = tableData.totalCount;
|
||||
const scopeTotalCount = tableData.scopeTotalCount;
|
||||
|
||||
const activeDocRow = useMemo(() => {
|
||||
if (!docRow) return null;
|
||||
return tableData.rows.find((row) => row.id === docRow.id) ?? docRow;
|
||||
}, [docRow, tableData.rows]);
|
||||
|
||||
const activeMediaRow = useMemo(() => {
|
||||
if (!mediaRow) return null;
|
||||
return tableData.rows.find((row) => row.id === mediaRow.id) ?? mediaRow;
|
||||
|
|
@ -112,6 +121,7 @@ export default function WorkOrdersListPage() {
|
|||
tableData.setSelectedId(row.id);
|
||||
navigate(edit ? `/workorders/edit/${row.id}` : `/workorders/${row.id}`);
|
||||
}}
|
||||
onOpenCompDoc={setDocRow}
|
||||
onOpenMedia={setMediaRow}
|
||||
onClearSearch={() => filters.handleSearchChange("")}
|
||||
onOpenAdvancedFilters={() => filters.setAdvOpen(true)}
|
||||
|
|
@ -131,6 +141,22 @@ export default function WorkOrdersListPage() {
|
|||
|
||||
<NewWoWizard open={wizardOpen} onOpenChange={setWizardOpen} users={users} />
|
||||
|
||||
{activeDocRow != null && (
|
||||
<CompDocDialog
|
||||
row={activeDocRow}
|
||||
open={!!activeDocRow}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setDocRow(null);
|
||||
}}
|
||||
data={completionDocs[String(activeDocRow.id)]}
|
||||
onSave={(data, status) => {
|
||||
setCompletionDocs((prev) => ({ ...prev, [String(activeDocRow.id)]: data }));
|
||||
patchField(activeDocRow.id, { docStatus: status });
|
||||
}}
|
||||
onDateCompletedChange={(date) => patchField(activeDocRow.id, { completedDate: date })}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeMediaRow != null && (
|
||||
<MediaUploadModal
|
||||
row={activeMediaRow}
|
||||
|
|
|
|||
|
|
@ -179,6 +179,7 @@ export function mapWorkOrderTableRow(raw: unknown): WorkOrderTableRow {
|
|||
dispatcherId,
|
||||
dispatcherName,
|
||||
scheduledOn,
|
||||
completedDate: toDateInputValue(readString(item, "completedDate", "CompletedDate")),
|
||||
targetWeek: readString(item, "targetWeek", "TargetWeek"),
|
||||
dayGroup,
|
||||
dayLabel,
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ function buildRow(seed: RowSeed): WorkOrderTableRow {
|
|||
...seed,
|
||||
...dayFields,
|
||||
scheduledOn,
|
||||
completedDate: seed.status === "Completed" ? todayIso() : "",
|
||||
targetWeek: seed.targetWeek ?? "",
|
||||
isPastDue: false,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ export interface WorkOrderTableRow {
|
|||
dispatcherId: string;
|
||||
dispatcherName: string;
|
||||
scheduledOn: string;
|
||||
/** Work completed date (API CompletedDate); distinct from scheduledOn. */
|
||||
completedDate: string;
|
||||
targetWeek: string;
|
||||
dayGroup: string;
|
||||
dayLabel: string;
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ export function useWorkOrderTableMutations(
|
|||
locationId: patch.locationId != null ? String(patch.locationId) : values.locationId,
|
||||
dueDate: patch.dueDate ?? values.dueDate,
|
||||
scheduledDate: patch.scheduledOn ?? values.scheduledDate,
|
||||
completedDate: patch.completedDate ?? values.completedDate,
|
||||
assignedTo: patch.dispatcherName ?? values.assignedTo,
|
||||
problem: patch.pm ?? values.problem,
|
||||
source: patch.type ?? values.source,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,129 @@
|
|||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { CompDocDialog } from "@/app/(protected)/workorders/_components/comp-doc/comp-doc-dialog";
|
||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||
|
||||
vi.mock("@/domain/locations/use-cases/use-location-detail", () => ({
|
||||
useLocationDetail: () => ({ data: undefined }),
|
||||
}));
|
||||
|
||||
vi.mock("react-toastify", () => ({
|
||||
toast: vi.fn(),
|
||||
}));
|
||||
|
||||
const SCHEDULED_ON = "2026-06-01";
|
||||
|
||||
function baseRow(overrides: Partial<WorkOrderTableRow> = {}): WorkOrderTableRow {
|
||||
return {
|
||||
id: 42,
|
||||
woNumber: "WO-42",
|
||||
site: "SEA1",
|
||||
type: "PM",
|
||||
dispatcherId: "u1",
|
||||
dispatcherName: "Dispatcher",
|
||||
scheduledOn: SCHEDULED_ON,
|
||||
completedDate: "",
|
||||
targetWeek: "",
|
||||
dayGroup: "",
|
||||
dayLabel: "",
|
||||
location: "Seattle",
|
||||
locationId: "mock-loc-1",
|
||||
pm: "HVAC",
|
||||
company: "Vendor Co",
|
||||
tech: "Tech One",
|
||||
techPhone: "",
|
||||
apptStart: "",
|
||||
apptEnd: "",
|
||||
apptTime: "",
|
||||
status: "Scheduled",
|
||||
docStatus: "No",
|
||||
pocName: "Amazon Rep",
|
||||
pocPhone: "",
|
||||
pocNotes: "",
|
||||
dueDate: "",
|
||||
rescheduleCount: 0,
|
||||
carriedOver: 0,
|
||||
originalWeek: "",
|
||||
originalDate: "",
|
||||
isPastDue: false,
|
||||
scheduleWeekOnly: false,
|
||||
mediaCount: 0,
|
||||
title: "PM — SEA1",
|
||||
priority: "Sev 3",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("CompDocDialog Save Draft / Generate completion contract", () => {
|
||||
const onSave = vi.fn();
|
||||
const onDateCompletedChange = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
onSave.mockReset();
|
||||
onDateCompletedChange.mockReset();
|
||||
});
|
||||
|
||||
function renderDialog(row: WorkOrderTableRow) {
|
||||
// Mirrors board wiring: only completedDate / docStatus — never scheduledOn or lifecycle.
|
||||
const patches: Array<Partial<WorkOrderTableRow>> = [];
|
||||
|
||||
render(
|
||||
<CompDocDialog
|
||||
row={row}
|
||||
open
|
||||
onOpenChange={vi.fn()}
|
||||
onSave={(data, status) => {
|
||||
onSave(data, status);
|
||||
patches.push({ docStatus: status });
|
||||
}}
|
||||
onDateCompletedChange={(date) => {
|
||||
onDateCompletedChange(date);
|
||||
patches.push({ completedDate: date });
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
return { patches };
|
||||
}
|
||||
|
||||
function changeDateCompleted(value: string) {
|
||||
const dateInput = document.querySelector('input[type="date"]');
|
||||
expect(dateInput).not.toBeNull();
|
||||
fireEvent.change(dateInput!, { target: { value } });
|
||||
}
|
||||
|
||||
it("Save draft emits completedDate without writing scheduledOn or lifecycle Completed", () => {
|
||||
const row = baseRow();
|
||||
const { patches } = renderDialog(row);
|
||||
|
||||
changeDateCompleted("2026-07-15");
|
||||
fireEvent.click(screen.getByRole("button", { name: /save draft/i }));
|
||||
|
||||
expect(onDateCompletedChange).toHaveBeenCalledWith("2026-07-15");
|
||||
expect(onSave).toHaveBeenCalledWith(expect.any(Object), "No");
|
||||
expect(patches).toEqual([{ docStatus: "No" }, { completedDate: "2026-07-15" }]);
|
||||
expect(patches.some((patch) => "scheduledOn" in patch)).toBe(false);
|
||||
expect(patches.some((patch) => patch.status === "Completed")).toBe(false);
|
||||
expect(row.scheduledOn).toBe(SCHEDULED_ON);
|
||||
expect(row.status).toBe("Scheduled");
|
||||
});
|
||||
|
||||
it("Generate emits completedDate and doc Yes without auto-setting lifecycle Completed", () => {
|
||||
const row = baseRow({ completedDate: "2026-07-01" });
|
||||
const { patches } = renderDialog(row);
|
||||
|
||||
changeDateCompleted("2026-07-20");
|
||||
fireEvent.click(screen.getByRole("button", { name: /^generate$/i }));
|
||||
|
||||
expect(onDateCompletedChange).toHaveBeenCalledWith("2026-07-20");
|
||||
expect(onSave).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ generatedAt: expect.any(String) }),
|
||||
"Yes",
|
||||
);
|
||||
expect(patches).toEqual([{ docStatus: "Yes" }, { completedDate: "2026-07-20" }]);
|
||||
expect(patches.some((patch) => "scheduledOn" in patch)).toBe(false);
|
||||
expect(patches.some((patch) => patch.status === "Completed")).toBe(false);
|
||||
expect(row.scheduledOn).toBe(SCHEDULED_ON);
|
||||
expect(row.status).toBe("Scheduled");
|
||||
});
|
||||
});
|
||||
|
|
@ -75,6 +75,7 @@ const canceledRow: WorkOrderTableRow = {
|
|||
dispatcherId: "u1",
|
||||
dispatcherName: "Test Dispatcher",
|
||||
scheduledOn: "2026-07-20",
|
||||
completedDate: "",
|
||||
targetWeek: "2026-07-20",
|
||||
dayGroup: "2026-07-20",
|
||||
dayLabel: "Mon",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ function makeRow(overrides: Partial<WorkOrderTableRow> = {}): WorkOrderTableRow
|
|||
dispatcherId: "u1",
|
||||
dispatcherName: "Alice",
|
||||
scheduledOn: "2026-06-23",
|
||||
completedDate: "",
|
||||
targetWeek: "",
|
||||
dayGroup: "06/23",
|
||||
dayLabel: "TUE · 06/23",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue