diff --git a/src/app/(protected)/workorders/[id].tsx b/src/app/(protected)/workorders/[id].tsx index 945bd3b3..e30574da 100644 --- a/src/app/(protected)/workorders/[id].tsx +++ b/src/app/(protected)/workorders/[id].tsx @@ -14,7 +14,12 @@ import { WorkOrderDispatchesAccordion } from "@/app/(protected)/workorders/_comp import { WorkOrderActivityPanel } from "@/app/(protected)/workorders/_components/view/work-order-activity-panel"; import { WorkOrderVendorEditorDialog } from "@/app/(protected)/workorders/_components/view/work-order-vendor-editor-dialog"; import { useWorkOrderViewController } from "@/app/(protected)/workorders/_hooks/use-work-order-view-controller"; - +import { + detailToClosabilityInput, + getClosabilityGaps, + isCompletionStatusLabel, +} from "@/domain/work-orders/utils/wo-closability"; +import type { WorkOrderStatus } from "@/domain/work-orders/types/work-order"; export default function WorkOrderViewPage() { const { id } = useParams(); const navigate = useNavigate(); @@ -41,13 +46,21 @@ export default function WorkOrderViewPage() { return ; } + const handleStatusChange = (status: WorkOrderStatus) => { + const isCompletionStatus = isCompletionStatusLabel(status); + if (isCompletionStatus && getClosabilityGaps(detailToClosabilityInput(wo)).length > 0) { + return; + } + controller.statusMutation.mutate(status); + }; + return ( controller.statusMutation.mutate(status)} + onStatusChange={handleStatusChange} onDispatch={() => controller.setDispatchOpen(true)} onEdit={() => navigate(`/workorders/edit/${id}`)} onBack={() => navigate("/workorders")} diff --git a/src/app/(protected)/workorders/_components/detail/work-order-status-menu.tsx b/src/app/(protected)/workorders/_components/detail/work-order-status-menu.tsx index 15945501..e01cd994 100644 --- a/src/app/(protected)/workorders/_components/detail/work-order-status-menu.tsx +++ b/src/app/(protected)/workorders/_components/detail/work-order-status-menu.tsx @@ -2,20 +2,29 @@ import { Menu, MenuItem } from "@mui/material"; import { WorkOrderStatusBadge } from "@/components/domain/status-badge"; import { MenuAnchorButton } from "@/components/ui/menu-anchor-button"; import { WORK_ORDER_STATUS_OPTIONS } from "@/domain/work-orders/types/work-order"; +import { + formatClosabilityMissingTitle, + getClosabilityGaps, + isCompletionStatusLabel, + type WoClosabilityInput, +} from "@/domain/work-orders/utils/wo-closability"; import { useMenuAnchor } from "@/hooks/use-menu-anchor"; type WorkOrderStatusMenuProps = { status: string; disabled?: boolean; + closability: WoClosabilityInput; onStatusChange: (status: string) => void; }; export function WorkOrderStatusMenu({ status, disabled = false, + closability, onStatusChange, }: WorkOrderStatusMenuProps) { const { anchorEl, open, setAnchor, close } = useMenuAnchor(); + const closabilityGaps = getClosabilityGaps(closability); return ( <> @@ -29,21 +38,31 @@ export function WorkOrderStatusMenu({ - {WORK_ORDER_STATUS_OPTIONS.map((option) => ( - { - close(); - if (option !== status) { - onStatusChange(option); + {WORK_ORDER_STATUS_OPTIONS.map((option) => { + const isCompletionStatus = isCompletionStatusLabel(option); + const isCompletedBlocked = isCompletionStatus && closabilityGaps.length > 0; + return ( + - {option} - - ))} + onClick={() => { + if (isCompletedBlocked) { + return; + } + close(); + if (option !== status) { + onStatusChange(option); + } + }} + > + {option} + + ); + })} ); diff --git a/src/app/(protected)/workorders/_components/list/table/cells/status-cell.tsx b/src/app/(protected)/workorders/_components/list/table/cells/status-cell.tsx index 733d17d0..04f73382 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/status-cell.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/status-cell.tsx @@ -11,6 +11,10 @@ import { } from "@/domain/work-orders/types/work-order-wizard"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; +import { + formatClosabilityMissingTitle, + getClosabilityGaps, +} from "@/domain/work-orders/utils/wo-closability"; import { HoverChevron } from "./hover-chevron"; @@ -36,6 +40,7 @@ export function StatusCell({ onPastDueAttempt, }: StatusCellProps) { const [anchorEl, setAnchorEl] = useState(null); + const closabilityGaps = getClosabilityGaps(wo); if (wo.isPastDue) { return ( @@ -135,24 +140,39 @@ export function StatusCell({ onClick={(e) => e.stopPropagation()} slotProps={{ paper: { className: "min-w-[168px] rounded-lg border border-border p-1" } }} > - {ALL_WIZARD_STATUSES.map((status) => ( - - ))} + {wo.status === status && ( + + )} + + ); + })} ); diff --git a/src/app/(protected)/workorders/_components/list/table/to-inline-draft-row.ts b/src/app/(protected)/workorders/_components/list/table/to-inline-draft-row.ts index 1ec7ee46..283b4ebd 100644 --- a/src/app/(protected)/workorders/_components/list/table/to-inline-draft-row.ts +++ b/src/app/(protected)/workorders/_components/list/table/to-inline-draft-row.ts @@ -15,7 +15,9 @@ export function toInlineDraftRow(draft: WorkOrderTableDraft) { site: draft.site, location: draft.location, dispatcherId: draft.dispatcherId, + dispatcherName: "", scheduledOn: draft.scheduledOn, + completedDate: "", dueDate: draft.dueDate, pm: draft.pm, serviceNotes: draft.serviceNotes, diff --git a/src/app/(protected)/workorders/_components/view/work-order-header-section.tsx b/src/app/(protected)/workorders/_components/view/work-order-header-section.tsx index 0fa4ede7..7fda2c33 100644 --- a/src/app/(protected)/workorders/_components/view/work-order-header-section.tsx +++ b/src/app/(protected)/workorders/_components/view/work-order-header-section.tsx @@ -5,6 +5,12 @@ import { WORK_ORDER_STATUS_OPTIONS, type WorkOrderStatus, } from "@/domain/work-orders/types/work-order"; +import { + detailToClosabilityInput, + formatClosabilityMissingTitle, + getClosabilityGaps, + isCompletionStatusLabel, +} from "@/domain/work-orders/utils/wo-closability"; const STATUS_COLORS: Record< string, @@ -36,6 +42,8 @@ export function WorkOrderHeaderSection({ onEdit, onBack, }: WorkOrderHeaderSectionProps) { + const closabilityGaps = getClosabilityGaps(detailToClosabilityInput(wo)); + return ( <> @@ -73,20 +81,31 @@ export function WorkOrderHeaderSection({ open={Boolean(statusAnchor)} onClose={() => onStatusAnchorChange(null)} > - {WORK_ORDER_STATUS_OPTIONS.map((status) => ( - { - onStatusAnchorChange(null); - if (status !== wo.status) { - onStatusChange(status); + {WORK_ORDER_STATUS_OPTIONS.map((status) => { + const isCompletionStatus = isCompletionStatusLabel(status); + const isCompletedBlocked = isCompletionStatus && closabilityGaps.length > 0; + return ( + - {status} - - ))} + onClick={() => { + if (isCompletedBlocked) { + return; + } + onStatusAnchorChange(null); + if (status !== wo.status) { + onStatusChange(status); + } + }} + > + {status} + + ); + })} diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-field-select-options.tsx b/src/app/(protected)/workorders/_components/wizard/wizard-field-select-options.tsx index 8eba4e0e..7c2771d7 100644 --- a/src/app/(protected)/workorders/_components/wizard/wizard-field-select-options.tsx +++ b/src/app/(protected)/workorders/_components/wizard/wizard-field-select-options.tsx @@ -53,8 +53,17 @@ export function WizardFieldSelectOptions({