From 725efd2703be0cb410f69baeabc1a07d145d3263 Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Mon, 27 Jul 2026 09:47:46 -0300 Subject: [PATCH 01/17] 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. --- .../_components/comp-doc/comp-doc-co-line.tsx | 19 + .../comp-doc/comp-doc-detail-row.tsx | 39 ++ .../_components/comp-doc/comp-doc-dialog.tsx | 398 ++++++++++++++++++ .../_components/comp-doc/comp-doc-preview.tsx | 295 +++++++++++++ .../comp-doc/comp-doc-procedure-list.tsx | 35 ++ .../_components/comp-doc/comp-doc-sidebar.tsx | 207 +++++++++ src/app/(protected)/workorders/index.tsx | 26 ++ .../work-orders/mappers/work-order-mapper.ts | 1 + .../work-orders/mocks/wo-table-mock-data.ts | 1 + .../work-orders/types/work-order-table-row.ts | 2 + .../use-work-order-table-mutations.ts | 1 + ...mp-doc-dialog-completion-contract.test.tsx | 129 ++++++ ...ork-orders-board-core-affordances.test.tsx | 1 + .../utils/table-filter-utils.test.ts | 1 + 14 files changed, 1155 insertions(+) create mode 100644 src/app/(protected)/workorders/_components/comp-doc/comp-doc-co-line.tsx create mode 100644 src/app/(protected)/workorders/_components/comp-doc/comp-doc-detail-row.tsx create mode 100644 src/app/(protected)/workorders/_components/comp-doc/comp-doc-dialog.tsx create mode 100644 src/app/(protected)/workorders/_components/comp-doc/comp-doc-preview.tsx create mode 100644 src/app/(protected)/workorders/_components/comp-doc/comp-doc-procedure-list.tsx create mode 100644 src/app/(protected)/workorders/_components/comp-doc/comp-doc-sidebar.tsx create mode 100644 src/test/app/(protected)/workorders/comp-doc-dialog-completion-contract.test.tsx diff --git a/src/app/(protected)/workorders/_components/comp-doc/comp-doc-co-line.tsx b/src/app/(protected)/workorders/_components/comp-doc/comp-doc-co-line.tsx new file mode 100644 index 00000000..5d3804bb --- /dev/null +++ b/src/app/(protected)/workorders/_components/comp-doc/comp-doc-co-line.tsx @@ -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 ( +
+ {text} + +
+ ); +} diff --git a/src/app/(protected)/workorders/_components/comp-doc/comp-doc-detail-row.tsx b/src/app/(protected)/workorders/_components/comp-doc/comp-doc-detail-row.tsx new file mode 100644 index 00000000..c5f08558 --- /dev/null +++ b/src/app/(protected)/workorders/_components/comp-doc/comp-doc-detail-row.tsx @@ -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 ( +
+ +
+
{label}
+
+ {value} +
+
+
+ ); +} diff --git a/src/app/(protected)/workorders/_components/comp-doc/comp-doc-dialog.tsx b/src/app/(protected)/workorders/_components/comp-doc/comp-doc-dialog.tsx new file mode 100644 index 00000000..833f9047 --- /dev/null +++ b/src/app/(protected)/workorders/_components/comp-doc/comp-doc-dialog.tsx @@ -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("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, 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( +
+ Draft saved +
, + { + 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 ( + 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) => e.stopPropagation(), + }, + }} + > + + Completion Document — WO #{row.woNumber} + + Generate, save or mark not-required the completion document for this work order. + + +
+
+
+ +
+
+ Completion Document +
+
+ WO{" "} + + #{row.woNumber} + + {" · "} + {serviceTitle(row)} +
+
+
+
+ + + {cfg.pillLabel} + + +
+
+ +
+ + +
+ +
+ {docState === "draft" && ( + <> + +
+ + +
+ + )} + {docState === "generated" && ( + <> + + Generated {generatedAt} + +
+ +
+ + )} + {docState === "not-required" && ( + <> + + Marked{" "} + not required for this work order + +
+ + +
+ + )} +
+
+
+
+ ); +} diff --git a/src/app/(protected)/workorders/_components/comp-doc/comp-doc-preview.tsx b/src/app/(protected)/workorders/_components/comp-doc/comp-doc-preview.tsx new file mode 100644 index 00000000..9769b8b4 --- /dev/null +++ b/src/app/(protected)/workorders/_components/comp-doc/comp-doc-preview.tsx @@ -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 ( +
+
+ + {isGenerated ? ( + + ) : ( + + )} + {isGenerated ? "Generated document" : "Preview — not yet generated"} + +
+ +
+
+
+
+
+
+
+ Seahaven Industries +
+
+ + + + + +
+
+
+ + {isGenerated && ( +
+ Generated · {generatedAt} · v1 +
+ )} + + + {serviceTitle(row).toUpperCase()} + + + WO# {row.woNumber} + + +
+ + Site ID — {row.site} + + + Amazon.com Services LLC {row.site} + + + {site.street}, + + + {site.label} + +
+ + + Vendor / Technician:  + {row.tech || "—"} + {row.company ? ` · ${row.company}` : ""} + + +
    + {SAFETY_BULLETS.map((b, i) => ( +
  • + {b} +
  • + ))} +
+ + + {proc.heading} + + + {proc.intro} + + + + {notes.length > 0 && ( + <> + + PM Comments + + + {notes} + + + )} +
+
+
+ + {isNotRequired && ( +
+
+ NOT REQUIRED +
+
+ )} +
+
+
+ ); +} diff --git a/src/app/(protected)/workorders/_components/comp-doc/comp-doc-procedure-list.tsx b/src/app/(protected)/workorders/_components/comp-doc/comp-doc-procedure-list.tsx new file mode 100644 index 00000000..a16fbbbe --- /dev/null +++ b/src/app/(protected)/workorders/_components/comp-doc/comp-doc-procedure-list.tsx @@ -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 ( +
    + {proc.steps.map((st, i) => { + const subSteps = st.sub ?? []; + return ( +
  1. + {st.text} + {subSteps.length > 0 && ( +
      + {subSteps.map((x, j) => ( +
    1. + {x} +
    2. + ))} +
    + )} +
  2. + ); + })} +
+ ); +} diff --git a/src/app/(protected)/workorders/_components/comp-doc/comp-doc-sidebar.tsx b/src/app/(protected)/workorders/_components/comp-doc/comp-doc-sidebar.tsx new file mode 100644 index 00000000..8e3b2c47 --- /dev/null +++ b/src/app/(protected)/workorders/_components/comp-doc/comp-doc-sidebar.tsx @@ -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 = "", + ) => ( +
+
+ {label} +
+ 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, + } + : {}), + }} + /> +
+ ); + + return ( +
+
+
+
+ +
+
+
+ {cfg.cardTitle} +
+
+ {docStatusCardDescription(docState, generatedAt)} +
+
+
+ +
+
+ Work Order Details +
+
+ + + + {editRow("Date Work Completed", editDateCompleted, onDateCompletedChange, "date")} + + +
+
+ +
+ +