import { useNavigate } from 'react-router-dom'; import { ActionIcon, Badge, Box, Button, Card, Divider, Group, Text, Tooltip, } from '@mantine/core'; import { IconDownload, IconRefresh } from '@tabler/icons-react'; import { extractErrorMessage, useLocalized, useCreateApplicationMutation, type IssuedLicense, } from '@ema-platform/api'; import { notify } from '@ema-platform/ui'; import { useDateDisplayer } from '@ema-platform/shared'; import { useTranslation } from 'react-i18next'; import { LICENSE_PERMISSIONS, PORTAL_PERMISSIONS, RequirePermission, } from '@ema-platform/auth'; /** * Renewal reuses the ordinary application wizard — a renewal is an * application of kind RENEWAL, asking for that licence type's renewal * document set. `previousLicenseId` is what ties it to the certificate being * replaced, and what the API requires. * * Shared by the dashboard and My Applications so both offer the same renew * action instead of drifting. */ export function useRenewLicense() { const navigate = useNavigate(); const { t } = useTranslation(); const [createApplication, { isLoading: isRenewing }] = useCreateApplicationMutation(); async function renewLicense(license: IssuedLicense) { const typeKey = license.licenseType?.key; if (!typeKey) return; try { const application = await createApplication({ licenseType: typeKey, kind: 'RENEWAL', previousLicenseId: license.id, }).unwrap(); navigate(`/licensing/${typeKey}/applications/${application.id}`); } catch (err) { notify.error(extractErrorMessage(err), t('licensing.card.renewFailed')); } } return { renewLicense, isRenewing }; } function daysUntil(date: string): number { const ms = new Date(date).getTime() - Date.now(); return Math.ceil(ms / 86_400_000); } export function LicenseCard({ license, isDownloading, isRenewing, onDownload, onRenew, }: { license: IssuedLicense; isDownloading: boolean; isRenewing: boolean; onDownload: () => void; onRenew: () => void; }) { // The API computes both in the authority's timezone; the local fallbacks are // only for a cached response from before those fields existed. const days = license.daysUntilExpiry ?? daysUntil(license.expiryDate); const expired = license.status === 'EXPIRED' || days < 0; // Suspended, cancelled and superseded are none of them "valid until" their // expiry date — the card used to say exactly that, because only EXPIRED was // treated as not-current. A suspended licence read as a live one with a grey // badge. const current = license.status === 'ACTIVE' && !expired; const renewable = license.renewable ?? false; const showDate = useDateDisplayer(); const localized = useLocalized(); const { t } = useTranslation(); return ( {localized(license.licenseType?.name) || t('licensing.card.fallbackName')} {license.certificateNumber} {/* The raw enum was rendered here, so an Amharic page showed "SUSPENDED" among otherwise translated text. */} {expired ? t('licensing.card.expired') : t(`licensing.card.status.${license.status}`, { defaultValue: license.status, })} {expired ? t('licensing.card.expiredOn', { date: showDate(license.expiryDate) }) : current ? t('licensing.card.validUntil', { date: showDate(license.expiryDate) }) : t(`licensing.card.status.${license.status}`, { defaultValue: license.status, })} {/* Why it stopped being current. The API returns it; the card threw it away, leaving the holder to guess. */} {!current && license.statusReason && ( {t('licensing.card.statusReason', { reason: license.statusReason })} )} {/* Renewal opens inside the licence type's window and stays open after expiry, so a lapsed licence is renewed rather than applied for afresh. */} {renewable && ( )} ); } export default LicenseCard;