import { useMemo, useState } from "react"; import { Link as RouterLink, useNavigate, useParams } from "react-router"; import { Accordion, AccordionDetails, AccordionSummary, Alert, Box, Breadcrumbs, Button, Chip, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, Link, Menu, MenuItem, Paper, Stack, Tab, Tabs, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Typography, } from "@mui/material"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { DispatchCreateModal } from "./_components/dispatch-create-modal"; import { DispatchDetailModal } from "./_components/dispatch-detail-modal"; import type { WorkOrderAuditEntry, WorkOrderComment } from "@/domain/work-orders/types/work-order"; import { WORK_ORDER_STATUS_OPTIONS } from "@/domain/work-orders/types/work-order"; import { useWorkOrderDetail } from "@/domain/work-orders/use-cases/use-work-order-detail"; import { useAddWorkOrderComment, useChangeWorkOrderAssignment, useChangeWorkOrderStatus, useUpdateWorkOrderVendor, } from "@/domain/work-orders/use-cases/use-work-order-mutations"; import { useWorkOrderUsers } from "@/domain/work-orders/use-cases/use-work-orders-list"; import { vendorsApi } from "@/domain/vendors/api/vendors-api"; import type { VendorPreferredContact } from "@/domain/vendors/types/vendor"; import { formatDateTime, timeSince, waitTimeColor } from "@/lib/time-utils"; const STATUS_COLORS: Record< string, "default" | "primary" | "secondary" | "error" | "info" | "success" | "warning" > = { Open: "primary", "In Progress": "info", "On Hold": "warning", Done: "success", Cancelled: "default", }; type ActivityTab = "customer" | "internal" | "audit"; interface VendorEditorState { vendorId: string | number; vendorName: string; contactName: string; preferredContact: VendorPreferredContact; phone: string; email: string; notes: string; } function formatPhoneMask(input: string): string { const digits = input.replace(/\D/g, "").slice(0, 10); if (digits.length <= 3) return digits; if (digits.length <= 6) return `(${digits.slice(0, 3)}) ${digits.slice(3)}`; return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`; } function isVendorEditorValid(editor: VendorEditorState): boolean { const phoneValid = editor.phone === "" || /^\(\d{3}\) \d{3}-\d{4}$/.test(editor.phone); const emailValid = editor.email === "" || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(editor.email); return ( phoneValid && emailValid && editor.notes.length <= 2000 && editor.contactName.length <= 200 ); } function CommentTimeline({ comments }: { comments: WorkOrderComment[] }) { const sorted = useMemo( () => [...comments].sort( (a, b) => new Date(a.createdDate).getTime() - new Date(b.createdDate).getTime(), ), [comments], ); if (!sorted.length) { return ( No comments yet. ); } return ( {sorted.map((comment) => ( {comment.userName || comment.commenter || "System"} {comment.createdDate ? formatDateTime(comment.createdDate) : ""} {comment.commenttext} ))} ); } function AuditTimeline({ entries }: { entries: WorkOrderAuditEntry[] }) { const sorted = useMemo( () => [...entries].sort( (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(), ), [entries], ); if (!sorted.length) { return ( No changes recorded. ); } return ( {sorted.map((entry) => ( {entry.userName || "System"} {entry.createdAt ? formatDateTime(entry.createdAt) : ""} {entry.fieldName} {entry.oldValue} → {entry.newValue} ))} ); } export default function WorkOrderViewPage() { const { id } = useParams(); const navigate = useNavigate(); const workOrderId = id ?? ""; const { data: wo, isLoading, error, refetch } = useWorkOrderDetail(workOrderId); const { data: users = [] } = useWorkOrderUsers(); const statusMutation = useChangeWorkOrderStatus(workOrderId); const assignmentMutation = useChangeWorkOrderAssignment(workOrderId); const commentMutation = useAddWorkOrderComment(workOrderId); const vendorMutation = useUpdateWorkOrderVendor(workOrderId); const [activeTab, setActiveTab] = useState("customer"); const [commentText, setCommentText] = useState(""); const [dispatchOpen, setDispatchOpen] = useState(false); const [dispatchDetailId, setDispatchDetailId] = useState(null); const [statusAnchor, setStatusAnchor] = useState(null); const [assignAnchor, setAssignAnchor] = useState(null); const [vendorEditor, setVendorEditor] = useState(null); const [vendorEditorLoading, setVendorEditorLoading] = useState(false); const [vendorEditorError, setVendorEditorError] = useState(""); const customerComments = useMemo( () => (wo?.comments ?? []).filter((comment) => comment.commentType === "customer"), [wo?.comments], ); const internalComments = useMemo( () => (wo?.comments ?? []).filter((comment) => comment.commentType === "internal"), [wo?.comments], ); const auditLog = wo?.auditLog ?? []; const handleCommentSubmit = (event: React.FormEvent) => { event.preventDefault(); if (!commentText.trim()) { return; } if (activeTab !== "customer" && activeTab !== "internal") { return; } commentMutation.mutate( { text: commentText.trim(), commentType: activeTab }, { onSuccess: () => setCommentText("") }, ); }; const openVendorEditor = async ( event: React.MouseEvent, vendorId: string | number, vendorName: string, ) => { event.stopPropagation(); setVendorEditorError(""); setVendorEditorLoading(true); setVendorEditor({ vendorId, vendorName, contactName: "", preferredContact: "Phone", phone: "", email: "", notes: "", }); try { const vendor = await vendorsApi.getById(vendorId); setVendorEditor({ vendorId, vendorName: vendor.companyName || vendorName, contactName: vendor.contactName, preferredContact: vendor.preferredContact, phone: vendor.phone, email: vendor.email, notes: vendor.notes, }); } catch (vendorError) { setVendorEditorError( vendorError instanceof Error ? vendorError.message : "Unable to load vendor details", ); } finally { setVendorEditorLoading(false); } }; const saveVendorEditor = () => { if (!vendorEditor || !isVendorEditorValid(vendorEditor)) return; const numericWorkOrderId = Number(workOrderId); if (!Number.isInteger(numericWorkOrderId) || numericWorkOrderId <= 0) { setVendorEditorError("A valid work order is required"); return; } vendorMutation.mutate( { vendorId: vendorEditor.vendorId, payload: { workOrderId: numericWorkOrderId, contactName: vendorEditor.contactName.trim(), preferredContact: vendorEditor.preferredContact, phone: vendorEditor.phone, email: vendorEditor.email.trim(), notes: vendorEditor.notes.trim(), }, }, { onSuccess: () => { setVendorEditor(null); void refetch(); }, onError: (vendorError) => setVendorEditorError(vendorError.message), }, ); }; if (isLoading) { return ( ); } if (error || !wo) { return ( {error instanceof Error ? error.message : "Work order not found."} ); } return ( Work Orders {wo.internalWONumber} {wo.title} setStatusAnchor(event.currentTarget)} className="w-fit cursor-pointer" /> setStatusAnchor(null)} > {WORK_ORDER_STATUS_OPTIONS.map((status) => ( { setStatusAnchor(null); if (status !== wo.status) { statusMutation.mutate(status); } }} > {status} ))} Details Assigned To setAssignAnchor(null)} > { setAssignAnchor(null); assignmentMutation.mutate({ userId: "", userName: "" }); }} > Unassigned {users.map((user) => ( { setAssignAnchor(null); assignmentMutation.mutate({ userId: user.id, userName: user.name }); }} > {user.name} ))} }> Classification }> Schedule }> Financial }> Dispatches ({wo.dispatches.length}) {wo.dispatches.length ? ( Dispatch # Vendor NTE Status Date Wait Actions {wo.dispatches.map((dispatch) => { const waitColor = waitTimeColor(dispatch.dispatchedAt); return ( setDispatchDetailId(dispatch.id)} > {dispatch.dispatchNumber || dispatch.poNumber} {dispatch.vendorName} {dispatch.nteAmount != null ? `$${dispatch.nteAmount.toFixed(2)}` : "—"} {dispatch.status} {dispatch.dispatchedAt ? formatDateTime(dispatch.dispatchedAt) : "—"} {dispatch.status === "Sent" ? ( {timeSince(dispatch.dispatchedAt)} ) : ( "—" )} ); })}
) : ( No dispatches yet. )}
}> Description {wo.description || "No description provided."} Activity setActiveTab(value)} className="mb-3" > {activeTab === "customer" && } {activeTab === "internal" && } {activeTab === "audit" && } {activeTab === "internal" && ( setCommentText(event.target.value)} placeholder="Type a comment..." disabled={commentMutation.isPending} /> )} {dispatchOpen && ( setDispatchOpen(false)} onDispatched={() => void refetch()} /> )} {dispatchDetailId != null && ( setDispatchDetailId(null)} onUpdated={() => void refetch()} /> )} { if (!vendorMutation.isPending) setVendorEditor(null); }} fullWidth maxWidth="sm" > Edit assigned vendor {vendorEditor?.vendorName} {Boolean(vendorEditorError) && {vendorEditorError}} {vendorEditorLoading ? ( ) : ( <> setVendorEditor((current) => current ? { ...current, contactName: event.target.value } : current, ) } slotProps={{ htmlInput: { maxLength: 200 } }} /> setVendorEditor((current) => current ? { ...current, preferredContact: event.target.value as VendorPreferredContact, } : current, ) } > Phone Email Text setVendorEditor((current) => current ? { ...current, phone: formatPhoneMask(event.target.value) } : current, ) } error={Boolean( vendorEditor?.phone && !/^\(\d{3}\) \d{3}-\d{4}$/.test(vendorEditor.phone), )} helperText="Use (XXX) XXX-XXXX" /> setVendorEditor((current) => current ? { ...current, email: event.target.value } : current, ) } /> setVendorEditor((current) => current ? { ...current, notes: event.target.value } : current, ) } slotProps={{ htmlInput: { maxLength: 2000 } }} /> )}
); } function Field({ label, value }: { label: string; value: string }) { return ( {label} {value} ); }