mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 06:35:42 +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 {
|
export interface TableCardProps {
|
||||||
children: ReactNode;
|
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
|
* 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
|
* 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
|
* Flush card shell for a `DataTable`: a padding-less card whose table region
|
||||||
* single child is a horizontally scrollable region. Pair with the table's
|
* runs edge to edge. Padding is applied per section rather than to the card, so
|
||||||
* `containerClassName="border-0 shadow-none bg-transparent"` so every table on
|
* the optional {@link TableCardProps.header} is inset like any other card
|
||||||
* the customer pages reads identically (same surface, same scroll behaviour).
|
* 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 (
|
return (
|
||||||
<Card p={0}>
|
<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 style={{ overflowX: "auto" }} w="100%">
|
||||||
<Box miw={minWidth}>{children}</Box>
|
<Box miw={minWidth}>{children}</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -240,12 +240,61 @@ export default function CustomerDetailPage() {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<ProfileTypeBadge type={row.original.type} />
|
<ProfileTypeBadge type={row.original.type} />
|
||||||
|
|
||||||
<Text size="sm" fw={600} c="edr-text">
|
{/* The profile reference (EX-A00001). Minted only when a reviewer
|
||||||
{row.original.reference}
|
approves the role, so an unapproved one has none — say so
|
||||||
</Text>
|
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>
|
</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",
|
id: "licenseFiles",
|
||||||
header: "License documents",
|
header: "License documents",
|
||||||
@@ -981,29 +1030,29 @@ export default function CustomerDetailPage() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
{/* Padding sits on the header section, not the card, so the
|
||||||
<Stack gap="md">
|
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">
|
<Group justify="space-between">
|
||||||
<Text fw={600} c="edr-text">
|
<Text fw={600} c="edr-text">
|
||||||
Role profiles
|
Role profiles
|
||||||
</Text>
|
</Text>
|
||||||
<ProfileChips profiles={profiles} />
|
<ProfileChips profiles={profiles} />
|
||||||
</Group>
|
</Group>
|
||||||
{/* Narrower than the old full-width layout — the table
|
}
|
||||||
shares the row with the people column now. */}
|
>
|
||||||
<Box style={{ overflowX: "auto" }} w="100%">
|
<DataTable
|
||||||
<Box miw={760}>
|
columns={profileColumns}
|
||||||
<DataTable
|
data={profiles}
|
||||||
columns={profileColumns}
|
status="success"
|
||||||
data={profiles}
|
emptyMessage="No profiles registered."
|
||||||
status="success"
|
containerClassName="border-0 shadow-none bg-transparent"
|
||||||
emptyMessage="No profiles registered."
|
/>
|
||||||
containerClassName="border-0 shadow-none bg-transparent"
|
</TableCard>
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
* API so the data layer can be swapped to live endpoints with no UI changes.
|
* API so the data layer can be swapped to live endpoints with no UI changes.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { ETradeBusinessOption } from "@edr/types";
|
||||||
|
|
||||||
/** Mirrors backend `CompanyType`. */
|
/** Mirrors backend `CompanyType`. */
|
||||||
export type CompanyType =
|
export type CompanyType =
|
||||||
| "customer"
|
| "customer"
|
||||||
@@ -65,6 +67,15 @@ export interface CompanyProfile {
|
|||||||
/** Business-license documents uploaded for this profile. */
|
/** Business-license documents uploaded for this profile. */
|
||||||
licenseFiles?: LicenseFile[];
|
licenseFiles?: LicenseFile[];
|
||||||
attributes?: Record<string, unknown> | null;
|
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. */
|
/** Reviewer note when the role is rejected. */
|
||||||
reviewNote?: string | null;
|
reviewNote?: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user