Merge remote-tracking branch 'origin/main' into lane/pr261

This commit is contained in:
Alexandre Brandizzi 2026-09-25 03:02:25 -03:00
commit b91682f53c
21 changed files with 1515 additions and 1025 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 195 KiB

View file

@ -349,7 +349,7 @@ test("internal approver can approve, reject, and revoke with audited notes", asy
await expect(page.getByRole("heading", { name: "Uplift Approvals" })).toBeVisible();
const approvalRow = page.getByRole("row").filter({ hasText: "DSP-41" });
await approvalRow.getByRole("button", { name: "Approve" }).click();
await approvalRow.getByRole("button", { name: "Approve $25.00" }).click();
const changesRow = page.getByRole("row").filter({ hasText: "DSP-42" });
await changesRow.getByRole("button", { name: "Reject" }).click();
@ -360,7 +360,7 @@ test("internal approver can approve, reject, and revoke with audited notes", asy
const approvedRow = page.getByRole("row").filter({ hasText: "DSP-41" });
await approvedRow.getByRole("button", { name: "Revoke" }).click();
await page.getByLabel("Reason for revoking").fill("Approval was made in error.");
await page.getByRole("button", { name: "Revoke $125" }).click();
await page.getByRole("button", { name: "Revoke $25.00" }).click();
await expect
.poll(() => decisions)

2026
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -50,63 +50,63 @@
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@fontsource/dm-sans": "^5.2.8",
"@fontsource/jetbrains-mono": "^5.2.8",
"@fontsource/montserrat": "^5.2.8",
"@fontsource/dm-sans": "^5.3.0",
"@fontsource/jetbrains-mono": "^5.3.0",
"@fontsource/montserrat": "^5.3.0",
"@fullcalendar/daygrid": "^6.1.21",
"@fullcalendar/interaction": "^6.1.21",
"@fullcalendar/react": "^6.1.21",
"@fullcalendar/timegrid": "^6.1.21",
"@generouted/react-router": "^1.20.2",
"@hookform/resolvers": "^5.4.0",
"@mui/icons-material": "^9.2.0",
"@mui/material": "^9.2.0",
"@sentry/react": "10.73.0",
"@hookform/resolvers": "^5.9.1",
"@mui/icons-material": "9.2.0",
"@mui/material": "9.2.0",
"@sentry/react": "10.75.0",
"@tanstack/query-broadcast-client-experimental": "5.103.1",
"@tanstack/react-query": "5.103.1",
"@tanstack/react-query-devtools": "5.103.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/dom": "^10.4.2",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@testing-library/react": "^16.3.3",
"@testing-library/user-event": "^14.6.7",
"clsx": "^2.1.1",
"ky": "^2.0.2",
"lucide-react": "^1.24.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-hook-form": "^7.81.0",
"ky": "^2.1.0",
"lucide-react": "^1.47.0",
"react": "^19.3.0",
"react-dom": "^19.3.0",
"react-hook-form": "^7.88.0",
"react-icons": "^5.7.0",
"react-router": "^8.2.0",
"react-router": "^8.4.0",
"react-toastify": "^11.1.0",
"tailwind-merge": "^3.6.0",
"zod": "^4.4.3"
"tailwind-merge": "^3.7.0",
"zod": "^4.6.5"
},
"devDependencies": {
"@commitlint/cli": "^21.2.1",
"@commitlint/config-conventional": "^21.2.0",
"@commitlint/cli": "^21.2.3",
"@commitlint/config-conventional": "^21.2.3",
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.61.1",
"@sentry/cli": "3.7.0",
"@tailwindcss/vite": "^4.3.2",
"@types/node": "^24.13.3",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.3",
"@vitest/coverage-v8": "^4.1.10",
"eslint": "^10.7.0",
"@sentry/cli": "3.8.0",
"@tailwindcss/vite": "^4.3.3",
"@types/node": "^24.13.6",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.1",
"@vitest/coverage-v8": "^4.1.11",
"eslint": "^10.11.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.3",
"globals": "^17.7.0",
"eslint-plugin-react-refresh": "^0.5.7",
"globals": "^17.12.0",
"husky": "^9.1.7",
"jsdom": "^29.1.1",
"lint-staged": "^17.0.8",
"prettier": "^3.9.5",
"tailwindcss": "^4.3.2",
"lint-staged": "^17.5.1",
"prettier": "^3.9.8",
"tailwindcss": "^4.3.3",
"typescript": "~6.0.3",
"typescript-eslint": "^8.63.0",
"vite": "^8.1.4",
"vitest": "^4.1.10"
"typescript-eslint": "^8.70.0",
"vite": "^8.3.0",
"vitest": "^4.1.11"
},
"engines": {
"node": ">=22.22.1"

View file

@ -4,3 +4,7 @@ export function formatUpliftMoney(value: number): string {
maximumFractionDigits: 2,
})}`;
}
export function isGrantedAmount(value: number | null): value is number {
return value != null && value > 0;
}

View file

@ -12,9 +12,13 @@ import {
TableRow,
Tooltip,
} from "@mui/material";
import { formatUpliftMoney } from "@/app/(protected)/uplifts/_components/format-uplift-money";
import {
formatUpliftMoney,
isGrantedAmount,
} from "@/app/(protected)/uplifts/_components/format-uplift-money";
import { Text } from "@/components/ui/text";
import { useUpliftsCanApprove } from "@/domain/uplifts/use-cases/use-uplifts-can-approve";
import { upliftAmount } from "@/domain/uplifts/utils/uplift-amount";
import type { UpliftQueueItem } from "@/domain/uplifts/types/uplift";
import { formatDateTime, getWaitTimeTextClass, timeSince, waitTimeColor } from "@/lib/time-utils";
@ -100,7 +104,7 @@ function PendingRowActions({
disabled={!canDecide || isDecisionPending}
onClick={() => onApprove(row)}
>
Approve
Approve {formatUpliftMoney(upliftAmount(row))}
</Button>
</span>
</Tooltip>
@ -211,11 +215,11 @@ function UpliftApprovalRow({
</TableCell>
<TableCell>
<Text variant="body" className="font-medium">
{formatUpliftMoney(row.requestedNTE)}
{formatUpliftMoney(upliftAmount(row))}
</Text>
</TableCell>
<TableCell>
{row.approvedOnWoTotal != null ? (
{isGrantedAmount(row.approvedOnWoTotal) ? (
<Text variant="body">{formatUpliftMoney(row.approvedOnWoTotal)}</Text>
) : (
<Text variant="caption" tone="muted">

View file

@ -1,5 +1,6 @@
import { RejectDialog } from "@/app/(protected)/uplifts/_components/reject-dialog";
import { RevokeDialog } from "@/app/(protected)/uplifts/_components/revoke-dialog";
import { upliftAmount } from "@/domain/uplifts/utils/uplift-amount";
import type { useUpliftApprovalController } from "@/domain/uplifts/use-cases/use-uplift-approval-controller";
export type UpliftApprovalController = ReturnType<typeof useUpliftApprovalController>;
@ -23,7 +24,7 @@ export function UpliftDecisionDialogs({ controller }: { controller: UpliftApprov
<>
<RejectDialog
open={rejectTarget != null}
amount={rejectTarget?.requestedNTE}
amount={rejectTarget ? upliftAmount(rejectTarget) : undefined}
woNumber={woFallbackNumber(rejectTarget)}
onClose={closeReject}
onConfirm={handleRejectConfirm}
@ -31,7 +32,7 @@ export function UpliftDecisionDialogs({ controller }: { controller: UpliftApprov
/>
<RevokeDialog
open={revokeTarget != null}
amount={revokeTarget?.requestedNTE}
amount={revokeTarget ? upliftAmount(revokeTarget) : undefined}
woNumber={woFallbackNumber(revokeTarget)}
onClose={closeRevoke}
onConfirm={handleRevokeConfirm}

View file

@ -16,10 +16,14 @@ import {
formatUpliftCalendarDate,
formatUpliftDateTime,
} from "@/app/(protected)/uplifts/_components/format-uplift-dates";
import { formatUpliftMoney } from "@/app/(protected)/uplifts/_components/format-uplift-money";
import {
formatUpliftMoney,
isGrantedAmount,
} from "@/app/(protected)/uplifts/_components/format-uplift-money";
import { Text } from "@/components/ui/text";
import { useWorkOrderUplifts } from "@/domain/work-orders/use-cases/use-work-order-uplifts";
import { useUpliftsCanApprove } from "@/domain/uplifts/use-cases/use-uplifts-can-approve";
import { upliftAmount } from "@/domain/uplifts/utils/uplift-amount";
import type { UpliftQueueItem } from "@/domain/uplifts/types/uplift";
const CLOSED_WO_TOOLTIP = "This work order is closed. Uplifts can no longer be revoked.";
@ -142,6 +146,7 @@ function ApprovedOnWoBreakdown({
breakdown: ExposureBreakdown;
unavailable: boolean;
}) {
const grantedTotal = isGrantedAmount(breakdown.total) ? breakdown.total : null;
return (
<Box sx={BORDERED_BLOCK_SX}>
<BreakdownRow
@ -156,7 +161,12 @@ function ApprovedOnWoBreakdown({
strong={false}
unavailable={unavailable}
/>
<BreakdownRow label="Total" value={breakdown.total} strong unavailable={unavailable} />
<BreakdownRow
label="Total"
value={grantedTotal}
strong
unavailable={unavailable && breakdown.total == null}
/>
</Box>
);
}
@ -272,7 +282,7 @@ function PendingModalActions({
disabled={!canDecide || approvePending}
onClick={() => onApprove(item)}
>
Approve
Approve {formatUpliftMoney(upliftAmount(item))}
</Button>
</span>
</Tooltip>
@ -385,7 +395,7 @@ function UpliftDetailContent({
Amount
</Text>
<Text variant="heading" as="span" tone="warning">
{formatUpliftMoney(item.requestedNTE)}
{formatUpliftMoney(upliftAmount(item))}
</Text>
</Box>
<WorkOrderSection item={item} />

View file

@ -5,6 +5,7 @@ import {
useRejectUplift,
useRevokeUplift,
} from "@/domain/uplifts/use-cases/use-uplift-actions";
import { upliftAmount } from "@/domain/uplifts/utils/uplift-amount";
export function useUpliftApprovalController() {
const [detailItem, setDetailItem] = useState<UpliftQueueItem | null>(null);
@ -15,7 +16,7 @@ export function useUpliftApprovalController() {
const revokeUplift = useRevokeUplift();
const handleApprove = (row: UpliftQueueItem) => {
approveUplift.mutate({ id: row.id, amount: row.delta, woNumber: row.woNumber });
approveUplift.mutate({ id: row.id, amount: upliftAmount(row), woNumber: row.woNumber });
setDetailItem(null);
};
@ -35,7 +36,7 @@ export function useUpliftApprovalController() {
{
id: rejectTarget.id,
note: reason,
amount: rejectTarget.delta,
amount: upliftAmount(rejectTarget),
woNumber: rejectTarget.woNumber,
},
{ onSuccess: () => setRejectTarget(null) },
@ -48,7 +49,7 @@ export function useUpliftApprovalController() {
{
upliftId: revokeTarget.id,
reason,
amount: revokeTarget.delta,
amount: upliftAmount(revokeTarget),
woNumber: revokeTarget.woNumber,
},
{ onSuccess: () => setRevokeTarget(null) },

View file

@ -0,0 +1,11 @@
import type { UpliftRequest } from "@/domain/uplifts/types/uplift";
/**
* The amount of an uplift: the increase being requested on top of the work
* order's current NTE. Every surface that names "the uplift" (queue Amount,
* Approve label, detail modal, reject/revoke dialogs and decision toasts)
* shows this figure; `requestedNTE` is the resulting NTE total, not the uplift.
*/
export function upliftAmount(request: Pick<UpliftRequest, "delta">): number {
return request.delta;
}

View file

@ -132,7 +132,7 @@ describe("Uplift Approvals approved tab", () => {
expect(screen.getByRole("heading", { name: "Revoke this approval?" })).toBeInTheDocument();
expect(
screen.getByText(
"The approved uplift of $900.00 on WO WO-56 will be withdrawn. This does not recover money already spent — it records that the authorization was a mistake.",
"The approved uplift of $400.00 on WO WO-56 will be withdrawn. This does not recover money already spent — it records that the authorization was a mistake.",
),
).toBeInTheDocument();
});

View file

@ -0,0 +1,189 @@
import { fireEvent, screen, waitFor, within } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { UpliftQueueItem, UpliftsQueueResult } from "@/domain/uplifts/types/uplift";
import { renderWithProviders } from "@/test/test-utils";
// A work order that already carries an NTE: the uplift (the increase) and the
// requested NTE total differ, so any surface reading the wrong field shows up.
const UPLIFT = "$4,999,997,000.00";
const REQUESTED_NTE_TOTAL = "$5,000,000,000.00";
const api = vi.hoisted(() => ({
approve: vi.fn(),
reject: vi.fn(),
revoke: vi.fn(),
}));
const toastSuccess = vi.hoisted(() => vi.fn());
const queueState = vi.hoisted(() => ({
data: null as UpliftsQueueResult | null,
}));
vi.mock("@/domain/uplifts/api/uplifts-api", () => ({
upliftsApi: {
approve: (...args: unknown[]) => api.approve(...args),
reject: (...args: unknown[]) => api.reject(...args),
revoke: (...args: unknown[]) => api.revoke(...args),
},
}));
vi.mock("react-toastify", async (importOriginal) => ({
...(await importOriginal<object>()),
toast: { error: vi.fn(), success: toastSuccess, warning: vi.fn() },
}));
vi.mock("@/domain/uplifts/use-cases/use-uplifts-queue", () => ({
useUpliftsQueue: () => ({
data: queueState.data,
isLoading: false,
isFetching: false,
error: null,
refetch: vi.fn(),
}),
}));
vi.mock("@/domain/work-orders/use-cases/use-work-order-uplifts", () => ({
useWorkOrderUplifts: () => ({ data: [], isLoading: false, isError: false }),
}));
vi.mock("@/domain/uplifts/use-cases/use-uplifts-can-approve", () => ({
useUpliftsCanApprove: () => ({ data: true }),
}));
vi.mock("@/providers/auth-context", async (importOriginal) => ({
...(await importOriginal<object>()),
useAuthContext: () => ({ user: { userRoles: "Admin" } }),
}));
const pendingItem: UpliftQueueItem = {
id: 41,
status: "Pending",
currentNTE: 3000,
requestedNTE: 5_000_000_000,
delta: 4_999_997_000,
vendorReason: "Full replacement",
requestedAt: "2026-01-15T10:00:00Z",
requestedByVendorName: "Gateway",
decidedAt: "",
decidedByName: "",
decisionNote: "",
requiredTier: 1,
canDecide: true,
expiresAt: "",
notificationStatus: "",
notificationError: "",
evidenceDocumentId: null,
evidenceFileName: "",
evidenceContentType: "",
evidenceSizeBytes: null,
dispatchNumber: "DSP-41",
poNumber: "PO-41",
vendorCompanyName: "Gateway Plumbing",
workOrderId: 99,
dispatchId: 7,
woNumber: "WO-99",
site: "Site A",
serviceName: "Plumbing repair",
technicianName: "",
workOrderDispatcherName: "",
workOrderScheduledDate: "",
attachmentCount: null,
approvedOnWoAuto: null,
approvedOnWoAdmin: null,
approvedOnWoTotal: null,
workOrderClosed: false,
};
const approvedItem: UpliftQueueItem = {
...pendingItem,
id: 42,
status: "Approved",
decidedAt: "2026-01-16T10:00:00Z",
decidedByName: "Admin User",
canDecide: false,
};
function queueOf(item: UpliftQueueItem): UpliftsQueueResult {
return { items: [item], totalCount: 1, page: 1, pageSize: 25, pendingExposureTotal: null };
}
async function renderQueue() {
const { default: UpliftQueuePage } = await import("@/app/(protected)/uplifts/index");
renderWithProviders(<UpliftQueuePage />);
}
function amountCell(): HTMLElement {
const row = screen.getByLabelText(/open uplift details for/i);
const header = screen.getByRole("columnheader", { name: "Amount" });
const index = Array.from(header.parentElement?.children ?? []).indexOf(header);
return row.querySelectorAll("td")[index] as HTMLElement;
}
describe("Uplift Approvals shows one amount per request", () => {
beforeEach(() => {
api.approve.mockReset().mockResolvedValue(undefined);
api.reject.mockReset().mockResolvedValue(undefined);
api.revoke.mockReset().mockResolvedValue(undefined);
toastSuccess.mockReset();
});
it("uses the uplift amount in the pending row, the detail modal and the approve toast", async () => {
queueState.data = queueOf(pendingItem);
await renderQueue();
expect(amountCell()).toHaveTextContent(UPLIFT);
expect(screen.getByRole("button", { name: `Approve ${UPLIFT}` })).toBeInTheDocument();
fireEvent.click(screen.getByLabelText(/open uplift details for/i));
const modal = screen.getByRole("dialog");
expect(within(modal).getByText(UPLIFT)).toBeInTheDocument();
fireEvent.click(within(modal).getByRole("button", { name: `Approve ${UPLIFT}` }));
await waitFor(() =>
expect(toastSuccess).toHaveBeenCalledWith(`Uplift approved — ${UPLIFT} on WO-99`),
);
expect(screen.queryByText(REQUESTED_NTE_TOTAL)).not.toBeInTheDocument();
});
it("uses the uplift amount in the reject dialog title and the reject toast", async () => {
queueState.data = queueOf(pendingItem);
await renderQueue();
fireEvent.click(screen.getByRole("button", { name: "Reject" }));
expect(
screen.getByRole("heading", { name: `Reject uplift of ${UPLIFT}?` }),
).toBeInTheDocument();
fireEvent.change(screen.getByLabelText(/reason for rejection/i), {
target: { value: "Out of scope" },
});
fireEvent.click(screen.getByRole("button", { name: "Reject uplift" }));
await waitFor(() =>
expect(toastSuccess).toHaveBeenCalledWith(`Uplift rejected — ${UPLIFT} on WO-99`),
);
expect(screen.queryByText(REQUESTED_NTE_TOTAL)).not.toBeInTheDocument();
});
it("uses the uplift amount in the approved row, the revoke dialog and the revoke toast", async () => {
queueState.data = queueOf(approvedItem);
await renderQueue();
fireEvent.click(screen.getByRole("tab", { name: "Approved" }));
expect(amountCell()).toHaveTextContent(UPLIFT);
fireEvent.click(screen.getByRole("button", { name: "Revoke" }));
expect(
screen.getByText(
`The approved uplift of ${UPLIFT} on WO WO-99 will be withdrawn. This does not recover money already spent — it records that the authorization was a mistake.`,
),
).toBeInTheDocument();
fireEvent.change(screen.getByLabelText(/reason for revoking/i), {
target: { value: "Approved in error" },
});
fireEvent.click(screen.getByRole("button", { name: `Revoke ${UPLIFT}` }));
await waitFor(() =>
expect(toastSuccess).toHaveBeenCalledWith(`Uplift revoked — ${UPLIFT} on WO-99`),
);
expect(screen.queryByText(REQUESTED_NTE_TOTAL)).not.toBeInTheDocument();
});
});

View file

@ -0,0 +1,160 @@
import { fireEvent, screen, within } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { UpliftQueueItem, UpliftsQueueResult } from "@/domain/uplifts/types/uplift";
import { renderWithProviders } from "@/test/test-utils";
const queueState = vi.hoisted(() => ({ data: null as UpliftsQueueResult | null }));
const woUplifts = vi.hoisted(() => ({
data: [] as Array<Record<string, unknown>> | null,
isError: false,
}));
vi.mock("@/domain/uplifts/use-cases/use-uplifts-queue", () => ({
useUpliftsQueue: () => ({
data: queueState.data,
isLoading: false,
isFetching: false,
error: null,
refetch: vi.fn(),
}),
}));
vi.mock("@/domain/uplifts/use-cases/use-uplift-actions", () => ({
useApproveUplift: () => ({ isPending: false, mutate: vi.fn() }),
useRejectUplift: () => ({ isPending: false, mutate: vi.fn() }),
useRevokeUplift: () => ({ isPending: false, mutate: vi.fn() }),
}));
vi.mock("@/providers/auth-context", async (importOriginal) => ({
...(await importOriginal<object>()),
useAuthContext: () => ({ user: { userRoles: "Admin" } }),
}));
vi.mock("@/domain/uplifts/use-cases/use-uplifts-can-approve", () => ({
useUpliftsCanApprove: () => ({ data: true }),
}));
vi.mock("@/domain/work-orders/use-cases/use-work-order-uplifts", () => ({
useWorkOrderUplifts: () => ({
data: woUplifts.data,
isLoading: false,
isError: woUplifts.isError,
}),
}));
const pendingItem: UpliftQueueItem = {
id: 41,
status: "Pending",
currentNTE: 100,
requestedNTE: 250,
delta: 150,
vendorReason: "Extra parts",
requestedAt: "2026-01-15T10:00:00Z",
requestedByVendorName: "Gateway",
decidedAt: "",
decidedByName: "",
decisionNote: "",
requiredTier: 1,
canDecide: true,
expiresAt: "",
notificationStatus: "",
notificationError: "",
evidenceDocumentId: null,
evidenceFileName: "",
evidenceContentType: "",
evidenceSizeBytes: null,
dispatchNumber: "DSP-41",
poNumber: "PO-41",
vendorCompanyName: "Gateway Plumbing",
workOrderId: 99,
dispatchId: 7,
woNumber: "WO-99",
site: "Site A",
serviceName: "Plumbing repair",
technicianName: "",
workOrderDispatcherName: "",
workOrderScheduledDate: "",
attachmentCount: null,
approvedOnWoAuto: null,
approvedOnWoAdmin: null,
approvedOnWoTotal: null,
workOrderClosed: false,
};
function seedQueue(overrides: Partial<UpliftQueueItem>) {
queueState.data = {
items: [{ ...pendingItem, ...overrides }],
totalCount: 1,
page: 1,
pageSize: 25,
pendingExposureTotal: 250,
};
}
async function renderQueuePage() {
const { default: UpliftQueuePage } = await import("@/app/(protected)/uplifts/index");
renderWithProviders(<UpliftQueuePage />);
}
function approvedOnWoColumnText(): string {
const table = screen.getByRole("table", { name: "pending uplift requests" });
const headers = within(table).getAllByRole("columnheader");
const columnIndex = headers.findIndex((header) => header.textContent === "Approved on WO");
const row = within(table).getByRole("row", { name: /Open uplift details for WO-99/ });
return within(row).getAllByRole("cell")[columnIndex]?.textContent ?? "";
}
function modalTotalText(): string {
fireEvent.click(screen.getByRole("row", { name: /Open uplift details for WO-99/ }));
const dialog = screen.getByRole("dialog");
const label = within(dialog).getByText("Total");
return label.parentElement?.lastElementChild?.textContent ?? "";
}
describe("Uplift Approvals: Approved on WO", () => {
beforeEach(() => {
woUplifts.data = [];
woUplifts.isError = false;
});
it.each([
{ total: 0, expected: "—" },
{ total: null, expected: "—" },
{ total: 125, expected: "$125.00" },
])(
"renders $expected in the column and the modal Total for total $total",
async ({ total, expected }) => {
seedQueue({ approvedOnWoTotal: total });
await renderQueuePage();
const column = approvedOnWoColumnText();
const modalTotal = modalTotalText();
expect(column).toBe(expected);
expect(modalTotal).toBe(column);
},
);
it("shows a dash in both places when the row total is unknown and the WO has nothing granted", async () => {
woUplifts.data = [
{ id: 1, status: "pending", amount: 250 },
{ id: 2, status: "rejected", amount: 80 },
{ id: 3, status: "revoked", amount: 40 },
];
seedQueue({ approvedOnWoTotal: null });
await renderQueuePage();
expect(approvedOnWoColumnText()).toBe("—");
expect(modalTotalText()).toBe("—");
});
it("keeps the modal Total as a dash when the WO uplift lookup fails but the row reports zero", async () => {
woUplifts.data = null;
woUplifts.isError = true;
seedQueue({ approvedOnWoAuto: 0, approvedOnWoAdmin: 0, approvedOnWoTotal: 0 });
await renderQueuePage();
expect(approvedOnWoColumnText()).toBe("—");
expect(modalTotalText()).toBe("—");
});
});

View file

@ -195,10 +195,10 @@ describe("UpliftDetailModal", () => {
const footerButtons = screen
.getAllByRole("button")
.map((button) => button.textContent)
.filter((label) => label === "Reject" || label === "Approve" || label === "Revoke");
expect(footerButtons).toEqual(["Reject", "Approve"]);
.filter((label) => label === "Reject" || label === "Approve $150.00" || label === "Revoke");
expect(footerButtons).toEqual(["Reject", "Approve $150.00"]);
fireEvent.click(screen.getByRole("button", { name: "Approve" }));
fireEvent.click(screen.getByRole("button", { name: "Approve $150.00" }));
expect(onApprove).toHaveBeenCalled();
fireEvent.click(screen.getByRole("button", { name: "Reject" }));
expect(onReject).toHaveBeenCalled();
@ -208,7 +208,7 @@ describe("UpliftDetailModal", () => {
canApproveState.data = false;
const { onApprove, onReject } = renderModal();
const approve = screen.getByRole("button", { name: "Approve" });
const approve = screen.getByRole("button", { name: "Approve $150.00" });
const reject = screen.getByRole("button", { name: "Reject" });
expect(approve).toBeDisabled();
expect(reject).toBeDisabled();
@ -223,7 +223,7 @@ describe("UpliftDetailModal", () => {
it("offers Revoke for an approved uplift and disables it with a tooltip on a closed work order", async () => {
const { onRevoke } = renderModal({ status: "Approved", workOrderClosed: true });
expect(screen.queryByRole("button", { name: "Approve" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /^Approve/ })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Reject" })).not.toBeInTheDocument();
const revoke = screen.getByRole("button", { name: "Revoke" });
expect(revoke).toBeDisabled();

View file

@ -106,7 +106,7 @@ describe("Uplift Approvals decision flow", () => {
const { default: UpliftQueuePage } = await import("@/app/(protected)/uplifts/index");
renderWithProviders(<UpliftQueuePage />);
fireEvent.click(screen.getByRole("button", { name: "Approve" }));
fireEvent.click(screen.getByRole("button", { name: "Approve $150.00" }));
expect(approveMutate).toHaveBeenCalledWith({ id: 41, amount: 150, woNumber: "WO-99" });
expect(screen.queryByRole("button", { name: "Confirm" })).not.toBeInTheDocument();
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
@ -117,7 +117,7 @@ describe("Uplift Approvals decision flow", () => {
renderWithProviders(<UpliftQueuePage />);
fireEvent.click(screen.getByRole("button", { name: "Reject" }));
expect(screen.getByRole("heading", { name: /reject uplift of \$250/i })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Reject uplift of $150.00?" })).toBeInTheDocument();
expect(screen.getByText("The dispatcher sees this reason on WO #WO-99.")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
expect(rejectMutate).not.toHaveBeenCalled();
@ -142,7 +142,7 @@ describe("Uplift Approvals decision flow", () => {
const { default: UpliftQueuePage } = await import("@/app/(protected)/uplifts/index");
renderWithProviders(<UpliftQueuePage />);
const approve = screen.getByRole("button", { name: "Approve" });
const approve = screen.getByRole("button", { name: "Approve $150.00" });
const reject = screen.getByRole("button", { name: "Reject" });
expect(approve).toBeDisabled();
expect(reject).toBeDisabled();
@ -160,7 +160,7 @@ describe("Uplift Approvals decision flow", () => {
const { default: UpliftQueuePage } = await import("@/app/(protected)/uplifts/index");
renderWithProviders(<UpliftQueuePage />);
expect(screen.getByRole("button", { name: "Approve" })).toBeEnabled();
expect(screen.getByRole("button", { name: "Approve $150.00" })).toBeEnabled();
expect(screen.getByRole("button", { name: "Reject" })).toBeEnabled();
});
});

View file

@ -33,7 +33,7 @@ function openSection() {
}
}
describe("Advanced Filters date range (SH-391)", () => {
describe("Advanced Filters date range", () => {
it("starts with no range selected, so the search spans every week", () => {
openSection();

View file

@ -420,7 +420,7 @@ describe("workOrdersApi.searchBoard", () => {
});
});
/** A WO# lookup spans every week and undated rows (SH-391). */
/** A WO# lookup spans every week and undated rows. */
const ALL_WEEKS_SEARCH = {
datePreset: "Custom",
dateFrom: "2000-01-01",

View file

@ -135,7 +135,7 @@ describe("toBoardSearchQueryParams", () => {
expect(thisWeek.dateTo).toBeUndefined();
});
it("narrows strictly for every selected range, This week included (SH-391)", () => {
it("narrows strictly for every selected range, This week included", () => {
for (const dateRange of ["this-week", "last-week", "last-3-months", "custom"] as const) {
const params = toBoardSearchQueryParams({
filters: {
@ -151,7 +151,7 @@ describe("toBoardSearchQueryParams", () => {
}
});
it("searches every week, undated rows included, when no range is selected (SH-391)", () => {
it("searches every week, undated rows included, when no range is selected", () => {
const params = toBoardSearchQueryParams({
filters: { ...EMPTY_ADVANCED_FILTERS, dispatchers: new Set(["__unassigned__"]) },
});
@ -165,7 +165,7 @@ describe("toBoardSearchQueryParams", () => {
});
});
it("keeps undated rows in the pinned Unassigned queue (SH-391)", () => {
it("keeps undated rows in the pinned Unassigned queue", () => {
expect(toUnassignedQueueQueryParams().includeDateless).toBe(true);
});

View file

@ -455,7 +455,7 @@ describe("matchesAdvancedFilters", () => {
expect(matchesAdvancedFilters(undatedAssigned, filters)).toBe(true);
});
it("matches a range the same way the board search does (SH-391)", () => {
it("matches a range the same way the board search does", () => {
const filters = {
...EMPTY_ADVANCED_FILTERS,
dateRange: "custom" as const,