diff --git a/src/app/(protected)/uplifts/_components/format-uplift-dates.ts b/src/app/(protected)/uplifts/_components/format-uplift-dates.ts
new file mode 100644
index 00000000..0e03fcc3
--- /dev/null
+++ b/src/app/(protected)/uplifts/_components/format-uplift-dates.ts
@@ -0,0 +1,27 @@
+const DATE_FORMAT: Intl.DateTimeFormatOptions = {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+};
+
+/** Formats an API UTC instant as local "Jan 15, 2026 · 10:00 AM"; "" when absent or invalid. */
+export function formatUpliftDateTime(value: string): string {
+ if (!value) return "";
+ const hasZone = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(value);
+ const date = new Date(hasZone ? value : `${value}Z`);
+ if (Number.isNaN(date.getTime())) return "";
+ const time = date.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
+ return `${date.toLocaleDateString("en-US", DATE_FORMAT)} · ${time}`;
+}
+
+/**
+ * Formats a work-order schedule as "Apr 10, 2026". The schedule is a calendar day,
+ * so only its date part is read and it never shifts with the viewer's time zone.
+ */
+export function formatUpliftCalendarDate(value: string): string {
+ const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(value);
+ if (match == null) return "";
+ const [, year, month, day] = match;
+ const date = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
+ return date.toLocaleDateString("en-US", { ...DATE_FORMAT, timeZone: "UTC" });
+}
diff --git a/src/app/(protected)/uplifts/_components/open-uplift-evidence.ts b/src/app/(protected)/uplifts/_components/open-uplift-evidence.ts
index 2654fedd..263d40e0 100644
--- a/src/app/(protected)/uplifts/_components/open-uplift-evidence.ts
+++ b/src/app/(protected)/uplifts/_components/open-uplift-evidence.ts
@@ -2,12 +2,27 @@ import { toast } from "react-toastify";
import type { UpliftQueueItem } from "@/domain/uplifts/types/uplift";
import { upliftsApi } from "@/domain/uplifts/api/uplifts-api";
+function reportEvidenceError(error: unknown): void {
+ toast.error(error instanceof Error ? error.message : "Unable to download evidence right now.");
+}
+
export function openUpliftEvidence(row: UpliftQueueItem): void {
void upliftsApi
.downloadEvidence(row.id, row.evidenceFileName || "uplift-evidence")
+ .catch(reportEvidenceError);
+}
+
+export function openUpliftEvidenceInNewTab(row: UpliftQueueItem): void {
+ // Open the tab inside the click handler; a tab opened after the fetch resolves
+ // is treated as an unsolicited popup and blocked.
+ const tab = window.open("about:blank", "_blank");
+ if (tab != null) {
+ tab.opener = null;
+ }
+ void upliftsApi
+ .openEvidence(row.id, tab, row.evidenceFileName || "uplift-evidence")
.catch((error: unknown) => {
- toast.error(
- error instanceof Error ? error.message : "Unable to download evidence right now.",
- );
+ tab?.close();
+ reportEvidenceError(error);
});
}
diff --git a/src/app/(protected)/uplifts/_components/uplift-detail-modal.tsx b/src/app/(protected)/uplifts/_components/uplift-detail-modal.tsx
index 55422e2e..904fe58e 100644
--- a/src/app/(protected)/uplifts/_components/uplift-detail-modal.tsx
+++ b/src/app/(protected)/uplifts/_components/uplift-detail-modal.tsx
@@ -1,4 +1,5 @@
import { useMemo, type ReactNode } from "react";
+import CloseIcon from "@mui/icons-material/Close";
import {
Box,
Button,
@@ -7,21 +8,30 @@ import {
DialogActions,
DialogContent,
DialogTitle,
+ Divider,
+ IconButton,
Tooltip,
} from "@mui/material";
+import {
+ formatUpliftCalendarDate,
+ formatUpliftDateTime,
+} from "@/app/(protected)/uplifts/_components/format-uplift-dates";
import { formatUpliftMoney } from "@/app/(protected)/uplifts/_components/format-uplift-money";
import { Text } from "@/components/ui/text";
-import { useWorkOrderBoardDetail } from "@/domain/work-orders/use-cases/use-work-order-detail";
import { useWorkOrderUplifts } from "@/domain/work-orders/use-cases/use-work-order-uplifts";
import { useUpliftsCanApprove } from "@/domain/uplifts/use-cases/use-uplifts-can-approve";
-import { isWorkOrderUpliftsReadOnly } from "@/domain/work-orders/utils/uplift-display-utils";
-import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import type { UpliftQueueItem } from "@/domain/uplifts/types/uplift";
-import { getWaitTimeTextClass, timeSince, waitTimeColor } from "@/lib/time-utils";
const CLOSED_WO_TOOLTIP = "This work order is closed. Uplifts can no longer be revoked.";
const ADMIN_ONLY_REVOKE_TOOLTIP = "Only admins can revoke uplifts";
const UNAVAILABLE_LABEL = "Unavailable";
+const BORDERED_BLOCK_SX = {
+ border: 1,
+ borderColor: "divider",
+ borderRadius: 1,
+ px: 1.75,
+ py: 1.25,
+} as const;
type ModalCallbacks = {
onClose: () => void;
@@ -31,24 +41,122 @@ type ModalCallbacks = {
onOpenAttachment: (row: UpliftQueueItem) => void;
};
+type ExposureBreakdown = { auto: number | null; admin: number | null; total: number | null };
+
function DetailField({ label, children }: { label: string; children: ReactNode }) {
return (
{label}
- {children}
+ {children}
);
}
-function DetailSection({ title, children }: { title: string; children: ReactNode }) {
+function SubsectionTitle({ children }: { children: ReactNode }) {
return (
-
-
- {title}
+
+ {children}
+
+ );
+}
+
+function vendorTechnicianLabel(item: UpliftQueueItem): string {
+ const parts = [item.vendorCompanyName.trim(), item.technicianName.trim()].filter(Boolean);
+ const unique = parts.filter((part, index) => parts.indexOf(part) === index);
+ return unique.length > 0 ? unique.join(" · ") : "Not assigned";
+}
+
+function WorkOrderSection({ item }: { item: UpliftQueueItem }) {
+ return (
+
+
+ Work order
- {children}
+
+ {item.serviceName || "Not specified"}
+ {vendorTechnicianLabel(item)}
+
+ {item.workOrderDispatcherName || "Unassigned"}
+
+
+ {formatUpliftCalendarDate(item.workOrderScheduledDate) || "Unscheduled"}
+
+
+
+ );
+}
+
+function JustificationBlock({ notes }: { notes: string }) {
+ const text = notes.trim();
+ return (
+
+ {text.length > 0 && {text}}
+ {text.length === 0 && (
+
+ No justification provided.
+
+ )}
+
+ );
+}
+
+function BreakdownRow({
+ label,
+ value,
+ strong,
+ unavailable,
+}: {
+ label: string;
+ value: number | null;
+ strong: boolean;
+ unavailable: boolean;
+}) {
+ const fallback = unavailable ? UNAVAILABLE_LABEL : "—";
+ return (
+
+
+ {label}
+
+
+ {value != null ? formatUpliftMoney(value) : fallback}
+
+
+ );
+}
+
+function ApprovedOnWoBreakdown({
+ breakdown,
+ unavailable,
+}: {
+ breakdown: ExposureBreakdown;
+ unavailable: boolean;
+}) {
+ return (
+
+
+
+
);
}
@@ -67,142 +175,61 @@ function UpliftAttachments({
);
}
+ const canOpen = item.evidenceDocumentId != null;
const extraCount =
typeof item.attachmentCount === "number" && item.attachmentCount > 1
? item.attachmentCount - 1
: 0;
return (
-
+
onOpenAttachment(item) : undefined}
- clickable={item.evidenceDocumentId != null}
+ title={canOpen ? "Opens in a new tab" : undefined}
+ onClick={canOpen ? () => onOpenAttachment(item) : undefined}
+ clickable={canOpen}
+ sx={{ flexShrink: 0, maxWidth: 176 }}
/>
- {extraCount > 0 && }
+ {extraCount > 0 && (
+
+ )}
);
}
-function WorkOrderSection({
- item,
- info,
- unavailable,
-}: {
- item: UpliftQueueItem;
- info?: WorkOrderTableRow;
- unavailable: boolean;
-}) {
- const statusFallback = unavailable ? UNAVAILABLE_LABEL : "—";
- return (
-
-
- {item.woNumber || "—"}
-
-
- {info?.site || item.site || "—"}
-
-
- {info?.status || statusFallback}
-
-
-
- {info?.scheduledOn || (unavailable ? UNAVAILABLE_LABEL : "Unscheduled")}
-
-
-
- {item.vendorCompanyName || "—"}
-
-
- );
-}
-
-function RequestSection({
+function UpliftRequestSection({
item,
+ breakdown,
+ breakdownUnavailable,
onOpenAttachment,
}: {
item: UpliftQueueItem;
+ breakdown: ExposureBreakdown;
+ breakdownUnavailable: boolean;
onOpenAttachment: (row: UpliftQueueItem) => void;
}) {
- const waitingClass = item.requestedAt
- ? getWaitTimeTextClass(waitTimeColor(item.requestedAt))
- : undefined;
return (
-
-
-
- {formatUpliftMoney(item.currentNTE ?? 0)} → {formatUpliftMoney(item.requestedNTE)}
-
-
-
- {item.requestedByVendorName || "—"}
-
-
-
- {item.requestedAt ? timeSince(item.requestedAt) : "—"}
-
-
-
- {item.vendorReason || "No justification provided."}
-
-
-
-
-
- );
-}
-
-function ApprovedOnWoBreakdown({
- auto,
- admin,
- total,
- unavailable,
-}: {
- auto: number | null;
- admin: number | null;
- total: number | null;
- unavailable: boolean;
-}) {
- const rows: Array<{ label: string; value: number | null; strong: boolean }> = [
- { label: "Auto-approved", value: auto, strong: false },
- { label: "Admin-approved", value: admin, strong: false },
- { label: "Total", value: total, strong: true },
- ];
- return (
-
- {rows.map((row) => (
-
- ))}
-
- );
-}
-
-function BreakdownRow({
- label,
- value,
- strong,
- unavailable,
-}: {
- label: string;
- value: number | null;
- strong: boolean;
- unavailable: boolean;
-}) {
- return (
-
-
- {label}
-
-
- {value != null ? formatUpliftMoney(value) : unavailable ? UNAVAILABLE_LABEL : "—"}
+
+
+ Uplift request
+
+
+ {item.requestedByVendorName || "Unknown requester"}
+
+
+ {formatUpliftDateTime(item.requestedAt) || "Not recorded"}
+
+
+ Justification
+
+ Approved on WO
+
+ Attachments
+
);
}
@@ -223,6 +250,19 @@ function PendingModalActions({
const tooltip = canDecide ? "" : `Requires Tier ${item.requiredTier} role`;
return (
<>
+
+
+
+
+
-
-
-
-
-
>
);
}
-function UpliftModalActions({
+function RevokeModalAction({
item,
- closed,
canRevoke,
- approvePending,
revokePending,
- onApprove,
- onReject,
onRevoke,
}: {
item: UpliftQueueItem;
- closed: boolean;
canRevoke: boolean;
- approvePending: boolean;
revokePending: boolean;
- onApprove: (row: UpliftQueueItem) => void;
- onReject: (row: UpliftQueueItem) => void;
onRevoke: (row: UpliftQueueItem) => void;
}) {
+ const closed = item.workOrderClosed === true;
const revokeTooltip = !canRevoke ? ADMIN_ONLY_REVOKE_TOOLTIP : closed ? CLOSED_WO_TOOLTIP : "";
return (
- <>
- {item.status === "Pending" && (
-
- )}
- {item.status === "Approved" && (
-
-
-
-
-
- )}
- >
+
+
+
+
+
);
}
-export function UpliftDetailModal({
+function useExposureBreakdown(item: UpliftQueueItem) {
+ // Permission failures (e.g. 403 for account-scoped users) are handled quietly:
+ // the breakdown falls back to "Unavailable" instead of toasting.
+ const woUpliftsQuery = useWorkOrderUplifts(item.workOrderId ?? null, {
+ suppressErrorToast: true,
+ });
+ const breakdown = useMemo(() => {
+ const woUplifts = woUpliftsQuery.data ?? [];
+ const autoFallback = sumUpliftAmounts(woUplifts, "auto_approved");
+ const adminFallback = sumUpliftAmounts(woUplifts, "approved");
+ const hasWoData = woUplifts.length > 0;
+ return {
+ auto: item.approvedOnWoAuto ?? (hasWoData ? autoFallback : null),
+ admin: item.approvedOnWoAdmin ?? (hasWoData ? adminFallback : null),
+ total: item.approvedOnWoTotal ?? (hasWoData ? autoFallback + adminFallback : null),
+ };
+ }, [item.approvedOnWoAuto, item.approvedOnWoAdmin, item.approvedOnWoTotal, woUpliftsQuery.data]);
+ return { breakdown, unavailable: woUpliftsQuery.isError };
+}
+
+function UpliftDetailContent({
item,
canRevoke,
approvePending,
@@ -312,98 +340,102 @@ export function UpliftDetailModal({
onReject,
onRevoke,
onOpenAttachment,
+}: {
+ item: UpliftQueueItem;
+ canRevoke: boolean;
+ approvePending: boolean;
+ revokePending: boolean;
+} & ModalCallbacks) {
+ const { breakdown, unavailable } = useExposureBreakdown(item);
+ const isPending = item.status === "Pending";
+ const isApproved = item.status === "Approved";
+ const subtitle = [item.site, item.status].filter(Boolean).join(" · ");
+ return (
+ <>
+
+
+ Uplift — WO #{item.woNumber || item.dispatchNumber || "—"}
+
+ 0}>
+ {subtitle}
+
+
+
+
+
+
+
+
+ Amount
+
+
+ {formatUpliftMoney(item.requestedNTE)}
+
+
+
+
+
+
+ {(isPending || isApproved) && (
+
+ {isPending && (
+
+ )}
+ {isApproved && (
+
+ )}
+
+ )}
+ >
+ );
+}
+
+export function UpliftDetailModal({
+ item,
+ ...rest
}: {
item: UpliftQueueItem | null;
canRevoke: boolean;
approvePending: boolean;
revokePending: boolean;
} & ModalCallbacks) {
- const open = item != null;
- // Permission failures (e.g. 403 for account-scoped users) are handled quietly
- // in this modal: dependent sections fall back to "Unavailable" instead of toasting.
- const quietMeta = { suppressErrorToast: true } as const;
- const boardDetail = useWorkOrderBoardDetail(
- item != null && item.workOrderId != null ? item.workOrderId : undefined,
- true,
- quietMeta,
- );
- const woUpliftsQuery = useWorkOrderUplifts(
- item != null && item.workOrderId != null ? item.workOrderId : null,
- quietMeta,
- );
-
- const info = boardDetail.data?.info;
- const closed = useMemo(() => {
- if (item?.workOrderClosed === true) return true;
- return info ? isWorkOrderUpliftsReadOnly(info.status) : false;
- }, [item?.workOrderClosed, info]);
-
- const breakdown = useMemo(() => {
- const woUplifts = woUpliftsQuery.data ?? [];
- const autoFallback = sumUpliftAmounts(woUplifts, "auto_approved");
- const adminFallback = sumUpliftAmounts(woUplifts, "approved");
- const hasWoData = woUplifts.length > 0;
- return {
- auto: item?.approvedOnWoAuto ?? (hasWoData ? autoFallback : null),
- admin: item?.approvedOnWoAdmin ?? (hasWoData ? adminFallback : null),
- total: item?.approvedOnWoTotal ?? (hasWoData ? autoFallback + adminFallback : null),
- };
- }, [
- item?.approvedOnWoAuto,
- item?.approvedOnWoAdmin,
- item?.approvedOnWoTotal,
- woUpliftsQuery.data,
- ]);
-
- if (!open) {
+ if (item == null) {
return null;
}
-
return (
-