import { useState } from "react"; import { Badge, Button, Card, Collapse, Group, Stack, Text, Textarea, Tooltip, } from "@mantine/core"; import type { UseMutationResult } from "@tanstack/react-query"; import { ArrowRight, CheckCircle2, Clock, XCircle } from "lucide-react"; import type { RateChangeRequest } from "@/services/ruleEngine/ruleEngine.service"; /** Field labels for the diff — anything not listed falls back to the raw key. */ const FIELD_LABELS: Record = { rateValue: "Rate", currency: "Currency", rateUnit: "Unit", appliesTo: "Applies to", trigger: "Trigger", tradeDirection: "Direction", containerTypeId: "Container type", cargoTypeId: "Cargo type", originYardId: "Origin yard", destinationYardId: "Destination yard", }; const fmtDateTime = (iso: string) => new Date(iso).toLocaleString("en-GB", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit", hour12: false, }); const fmtValue = ( field: string, value: unknown, labels?: Record, ): string => { if (value === null || value === undefined || value === "") return "—"; if (field === "rateValue") { const num = Number(value); return Number.isNaN(num) ? String(value) : num.toLocaleString(); } // Yard ids are unreadable — an approver decides on the route, not a UUID. if (field === "originYardId" || field === "destinationYardId") { return labels?.[String(value)] ?? String(value); } return String(value).replace(/_/g, " "); }; /** "Ocean freight · 40HC" — what rate this change targets. */ const rateSummary = (r: RateChangeRequest): string => { const rate = (r.rate ?? {}) as Record; const parts = [ rate.rateType ? String(rate.rateType).replace(/_/g, " ") : null, rate.appliesTo ? String(rate.appliesTo) : null, rate.trigger && rate.trigger !== "ALWAYS" ? String(rate.trigger) : null, ].filter(Boolean); return parts.join(" · ") || "Rate"; }; /** The headline change, so the queue is scannable without expanding: "100 → 200 USD". */ const headline = (r: RateChangeRequest): string | null => { if (!("rateValue" in r.payload)) return null; const currency = String(r.payload.currency ?? r.previousValues.currency ?? (r.rate as Record | undefined)?.currency ?? ""); const before = fmtValue("rateValue", r.previousValues.rateValue); const after = fmtValue("rateValue", r.payload.rateValue); return `${before} → ${after}${currency ? ` ${currency}` : ""}`; }; type Decide = UseMutationResult< RateChangeRequest, unknown, { id: string; decisionNote?: string } >; interface RateApprovalsSectionProps { requests: RateChangeRequest[]; /** Whether this user holds the rates approve permission. */ canDecide: boolean; approve: Decide; reject: Decide; /** yardId → label, so a re-routed rate reads as yards, not UUIDs. */ yardLabels?: Record; } /** * Pending edits to LIVE rates. Each row is a before→after diff: the left value * is what pricing charges right now and keeps charging until someone approves. * Rendered above the rates table. */ const RateApprovalsSection = ({ requests, canDecide, approve, reject, yardLabels, }: RateApprovalsSectionProps) => { const [openId, setOpenId] = useState(null); const [notes, setNotes] = useState>({}); if (requests.length === 0) return null; const decidingId = approve.variables?.id ?? reject.variables?.id ?? null; return ( Pending rate changes {requests.length} Each rate below still charges its current value. Nothing changes until approved. {requests.map((r) => { const isOpen = openId === r.id; const fields = Object.keys(r.payload); const summaryLine = headline(r); // Only the row being decided shows a spinner — the mutation's // isPending is shared across every row. const busy = decidingId === r.id; return ( update {rateSummary(r)} {summaryLine ? ( {fmtValue("rateValue", r.previousValues.rateValue)} {fmtValue("rateValue", r.payload.rateValue)} {String( r.payload.currency ?? r.previousValues.currency ?? (r.rate as Record | undefined)?.currency ?? "", )} ) : null} Submitted {fmtDateTime(r.createdAt)} · {fields.length}{" "} {fields.length === 1 ? "field" : "fields"} changed {canDecide ? ( ) : ( Awaiting approver )} {fields.map((field) => ( {FIELD_LABELS[field] ?? field} {fmtValue(field, r.previousValues[field], yardLabels)} {fmtValue(field, r.payload[field], yardLabels)} ))} {canDecide ? (