mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-02 11:33:42 +00:00
feat: add internationalization support for seafarer registry
- Integrated i18n for seafarer registry page, including translations for various UI elements and statuses. - Updated SeafarerDetailDrawer and StatusModal components to utilize translation hooks. - Enhanced user feedback messages for status updates and errors. - Added new translations for Amharic and English locales, covering seafarer statuses, drawer labels, modal content, and table headers. localization for backoffice
This commit is contained in:
@@ -38,6 +38,7 @@ import {
|
||||
STATUS_LABELS,
|
||||
applicantOrCompanyName,
|
||||
extractErrorMessage,
|
||||
localized,
|
||||
useClaimApplicationMutation,
|
||||
useGetAllApplicationsQuery,
|
||||
useGetAssignedToMeQuery,
|
||||
@@ -409,7 +410,9 @@ export function LicenseQueuePage() {
|
||||
{
|
||||
header: t("queue.typeCol", "Type"),
|
||||
cell: ({ row }) => (
|
||||
<Text size="sm">{row.original.licenseType?.name?.en ?? "—"}</Text>
|
||||
<Text size="sm">
|
||||
{localized(row.original.licenseType?.name, i18n.language) || "—"}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -417,7 +420,10 @@ export function LicenseQueuePage() {
|
||||
label: t("queue.statusCol", "Status"),
|
||||
cell: ({ row }) => (
|
||||
<Badge color={STATUS_COLORS[row.original.status]} variant="light">
|
||||
{STATUS_LABELS[row.original.status]}
|
||||
{t(
|
||||
`queue.statusValues.${row.original.status}`,
|
||||
STATUS_LABELS[row.original.status],
|
||||
)}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
@@ -565,7 +571,7 @@ export function LicenseQueuePage() {
|
||||
placeholder={t("queue.anyStatus", "Any")}
|
||||
data={ALL_STATUSES.map((s) => ({
|
||||
value: s,
|
||||
label: STATUS_LABELS[s],
|
||||
label: t(`queue.statusValues.${s}`, STATUS_LABELS[s]),
|
||||
}))}
|
||||
value={urlFilter.status ?? []}
|
||||
onChange={(v) => setFacet({ status: v as LicenseStatus[] })}
|
||||
@@ -578,7 +584,7 @@ export function LicenseQueuePage() {
|
||||
placeholder={t("queue.anyType", "Any")}
|
||||
data={(licenseTypes?.items ?? []).map((type) => ({
|
||||
value: type.id,
|
||||
label: type.name.en ?? type.key,
|
||||
label: localized(type.name, i18n.language) || type.key,
|
||||
}))}
|
||||
value={urlFilter.licenseTypeId ?? null}
|
||||
onChange={(v) => setFacet({ licenseTypeId: v ?? undefined })}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -64,12 +65,13 @@ function RejectModal({
|
||||
onConfirm: (remark: string) => void;
|
||||
loading: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [remark, setRemark] = useState('');
|
||||
return (
|
||||
<Modal opened={opened} onClose={onClose} title={title} centered>
|
||||
<Stack>
|
||||
<Textarea
|
||||
label="What must the seafarer fix?"
|
||||
label={t('recordVerification.rejectReasonLabel', 'What must the seafarer fix?')}
|
||||
required
|
||||
minRows={2}
|
||||
value={remark}
|
||||
@@ -77,7 +79,7 @@ function RejectModal({
|
||||
/>
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={onClose}>
|
||||
Cancel
|
||||
{t('recordVerification.cancel', 'Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
color="red"
|
||||
@@ -88,7 +90,7 @@ function RejectModal({
|
||||
setRemark('');
|
||||
}}
|
||||
>
|
||||
Reject
|
||||
{t('recordVerification.reject', 'Reject')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
@@ -110,6 +112,7 @@ function AttachmentsModal({
|
||||
title: string;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { data: attachments, isLoading } = useGetAttachmentsQuery(
|
||||
{ ownerType, ownerId: ownerId ?? '' },
|
||||
{ skip: !ownerId },
|
||||
@@ -132,7 +135,10 @@ function AttachmentsModal({
|
||||
<Group gap="xs" c="dimmed">
|
||||
<IconInbox size={18} />
|
||||
<Text size="sm" c="dimmed">
|
||||
No evidence attachments uploaded for this record.
|
||||
{t(
|
||||
'recordVerification.noAttachments',
|
||||
'No evidence attachments uploaded for this record.',
|
||||
)}
|
||||
</Text>
|
||||
</Group>
|
||||
</Center>
|
||||
@@ -163,11 +169,11 @@ function AttachmentsModal({
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
View
|
||||
{t('recordVerification.view', 'View')}
|
||||
</Button>
|
||||
) : (
|
||||
<Badge size="sm" variant="light" color="gray">
|
||||
No URL
|
||||
{t('recordVerification.noUrl', 'No URL')}
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
@@ -186,6 +192,7 @@ function AttachmentsModal({
|
||||
* certificate starts satisfying the submission gate.
|
||||
*/
|
||||
export function MedicalVerificationPage() {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
data: pendingMedical,
|
||||
isLoading: loadingMedical,
|
||||
@@ -230,10 +237,12 @@ export function MedicalVerificationPage() {
|
||||
await run();
|
||||
notify.success(done);
|
||||
} catch (error) {
|
||||
notify.error(extractErrorMessage(error, 'Could not record the ruling'));
|
||||
notify.error(
|
||||
extractErrorMessage(error, t('recordVerification.rulingFailed', 'Could not record the ruling')),
|
||||
);
|
||||
}
|
||||
},
|
||||
[],
|
||||
[t],
|
||||
);
|
||||
|
||||
const pendingMedicalList = useMemo(
|
||||
@@ -268,8 +277,8 @@ export function MedicalVerificationPage() {
|
||||
const medicalColumns: AdvancedColumn<MedicalCertificate>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
header: 'Seafarer',
|
||||
label: 'Seafarer',
|
||||
header: t('recordVerification.columns.seafarer', 'Seafarer'),
|
||||
label: t('recordVerification.columns.seafarer', 'Seafarer'),
|
||||
accessorKey: 'profile.firstName',
|
||||
cell: ({ row }) => (
|
||||
<div>
|
||||
@@ -285,8 +294,8 @@ export function MedicalVerificationPage() {
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Issuer',
|
||||
label: 'Issuer',
|
||||
header: t('recordVerification.columns.issuer', 'Issuer'),
|
||||
label: t('recordVerification.columns.issuer', 'Issuer'),
|
||||
accessorKey: 'issuerName',
|
||||
cell: ({ row }) => (
|
||||
<div>
|
||||
@@ -300,8 +309,8 @@ export function MedicalVerificationPage() {
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Validity',
|
||||
label: 'Validity',
|
||||
header: t('recordVerification.columns.validity', 'Validity'),
|
||||
label: t('recordVerification.columns.validity', 'Validity'),
|
||||
cell: ({ row }) => (
|
||||
<Text size="sm">
|
||||
{showDate(row.original.issueDate)} → {showDate(row.original.expiryDate)}
|
||||
@@ -309,18 +318,18 @@ export function MedicalVerificationPage() {
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Fitness',
|
||||
label: 'Fitness',
|
||||
header: t('recordVerification.columns.fitness', 'Fitness'),
|
||||
label: t('recordVerification.columns.fitness', 'Fitness'),
|
||||
accessorKey: 'fitnessStatus',
|
||||
cell: ({ row }) => (
|
||||
<Badge size="sm" variant="light">
|
||||
{row.original.fitnessStatus}
|
||||
{t(`recordVerification.fitness.${row.original.fitnessStatus}`, row.original.fitnessStatus)}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: '',
|
||||
label: 'Actions',
|
||||
label: t('recordVerification.columns.actions', 'Actions'),
|
||||
align: 'right',
|
||||
cell: ({ row }) => (
|
||||
<Group gap="xs" justify="flex-end" wrap="nowrap">
|
||||
@@ -333,11 +342,14 @@ export function MedicalVerificationPage() {
|
||||
setAttachmentModal({
|
||||
ownerType: 'MEDICAL_CERTIFICATE',
|
||||
ownerId: row.original.id,
|
||||
title: `Evidence for ${ownerName(row.original.profile)} Medical Certificate`,
|
||||
title: t('recordVerification.evidenceTitleMedical', {
|
||||
name: ownerName(row.original.profile),
|
||||
defaultValue: 'Evidence for {{name}} Medical Certificate',
|
||||
}),
|
||||
})
|
||||
}
|
||||
>
|
||||
Evidence
|
||||
{t('recordVerification.evidence', 'Evidence')}
|
||||
</Button>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
@@ -351,11 +363,11 @@ export function MedicalVerificationPage() {
|
||||
id: row.original.id,
|
||||
outcome: 'VERIFIED',
|
||||
}).unwrap(),
|
||||
'Certificate verified',
|
||||
t('recordVerification.certificateVerified', 'Certificate verified'),
|
||||
)
|
||||
}
|
||||
>
|
||||
Verify
|
||||
{t('recordVerification.verify', 'Verify')}
|
||||
</Button>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
@@ -364,20 +376,20 @@ export function MedicalVerificationPage() {
|
||||
leftSection={<IconX size={14} />}
|
||||
onClick={() => setRejectMedical(row.original)}
|
||||
>
|
||||
Reject
|
||||
{t('recordVerification.reject', 'Reject')}
|
||||
</Button>
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
],
|
||||
[rulingMedical, rule, verifyMedical, showDate],
|
||||
[rulingMedical, rule, verifyMedical, showDate, t],
|
||||
);
|
||||
|
||||
const seaServiceColumns: AdvancedColumn<SeaServiceRecord>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
header: 'Seafarer',
|
||||
label: 'Seafarer',
|
||||
header: t('recordVerification.columns.seafarer', 'Seafarer'),
|
||||
label: t('recordVerification.columns.seafarer', 'Seafarer'),
|
||||
accessorKey: 'profile.firstName',
|
||||
cell: ({ row }) => (
|
||||
<div>
|
||||
@@ -393,8 +405,8 @@ export function MedicalVerificationPage() {
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Vessel',
|
||||
label: 'Vessel',
|
||||
header: t('recordVerification.columns.vessel', 'Vessel'),
|
||||
label: t('recordVerification.columns.vessel', 'Vessel'),
|
||||
accessorKey: 'vesselName',
|
||||
cell: ({ row }) => (
|
||||
<div>
|
||||
@@ -408,14 +420,14 @@ export function MedicalVerificationPage() {
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Rank',
|
||||
label: 'Rank',
|
||||
header: t('recordVerification.columns.rank', 'Rank'),
|
||||
label: t('recordVerification.columns.rank', 'Rank'),
|
||||
accessorKey: 'rank',
|
||||
cell: ({ row }) => <Text size="sm">{row.original.rank}</Text>,
|
||||
},
|
||||
{
|
||||
header: 'Period',
|
||||
label: 'Period',
|
||||
header: t('recordVerification.columns.period', 'Period'),
|
||||
label: t('recordVerification.columns.period', 'Period'),
|
||||
cell: ({ row }) => (
|
||||
<Text size="sm">
|
||||
{showDate(row.original.engagementDate)} → {showDate(row.original.dischargeDate)}
|
||||
@@ -424,7 +436,7 @@ export function MedicalVerificationPage() {
|
||||
},
|
||||
{
|
||||
header: '',
|
||||
label: 'Actions',
|
||||
label: t('recordVerification.columns.actions', 'Actions'),
|
||||
align: 'right',
|
||||
cell: ({ row }) => (
|
||||
<Group gap="xs" justify="flex-end" wrap="nowrap">
|
||||
@@ -437,11 +449,14 @@ export function MedicalVerificationPage() {
|
||||
setAttachmentModal({
|
||||
ownerType: 'SEA_SERVICE_RECORD',
|
||||
ownerId: row.original.id,
|
||||
title: `Evidence for ${ownerName(row.original.profile)} Sea Service Record`,
|
||||
title: t('recordVerification.evidenceTitleSeaService', {
|
||||
name: ownerName(row.original.profile),
|
||||
defaultValue: 'Evidence for {{name}} Sea Service Record',
|
||||
}),
|
||||
})
|
||||
}
|
||||
>
|
||||
Evidence
|
||||
{t('recordVerification.evidence', 'Evidence')}
|
||||
</Button>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
@@ -455,11 +470,11 @@ export function MedicalVerificationPage() {
|
||||
id: row.original.id,
|
||||
outcome: 'VERIFIED',
|
||||
}).unwrap(),
|
||||
'Sea-service record verified',
|
||||
t('recordVerification.seaServiceVerified', 'Sea-service record verified'),
|
||||
)
|
||||
}
|
||||
>
|
||||
Verify
|
||||
{t('recordVerification.verify', 'Verify')}
|
||||
</Button>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
@@ -468,33 +483,40 @@ export function MedicalVerificationPage() {
|
||||
leftSection={<IconX size={14} />}
|
||||
onClick={() => setRejectSeaService(row.original)}
|
||||
>
|
||||
Reject
|
||||
{t('recordVerification.reject', 'Reject')}
|
||||
</Button>
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
],
|
||||
[rulingSeaService, rule, verifySeaService, showDate],
|
||||
[rulingSeaService, rule, verifySeaService, showDate, t],
|
||||
);
|
||||
|
||||
return (
|
||||
<Container size="xl" py="md">
|
||||
<Title order={3} mb={4}>
|
||||
Record verification
|
||||
{t('recordVerification.title', 'Record verification')}
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed" mb="md">
|
||||
Submitted sea-service records and medical certificates awaiting a
|
||||
ruling. Verified records are frozen; rejections return to the seafarer
|
||||
with your remark.
|
||||
{t(
|
||||
'recordVerification.subtitle',
|
||||
'Submitted sea-service records and medical certificates awaiting a ruling. Verified records are frozen; rejections return to the seafarer with your remark.',
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<Tabs defaultValue="medical" keepMounted={false}>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="medical" leftSection={<IconStethoscope size={16} />}>
|
||||
Medical ({pendingMedicalList.length})
|
||||
{t('recordVerification.tabs.medical', {
|
||||
count: pendingMedicalList.length,
|
||||
defaultValue: 'Medical ({{count}})',
|
||||
})}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="sea-service" leftSection={<IconAnchor size={16} />}>
|
||||
Sea Service ({pendingSeaServiceList.length})
|
||||
{t('recordVerification.tabs.seaService', {
|
||||
count: pendingSeaServiceList.length,
|
||||
defaultValue: 'Sea Service ({{count}})',
|
||||
})}
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
@@ -502,7 +524,10 @@ export function MedicalVerificationPage() {
|
||||
<AdvancedTable
|
||||
columns={medicalColumns}
|
||||
data={pagedMedical}
|
||||
tableName="Medical Verification"
|
||||
tableName={t('recordVerification.tabs.medical', {
|
||||
count: pendingMedicalList.length,
|
||||
defaultValue: 'Medical ({{count}})',
|
||||
})}
|
||||
itemCount={pendingMedicalList.length}
|
||||
pageIndex={medicalPage}
|
||||
onPageChange={setMedicalPage}
|
||||
@@ -510,7 +535,7 @@ export function MedicalVerificationPage() {
|
||||
onPageSizeChange={handleMedicalPageSizeChange}
|
||||
refresh={refetchMedical}
|
||||
isLoading={loadingMedical || fetchingMedical}
|
||||
emptyText="Nothing awaiting verification."
|
||||
emptyText={t('recordVerification.emptyText', 'Nothing awaiting verification.')}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
@@ -518,7 +543,10 @@ export function MedicalVerificationPage() {
|
||||
<AdvancedTable
|
||||
columns={seaServiceColumns}
|
||||
data={pagedSeaService}
|
||||
tableName="Sea Service Verification"
|
||||
tableName={t('recordVerification.tabs.seaService', {
|
||||
count: pendingSeaServiceList.length,
|
||||
defaultValue: 'Sea Service ({{count}})',
|
||||
})}
|
||||
itemCount={pendingSeaServiceList.length}
|
||||
pageIndex={seaServicePage}
|
||||
onPageChange={setSeaServicePage}
|
||||
@@ -526,7 +554,7 @@ export function MedicalVerificationPage() {
|
||||
onPageSizeChange={handleSeaServicePageSizeChange}
|
||||
refresh={refetchSeaService}
|
||||
isLoading={loadingSeaService || fetchingSeaService}
|
||||
emptyText="Nothing awaiting verification."
|
||||
emptyText={t('recordVerification.emptyText', 'Nothing awaiting verification.')}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
@@ -535,12 +563,12 @@ export function MedicalVerificationPage() {
|
||||
opened={Boolean(attachmentModal)}
|
||||
ownerType={attachmentModal?.ownerType ?? 'MEDICAL_CERTIFICATE'}
|
||||
ownerId={attachmentModal?.ownerId ?? null}
|
||||
title={attachmentModal?.title ?? 'Evidence Attachments'}
|
||||
title={attachmentModal?.title ?? t('recordVerification.evidenceModalTitle', 'Evidence Attachments')}
|
||||
onClose={() => setAttachmentModal(null)}
|
||||
/>
|
||||
|
||||
<RejectModal
|
||||
title="Reject medical certificate"
|
||||
title={t('recordVerification.rejectMedicalTitle', 'Reject medical certificate')}
|
||||
opened={Boolean(rejectMedical)}
|
||||
onClose={() => setRejectMedical(null)}
|
||||
loading={rulingMedical}
|
||||
@@ -553,13 +581,13 @@ export function MedicalVerificationPage() {
|
||||
outcome: 'REJECTED',
|
||||
remark,
|
||||
}).unwrap(),
|
||||
'Certificate rejected',
|
||||
t('recordVerification.certificateRejected', 'Certificate rejected'),
|
||||
);
|
||||
setRejectMedical(null);
|
||||
}}
|
||||
/>
|
||||
<RejectModal
|
||||
title="Reject sea-service record"
|
||||
title={t('recordVerification.rejectSeaServiceTitle', 'Reject sea-service record')}
|
||||
opened={Boolean(rejectSeaService)}
|
||||
onClose={() => setRejectSeaService(null)}
|
||||
loading={rulingSeaService}
|
||||
@@ -572,7 +600,7 @@ export function MedicalVerificationPage() {
|
||||
outcome: 'REJECTED',
|
||||
remark,
|
||||
}).unwrap(),
|
||||
'Sea-service record rejected',
|
||||
t('recordVerification.seaServiceRejected', 'Sea-service record rejected'),
|
||||
);
|
||||
setRejectSeaService(null);
|
||||
}}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
@@ -59,6 +60,7 @@ function feeText(amount: string | number | null, currency: string): string {
|
||||
}
|
||||
|
||||
export function PaymentConfigPage() {
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading, isFetching, error, refetch } =
|
||||
useGetLicenseTypesQuery();
|
||||
const { data: capabilities } = useGetPaymentCapabilitiesQuery();
|
||||
@@ -78,7 +80,7 @@ export function PaymentConfigPage() {
|
||||
<Alert
|
||||
color="red"
|
||||
icon={<IconAlertTriangle size={18} />}
|
||||
title="Could not load licence types"
|
||||
title={t('paymentConfig.loadError', 'Could not load licence types')}
|
||||
>
|
||||
<Text size="sm">{extractErrorMessage(error)}</Text>
|
||||
</Alert>
|
||||
@@ -92,7 +94,7 @@ export function PaymentConfigPage() {
|
||||
|
||||
const columns: AdvancedColumn<LicenseType>[] = [
|
||||
{
|
||||
header: 'Licence type',
|
||||
header: t('paymentConfig.columns.type', 'Licence type'),
|
||||
cell: ({ row }) => (
|
||||
<>
|
||||
<Text size="sm" fw={600}>
|
||||
@@ -105,7 +107,7 @@ export function PaymentConfigPage() {
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'New application',
|
||||
header: t('paymentConfig.columns.newApplication', 'New application'),
|
||||
cell: ({ row }) => (
|
||||
<Text size="sm" fw={500}>
|
||||
{feeText(row.original.feeNewApplication, row.original.feeCurrency)}
|
||||
@@ -113,7 +115,7 @@ export function PaymentConfigPage() {
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Renewal',
|
||||
header: t('paymentConfig.columns.renewal', 'Renewal'),
|
||||
cell: ({ row }) => {
|
||||
const type = row.original;
|
||||
if (type.feeNewApplication === null) {
|
||||
@@ -126,11 +128,16 @@ export function PaymentConfigPage() {
|
||||
}
|
||||
if (type.feeRenewal === null) {
|
||||
return (
|
||||
<Tooltip label="No separate renewal fee — renewal is charged at the new-application rate">
|
||||
<Tooltip
|
||||
label={t(
|
||||
'paymentConfig.renewalSameTooltip',
|
||||
'No separate renewal fee — renewal is charged at the new-application rate',
|
||||
)}
|
||||
>
|
||||
<Text size="sm" c="dimmed">
|
||||
{feeText(type.feeNewApplication, type.feeCurrency)}{' '}
|
||||
<Text span size="xs" c="dimmed">
|
||||
(same as new)
|
||||
{t('paymentConfig.renewalSameAsNew', '(same as new)')}
|
||||
</Text>
|
||||
</Text>
|
||||
</Tooltip>
|
||||
@@ -144,16 +151,21 @@ export function PaymentConfigPage() {
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Charged?',
|
||||
header: t('paymentConfig.columns.charged', 'Charged?'),
|
||||
cell: ({ row }) =>
|
||||
row.original.issuesCertificate ? (
|
||||
<Badge variant="light" color="teal" size="sm">
|
||||
On approval
|
||||
{t('paymentConfig.chargedOnApproval', 'On approval')}
|
||||
</Badge>
|
||||
) : (
|
||||
<Tooltip label="This licence type ends with an EMA decision and never reaches a payment stage">
|
||||
<Tooltip
|
||||
label={t(
|
||||
'paymentConfig.chargedNotChargedTooltip',
|
||||
'This licence type ends with an EMA decision and never reaches a payment stage',
|
||||
)}
|
||||
>
|
||||
<Badge variant="light" color="gray" size="sm">
|
||||
Not charged
|
||||
{t('paymentConfig.chargedNotCharged', 'Not charged')}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
),
|
||||
@@ -169,7 +181,7 @@ export function PaymentConfigPage() {
|
||||
leftSection={<IconEdit size={14} />}
|
||||
onClick={() => setEditing(row.original)}
|
||||
>
|
||||
Edit
|
||||
{t('paymentConfig.edit', 'Edit')}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
@@ -179,10 +191,12 @@ export function PaymentConfigPage() {
|
||||
<Stack gap="lg">
|
||||
<Group justify="space-between" align="flex-start">
|
||||
<div>
|
||||
<Title order={3}>Payment configuration</Title>
|
||||
<Title order={3}>{t('paymentConfig.title', 'Payment configuration')}</Title>
|
||||
<Text size="sm" c="dimmed" mt={4}>
|
||||
What each licence costs. Applicants are charged after approval, and
|
||||
the amount is fixed onto the application at that moment.
|
||||
{t(
|
||||
'paymentConfig.subtitle',
|
||||
'What each licence costs. Applicants are charged after approval, and the amount is fixed onto the application at that moment.',
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
<ThemeIcon size="xl" radius="md" variant="light">
|
||||
@@ -197,9 +211,10 @@ export function PaymentConfigPage() {
|
||||
icon={<IconInfoCircle size={18} />}
|
||||
>
|
||||
<Text size="sm">
|
||||
A change applies to applications approved from now on. Anything
|
||||
already approved keeps the amount it was quoted, so an edit here can
|
||||
never alter what an applicant has already been asked to pay.
|
||||
{t(
|
||||
'paymentConfig.changeNotice',
|
||||
'A change applies to applications approved from now on. Anything already approved keeps the amount it was quoted, so an edit here can never alter what an applicant has already been asked to pay.',
|
||||
)}
|
||||
</Text>
|
||||
</Alert>
|
||||
|
||||
@@ -229,6 +244,7 @@ export function PaymentConfigPage() {
|
||||
* them as editable fields would be a lie.
|
||||
*/
|
||||
function GatewayPanel({ bypassEnabled }: { bypassEnabled?: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Paper withBorder radius="md" p="md">
|
||||
<Group gap="xs" mb="xs">
|
||||
@@ -236,23 +252,31 @@ function GatewayPanel({ bypassEnabled }: { bypassEnabled?: boolean }) {
|
||||
<IconLock size={13} />
|
||||
</ThemeIcon>
|
||||
<Text fw={600} size="sm">
|
||||
Payment gateway
|
||||
{t('paymentConfig.gateway.title', 'Payment gateway')}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
Set by the payment service environment — not editable here.
|
||||
{t(
|
||||
'paymentConfig.gateway.subtitle',
|
||||
'Set by the payment service environment — not editable here.',
|
||||
)}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group gap="sm">
|
||||
<Badge variant="light">Telebirr</Badge>
|
||||
<Badge variant="light">{t('paymentConfig.gateway.provider', 'Telebirr')}</Badge>
|
||||
{bypassEnabled ? (
|
||||
<Tooltip label="ALLOW_PAYMENT_BYPASS is on, so an applicant can settle a fee without paying. It is refused in production.">
|
||||
<Tooltip
|
||||
label={t(
|
||||
'paymentConfig.gateway.bypassTooltip',
|
||||
'ALLOW_PAYMENT_BYPASS is on, so an applicant can settle a fee without paying. It is refused in production.',
|
||||
)}
|
||||
>
|
||||
<Badge variant="light" color="orange">
|
||||
Test bypass enabled
|
||||
{t('paymentConfig.gateway.bypassEnabled', 'Test bypass enabled')}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Badge variant="light" color="gray">
|
||||
Test bypass off
|
||||
{t('paymentConfig.gateway.bypassOff', 'Test bypass off')}
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
@@ -267,6 +291,7 @@ function FeeEditModal({
|
||||
licenseType: LicenseType | null;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [updateFees, { isLoading }] = useUpdateLicenseFeesMutation();
|
||||
const [newFee, setNewFee] = useState<number | ''>('');
|
||||
const [renewalFee, setRenewalFee] = useState<number | ''>('');
|
||||
@@ -294,11 +319,21 @@ function FeeEditModal({
|
||||
async function save() {
|
||||
if (!licenseType) return;
|
||||
if (chargeable && newFee === '') {
|
||||
notify.error('Enter a new-application fee, or turn off "carries a fee".');
|
||||
notify.error(
|
||||
t(
|
||||
'paymentConfig.modal.missingNewFee',
|
||||
'Enter a new-application fee, or turn off "carries a fee".',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (chargeable && !sameAsNew && renewalFee === '') {
|
||||
notify.error('Enter a renewal fee, or charge renewal at the same rate.');
|
||||
notify.error(
|
||||
t(
|
||||
'paymentConfig.modal.missingRenewalFee',
|
||||
'Enter a renewal fee, or charge renewal at the same rate.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -308,10 +343,17 @@ function FeeEditModal({
|
||||
feeRenewal: chargeable && !sameAsNew ? Number(renewalFee) : null,
|
||||
feeCurrency: currency.trim() || 'ETB',
|
||||
}).unwrap();
|
||||
notify.success(`${localized(licenseType.name)} fees updated.`);
|
||||
notify.success(
|
||||
t('paymentConfig.modal.updated', {
|
||||
type: localized(licenseType.name),
|
||||
defaultValue: '{{type}} fees updated.',
|
||||
}),
|
||||
);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
notify.error(extractErrorMessage(err, 'Could not save the fees'));
|
||||
notify.error(
|
||||
extractErrorMessage(err, t('paymentConfig.modal.saveFailed', 'Could not save the fees')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,7 +361,14 @@ function FeeEditModal({
|
||||
<Modal
|
||||
opened={!!licenseType}
|
||||
onClose={onClose}
|
||||
title={licenseType ? `Fees — ${localized(licenseType.name)}` : ''}
|
||||
title={
|
||||
licenseType
|
||||
? t('paymentConfig.modal.title', {
|
||||
type: localized(licenseType.name),
|
||||
defaultValue: 'Fees — {{type}}',
|
||||
})
|
||||
: ''
|
||||
}
|
||||
centered
|
||||
radius="lg"
|
||||
>
|
||||
@@ -332,9 +381,10 @@ function FeeEditModal({
|
||||
icon={<IconInfoCircle size={16} />}
|
||||
>
|
||||
<Text size="sm">
|
||||
This licence type concludes with an EMA decision and never
|
||||
reaches a payment stage, so a fee set here stays unused until
|
||||
that changes.
|
||||
{t(
|
||||
'paymentConfig.modal.noPaymentStage',
|
||||
'This licence type concludes with an EMA decision and never reaches a payment stage, so a fee set here stays unused until that changes.',
|
||||
)}
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -342,14 +392,17 @@ function FeeEditModal({
|
||||
<Switch
|
||||
checked={chargeable}
|
||||
onChange={(e) => setChargeable(e.currentTarget.checked)}
|
||||
label="This licence carries a fee"
|
||||
description="Turn off for licence types applicants are never charged for."
|
||||
label={t('paymentConfig.modal.chargeableLabel', 'This licence carries a fee')}
|
||||
description={t(
|
||||
'paymentConfig.modal.chargeableDescription',
|
||||
'Turn off for licence types applicants are never charged for.',
|
||||
)}
|
||||
/>
|
||||
|
||||
{chargeable && (
|
||||
<>
|
||||
<NumberInput
|
||||
label="New application fee"
|
||||
label={t('paymentConfig.modal.newFeeLabel', 'New application fee')}
|
||||
value={newFee}
|
||||
onChange={(v) => setNewFee(v === '' ? '' : Number(v))}
|
||||
min={0}
|
||||
@@ -362,13 +415,16 @@ function FeeEditModal({
|
||||
<Switch
|
||||
checked={sameAsNew}
|
||||
onChange={(e) => setSameAsNew(e.currentTarget.checked)}
|
||||
label="Charge renewal at the same rate"
|
||||
description="Turn off to set a separate renewal fee."
|
||||
label={t('paymentConfig.modal.sameRateLabel', 'Charge renewal at the same rate')}
|
||||
description={t(
|
||||
'paymentConfig.modal.sameRateDescription',
|
||||
'Turn off to set a separate renewal fee.',
|
||||
)}
|
||||
/>
|
||||
|
||||
{!sameAsNew && (
|
||||
<NumberInput
|
||||
label="Renewal fee"
|
||||
label={t('paymentConfig.modal.renewalFeeLabel', 'Renewal fee')}
|
||||
value={renewalFee}
|
||||
onChange={(v) => setRenewalFee(v === '' ? '' : Number(v))}
|
||||
min={0}
|
||||
@@ -380,7 +436,7 @@ function FeeEditModal({
|
||||
)}
|
||||
|
||||
<TextInput
|
||||
label="Currency"
|
||||
label={t('paymentConfig.modal.currencyLabel', 'Currency')}
|
||||
value={currency}
|
||||
onChange={(e) =>
|
||||
setCurrency(e.currentTarget.value.toUpperCase())
|
||||
@@ -392,10 +448,10 @@ function FeeEditModal({
|
||||
|
||||
<ModalFooter mt="xs">
|
||||
<Button variant="default" onClick={onClose} disabled={isLoading}>
|
||||
Cancel
|
||||
{t('paymentConfig.modal.cancel', 'Cancel')}
|
||||
</Button>
|
||||
<Button onClick={save} loading={isLoading}>
|
||||
Save fees
|
||||
{t('paymentConfig.modal.save', 'Save fees')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Stack>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -77,6 +78,7 @@ function SeafarerDetailDrawer({
|
||||
profile: ProfileRow | null;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const profileId = profile?.id ?? '';
|
||||
const { data: seaService, isLoading: loadingSea } =
|
||||
useGetSeaServiceForProfileQuery(profileId, { skip: !profileId });
|
||||
@@ -103,7 +105,7 @@ function SeafarerDetailDrawer({
|
||||
<Group gap="xl">
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase">
|
||||
Seafarer number
|
||||
{t('seafarerRegistry.drawer.seafarerNumber', 'Seafarer number')}
|
||||
</Text>
|
||||
<Text fw={700} ff="monospace">
|
||||
{profile.seafarerNumber ?? '—'}
|
||||
@@ -111,41 +113,49 @@ function SeafarerDetailDrawer({
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase">
|
||||
Department
|
||||
{t('seafarerRegistry.drawer.department', 'Department')}
|
||||
</Text>
|
||||
<Text fw={600}>
|
||||
{profile.seafarerDepartment
|
||||
? DEPARTMENT_LABELS[profile.seafarerDepartment] ??
|
||||
profile.seafarerDepartment
|
||||
? t(
|
||||
`seafarerRegistry.departments.${profile.seafarerDepartment}`,
|
||||
DEPARTMENT_LABELS[profile.seafarerDepartment] ??
|
||||
profile.seafarerDepartment,
|
||||
)
|
||||
: '—'}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase">
|
||||
Status
|
||||
{t('seafarerRegistry.drawer.status', 'Status')}
|
||||
</Text>
|
||||
<Badge
|
||||
color={
|
||||
SEAFARER_STATUS_COLORS[profile.seafarerStatus ?? ''] ?? 'gray'
|
||||
}
|
||||
>
|
||||
{profile.seafarerStatus ?? 'NOT REGISTERED'}
|
||||
{profile.seafarerStatus
|
||||
? t(`seafarerRegistry.status.${profile.seafarerStatus}`, profile.seafarerStatus)
|
||||
: t('seafarerRegistry.drawer.notRegistered', 'NOT REGISTERED')}
|
||||
</Badge>
|
||||
</div>
|
||||
</Group>
|
||||
{profile.seafarerStatusReason && (
|
||||
<Text size="sm" c="dimmed">
|
||||
Status reason: {profile.seafarerStatusReason}
|
||||
{t('seafarerRegistry.drawer.statusReason', {
|
||||
reason: profile.seafarerStatusReason,
|
||||
defaultValue: 'Status reason: {{reason}}',
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Tabs defaultValue="sea-service" keepMounted={false}>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="sea-service" leftSection={<IconAnchor size={14} />}>
|
||||
Sea Service
|
||||
{t('seafarerRegistry.drawer.seaServiceTab', 'Sea Service')}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="medical" leftSection={<IconStethoscope size={14} />}>
|
||||
Medical
|
||||
{t('seafarerRegistry.drawer.medicalTab', 'Medical')}
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
@@ -154,16 +164,16 @@ function SeafarerDetailDrawer({
|
||||
<Loader size="sm" />
|
||||
) : (seaService ?? []).length === 0 ? (
|
||||
<Text size="sm" c="dimmed">
|
||||
No sea-service records.
|
||||
{t('seafarerRegistry.drawer.noSeaService', 'No sea-service records.')}
|
||||
</Text>
|
||||
) : (
|
||||
<Table striped>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Vessel</Table.Th>
|
||||
<Table.Th>Rank</Table.Th>
|
||||
<Table.Th>Period</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>{t('seafarerRegistry.drawer.vessel', 'Vessel')}</Table.Th>
|
||||
<Table.Th>{t('seafarerRegistry.drawer.rank', 'Rank')}</Table.Th>
|
||||
<Table.Th>{t('seafarerRegistry.drawer.period', 'Period')}</Table.Th>
|
||||
<Table.Th>{t('seafarerRegistry.drawer.status', 'Status')}</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
@@ -173,7 +183,10 @@ function SeafarerDetailDrawer({
|
||||
{record.vesselName}
|
||||
{record.imoNumber && (
|
||||
<Text size="xs" c="dimmed">
|
||||
IMO {record.imoNumber}
|
||||
{t('seafarerRegistry.drawer.imoPrefix', {
|
||||
number: record.imoNumber,
|
||||
defaultValue: 'IMO {{number}}',
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
</Table.Td>
|
||||
@@ -183,7 +196,7 @@ function SeafarerDetailDrawer({
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge size="sm" color={RECORD_STATUS_COLORS[record.status]}>
|
||||
{record.status}
|
||||
{t(`seafarerRegistry.recordStatus.${record.status}`, record.status)}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
@@ -198,16 +211,16 @@ function SeafarerDetailDrawer({
|
||||
<Loader size="sm" />
|
||||
) : (medical ?? []).length === 0 ? (
|
||||
<Text size="sm" c="dimmed">
|
||||
No medical certificates.
|
||||
{t('seafarerRegistry.drawer.noMedical', 'No medical certificates.')}
|
||||
</Text>
|
||||
) : (
|
||||
<Table striped>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Issuer</Table.Th>
|
||||
<Table.Th>Validity</Table.Th>
|
||||
<Table.Th>Fitness</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>{t('seafarerRegistry.drawer.issuer', 'Issuer')}</Table.Th>
|
||||
<Table.Th>{t('seafarerRegistry.drawer.validity', 'Validity')}</Table.Th>
|
||||
<Table.Th>{t('seafarerRegistry.drawer.fitness', 'Fitness')}</Table.Th>
|
||||
<Table.Th>{t('seafarerRegistry.drawer.status', 'Status')}</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
@@ -223,7 +236,7 @@ function SeafarerDetailDrawer({
|
||||
size="sm"
|
||||
color={RECORD_STATUS_COLORS[certificate.status]}
|
||||
>
|
||||
{certificate.status}
|
||||
{t(`seafarerRegistry.recordStatus.${certificate.status}`, certificate.status)}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
@@ -249,6 +262,7 @@ function StatusModal({
|
||||
onClose: () => void;
|
||||
onDone: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [status, setStatus] = useState<string | null>(null);
|
||||
const [reason, setReason] = useState('');
|
||||
const [updateStatus, { isLoading }] = useUpdateSeafarerStatusMutation();
|
||||
@@ -261,11 +275,13 @@ function StatusModal({
|
||||
status: status as 'ACTIVE' | 'INACTIVE' | 'SUSPENDED' | 'PENDING',
|
||||
reason,
|
||||
}).unwrap();
|
||||
notify.success('Seafarer status updated');
|
||||
notify.success(t('seafarerRegistry.modal.updated', 'Seafarer status updated'));
|
||||
onClose();
|
||||
onDone();
|
||||
} catch (error) {
|
||||
notify.error(extractErrorMessage(error, 'Could not update the status'));
|
||||
notify.error(
|
||||
extractErrorMessage(error, t('seafarerRegistry.modal.updateFailed', 'Could not update the status')),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -273,27 +289,33 @@ function StatusModal({
|
||||
<Modal
|
||||
opened={Boolean(profile)}
|
||||
onClose={onClose}
|
||||
title="Change seafarer status"
|
||||
title={t('seafarerRegistry.modal.title', 'Change seafarer status')}
|
||||
centered
|
||||
>
|
||||
<Stack>
|
||||
<Text size="sm" c="dimmed">
|
||||
{profile?.seafarerNumber} — currently {profile?.seafarerStatus}. The
|
||||
reason is recorded and visible to the seafarer.
|
||||
{t('seafarerRegistry.modal.body', {
|
||||
number: profile?.seafarerNumber,
|
||||
status: profile?.seafarerStatus
|
||||
? t(`seafarerRegistry.status.${profile.seafarerStatus}`, profile.seafarerStatus)
|
||||
: profile?.seafarerStatus,
|
||||
defaultValue:
|
||||
'{{number}} — currently {{status}}. The reason is recorded and visible to the seafarer.',
|
||||
})}
|
||||
</Text>
|
||||
<Select
|
||||
label="New status"
|
||||
label={t('seafarerRegistry.modal.newStatus', 'New status')}
|
||||
required
|
||||
data={[
|
||||
{ value: 'SUSPENDED', label: 'Suspend' },
|
||||
{ value: 'INACTIVE', label: 'Close' },
|
||||
{ value: 'ACTIVE', label: 'Reinstate' },
|
||||
{ value: 'SUSPENDED', label: t('seafarerRegistry.modal.suspend', 'Suspend') },
|
||||
{ value: 'INACTIVE', label: t('seafarerRegistry.modal.close', 'Close') },
|
||||
{ value: 'ACTIVE', label: t('seafarerRegistry.modal.reinstate', 'Reinstate') },
|
||||
].filter((o) => o.value !== profile?.seafarerStatus)}
|
||||
value={status}
|
||||
onChange={setStatus}
|
||||
/>
|
||||
<Textarea
|
||||
label="Reason"
|
||||
label={t('seafarerRegistry.modal.reason', 'Reason')}
|
||||
required
|
||||
minRows={2}
|
||||
value={reason}
|
||||
@@ -301,7 +323,7 @@ function StatusModal({
|
||||
/>
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={onClose}>
|
||||
Cancel
|
||||
{t('seafarerRegistry.modal.cancel', 'Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
color={status === 'ACTIVE' ? 'green' : 'orange'}
|
||||
@@ -309,7 +331,7 @@ function StatusModal({
|
||||
loading={isLoading}
|
||||
onClick={submit}
|
||||
>
|
||||
Confirm
|
||||
{t('seafarerRegistry.modal.confirm', 'Confirm')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
@@ -325,6 +347,7 @@ function StatusModal({
|
||||
* register — numbers, departments, statuses, and each seafarer's records.
|
||||
*/
|
||||
export function SeafarerRegistryPage() {
|
||||
const { t } = useTranslation();
|
||||
const [search, setSearch] = useState('');
|
||||
const [detail, setDetail] = useState<ProfileRow | null>(null);
|
||||
const [statusTarget, setStatusTarget] = useState<ProfileRow | null>(null);
|
||||
@@ -355,7 +378,7 @@ export function SeafarerRegistryPage() {
|
||||
|
||||
const columns: AdvancedColumn<ProfileRow>[] = [
|
||||
{
|
||||
header: 'Name',
|
||||
header: t('seafarerRegistry.columns.name', 'Name'),
|
||||
cell: ({ row }) => (
|
||||
<Text
|
||||
size="sm"
|
||||
@@ -368,53 +391,58 @@ export function SeafarerRegistryPage() {
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Seafarer №',
|
||||
header: t('seafarerRegistry.columns.number', 'Seafarer №'),
|
||||
cell: ({ row }) => <Text size="sm" ff="monospace">{row.original.seafarerNumber ?? '—'}</Text>,
|
||||
},
|
||||
{
|
||||
header: 'Department',
|
||||
header: t('seafarerRegistry.columns.department', 'Department'),
|
||||
cell: ({ row }) => (
|
||||
<Text size="sm">
|
||||
{row.original.seafarerDepartment
|
||||
? DEPARTMENT_LABELS[row.original.seafarerDepartment] ?? row.original.seafarerDepartment
|
||||
? t(
|
||||
`seafarerRegistry.departments.${row.original.seafarerDepartment}`,
|
||||
DEPARTMENT_LABELS[row.original.seafarerDepartment] ?? row.original.seafarerDepartment,
|
||||
)
|
||||
: '—'}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'ID number',
|
||||
header: t('seafarerRegistry.columns.idNumber', 'ID number'),
|
||||
cell: ({ row }) => <Text size="sm" c="dimmed">{row.original.address?.idNumber ?? '—'}</Text>,
|
||||
},
|
||||
{
|
||||
header: 'Phone',
|
||||
header: t('seafarerRegistry.columns.phone', 'Phone'),
|
||||
cell: ({ row }) => <Text size="sm" c="dimmed">{row.original.address?.primaryPhoneNumber ?? '—'}</Text>,
|
||||
},
|
||||
{
|
||||
header: 'Status',
|
||||
header: t('seafarerRegistry.columns.status', 'Status'),
|
||||
cell: ({ row }) =>
|
||||
row.original.seafarerNumber ? (
|
||||
<Badge size="sm" variant="light" color={SEAFARER_STATUS_COLORS[row.original.seafarerStatus ?? ''] ?? 'gray'}>
|
||||
{row.original.seafarerStatus}
|
||||
{t(`seafarerRegistry.status.${row.original.seafarerStatus}`, row.original.seafarerStatus ?? '')}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge size="sm" variant="light" color={row.original.isComplete ? 'teal' : 'gray'}>
|
||||
{row.original.isComplete ? 'Not registered' : 'Incomplete'}
|
||||
{row.original.isComplete
|
||||
? t('seafarerRegistry.notRegistered', 'Not registered')
|
||||
: t('seafarerRegistry.incomplete', 'Incomplete')}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: '',
|
||||
label: 'Actions',
|
||||
label: t('seafarerRegistry.columns.actions', 'Actions'),
|
||||
cell: ({ row }) =>
|
||||
row.original.seafarerNumber ? (
|
||||
<Tooltip label="Suspend / reinstate / close">
|
||||
<Tooltip label={t('seafarerRegistry.statusActionTooltip', 'Suspend / reinstate / close')}>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="subtle"
|
||||
leftSection={<IconShieldCog size={14} />}
|
||||
onClick={() => setStatusTarget(row.original)}
|
||||
>
|
||||
Status
|
||||
{t('seafarerRegistry.statusAction', 'Status')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : null,
|
||||
@@ -425,13 +453,16 @@ export function SeafarerRegistryPage() {
|
||||
<Container size="xl" py="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<div>
|
||||
<Title order={3}>Seafarer registry</Title>
|
||||
<Title order={3}>{t('seafarerRegistry.title', 'Seafarer registry')}</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
{data?.total ?? 0} profile{(data?.total ?? 0) === 1 ? '' : 's'}
|
||||
{t('seafarerRegistry.profileCount', {
|
||||
count: data?.total ?? 0,
|
||||
defaultValue: '{{count}} profile(s)',
|
||||
})}
|
||||
</Text>
|
||||
</div>
|
||||
<TextInput
|
||||
placeholder="Name, ID or seafarer number"
|
||||
placeholder={t('seafarerRegistry.searchPlaceholder', 'Name, ID or seafarer number')}
|
||||
leftSection={<IconSearch size={14} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
@@ -443,7 +474,7 @@ export function SeafarerRegistryPage() {
|
||||
<AdvancedTable
|
||||
columns={columns}
|
||||
data={page.rows}
|
||||
tableName="Seafarer registry"
|
||||
tableName={t('seafarerRegistry.title', 'Seafarer registry')}
|
||||
itemCount={page.itemCount}
|
||||
pageIndex={page.pageIndex}
|
||||
onPageChange={setPageIndex}
|
||||
@@ -451,7 +482,11 @@ export function SeafarerRegistryPage() {
|
||||
onPageSizeChange={setPageSize}
|
||||
refresh={refetch}
|
||||
isLoading={isLoading}
|
||||
emptyText={search ? 'No profiles match that search.' : 'No seafarers registered yet.'}
|
||||
emptyText={
|
||||
search
|
||||
? t('seafarerRegistry.emptySearch', 'No profiles match that search.')
|
||||
: t('seafarerRegistry.emptyNone', 'No seafarers registered yet.')
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user