feat(backoffice): show each role's eTrade business on the customer detail page

The reviewer approving a role had no way to see which business it claims
to operate as, so there was nothing to check the uploaded licence
document against. The Role profiles table now carries a column with the
trade name, the licensed activity (does it actually cover this role?),
the licence number (the only unambiguous handle — trade names repeat
across a TIN's licences) and the renewal date.

A role with nothing attached reads as a yellow "Not attached" rather than
a blank: it is a review finding. Yellow, not red, because a co-operative
or investment-licence company legitimately has none.

Two fixes alongside it:

- The profile reference was already rendered but is minted only on
  approval, so every pending role drew an empty line. It now says so.
- `TableCard` gained an optional header section, so padding sits per
  section and the table runs edge to edge. The header stays outside the
  scroll region — inside, a title slides away from its own table.
This commit is contained in:
Nathnael
2026-08-27 09:29:41 +00:00
parent ab5e7e6db6
commit a48d77aff8
3 changed files with 104 additions and 25 deletions

View File

@@ -3,6 +3,12 @@ import type { ReactNode } from "react";
export interface TableCardProps {
children: ReactNode;
/**
* Optional heading row (title, chips, actions). Rendered in its own padded
* section above the table and OUTSIDE the scroll region — a header inside it
* would slide away from its own table on a narrow viewport.
*/
header?: ReactNode;
/**
* Minimum width (px) the table is forced to occupy. The Mantine `Table` is
* always `width: 100%`, so without a floor it can never overflow its
@@ -14,14 +20,27 @@ export interface TableCardProps {
}
/**
* Flush card shell for a `DataTable`: a borderless, padding-less card whose
* single child is a horizontally scrollable region. Pair with the table's
* `containerClassName="border-0 shadow-none bg-transparent"` so every table on
* the customer pages reads identically (same surface, same scroll behaviour).
* Flush card shell for a `DataTable`: a padding-less card whose table region
* runs edge to edge. Padding is applied per section rather than to the card, so
* the optional {@link TableCardProps.header} is inset like any other card
* content while the table's own rows and header cells reach both edges.
*
* Pair with the table's `containerClassName="border-0 shadow-none bg-transparent"`
* so every table on the customer pages reads identically (same surface, same
* scroll behaviour).
*/
export function TableCard({ children, minWidth = 860 }: TableCardProps) {
export function TableCard({
children,
minWidth = 860,
header,
}: TableCardProps) {
return (
<Card p={0}>
{header && (
<Box p="md" style={{ borderBottom: "1px solid var(--mantine-color-edr-border-0)" }}>
{header}
</Box>
)}
<Box style={{ overflowX: "auto" }} w="100%">
<Box miw={minWidth}>{children}</Box>
</Box>

View File

@@ -240,12 +240,61 @@ export default function CustomerDetailPage() {
<div className="space-y-2">
<ProfileTypeBadge type={row.original.type} />
<Text size="sm" fw={600} c="edr-text">
{row.original.reference}
</Text>
{/* The profile reference (EX-A00001). Minted only when a reviewer
approves the role, so an unapproved one has none — say so
rather than rendering an empty line that reads as a bug. */}
{row.original.reference ? (
<Text size="sm" fw={600} c="edr-text">
{row.original.reference}
</Text>
) : (
<Text size="xs" c="dimmed" fs="italic">
Ref. issued on approval
</Text>
)}
</div>
),
},
{
id: "etradeBusiness",
header: "eTrade business",
cell: ({ row }) => {
const business = row.original.etradeBusiness;
// Not attached is a review finding, not a blank: the role names no
// business, so there is nothing to check the uploaded licence
// against. Companies with no eTrade record legitimately show this,
// which is why it reads as a warning rather than an error.
if (!business) {
return (
<Badge size="xs" color="yellow" variant="light">
Not attached
</Badge>
);
}
return (
<Stack gap={2} maw={230}>
<Text size="sm" fw={600} c="edr-text" lineClamp={2}>
{business.tradeName || "(no trade name on this licence)"}
</Text>
{business.activity && (
<Text size="xs" c="dimmed" lineClamp={2}>
{business.activity}
</Text>
)}
{/* The licence number is what the reviewer matches against the
uploaded document — trade names repeat across licences. */}
<Text size="xs" c="dimmed">
{business.licenceNumber}
</Text>
{business.renewedTo && (
<Text size="xs" c="dimmed">
Renewed to {business.renewedTo}
</Text>
)}
</Stack>
);
},
},
{
id: "licenseFiles",
header: "License documents",
@@ -981,29 +1030,29 @@ export default function CustomerDetailPage() {
</Stack>
</Card>
<Card>
<Stack gap="md">
{/* Padding sits on the header section, not the card, so the
table runs edge to edge. minWidth carries the eTrade
business column; the region scrolls rather than squashing
the other columns. */}
<TableCard
minWidth={980}
header={
<Group justify="space-between">
<Text fw={600} c="edr-text">
Role profiles
</Text>
<ProfileChips profiles={profiles} />
</Group>
{/* Narrower than the old full-width layout — the table
shares the row with the people column now. */}
<Box style={{ overflowX: "auto" }} w="100%">
<Box miw={760}>
<DataTable
columns={profileColumns}
data={profiles}
status="success"
emptyMessage="No profiles registered."
containerClassName="border-0 shadow-none bg-transparent"
/>
</Box>
</Box>
</Stack>
</Card>
}
>
<DataTable
columns={profileColumns}
data={profiles}
status="success"
emptyMessage="No profiles registered."
containerClassName="border-0 shadow-none bg-transparent"
/>
</TableCard>
</Stack>
</Grid.Col>

View File

@@ -8,6 +8,8 @@
* API so the data layer can be swapped to live endpoints with no UI changes.
*/
import type { ETradeBusinessOption } from "@edr/types";
/** Mirrors backend `CompanyType`. */
export type CompanyType =
| "customer"
@@ -65,6 +67,15 @@ export interface CompanyProfile {
/** Business-license documents uploaded for this profile. */
licenseFiles?: LicenseFile[];
attributes?: Record<string, unknown> | null;
/**
* Which of the TIN's eTrade business licences this role operates as.
*
* A TIN routinely holds a dozen licences split by activity, so "exporter" and
* "freight forwarder" are usually two different businesses under one company.
* Null when the customer has not attached one, or when the company registered
* without eTrade at all (co-operative / investment licence).
*/
etradeBusiness?: ETradeBusinessOption | null;
/** Reviewer note when the role is rejected. */
reviewNote?: string | null;
createdAt: string;