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:
Arthur Bassi 2026-09-28 12:51:28 -03:00
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

View file

@ -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 &&

View file

@ -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,

View file

@ -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

View file

@ -30,7 +30,7 @@ export default function WorkOrdersListPage() {
const fromDashboard = useDashboardDrilldownFilters(filters.applyAdvancedFilters);
useWorkOrderDeepLink({
onOpenSlideOver: page.handleOpenSlideOver,
onOpenUplifts: page.setUpliftsRow,
onOpenUplifts: page.handleOpenUplifts,
});
return (

View file

@ -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();
});
});

View file

@ -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);