diff --git a/src/app/(protected)/workorders/[id].tsx b/src/app/(protected)/workorders/[id].tsx
index 9deeb752..fb4d999f 100644
--- a/src/app/(protected)/workorders/[id].tsx
+++ b/src/app/(protected)/workorders/[id].tsx
@@ -1,20 +1,36 @@
-import { Link as RouterLink, useNavigate } from "react-router";
+import { useMemo, useState } from "react";
+import { Link as RouterLink, useNavigate, useParams } from "react-router";
import {
+ Accordion,
+ AccordionDetails,
+ AccordionSummary,
Alert,
Box,
Breadcrumbs,
Button,
+ Chip,
CircularProgress,
Link,
+ Menu,
+ MenuItem,
+ Paper,
Stack,
+ Tab,
+ Tabs,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ TextField,
Typography,
} from "@mui/material";
-import { useState } from "react";
-import { useParams } from "react-router";
+import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { DispatchCreateModal } from "./_components/dispatch-create-modal";
import { DispatchDetailModal } from "./_components/dispatch-detail-modal";
-import { WorkOrderDetailsSection } from "./_components/detail/work-order-detail-sections";
-import { WorkOrderStatusMenu } from "./_components/detail/work-order-status-menu";
+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,
@@ -22,20 +38,167 @@ import {
useChangeWorkOrderStatus,
} from "@/domain/work-orders/use-cases/use-work-order-mutations";
import { useWorkOrderUsers } from "@/domain/work-orders/use-cases/use-work-orders-list";
+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";
+
+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 } = useWorkOrderDetail(workOrderId);
+ 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 [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 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("") },
+ );
+ };
if (isLoading) {
return (
@@ -61,9 +224,14 @@ export default function WorkOrderViewPage() {
Work Orders
- {wo.internalWONumber}
+
+ {wo.internalWONumber}
+
-
{wo.title}
- statusMutation.mutate(status)}
+ setStatusAnchor(event.currentTarget)}
+ className="w-fit cursor-pointer"
/>
+
-
+
@@ -94,27 +290,283 @@ export default function WorkOrderViewPage() {
+
+
+ Details
+
+
+
+
+
+
+
+
+ Assigned To
+
+
+
+
+
+
+
+
+
+ }>
+
+ Classification
+
+
+
+
+
+
+
+
+
+
+
+ }>
+
+ Schedule
+
+
+
+
+
+
+
+
+
+
+
+
+ }>
+
+ Financial
+
+
+
+
+
+
+
+
+
+
+
+ }>
+
+ Dispatches ({wo.dispatches.length})
+
+
+
+ {wo.dispatches.length ? (
+
+
+
+
+ Dispatch #
+ Vendor
+ NTE
+ Status
+ Date
+ Wait
+
+
+
+ {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"
+ >
+
+
+
+
- assignmentMutation.mutate({ userId, userName })}
- onDispatchClick={setDispatchDetailId}
- onCommentSubmit={(text, commentType) => commentMutation.mutate({ text, commentType })}
- isCommentPending={commentMutation.isPending}
- />
+ {activeTab === "customer" && }
+ {activeTab === "internal" && }
+ {activeTab === "audit" && }
+ {activeTab === "internal" && (
+
+ setCommentText(event.target.value)}
+ placeholder="Type a comment..."
+ disabled={commentMutation.isPending}
+ />
+
+
+ )}
+
{dispatchOpen && (
- setDispatchOpen(false)} />
+ setDispatchOpen(false)}
+ onDispatched={() => void refetch()}
+ />
)}
-
{dispatchDetailId && (
setDispatchDetailId(null)}
+ onUpdated={() => void refetch()}
/>
)}
);
}
+
+function Field({ label, value }: { label: string; value: string }) {
+ return (
+
+
+ {label}
+
+ {value}
+
+ );
+}
diff --git a/src/app/(protected)/workorders/index.tsx b/src/app/(protected)/workorders/index.tsx
index 98de7ec7..dd5b0df8 100644
--- a/src/app/(protected)/workorders/index.tsx
+++ b/src/app/(protected)/workorders/index.tsx
@@ -1,191 +1,337 @@
import { useMemo, useState } from "react";
-import { Box } from "@mui/material";
-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 { WorkOrderSlideOver } from "@/app/(protected)/workorders/_components/detail/work-order-slide-over";
-import { NewWoWizard } from "@/app/(protected)/workorders/_components/wizard/new-wo-wizard";
-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";
-import { WorkOrdersFilterBar } from "@/app/(protected)/workorders/_components/list/filter-bar/work-orders-filter-bar";
-import { WorkOrdersListHeader } from "@/app/(protected)/workorders/_components/list/work-orders-list-header";
-import { WoTable } from "@/app/(protected)/workorders/_components/list/table/wo-table";
-import { useWorkOrdersListFilters } from "@/app/(protected)/workorders/_hooks/use-work-orders-list-filters";
-import { useWorkOrdersTableData } from "@/app/(protected)/workorders/_hooks/use-work-orders-table-data";
-import { useDropdownOptionsByCategory } from "@/domain/settings/dropdown-options/use-cases/use-dropdown-options-by-category";
+import { useNavigate } from "react-router";
+import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
+import VisibilityOutlinedIcon from "@mui/icons-material/VisibilityOutlined";
+import { WorkOrderStatusBadge } from "@/components/domain/status-badge";
import {
- useWorkOrderLocations,
- useWorkOrderVendors,
-} from "@/domain/work-orders/use-cases/use-work-order-detail";
-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";
-import { useAuthContext } from "@/providers/auth-context";
+ Alert,
+ Box,
+ Button,
+ Chip,
+ CircularProgress,
+ FormControl,
+ IconButton,
+ InputLabel,
+ MenuItem,
+ Paper,
+ Select,
+ Stack,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TablePagination,
+ TableRow,
+ TextField,
+ Tooltip,
+ Typography,
+} from "@mui/material";
+import { useDebounce } from "@/hooks/use-debounce";
+import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order";
+import {
+ useWorkOrderUsers,
+ useWorkOrdersList,
+} from "@/domain/work-orders/use-cases/use-work-orders-list";
+
+const STATUS_OPTIONS = ["Open", "In Progress", "On Hold", "Done", "Cancelled"] as const;
+
+function formatDateTime(value: string): string {
+ if (!value) {
+ return "—";
+ }
+
+ const normalized = value.endsWith("Z") ? value : `${value}Z`;
+ const date = new Date(normalized);
+
+ if (Number.isNaN(date.getTime())) {
+ return value;
+ }
+
+ const datePart = date.toLocaleDateString("en-CA");
+ const timePart = date.toLocaleTimeString(undefined, {
+ hour: "2-digit",
+ minute: "2-digit",
+ hour12: false,
+ });
+ const tz = date.toLocaleTimeString(undefined, { timeZoneName: "short" }).split(" ").pop();
+
+ return `${datePart} ${timePart} ${tz ?? ""}`.trim();
+}
export default function WorkOrdersListPage() {
- const { user } = useAuthContext();
- const [wizardOpen, setWizardOpen] = useState(false);
- const [slideOverRow, setSlideOverRow] = useState(null);
- const [slideOverEdit, setSlideOverEdit] = useState(false);
- const [docRow, setDocRow] = useState(null);
- const [mediaRow, setMediaRow] = useState(null);
- const [completionDocs, setCompletionDocs] = useState>({});
- const [mediaFiles, setMediaFiles] = useState>({});
- const filters = useWorkOrdersListFilters(user?.id);
- const tableData = useWorkOrdersTableData(filters);
- const { patchField } = useWorkOrderTableMutations(tableData.patchRow);
+ const navigate = useNavigate();
+ const [search, setSearch] = useState("");
+ const [statusFilter, setStatusFilter] = useState("");
+ const [assigneeFilter, setAssigneeFilter] = useState("");
+ const [page, setPage] = useState(0);
+ const [pageSize, setPageSize] = useState(12);
+ const [sortBy, setSortBy] = useState("");
+ const [sortDir, setSortDir] = useState<"asc" | "desc" | "">("");
+ const debouncedSearch = useDebounce(search, 300);
+
+ const listParams = useMemo(
+ () => ({
+ search: debouncedSearch,
+ page: page + 1,
+ pageSize,
+ sortBy,
+ sortDir,
+ status: statusFilter,
+ assignee: assigneeFilter,
+ }),
+ [debouncedSearch, page, pageSize, sortBy, sortDir, statusFilter, assigneeFilter],
+ );
+
+ const { data, isLoading, isFetching, error } = useWorkOrdersList(listParams);
const { data: users = [] } = useWorkOrderUsers();
- const { data: locations = [] } = useWorkOrderLocations();
- const { data: vendors = [] } = useWorkOrderVendors();
- const { data: problemOptions = [] } = useDropdownOptionsByCategory("Problem");
- const pmTypes = useMemo(
- () =>
- isWoTableMockEnabled()
- ? getMockPmTypes()
- : problemOptions.map((option) => option.value).filter(Boolean),
- [problemOptions],
- );
+ const rows = data?.items ?? [];
+ const totalCount = data?.totalCount ?? 0;
- const vendorTechs = useMemo(
- () => [...new Set(vendors.map((vendor) => vendor.companyName).filter(Boolean))],
- [vendors],
- );
+ const handleSort = (column: string) => {
+ if (sortBy === column) {
+ setSortDir((current) => (current === "asc" ? "desc" : current === "desc" ? "" : "asc"));
+ if (sortDir === "desc") {
+ setSortBy("");
+ }
+ return;
+ }
- const totalCount = tableData.totalCount;
- const scopeTotalCount = tableData.scopeTotalCount;
+ setSortBy(column);
+ setSortDir("asc");
+ };
- const activeDocRow = useMemo(() => {
- if (!docRow) return null;
- return tableData.rows.find((row) => row.id === docRow.id) ?? docRow;
- }, [docRow, tableData.rows]);
+ const sortIndicator = (column: string) => {
+ if (sortBy !== column) {
+ return "";
+ }
+ return sortDir === "desc" ? " ↓" : " ↑";
+ };
- const activeMediaRow = useMemo(() => {
- if (!mediaRow) return null;
- return tableData.rows.find((row) => row.id === mediaRow.id) ?? mediaRow;
- }, [mediaRow, tableData.rows]);
-
- const handleOpenSlideOver = (row: WorkOrderTableRow, edit = false) => {
- setSlideOverRow(row);
- setSlideOverEdit(edit);
+ const handleStatusFilter = (status: string) => {
+ setStatusFilter((current) => (current === status ? "" : status));
+ setPage(0);
};
return (
-
- filters.setAdvOpen(true)}
- onNewWorkOrder={() => setWizardOpen(true)}
- activeFilterCount={filters.activeFilterCount}
- isFetching={tableData.isFetching && !tableData.isLoading}
- />
- }
- />
-
-
-
- {filters.advApplied && (
-
- )}
-
- filters.handleSearchChange("")}
- onOpenAdvancedFilters={() => filters.setAdvOpen(true)}
- />
-
-
-
-
-
- {
- setSlideOverRow(null);
- setSlideOverEdit(false);
- tableData.setSelectedId(null);
+
+
+
+
+ Work Orders
+
+
+ {totalCount} total
+
+ {isFetching && !isLoading && }
+
+
+
+ {
+ setSearch(event.target.value);
+ setPage(0);
+ }}
+ placeholder="Search..."
+ className="min-w-[220px]"
+ />
+
+
+
+
+ {
+ setStatusFilter("");
+ setPage(0);
+ }}
+ />
+ {STATUS_OPTIONS.map((status) => (
+ handleStatusFilter(status)}
+ />
+ ))}
+
+
+ Assignee
+
+
+
+ {error && (
+
+ {error instanceof Error ? error.message : "Failed to load work orders"}
+
+ )}
+
+
+
+
+ handleSort("number")}
+ >
+ WO #{sortIndicator("number")}
+
+ handleSort("title")}
+ >
+ Title{sortIndicator("title")}
+
+ Location
+ Priority
+ Status
+ Created
+ Assigned To
+ Last Updated
+
+ Actions
+
+
+
+
+ {isLoading ? (
+
+
+
+
+
+ ) : rows.length === 0 ? (
+
+
+
+ No work orders found
+
+
+
+ ) : (
+ rows.map((row: WorkOrderTableRow) => (
+
+
+
+
+ {row.title}
+ {row.location}
+ {row.priority}
+
+
+
+
+ {formatDateTime(row.scheduledOn)}
+
+ {row.tech || "Unassigned"}
+ {formatDateTime(row.dueDate)}
+
+
+ navigate(`/workorders/${row.id}`)}
+ >
+
+
+
+
+ navigate(`/workorders/edit/${row.id}`)}
+ >
+
+
+
+
+
+ ))
+ )}
+
+
+
+ setPage(nextPage)}
+ rowsPerPage={pageSize}
+ onRowsPerPageChange={(event) => {
+ setPageSize(Number(event.target.value));
+ setPage(0);
+ }}
+ rowsPerPageOptions={[12, 24, 48]}
/>
-
- {activeMediaRow && (
- {
- if (!open) setMediaRow(null);
- }}
- uploads={mediaFiles[String(activeMediaRow.id)] ?? EMPTY_MEDIA}
- onChange={(files) => {
- setMediaFiles((prev) => ({ ...prev, [String(activeMediaRow.id)]: files }));
- patchField(activeMediaRow.id, { mediaCount: files.length });
- }}
- />
- )}
-
- {activeDocRow && (
- {
- if (!open) setDocRow(null);
- }}
- data={completionDocs[String(activeDocRow.id)]}
- onSave={(data, status) => {
- setCompletionDocs((prev) => ({ ...prev, [String(activeDocRow.id)]: data }));
- patchField(activeDocRow.id, { docStatus: status });
- }}
- onStatusChange={(status) => patchField(activeDocRow.id, { status })}
- onDateCompletedChange={(date) => patchField(activeDocRow.id, { scheduledOn: date })}
- />
- )}
);
}