mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 22:30:55 +00:00
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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user