From 70f8b303b5a6a338dded613c747a74987de97ec6 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Fri, 25 Sep 2026 02:55:04 -0300 Subject: [PATCH 1/3] Show work-order uplift requests read-only in the Vendor Portal Revise and Withdraw now appear only on uplift requests the vendor raised. Requests raised from the work order stay visible without either control. --- .../_components/uplift-request-card.tsx | 6 +- .../vendor-portal/types/vendor-portal.ts | 2 + src/test/app/v/uplift-request-card.test.tsx | 59 +++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 src/test/app/v/uplift-request-card.test.tsx diff --git a/src/app/v/[token]/dispatch/_components/uplift-request-card.tsx b/src/app/v/[token]/dispatch/_components/uplift-request-card.tsx index c9f4845e..511cfa08 100644 --- a/src/app/v/[token]/dispatch/_components/uplift-request-card.tsx +++ b/src/app/v/[token]/dispatch/_components/uplift-request-card.tsx @@ -35,8 +35,10 @@ export function UpliftRequestCard({ onCancelRevise, }: UpliftRequestCardProps) { const [revising, setRevising] = useState(false); - const canWithdraw = !locked && canAct && WITHDRAWABLE_STATUSES.has(request.status); - const canRevise = !locked && canAct && request.status === "ChangesRequested"; + // Requests raised from the work order are visible but read-only to the vendor. + const ownRequest = request.raisedByVendor !== false; + const canWithdraw = !locked && canAct && ownRequest && WITHDRAWABLE_STATUSES.has(request.status); + const canRevise = !locked && canAct && ownRequest && request.status === "ChangesRequested"; return (
diff --git a/src/domain/vendor-portal/types/vendor-portal.ts b/src/domain/vendor-portal/types/vendor-portal.ts index d9853b07..d5c1cada 100644 --- a/src/domain/vendor-portal/types/vendor-portal.ts +++ b/src/domain/vendor-portal/types/vendor-portal.ts @@ -66,6 +66,8 @@ export interface VendorPortalUpliftRequest { requestedNTE: number; currentNTE?: number; vendorReason?: string; + /** False when the request was raised from the work order; the vendor cannot revise or withdraw it. */ + raisedByVendor?: boolean; requestedAt?: string; decidedAt?: string; decidedByName?: string; diff --git a/src/test/app/v/uplift-request-card.test.tsx b/src/test/app/v/uplift-request-card.test.tsx new file mode 100644 index 00000000..e64693ef --- /dev/null +++ b/src/test/app/v/uplift-request-card.test.tsx @@ -0,0 +1,59 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { UpliftRequestCard } from "@/app/v/[token]/dispatch/_components/uplift-request-card"; +import type { VendorPortalUpliftRequest } from "@/domain/vendor-portal/types/vendor-portal"; + +function request(overrides: Partial = {}): VendorPortalUpliftRequest { + return { + id: 7, + status: "Pending", + requestedNTE: 1500, + currentNTE: 1000, + vendorReason: "Extra parts", + raisedByVendor: true, + ...overrides, + }; +} + +function renderCard(req: VendorPortalUpliftRequest) { + return render( + , + ); +} + +describe("UpliftRequestCard actions", () => { + it("offers Withdraw on a pending request the vendor raised", () => { + renderCard(request({ status: "Pending" })); + + expect(screen.getByRole("button", { name: /withdraw request/i })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: /revise/i })).not.toBeInTheDocument(); + }); + + it("offers Withdraw and Revise when changes are requested on a request the vendor raised", () => { + renderCard(request({ status: "ChangesRequested" })); + + expect(screen.getByRole("button", { name: /withdraw request/i })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /revise/i })).toBeInTheDocument(); + }); + + it.each(["Pending", "ChangesRequested"])( + "shows a %s request raised from the work order read-only", + (status) => { + renderCard(request({ status, raisedByVendor: false, requestedNTE: 90, currentNTE: 600 })); + + expect(screen.getByText(status)).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: /withdraw request/i })).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: /revise/i })).not.toBeInTheDocument(); + }, + ); +}); From 33f71725988fe1786905315b68379bf100a69cfb Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Fri, 25 Sep 2026 03:03:30 -0300 Subject: [PATCH 2/3] Move uplift card action gating into a helper Keeps UpliftRequestCard within the changed-file complexity limit. --- .../dispatch/_components/uplift-request-card.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/app/v/[token]/dispatch/_components/uplift-request-card.tsx b/src/app/v/[token]/dispatch/_components/uplift-request-card.tsx index 511cfa08..053ca8d1 100644 --- a/src/app/v/[token]/dispatch/_components/uplift-request-card.tsx +++ b/src/app/v/[token]/dispatch/_components/uplift-request-card.tsx @@ -23,6 +23,15 @@ function statusClassName(status: string): string { return `vp-uplift-status-${String(status).toLowerCase()}`; } +function availableActions(request: VendorPortalUpliftRequest, locked: boolean, canAct: boolean) { + // Requests raised from the work order are visible but read-only to the vendor. + const actionable = !locked && canAct && request.raisedByVendor !== false; + return { + canWithdraw: actionable && WITHDRAWABLE_STATUSES.has(request.status), + canRevise: actionable && request.status === "ChangesRequested", + }; +} + export function UpliftRequestCard({ request, locked, @@ -35,10 +44,7 @@ export function UpliftRequestCard({ onCancelRevise, }: UpliftRequestCardProps) { const [revising, setRevising] = useState(false); - // Requests raised from the work order are visible but read-only to the vendor. - const ownRequest = request.raisedByVendor !== false; - const canWithdraw = !locked && canAct && ownRequest && WITHDRAWABLE_STATUSES.has(request.status); - const canRevise = !locked && canAct && ownRequest && request.status === "ChangesRequested"; + const { canWithdraw, canRevise } = availableActions(request, locked, canAct); return (
From 43dd95e7fd88e116e575a3c51b4e443f13abe18e Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Fri, 25 Sep 2026 03:12:17 -0300 Subject: [PATCH 3/3] Show the requested NTE total on work-order uplift cards A request raised from the work order stores the increase, so the vendor card read a +90 request on a 600 NTE as "Requested $90.00 (was $600.00)". The card now shows the new total, $690.00, for those requests; vendor requests are unchanged. --- .../_components/uplift-request-card.tsx | 9 +++++-- src/test/app/v/uplift-request-card.test.tsx | 25 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/app/v/[token]/dispatch/_components/uplift-request-card.tsx b/src/app/v/[token]/dispatch/_components/uplift-request-card.tsx index 053ca8d1..2d316578 100644 --- a/src/app/v/[token]/dispatch/_components/uplift-request-card.tsx +++ b/src/app/v/[token]/dispatch/_components/uplift-request-card.tsx @@ -32,6 +32,12 @@ function availableActions(request: VendorPortalUpliftRequest, locked: boolean, c }; } +// A vendor request stores the requested new NTE total; a work-order request stores the increase. +function requestedTotal(request: VendorPortalUpliftRequest): number { + const requested = Number(request.requestedNTE); + return request.raisedByVendor === false ? Number(request.currentNTE ?? 0) + requested : requested; +} + export function UpliftRequestCard({ request, locked, @@ -51,8 +57,7 @@ export function UpliftRequestCard({
Requested - ${Number(request.requestedNTE).toFixed(2)} (was $ - {Number(request.currentNTE ?? 0).toFixed(2)}) + ${requestedTotal(request).toFixed(2)} (was ${Number(request.currentNTE ?? 0).toFixed(2)})
diff --git a/src/test/app/v/uplift-request-card.test.tsx b/src/test/app/v/uplift-request-card.test.tsx index e64693ef..c378a21c 100644 --- a/src/test/app/v/uplift-request-card.test.tsx +++ b/src/test/app/v/uplift-request-card.test.tsx @@ -46,6 +46,31 @@ describe("UpliftRequestCard actions", () => { expect(screen.getByRole("button", { name: /revise/i })).toBeInTheDocument(); }); + it("shows a work-order increase as the new NTE total", () => { + renderCard(request({ raisedByVendor: false, requestedNTE: 90, currentNTE: 600 })); + + expect(screen.getByText("Requested").nextElementSibling).toHaveTextContent( + "$690.00 (was $600.00)", + ); + }); + + it("shows a vendor request's requested total unchanged", () => { + renderCard(request({ raisedByVendor: true, requestedNTE: 900, currentNTE: 600 })); + + expect(screen.getByText("Requested").nextElementSibling).toHaveTextContent( + "$900.00 (was $600.00)", + ); + }); + + it("treats a request without the raised-by flag as vendor-raised", () => { + renderCard(request({ raisedByVendor: undefined, requestedNTE: 900, currentNTE: 600 })); + + expect(screen.getByText("Requested").nextElementSibling).toHaveTextContent( + "$900.00 (was $600.00)", + ); + expect(screen.getByRole("button", { name: /withdraw request/i })).toBeInTheDocument(); + }); + it.each(["Pending", "ChangesRequested"])( "shows a %s request raised from the work order read-only", (status) => {