feat(MyApplicationsPage): implement advanced table for licenses and applications with improved loading and pagination

This commit is contained in:
estifanos
2026-08-04 11:18:11 +00:00
parent 01105446a0
commit acd30ce071

View File

@@ -3,17 +3,15 @@ import {
Badge,
Button,
Card,
Center,
Container,
Group,
Loader,
SimpleGrid,
Stack,
Table,
Text,
Title,
} from '@mantine/core';
import { IconDownload } from '@tabler/icons-react';
import { AdvancedTable, useServerTable, type AdvancedColumn } from '@ema-platform/ui';
import { LicenseCatalogue } from '../components/LicenseCatalogue';
import { useApplicationPayment } from '../../payments/hooks/useApplicationPayment';
import { notifications } from '@mantine/notifications';
@@ -27,6 +25,8 @@ import {
useGetMyApplicationsQuery,
useGetMyLicensesQuery,
useGetPaymentCapabilitiesQuery,
type LicenseApplication,
type IssuedLicense,
} from '@ema-platform/api';
/**
@@ -39,10 +39,10 @@ import {
*/
export function MyApplicationsPage() {
const navigate = useNavigate();
const { data, isLoading } = useGetMyApplicationsQuery();
const { data, isFetching, refetch } = useGetMyApplicationsQuery();
const { pay, isPaying } = useApplicationPayment();
const { data: capabilities } = useGetPaymentCapabilitiesQuery();
const { data: licences } = useGetMyLicensesQuery();
const { data: licences, isFetching: isFetchingLicences, refetch: refetchLicences } = useGetMyLicensesQuery();
const [bypassPayment, { isLoading: bypassing }] = useBypassPaymentMutation();
const [getCertificateUrl] = useGetCertificateUrlMutation();
@@ -101,15 +101,165 @@ export function MyApplicationsPage() {
}
}
if (isLoading) {
return (
<Center h={300}>
<Loader />
</Center>
);
}
const items = data?.items ?? [];
const licenceItems = licences?.items ?? [];
const { setPageIndex: setLicencePage, pageSize: licencePageSize, paginate: paginateLicences } =
useServerTable({ pageSize: 10 });
const licencePage = paginateLicences(licenceItems);
const licenceColumns: AdvancedColumn<IssuedLicense>[] = [
{
header: 'Certificate',
cell: ({ row }) => (
<Text size="sm" fw={500} ff="monospace">
{row.original.certificateNumber}
</Text>
),
},
{
header: 'Licence',
cell: ({ row }) => <Text size="sm">{localized(row.original.licenseType?.name) || '—'}</Text>,
},
{
header: 'Valid until',
cell: ({ row }) => (
<Text size="sm" c="dimmed">
{new Date(row.original.expiryDate).toLocaleDateString()}
</Text>
),
},
{
header: 'Status',
cell: ({ row }) => (
<Badge variant="light" color={row.original.status === 'ACTIVE' ? 'green' : 'gray'}>
{row.original.status}
</Badge>
),
},
{
header: '',
label: 'Actions',
align: 'right',
cell: ({ row }) => (
<Button
size="xs"
variant="light"
leftSection={<IconDownload size={14} />}
onClick={() => downloadCertificate(row.original.id)}
>
Certificate
</Button>
),
},
];
const { setPageIndex, pageSize, paginate } = useServerTable({ pageSize: 10 });
const page = paginate(items);
const applicationColumns: AdvancedColumn<LicenseApplication>[] = [
{
header: 'Number',
cell: ({ row }) => (
<Text size="sm" fw={500}>
{row.original.applicationNumber}
</Text>
),
},
{
header: 'Company',
cell: ({ row }) => <Text size="sm">{row.original.companyName ?? '—'}</Text>,
},
{
header: 'Status',
cell: ({ row }) => (
<Badge color={STATUS_COLORS[row.original.status]} variant="light">
{STATUS_LABELS[row.original.status]}
</Badge>
),
},
{
header: 'Submitted',
cell: ({ row }) => (
<Text size="sm" c="dimmed">
{row.original.submittedAt
? new Date(row.original.submittedAt).toLocaleDateString()
: '—'}
</Text>
),
},
{
header: '',
label: 'Actions',
align: 'right',
cell: ({ row }) => {
const app = row.original;
return (
<Group gap="xs" justify="flex-end" wrap="nowrap">
{capabilities?.bypassEnabled && app.status === 'PAYMENT_PENDING' && (
<Button
size="xs"
variant="default"
loading={bypassing}
onClick={() => handleBypass(app.id)}
title="Testing only — marks the fee paid and issues the licence"
>
Bypass payment
</Button>
)}
{/* An issued application's primary action is the certificate. It
used to be "View", which opened the application wizard — so
the one thing the applicant came back for was the one thing
the button did not do. */}
{app.status === 'CERTIFICATE_ISSUED' && (
<Button
size="xs"
leftSection={<IconDownload size={14} />}
onClick={() => openCertificateForApplication(app.id)}
>
Certificate
</Button>
)}
<Button
size="xs"
loading={isPaying && app.status === 'PAYMENT_PENDING'}
variant={
app.status === 'RESUBMIT_REQUIRED' || app.status === 'PAYMENT_PENDING'
? 'filled'
: 'subtle'
}
color={
app.status === 'RESUBMIT_REQUIRED'
? 'orange'
: app.status === 'PAYMENT_PENDING'
? 'yellow'
: undefined
}
onClick={() =>
// Paying leaves the SPA for Telebirr, so this is a provider
// hand-off rather than a route change.
app.status === 'PAYMENT_PENDING'
? pay(app.id)
: navigate(
`/licensing/${app.licenseType?.key ?? 'FREIGHT_FORWARDER'}/applications/${app.id}`,
)
}
>
{app.status === 'DRAFT'
? 'Continue'
: app.status === 'RESUBMIT_REQUIRED'
? 'Fix & resubmit'
: app.status === 'PAYMENT_PENDING'
? `Pay ${Number(app.feeAmount ?? 0).toLocaleString()} ${app.feeCurrency}`
: app.status === 'CERTIFICATE_ISSUED'
? 'Application'
: 'View'}
</Button>
</Group>
);
},
},
];
return (
<Container size="lg" py="md">
@@ -126,56 +276,17 @@ export function MyApplicationsPage() {
My licences
</Title>
<Card withBorder padding={0} mb="xl">
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Certificate</Table.Th>
<Table.Th>Licence</Table.Th>
<Table.Th>Valid until</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th />
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{(licences?.items ?? []).map((licence) => (
<Table.Tr key={licence.id}>
<Table.Td>
<Text size="sm" fw={500} ff="monospace">
{licence.certificateNumber}
</Text>
</Table.Td>
<Table.Td>
<Text size="sm">
{localized(licence.licenseType?.name) || '—'}
</Text>
</Table.Td>
<Table.Td>
<Text size="sm" c="dimmed">
{new Date(licence.expiryDate).toLocaleDateString()}
</Text>
</Table.Td>
<Table.Td>
<Badge
variant="light"
color={licence.status === 'ACTIVE' ? 'green' : 'gray'}
>
{licence.status}
</Badge>
</Table.Td>
<Table.Td align="right">
<Button
size="xs"
variant="light"
leftSection={<IconDownload size={14} />}
onClick={() => downloadCertificate(licence.id)}
>
Certificate
</Button>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
<AdvancedTable
columns={licenceColumns}
data={licencePage.rows}
tableName="My licences"
itemCount={licencePage.itemCount}
pageIndex={licencePage.pageIndex}
onPageChange={setLicencePage}
pageSize={licencePageSize}
refresh={refetchLicences}
isLoading={isFetchingLicences}
/>
</Card>
</>
)}
@@ -205,107 +316,17 @@ export function MyApplicationsPage() {
</Card>
) : (
<Card withBorder padding={0}>
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Number</Table.Th>
<Table.Th>Company</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>Submitted</Table.Th>
<Table.Th />
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{items.map((app) => (
<Table.Tr key={app.id}>
<Table.Td>
<Text size="sm" fw={500}>
{app.applicationNumber}
</Text>
</Table.Td>
<Table.Td>
<Text size="sm">{app.companyName ?? '—'}</Text>
</Table.Td>
<Table.Td>
<Badge color={STATUS_COLORS[app.status]} variant="light">
{STATUS_LABELS[app.status]}
</Badge>
</Table.Td>
<Table.Td>
<Text size="sm" c="dimmed">
{app.submittedAt
? new Date(app.submittedAt).toLocaleDateString()
: '—'}
</Text>
</Table.Td>
<Table.Td align="right">
<Group gap="xs" justify="flex-end" wrap="nowrap">
{capabilities?.bypassEnabled && app.status === 'PAYMENT_PENDING' && (
<Button
size="xs"
variant="default"
loading={bypassing}
onClick={() => handleBypass(app.id)}
title="Testing only — marks the fee paid and issues the licence"
>
Bypass payment
</Button>
)}
{/* An issued application's primary action is the
certificate. It used to be "View", which opened the
application wizard — so the one thing the applicant
came back for was the one thing the button did not do. */}
{app.status === 'CERTIFICATE_ISSUED' && (
<Button
size="xs"
leftSection={<IconDownload size={14} />}
onClick={() => openCertificateForApplication(app.id)}
>
Certificate
</Button>
)}
<Button
size="xs"
loading={isPaying && app.status === 'PAYMENT_PENDING'}
variant={
app.status === 'RESUBMIT_REQUIRED' ||
app.status === 'PAYMENT_PENDING'
? 'filled'
: 'subtle'
}
color={
app.status === 'RESUBMIT_REQUIRED'
? 'orange'
: app.status === 'PAYMENT_PENDING'
? 'yellow'
: undefined
}
onClick={() =>
// Paying leaves the SPA for Telebirr, so this is a
// provider hand-off rather than a route change.
app.status === 'PAYMENT_PENDING'
? pay(app.id)
: navigate(
`/licensing/${app.licenseType?.key ?? 'FREIGHT_FORWARDER'}/applications/${app.id}`,
)
}
>
{app.status === 'DRAFT'
? 'Continue'
: app.status === 'RESUBMIT_REQUIRED'
? 'Fix & resubmit'
: app.status === 'PAYMENT_PENDING'
? `Pay ${Number(app.feeAmount ?? 0).toLocaleString()} ${app.feeCurrency}`
: app.status === 'CERTIFICATE_ISSUED'
? 'Application'
: 'View'}
</Button>
</Group>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
<AdvancedTable
columns={applicationColumns}
data={page.rows}
tableName="My applications"
itemCount={page.itemCount}
pageIndex={page.pageIndex}
onPageChange={setPageIndex}
pageSize={pageSize}
refresh={refetch}
isLoading={isFetching}
/>
</Card>
)}