mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 15:52:04 +00:00
fix(work-orders): hide unfinished affordances and route View to detail
This commit is contained in:
parent
0709bd1172
commit
becc4913e3
3 changed files with 174 additions and 32 deletions
|
|
@ -1,36 +1,25 @@
|
||||||
import { CircularProgress } from "@mui/material";
|
import { CircularProgress } from "@mui/material";
|
||||||
|
|
||||||
import { Plus, SlidersHorizontal } from "lucide-react";
|
import { Plus, SlidersHorizontal } from "lucide-react";
|
||||||
|
|
||||||
import { SearchInput } from "@/components/ui/search-input";
|
import { SearchInput } from "@/components/ui/search-input";
|
||||||
|
|
||||||
import { ToolbarButton } from "@/components/ui/toolbar-button";
|
import { ToolbarButton } from "@/components/ui/toolbar-button";
|
||||||
|
|
||||||
type WorkOrdersListHeaderProps = {
|
type WorkOrdersListHeaderProps = {
|
||||||
search: string;
|
search: string;
|
||||||
|
|
||||||
onSearchChange: (value: string) => void;
|
onSearchChange: (value: string) => void;
|
||||||
|
/** Omit until Advanced Search slice mounts the sheet. */
|
||||||
onOpenAdvancedFilters: () => void;
|
onOpenAdvancedFilters?: () => void;
|
||||||
|
/** Omit until wizard shell slice mounts New WO. */
|
||||||
onNewWorkOrder: () => void;
|
onNewWorkOrder?: () => void;
|
||||||
|
|
||||||
activeFilterCount: number;
|
activeFilterCount: number;
|
||||||
|
|
||||||
isFetching?: boolean;
|
isFetching?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function WorkOrdersListHeader({
|
export function WorkOrdersListHeader({
|
||||||
search,
|
search,
|
||||||
|
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
|
|
||||||
onOpenAdvancedFilters,
|
onOpenAdvancedFilters,
|
||||||
|
|
||||||
onNewWorkOrder,
|
onNewWorkOrder,
|
||||||
|
|
||||||
activeFilterCount,
|
activeFilterCount,
|
||||||
|
|
||||||
isFetching = false,
|
isFetching = false,
|
||||||
}: WorkOrdersListHeaderProps) {
|
}: WorkOrdersListHeaderProps) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -42,20 +31,24 @@ export function WorkOrdersListHeader({
|
||||||
aria-label="Search work orders"
|
aria-label="Search work orders"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ToolbarButton
|
{onOpenAdvancedFilters && (
|
||||||
variant="outline"
|
<ToolbarButton
|
||||||
icon={<SlidersHorizontal size={13} />}
|
variant="outline"
|
||||||
onClick={onOpenAdvancedFilters}
|
icon={<SlidersHorizontal size={13} />}
|
||||||
aria-label="Advanced Filters"
|
onClick={onOpenAdvancedFilters}
|
||||||
title="Advanced Filters"
|
aria-label="Advanced Filters"
|
||||||
badge={activeFilterCount}
|
title="Advanced Filters"
|
||||||
>
|
badge={activeFilterCount}
|
||||||
Filters
|
>
|
||||||
</ToolbarButton>
|
Filters
|
||||||
|
</ToolbarButton>
|
||||||
|
)}
|
||||||
|
|
||||||
<ToolbarButton variant="primary" icon={<Plus size={13} />} onClick={onNewWorkOrder}>
|
{onNewWorkOrder && (
|
||||||
New WO
|
<ToolbarButton variant="primary" icon={<Plus size={13} />} onClick={onNewWorkOrder}>
|
||||||
</ToolbarButton>
|
New WO
|
||||||
|
</ToolbarButton>
|
||||||
|
)}
|
||||||
|
|
||||||
{isFetching && <CircularProgress size={14} />}
|
{isFetching && <CircularProgress size={14} />}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Box } from "@mui/material";
|
import { Box } from "@mui/material";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
import { PageHeader } from "@/components/ui/page-header";
|
import { PageHeader } from "@/components/ui/page-header";
|
||||||
import { MediaUploadModal } from "@/app/(protected)/workorders/_components/media/media-upload-modal";
|
import { MediaUploadModal } from "@/app/(protected)/workorders/_components/media/media-upload-modal";
|
||||||
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";
|
||||||
|
|
@ -22,6 +23,7 @@ import { useWorkOrderTableMutations } from "@/domain/work-orders/use-cases/use-w
|
||||||
import { useAuthContext } from "@/providers/auth-context";
|
import { useAuthContext } from "@/providers/auth-context";
|
||||||
|
|
||||||
export default function WorkOrdersListPage() {
|
export default function WorkOrdersListPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
const { user } = useAuthContext();
|
const { user } = useAuthContext();
|
||||||
const [mediaRow, setMediaRow] = useState<WorkOrderTableRow | null>(null);
|
const [mediaRow, setMediaRow] = useState<WorkOrderTableRow | null>(null);
|
||||||
const [mediaFiles, setMediaFiles] = useState<Record<string, MediaFile[]>>({});
|
const [mediaFiles, setMediaFiles] = useState<Record<string, MediaFile[]>>({});
|
||||||
|
|
@ -60,8 +62,6 @@ export default function WorkOrdersListPage() {
|
||||||
<WorkOrdersListHeader
|
<WorkOrdersListHeader
|
||||||
search={filters.search}
|
search={filters.search}
|
||||||
onSearchChange={filters.handleSearchChange}
|
onSearchChange={filters.handleSearchChange}
|
||||||
onOpenAdvancedFilters={() => filters.setAdvOpen(true)}
|
|
||||||
onNewWorkOrder={() => undefined}
|
|
||||||
activeFilterCount={filters.activeFilterCount}
|
activeFilterCount={filters.activeFilterCount}
|
||||||
isFetching={tableData.isFetching && !tableData.isLoading}
|
isFetching={tableData.isFetching && !tableData.isLoading}
|
||||||
/>
|
/>
|
||||||
|
|
@ -98,11 +98,13 @@ export default function WorkOrdersListPage() {
|
||||||
sites={locations}
|
sites={locations}
|
||||||
vendors={vendors}
|
vendors={vendors}
|
||||||
pmTypes={pmTypes}
|
pmTypes={pmTypes}
|
||||||
onOpenSlideOver={(row) => tableData.setSelectedId(row.id)}
|
onOpenSlideOver={(row, edit) => {
|
||||||
|
tableData.setSelectedId(row.id);
|
||||||
|
navigate(edit ? `/workorders/edit/${row.id}` : `/workorders/${row.id}`);
|
||||||
|
}}
|
||||||
onOpenCompDoc={() => undefined}
|
onOpenCompDoc={() => undefined}
|
||||||
onOpenMedia={setMediaRow}
|
onOpenMedia={setMediaRow}
|
||||||
onClearSearch={() => filters.handleSearchChange("")}
|
onClearSearch={() => filters.handleSearchChange("")}
|
||||||
onOpenAdvancedFilters={() => filters.setAdvOpen(true)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{activeMediaRow && (
|
{activeMediaRow && (
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
import { fireEvent, screen } from "@testing-library/react";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { WorkOrdersListHeader } from "@/app/(protected)/workorders/_components/list/work-orders-list-header";
|
||||||
|
import WorkOrdersListPage from "@/app/(protected)/workorders/index";
|
||||||
|
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
import { renderWithProviders } from "@/test/test-utils";
|
||||||
|
|
||||||
|
const navigate = vi.fn();
|
||||||
|
|
||||||
|
vi.mock("react-router", async () => {
|
||||||
|
const actual = await vi.importActual<typeof import("react-router")>("react-router");
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useNavigate: () => navigate,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("@/providers/auth-context", () => ({
|
||||||
|
useAuthContext: () => ({ user: { id: "u1" } }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/app/(protected)/workorders/_hooks/use-work-orders-list-filters", () => ({
|
||||||
|
useWorkOrdersListFilters: () => ({
|
||||||
|
search: "",
|
||||||
|
handleSearchChange: vi.fn(),
|
||||||
|
setAdvOpen: vi.fn(),
|
||||||
|
activeFilterCount: 0,
|
||||||
|
advApplied: null,
|
||||||
|
clearAdvancedFilterChip: vi.fn(),
|
||||||
|
clearAdvancedFilters: vi.fn(),
|
||||||
|
weekMonday: "2026-07-20",
|
||||||
|
setWeekMonday: vi.fn(),
|
||||||
|
dispatcherIds: new Set<string>(),
|
||||||
|
setDispatcherIds: vi.fn(),
|
||||||
|
typeFilter: "All",
|
||||||
|
setTypeFilter: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/app/(protected)/workorders/_components/list/filter-bar/work-orders-filter-bar", () => ({
|
||||||
|
WorkOrdersFilterBar: () => null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock(
|
||||||
|
"@/app/(protected)/workorders/_components/list/work-orders-advanced-filters-banner",
|
||||||
|
() => ({
|
||||||
|
WorkOrdersAdvancedFiltersBanner: () => null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.mock("@/app/(protected)/workorders/_components/media/media-upload-modal", () => ({
|
||||||
|
MediaUploadModal: () => null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const canceledRow = {
|
||||||
|
id: 77,
|
||||||
|
status: "Canceled",
|
||||||
|
woNumber: "WO-77",
|
||||||
|
} as WorkOrderTableRow;
|
||||||
|
|
||||||
|
vi.mock("@/app/(protected)/workorders/_hooks/use-work-orders-table-data", () => ({
|
||||||
|
useWorkOrdersTableData: () => ({
|
||||||
|
rows: [canceledRow],
|
||||||
|
totalCount: 1,
|
||||||
|
scopeTotalCount: 1,
|
||||||
|
isFetching: false,
|
||||||
|
isLoading: false,
|
||||||
|
setSelectedId: vi.fn(),
|
||||||
|
patchRow: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/domain/work-orders/use-cases/use-work-order-table-mutations", () => ({
|
||||||
|
useWorkOrderTableMutations: () => ({ patchField: vi.fn() }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/domain/work-orders/use-cases/use-work-orders-list", () => ({
|
||||||
|
useWorkOrderUsers: () => ({ data: [] }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/domain/work-orders/use-cases/use-work-order-detail", () => ({
|
||||||
|
useWorkOrderLocations: () => ({ data: [] }),
|
||||||
|
useWorkOrderVendors: () => ({ data: [] }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/domain/settings/dropdown-options/use-cases/use-dropdown-options-by-category", () => ({
|
||||||
|
useDropdownOptionsByCategory: () => ({ data: [] }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/domain/work-orders/mocks/is-wo-table-mock-enabled", () => ({
|
||||||
|
isWoTableMockEnabled: () => false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/app/(protected)/workorders/_components/list/table/wo-table", () => ({
|
||||||
|
WoTable: ({
|
||||||
|
onOpenSlideOver,
|
||||||
|
}: {
|
||||||
|
onOpenSlideOver: (row: WorkOrderTableRow, edit?: boolean) => void;
|
||||||
|
}) => (
|
||||||
|
<button type="button" onClick={() => onOpenSlideOver(canceledRow, false)}>
|
||||||
|
View canceled row
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("WorkOrdersListHeader slice affordances", () => {
|
||||||
|
it("hides New WO and Advanced Filters when callbacks are omitted", () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<WorkOrdersListHeader search="" onSearchChange={vi.fn()} activeFilterCount={0} />,
|
||||||
|
{ withAuth: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByRole("button", { name: /new wo/i })).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByLabelText("Advanced Filters")).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText("Search work orders")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows New WO and Advanced Filters when callbacks are provided", () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<WorkOrdersListHeader
|
||||||
|
search=""
|
||||||
|
onSearchChange={vi.fn()}
|
||||||
|
onNewWorkOrder={vi.fn()}
|
||||||
|
onOpenAdvancedFilters={vi.fn()}
|
||||||
|
activeFilterCount={2}
|
||||||
|
/>,
|
||||||
|
{ withAuth: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByRole("button", { name: /new wo/i })).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText("Advanced Filters")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("WorkOrdersListPage board-core public interface", () => {
|
||||||
|
it("does not render New WO or Advanced Filters, and View navigates to detail", () => {
|
||||||
|
navigate.mockReset();
|
||||||
|
|
||||||
|
renderWithProviders(<WorkOrdersListPage />, { route: "/workorders", withAuth: false });
|
||||||
|
|
||||||
|
expect(screen.queryByRole("button", { name: /new wo/i })).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByLabelText("Advanced Filters")).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /view canceled row/i }));
|
||||||
|
expect(navigate).toHaveBeenCalledWith("/workorders/77");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue