mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
fea: add poa section to backoffice customer detail page
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
Card,
|
||||
Center,
|
||||
Container,
|
||||
Divider,
|
||||
Group,
|
||||
Loader,
|
||||
SimpleGrid,
|
||||
@@ -90,6 +91,9 @@ function tableStatus(query: { isLoading: boolean; isError: boolean }) {
|
||||
return query.isLoading ? "loading" : query.isError ? "error" : "success";
|
||||
}
|
||||
|
||||
/** Matches the fileKey seeded in the API's file-upload-settings seeder. */
|
||||
const POA_DELEGATION_CODE = "poa_delegation_letter";
|
||||
|
||||
export default function CustomerDetailPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
@@ -531,6 +535,26 @@ export default function CustomerDetailPage() {
|
||||
(p) => p.licenseFiles && p.licenseFiles.length > 0,
|
||||
);
|
||||
|
||||
const poaDocuments = useMemo(
|
||||
() => documents.filter((d) => d.code === POA_DELEGATION_CODE),
|
||||
[documents],
|
||||
);
|
||||
const poaFields = [
|
||||
{ label: "PoA name", value: company?.poaName },
|
||||
{ label: "PoA email", value: company?.poaEmail },
|
||||
{ label: "PoA phone", value: company?.poaPhone },
|
||||
{ label: "PoA location", value: company?.poaLocation },
|
||||
{ label: "PoA address", value: company?.poaAddress },
|
||||
];
|
||||
const hasPoaDetails = poaFields.some((f) => f.value?.trim());
|
||||
// A freight forwarder acts on other companies' behalf, so its PoA — details
|
||||
// and delegation letter both — is mandatory rather than optional.
|
||||
const poaMandatory = (company?.companyProfiles ?? []).some(
|
||||
(p) => p.type === "freight_forwarder",
|
||||
);
|
||||
const delegationMissing =
|
||||
(hasPoaDetails || poaMandatory) && poaDocuments.length === 0;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Center mih="60vh">
|
||||
@@ -672,6 +696,149 @@ export default function CustomerDetailPage() {
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Stack gap="lg">
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Text fw={600} c="edr-text">
|
||||
Power of Attorney
|
||||
</Text>
|
||||
{poaMandatory && (
|
||||
<Badge size="xs" color="blue" variant="light">
|
||||
Required for freight forwarder
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
{delegationMissing ? (
|
||||
<Badge size="sm" color="red" variant="light">
|
||||
Delegation letter missing
|
||||
</Badge>
|
||||
) : poaDocuments.length > 0 ? (
|
||||
<Badge size="sm" color="edr-green" variant="light">
|
||||
Delegation letter on file
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge size="sm" color="gray" variant="light">
|
||||
Not provided
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
{hasPoaDetails ? (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="lg">
|
||||
{poaFields.map((f) => (
|
||||
<InfoField
|
||||
key={f.label}
|
||||
label={f.label}
|
||||
value={f.value}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
) : (
|
||||
<Text size="sm" c="dimmed">
|
||||
No Power of Attorney representative recorded for this
|
||||
customer.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
<Stack gap="sm">
|
||||
<Text
|
||||
size="xs"
|
||||
fw={600}
|
||||
c="edr-muted"
|
||||
tt="uppercase"
|
||||
style={{ letterSpacing: "0.04em" }}
|
||||
>
|
||||
Delegation letter
|
||||
</Text>
|
||||
|
||||
{documentsQuery.isLoading ? (
|
||||
<Group gap="xs">
|
||||
<Loader size="xs" />
|
||||
<Text size="sm" c="dimmed">
|
||||
Loading documents…
|
||||
</Text>
|
||||
</Group>
|
||||
) : documentsQuery.isError ? (
|
||||
<Group gap="sm">
|
||||
<Text size="sm" c="red">
|
||||
Failed to load documents.
|
||||
</Text>
|
||||
<Anchor
|
||||
component="button"
|
||||
type="button"
|
||||
size="xs"
|
||||
onClick={() => void documentsQuery.refetch()}
|
||||
>
|
||||
Retry
|
||||
</Anchor>
|
||||
</Group>
|
||||
) : poaDocuments.length === 0 ? (
|
||||
<Text size="sm" c="dimmed">
|
||||
No delegation letter uploaded.
|
||||
</Text>
|
||||
) : (
|
||||
poaDocuments.map((doc) => (
|
||||
<Group key={doc.id} justify="space-between" wrap="nowrap">
|
||||
<Group gap="sm" wrap="nowrap" style={{ minWidth: 0 }}>
|
||||
<Paperclip
|
||||
size={14}
|
||||
className="shrink-0 text-edr-muted"
|
||||
/>
|
||||
<Anchor
|
||||
component="button"
|
||||
type="button"
|
||||
size="sm"
|
||||
lineClamp={1}
|
||||
onClick={() =>
|
||||
view({
|
||||
name: doc.name,
|
||||
url: fileViewUrl(doc.id),
|
||||
mimeType: doc.mimeType,
|
||||
})
|
||||
}
|
||||
>
|
||||
{doc.name}
|
||||
</Anchor>
|
||||
<Text size="xs" c="dimmed" className="shrink-0">
|
||||
{formatBytes(doc.size)} ·{" "}
|
||||
{formatDate(doc.uploadedAt)}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label={`Preview ${doc.name}`}
|
||||
onClick={() =>
|
||||
view({
|
||||
name: doc.name,
|
||||
url: fileViewUrl(doc.id),
|
||||
mimeType: doc.mimeType,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Eye size={16} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
component="a"
|
||||
href={fileViewUrl(doc.id, true)}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label={`Download ${doc.name}`}
|
||||
>
|
||||
<Download size={16} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Group>
|
||||
))
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between">
|
||||
|
||||
@@ -33,6 +33,11 @@ function mapCompany(dto: Record<string, unknown>): Company {
|
||||
generalManagerName: (attrs.generalManagerName as string | null) ?? null,
|
||||
generalManagerEmail: (attrs.generalManagerEmail as string | null) ?? null,
|
||||
generalManagerPhone: (attrs.generalManagerPhone as string | null) ?? null,
|
||||
poaName: (attrs.poaName as string | null) ?? null,
|
||||
poaEmail: (attrs.poaEmail as string | null) ?? null,
|
||||
poaPhone: (attrs.poaPhone as string | null) ?? null,
|
||||
poaLocation: (attrs.poaLocation as string | null) ?? null,
|
||||
poaAddress: (attrs.poaAddress as string | null) ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -127,6 +127,11 @@ export interface Company {
|
||||
generalManagerName?: string | null;
|
||||
generalManagerEmail?: string | null;
|
||||
generalManagerPhone?: string | null;
|
||||
poaName?: string | null;
|
||||
poaEmail?: string | null;
|
||||
poaPhone?: string | null;
|
||||
poaLocation?: string | null;
|
||||
poaAddress?: string | null;
|
||||
website?: string | null;
|
||||
attributes?: Record<string, unknown> | null;
|
||||
companyProfiles: CompanyProfile[];
|
||||
|
||||
Reference in New Issue
Block a user