feat(freight): track onboarding-phase edit history for companies

Ticket #238 — pre-approval edits and document uploads write straight
to the live company row with no approval gate and, until now, no
trace. Adds an append-only company_revisions log (diffed field
changes, document uploads) recorded from updateProfile and
uploadCompanyDocuments, exposed via GET /companies/:id/revisions and
shown as "Version history" on the backoffice customer detail page.
This commit is contained in:
Nathnael
2026-07-31 11:45:09 +00:00
parent d67297a601
commit 7c5d96795c
18 changed files with 383 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
import { Badge, Box, Card, Group, Stack, Text } from "@mantine/core";
import { useQuery } from "@tanstack/react-query";
import { History } from "lucide-react";
import { api } from "@/services/api";
import { formatDate } from "./format";
/**
* Onboarding-phase edit history: what changed on the company record before it
* reached Active, the write path that has no approval gate (unlike edits made
* after approval, which go through {@link ChangeRequestReview} instead).
*/
export function CompanyRevisionHistory({ companyId }: { companyId: string }) {
const query = useQuery(
api.customers.revisions.queryOptions({ input: { id: companyId } }),
);
const revisions = query.data ?? [];
if (revisions.length === 0) return null;
return (
<Card withBorder>
<Stack gap="sm">
<Group gap="xs">
<History size={16} />
<Text fw={600} c="edr-text">
Version history
</Text>
</Group>
{revisions.map((rev) => (
<Box key={rev.id}>
<Group gap="sm" wrap="nowrap" align="flex-start">
<Badge color="gray" variant="light" radius="md" tt="none">
{formatDate(rev.createdAt)}
</Badge>
<Text size="sm" c="edr-text" tt="capitalize">
{rev.summary}
</Text>
</Group>
{rev.changes.length > 0 && (
<Stack gap={2} ml={4} mt={4}>
{rev.changes.map((c, i) => (
<Text key={i} size="xs" c="dimmed">
{c.label}: {c.from ?? "—"} {c.to ?? "—"}
</Text>
))}
</Stack>
)}
</Box>
))}
</Stack>
</Card>
);
}

View File

@@ -13,6 +13,7 @@ export {
ChangeRequestReview,
ChangeRequestPendingBadge,
} from "./ChangeRequestReview";
export { CompanyRevisionHistory } from "./CompanyRevisionHistory";
export {
RequestDocumentChangeModal,
type RequestDocumentChangeModalProps,

View File

@@ -42,6 +42,8 @@ export const QUERY_KEYS = {
["customers", "detail", id, "reset-target"] as const,
changeRequests: (id: string) =>
["customers", "detail", id, "change-requests"] as const,
revisions: (id: string) =>
["customers", "detail", id, "revisions"] as const,
},
INVOICES: {

View File

@@ -78,6 +78,7 @@ export const URL_CONSTANTS = {
`/companies/company-profiles/${profileId}/status`,
CHANGE_REQUESTS: (companyId: string) =>
`/companies/${companyId}/change-requests`,
REVISIONS: (companyId: string) => `/companies/${companyId}/revisions`,
CHANGE_REQUEST_APPROVE: (id: string) =>
`/companies/change-requests/${id}/approve`,
CHANGE_REQUEST_REJECT: (id: string) =>

View File

@@ -39,6 +39,7 @@ import {
BookingStatusBadge,
ChangeRequestPendingBadge,
ChangeRequestReview,
CompanyRevisionHistory,
CompanyStatusBadge,
CompanyTypeBadge,
InvoiceStatusBadge,
@@ -710,6 +711,7 @@ export default function CustomerDetailPage() {
)}
<ChangeRequestReview company={company} />
<CompanyRevisionHistory companyId={company.id} />
<KpiStrip
items={[

View File

@@ -8,6 +8,7 @@ import type {
CompanyChangeRequest,
CompanyListFilter,
CompanyProfile,
CompanyRevision,
CompanyStats,
CustomerBooking,
CustomerDocument,
@@ -2778,6 +2779,13 @@ export const api = {
({ id }) => QUERY_KEYS.CUSTOMERS.changeRequests(id),
),
revisions: endpoint<{ id: string }, CompanyRevision[]>(
"customers",
"revisions",
({ id }) => customersService.revisions(id),
({ id }) => QUERY_KEYS.CUSTOMERS.revisions(id),
),
approveChangeRequest: endpoint<{ id: string }, CompanyChangeRequest>(
"customers",
"approveChangeRequest",

View File

@@ -5,6 +5,7 @@ import type {
CompanyChangeRequest,
CompanyListFilter,
CompanyProfile,
CompanyRevision,
CompanyStats,
CustomerBooking,
CustomerDocument,
@@ -146,6 +147,13 @@ export const customersService = {
.then((r) => r.data);
},
/** Onboarding-phase edit history for a company (version history), newest first. */
revisions(companyId: string): Promise<CompanyRevision[]> {
return apiClient
.get<CompanyRevision[]>(URL_CONSTANTS.COMPANIES.REVISIONS(companyId))
.then((r) => r.data);
},
/** Approve a pending change request — applies the proposed changes. */
approveChangeRequest(id: string): Promise<CompanyChangeRequest> {
return apiClient