diff --git a/apps/portal/src/app/features/licensing/pages/MyApplicationsPage.tsx b/apps/portal/src/app/features/licensing/pages/MyApplicationsPage.tsx index f0de46904..e5794148c 100644 --- a/apps/portal/src/app/features/licensing/pages/MyApplicationsPage.tsx +++ b/apps/portal/src/app/features/licensing/pages/MyApplicationsPage.tsx @@ -9,7 +9,6 @@ import { Card, Container, Group, - Pagination, Paper, Progress, Select, @@ -27,14 +26,13 @@ import { IconCertificate, IconClipboardList, IconClockHour4, - IconCreditCard, IconDownload, IconFileText, IconPlus, IconSearch, IconX, } from '@tabler/icons-react'; -import { AmharicDatePicker, EmptyState } from '@ema-platform/ui'; +import { AdvancedTable, AmharicDatePicker, EmptyState, useServerTable, type AdvancedColumn } from '@ema-platform/ui'; import { LicenseCatalogue } from '../components/LicenseCatalogue'; import { LicenseCard, useRenewLicense } from '../components/LicenseCard'; import { useApplicationPayment } from '../../payments/hooks/useApplicationPayment'; @@ -57,8 +55,6 @@ import { } from '@ema-platform/api'; import classes from './MyApplicationsPage.module.css'; -const PAGE_SIZE = 8; - /** Days before expiry at which a licence is worth flagging. */ const EXPIRY_WARNING_DAYS = 60; @@ -94,7 +90,7 @@ function tabFromHash(hash: string): Tab { export function MyApplicationsPage() { const navigate = useNavigate(); const { t, i18n } = useTranslation(); - const { data, isFetching } = useGetMyApplicationsQuery(); + const { data, isFetching, refetch } = useGetMyApplicationsQuery(); const { pay, isPaying } = useApplicationPayment(); const { data: capabilities } = useGetPaymentCapabilitiesQuery(); const { data: licences, isFetching: isFetchingLicences } = useGetMyLicensesQuery(); @@ -181,8 +177,8 @@ export function MyApplicationsPage() { const [dateFrom, setDateFrom] = useState(''); const [dateTo, setDateTo] = useState(''); const [bucketFilter, setBucketFilter] = useState(null); - const [pageIndex, setPageIndex] = useState(0); const hasFilters = Boolean(search || statusFilter || dateFrom || dateTo || bucketFilter); + const { setPageIndex, pageSize, setPageSize, paginate } = useServerTable({ pageSize: 10 }); const counts = useMemo(() => { const result = { needsYou: 0, inProgress: 0, completed: 0 }; @@ -218,9 +214,7 @@ export function MyApplicationsPage() { }); }, [allItems, search, statusFilter, dateFrom, dateTo, bucketFilter]); - const pageCount = Math.max(1, Math.ceil(items.length / PAGE_SIZE)); - const clampedPage = Math.min(pageIndex, pageCount - 1); - const pageItems = items.slice(clampedPage * PAGE_SIZE, clampedPage * PAGE_SIZE + PAGE_SIZE); + const page = paginate(items); function clearFilters() { setSearch(''); @@ -242,6 +236,126 @@ export function MyApplicationsPage() { const allStatuses = Object.keys(STATUS_COLORS) as LicenseStatus[]; + const applicationColumns: AdvancedColumn[] = [ + { + header: t('applications.table.licence'), + cell: ({ row }) => ( + + + {localized(row.original.licenseType?.name, i18n.language) || '—'} + + + {row.original.applicationNumber} + + + ), + }, + { + header: t('applications.table.applicant'), + cell: ({ row }) => {applicantOrCompanyName(row.original) ?? '—'}, + }, + { + header: t('common.status'), + cell: ({ row }) => ( + + {statusLabel(row.original.status)} + + ), + }, + { + header: t('applications.table.progress'), + size: 140, + cell: ({ row }) => ( + + ), + }, + { + header: t('common.date'), + cell: ({ row }) => ( + + {row.original.submittedAt + ? new Date(row.original.submittedAt).toLocaleDateString() + : t('applications.card.notFiled')} + + ), + }, + { + header: '', + label: t('common.actions'), + align: 'right', + cell: ({ row }) => { + const app = row.original; + return ( + + {capabilities?.bypassEnabled && app.status === 'PAYMENT_PENDING' && ( + + )} + {/* 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' && ( + + )} + + + ); + }, + }, + ]; + return ( @@ -368,13 +482,7 @@ export function MyApplicationsPage() { )} - {isFetching ? ( - - - - - - ) : items.length === 0 ? ( + {!isFetching && items.length === 0 ? ( ) : ( - <> - - {pageItems.map((app) => ( - handleBypass(app.id)} - onCertificate={() => openCertificateForApplication(app.id)} - onPay={() => pay(app.id)} - onNavigate={() => - navigate(`/licensing/${app.licenseType?.key ?? 'FREIGHT_FORWARDER'}/applications/${app.id}`) - } - /> - ))} - - {pageCount > 1 && ( - - setPageIndex(p - 1)} - /> - - )} - + )} )} @@ -510,119 +600,5 @@ function StatTile({ ); } -function ApplicationCard({ - app, - language, - statusLabel, - bypassEnabled, - bypassing, - isPaying, - onBypass, - onCertificate, - onPay, - onNavigate, -}: { - app: LicenseApplication; - language: string; - statusLabel: (status: LicenseStatus) => string; - bypassEnabled: boolean; - bypassing: boolean; - isPaying: boolean; - onBypass: () => void; - onCertificate: () => void; - onPay: () => void; - onNavigate: () => void; -}) { - const { t } = useTranslation(); - const status = app.status; - const Icon = - status === 'PAYMENT_PENDING' - ? IconCreditCard - : status === 'RESUBMIT_REQUIRED' - ? IconAlertTriangle - : IconFileText; - - return ( - - - - - - - - - {localized(app.licenseType?.name, language) || 'Licence application'} - - - {app.applicationNumber} - {applicantOrCompanyName(app) ? ` · ${applicantOrCompanyName(app)}` : ''} - - - - - {statusLabel(status)} - - - - - - - - {app.submittedAt - ? t('applications.card.submitted', { date: new Date(app.submittedAt).toLocaleDateString() }) - : t('applications.card.notFiled')} - {status === 'PAYMENT_PENDING' && - ` · ${t('applications.card.feeDue')}: ${Number(app.feeAmount ?? 0).toLocaleString()} ${app.feeCurrency}`} - - - {bypassEnabled && status === 'PAYMENT_PENDING' && ( - - )} - {/* 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. */} - {status === 'CERTIFICATE_ISSUED' && ( - - )} - - - - - ); -} export default MyApplicationsPage; diff --git a/apps/portal/src/app/i18n/locales/am.ts b/apps/portal/src/app/i18n/locales/am.ts index 46cfb5e5f..98747c918 100644 --- a/apps/portal/src/app/i18n/locales/am.ts +++ b/apps/portal/src/app/i18n/locales/am.ts @@ -148,6 +148,11 @@ export const am: Translations = { notFiled: 'ገና አልገባም', feeDue: 'የሚከፈል ክፍያ', }, + table: { + licence: 'ፍቃድ', + applicant: 'አመልካች', + progress: 'ደረጃ', + }, actions: { continue: 'ቀጥል', fixResubmit: 'አስተካክለህ እንደገና አስገባ', diff --git a/apps/portal/src/app/i18n/locales/en.ts b/apps/portal/src/app/i18n/locales/en.ts index 55f22dc27..bbb221d44 100644 --- a/apps/portal/src/app/i18n/locales/en.ts +++ b/apps/portal/src/app/i18n/locales/en.ts @@ -146,6 +146,11 @@ export const en = { notFiled: 'Not filed yet', feeDue: 'Fee due', }, + table: { + licence: 'Licence', + applicant: 'Applicant', + progress: 'Progress', + }, actions: { continue: 'Continue', fixResubmit: 'Fix & resubmit',