diff --git a/src/app/(protected)/workorders/_components/uplifts/work-order-uplift-list-item.tsx b/src/app/(protected)/workorders/_components/uplifts/work-order-uplift-list-item.tsx index 0578302c..32d4d1fd 100644 --- a/src/app/(protected)/workorders/_components/uplifts/work-order-uplift-list-item.tsx +++ b/src/app/(protected)/workorders/_components/uplifts/work-order-uplift-list-item.tsx @@ -12,6 +12,7 @@ type WorkOrderUpliftListItemProps = { uplift: WorkOrderUplift; readOnly: boolean; currentUserId: string | number | null | undefined; + currentUserIsAdmin: boolean; pendingAction?: boolean; onCancel?: () => void; onRevoke?: () => void; @@ -21,6 +22,7 @@ export function WorkOrderUpliftListItem({ uplift, readOnly, currentUserId, + currentUserIsAdmin, pendingAction = false, onCancel, onRevoke, @@ -28,7 +30,9 @@ export function WorkOrderUpliftListItem({ const pillStyle = getUpliftStatusPillStyle(uplift.status); const showCancel = !readOnly && uplift.status === "pending" && Boolean(onCancel); const showRevoke = - !readOnly && canRevokeWorkOrderUplift(uplift, currentUserId) && Boolean(onRevoke); + !readOnly && + canRevokeWorkOrderUplift(uplift, currentUserId, currentUserIsAdmin) && + Boolean(onRevoke); return (
diff --git a/src/app/(protected)/workorders/_components/uplifts/work-order-uplifts-dialog-content.tsx b/src/app/(protected)/workorders/_components/uplifts/work-order-uplifts-dialog-content.tsx index b0c0c6a0..d8159230 100644 --- a/src/app/(protected)/workorders/_components/uplifts/work-order-uplifts-dialog-content.tsx +++ b/src/app/(protected)/workorders/_components/uplifts/work-order-uplifts-dialog-content.tsx @@ -12,6 +12,7 @@ import { hasOpenWorkOrderUplift } from "@/domain/work-orders/utils/uplift-displa type WorkOrderUpliftsDialogContentProps = { readOnly: boolean; currentUserId: string | number | null | undefined; + currentUserIsAdmin: boolean; readOnlyStatusLabel?: string; isLoading: boolean; error: Error | null; @@ -28,6 +29,7 @@ type WorkOrderUpliftsDialogContentProps = { export function WorkOrderUpliftsDialogContent({ readOnly, currentUserId, + currentUserIsAdmin, readOnlyStatusLabel, isLoading, error, @@ -77,6 +79,7 @@ export function WorkOrderUpliftsDialogContent({ uplift={uplift} readOnly={readOnly} currentUserId={currentUserId} + currentUserIsAdmin={currentUserIsAdmin} pendingAction={actionPending} onCancel={readOnly ? undefined : () => onCancelUplift(uplift.id)} onRevoke={readOnly ? undefined : () => onRevokeUplift(uplift.id)} diff --git a/src/app/(protected)/workorders/_components/uplifts/work-order-uplifts-dialog.tsx b/src/app/(protected)/workorders/_components/uplifts/work-order-uplifts-dialog.tsx index 7e6cbc27..c7a60390 100644 --- a/src/app/(protected)/workorders/_components/uplifts/work-order-uplifts-dialog.tsx +++ b/src/app/(protected)/workorders/_components/uplifts/work-order-uplifts-dialog.tsx @@ -15,6 +15,7 @@ import { isWorkOrderUpliftsReadOnly, upliftRevokeRequiresReason, } from "@/domain/work-orders/utils/uplift-display-utils"; +import { isAdminUser } from "@/lib/auth/user-utils"; import { useAuthContext } from "@/providers/auth-context"; type RevokeTarget = { @@ -78,6 +79,7 @@ export function WorkOrderUpliftsDialog({ row, open, onClose }: WorkOrderUpliftsD , currentUserId: string | number | null | undefined, + currentUserIsAdmin: boolean, ): boolean { if (uplift.status !== "auto_approved") { // SH-214/SH-212: admin-approved revoke lives on Uplift Approvals, not the WO dialog. return false; } + if (currentUserIsAdmin) { + // Admins revoke only admin-approved uplifts; an auto-approval is the requester's to revoke. + return false; + } return ( currentUserId != null && currentUserId !== "" && diff --git a/src/test/app/(protected)/workorders/work-order-uplifts-affordances.test.tsx b/src/test/app/(protected)/workorders/work-order-uplifts-affordances.test.tsx index 1afe8888..624c6ac3 100644 --- a/src/test/app/(protected)/workorders/work-order-uplifts-affordances.test.tsx +++ b/src/test/app/(protected)/workorders/work-order-uplifts-affordances.test.tsx @@ -22,6 +22,12 @@ const pendingUplift: WorkOrderUplift = { const mockState = vi.hoisted(() => ({ uplifts: [] as WorkOrderUplift[], + authUser: { id: "u1", userRoles: "Dispatcher" }, +})); + +vi.mock("@/providers/auth-context", async (importOriginal) => ({ + ...(await importOriginal()), + useAuthContext: () => ({ user: mockState.authUser }), })); const baseRow: WorkOrderTableRow = { @@ -145,6 +151,31 @@ describe("WorkOrderUpliftsDialog affordances", () => { expect(screen.queryByRole("button", { name: /create uplift/i })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: /cancel pending/i })).not.toBeInTheDocument(); }); + + it("shows no Revoke to an admin on the auto-approved uplift they requested", () => { + mockState.authUser = { id: "admin-1", userRoles: "Admin" }; + mockState.uplifts = [{ ...pendingUplift, status: "auto_approved", requestedById: "admin-1" }]; + + renderWithProviders(, { + withAuth: true, + }); + + expect(screen.getByText("Auto")).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: /revoke/i })).not.toBeInTheDocument(); + }); + + it("shows Revoke to the dispatcher on the auto-approved uplift they requested", () => { + mockState.authUser = { id: "dispatcher-1", userRoles: "Dispatcher" }; + mockState.uplifts = [ + { ...pendingUplift, status: "auto_approved", requestedById: "dispatcher-1" }, + ]; + + renderWithProviders(, { + withAuth: true, + }); + + expect(screen.getByRole("button", { name: /revoke/i })).toBeInTheDocument(); + }); }); describe("WorkOrderUpliftListItem revoke affordances", () => { @@ -154,6 +185,7 @@ describe("WorkOrderUpliftListItem revoke affordances", () => { uplift={{ ...pendingUplift, status: "approved" }} readOnly={false} currentUserId="admin-1" + currentUserIsAdmin onRevoke={vi.fn()} />, { withAuth: false }, @@ -168,6 +200,7 @@ describe("WorkOrderUpliftListItem revoke affordances", () => { uplift={{ ...pendingUplift, status: "auto_approved", requestedById: "dispatcher-1" }} readOnly={false} currentUserId="dispatcher-1" + currentUserIsAdmin={false} onRevoke={vi.fn()} />, { withAuth: false }, diff --git a/src/test/domain/work-orders/utils/uplift-display-utils.test.ts b/src/test/domain/work-orders/utils/uplift-display-utils.test.ts index af91feeb..a9c0bc65 100644 --- a/src/test/domain/work-orders/utils/uplift-display-utils.test.ts +++ b/src/test/domain/work-orders/utils/uplift-display-utils.test.ts @@ -12,6 +12,7 @@ describe("canRevokeWorkOrderUplift", () => { canRevokeWorkOrderUplift( { status: "auto_approved", requestedById: "dispatcher-1" }, "dispatcher-1", + false, ), ).toBe(true); }); @@ -21,6 +22,7 @@ describe("canRevokeWorkOrderUplift", () => { canRevokeWorkOrderUplift( { status: "auto_approved", requestedById: "other-dispatcher" }, "dispatcher-1", + false, ), ).toBe(false); }); @@ -30,28 +32,34 @@ describe("canRevokeWorkOrderUplift", () => { canRevokeWorkOrderUplift( { status: "auto_approved", requestedById: "dispatcher-1" }, "admin-user", + true, ), ).toBe(false); }); - it("allows admin to revoke their own auto-approved uplift as the requester", () => { + it("rejects admin revoking an auto-approved uplift even as its requester", () => { expect( canRevokeWorkOrderUplift( { status: "auto_approved", requestedById: "admin-user" }, "admin-user", + true, ), - ).toBe(true); + ).toBe(false); }); it("rejects approved revoke on the Work Order surface (SH-214)", () => { const uplift = { status: "approved" as const, requestedById: "dispatcher-1" }; - expect(canRevokeWorkOrderUplift(uplift, "dispatcher-1")).toBe(false); - expect(canRevokeWorkOrderUplift(uplift, "admin-user")).toBe(false); + expect(canRevokeWorkOrderUplift(uplift, "dispatcher-1", false)).toBe(false); + expect(canRevokeWorkOrderUplift(uplift, "admin-user", true)).toBe(false); }); it("rejects revoke for other statuses", () => { - expect(canRevokeWorkOrderUplift({ status: "pending", requestedById: "u1" }, "u1")).toBe(false); - expect(canRevokeWorkOrderUplift({ status: "rejected", requestedById: "u1" }, "u1")).toBe(false); + expect(canRevokeWorkOrderUplift({ status: "pending", requestedById: "u1" }, "u1", false)).toBe( + false, + ); + expect(canRevokeWorkOrderUplift({ status: "rejected", requestedById: "u1" }, "u1", false)).toBe( + false, + ); }); });