feat: add inspection, issuance, and renewal toggles to certificate requirement settings

This commit is contained in:
estifanos
2026-08-31 07:01:09 +00:00
parent e25a786139
commit 4eb9fe1ec2
4 changed files with 93 additions and 24 deletions

View File

@@ -33,6 +33,9 @@ interface Draft {
completionEffect: CompletionEffect | null;
certificateCategory: CertificateCategory | null;
requiresExamination: boolean;
inspectionRequired: boolean;
issuesCertificate: boolean;
renewalEnabled: boolean;
requiresSeafarerRegistration: boolean;
requiresValidMedical: boolean;
minSeaTimeDays: number | null;
@@ -58,6 +61,9 @@ function toDraft(licenseType: LicenseType): Draft {
completionEffect: licenseType.completionEffect ?? null,
certificateCategory: licenseType.certificateCategory ?? null,
requiresExamination: licenseType.requiresExamination ?? false,
inspectionRequired: licenseType.inspectionRequired ?? true,
issuesCertificate: licenseType.issuesCertificate ?? true,
renewalEnabled: licenseType.renewalEnabled ?? true,
requiresSeafarerRegistration:
licenseType.requiresSeafarerRegistration ?? false,
requiresValidMedical: licenseType.requiresValidMedical ?? false,
@@ -182,6 +188,17 @@ export function BehaviorTab({ licenseType }: { licenseType: LicenseType }) {
disabled={!canEdit}
/>
<Switch
checked={draft.issuesCertificate}
onChange={(e) => set('issuesCertificate', e.currentTarget.checked)}
label={t('certReq.behavior.issuesCertificate', 'Issues a certificate')}
description={t(
'certReq.behavior.issuesCertificateHint',
'Turn off for a type that ends with an EMA decision and never reaches a payment stage. Turning it back on requires a new-application fee to be set, or approved applicants would be asked for a fee that does not exist.',
)}
disabled={!canEdit}
/>
<Select
label={t('certReq.behavior.serviceKind', 'Service kind')}
description={t(
@@ -315,33 +332,53 @@ export function BehaviorTab({ licenseType }: { licenseType: LicenseType }) {
/>
</Group>
<NumberInput
label={t('certReq.behavior.renewalWindow', 'Renewal opens (days before expiry)')}
value={draft.renewalWindowDays}
onChange={(v) => set('renewalWindowDays', typeof v === 'number' ? v : draft.renewalWindowDays)}
min={1}
max={365}
allowNegative={false}
<Switch
checked={draft.renewalEnabled}
onChange={(e) => set('renewalEnabled', e.currentTarget.checked)}
label={t('certReq.behavior.renewalEnabled', 'Holders may renew this licence')}
description={t(
'certReq.behavior.renewalEnabledHint',
'Off for one-off issues — a registration that does not lapse, or a waiver written for a single shipment.',
)}
disabled={!canEdit}
/>
<MultiSelect
label={t('certReq.behavior.reminders', 'Expiry reminders (days before)')}
description={t(
'certReq.behavior.remindersHint',
'The holder is reminded at each of these offsets.',
)}
data={REMINDER_OFFSETS}
value={draft.expiryReminderDays.map(String)}
onChange={(values) =>
set(
'expiryReminderDays',
values.map(Number).sort((a, b) => b - a),
)
}
disabled={!canEdit}
clearable
/>
{/* The window and the reminders are both measured against an expiry a
non-renewing licence never reaches, so they are hidden rather than
shown as settings that quietly do nothing. */}
{draft.renewalEnabled && (
<>
<NumberInput
label={t('certReq.behavior.renewalWindow', 'Renewal opens (days before expiry)')}
value={draft.renewalWindowDays}
onChange={(v) =>
set('renewalWindowDays', typeof v === 'number' ? v : draft.renewalWindowDays)
}
min={1}
max={365}
allowNegative={false}
disabled={!canEdit}
/>
<MultiSelect
label={t('certReq.behavior.reminders', 'Expiry reminders (days before)')}
description={t(
'certReq.behavior.remindersHint',
'The holder is reminded at each of these offsets.',
)}
data={REMINDER_OFFSETS}
value={draft.expiryReminderDays.map(String)}
onChange={(values) =>
set(
'expiryReminderDays',
values.map(Number).sort((a, b) => b - a),
)
}
disabled={!canEdit}
clearable
/>
</>
)}
</Section>
<Section title={t('certReq.behavior.applicantRules', 'Applicant rules')}>
@@ -367,6 +404,17 @@ export function BehaviorTab({ licenseType }: { licenseType: LicenseType }) {
disabled={!canEdit}
/>
<Switch
checked={draft.inspectionRequired}
onChange={(e) => set('inspectionRequired', e.currentTarget.checked)}
label={t('certReq.behavior.inspectionRequired', 'Requires a physical inspection')}
description={t(
'certReq.behavior.inspectionRequiredHint',
'An officer cannot approve until an inspection has been recorded and passed. Safe to change with applications in progress — an officer can still assign an inspector to a file already under evaluation.',
)}
disabled={!canEdit}
/>
<Switch
checked={draft.requiresIssuanceScheduling}
onChange={(e) => set('requiresIssuanceScheduling', e.currentTarget.checked)}

View File

@@ -1292,6 +1292,15 @@ export const am: Translations = {
requiresExamination: "ፈተና ያስፈልገዋል",
requiresExaminationHint:
"ማመልከቻውን በብቁነት፣ ከዚያ በፈተና፣ ከዚያ በምስክር ወረቀት ሂደት ያሳልፋል። ሦስቱን የደረጃ ክፍያዎች በክፍያ ቅንብር ገጽ ላይ ያስቀምጡ።",
issuesCertificate: "የምስክር ወረቀት ይሰጣል",
issuesCertificateHint:
"በኢማ ውሳኔ ተጠናቆ ወደ ክፍያ ደረጃ ለማይደርስ ዓይነት ያጥፉ። እንደገና ማብራት የአዲስ ማመልከቻ ክፍያ እንዲዘጋጅ ይጠይቃል፤ አለበለዚያ የጸደቁ አመልካቾች የሌለ ክፍያ እንዲከፍሉ ይጠየቃሉ።",
inspectionRequired: "አካላዊ ምርመራ ያስፈልገዋል",
inspectionRequiredHint:
"ምርመራ ተመዝግቦ እስኪያልፍ ድረስ ኃላፊው ማጽደቅ አይችልም። ማመልከቻዎች በሂደት ላይ እያሉ መቀየር አስተማማኝ ነው — ኃላፊው በግምገማ ላይ ላለ ማመልከቻ አሁንም መርማሪ መመደብ ይችላል።",
renewalEnabled: "ባለቤቶች ይህንን ፈቃድ ማደስ ይችላሉ",
renewalEnabledHint:
"አንድ ጊዜ ብቻ ለሚሰጡ ያጥፉ — ጊዜው ለማያልፍ ምዝገባ፣ ወይም ለአንድ ጭነት ለተጻፈ ነፃ ፈቃድ።",
serviceKind: "የአገልግሎት ዓይነት",
serviceKindHint: "ለዝርዝር ምድብ ብቻ — ምንም የሥራ ሂደት በእሱ ላይ አይመሠረትም።",
license: "ፈቃድ",

View File

@@ -1297,6 +1297,15 @@ export const en = {
requiresExamination: 'Requires an examination',
requiresExaminationHint:
'Routes the application through eligibility, then exam, then certificate. Set the three stage fees on the payment configuration screen.',
issuesCertificate: 'Issues a certificate',
issuesCertificateHint:
'Turn off for a type that ends with an EMA decision and never reaches a payment stage. Turning it back on requires a new-application fee to be set, or approved applicants would be asked for a fee that does not exist.',
inspectionRequired: 'Requires a physical inspection',
inspectionRequiredHint:
'An officer cannot approve until an inspection has been recorded and passed. Safe to change with applications in progress — an officer can still assign an inspector to a file already under evaluation.',
renewalEnabled: 'Holders may renew this licence',
renewalEnabledHint:
'Off for one-off issues — a registration that does not lapse, or a waiver written for a single shipment.',
serviceKind: 'Service kind',
serviceKindHint: 'Catalogue classification only — no workflow depends on it.',
license: 'Licence',

View File

@@ -223,6 +223,9 @@ export const licensingApi = baseApi
completionEffect?: CompletionEffect | null;
certificateCategory?: CertificateCategory | null;
requiresExamination?: boolean;
inspectionRequired?: boolean;
issuesCertificate?: boolean;
renewalEnabled?: boolean;
requiresSeafarerRegistration?: boolean;
requiresValidMedical?: boolean;
minSeaTimeDays?: number | null;