From ec3f97dcf6e3197109ebb22472b2cd2038f22c8c Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Thu, 24 Sep 2026 22:54:39 -0300 Subject: [PATCH] fix(uplifts): show a dash for Approved on WO when nothing is granted (SH-394) A work order with a zero granted total rendered $0.00 in the Pending table's Approved on WO column and in the detail modal's Total line. Both now share isGrantedAmount, so zero or unknown totals render a dash and the column always matches the modal Total. --- .../_components/format-uplift-money.ts | 4 + .../_components/uplift-approvals-table.tsx | 7 +- .../_components/uplift-detail-modal.tsx | 13 +- .../uplift-approved-on-wo-dash.test.tsx | 147 ++++++++++++++++++ 4 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 src/test/app/(protected)/uplifts/uplift-approved-on-wo-dash.test.tsx diff --git a/src/app/(protected)/uplifts/_components/format-uplift-money.ts b/src/app/(protected)/uplifts/_components/format-uplift-money.ts index 01aa64ff..b40357db 100644 --- a/src/app/(protected)/uplifts/_components/format-uplift-money.ts +++ b/src/app/(protected)/uplifts/_components/format-uplift-money.ts @@ -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; +} diff --git a/src/app/(protected)/uplifts/_components/uplift-approvals-table.tsx b/src/app/(protected)/uplifts/_components/uplift-approvals-table.tsx index fb40d7c1..5cc446ce 100644 --- a/src/app/(protected)/uplifts/_components/uplift-approvals-table.tsx +++ b/src/app/(protected)/uplifts/_components/uplift-approvals-table.tsx @@ -12,7 +12,10 @@ 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 type { UpliftQueueItem } from "@/domain/uplifts/types/uplift"; @@ -215,7 +218,7 @@ function UpliftApprovalRow({ - {row.approvedOnWoTotal != null ? ( + {isGrantedAmount(row.approvedOnWoTotal) ? ( {formatUpliftMoney(row.approvedOnWoTotal)} ) : ( diff --git a/src/app/(protected)/uplifts/_components/uplift-detail-modal.tsx b/src/app/(protected)/uplifts/_components/uplift-detail-modal.tsx index 904fe58e..4d1429f0 100644 --- a/src/app/(protected)/uplifts/_components/uplift-detail-modal.tsx +++ b/src/app/(protected)/uplifts/_components/uplift-detail-modal.tsx @@ -16,7 +16,10 @@ 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"; @@ -142,6 +145,7 @@ function ApprovedOnWoBreakdown({ breakdown: ExposureBreakdown; unavailable: boolean; }) { + const grantedTotal = isGrantedAmount(breakdown.total) ? breakdown.total : null; return ( - + ); } diff --git a/src/test/app/(protected)/uplifts/uplift-approved-on-wo-dash.test.tsx b/src/test/app/(protected)/uplifts/uplift-approved-on-wo-dash.test.tsx new file mode 100644 index 00000000..9a874e2e --- /dev/null +++ b/src/test/app/(protected)/uplifts/uplift-approved-on-wo-dash.test.tsx @@ -0,0 +1,147 @@ +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> | 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()), + 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) { + 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(); +} + +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 (SH-394)", () => { + 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("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("—"); + }); +});