mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 03:32:06 +00:00
fix(work-orders): resolve CompDoc/dev merge type and maintainability issues
This commit is contained in:
parent
e3aa69e577
commit
b2d44944ab
8 changed files with 157 additions and 76 deletions
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { CompDocDialog } from "@/app/(protected)/workorders/_components/comp-doc/comp-doc-dialog";
|
||||||
|
import { MediaUploadModal } from "@/app/(protected)/workorders/_components/media/media-upload-modal";
|
||||||
|
import { WorkOrdersAdvancedSearchSheet } from "@/app/(protected)/workorders/_components/list/advanced-search/work-orders-advanced-search-sheet";
|
||||||
|
import { NewWoWizard } from "@/app/(protected)/workorders/_components/wizard/new-wo-wizard";
|
||||||
|
import type { CompDocData, CompDocSaveStatus } from "@/domain/work-orders/types/completion-doc";
|
||||||
|
import type { WorkOrderAdvancedFilters } from "@/domain/work-orders/types/work-order-advanced-filters";
|
||||||
|
import type { LocationOption } from "@/domain/work-orders/types/work-order-common";
|
||||||
|
import type { WorkOrderUser } from "@/domain/work-orders/types/work-order-list";
|
||||||
|
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
|
||||||
|
type WorkOrdersListOverlaysProps = {
|
||||||
|
wizardOpen: boolean;
|
||||||
|
onWizardOpenChange: (open: boolean) => void;
|
||||||
|
users: WorkOrderUser[];
|
||||||
|
sites: LocationOption[];
|
||||||
|
pmTypes: string[];
|
||||||
|
vendorTechs: string[];
|
||||||
|
advOpen: boolean;
|
||||||
|
onAdvOpenChange: (open: boolean) => void;
|
||||||
|
advInitial: WorkOrderAdvancedFilters;
|
||||||
|
onApplyAdvanced: (next: WorkOrderAdvancedFilters) => void;
|
||||||
|
onClearAdvanced: () => void;
|
||||||
|
activeDocRow: WorkOrderTableRow | null;
|
||||||
|
onDocOpenChange: (open: boolean) => void;
|
||||||
|
completionDocs: Record<string, CompDocData>;
|
||||||
|
onSaveCompDoc: (data: CompDocData, status: CompDocSaveStatus) => void;
|
||||||
|
onDateCompletedChange: (date: string) => void;
|
||||||
|
activeMediaRow: WorkOrderTableRow | null;
|
||||||
|
onMediaOpenChange: (open: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function WorkOrdersListOverlays({
|
||||||
|
wizardOpen,
|
||||||
|
onWizardOpenChange,
|
||||||
|
users,
|
||||||
|
sites,
|
||||||
|
pmTypes,
|
||||||
|
vendorTechs,
|
||||||
|
advOpen,
|
||||||
|
onAdvOpenChange,
|
||||||
|
advInitial,
|
||||||
|
onApplyAdvanced,
|
||||||
|
onClearAdvanced,
|
||||||
|
activeDocRow,
|
||||||
|
onDocOpenChange,
|
||||||
|
completionDocs,
|
||||||
|
onSaveCompDoc,
|
||||||
|
onDateCompletedChange,
|
||||||
|
activeMediaRow,
|
||||||
|
onMediaOpenChange,
|
||||||
|
}: WorkOrdersListOverlaysProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<WorkOrdersAdvancedSearchSheet
|
||||||
|
open={advOpen}
|
||||||
|
onOpenChange={onAdvOpenChange}
|
||||||
|
initial={advInitial}
|
||||||
|
onApply={onApplyAdvanced}
|
||||||
|
onClearAll={onClearAdvanced}
|
||||||
|
sites={sites}
|
||||||
|
users={users}
|
||||||
|
pmTypes={pmTypes}
|
||||||
|
vendorTechs={vendorTechs}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NewWoWizard open={wizardOpen} onOpenChange={onWizardOpenChange} users={users} />
|
||||||
|
|
||||||
|
{activeDocRow != null && (
|
||||||
|
<CompDocDialog
|
||||||
|
row={activeDocRow}
|
||||||
|
open={!!activeDocRow}
|
||||||
|
onOpenChange={onDocOpenChange}
|
||||||
|
data={completionDocs[String(activeDocRow.id)]}
|
||||||
|
// docStatus persists via board PATCH (Yes|No|NotNeeded); completedDate via
|
||||||
|
// EditWorkorder. patchField serializes same-id saves so they never race.
|
||||||
|
// Notes/generatedAt stay client-local (no content endpoint on this branch).
|
||||||
|
onSave={onSaveCompDoc}
|
||||||
|
onDateCompletedChange={onDateCompletedChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeMediaRow != null && (
|
||||||
|
<MediaUploadModal
|
||||||
|
row={activeMediaRow}
|
||||||
|
open={!!activeMediaRow}
|
||||||
|
onOpenChange={onMediaOpenChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,15 +2,12 @@ import { useMemo, useState } from "react";
|
||||||
import { Box } from "@mui/material";
|
import { Box } from "@mui/material";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { PageHeader } from "@/components/ui/page-header";
|
import { PageHeader } from "@/components/ui/page-header";
|
||||||
import { CompDocDialog } from "@/app/(protected)/workorders/_components/comp-doc/comp-doc-dialog";
|
|
||||||
import { QueryErrorPanel } from "@/components/ui/query-error-panel";
|
import { QueryErrorPanel } from "@/components/ui/query-error-panel";
|
||||||
import { MediaUploadModal } from "@/app/(protected)/workorders/_components/media/media-upload-modal";
|
|
||||||
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 { 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 { 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 { WorkOrdersListHeader } from "@/app/(protected)/workorders/_components/list/work-orders-list-header";
|
||||||
|
import { WorkOrdersListOverlays } from "@/app/(protected)/workorders/_components/list/work-orders-list-overlays";
|
||||||
import { WoTable } from "@/app/(protected)/workorders/_components/list/table/wo-table";
|
import { WoTable } from "@/app/(protected)/workorders/_components/list/table/wo-table";
|
||||||
import { NewWoWizard } from "@/app/(protected)/workorders/_components/wizard/new-wo-wizard";
|
|
||||||
import { useWorkOrdersListFilters } from "@/app/(protected)/workorders/_hooks/use-work-orders-list-filters";
|
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 { 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 { useDropdownOptionsByCategory } from "@/domain/settings/dropdown-options/use-cases/use-dropdown-options-by-category";
|
||||||
|
|
@ -25,6 +22,14 @@ import { useWorkOrderTableMutations } from "@/domain/work-orders/use-cases/use-w
|
||||||
import { hasCompletionDocService } from "@/domain/work-orders/utils/completion-doc-service-gate";
|
import { hasCompletionDocService } from "@/domain/work-orders/utils/completion-doc-service-gate";
|
||||||
import { useAuthContext } from "@/providers/auth-context";
|
import { useAuthContext } from "@/providers/auth-context";
|
||||||
|
|
||||||
|
function resolveActiveRow(
|
||||||
|
selected: WorkOrderTableRow | null,
|
||||||
|
rows: WorkOrderTableRow[],
|
||||||
|
): WorkOrderTableRow | null {
|
||||||
|
if (!selected) return null;
|
||||||
|
return rows.find((row) => row.id === selected.id) ?? selected;
|
||||||
|
}
|
||||||
|
|
||||||
export default function WorkOrdersListPage() {
|
export default function WorkOrdersListPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useAuthContext();
|
const { user } = useAuthContext();
|
||||||
|
|
@ -39,35 +44,26 @@ export default function WorkOrdersListPage() {
|
||||||
clearPatch: tableData.clearRowPatch,
|
clearPatch: tableData.clearRowPatch,
|
||||||
getRow: (id) => tableData.rows.find((row) => String(row.id) === String(id)),
|
getRow: (id) => tableData.rows.find((row) => String(row.id) === String(id)),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: users = [], isLoading: usersLoading } = useWorkOrderUsers();
|
const { data: users = [], isLoading: usersLoading } = useWorkOrderUsers();
|
||||||
const { data: locations = [] } = useWorkOrderLocations();
|
const { data: locations = [] } = useWorkOrderLocations();
|
||||||
const { data: vendors = [] } = useWorkOrderVendors();
|
const { data: vendors = [] } = useWorkOrderVendors();
|
||||||
const { data: problemOptions = [] } = useDropdownOptionsByCategory("Problem");
|
const { data: problemOptions = [] } = useDropdownOptionsByCategory("Problem");
|
||||||
|
|
||||||
const pmTypes = useMemo(
|
const pmTypes = useMemo(
|
||||||
() => problemOptions.map((option) => option.value).filter(Boolean),
|
() => problemOptions.map((option) => option.value).filter(Boolean),
|
||||||
[problemOptions],
|
[problemOptions],
|
||||||
);
|
);
|
||||||
|
|
||||||
const vendorTechs = useMemo(
|
const vendorTechs = useMemo(
|
||||||
() => [...new Set(vendors.map((vendor) => vendor.companyName).filter(Boolean))],
|
() => [...new Set(vendors.map((vendor) => vendor.companyName).filter(Boolean))],
|
||||||
[vendors],
|
[vendors],
|
||||||
);
|
);
|
||||||
|
const activeDocRow = useMemo(
|
||||||
const totalCount = tableData.totalCount;
|
() => resolveActiveRow(docRow, tableData.rows),
|
||||||
const scopeTotalCount = tableData.scopeTotalCount;
|
[docRow, tableData.rows],
|
||||||
|
);
|
||||||
const activeDocRow = useMemo(() => {
|
const activeMediaRow = useMemo(
|
||||||
if (!docRow) return null;
|
() => resolveActiveRow(mediaRow, tableData.rows),
|
||||||
return tableData.rows.find((row) => row.id === docRow.id) ?? docRow;
|
[mediaRow, tableData.rows],
|
||||||
}, [docRow, tableData.rows]);
|
);
|
||||||
|
|
||||||
const activeMediaRow = useMemo(() => {
|
|
||||||
if (!mediaRow) return null;
|
|
||||||
return tableData.rows.find((row) => row.id === mediaRow.id) ?? mediaRow;
|
|
||||||
}, [mediaRow, tableData.rows]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="flex h-full flex-col overflow-hidden p-2.5">
|
<Box className="flex h-full flex-col overflow-hidden p-2.5">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
|
|
@ -96,14 +92,14 @@ export default function WorkOrdersListPage() {
|
||||||
users={users}
|
users={users}
|
||||||
usersLoading={usersLoading}
|
usersLoading={usersLoading}
|
||||||
currentUserId={user?.id}
|
currentUserId={user?.id}
|
||||||
filteredCount={totalCount}
|
filteredCount={tableData.totalCount}
|
||||||
scopeTotalCount={scopeTotalCount}
|
scopeTotalCount={tableData.scopeTotalCount}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{filters.advApplied != null && (
|
{filters.advApplied != null && (
|
||||||
<WorkOrdersAdvancedFiltersBanner
|
<WorkOrdersAdvancedFiltersBanner
|
||||||
applied={filters.advApplied}
|
applied={filters.advApplied}
|
||||||
totalCount={totalCount}
|
totalCount={tableData.totalCount}
|
||||||
sites={locations}
|
sites={locations}
|
||||||
users={users}
|
users={users}
|
||||||
onClearChip={filters.clearAdvancedFilterChip}
|
onClearChip={filters.clearAdvancedFilterChip}
|
||||||
|
|
@ -146,48 +142,37 @@ export default function WorkOrdersListPage() {
|
||||||
onPatch={patchField}
|
onPatch={patchField}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<WorkOrdersAdvancedSearchSheet
|
<WorkOrdersListOverlays
|
||||||
open={filters.advOpen}
|
wizardOpen={wizardOpen}
|
||||||
onOpenChange={filters.setAdvOpen}
|
onWizardOpenChange={setWizardOpen}
|
||||||
initial={filters.advInitial}
|
|
||||||
onApply={filters.applyAdvancedFilters}
|
|
||||||
onClearAll={filters.clearAdvancedFilters}
|
|
||||||
sites={locations}
|
|
||||||
users={users}
|
users={users}
|
||||||
|
sites={locations}
|
||||||
pmTypes={pmTypes}
|
pmTypes={pmTypes}
|
||||||
vendorTechs={vendorTechs}
|
vendorTechs={vendorTechs}
|
||||||
|
advOpen={filters.advOpen}
|
||||||
|
onAdvOpenChange={filters.setAdvOpen}
|
||||||
|
advInitial={filters.advInitial}
|
||||||
|
onApplyAdvanced={filters.applyAdvancedFilters}
|
||||||
|
onClearAdvanced={filters.clearAdvancedFilters}
|
||||||
|
activeDocRow={activeDocRow}
|
||||||
|
onDocOpenChange={(open) => {
|
||||||
|
if (!open) setDocRow(null);
|
||||||
|
}}
|
||||||
|
completionDocs={completionDocs}
|
||||||
|
onSaveCompDoc={(data, status) => {
|
||||||
|
if (!activeDocRow) return;
|
||||||
|
setCompletionDocs((prev) => ({ ...prev, [String(activeDocRow.id)]: data }));
|
||||||
|
patchField(activeDocRow.id, { docStatus: status });
|
||||||
|
}}
|
||||||
|
onDateCompletedChange={(date) => {
|
||||||
|
if (!activeDocRow) return;
|
||||||
|
patchField(activeDocRow.id, { completedDate: date });
|
||||||
|
}}
|
||||||
|
activeMediaRow={activeMediaRow}
|
||||||
|
onMediaOpenChange={(open) => {
|
||||||
|
if (!open) setMediaRow(null);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NewWoWizard open={wizardOpen} onOpenChange={setWizardOpen} users={users} />
|
|
||||||
|
|
||||||
{activeDocRow != null && (
|
|
||||||
<CompDocDialog
|
|
||||||
row={activeDocRow}
|
|
||||||
open={!!activeDocRow}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) setDocRow(null);
|
|
||||||
}}
|
|
||||||
data={completionDocs[String(activeDocRow.id)]}
|
|
||||||
// docStatus persists via board PATCH (Yes|No|NotNeeded); completedDate via
|
|
||||||
// EditWorkorder. patchField serializes same-id saves so they never race.
|
|
||||||
// Notes/generatedAt stay client-local (no content endpoint on this branch).
|
|
||||||
onSave={(data, status) => {
|
|
||||||
setCompletionDocs((prev) => ({ ...prev, [String(activeDocRow.id)]: data }));
|
|
||||||
patchField(activeDocRow.id, { docStatus: status });
|
|
||||||
}}
|
|
||||||
onDateCompletedChange={(date) => patchField(activeDocRow.id, { completedDate: date })}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeMediaRow != null && (
|
|
||||||
<MediaUploadModal
|
|
||||||
row={activeMediaRow}
|
|
||||||
open={!!activeMediaRow}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) setMediaRow(null);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -237,6 +237,7 @@ export function mapWorkOrderTableRow(raw: unknown): WorkOrderTableRow {
|
||||||
dispatcherId,
|
dispatcherId,
|
||||||
dispatcherName,
|
dispatcherName,
|
||||||
scheduledOn,
|
scheduledOn,
|
||||||
|
completedDate: toDateInputValue(readString(item, "completedDate", "CompletedDate")),
|
||||||
targetWeek: toDateInputValue(readString(item, "targetWeek", "TargetWeek")),
|
targetWeek: toDateInputValue(readString(item, "targetWeek", "TargetWeek")),
|
||||||
dayGroup,
|
dayGroup,
|
||||||
dayLabel,
|
dayLabel,
|
||||||
|
|
|
||||||
|
|
@ -47,17 +47,6 @@ export function useWorkOrderMedia(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useWorkOrderBoardDetail(
|
|
||||||
id: string | number | undefined,
|
|
||||||
enabled = true,
|
|
||||||
): UseQueryResult<WorkOrderBoardDetail, Error> {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: queryKeys.workOrders.boardDetail(id ?? ""),
|
|
||||||
queryFn: () => workOrdersApi.getBoardDetail(requireQueryParam(id, "Work order id")),
|
|
||||||
enabled: enabled && id !== undefined && id !== "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useWorkOrderLocations(): UseQueryResult<LocationOption[], Error> {
|
export function useWorkOrderLocations(): UseQueryResult<LocationOption[], Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: queryKeys.locations.dropdown(),
|
queryKey: queryKeys.locations.dropdown(),
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ function baseRow(overrides: Partial<WorkOrderTableRow> = {}): WorkOrderTableRow
|
||||||
location: "Seattle",
|
location: "Seattle",
|
||||||
locationId: "mock-loc-1",
|
locationId: "mock-loc-1",
|
||||||
pm: "HVAC",
|
pm: "HVAC",
|
||||||
|
serviceNotes: "",
|
||||||
|
extraServices: [],
|
||||||
|
vendorId: "",
|
||||||
company: "Vendor Co",
|
company: "Vendor Co",
|
||||||
tech: "Tech One",
|
tech: "Tech One",
|
||||||
techPhone: "",
|
techPhone: "",
|
||||||
|
|
@ -51,6 +54,10 @@ function baseRow(overrides: Partial<WorkOrderTableRow> = {}): WorkOrderTableRow
|
||||||
mediaCount: 0,
|
mediaCount: 0,
|
||||||
title: "PM — SEA1",
|
title: "PM — SEA1",
|
||||||
priority: "Sev 3",
|
priority: "Sev 3",
|
||||||
|
primaryDispatchId: null,
|
||||||
|
rowVersion: "",
|
||||||
|
dispatchRowVersion: "",
|
||||||
|
flagColor: null,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,9 @@ function baseRow(): WorkOrderTableRow {
|
||||||
location: "Seattle",
|
location: "Seattle",
|
||||||
locationId: "loc-1",
|
locationId: "loc-1",
|
||||||
pm: "",
|
pm: "",
|
||||||
|
serviceNotes: "",
|
||||||
|
extraServices: [],
|
||||||
|
vendorId: "",
|
||||||
company: "",
|
company: "",
|
||||||
tech: "",
|
tech: "",
|
||||||
techPhone: "",
|
techPhone: "",
|
||||||
|
|
@ -70,6 +73,10 @@ function baseRow(): WorkOrderTableRow {
|
||||||
mediaCount: 1,
|
mediaCount: 1,
|
||||||
title: "Emergency",
|
title: "Emergency",
|
||||||
priority: "Sev 1",
|
priority: "Sev 1",
|
||||||
|
primaryDispatchId: null,
|
||||||
|
rowVersion: "",
|
||||||
|
dispatchRowVersion: "",
|
||||||
|
flagColor: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -144,9 +144,9 @@ describe("mapWorkOrderFormToUpdateFormData", () => {
|
||||||
docStatus: "NN" as const,
|
docStatus: "NN" as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
const payload = mapWorkOrderFormToUpdatePayload(1, values);
|
const formData = mapWorkOrderFormToUpdateFormData(1, values);
|
||||||
|
|
||||||
expect(payload.CompletedDate).toBe("2026-07-20");
|
expect(formData.get("CompletedDate")).toBe("2026-07-20");
|
||||||
expect(payload).not.toHaveProperty("DocStatus");
|
expect(formData.has("DocStatus")).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ function baseRow(overrides: Partial<WorkOrderTableRow> = {}): WorkOrderTableRow
|
||||||
dispatcherId: "10",
|
dispatcherId: "10",
|
||||||
dispatcherName: "Alex",
|
dispatcherName: "Alex",
|
||||||
scheduledOn: "2026-07-16",
|
scheduledOn: "2026-07-16",
|
||||||
|
completedDate: "",
|
||||||
targetWeek: "",
|
targetWeek: "",
|
||||||
dayGroup: "2026-07-16",
|
dayGroup: "2026-07-16",
|
||||||
dayLabel: "Thu",
|
dayLabel: "Thu",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue