diff --git a/src/app/(protected)/uplifts/_components/format-uplift-money.ts b/src/app/(protected)/uplifts/_components/format-uplift-money.ts
new file mode 100644
index 00000000..4f269493
--- /dev/null
+++ b/src/app/(protected)/uplifts/_components/format-uplift-money.ts
@@ -0,0 +1,3 @@
+export function formatUpliftMoney(value: number): string {
+ return `$${value.toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
+}
diff --git a/src/app/(protected)/uplifts/_components/reject-dialog.tsx b/src/app/(protected)/uplifts/_components/reject-dialog.tsx
new file mode 100644
index 00000000..016da452
--- /dev/null
+++ b/src/app/(protected)/uplifts/_components/reject-dialog.tsx
@@ -0,0 +1,100 @@
+import { useEffect, useState } from "react";
+import { Dialog, DialogActions, DialogContent, DialogTitle } from "@mui/material";
+import { formatUpliftMoney } from "@/app/(protected)/uplifts/_components/format-uplift-money";
+import { UpliftReasonField } from "@/app/(protected)/uplifts/_components/uplift-reason-field";
+import { Button } from "@/components/ui/button";
+import { DialogCancelButton } from "@/components/ui/dialog-cancel-button";
+import { dialogBlurBackdropSlotProps } from "@/components/ui/dialog-blur-backdrop";
+import { Text } from "@/components/ui/text";
+import { guardDialogClose } from "@/lib/dialog-guard";
+
+export type RejectDialogProps = {
+ open: boolean;
+ amount?: number | null;
+ woNumber?: string | null;
+ onClose: () => void;
+ onConfirm: (reason: string) => void;
+ isPending?: boolean;
+};
+
+function rejectTitle(amount: number | null | undefined): string {
+ if (amount != null && Number.isFinite(amount)) {
+ return `Reject uplift of ${formatUpliftMoney(amount)}?`;
+ }
+ return "Reject uplift?";
+}
+
+export function RejectDialog({
+ open,
+ amount,
+ woNumber,
+ onClose,
+ onConfirm,
+ isPending = false,
+}: RejectDialogProps) {
+ const [reason, setReason] = useState("");
+ const canConfirm = reason.trim().length > 0 && !isPending;
+
+ useEffect(() => {
+ if (open) {
+ setReason("");
+ }
+ }, [open]);
+
+ const handleClose = () => {
+ if (isPending) return;
+ setReason("");
+ onClose();
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/(protected)/uplifts/_components/revoke-dialog.tsx b/src/app/(protected)/uplifts/_components/revoke-dialog.tsx
new file mode 100644
index 00000000..38bf8024
--- /dev/null
+++ b/src/app/(protected)/uplifts/_components/revoke-dialog.tsx
@@ -0,0 +1,110 @@
+import { useEffect, useState } from "react";
+import { Dialog, DialogActions, DialogContent, DialogTitle } from "@mui/material";
+import { formatUpliftMoney } from "@/app/(protected)/uplifts/_components/format-uplift-money";
+import { UpliftReasonField } from "@/app/(protected)/uplifts/_components/uplift-reason-field";
+import { Button } from "@/components/ui/button";
+import { DialogCancelButton } from "@/components/ui/dialog-cancel-button";
+import { dialogBlurBackdropSlotProps } from "@/components/ui/dialog-blur-backdrop";
+import { Text } from "@/components/ui/text";
+import { guardDialogClose } from "@/lib/dialog-guard";
+
+export type RevokeDialogProps = {
+ open: boolean;
+ amount?: number | null;
+ woNumber?: string | null;
+ onClose: () => void;
+ onConfirm: (reason: string) => void;
+ isPending?: boolean;
+};
+
+function revokeAmountLabel(amount: number | null | undefined): string {
+ if (amount != null && Number.isFinite(amount)) {
+ return formatUpliftMoney(amount);
+ }
+ return "this uplift";
+}
+
+function revokeConfirmLabel(amount: number | null | undefined): string {
+ if (amount != null && Number.isFinite(amount)) {
+ return `Revoke ${formatUpliftMoney(amount)}`;
+ }
+ return "Revoke";
+}
+
+export function RevokeDialog({
+ open,
+ amount,
+ woNumber,
+ onClose,
+ onConfirm,
+ isPending = false,
+}: RevokeDialogProps) {
+ const [reason, setReason] = useState("");
+ const canConfirm = reason.trim().length > 0 && !isPending;
+
+ useEffect(() => {
+ if (open) {
+ setReason("");
+ }
+ }, [open]);
+
+ const handleClose = () => {
+ if (isPending) return;
+ setReason("");
+ onClose();
+ };
+
+ const amountLabel = revokeAmountLabel(amount);
+ const woSuffix = woNumber ? ` on WO ${woNumber}` : "";
+
+ return (
+
+ );
+}
diff --git a/src/app/(protected)/uplifts/_components/uplift-reason-field.tsx b/src/app/(protected)/uplifts/_components/uplift-reason-field.tsx
new file mode 100644
index 00000000..37e3899d
--- /dev/null
+++ b/src/app/(protected)/uplifts/_components/uplift-reason-field.tsx
@@ -0,0 +1,44 @@
+import {
+ DLG_INPUT_CLS,
+ DLG_INPUT_STYLE,
+} from "@/app/(protected)/workorders/_components/wizard/wizard-input-styles";
+import { FieldLabel } from "@/app/(protected)/workorders/_components/list/table/cells/dialog-field-label";
+
+type UpliftReasonFieldProps = {
+ id: string;
+ label: string;
+ value: string;
+ placeholder: string;
+ required?: boolean;
+ disabled?: boolean;
+ onChange: (value: string) => void;
+};
+
+export function UpliftReasonField({
+ id,
+ label,
+ value,
+ placeholder,
+ required = false,
+ disabled = false,
+ onChange,
+}: UpliftReasonFieldProps) {
+ return (
+
+
+ {label}
+
+
+ );
+}
diff --git a/src/app/(protected)/uplifts/index.tsx b/src/app/(protected)/uplifts/index.tsx
index 8648831f..382214d2 100644
--- a/src/app/(protected)/uplifts/index.tsx
+++ b/src/app/(protected)/uplifts/index.tsx
@@ -38,6 +38,7 @@ import {
import { useUpliftsQueue } from "@/domain/uplifts/use-cases/use-uplifts-queue";
import { formatDateTime } from "@/lib/time-utils";
import { toast } from "react-toastify";
+import { RejectDialog } from "@/app/(protected)/uplifts/_components/reject-dialog";
const STATUS_OPTIONS = ["", ...UPLIFT_STATUSES];
@@ -297,9 +298,7 @@ function UpliftDecisionDialog({
{action?.type === "approve"
? `Approve uplift to ${formatMoney(action.item.requestedNTE)}?`
- : action?.type === "reject"
- ? "Reject Uplift"
- : "Request Changes"}
+ : "Request Changes"}
{action?.type === "approve" && (
@@ -354,9 +353,9 @@ export default function UpliftQueuePage() {
const rows = data?.items ?? [];
const totalCount = data?.totalCount ?? 0;
- const noteRequired = upliftAction?.type === "reject" || upliftAction?.type === "request-changes";
- const decisionPending =
- approveUplift.isPending || rejectUplift.isPending || requestChangesUplift.isPending;
+ const genericAction = upliftAction?.type === "reject" ? null : upliftAction;
+ const noteRequired = genericAction?.type === "request-changes";
+ const decisionPending = approveUplift.isPending || requestChangesUplift.isPending;
const handleStatusChange = (value: string) => {
setStatus(value);
@@ -382,13 +381,26 @@ export default function UpliftQueuePage() {
};
if (upliftAction.type === "approve") {
approveUplift.mutate({ id, note: note || undefined }, { onSuccess: onDone });
- } else if (upliftAction.type === "reject") {
- rejectUplift.mutate({ id, note: note.trim() }, { onSuccess: onDone });
- } else {
+ return;
+ }
+ if (upliftAction.type === "request-changes") {
requestChangesUplift.mutate({ id, note: note.trim() }, { onSuccess: onDone });
}
};
+ const handleRejectConfirm = (reason: string) => {
+ if (upliftAction?.type !== "reject") return;
+ rejectUplift.mutate(
+ { id: upliftAction.item.id, note: reason },
+ {
+ onSuccess: () => {
+ closeDialog();
+ void refetch();
+ },
+ },
+ );
+ };
+
const downloadEvidence = async (item: UpliftQueueItem) => {
try {
await upliftsApi.downloadEvidence(item.id, item.evidenceFileName || "uplift-evidence");
@@ -440,8 +452,15 @@ export default function UpliftQueuePage() {
rowsPerPage={pageSize}
rowsPerPageOptions={[25]}
/>
+
+
+ {noteRequired ? "Note" : "Optional note"}
+
onNoteChange(e.target.value)}
/>
-
+