feat: add non-expiring license support to backoffice and portal UI with updated i18n and selective form patching

This commit is contained in:
estifanos
2026-09-01 05:57:54 +00:00
parent 41995842d7
commit 32ad5124f1
7 changed files with 66 additions and 8 deletions

View File

@@ -84,9 +84,11 @@ export function DesignerToolbar({
<Text size="sm" fw={600}>
{validityDays != null
? t('designer.validityDays', '{{count}} days', { count: validityDays })
: t('designer.validityMonths', '{{count}} months', {
count: validityMonths,
})}
: validityMonths
? t('designer.validityMonths', '{{count}} months', {
count: validityMonths,
})
: t('designer.validityNone', 'Does not expire')}
</Text>
<Text size="xs" c="dimmed">
{t('designer.validityEditedOn', '— set on Certificate Requirements → Behaviour')}

View File

@@ -72,9 +72,13 @@ function toDraft(licenseType: LicenseType): Draft {
licenseType.capitalThreshold == null
? null
: Number(licenseType.capitalThreshold),
// Zero is a real stored state for `validityMonths` (a type that issues
// nothing with an expiry) and is kept. Zero in the other two is not a
// policy anyone set — it is an unfilled column — and seeding a box with a
// value below its own floor only produces a save the server rejects.
validityMonths: licenseType.validityMonths ?? 12,
validityDays: licenseType.validityDays ?? null,
renewalWindowDays: licenseType.renewalWindowDays ?? 60,
validityDays: licenseType.validityDays || null,
renewalWindowDays: licenseType.renewalWindowDays || 60,
expiryReminderDays: licenseType.expiryReminderDays ?? [60, 30, 7],
requiresOperatorMode: licenseType.requiresOperatorMode ?? true,
allowMultipleOpenDrafts: licenseType.allowMultipleOpenDrafts ?? false,
@@ -128,9 +132,38 @@ export function BehaviorTab({ licenseType }: { licenseType: LicenseType }) {
// against an expiry that never arrives, so none of them are asked for.
const expires = draft.validityDays !== null || draft.validityMonths > 0;
// The server's floor for a stated term is 6 months, so no-expiry is a state
// this form can hold and edit around but cannot switch a type into. Offered
// only where it is already what the type is, rather than as an option whose
// save would be refused.
const noExpiryAvailable =
(licenseType.validityMonths ?? 12) === 0 && !licenseType.validityDays;
async function onSave() {
// Only the settings this form actually asked for. The endpoint patches, so
// an omitted field keeps its stored value — and the fields hidden above are
// hidden precisely because the type has no such policy, which the server
// stores as a zero its own validators then refuse (`validityMonths` has a
// floor of 6, `renewalWindowDays` of 1). Echoing those back is what made
// saving an ownership transfer fail outright.
const {
validityMonths,
validityDays,
renewalWindowDays,
expiryReminderDays,
...rest
} = draft;
const ok = await run(
() => save({ id: licenseType.id, ...draft }).unwrap(),
() =>
save({
id: licenseType.id,
...rest,
...(expires ? { validityMonths, validityDays } : {}),
...(expires && draft.renewalEnabled
? { renewalWindowDays, expiryReminderDays }
: {}),
}).unwrap(),
t('certReq.behavior.saved', 'Configuration saved.'),
);
if (ok) setDirty(false);
@@ -334,7 +367,14 @@ export function BehaviorTab({ licenseType }: { licenseType: LicenseType }) {
data={[
{ value: 'MONTHS', label: t('certReq.behavior.unitMonths', 'Months') },
{ value: 'DAYS', label: t('certReq.behavior.unitDays', 'Days') },
{ value: 'NONE', label: t('certReq.behavior.unitNone', 'Does not expire') },
...(noExpiryAvailable
? [
{
value: 'NONE',
label: t('certReq.behavior.unitNone', 'Does not expire'),
},
]
: []),
]}
value={
draft.validityDays !== null

View File

@@ -1253,6 +1253,7 @@ export const am: Translations = {
validityMonths_other: "{{count}} ወራት",
validityDays_one: "{{count}} ቀን",
validityDays_other: "{{count}} ቀናት",
validityNone: "ጊዜው አያልፍም",
validityEditedOn: "— በምስክር ወረቀት መስፈርቶች → ባህሪ ውስጥ ይዘጋጃል",
newVersion: "አዲስ ስሪት",
versions: "ስሪቶች",

View File

@@ -1260,6 +1260,7 @@ export const en = {
validityMonths_other: '{{count}} months',
validityDays_one: '{{count}} day',
validityDays_other: '{{count}} days',
validityNone: 'Does not expire',
validityEditedOn: '— set on Certificate Requirements → Behaviour',
newVersion: 'New version',
versions: 'Versions',

View File

@@ -322,8 +322,16 @@ function LicenseTypeCard({
</Tooltip>
)}
{type.issuesCertificate ? (
// A term in days wins over the months column, and a type with
// neither issues something that simply does not expire — a
// transfer, or any one-off record. Reading `validityMonths`
// alone badged those "0 months".
<Badge size="sm" variant="light" color="teal">
{t('licensing.catalogue.validityBadge', { months: type.validityMonths })}
{type.validityDays
? t('licensing.catalogue.validityDaysBadge', { count: type.validityDays })
: type.validityMonths
? t('licensing.catalogue.validityBadge', { months: type.validityMonths })
: t('licensing.catalogue.noExpiryBadge')}
</Badge>
) : (
<Tooltip label={t('licensing.catalogue.evaluationTooltip')}>

View File

@@ -884,6 +884,9 @@ export const am: Translations = {
capitalTooltip: 'በባንክ ደብዳቤ መረጋገጥ ያለበት ዝቅተኛ ካፒታል',
capitalBadge: 'ካፒታል {{amount}}',
validityBadge: '{{months}} ወራት',
validityDaysBadge_one: '{{count}} ቀን',
validityDaysBadge_other: '{{count}} ቀናት',
noExpiryBadge: 'ጊዜው አያልፍም',
evaluationTooltip: 'በምስክር ወረቀት ፈንታ በባለሥልጣኑ ውሳኔ የሚጠናቀቅ',
evaluationOnly: 'ግምገማ ብቻ',
startApplication: 'ማመልከቻ ጀምር',

View File

@@ -886,6 +886,9 @@ export const en = {
capitalTooltip: 'Minimum capital that must be evidenced by a bank letter',
capitalBadge: 'Capital {{amount}}',
validityBadge: '{{months}} months',
validityDaysBadge_one: '{{count}} day',
validityDaysBadge_other: '{{count}} days',
noExpiryBadge: 'No expiry',
evaluationTooltip: 'Concludes with an EMA decision rather than a certificate',
evaluationOnly: 'Evaluation only',
startApplication: 'Start application',