diff --git a/src/app/(protected)/vendors/index.tsx b/src/app/(protected)/vendors/index.tsx index 94be788d..e7e0d746 100644 --- a/src/app/(protected)/vendors/index.tsx +++ b/src/app/(protected)/vendors/index.tsx @@ -535,19 +535,21 @@ export default function VendorsListPage() { )} - {impactError && !isLoadingImpact && ( + {Boolean(impactError) && !isLoadingImpact && ( Could not verify linked work orders. Close and try again. )} - {deactivationImpact && !isLoadingImpact && !deactivationImpact.canDeactivate && ( - - This vendor cannot be deactivated because it still has open work orders. - - )} + {deactivationImpact != null && + !isLoadingImpact && + !deactivationImpact.canDeactivate && ( + + This vendor cannot be deactivated because it still has open work orders. + + )} - {deactivationImpact && deactivationImpact.openWorkOrders.length > 0 && ( + {deactivationImpact != null && deactivationImpact.openWorkOrders.length > 0 && ( Open work orders ({deactivationImpact.openWorkOrders.length}) @@ -571,7 +573,7 @@ export default function VendorsListPage() { ? `${wo.workOrderNumber} — ${wo.workOrderTitle || "Untitled"}` : wo.workOrderTitle || `Work order ${wo.workOrderId}`} - {wo.status && ( + {Boolean(wo.status) && ( {wo.status} @@ -583,7 +585,7 @@ export default function VendorsListPage() { )} - {deleteError && {deleteError}} + {Boolean(deleteError) && {deleteError}} diff --git a/src/app/(protected)/workorders/[id].tsx b/src/app/(protected)/workorders/[id].tsx index 3e10b6c1..f7350669 100644 --- a/src/app/(protected)/workorders/[id].tsx +++ b/src/app/(protected)/workorders/[id].tsx @@ -226,12 +226,12 @@ export default function WorkOrderViewPage() { if (!commentText.trim()) { return; } + if (activeTab !== "customer" && activeTab !== "internal") { + return; + } commentMutation.mutate( - { - text: commentText.trim(), - commentType: activeTab === "customer" ? "customer" : "internal", - }, + { text: commentText.trim(), commentType: activeTab }, { onSuccess: () => setCommentText("") }, ); }; @@ -680,7 +680,7 @@ export default function WorkOrderViewPage() { {vendorEditor?.vendorName} - {vendorEditorError && {vendorEditorError}} + {Boolean(vendorEditorError) && {vendorEditorError}} {vendorEditorLoading ? ( diff --git a/src/app/(protected)/workorders/_components/list/filter-bar/dispatcher-filter.tsx b/src/app/(protected)/workorders/_components/list/filter-bar/dispatcher-filter.tsx new file mode 100644 index 00000000..a86e37f5 --- /dev/null +++ b/src/app/(protected)/workorders/_components/list/filter-bar/dispatcher-filter.tsx @@ -0,0 +1,168 @@ +import { useMemo } from "react"; +import { Popover } from "@mui/material"; +import { ChevronDown, UserX, Users } from "lucide-react"; +import { ASSIGNEE_FILTER_UNASSIGNED } from "@/domain/work-orders/types/work-order-list"; +import type { WorkOrderUser } from "@/domain/work-orders/types/work-order"; +import { + MOCK_DISPATCHER_USERS, + getDispatcherAvatarColor, +} from "@/domain/work-orders/mocks/dispatcher-filter-mock-data"; +import { CheckboxMenuItem } from "@/components/ui/checkbox-menu-item"; +import { MenuAnchorButton } from "@/components/ui/menu-anchor-button"; +import { useMenuAnchor } from "@/hooks/use-menu-anchor"; + +type DispatcherFilterProps = { + selectedIds: Set; + onChange: (ids: Set) => void; + users: WorkOrderUser[]; + currentUserId?: string | number; +}; + +function userInitials(name: string): string { + const parts = name.trim().split(/\s+/).filter(Boolean); + if (parts.length === 0) return "?"; + if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase(); + return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase(); +} + +function DispatcherAvatar({ name, color }: { name: string; color: string }) { + return ( + + {userInitials(name)} + + ); +} + +export function DispatcherFilter({ + selectedIds, + onChange, + users, + currentUserId, +}: DispatcherFilterProps) { + const { anchorEl, open, setAnchor, close } = useMenuAnchor(); + + const meId = currentUserId != null && currentUserId !== "" ? String(currentUserId) : null; + const allSelected = selectedIds.size === 0; + const meOnly = meId != null && selectedIds.size === 1 && selectedIds.has(meId); + const unassignedOnly = selectedIds.size === 1 && selectedIds.has(ASSIGNEE_FILTER_UNASSIGNED); + + const label = useMemo(() => { + if (allSelected) return "All dispatchers"; + if (meOnly) return "My WOs"; + if (unassignedOnly) return "Unassigned"; + return `${selectedIds.size} selected`; + }, [allSelected, meOnly, unassignedOnly, selectedIds.size]); + + const dispatchers = useMemo(() => { + const byId = new Map(); + for (const user of [...MOCK_DISPATCHER_USERS, ...users]) { + byId.set(String(user.id), user); + } + return [...byId.values()].sort((a, b) => a.name.localeCompare(b.name)); + }, [users]); + + const toggle = (id: string) => { + const next = new Set(selectedIds); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + onChange(next); + }; + + return ( + <> + + + {label} + + + + + onChange(new Set())} + /> + + {meId != null && ( + onChange(new Set([meId]))} + /> + )} + + } + className="mb-1 pb-2" + style={{ borderBottom: "1px solid var(--color-border)" }} + onClick={() => toggle(ASSIGNEE_FILTER_UNASSIGNED)} + /> + + {dispatchers.map((user) => { + const id = String(user.id); + const isMe = meId === id; + return ( + } + right={ + isMe ? me : undefined + } + onClick={() => toggle(id)} + /> + ); + })} + + + ); +} diff --git a/src/app/(protected)/workorders/_components/list/filter-bar/week-nav.tsx b/src/app/(protected)/workorders/_components/list/filter-bar/week-nav.tsx new file mode 100644 index 00000000..a99e3a02 --- /dev/null +++ b/src/app/(protected)/workorders/_components/list/filter-bar/week-nav.tsx @@ -0,0 +1,131 @@ +import { useEffect, useMemo, useState } from "react"; + +import { ChevronLeft, ChevronRight } from "lucide-react"; + +import { NavArrowButton } from "@/components/ui/nav-arrow-button"; + +import { NavPillButton } from "@/components/ui/nav-pill-button"; + +import { + buildWeekWindow, + isFutureWeek, + isLiveWeek, +} from "@/domain/work-orders/utils/list-week-utils"; + +type WeekNavProps = { + weekMonday: string; + + onChange: (mondayIso: string) => void; +}; + +const VISIBLE_COUNT = 3; + +const WINDOW_BEFORE = 12; + +const WINDOW_AFTER = 1; + +function WeekLiveBadge() { + return ( + + Live + + ); +} + +export function WeekNav({ weekMonday, onChange }: WeekNavProps) { + const weeks = useMemo( + () => buildWeekWindow(weekMonday, WINDOW_BEFORE, WINDOW_AFTER), + + [weekMonday], + ); + + const activeIndex = useMemo( + () => weeks.findIndex((week) => week.mondayIso === weekMonday), + + [weeks, weekMonday], + ); + + const [offset, setOffset] = useState(() => Math.max(0, activeIndex >= 0 ? activeIndex - 1 : 0)); + + useEffect(() => { + if (activeIndex < 0) return; + + const maxOffset = Math.max(0, weeks.length - VISIBLE_COUNT); + + setOffset((current) => { + if (activeIndex < current) return Math.min(activeIndex, maxOffset); + + if (activeIndex > current + VISIBLE_COUNT - 1) { + return Math.min(Math.max(0, activeIndex - (VISIBLE_COUNT - 1)), maxOffset); + } + + return current; + }); + }, [activeIndex, weeks.length]); + + const visible = weeks.slice(offset, offset + VISIBLE_COUNT); + + const canGoBack = offset > 0; + + const canGoForward = offset + VISIBLE_COUNT < weeks.length; + + return ( +
+ setOffset((current) => current - 1)} + aria-label="Previous weeks" + > + + + + {visible.map((week) => { + const isActive = weekMonday === week.mondayIso; + + const isDisabled = isFutureWeek(week.mondayIso); + + const showLiveBadge = isLiveWeek(week.mondayIso); + + return ( + onChange(week.mondayIso)} + > + {week.label} + + {showLiveBadge && } + + ); + })} + + setOffset((current) => current + 1)} + aria-label="Next weeks" + > + + +
+ ); +} diff --git a/src/app/(protected)/workorders/_components/list/filter-bar/wo-type-segment.tsx b/src/app/(protected)/workorders/_components/list/filter-bar/wo-type-segment.tsx new file mode 100644 index 00000000..3810f8ec --- /dev/null +++ b/src/app/(protected)/workorders/_components/list/filter-bar/wo-type-segment.tsx @@ -0,0 +1,25 @@ +import { SegmentButton, SegmentGroup } from "@/components/ui/segment-control"; + +import { WO_TYPES } from "@/domain/work-orders/types/work-order-wizard"; + +import type { WorkOrderTypeFilter } from "@/domain/work-orders/types/work-order-list-filters"; + +type WoTypeSegmentProps = { + value: WorkOrderTypeFilter; + + onChange: (value: WorkOrderTypeFilter) => void; +}; + +const OPTIONS: WorkOrderTypeFilter[] = ["All", ...WO_TYPES]; + +export function WoTypeSegment({ value, onChange }: WoTypeSegmentProps) { + return ( + + {OPTIONS.map((option) => ( + onChange(option)}> + {option} + + ))} + + ); +} diff --git a/src/app/(protected)/workorders/_components/list/filter-bar/work-orders-filter-bar.tsx b/src/app/(protected)/workorders/_components/list/filter-bar/work-orders-filter-bar.tsx new file mode 100644 index 00000000..4801105c --- /dev/null +++ b/src/app/(protected)/workorders/_components/list/filter-bar/work-orders-filter-bar.tsx @@ -0,0 +1,48 @@ +import type { WorkOrderUser } from "@/domain/work-orders/types/work-order"; +import type { WorkOrderTypeFilter } from "@/domain/work-orders/types/work-order-list-filters"; +import { DispatcherFilter } from "@/app/(protected)/workorders/_components/list/filter-bar/dispatcher-filter"; +import { WeekNav } from "@/app/(protected)/workorders/_components/list/filter-bar/week-nav"; +import { WoTypeSegment } from "@/app/(protected)/workorders/_components/list/filter-bar/wo-type-segment"; + +type WorkOrdersFilterBarProps = { + weekMonday: string; + onWeekChange: (mondayIso: string) => void; + dispatcherIds: Set; + onDispatcherChange: (ids: Set) => void; + typeFilter: WorkOrderTypeFilter; + onTypeChange: (value: WorkOrderTypeFilter) => void; + users: WorkOrderUser[]; + currentUserId?: string | number; + filteredCount: number; + scopeTotalCount: number; +}; + +export function WorkOrdersFilterBar({ + weekMonday, + onWeekChange, + dispatcherIds, + onDispatcherChange, + typeFilter, + onTypeChange, + users, + currentUserId, + filteredCount, + scopeTotalCount, +}: WorkOrdersFilterBarProps) { + const resultLabel = `${filteredCount} of ${scopeTotalCount}`; + + return ( +
+ + + +
+ {resultLabel} +
+ ); +} diff --git a/src/app/(protected)/workorders/_components/list/work-orders-advanced-filters-banner.tsx b/src/app/(protected)/workorders/_components/list/work-orders-advanced-filters-banner.tsx new file mode 100644 index 00000000..7b729bd1 --- /dev/null +++ b/src/app/(protected)/workorders/_components/list/work-orders-advanced-filters-banner.tsx @@ -0,0 +1,53 @@ +import { AppliedFilterChip } from "@/components/ui/applied-filter-chip"; +import { AppliedFiltersBar } from "@/components/ui/applied-filters-bar"; +import type { WorkOrderAdvancedFilters } from "@/domain/work-orders/types/work-order-advanced-filters"; +import type { WorkOrderUser } from "@/domain/work-orders/types/work-order"; +import { + buildAdvancedFilterChips, + countAdvancedFilters, +} from "@/domain/work-orders/utils/advanced-filters-utils"; + +type WorkOrdersAdvancedFiltersBannerProps = { + applied: WorkOrderAdvancedFilters; + totalCount: number; + sites: { id: string | number; name: string }[]; + users: WorkOrderUser[]; + onClearChip: (chipKey: string) => void; + onClearAll: () => void; +}; + +export function WorkOrdersAdvancedFiltersBanner({ + applied, + totalCount, + sites, + users, + onClearChip, + onClearAll, +}: WorkOrdersAdvancedFiltersBannerProps) { + if (countAdvancedFilters(applied) === 0) { + return null; + } + + const siteNameOf = (id: string) => sites.find((site) => String(site.id) === id)?.name ?? id; + const dispatcherNameOf = (id: string) => users.find((user) => String(user.id) === id)?.name ?? id; + + const chips = buildAdvancedFilterChips(applied, { siteNameOf, dispatcherNameOf }); + + return ( + + {chips.map((chip) => ( + onClearChip(chip.key)} + /> + ))} + + ); +} diff --git a/src/app/(protected)/workorders/_components/list/work-orders-list-header.tsx b/src/app/(protected)/workorders/_components/list/work-orders-list-header.tsx new file mode 100644 index 00000000..71dd3c7e --- /dev/null +++ b/src/app/(protected)/workorders/_components/list/work-orders-list-header.tsx @@ -0,0 +1,63 @@ +import { CircularProgress } from "@mui/material"; + +import { Plus, SlidersHorizontal } from "lucide-react"; + +import { SearchInput } from "@/components/ui/search-input"; + +import { ToolbarButton } from "@/components/ui/toolbar-button"; + +type WorkOrdersListHeaderProps = { + search: string; + + onSearchChange: (value: string) => void; + + onOpenAdvancedFilters: () => void; + + onNewWorkOrder: () => void; + + activeFilterCount: number; + + isFetching?: boolean; +}; + +export function WorkOrdersListHeader({ + search, + + onSearchChange, + + onOpenAdvancedFilters, + + onNewWorkOrder, + + activeFilterCount, + + isFetching = false, +}: WorkOrdersListHeaderProps) { + return ( + <> + + + } + onClick={onOpenAdvancedFilters} + aria-label="Advanced Filters" + title="Advanced Filters" + badge={activeFilterCount} + > + Filters + + + } onClick={onNewWorkOrder}> + New WO + + + {isFetching && } + + ); +} diff --git a/src/app/(protected)/workorders/_components/list/work-orders-table.tsx b/src/app/(protected)/workorders/_components/list/work-orders-table.tsx new file mode 100644 index 00000000..5579f5ca --- /dev/null +++ b/src/app/(protected)/workorders/_components/list/work-orders-table.tsx @@ -0,0 +1,117 @@ +import EditOutlinedIcon from "@mui/icons-material/EditOutlined"; +import VisibilityOutlinedIcon from "@mui/icons-material/VisibilityOutlined"; +import { Button, IconButton, Tooltip } from "@mui/material"; +import { ResourceListTable, type ResourceListColumn } from "@/components/ui/resource-list-table"; +import { WorkOrderStatusBadge } from "@/components/domain/status-badge"; +import type { WorkOrderListItem } from "@/domain/work-orders/types/work-order"; +import type { SortDirection } from "@/domain/work-orders/types/work-order-list"; +import { formatDateTime } from "@/lib/time-utils"; + +function displayDateTime(value: string): string { + return formatDateTime(value) || "—"; +} + +type WorkOrdersTableProps = { + rows: WorkOrderListItem[]; + isLoading: boolean; + sort: { column: string; direction: SortDirection }; + onSort: (column: string) => void; + onView: (id: string | number) => void; + onEdit: (id: string | number) => void; +}; + +export function WorkOrdersTable({ + rows, + isLoading, + sort, + onSort, + onView, + onEdit, +}: WorkOrdersTableProps) { + const columns: ResourceListColumn[] = [ + { + id: "number", + header: "WO #", + sortable: true, + render: (row) => ( + + ), + }, + { + id: "title", + header: "Title", + sortable: true, + render: (row) => row.title, + }, + { + id: "location", + header: "Location", + render: (row) => row.location, + }, + { + id: "priority", + header: "Priority", + render: (row) => row.priority, + }, + { + id: "status", + header: "Status", + render: (row) => , + }, + { + id: "createdDate", + header: "Created", + cellClassName: "whitespace-nowrap", + render: (row) => displayDateTime(row.createdDate), + }, + { + id: "assignedTo", + header: "Assigned To", + render: (row) => row.assignedTo || "Unassigned", + }, + { + id: "lastUpdated", + header: "Last Updated", + cellClassName: "whitespace-nowrap", + render: (row) => displayDateTime(row.lastUpdated), + }, + { + id: "actions", + header: "Actions", + align: "right", + render: (row) => ( + <> + + onView(row.id)}> + + + + + onEdit(row.id)}> + + + + + ), + }, + ]; + + return ( + String(row.id)} + isLoading={isLoading} + emptyMessage="No work orders found" + sort={sort} + onSort={onSort} + /> + ); +} diff --git a/src/app/(protected)/workorders/_components/media/media-upload-modal.tsx b/src/app/(protected)/workorders/_components/media/media-upload-modal.tsx new file mode 100644 index 00000000..779bdb28 --- /dev/null +++ b/src/app/(protected)/workorders/_components/media/media-upload-modal.tsx @@ -0,0 +1,68 @@ +import { type MouseEvent } from "react"; +import { Dialog, DialogContent, DialogTitle } from "@mui/material"; +import { Image as ImageIcon } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { dialogBlurBackdropSlotProps } from "@/components/ui/dialog-blur-backdrop"; +import { Text } from "@/components/ui/text"; +import type { MediaFile } from "@/domain/work-orders/types/work-order-media"; +import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; +import { MediaUploader } from "./media-uploader"; + +type MediaUploadModalProps = { + row: WorkOrderTableRow; + open: boolean; + onOpenChange: (open: boolean) => void; + uploads: MediaFile[]; + onChange: (next: MediaFile[]) => void; +}; + +export function MediaUploadModal({ + row, + open, + onOpenChange, + uploads, + onChange, +}: MediaUploadModalProps) { + return ( + onOpenChange(false)} + maxWidth={false} + slotProps={{ + ...dialogBlurBackdropSlotProps, + paper: { + style: { borderRadius: 12, maxWidth: 520, width: "100%" }, + onClick: (e: MouseEvent) => e.stopPropagation(), + }, + }} + > + e.stopPropagation()}> +
+ + + Photos & Videos + + #{row.woNumber} +
+ + Upload photos and videos for this work order. + + +
+ +
+ +
+ +
+
+
+ ); +} diff --git a/src/app/(protected)/workorders/_components/media/media-uploader.tsx b/src/app/(protected)/workorders/_components/media/media-uploader.tsx new file mode 100644 index 00000000..bd7fcc9a --- /dev/null +++ b/src/app/(protected)/workorders/_components/media/media-uploader.tsx @@ -0,0 +1,143 @@ +import { useRef } from "react"; +import { + File as FileIcon, + FileText, + Film, + Image as ImageIcon, + Paperclip, + Play, + Upload, + X, +} from "lucide-react"; +import type { FileCategory, MediaFile } from "@/domain/work-orders/types/work-order-media"; +import { makeMediaFile } from "@/domain/work-orders/utils/work-order-media-utils"; + +const FILETYPE_ICON: Record = { + image: ImageIcon, + video: Film, + pdf: FileText, + doc: FileIcon, + other: Paperclip, +}; + +type MediaUploaderProps = { + uploads: MediaFile[]; + onChange: (next: MediaFile[]) => void; +}; + +export function MediaUploader({ uploads, onChange }: MediaUploaderProps) { + const inputRef = useRef(null); + + const addFiles = (files: FileList | null) => { + if (!files || files.length === 0) return; + onChange([...uploads, ...Array.from(files).map(makeMediaFile)]); + }; + + const remove = (id: string) => onChange(uploads.filter((u) => u.id !== id)); + + return ( +
+ + addFiles(e.target.files)} + /> + {uploads.length > 0 && ( +
+ {uploads.map((u) => { + const TypeIcon = FILETYPE_ICON[u.fileType]; + return ( +
+
+ {u.fileType === "image" ? ( + {u.name} + ) : u.fileType === "video" ? ( + <> +
+
+
+ {u.name} +
+
+
+ ); + })} +
+ )} +
+ ); +} diff --git a/src/app/(protected)/workorders/_hooks/use-work-orders-list-filters.ts b/src/app/(protected)/workorders/_hooks/use-work-orders-list-filters.ts new file mode 100644 index 00000000..a02cce3c --- /dev/null +++ b/src/app/(protected)/workorders/_hooks/use-work-orders-list-filters.ts @@ -0,0 +1,191 @@ +import { useMemo, useState } from "react"; +import { useDebounce } from "@/hooks/use-debounce"; +import { isWoTableMockEnabled } from "@/domain/work-orders/mocks/is-wo-table-mock-enabled"; +import type { WorkOrdersListParams } from "@/domain/work-orders/types/work-order-list"; +import { WORK_ORDERS_TABLE_PAGE_SIZE } from "@/domain/work-orders/types/work-order-table-row"; +import type { WorkOrderAdvancedFilters } from "@/domain/work-orders/types/work-order-advanced-filters"; +import type { + WorkOrderBarFilters, + WorkOrderTypeFilter, +} from "@/domain/work-orders/types/work-order-list-filters"; +import { + clearAdvancedFilterGroup, + cloneAdvancedFilters, + countAdvancedFilters, + getInitialAdvancedFilters, +} from "@/domain/work-orders/utils/advanced-filters-utils"; +import { + compactWeekLabel, + currentWeekMonday, + weekRange, +} from "@/domain/work-orders/utils/list-week-utils"; + +function defaultDispatcherIds(currentUserId?: string | number): Set { + if (isWoTableMockEnabled()) { + return new Set(); + } + if (currentUserId != null && currentUserId !== "") { + return new Set([String(currentUserId)]); + } + return new Set(); +} + +/** Phase 2: map bar filters to API list params. */ +export function toListParamsFromBar( + bar: WorkOrderBarFilters, +): Pick { + const [dateFrom, dateTo] = weekRange(bar.weekMonday); + return { + dateFrom, + dateTo, + type: bar.type === "All" ? undefined : bar.type, + assigneeIds: bar.dispatcherIds.size > 0 ? [...bar.dispatcherIds].join(",") : undefined, + }; +} + +export function useWorkOrdersListFilters(currentUserId?: string | number) { + const [search, setSearch] = useState(""); + const [advOpen, setAdvOpen] = useState(false); + const [advApplied, setAdvApplied] = useState(null); + const [page, setPage] = useState(0); + const [pageSize, setPageSize] = useState(12); + const [sortBy, setSortBy] = useState(""); + const [sortDir, setSortDir] = useState<"asc" | "desc" | "">(""); + const [weekMonday, setWeekMondayState] = useState(currentWeekMonday); + const [dispatcherIds, setDispatcherIdsState] = useState>(() => + defaultDispatcherIds(currentUserId), + ); + const [typeFilter, setTypeFilterState] = useState("All"); + + const debouncedSearch = useDebounce(search, 300); + + const barFilters = useMemo( + () => ({ + weekMonday, + dispatcherIds, + type: typeFilter, + }), + [weekMonday, dispatcherIds, typeFilter], + ); + + const weekLabel = useMemo(() => compactWeekLabel(weekMonday), [weekMonday]); + + const scopeListParams = useMemo( + () => ({ + page: 1, + pageSize: 1, + ...toListParamsFromBar(barFilters), + }), + [barFilters], + ); + + const listParams = useMemo( + () => ({ + search: debouncedSearch, + page: 1, + pageSize: WORK_ORDERS_TABLE_PAGE_SIZE, + sortBy, + sortDir, + ...toListParamsFromBar(barFilters), + }), + [debouncedSearch, sortBy, sortDir, barFilters], + ); + + const activeFilterCount = useMemo( + () => (advApplied ? countAdvancedFilters(advApplied) : 0), + [advApplied], + ); + + const setWeekMonday = (value: string) => { + setWeekMondayState(value); + setPage(0); + }; + + const setDispatcherIds = (value: Set) => { + setDispatcherIdsState(new Set(value)); + setPage(0); + }; + + const setTypeFilter = (value: WorkOrderTypeFilter) => { + setTypeFilterState(value); + setPage(0); + }; + + const handleSearchChange = (value: string) => { + setSearch(value); + setPage(0); + }; + + const applyAdvancedFilters = (filters: WorkOrderAdvancedFilters) => { + setAdvApplied(cloneAdvancedFilters(filters)); + setPage(0); + }; + + const clearAdvancedFilters = () => { + setAdvApplied(null); + setPage(0); + }; + + const clearAdvancedFilterChip = (chipKey: string) => { + if (!advApplied) return; + const next = clearAdvancedFilterGroup(advApplied, chipKey); + setAdvApplied(next); + setPage(0); + }; + + const handleSort = (column: string) => { + if (sortBy === column) { + setSortDir((current) => { + const next = current === "asc" ? "desc" : current === "desc" ? "" : "asc"; + if (next === "") { + setSortBy(""); + } + return next; + }); + return; + } + + setSortBy(column); + setSortDir("asc"); + }; + + const sortIndicator = (column: string) => { + if (sortBy !== column) { + return ""; + } + return sortDir === "desc" ? " ↓" : " ↑"; + }; + + return { + search, + advOpen, + advApplied, + advInitial: getInitialAdvancedFilters(advApplied), + page, + pageSize, + sortBy, + sortDir, + weekMonday, + dispatcherIds, + typeFilter, + barFilters, + weekLabel, + listParams, + scopeListParams, + activeFilterCount, + handleSearchChange, + applyAdvancedFilters, + clearAdvancedFilters, + clearAdvancedFilterChip, + handleSort, + sortIndicator, + setAdvOpen, + setPage, + setPageSize, + setWeekMonday, + setDispatcherIds, + setTypeFilter, + setSortBy, + setSortDir, + }; +} diff --git a/src/app/(protected)/workorders/index.tsx b/src/app/(protected)/workorders/index.tsx index d918878e..d45464c5 100644 --- a/src/app/(protected)/workorders/index.tsx +++ b/src/app/(protected)/workorders/index.tsx @@ -28,7 +28,7 @@ import { Typography, } from "@mui/material"; import { useDebounce } from "@/hooks/use-debounce"; -import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; +import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order"; import { useWorkOrderUsers, useWorkOrdersList, @@ -292,7 +292,7 @@ export default function WorkOrdersListPage() { {formatDateTime(row.scheduledOn)} - {row.dispatcherName || "Unassigned"} + {row.tech || "Unassigned"} {formatDateTime(row.dueDate)}