mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
feat(MyApplicationsPage): implement advanced table for licenses and applications with improved loading and pagination
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user