style: invoice

This commit is contained in:
Nathnael
2026-08-12 09:33:21 +00:00
parent bd9f7f354a
commit 068ee49a9f
3 changed files with 528 additions and 419 deletions

View File

@@ -0,0 +1,112 @@
import { Badge, Card, Divider, Group, Stack, Text } from "@mantine/core";
import type { ReactNode } from "react";
export interface PersonField {
label: string;
value?: string | null;
}
export interface PersonCardProps {
/** OWNER / POA / CONTACT PERSON — the person's role, not their name. */
title: string;
icon?: ReactNode;
/**
* Identity state. `undefined` = this person has no identity check at all
* (contact person), so no badge is rendered rather than a misleading "not
* verified" one.
*/
verified?: boolean;
/** Extra pills after the verification badge (e.g. "Verifies for this company"). */
badges?: ReactNode;
/** Rendered between the header and the fields — alerts, match warnings. */
notice?: ReactNode;
fields: PersonField[];
/** Shown when the API returned nothing for every field. */
emptyMessage: string;
/** Attachments or anything else that belongs to this person. */
children?: ReactNode;
}
/**
* One person in the customer's people column: owner, power of attorney, contact
* person. Empty fields are dropped rather than rendered as "—", so a field the
* API stops sending simply disappears instead of leaving a dead row behind.
*/
export function PersonCard({
title,
icon,
verified,
badges,
notice,
fields,
emptyMessage,
children,
}: PersonCardProps) {
const filled = fields.filter(
(f) => f.value != null && String(f.value).trim(),
);
return (
<Card>
<Stack gap="sm">
<Group gap={8} wrap="wrap">
{icon}
<Text
size="xs"
fw={700}
c="edr-muted"
tt="uppercase"
style={{ letterSpacing: "0.06em" }}
>
{title}
</Text>
{verified !== undefined &&
(verified ? (
<Badge size="xs" color="edr-green" variant="light">
Fayda verified
</Badge>
) : (
<Badge size="xs" color="gray" variant="light">
Not verified
</Badge>
))}
{badges}
</Group>
{notice}
{filled.length > 0 ? (
<Stack gap="xs">
{filled.map((f) => (
<Stack key={f.label} gap={0}>
<Text size="xs" c="edr-muted">
{f.label}
</Text>
<Text
size="sm"
c="edr-text"
style={{ wordBreak: "break-word" }}
>
{f.value}
</Text>
</Stack>
))}
</Stack>
) : (
<Text size="sm" c="dimmed">
{emptyMessage}
</Text>
)}
{children && (
<>
<Divider />
{children}
</>
)}
</Stack>
</Card>
);
}
export default PersonCard;

View File

@@ -24,4 +24,9 @@ export {
type ResetPasswordActionProps,
} from "./ResetPasswordAction";
export { formatBytes, formatDate, formatMoney, humanize } from "./format";
export {
PersonCard,
type PersonCardProps,
type PersonField,
} from "./PersonCard";
export { TableCard, type TableCardProps } from "./TableCard";

View File

@@ -8,7 +8,7 @@ import {
Card,
Center,
Container,
Divider,
Grid,
Group,
Loader,
SimpleGrid,
@@ -21,6 +21,7 @@ import {
ArrowLeft,
ArrowRight,
Banknote,
Contact,
Download,
Eye,
FileText,
@@ -32,6 +33,8 @@ import {
FilePen,
Paperclip,
Receipt,
UserCheck,
UserRound,
} from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { useMemo, useState } from "react";
@@ -46,6 +49,7 @@ import {
CompanyTypeBadge,
InvoiceStatusBadge,
PaymentStatusBadge,
PersonCard,
ProfileApprovalActions,
ProfileChips,
ProfileStatusBadge,
@@ -259,7 +263,9 @@ export default function CustomerDetailPage() {
variant="subtle"
color="gray"
aria-label={`View ${f.name}`}
onClick={() => void fetchViewableFile(f.id, f.name).then(view)}
onClick={() =>
void fetchViewableFile(f.id, f.name).then(view)
}
>
<Eye size={14} />
</ActionIcon>
@@ -268,7 +274,9 @@ export default function CustomerDetailPage() {
type="button"
size="xs"
lineClamp={1}
onClick={() => void fetchViewableFile(f.id, f.name).then(view)}
onClick={() =>
void fetchViewableFile(f.id, f.name).then(view)
}
style={{
maxWidth: 170,
textAlign: "left",
@@ -615,7 +623,10 @@ export default function CustomerDetailPage() {
[],
);
const licenseProfiles = (company?.companyProfiles ?? []).filter(
/** Never read `company.companyProfiles` directly — an endpoint that stops
* loading the relation would otherwise crash the whole page. */
const profiles = company?.companyProfiles ?? [];
const licenseProfiles = profiles.filter(
(p) => p.licenseFiles && p.licenseFiles.length > 0,
);
@@ -629,14 +640,13 @@ export default function CustomerDetailPage() {
[documents],
);
const poaLive = poaDocuments.filter((d) => d.code === POA_DELEGATION_CODE);
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());
const hasPoaDetails = [
company?.poaName,
company?.poaEmail,
company?.poaPhone,
company?.poaLocation,
company?.poaAddress,
].some((v) => v?.trim());
// Shared with the portal (buildCompanyIdentityState) — same derivation, so
// this page can never disagree with the rule the API actually enforces.
const identityState = company?.identity;
@@ -645,9 +655,7 @@ export default function CustomerDetailPage() {
const hasEtradeRecord = Boolean(company?.licenceNumber?.trim());
// A freight forwarder acts on other companies' behalf, so its PoA — details
// and DARS delegation paper both — is mandatory rather than optional.
const poaMandatory = (company?.companyProfiles ?? []).some(
(p) => p.type === "freight_forwarder",
);
const poaMandatory = profiles.some((p) => p.type === "freight_forwarder");
const delegationMissing =
company?.identity?.poaDeclared === "yes" && poaLive.length === 0;
@@ -685,8 +693,9 @@ export default function CustomerDetailPage() {
]}
backTo="/dashboard/customers"
title={company.name}
subtitle={`TIN ${company.tin}${company.country ? ` · ${company.country}` : ""
}`}
subtitle={`TIN ${company.tin}${
company.country ? ` · ${company.country}` : ""
}`}
meta={
<Group gap="xs" wrap="nowrap">
<CompanyTypeBadge type={company.type} />
@@ -749,7 +758,7 @@ export default function CustomerDetailPage() {
items={[
{
label: "Profiles",
value: company.companyProfiles.length,
value: profiles.length,
icon: IdCard,
color: "edr-green",
},
@@ -761,9 +770,7 @@ export default function CustomerDetailPage() {
: "Pending approval",
value: stillOnboarding
? "—"
: company.companyProfiles.filter(
(p) => p.status === "pending",
).length,
: profiles.filter((p) => p.status === "pending").length,
icon: IdCard,
color: "yellow",
},
@@ -782,407 +789,392 @@ export default function CustomerDetailPage() {
]}
/>
<Card>
<Stack gap="lg">
<Text fw={600} c="edr-text">
Company information
</Text>
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="lg">
<InfoField label="TIN" value={company.tin} />
<InfoField label="VAT number" value={company.vatNumber} />
<InfoField label="FAN number" value={company.fanNumber} />
<InfoField
label="Submitted on"
value={formatDate(company.createdAt)}
/>
<InfoField
label="Approved on"
value={
company.approvedAt
? formatDate(company.approvedAt)
: "Not yet approved"
}
/>
<InfoField
label="Owner identity"
value={
ownerIdentity?.verified
? "Fayda verified"
: ownerIdentity?.passportNumber
? `Passport ${ownerIdentity.passportNumber}`
: "Not verified"
}
/>
<InfoField label="Country" value={company.country} />
<InfoField
label="Nationality"
value={
company.nationality
? humanize(company.nationality)
: undefined
}
/>
{/* Why this company's registration was typed rather than
fetched, and why it carries no business licence. */}
<InfoField
label="Registration"
value={
company.cooperative
? "Co-operative union / farm (no trade licence)"
: "eTrade trade licence"
}
/>
<InfoField label="Address" value={company.address} />
<InfoField label="Website" value={company.website} />
<InfoField label="Email" value={company.email} />
<InfoField label="Phone" value={company.phone} />
<Box />
<InfoField
label="Contact person"
value={company.contactPersonName}
/>
<InfoField
label="Contact phone"
value={company.contactPersonPhone}
/>
<Box />
<InfoField label="Owner" value={company.ownerName} />
<InfoField label="Owner email" value={company.ownerEmail} />
<InfoField label="Owner phone" value={company.ownerPhone} />
</SimpleGrid>
</Stack>
</Card>
<Card>
<Stack gap="lg">
<Group gap="xs" wrap="nowrap" justify="space-between">
<Group gap="xs" wrap="nowrap">
<Text fw={600} c="edr-text">
eTrade registration
</Text>
{hasEtradeRecord ? (
<Badge size="sm" color="edr-green" variant="light">
Verified with eTrade
</Badge>
) : (
<Badge size="sm" color="gray" variant="light">
No eTrade record
</Badge>
)}
</Group>
{hasEtradeRecord && (
<ActionIcon
variant="default"
aria-label="Download TIN record"
onClick={() => downloadTinRecord(company)}
>
<Download size={16} />
</ActionIcon>
)}
</Group>
{hasEtradeRecord ? (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="lg">
<InfoField
label="License number"
value={company.licenceNumber}
/>
<InfoField label="Status" value={company.statusDescription} />
<InfoField
label="Date registered"
value={company.dateRegistered}
/>
<InfoField label="Renewed from" value={company.renewedFrom} />
<InfoField label="Renewal date" value={company.renewalDate} />
<InfoField label="Renewed to" value={company.renewedTo} />
<InfoField label="Region" value={company.region} />
<InfoField label="Zone" value={company.zone} />
<InfoField label="Woreda" value={company.woreda} />
<InfoField label="Kebele" value={company.kebele} />
<InfoField label="House No" value={company.houseNo} />
</SimpleGrid>
) : (
<Text size="sm" c="dimmed">
No eTrade registration record on file for this customer's
TIN.
</Text>
)}
</Stack>
</Card>
<Card>
<Stack gap="lg">
<Group gap="xs" wrap="nowrap">
<Text fw={600} c="edr-text">
Owner identity
</Text>
{identityState?.subject === "owner" && (
<Badge size="sm" color="blue" variant="light">
Verifies for this company
</Badge>
)}
{ownerIdentity?.verified ? (
<Badge size="sm" color="edr-green" variant="light">
Fayda verified
</Badge>
) : (
<Badge size="sm" color="gray" variant="light">
Not verified
</Badge>
)}
</Group>
{/* THE check: is the owner the company put forward the person
the eTrade licence actually names? Advisory — eTrade and
Fayda transliterate Amharic names differently, so this is a
prompt to look, not a verdict. */}
{identityState?.ownerMatchesEtrade === false ? (
<Alert
color="amber"
variant="light"
icon={<AlertTriangle size={16} />}
title="Does not match the eTrade licence"
>
The licence names{" "}
<strong>{identityState.etradeManagerName}</strong>, but this
company recorded <strong>{company.ownerName}</strong>.
</Alert>
) : identityState?.ownerMatchesEtrade === true ? (
<Badge
size="sm"
color="edr-green"
variant="light"
style={{ alignSelf: "flex-start" }}
>
Matches the eTrade licence
</Badge>
) : company.cooperative ? (
<Text size="xs" c="dimmed">
A co-operative union or farm holds no trade licence, so
there is no eTrade record to check the owner against.
</Text>
) : (
<Text size="xs" c="dimmed">
No eTrade manager name on file to compare against.
</Text>
)}
{ownerIdentity?.verified ? (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="lg">
<InfoField label="Name" value={ownerIdentity.name} />
<InfoField label="Phone" value={ownerIdentity.phone} />
<InfoField label="Email" value={ownerIdentity.email} />
<InfoField label="Address" value={ownerIdentity.address} />
<InfoField
label="Verified at"
value={formatDate(ownerIdentity.verifiedAt)}
/>
<InfoField
label="Birthdate"
value={ownerIdentity.birthdate}
/>
<InfoField label="Gender" value={ownerIdentity.gender} />
<InfoField
label="Passport number"
value={ownerIdentity.passportNumber}
/>
</SimpleGrid>
) : (
<Text size="sm" c="dimmed">
{ownerIdentity?.passportNumber
? `Not Fayda verified — identified by passport ${ownerIdentity.passportNumber}.`
: "The company owner has not verified their identity with Fayda."}
</Text>
)}
</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">
DARS delegation paper missing
</Badge>
) : poaLive.length > 0 ? (
<Badge size="sm" color="edr-green" variant="light">
DARS delegation paper 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}
/>
))}
<InfoField
label="PoA Fayda"
value={
poaIdentity?.verified ? "Verified" : "Not verified"
}
/>
{poaIdentity?.verified && (
<>
<Grid gap="lg" align="flex-start">
{/* Company facts — the wide column. People live in the narrow one
beside it, so nothing about a person is stated twice. */}
<Grid.Col span={{ base: 12, lg: 8 }}>
<Stack gap="lg">
<Card>
<Stack gap="lg">
<Text fw={600} c="edr-text">
Company information
</Text>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
<InfoField label="TIN" value={company.tin} />
<InfoField
label="PoA verified at"
value={formatDate(poaIdentity.verifiedAt)}
label="VAT number"
value={company.vatNumber}
/>
<InfoField label="Country" value={company.country} />
<InfoField
label="Nationality"
value={
company.nationality
? humanize(company.nationality)
: undefined
}
/>
{/* Why this company's registration was typed rather
than fetched, and why it carries no licence. */}
<InfoField
label="Registration"
value={
company.cooperative
? "Co-operative union / farm (no trade licence)"
: "eTrade trade licence"
}
/>
<InfoField label="Address" value={company.address} />
<InfoField label="Email" value={company.email} />
<InfoField label="Phone" value={company.phone} />
<InfoField label="Website" value={company.website} />
<InfoField
label="Submitted on"
value={formatDate(company.createdAt)}
/>
<InfoField
label="PoA birthdate"
value={poaIdentity.birthdate}
label="Approved on"
value={
company.approvedAt
? formatDate(company.approvedAt)
: "Not yet approved"
}
/>
<InfoField label="PoA gender" value={poaIdentity.gender} />
</>
)}
</SimpleGrid>
) : (
<Text size="sm" c="dimmed">
No Power of Attorney representative recorded for this
customer.
</Text>
)}
</SimpleGrid>
</Stack>
</Card>
<Divider />
<Stack gap="sm">
<Text
size="xs"
fw={600}
c="edr-muted"
tt="uppercase"
style={{ letterSpacing: "0.04em" }}
>
DARS delegation paper
</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 DARS delegation paper 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={() =>
void fetchViewableFile(doc.id, doc.name).then(view)
}
>
{doc.name}
</Anchor>
<Text size="xs" c="dimmed" className="shrink-0">
{formatBytes(doc.size)} ·{" "}
{formatDate(doc.uploadedAt)}
<Card>
<Stack gap="lg">
<Group gap="xs" wrap="nowrap" justify="space-between">
<Group gap="xs" wrap="nowrap">
<Text fw={600} c="edr-text">
eTrade registration
</Text>
{doc.code === POA_DELEGATION_PENDING_CODE && (
<Badge
size="xs"
color="yellow"
variant="light"
className="shrink-0"
>
Pending approval
{hasEtradeRecord ? (
<Badge size="sm" color="edr-green" variant="light">
Verified with eTrade
</Badge>
) : (
<Badge size="sm" color="gray" variant="light">
No eTrade record
</Badge>
)}
</Group>
<Group gap={4} wrap="nowrap">
{hasEtradeRecord && (
<ActionIcon
variant="subtle"
color="gray"
aria-label={`Preview ${doc.name}`}
onClick={() =>
void fetchViewableFile(doc.id, doc.name).then(view)
}
>
<Eye size={16} />
</ActionIcon>
<ActionIcon
component="button"
type="button"
onClick={() =>
void downloadBookingFile(doc.id, doc.name)
}
variant="subtle"
color="gray"
aria-label={`Download ${doc.name}`}
variant="default"
aria-label="Download TIN record"
onClick={() => downloadTinRecord(company)}
>
<Download size={16} />
</ActionIcon>
</Group>
)}
</Group>
))
)}
</Stack>
</Stack>
</Card>
{hasEtradeRecord ? (
<SimpleGrid
cols={{ base: 1, sm: 2, lg: 3 }}
spacing="lg"
>
<InfoField
label="License number"
value={company.licenceNumber}
/>
<InfoField
label="Status"
value={company.statusDescription}
/>
<InfoField
label="Date registered"
value={company.dateRegistered}
/>
<InfoField
label="Renewed from"
value={company.renewedFrom}
/>
<InfoField
label="Renewal date"
value={company.renewalDate}
/>
<InfoField
label="Renewed to"
value={company.renewedTo}
/>
<InfoField label="Region" value={company.region} />
<InfoField label="Zone" value={company.zone} />
<InfoField label="Woreda" value={company.woreda} />
<InfoField label="Kebele" value={company.kebele} />
<InfoField label="House No" value={company.houseNo} />
</SimpleGrid>
) : (
<Text size="sm" c="dimmed">
No eTrade registration record on file for this
customer's TIN.
</Text>
)}
</Stack>
</Card>
<Card>
<Stack gap="md">
<Group justify="space-between">
<Text fw={600} c="edr-text">
Role profiles
</Text>
<ProfileChips profiles={company.companyProfiles} />
</Group>
<Box style={{ overflowX: "auto" }} w="100%">
<Box miw={1040}>
<DataTable
columns={profileColumns}
data={company.companyProfiles}
status="success"
emptyMessage="No profiles registered."
containerClassName="border-0 shadow-none bg-transparent"
/>
</Box>
</Box>
</Stack>
</Card>
<Card>
<Stack gap="md">
<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>
</Stack>
</Grid.Col>
{/* People: owner, then power of attorney, then contact person —
the order a reviewer checks them in. */}
<Grid.Col span={{ base: 12, lg: 4 }}>
<Stack gap="md">
<PersonCard
title="Owner"
icon={<UserRound size={15} className="text-edr-muted" />}
verified={Boolean(ownerIdentity?.verified)}
badges={
<>
{identityState?.subject === "owner" && (
<Badge size="xs" color="blue" variant="light">
Verifies for this company
</Badge>
)}
{identityState?.ownerMatchesEtrade === true && (
<Badge size="xs" color="edr-green" variant="light">
Matches eTrade licence
</Badge>
)}
</>
}
notice={
/* THE check: is the owner the company put forward the
person the eTrade licence actually names? Advisory —
eTrade and Fayda transliterate Amharic names
differently, so this is a prompt to look, not a
verdict. */
identityState?.ownerMatchesEtrade === false ? (
<Alert
color="amber"
variant="light"
p="xs"
icon={<AlertTriangle size={16} />}
title="Does not match the eTrade licence"
>
<Text size="xs">
The licence names{" "}
<strong>{identityState.etradeManagerName}</strong>,
but this company recorded{" "}
<strong>{company.ownerName ?? "nobody"}</strong>.
</Text>
</Alert>
) : !ownerIdentity?.verified &&
ownerIdentity?.passportNumber ? (
<Text size="xs" c="dimmed">
Identified by passport rather than Fayda.
</Text>
) : null
}
fields={[
{
label: "Name",
value: company.ownerName ?? ownerIdentity?.name,
},
{
label: "Email",
value: company.ownerEmail ?? ownerIdentity?.email,
},
{
label: "Phone",
value: company.ownerPhone ?? ownerIdentity?.phone,
},
{
label: "Passport number",
value: ownerIdentity?.passportNumber,
},
{ label: "Address", value: ownerIdentity?.address },
{ label: "Birthdate", value: ownerIdentity?.birthdate },
{ label: "Gender", value: ownerIdentity?.gender },
{
label: "Verified on",
value: ownerIdentity?.verifiedAt
? formatDate(ownerIdentity.verifiedAt)
: null,
},
]}
emptyMessage="No owner recorded for this company."
/>
<PersonCard
title="Power of attorney"
icon={<UserCheck size={15} className="text-edr-muted" />}
verified={
// No PoA at all → no badge, rather than a "not verified"
// that reads as a problem where none exists.
hasPoaDetails || identityState?.poaDeclared === "yes"
? Boolean(poaIdentity?.verified)
: undefined
}
badges={
<>
{identityState?.subject === "poa" && (
<Badge size="xs" color="blue" variant="light">
Verifies for this company
</Badge>
)}
{poaMandatory && (
<Badge size="xs" color="blue" variant="light">
Required for freight forwarder
</Badge>
)}
{delegationMissing && (
<Badge size="xs" color="red" variant="light">
Delegation paper missing
</Badge>
)}
</>
}
fields={[
{ label: "Name", value: company.poaName },
{ label: "Email", value: company.poaEmail },
{ label: "Phone", value: company.poaPhone },
{ label: "Location", value: company.poaLocation },
{ label: "Address", value: company.poaAddress },
{ label: "Birthdate", value: poaIdentity?.birthdate },
{ label: "Gender", value: poaIdentity?.gender },
{
label: "Verified on",
value: poaIdentity?.verifiedAt
? formatDate(poaIdentity.verifiedAt)
: null,
},
]}
emptyMessage="No representative recorded for this customer."
>
<Stack gap="xs">
<Text size="xs" c="edr-muted">
DARS delegation paper
</Text>
{documentsQuery.isLoading ? (
<Group gap="xs">
<Loader size="xs" />
<Text size="sm" c="dimmed">
Loading
</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">
Not uploaded.
</Text>
) : (
poaDocuments.map((doc) => (
<Stack key={doc.id} gap={2}>
<Group gap={6} wrap="nowrap">
<Paperclip
size={13}
className="shrink-0 text-edr-muted"
/>
<Anchor
component="button"
type="button"
size="sm"
lineClamp={1}
style={{ flex: 1, textAlign: "left" }}
onClick={() =>
void fetchViewableFile(doc.id, doc.name).then(
view,
)
}
>
{doc.name}
</Anchor>
<ActionIcon
size="sm"
variant="subtle"
color="gray"
aria-label={`Preview ${doc.name}`}
onClick={() =>
void fetchViewableFile(doc.id, doc.name).then(
view,
)
}
>
<Eye size={15} />
</ActionIcon>
<ActionIcon
size="sm"
component="button"
type="button"
variant="subtle"
color="gray"
aria-label={`Download ${doc.name}`}
onClick={() =>
void downloadBookingFile(doc.id, doc.name)
}
>
<Download size={15} />
</ActionIcon>
</Group>
<Group gap={6} pl={19} wrap="wrap">
<Text size="xs" c="dimmed">
{formatBytes(doc.size)} ·{" "}
{formatDate(doc.uploadedAt)}
</Text>
{doc.code === POA_DELEGATION_PENDING_CODE && (
<Badge size="xs" color="yellow" variant="light">
Pending approval
</Badge>
)}
</Group>
</Stack>
))
)}
</Stack>
</PersonCard>
<PersonCard
title="Contact person"
icon={<Contact size={15} className="text-edr-muted" />}
fields={[
{ label: "Name", value: company.contactPersonName },
{ label: "Phone", value: company.contactPersonPhone },
]}
emptyMessage="No contact person recorded."
/>
</Stack>
</Grid.Col>
</Grid>
</Stack>
</Tabs.Panel>
@@ -1198,9 +1190,9 @@ export default function CustomerDetailPage() {
error={
bookingsQuery.isError
? {
message: "Failed to load bookings.",
onRetry: () => void bookingsQuery.refetch(),
}
message: "Failed to load bookings.",
onRetry: () => void bookingsQuery.refetch(),
}
: undefined
}
/>
@@ -1220,9 +1212,9 @@ export default function CustomerDetailPage() {
error={
documentsQuery.isError
? {
message: "Failed to load documents.",
onRetry: () => void documentsQuery.refetch(),
}
message: "Failed to load documents.",
onRetry: () => void documentsQuery.refetch(),
}
: undefined
}
/>
@@ -1297,9 +1289,9 @@ export default function CustomerDetailPage() {
error={
paymentsQuery.isError
? {
message: "Failed to load payments.",
onRetry: () => void paymentsQuery.refetch(),
}
message: "Failed to load payments.",
onRetry: () => void paymentsQuery.refetch(),
}
: undefined
}
/>
@@ -1320,9 +1312,9 @@ export default function CustomerDetailPage() {
error={
invoicesQuery.isError
? {
message: "Failed to load invoices.",
onRetry: () => void invoicesQuery.refetch(),
}
message: "Failed to load invoices.",
onRetry: () => void invoicesQuery.refetch(),
}
: undefined
}
pagination={{