import { Alert, Anchor, Badge, Box, Button, Card, Group, Modal, SimpleGrid, Stack, Text, Textarea, } from "@mantine/core"; import { useQuery, useMutation } from "@tanstack/react-query"; import { AlertTriangle, ClipboardCheck, Clock, FilePlus2, FileX2, } from "lucide-react"; import { useState } from "react"; import { useFileViewer } from "@edr/ui-common"; import { useAuth } from "@/auth/useAuth"; import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions"; import { fetchViewableFile } from "@/services/files.service"; 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 { user } = useAuth(); const canReview = hasPermission(user, FREIGHT_PERMS.customers.verify); 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 { view, viewer } = useFileViewer(); 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 licenseChanges = pending?.licenseChanges ?? []; const documentChanges = pending?.documentChanges ?? []; 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. )} {documentChanges.length > 0 && ( Document changes {documentChanges.map((c, i) => ( {c.op === "add" ? ( ) : ( )} {c.op === "add" ? "Add" : "Remove"} void fetchViewableFile( c.fileId, c.fileName ?? humanize(c.code), ).then(view) } style={{ textDecoration: c.op === "remove" ? "line-through" : undefined, }} > {c.fileName ?? humanize(c.code)} {humanize(c.code)} ))} )} {docCount > 0 && ( Documents uploaded with this request {pending!.documentFileIds.map((fileId, i) => ( void fetchViewableFile(fileId, `Document ${i + 1}`).then( view, ) } > Document {i + 1} ))} )} {licenseChanges.length > 0 && ( Business license changes {licenseChanges.map((c, i) => ( {c.op === "add" ? ( ) : ( )} {c.op === "add" ? "Add" : "Remove"} void fetchViewableFile( c.fileId, c.fileName ?? "License document", ).then(view) } style={{ textDecoration: c.op === "remove" ? "line-through" : undefined, }} > {c.fileName ?? "License document"} ))} )} {/* Reviewing the diff is `customers:view`; deciding on it is `customers:verify`. Without it the request stays readable but un-actionable. */} {canReview && ( )} )} {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.