mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-02 01:33:12 +00:00
fix(work-orders): keep in-flight uplift requests on the slide-over
Point notification deep links at the Uplifts tab and follow the unsaved Info draft. Block close while an evidence scan is still running.
This commit is contained in:
parent
3c5941b895
commit
fd561aa578
7 changed files with 252 additions and 11 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 144 KiB |
|
|
@ -34,6 +34,7 @@ type SlideOverUpliftsTabProps = {
|
|||
workOrderId: string | number;
|
||||
workOrderType: string;
|
||||
status: WorkOrderTableRow["status"];
|
||||
onCreatePendingChange?: (pending: boolean) => void;
|
||||
};
|
||||
|
||||
function revokeDialogDescription(amount: number | undefined): string {
|
||||
|
|
@ -71,6 +72,7 @@ export function SlideOverUpliftsTab({
|
|||
workOrderId,
|
||||
workOrderType,
|
||||
status,
|
||||
onCreatePendingChange,
|
||||
}: SlideOverUpliftsTabProps) {
|
||||
const { user } = useAuthContext();
|
||||
const readOnly = isWorkOrderUpliftsReadOnly(status);
|
||||
|
|
@ -134,7 +136,14 @@ export function SlideOverUpliftsTab({
|
|||
<WorkOrderUpliftCreateForm
|
||||
pending={createUplift.isPending}
|
||||
remainingAllowance={getRemainingUpliftAllowance(workOrderType, uplifts)}
|
||||
onSubmit={(input) => createUplift.mutateAsync(input)}
|
||||
onSubmit={async (input) => {
|
||||
onCreatePendingChange?.(true);
|
||||
try {
|
||||
await createUplift.mutateAsync(input);
|
||||
} finally {
|
||||
onCreatePendingChange?.(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showBody &&
|
||||
|
|
|
|||
|
|
@ -132,7 +132,8 @@ export function useWorkOrderSlideOver({
|
|||
categorizeMediaMutation,
|
||||
});
|
||||
const completionPending = uploadCompletionMutation.isPending;
|
||||
const closeDisabled = Boolean(saving || mediaPending || completionPending);
|
||||
const [upliftCreatePending, setUpliftCreatePending] = useState(false);
|
||||
const closeDisabled = Boolean(saving || mediaPending || completionPending || upliftCreatePending);
|
||||
|
||||
const editState = useSlideOverEditState({
|
||||
row,
|
||||
|
|
@ -176,6 +177,8 @@ export function useWorkOrderSlideOver({
|
|||
mediaPending,
|
||||
completionPending,
|
||||
closeDisabled,
|
||||
upliftCreatePending,
|
||||
setUpliftCreatePending,
|
||||
uploadMediaRequests,
|
||||
uploadExtraFiles,
|
||||
categorizeMedia,
|
||||
|
|
|
|||
|
|
@ -20,18 +20,31 @@ type SlideOverState = ReturnType<typeof useWorkOrderSlideOver>;
|
|||
|
||||
function SlideOverUpliftsPanel({
|
||||
tab,
|
||||
editing,
|
||||
infoSource,
|
||||
activeDraft,
|
||||
upliftCreatePending,
|
||||
onUpliftCreatePendingChange,
|
||||
}: {
|
||||
tab: SlideOverTab;
|
||||
editing: boolean;
|
||||
infoSource: WorkOrderTableRow | null | undefined;
|
||||
activeDraft: WorkOrderTableRow | null | undefined;
|
||||
upliftCreatePending: boolean;
|
||||
onUpliftCreatePendingChange?: (pending: boolean) => void;
|
||||
}) {
|
||||
if (tab !== "uplifts" || infoSource == null) return null;
|
||||
const visible = tab === "uplifts";
|
||||
if (infoSource == null || (!visible && !upliftCreatePending)) return null;
|
||||
const source = editing && activeDraft != null ? activeDraft : infoSource;
|
||||
return (
|
||||
<SlideOverUpliftsTab
|
||||
workOrderId={infoSource.id}
|
||||
workOrderType={infoSource.type}
|
||||
status={infoSource.status}
|
||||
/>
|
||||
<div hidden={!visible}>
|
||||
<SlideOverUpliftsTab
|
||||
workOrderId={infoSource.id}
|
||||
workOrderType={source.type}
|
||||
status={source.status}
|
||||
onCreatePendingChange={onUpliftCreatePendingChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +171,14 @@ export function WorkOrderSlideOverTabs({
|
|||
</WorkOrderSlideOverTabPanel>
|
||||
)}
|
||||
|
||||
<SlideOverUpliftsPanel tab={tab} infoSource={infoSource} />
|
||||
<SlideOverUpliftsPanel
|
||||
tab={tab}
|
||||
editing={Boolean(state.editing)}
|
||||
infoSource={infoSource}
|
||||
activeDraft={activeDraft}
|
||||
upliftCreatePending={Boolean(state.upliftCreatePending)}
|
||||
onUpliftCreatePendingChange={state.setUpliftCreatePending}
|
||||
/>
|
||||
|
||||
{tab === "extras" && (
|
||||
<WorkOrderSlideOverTabPanel
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export default function WorkOrdersListPage() {
|
|||
const fromDashboard = useDashboardDrilldownFilters(filters.applyAdvancedFilters);
|
||||
useWorkOrderDeepLink({
|
||||
onOpenSlideOver: page.handleOpenSlideOver,
|
||||
onOpenUplifts: page.setUpliftsRow,
|
||||
onOpenUplifts: page.handleOpenUplifts,
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,209 @@
|
|||
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { WorkOrderSlideOver } from "@/app/(protected)/workorders/_components/detail/work-order-slide-over";
|
||||
import { WorkOrderSlideOverTabs } from "@/app/(protected)/workorders/_components/detail/work-order-slide-over-tabs";
|
||||
import type { useWorkOrderSlideOver } from "@/app/(protected)/workorders/_components/detail/use-work-order-slide-over";
|
||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||
import { renderWithProviders } from "@/test/test-utils";
|
||||
|
||||
type SlideOverState = ReturnType<typeof useWorkOrderSlideOver>;
|
||||
|
||||
vi.mock("@/providers/auth-context", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@/providers/auth-context")>();
|
||||
return { ...actual, useAuthContext: () => ({ user: { id: "u1" } }) };
|
||||
});
|
||||
|
||||
vi.mock("@/domain/work-orders/use-cases/use-work-order-detail", () => ({
|
||||
useWorkOrderBoardDetail: () => ({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/domain/work-orders/use-cases/use-work-order-mutations", () => ({
|
||||
useAddWorkOrderComment: () => ({ mutate: vi.fn(), isPending: false }),
|
||||
useUpdateWorkOrderComment: () => ({ mutate: vi.fn(), isPending: false }),
|
||||
useUploadWorkOrderMedia: () => ({ mutate: vi.fn(), mutateAsync: vi.fn(), isPending: false }),
|
||||
useUpdateWorkOrderMediaCategory: () => ({ mutate: vi.fn(), isPending: false }),
|
||||
useDeleteWorkOrderMedia: () => ({ mutate: vi.fn(), isPending: false }),
|
||||
useUploadCompletionDoc: () => ({ mutate: vi.fn(), isPending: false }),
|
||||
}));
|
||||
|
||||
vi.mock("@/domain/work-orders/use-cases/use-work-order-uplifts", async () => {
|
||||
const { useState } = await import("react");
|
||||
return {
|
||||
useWorkOrderUplifts: () => ({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
}),
|
||||
useCreateWorkOrderUplift: () => {
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
return {
|
||||
isPending,
|
||||
mutateAsync: () => {
|
||||
setIsPending(true);
|
||||
return new Promise(() => undefined);
|
||||
},
|
||||
};
|
||||
},
|
||||
useCancelWorkOrderUplift: () => ({ mutate: vi.fn(), isPending: false }),
|
||||
useRevokeWorkOrderUplift: () => ({ mutate: vi.fn(), isPending: false }),
|
||||
};
|
||||
});
|
||||
|
||||
function baseRow(overrides: Partial<WorkOrderTableRow> = {}): WorkOrderTableRow {
|
||||
return {
|
||||
id: 42,
|
||||
woNumber: "WO-42",
|
||||
site: "SEA1",
|
||||
type: "PM",
|
||||
severity: null,
|
||||
dispatcherId: "u1",
|
||||
dispatcherName: "Dispatcher",
|
||||
scheduledOn: "2026-07-20",
|
||||
completedDate: "",
|
||||
targetWeek: "",
|
||||
dayGroup: "07/20",
|
||||
dayLabel: "Mon",
|
||||
location: "Seattle",
|
||||
locationId: "loc-1",
|
||||
pm: "HVAC",
|
||||
serviceNotes: "",
|
||||
extraServices: [],
|
||||
company: "Vendor Co",
|
||||
tech: "Tech",
|
||||
techPhone: "",
|
||||
vendorId: "v1",
|
||||
apptStart: "",
|
||||
apptEnd: "",
|
||||
apptTime: "",
|
||||
status: "Scheduled",
|
||||
docStatus: "No",
|
||||
pocName: "",
|
||||
pocPhone: "",
|
||||
pocNotes: "",
|
||||
additionalContacts: [],
|
||||
dueDate: "",
|
||||
rescheduleCount: 0,
|
||||
carriedOver: 0,
|
||||
originalWeek: "",
|
||||
originalDate: "",
|
||||
isPastDue: false,
|
||||
scheduleWeekOnly: false,
|
||||
mediaCount: 0,
|
||||
title: "PM — SEA1",
|
||||
priority: "Sev 3",
|
||||
primaryDispatchId: null,
|
||||
rowVersion: "v1",
|
||||
dispatchRowVersion: "",
|
||||
flagColor: null,
|
||||
isAddOn: false,
|
||||
avetaRequired: false,
|
||||
upliftSummary: { hasUplift: false, pendingCount: 0 },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function upliftsState(
|
||||
editing: boolean,
|
||||
persisted: Partial<WorkOrderTableRow>,
|
||||
draft: Partial<WorkOrderTableRow>,
|
||||
): SlideOverState {
|
||||
return {
|
||||
error: null,
|
||||
detailForRow: undefined,
|
||||
isLoading: false,
|
||||
editing,
|
||||
infoSource: baseRow(persisted),
|
||||
activeDraft: baseRow(draft),
|
||||
upliftCreatePending: false,
|
||||
setUpliftCreatePending: vi.fn(),
|
||||
} as unknown as SlideOverState;
|
||||
}
|
||||
|
||||
function renderUplifts(state: SlideOverState) {
|
||||
return renderWithProviders(
|
||||
<WorkOrderSlideOverTabs
|
||||
tab="uplifts"
|
||||
state={state}
|
||||
users={[]}
|
||||
sites={[]}
|
||||
vendors={[]}
|
||||
pmTypes={[]}
|
||||
allOrders={[]}
|
||||
onOpenCompletionDoc={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("WorkOrderSlideOverTabs Uplifts draft", () => {
|
||||
it("uses the unsaved Info type for the auto-approval allowance", () => {
|
||||
renderUplifts(upliftsState(true, { type: "PM" }, { type: "Emergency" }));
|
||||
|
||||
expect(screen.getByText(/\$5,000 left to auto-approve/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps the persisted allowance when the slide-over is not editing", () => {
|
||||
renderUplifts(upliftsState(false, { type: "PM" }, { type: "Emergency" }));
|
||||
|
||||
expect(screen.getByText(/\$500 left to auto-approve/i)).toBeInTheDocument();
|
||||
expect(screen.queryByText(/\$5,000 left to auto-approve/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("closes uplift requests from an unsaved completed status", () => {
|
||||
renderUplifts(upliftsState(true, { status: "Scheduled" }, { status: "Completed" }));
|
||||
|
||||
expect(screen.getByText(/uplifts can no longer be requested/i)).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /request uplift/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps the request form when a completed status exists only on an inactive draft", () => {
|
||||
renderUplifts(upliftsState(false, { status: "Scheduled" }, { status: "Completed" }));
|
||||
|
||||
expect(screen.queryByText(/uplifts can no longer be requested/i)).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /request uplift/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("WorkOrderSlideOver in-flight uplift create", () => {
|
||||
it("keeps the request pending and blocks close after leaving the Uplifts tab", async () => {
|
||||
const row = baseRow();
|
||||
renderWithProviders(
|
||||
<WorkOrderSlideOver
|
||||
row={row}
|
||||
editMode={false}
|
||||
initialTab="uplifts"
|
||||
users={[{ id: "u1", name: "Dispatcher" }]}
|
||||
sites={[{ id: "loc-1", name: "Seattle" }]}
|
||||
vendors={[]}
|
||||
pmTypes={["HVAC"]}
|
||||
allOrders={[row]}
|
||||
onSave={vi.fn()}
|
||||
onOpenCompletionDoc={vi.fn()}
|
||||
onCancelWO={vi.fn()}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/amount/i), { target: { value: "120" } });
|
||||
fireEvent.change(screen.getByLabelText(/^notes/i), { target: { value: "Extra parts" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: /request uplift/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Close" })).toBeDisabled();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Info" }));
|
||||
|
||||
expect(screen.getByRole("button", { name: "Close" })).toBeDisabled();
|
||||
const amount = document.getElementById("wo-uplift-amount");
|
||||
expect(amount).toHaveValue(120);
|
||||
expect(amount?.closest("[hidden]")).not.toBeNull();
|
||||
expect(screen.getByRole("button", { name: /request uplift/i, hidden: true })).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
|
@ -86,7 +86,7 @@ describe("work order deep link", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("opens the uplifts dialog for an uplifts link instead of the slide-over", async () => {
|
||||
it("opens the uplifts handler for an uplifts link", async () => {
|
||||
const open = handlers();
|
||||
const { result } = renderLink("/workorders?wo=42&tab=uplifts", open);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue