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 ( + + + {rejectTitle(amount)} + + + {Boolean(woNumber) && ( + + The dispatcher sees this reason on WO #{woNumber}. + + )} + + + + + + + + ); +} 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 ( + + + Revoke this approval? + + + + The approved uplift of {amountLabel} + {woSuffix} will be withdrawn. This does not recover money already spent — it records that + the authorization was a mistake. + + + + + + + + + ); +} 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} + +