feat: add configuration UI for license behavior and multi-stage examination fees

This commit is contained in:
estifanos
2026-08-29 06:34:33 +00:00
parent af3fad018a
commit 2e9083211f
8 changed files with 764 additions and 1 deletions

View File

@@ -179,9 +179,25 @@ function FeeEditModal({
// are real configuration states, not blank fields.
const [chargeable, setChargeable] = useState(true);
const [sameAsNew, setSameAsNew] = useState(true);
// The examined-certificate stages. Held the same way — null means "this stage
// is not charged", which is different from a blank box.
const [eligibilityFee, setEligibilityFee] = useState<number | ''>('');
const [examinationFee, setExaminationFee] = useState<number | ''>('');
const [certificateFee, setCertificateFee] = useState<number | ''>('');
const examined = licenseType?.requiresExamination ?? false;
useEffect(() => {
if (!licenseType) return;
setEligibilityFee(
licenseType.feeEligibility == null ? '' : Number(licenseType.feeEligibility),
);
setExaminationFee(
licenseType.feeExamination == null ? '' : Number(licenseType.feeExamination),
);
setCertificateFee(
licenseType.feeCertificate == null ? '' : Number(licenseType.feeCertificate),
);
setChargeable(licenseType.feeNewApplication !== null);
setNewFee(
licenseType.feeNewApplication === null
@@ -221,6 +237,14 @@ function FeeEditModal({
feeNewApplication: chargeable ? Number(newFee) : null,
feeRenewal: chargeable && !sameAsNew ? Number(renewalFee) : null,
feeCurrency: currency.trim() || 'ETB',
// Only sent for examined types; otherwise the columns stay untouched.
...(examined
? {
feeEligibility: eligibilityFee === '' ? null : Number(eligibilityFee),
feeExamination: examinationFee === '' ? null : Number(examinationFee),
feeCertificate: certificateFee === '' ? null : Number(certificateFee),
}
: {}),
}).unwrap();
notify.success(
t('paymentConfig.modal.updated', {
@@ -325,6 +349,68 @@ function FeeEditModal({
</>
)}
{examined && (
<>
<Alert
variant="light"
color="blue"
icon={<IconInfoCircle size={16} />}
>
<Text size="sm">
{t(
'paymentConfig.modal.examinedNotice',
'This certificate is earned by examination, so it is charged in three stages. Unlike the fees above, these are not fixed at approval — a change applies to candidates already part-way through. Clearing one while candidates are waiting to pay it will be refused.',
)}
</Text>
</Alert>
<NumberInput
label={t('paymentConfig.modal.eligibilityFeeLabel', 'Eligibility assessment fee')}
description={t(
'paymentConfig.modal.eligibilityFeeHint',
'Due on submission, before an officer reviews the application. Leave empty for no charge.',
)}
value={eligibilityFee}
onChange={(v) => setEligibilityFee(v === '' ? '' : Number(v))}
min={0}
max={9_999_999_999}
thousandSeparator=","
allowNegative={false}
decimalScale={2}
/>
<NumberInput
label={t('paymentConfig.modal.examinationFeeLabel', 'Examination fee')}
description={t(
'paymentConfig.modal.examinationFeeHint',
'Due once eligibility is approved, and again for a retake.',
)}
value={examinationFee}
onChange={(v) => setExaminationFee(v === '' ? '' : Number(v))}
min={0}
max={9_999_999_999}
thousandSeparator=","
allowNegative={false}
decimalScale={2}
/>
<NumberInput
label={t('paymentConfig.modal.certificateFeeLabel', 'Certificate fee')}
description={t(
'paymentConfig.modal.certificateFeeHint',
'Due after a pass, before the certificate is issued.',
)}
value={certificateFee}
onChange={(v) => setCertificateFee(v === '' ? '' : Number(v))}
min={0}
max={9_999_999_999}
thousandSeparator=","
allowNegative={false}
decimalScale={2}
/>
</>
)}
<ModalFooter mt="xs">
<Button variant="default" onClick={onClose} disabled={isLoading}>
{t('paymentConfig.modal.cancel', 'Cancel')}