From 90d1f6c462de0d3c85ad0c9560e72ac40ea5f199 Mon Sep 17 00:00:00 2001 From: estifanos Date: Wed, 2 Sep 2026 11:26:34 +0000 Subject: [PATCH] remove LicenseTypesTab component and associated creation logic from licensing configuration --- .../components/LicenseTypesTab.tsx | 457 ------------------ .../ConfigurationPage/LicenseTypesTab.tsx | 112 ++++- .../pages/ConfigurationPage/index.tsx | 9 +- apps/backoffice/src/app/i18n/locales/am.ts | 100 ++-- apps/backoffice/src/app/i18n/locales/en.ts | 102 ++-- .../lib/features/licensing/licensing-api.ts | 36 -- 6 files changed, 177 insertions(+), 639 deletions(-) delete mode 100644 apps/backoffice/src/app/features/configuration/components/LicenseTypesTab.tsx diff --git a/apps/backoffice/src/app/features/configuration/components/LicenseTypesTab.tsx b/apps/backoffice/src/app/features/configuration/components/LicenseTypesTab.tsx deleted file mode 100644 index acc686d96..000000000 --- a/apps/backoffice/src/app/features/configuration/components/LicenseTypesTab.tsx +++ /dev/null @@ -1,457 +0,0 @@ -import { useMemo, useState } from 'react'; -import { - Alert, - Anchor, - Badge, - Button, - Card, - Checkbox, - Group, - Modal, - NumberInput, - Select, - SimpleGrid, - Stack, - Switch, - Text, - TextInput, - Textarea, -} from '@mantine/core'; -import { useForm } from '@mantine/form'; -import { IconInfoCircle, IconPlus } from '@tabler/icons-react'; -import { useTranslation } from 'react-i18next'; -import { Link } from 'react-router-dom'; -import { - AdvancedTable, - ErrorState, - ModalFooter, - notify, - PageLoader, - useServerTable, - type AdvancedColumn, -} from '@ema-platform/ui'; -import { - extractErrorMessage, - useCreateLicenseTypeMutation, - useGetLicenseCategoriesQuery, - useGetLicenseTypesQuery, - useLocalized, - useUpdateLicenseStatusMutation, - type CreateLicenseTypeInput, - type FamilyKind, - type LicenseCategory, - type LicenseType, - type ServiceKind, - type WorkflowProfile, -} from '@ema-platform/api'; -import { LICENSE_PERMISSIONS, RequirePermission, usePermissions } from '@ema-platform/auth'; - -/** Upper snake case, as the server normalises and then requires. */ -const KEY_PATTERN = /^[A-Z][A-Z0-9_]*$/; - -interface FormValues { - key: string; - nameEn: string; - nameAm: string; - descEn: string; - descAm: string; - category: LicenseCategory | ''; - familyKind: FamilyKind; - serviceKind: ServiceKind; - workflowProfile: WorkflowProfile; - certificatePrefix: string; - feeNewApplication: number | ''; - feeCurrency: string; - validityMonths: number; - issuesCertificate: boolean; - renewalEnabled: boolean; - inspectionRequired: boolean; -} - -const INITIAL: FormValues = { - key: '', - nameEn: '', - nameAm: '', - descEn: '', - descAm: '', - category: '', - familyKind: 'LOGISTICS_LICENSE', - serviceKind: 'LICENSE', - workflowProfile: 'STANDARD', - certificatePrefix: '', - feeNewApplication: '', - feeCurrency: 'ETB', - validityMonths: 12, - issuesCertificate: true, - renewalEnabled: true, - inspectionRequired: true, -}; - -/** - * The catalogue of licence types, with the one thing no other screen offers: - * creating a new one, and switching one on or off. - * - * Deliberately thin. A type is created with only what it needs to exist and - * be classified; its form, document slots, fees and behaviour rules each - * have a dedicated screen, and this one links there rather than duplicating - * them. Deactivating hides the type from the portal catalogue without - * touching applications already in flight, which hold the type by id. - */ -export function LicenseTypesTab() { - const { t } = useTranslation(); - const localized = useLocalized(); - const { can } = usePermissions(); - - const { data, isLoading, isFetching, isError, error, refetch } = useGetLicenseTypesQuery(); - const { data: categoriesRes } = useGetLicenseCategoriesQuery(); - const [createType, { isLoading: isCreating }] = useCreateLicenseTypeMutation(); - const [updateStatus, { isLoading: isToggling }] = useUpdateLicenseStatusMutation(); - - const [showForm, setShowForm] = useState(false); - const [pendingToggle, setPendingToggle] = useState(null); - const { setPageIndex, pageSize, setPageSize, paginate } = useServerTable(); - - const types = useMemo( - () => [...(data?.items ?? [])].sort((a, b) => a.sortOrder - b.sortOrder || a.key.localeCompare(b.key)), - [data], - ); - - const categoryOptions = useMemo( - () => - [...(categoriesRes?.items ?? [])] - .sort((a, b) => a.sortOrder - b.sortOrder) - .map((c) => ({ value: c.key, label: localized(c.name) })), - [categoriesRes, localized], - ); - const categoryLabel = (key: string) => categoryOptions.find((c) => c.value === key)?.label ?? key; - - const familyOptions: { value: FamilyKind; label: string }[] = [ - { value: 'LOGISTICS_LICENSE', label: t('licenseTypeConfig.family.LOGISTICS_LICENSE', 'Logistics licence') }, - { value: 'CERTIFICATE', label: t('licenseTypeConfig.family.CERTIFICATE', 'Seafarer certificate') }, - { value: 'DOCUMENT', label: t('licenseTypeConfig.family.DOCUMENT', 'Identity / statutory document') }, - ]; - const serviceOptions: { value: ServiceKind; label: string }[] = [ - { value: 'LICENSE', label: t('licenseTypeConfig.service.LICENSE', 'Licence') }, - { value: 'REGISTRATION', label: t('licenseTypeConfig.service.REGISTRATION', 'Registration') }, - ]; - const workflowOptions: { value: WorkflowProfile; label: string }[] = [ - { value: 'STANDARD', label: t('licenseTypeConfig.workflow.STANDARD', 'Standard (review → evaluation → inspection → approval)') }, - { value: 'REGISTRATION', label: t('licenseTypeConfig.workflow.REGISTRATION', 'Registration (review → approval)') }, - ]; - - const form = useForm({ - initialValues: INITIAL, - transformValues: (v) => ({ ...v, key: v.key.trim().toUpperCase(), certificatePrefix: v.certificatePrefix.trim() }), - validate: { - key: (v) => { - const key = v.trim().toUpperCase(); - if (!key) return t('licenseTypeConfig.validation.keyRequired', 'Key is required'); - if (key.length > 64) return t('licenseTypeConfig.validation.keyTooLong', 'Key must be at most 64 characters'); - if (!KEY_PATTERN.test(key)) { - return t('licenseTypeConfig.validation.keyFormat', 'Use letters, digits and underscores, e.g. PORT_AGENT'); - } - if (types.some((lt) => lt.key === key)) { - return t('licenseTypeConfig.validation.keyTaken', 'A licence type with this key already exists'); - } - return null; - }, - nameEn: (v) => (v.trim() ? null : t('configuration.validation.nameEnRequired')), - nameAm: (v) => (v.trim() ? null : t('configuration.validation.nameAmRequired')), - certificatePrefix: (v) => { - const prefix = v.trim(); - if (!prefix) return t('licenseTypeConfig.validation.prefixRequired', 'Certificate prefix is required'); - if (prefix.length > 12) return t('licenseTypeConfig.validation.prefixTooLong', 'Prefix must be at most 12 characters'); - return null; - }, - feeCurrency: (v) => (v.trim().length > 8 ? t('licenseTypeConfig.validation.currencyTooLong', 'Use a short currency code') : null), - validityMonths: (v) => - v >= 6 && v <= 240 ? null : t('licenseTypeConfig.validation.validityRange', 'Validity must be between 6 and 240 months'), - }, - }); - - const closeForm = () => { - form.reset(); - setShowForm(false); - }; - - const submit = form.onSubmit(async (values) => { - const body: CreateLicenseTypeInput = { - key: values.key, - name: { en: values.nameEn.trim(), am: values.nameAm.trim() }, - certificatePrefix: values.certificatePrefix, - familyKind: values.familyKind, - serviceKind: values.serviceKind, - workflowProfile: values.workflowProfile, - feeCurrency: values.feeCurrency.trim() || 'ETB', - validityMonths: values.validityMonths, - issuesCertificate: values.issuesCertificate, - renewalEnabled: values.renewalEnabled, - inspectionRequired: values.inspectionRequired, - // The last row by default; the seeded order is EMA's and stays put. - sortOrder: types.length, - }; - if (values.descEn.trim() || values.descAm.trim()) { - body.description = { en: values.descEn.trim(), am: values.descAm.trim() }; - } - if (values.category) body.category = values.category; - if (values.feeNewApplication !== '') body.feeNewApplication = values.feeNewApplication; - - try { - await createType(body).unwrap(); - notify.success(t('licenseTypeConfig.created', 'Licence type created. Configure its form, documents and fees next.')); - closeForm(); - } catch (e) { - notify.error(extractErrorMessage(e, t('configuration.error'))); - } - }); - - const confirmToggle = async () => { - if (!pendingToggle) return; - const next = !pendingToggle.isActive; - try { - await updateStatus({ id: pendingToggle.id, isActive: next }).unwrap(); - notify.success( - next - ? t('licenseTypeConfig.activated', 'Licence type is now accepting applications') - : t('licenseTypeConfig.deactivated', 'Licence type is closed to new applications'), - ); - setPendingToggle(null); - } catch (e) { - notify.error(extractErrorMessage(e, t('configuration.error'))); - } - }; - - if (isError) { - return ( - refetch()} - /> - ); - } - if (isLoading) return ; - - const canToggle = can([LICENSE_PERMISSIONS.UPDATE_LICENSE_TYPE]); - - const columns: AdvancedColumn[] = [ - { header: t('configuration.name'), cell: ({ row }) => localized(row.original.name) }, - { header: t('configuration.key', 'Key'), cell: ({ row }) => {row.original.key} }, - { header: t('licenseTypeConfig.columns.category', 'Category'), cell: ({ row }) => categoryLabel(row.original.category) }, - { - header: t('licenseTypeConfig.columns.family', 'Family'), - cell: ({ row }) => familyOptions.find((f) => f.value === row.original.familyKind)?.label ?? row.original.familyKind, - }, - { header: t('licenseTypeConfig.columns.prefix', 'Prefix'), cell: ({ row }) => row.original.certificatePrefix }, - { - header: t('licenseTypeConfig.columns.status', 'Status'), - size: 110, - cell: ({ row }) => ( - - {row.original.isActive ? t('licenseTypeConfig.active', 'Active') : t('licenseTypeConfig.inactive', 'Inactive')} - - ), - }, - { - header: '', - label: t('licenseTypeConfig.columns.actions', 'Actions'), - size: 140, - cell: ({ row }) => ( - setPendingToggle(row.original)} - label={row.original.isActive ? t('licenseTypeConfig.deactivate', 'Deactivate') : t('licenseTypeConfig.activate', 'Activate')} - aria-label={t('licenseTypeConfig.toggleAria', 'Toggle whether this licence type accepts applications')} - /> - ), - }, - ]; - - const page = paginate(types); - - return ( - - }> - - {t( - 'licenseTypeConfig.notice', - 'A new type starts empty. After creating it, set up its form and document slots under Certificate Requirements, and its fees under Payment Configuration.', - )}{' '} - - {t('licenseTypeConfig.goToRequirements', 'Certificate requirements')} - - {' · '} - - {t('licenseTypeConfig.goToFees', 'Payment configuration')} - - - - - - - - - - - - - - - -
- - - form.setFieldValue('key', e.currentTarget.value.toUpperCase())} - /> - - - - - - -