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,
+ );
});
});