From 3bc4514b04bc920a8f266b7f970e28cddcfa5114 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Wed, 8 Jul 2026 08:07:10 +0000 Subject: [PATCH] feat: implemented the changes request to the company profile to backoffice --- .../customers/ChangeRequestReview.tsx | 324 ++++++++++++++++++ .../src/components/customers/badges.tsx | 130 +++++-- .../src/components/customers/index.ts | 4 + .../backoffice/src/constants/QUERY_KEYS.ts | 2 + .../backoffice/src/constants/URLS.ts | 6 + .../pages/customers/CustomerDetailPage.tsx | 5 + .../backoffice/src/services/api.ts | 38 +- .../src/services/customers.service.ts | 37 +- .../backoffice/src/types/customer.ts | 30 +- 9 files changed, 546 insertions(+), 30 deletions(-) create mode 100644 apps/edr-freight-web/backoffice/src/components/customers/ChangeRequestReview.tsx 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. + +