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 f9b6dd8f..fd83575e 100644
--- a/src/app/v/[token]/dispatch/_components/uplift-request-card.tsx
+++ b/src/app/v/[token]/dispatch/_components/uplift-request-card.tsx
@@ -25,6 +25,21 @@ 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",
+ };
+}
+
+// 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,
@@ -38,16 +53,14 @@ export function UpliftRequestCard({
mediaUsage,
}: UpliftRequestCardProps) {
const [revising, setRevising] = useState(false);
- const canWithdraw = !locked && canAct && WITHDRAWABLE_STATUSES.has(request.status);
- const canRevise = !locked && canAct && request.status === "ChangesRequested";
+ const { canWithdraw, canRevise } = availableActions(request, locked, canAct);
return (
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/domain/vendor-portal/types/vendor-portal.ts b/src/domain/vendor-portal/types/vendor-portal.ts
index ec7da972..2344d703 100644
--- a/src/domain/vendor-portal/types/vendor-portal.ts
+++ b/src/domain/vendor-portal/types/vendor-portal.ts
@@ -76,6 +76,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..c378a21c
--- /dev/null
+++ b/src/test/app/v/uplift-request-card.test.tsx
@@ -0,0 +1,84 @@
+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("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) => {
+ 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();
+ },
+ );
+});