diff --git a/apps/edr-freight-web/backoffice/src/components/customers/ChangeRequestReview.tsx b/apps/edr-freight-web/backoffice/src/components/customers/ChangeRequestReview.tsx new file mode 100644 index 000000000..a5de7335a --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/customers/ChangeRequestReview.tsx @@ -0,0 +1,324 @@ +import { + Alert, + Badge, + Box, + Button, + Card, + Group, + Modal, + SimpleGrid, + Stack, + Text, + Textarea, +} from "@mantine/core"; +import { useQuery, useMutation } from "@tanstack/react-query"; +import { AlertTriangle, ClipboardCheck, Clock } from "lucide-react"; +import { useState } from "react"; + +import { api } from "@/services/api"; +import type { Company, CompanyChangeRequest } from "@/types/customer"; +import { formatDate, humanize } from "./format"; + +/** Friendly labels for the proposed-change snapshot keys (UpdateProfileDto). */ +const FIELD_LABELS: Record = { + companyName: "Company name", + companyEmail: "Company email", + companyPhone: "Company phone", + companyLocation: "Location", + companyAddress: "Address", + tin: "TIN", + vatNumber: "VAT number", + fanNumber: "FAN number", + nationality: "Nationality", + licenceNumber: "Licence number", + contactPersonName: "Contact person", + contactPersonPosition: "Contact position", + contactPersonEmail: "Contact email", + contactPersonPhone: "Contact phone", + generalManagerName: "General manager", + generalManagerEmail: "GM email", + generalManagerPhone: "GM phone", + poaName: "PoA name", + poaPhone: "PoA phone", + poaEmail: "PoA email", + poaLocation: "PoA location", + poaAddress: "PoA address", + region: "Region", + zone: "Zone", + woreda: "Woreda", + kebele: "Kebele", + houseNo: "House no.", +}; + +/** Best-effort current value on the live company for a proposed field key. */ +function currentValue(company: Company, key: string): string { + const c = company as unknown as Record; + const attrs = (company.attributes ?? {}) as Record; + const map: Record = { + companyName: c.name, + companyEmail: c.email, + companyPhone: c.phone, + companyLocation: c.country, + companyAddress: c.address, + tin: c.tin, + vatNumber: c.vatNumber, + fanNumber: c.fanNumber, + nationality: c.nationality, + contactPersonName: c.contactPersonName ?? attrs.contactPersonName, + contactPersonPhone: c.contactPersonPhone ?? attrs.contactPersonPhone, + generalManagerName: c.generalManagerName ?? attrs.generalManagerName, + generalManagerEmail: c.generalManagerEmail ?? attrs.generalManagerEmail, + generalManagerPhone: c.generalManagerPhone ?? attrs.generalManagerPhone, + }; + const v = key in map ? map[key] : (c[key] ?? attrs[key]); + return v === null || v === undefined || v === "" ? "—" : String(v); +} + +function DiffRow({ + label, + from, + to, +}: { + label: string; + from: string; + to: string; +}) { + const changed = from !== to; + return ( + + + {label} + + + + {from} + + {changed && ( + <> + + → + + + {to} + + + )} + + + ); +} + +/** + * Backoffice review surface for a customer's staged profile edits. Shows the + * pending change request as a proposed-vs-current diff with Approve / Reject + * (with note) actions, plus a short history of past decisions. + */ +export function ChangeRequestReview({ company }: { company: Company }) { + const query = useQuery( + api.customers.changeRequests.queryOptions({ input: { id: company.id } }), + ); + const approve = useMutation( + api.customers.approveChangeRequest.mutationOptions(), + ); + const reject = useMutation( + api.customers.rejectChangeRequest.mutationOptions(), + ); + + const [rejectId, setRejectId] = useState(null); + const [note, setNote] = useState(""); + + const requests = query.data ?? []; + const pending = requests.find((r) => r.status === "pending"); + const history = requests.filter((r) => r.status !== "pending").slice(0, 5); + + if (!pending && history.length === 0) return null; + + const proposedKeys = pending + ? Object.keys(pending.snapshot ?? {}) + : ([] as string[]); + const docCount = pending?.documentFileIds?.length ?? 0; + + const confirmReject = () => { + if (!rejectId) return; + reject.mutate( + { id: rejectId, note: note.trim() }, + { + onSuccess: () => { + setRejectId(null); + setNote(""); + }, + }, + ); + }; + + return ( + <> + {pending && ( + + + + + + + Profile changes awaiting review + + + Pending + + + + Submitted {formatDate(pending.submittedAt ?? pending.createdAt)} + + + + {proposedKeys.length > 0 ? ( + + {proposedKeys.map((key) => ( + + ))} + + ) : ( + + No field changes — document uploads only. + + )} + + {docCount > 0 && ( + + {docCount} document{docCount === 1 ? "" : "s"} uploaded with this + request — review them in the Documents tab. + + )} + + + + + + + + )} + + {history.length > 0 && ( + + + + Review history + + {history.map((r: CompanyChangeRequest) => ( + + + {r.status} + + + + {formatDate(r.reviewedAt ?? r.updatedAt)} + + {r.note && ( + + Note: {r.note} + + )} + + + ))} + + + )} + + setRejectId(null)} + title="Reject changes" + centered + radius="lg" + > + + }> + The customer will see this note and can amend and resubmit. + +