mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-03 08:33:41 +00:00
feat: add REISSUE application kind and dynamic file size limits, and remove unused serviceKind logic
This commit is contained in:
@@ -21,7 +21,6 @@ import {
|
||||
type CertificateCategory,
|
||||
type CompletionEffect,
|
||||
type LicenseType,
|
||||
type ServiceKind,
|
||||
type WorkflowProfile,
|
||||
} from '@ema-platform/api';
|
||||
import { useRequirementActions } from '../hooks/useRequirementActions';
|
||||
@@ -29,7 +28,6 @@ import { useRequirementActions } from '../hooks/useRequirementActions';
|
||||
/** The shape the form edits — every field the behaviour endpoint accepts. */
|
||||
interface Draft {
|
||||
workflowProfile: WorkflowProfile;
|
||||
serviceKind: ServiceKind;
|
||||
completionEffect: CompletionEffect | null;
|
||||
certificateCategory: CertificateCategory | null;
|
||||
requiresExamination: boolean;
|
||||
@@ -57,7 +55,6 @@ const REMINDER_OFFSETS = ['90', '60', '30', '14', '7'];
|
||||
function toDraft(licenseType: LicenseType): Draft {
|
||||
return {
|
||||
workflowProfile: licenseType.workflowProfile ?? 'STANDARD',
|
||||
serviceKind: licenseType.serviceKind ?? 'LICENSE',
|
||||
completionEffect: licenseType.completionEffect ?? null,
|
||||
certificateCategory: licenseType.certificateCategory ?? null,
|
||||
requiresExamination: licenseType.requiresExamination ?? false,
|
||||
@@ -154,12 +151,22 @@ export function BehaviorTab({ licenseType }: { licenseType: LicenseType }) {
|
||||
...rest
|
||||
} = draft;
|
||||
|
||||
// A type switched from "does not expire" straight to a term in days still
|
||||
// carries `validityMonths: 0`, which the server's 6-month floor refuses.
|
||||
// Days win over months at issuance, so the zero is simply left alone.
|
||||
const term = expires
|
||||
? {
|
||||
validityDays,
|
||||
...(validityMonths > 0 ? { validityMonths } : {}),
|
||||
}
|
||||
: {};
|
||||
|
||||
const ok = await run(
|
||||
() =>
|
||||
save({
|
||||
id: licenseType.id,
|
||||
...rest,
|
||||
...(expires ? { validityMonths, validityDays } : {}),
|
||||
...term,
|
||||
...(expires && draft.renewalEnabled
|
||||
? { renewalWindowDays, expiryReminderDays }
|
||||
: {}),
|
||||
@@ -241,21 +248,10 @@ export function BehaviorTab({ licenseType }: { licenseType: LicenseType }) {
|
||||
disabled={!canEdit}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label={t('certReq.behavior.serviceKind', 'Service kind')}
|
||||
description={t(
|
||||
'certReq.behavior.serviceKindHint',
|
||||
'Catalogue classification only — no workflow depends on it.',
|
||||
)}
|
||||
data={[
|
||||
{ value: 'LICENSE', label: t('certReq.behavior.license', 'Licence') },
|
||||
{ value: 'REGISTRATION', label: t('certReq.behavior.registrationKind', 'Registration') },
|
||||
]}
|
||||
value={draft.serviceKind}
|
||||
onChange={(v) => v && set('serviceKind', v as ServiceKind)}
|
||||
allowDeselect={false}
|
||||
disabled={!canEdit}
|
||||
/>
|
||||
{/* `serviceKind` is deliberately not offered here: nothing in the
|
||||
backend or either portal reads it, so a control for it would be
|
||||
a setting that changes nothing. Bring it back once something
|
||||
branches on it. */}
|
||||
</Section>
|
||||
|
||||
<Section title={t('certReq.behavior.eligibility', 'Eligibility gates')}>
|
||||
|
||||
@@ -322,6 +322,7 @@ export function DocumentRequirementEditorDrawer({
|
||||
data={[
|
||||
{ value: 'NEW', label: t('certReq.doc.kindNew', 'New application') },
|
||||
{ value: 'RENEWAL', label: t('certReq.doc.kindRenewal', 'Renewal') },
|
||||
{ value: 'REISSUE', label: t('certReq.doc.kindReissue', 'Damaged / reissue') },
|
||||
]}
|
||||
value={draft.applicationKind}
|
||||
onChange={(v) => v && setDraft((d) => ({ ...d, applicationKind: v as ApplicationKind }))}
|
||||
|
||||
@@ -20,7 +20,14 @@ import { useRequirementActions } from '../hooks/useRequirementActions';
|
||||
import { describeCondition } from './ConditionBuilder';
|
||||
import { DocumentRequirementEditorDrawer } from './DocumentRequirementEditorDrawer';
|
||||
|
||||
const KINDS: ApplicationKind[] = ['NEW', 'RENEWAL'];
|
||||
/** Every kind the server validates a document set for, so each can be configured. */
|
||||
const KINDS: ApplicationKind[] = ['NEW', 'RENEWAL', 'REISSUE'];
|
||||
|
||||
const KIND_LABEL: Record<ApplicationKind, [string, string]> = {
|
||||
NEW: ['certReq.doc.kindNew', 'New application'],
|
||||
RENEWAL: ['certReq.doc.kindRenewal', 'Renewal'],
|
||||
REISSUE: ['certReq.doc.kindReissue', 'Damaged / reissue'],
|
||||
};
|
||||
|
||||
const MODE_COLOR: Record<DocumentRequirement['mode'], string> = {
|
||||
ALWAYS: 'blue',
|
||||
@@ -112,9 +119,7 @@ export function DocumentRequirementsTab({ licenseType }: { licenseType: LicenseT
|
||||
return (
|
||||
<Card key={kind} withBorder radius="md" p="md">
|
||||
<Group justify="space-between" mb="sm">
|
||||
<Title order={5}>
|
||||
{kind === 'NEW' ? t('certReq.doc.kindNew', 'New application') : t('certReq.doc.kindRenewal', 'Renewal')}
|
||||
</Title>
|
||||
<Title order={5}>{t(...KIND_LABEL[kind])}</Title>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
|
||||
@@ -1496,6 +1496,7 @@ export const am: Translations = {
|
||||
applicationKind: "የማመልከቻ ዓይነት",
|
||||
kindNew: "አዲስ ማመልከቻ",
|
||||
kindRenewal: "ዕድሳት",
|
||||
kindReissue: "የተበላሸ / ምትክ",
|
||||
kindLocked: "ከተፈጠረ በኋላ የማመልከቻ ዓይነት መቀየር አይቻልም",
|
||||
mode: "ዘዴ",
|
||||
modeAlways: "ሁልጊዜ ያስፈልጋል",
|
||||
|
||||
@@ -1502,6 +1502,7 @@ export const en = {
|
||||
applicationKind: 'Application kind',
|
||||
kindNew: 'New application',
|
||||
kindRenewal: 'Renewal',
|
||||
kindReissue: 'Damaged / reissue',
|
||||
kindLocked: 'Application kind cannot change once created',
|
||||
mode: 'Mode',
|
||||
modeAlways: 'Always required',
|
||||
|
||||
@@ -25,8 +25,6 @@ import {
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FilePreviewModal } from '@ema-platform/ui';
|
||||
|
||||
const MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024;
|
||||
|
||||
interface Props {
|
||||
requirements: DocumentRequirement[];
|
||||
attachments: Attachment[];
|
||||
@@ -87,7 +85,11 @@ export function DocumentSlots({
|
||||
|
||||
async function handle(documentKey: string, file: File | null) {
|
||||
if (!file) return;
|
||||
if (file.size > MAX_FILE_SIZE_BYTES) {
|
||||
// The slot's own limit, as configured in the backoffice — the server
|
||||
// refuses anything larger, so the check here only saves the round trip.
|
||||
const maxSizeMb =
|
||||
requirements.find((r) => r.key === documentKey)?.maxSizeMb ?? 5;
|
||||
if (file.size > maxSizeMb * 1024 * 1024) {
|
||||
setError(
|
||||
t('licensing.msg.fileTooLarge', {
|
||||
size: `${(file.size / 1024 / 1024).toFixed(1)}MB`,
|
||||
|
||||
@@ -118,8 +118,20 @@ export function LicenseApplicationPage() {
|
||||
const localized = useLocalized();
|
||||
const accountUser = useAppSelector((state) => state.auth.user);
|
||||
|
||||
const [appId, setAppId] = useState<string | undefined>(applicationId);
|
||||
// Fetched before the requirements because the application's kind decides
|
||||
// which document set is asked for: a renewal started from a licence card
|
||||
// must be shown the RENEWAL slots, not the NEW ones — the same set the
|
||||
// server validates against at submission.
|
||||
const { data: detail, refetch } = useGetApplicationQuery(appId as string, {
|
||||
skip: !appId,
|
||||
});
|
||||
|
||||
const { data: config, isLoading: loadingConfig } =
|
||||
useGetLicenseTypeRequirementsQuery({ idOrKey: typeCode });
|
||||
useGetLicenseTypeRequirementsQuery({
|
||||
idOrKey: typeCode,
|
||||
kind: detail?.application?.kind ?? "NEW",
|
||||
});
|
||||
const { profile } = useCurrentProfile();
|
||||
const { can: hasPermission, known: permissionsKnown } = usePermissions();
|
||||
// Only the vessel-select field (ConfigDrivenSection) reads this — fetched
|
||||
@@ -130,7 +142,6 @@ export function LicenseApplicationPage() {
|
||||
skip: !permissionsKnown || !hasPermission([PORTAL_PERMISSIONS.VIEW_OWN_VESSELS]),
|
||||
});
|
||||
const [createApplication] = useCreateApplicationMutation();
|
||||
const [appId, setAppId] = useState<string | undefined>(applicationId);
|
||||
|
||||
// Create (or resume) the draft up front, so uploads have a real owner to
|
||||
// attach to and nothing is lost if the browser is closed mid-wizard.
|
||||
@@ -151,9 +162,6 @@ export function LicenseApplicationPage() {
|
||||
);
|
||||
}, [appId, config, createApplication, typeCode]);
|
||||
|
||||
const { data: detail, refetch } = useGetApplicationQuery(appId as string, {
|
||||
skip: !appId,
|
||||
});
|
||||
const { data: attachments = [], refetch: refetchAttachments } =
|
||||
useGetAttachmentsQuery(
|
||||
{ ownerType: "APPLICATION", ownerId: appId as string },
|
||||
|
||||
Reference in New Issue
Block a user