shoc-frontend-new/src/domain/work-orders/utils/uplift-display-utils.ts
Arthur Bassi 94aa7a1d66 Merge remote-tracking branch 'origin/main' into feat/SH-388-uplift-slide-over
# Conflicts:
#	e2e/__screenshots__/work-orders/work-orders.visual.spec.ts/wo-detail.png
#	src/app/(protected)/workorders/_components/list/table/cells/uplift-cell.tsx
#	src/app/(protected)/workorders/_components/list/table/wo-table-row-service-cells.tsx
#	src/app/(protected)/workorders/_components/uplifts/work-order-uplift-list-item.tsx
#	src/app/(protected)/workorders/_components/uplifts/work-order-uplifts-dialog-content.tsx
#	src/app/(protected)/workorders/_components/uplifts/work-order-uplifts-dialog.tsx
#	src/components/common/address-autocomplete-field.tsx
#	src/domain/work-orders/utils/uplift-display-utils.ts
#	src/test/app/(protected)/workorders/work-order-uplifts-affordances.test.tsx
2026-09-28 14:08:51 -03:00

269 lines
8.5 KiB
TypeScript

import type {
WorkOrderUplift,
UpliftSummary,
WorkOrderUpliftStatus,
} from "@/domain/work-orders/types/work-order-uplift";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
export function formatUpliftMoney(value: number | null | undefined): string {
if (value == null || Number.isNaN(value)) {
return "—";
}
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
}).format(value);
}
const PILL_LABELS: Record<WorkOrderUpliftStatus, string> = {
pending: "Pending approval",
approved: "Approved",
auto_approved: "Auto-approved",
rejected: "Rejected",
cancelled: "Cancelled",
revoked: "Revoked",
};
const CELL_STATUS_WORD: Record<WorkOrderUpliftStatus, string> = {
pending: "Pending",
approved: "Approved",
auto_approved: "Approved",
rejected: "Rejected",
cancelled: "Cancelled",
revoked: "Revoked",
};
export function formatUpliftStatusLabel(status: WorkOrderUpliftStatus): string {
return PILL_LABELS[status];
}
export function getUpliftCellLabel(summary: UpliftSummary): string | null {
if (!summary.hasUplift && summary.pendingCount <= 0) {
return null;
}
if (summary.pendingCount > 0) {
if (summary.amount != null) {
return `${formatUpliftMoney(summary.amount)} Pending`;
}
return summary.pendingCount === 1 ? "1 Pending" : `${summary.pendingCount} Pending`;
}
if (summary.primaryStatus) {
const label = CELL_STATUS_WORD[summary.primaryStatus];
if (summary.amount != null) {
return `${formatUpliftMoney(summary.amount)} ${label}`;
}
return label;
}
if (summary.hasUplift) {
return "Uplift";
}
return null;
}
export function getUpliftStatusPillStyle(status: WorkOrderUpliftStatus): {
background: string;
color: string;
} {
switch (status) {
case "pending":
return { background: "var(--status-pending-bg)", color: "var(--status-pending-text)" };
case "approved":
case "auto_approved":
return { background: "var(--status-completed-bg)", color: "var(--status-completed-text)" };
case "rejected":
return { background: "var(--status-pastdue-bg)", color: "var(--status-pastdue-text)" };
case "cancelled":
case "revoked":
return { background: "var(--status-canceled-bg)", color: "var(--status-canceled-text)" };
default:
return { background: "var(--color-bg-muted)", color: "var(--color-text-muted)" };
}
}
export function isWorkOrderUpliftsReadOnly(status: WorkOrderTableRow["status"]): boolean {
return status === "Completed" || status === "Canceled";
}
export type UpliftAllowanceHelperTone = "muted" | "success" | "warning";
export type UpliftAllowanceHelperIcon = "check" | "alert" | null;
export type UpliftAllowanceHelper = {
text: string;
tone: UpliftAllowanceHelperTone;
tint: boolean;
icon: UpliftAllowanceHelperIcon;
};
export function upliftAllowanceHelper(
amount: number,
remainingAllowance: number,
): UpliftAllowanceHelper {
const typed = Number.isFinite(amount) && amount > 0;
if (!typed) {
if (remainingAllowance > 0) {
return {
text: `${formatUpliftMoney(remainingAllowance)} left to auto-approve`,
tone: "muted",
tint: false,
icon: null,
};
}
return {
text: "Limit used — new uplifts need admin approval",
tone: "warning",
tint: false,
icon: "alert",
};
}
if (amount <= remainingAllowance) {
return {
text: `Approves instantly · ${formatUpliftMoney(remainingAllowance - amount)} left after this`,
tone: "success",
tint: false,
icon: "check",
};
}
return {
text: `Needs admin approval · ${formatUpliftMoney(remainingAllowance)} limit`,
tone: "warning",
tint: true,
icon: "alert",
};
}
export function canRevokeWorkOrderUplift(
uplift: Pick<WorkOrderUplift, "status" | "requestedById">,
currentUserId: string | number | null | undefined,
currentUserIsAdmin: boolean,
): boolean {
if (uplift.status !== "auto_approved") {
// why: SH-214/SH-212 keep admin-approved revoke on Uplift Approvals, not this work order panel.
return false;
}
if (currentUserIsAdmin) {
return false;
}
return (
currentUserId != null &&
currentUserId !== "" &&
uplift.requestedById != null &&
String(uplift.requestedById) === String(currentUserId)
);
}
export function upliftRevokeRequiresReason(status: WorkOrderUpliftStatus): boolean {
return status === "approved";
}
export const UPLIFT_STANDARD_ALLOWANCE_USD = 500;
export const UPLIFT_EMERGENCY_ALLOWANCE_USD = 5000;
export function getUpliftAllowanceCap(workOrderType: string): number {
return workOrderType === "Emergency"
? UPLIFT_EMERGENCY_ALLOWANCE_USD
: UPLIFT_STANDARD_ALLOWANCE_USD;
}
export function hasOpenWorkOrderUplift(
uplifts: ReadonlyArray<Pick<WorkOrderUplift, "status">>,
): boolean {
return uplifts.some((uplift) => uplift.status === "pending");
}
export function getRemainingUpliftAllowance(
workOrderType: string,
uplifts: ReadonlyArray<Pick<WorkOrderUplift, "status" | "amount">>,
): number {
const consumed = uplifts
.filter((uplift) => uplift.status === "auto_approved")
.reduce((sum, uplift) => sum + uplift.amount, 0);
return Math.max(0, getUpliftAllowanceCap(workOrderType) - consumed);
}
function displayActor(name: string): string {
return name.trim() === "" ? "—" : name;
}
export function formatUpliftAbsoluteDate(iso: string): string {
if (iso.trim() === "") return "—";
const date = new Date(iso);
if (Number.isNaN(date.getTime())) return "—";
return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
}
export function formatUpliftRelativeTime(iso: string, nowMs: number): string {
if (iso.trim() === "") return "—";
const timestamp = new Date(iso).getTime();
if (Number.isNaN(timestamp)) return "—";
const minutes = Math.floor((nowMs - timestamp) / 60000);
if (minutes < 1) return "just now";
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
return `${Math.floor(hours / 24)}d ago`;
}
type UpliftMetaSource = Pick<
WorkOrderUplift,
"status" | "requestedAt" | "requestedByName" | "decidedAt" | "decidedByName"
>;
export function formatUpliftCardMeta(uplift: UpliftMetaSource, nowMs: number): string {
const requester = displayActor(uplift.requestedByName);
const decider = displayActor(uplift.decidedByName);
if (uplift.status === "pending") {
return `${requester} · ${formatUpliftRelativeTime(uplift.requestedAt, nowMs)}`;
}
if (uplift.status === "auto_approved") {
return `Within allowance · ${requester} · ${formatUpliftAbsoluteDate(uplift.requestedAt)}`;
}
if (uplift.status === "approved") {
return `Approved by ${decider} · ${formatUpliftAbsoluteDate(uplift.decidedAt)}`;
}
if (uplift.status === "rejected") {
return `Rejected by ${decider} · ${formatUpliftAbsoluteDate(uplift.decidedAt)}`;
}
if (uplift.status === "revoked") {
return `Revoked by ${decider} · ${formatUpliftAbsoluteDate(uplift.decidedAt)}`;
}
const when = uplift.decidedAt.trim() === "" ? uplift.requestedAt : uplift.decidedAt;
return `Cancelled by ${requester} · ${formatUpliftAbsoluteDate(when)}`;
}
export function isVoidUpliftStatus(status: WorkOrderUpliftStatus): boolean {
return status === "rejected" || status === "cancelled" || status === "revoked";
}
export type UpliftCellVisual =
| { kind: "pending"; amount: string | null; statusLabel: "Pending" }
| { kind: "approved"; amount: string | null; statusLabel: "Approved" }
| { kind: "rejected"; amount: string | null; statusLabel: "Rejected" }
| { kind: "plain"; label: string };
export function getUpliftCellVisual(summary: UpliftSummary): UpliftCellVisual | null {
const label = getUpliftCellLabel(summary);
if (label == null) return null;
if (summary.pendingCount > 0) {
return {
kind: "pending",
amount: summary.amount != null ? formatUpliftMoney(summary.amount) : null,
statusLabel: "Pending",
};
}
if (summary.primaryStatus === "approved" || summary.primaryStatus === "auto_approved") {
return {
kind: "approved",
amount: summary.amount != null ? formatUpliftMoney(summary.amount) : null,
statusLabel: "Approved",
};
}
if (summary.primaryStatus === "rejected") {
return {
kind: "rejected",
amount: summary.amount != null ? formatUpliftMoney(summary.amount) : null,
statusLabel: "Rejected",
};
}
return { kind: "plain", label };
}