mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
Merge branch 'feature/exam-attempt-domain' of github.com:Tria-plc/emaui into feature/exam-attempt-domain
This commit is contained in:
@@ -77,21 +77,23 @@ function ExamForm({
|
||||
editing?.cuttingPoint ?? 0,
|
||||
);
|
||||
const [status, setStatus] = useState<string | null>(editing?.status ?? null);
|
||||
const [activeTab, setActiveTab] = useState<string | null>("basic");
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (
|
||||
!certificationId ||
|
||||
!titleEn ||
|
||||
!titleAm ||
|
||||
!date ||
|
||||
!type ||
|
||||
!form ||
|
||||
!venue ||
|
||||
!adminMethod ||
|
||||
!evalMethod
|
||||
) {
|
||||
notify.error("Please fill all required fields");
|
||||
if (!certificationId || !titleEn || !titleAm || !date || !venue) {
|
||||
setActiveTab("basic");
|
||||
notify.error(t("exam.form.fillRequiredBasic"));
|
||||
return;
|
||||
}
|
||||
if ((directionEn || directionAm) && !(directionEn && directionAm)) {
|
||||
setActiveTab("basic");
|
||||
notify.error(t("exam.form.directionBothLanguages"));
|
||||
return;
|
||||
}
|
||||
if (!type || !form || !adminMethod || !evalMethod || !cuttingPoint) {
|
||||
setActiveTab("settings");
|
||||
notify.error(t("exam.form.fillRequiredSettings"));
|
||||
return;
|
||||
}
|
||||
onSubmit(
|
||||
@@ -121,7 +123,7 @@ function ExamForm({
|
||||
return (
|
||||
<Modal opened onClose={onCancel} title={editing ? t("exam.update") : t("exam.add")} size="xl">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Tabs defaultValue="basic" variant="outline" radius="md">
|
||||
<Tabs value={activeTab} onChange={setActiveTab} variant="outline" radius="md">
|
||||
<Tabs.List mb="md">
|
||||
<Tabs.Tab value="basic" leftSection={<IconInfoCircle size={15} />}>
|
||||
{t("exam.form.basicInfo")}
|
||||
@@ -296,7 +298,7 @@ function ExamForm({
|
||||
onChange={(v) => setCuttingPoint(Number(v))}
|
||||
min={0}
|
||||
size="sm"
|
||||
required
|
||||
withAsterisk
|
||||
/>
|
||||
</SimpleGrid>
|
||||
{editing && (
|
||||
|
||||
@@ -257,6 +257,9 @@ export const am: Translations = {
|
||||
random: "በዘፈቀደ",
|
||||
cuttingPoint: "የማለፊያ ነጥብ",
|
||||
cuttingPointPlaceholder: "ለማለፍ ዝቅተኛ ነጥብ",
|
||||
fillRequiredBasic: "በመሠረታዊ መረጃ ውስጥ ያሉ አስፈላጊ መስኮችን ይሙሉ።",
|
||||
fillRequiredSettings: "በቅንብሮች ውስጥ ያሉ አስፈላጊ መስኮችን ይሙሉ — ዓይነት፣ ቅጽ፣ የአስተዳደር ዘዴ፣ የግምገማ ዘዴ እና የማለፊያ ነጥብ።",
|
||||
directionBothLanguages: "መመሪያ በሁለቱም እንግሊዝኛ እና አማርኛ ጽሑፍ ያስፈልገዋል፣ ወይም ሁለቱንም ባዶ ይተዉ።",
|
||||
status: "ሁኔታ",
|
||||
statusPlaceholder: "የፈተና ሁኔታ",
|
||||
pending: "በመጠባበቅ ላይ",
|
||||
|
||||
@@ -255,6 +255,9 @@ export const en = {
|
||||
random: 'Random',
|
||||
cuttingPoint: 'Cutting Point (Pass Mark)',
|
||||
cuttingPointPlaceholder: 'Minimum score to pass',
|
||||
fillRequiredBasic: 'Please fill all required fields in Basic Info.',
|
||||
fillRequiredSettings: 'Please fill all required fields in Settings — type, form, administration method, evaluation method, and cutting point.',
|
||||
directionBothLanguages: 'Direction needs text in both English and Amharic, or leave both empty.',
|
||||
status: 'Status',
|
||||
statusPlaceholder: 'Exam status',
|
||||
pending: 'Pending',
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { fetchBaseQuery, type BaseQueryFn } from '@reduxjs/toolkit/query/react';
|
||||
import type { FetchArgs, FetchBaseQueryError } from '@reduxjs/toolkit/query';
|
||||
import { resolveSessionContext } from '../session';
|
||||
import { fetchBaseQuery, type BaseQueryFn } from "@reduxjs/toolkit/query/react";
|
||||
import type { FetchArgs, FetchBaseQueryError } from "@reduxjs/toolkit/query";
|
||||
import { resolveSessionContext } from "../session";
|
||||
|
||||
export const BASE_API_URL =
|
||||
(import.meta as { env?: Record<string, string> }).env?.['VITE_BASE_API_URL'] ??
|
||||
'http://localhost:3001/api';
|
||||
(import.meta as { env?: Record<string, string> }).env?.[
|
||||
"VITE_BASE_API_URL"
|
||||
] ?? "http://localhost:3000/api";
|
||||
|
||||
let _onTokenExpired: (() => Promise<string>) | null = null;
|
||||
let _onAuthFailure: (() => void) | null = null;
|
||||
@@ -28,7 +29,7 @@ export const baseQueryWithReauth: BaseQueryFn<
|
||||
const { token, sessionHeaders } = resolveSessionContext(
|
||||
api.getState() as { auth?: { token?: string } },
|
||||
);
|
||||
if (token) headers.set('Authorization', `Bearer ${token}`);
|
||||
if (token) headers.set("Authorization", `Bearer ${token}`);
|
||||
Object.entries(sessionHeaders).forEach(([k, v]) => headers.set(k, v));
|
||||
return headers;
|
||||
},
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { resolveTokenFromStorage } from '../../session';
|
||||
import { resolveTokenFromStorage } from "../../session";
|
||||
import type {
|
||||
Bilingual,
|
||||
FormSectionConfig,
|
||||
LicenseApplication,
|
||||
LicenseStatus,
|
||||
ValidationIssue,
|
||||
} from './licensing.types';
|
||||
} from "./licensing.types";
|
||||
|
||||
const BASE_API_URL =
|
||||
(import.meta as { env?: Record<string, string> }).env?.['VITE_BASE_API_URL'] ??
|
||||
'http://localhost:3001/api';
|
||||
(import.meta as { env?: Record<string, string> }).env?.[
|
||||
"VITE_BASE_API_URL"
|
||||
] ?? "http://localhost:3000/api";
|
||||
|
||||
/**
|
||||
* Uploads a document straight to the API.
|
||||
@@ -20,12 +21,12 @@ const BASE_API_URL =
|
||||
*/
|
||||
export async function uploadDocument(params: {
|
||||
ownerType:
|
||||
| 'APPLICATION'
|
||||
| 'APPLICATION_STAFF'
|
||||
| 'INSPECTION'
|
||||
| 'LICENSE'
|
||||
| 'SEA_SERVICE_RECORD'
|
||||
| 'MEDICAL_CERTIFICATE';
|
||||
| "APPLICATION"
|
||||
| "APPLICATION_STAFF"
|
||||
| "INSPECTION"
|
||||
| "LICENSE"
|
||||
| "SEA_SERVICE_RECORD"
|
||||
| "MEDICAL_CERTIFICATE";
|
||||
ownerId: string;
|
||||
documentKey: string;
|
||||
file: File;
|
||||
@@ -34,17 +35,17 @@ export async function uploadDocument(params: {
|
||||
validTo?: string;
|
||||
}): Promise<{ ok: true } | { ok: false; error: string }> {
|
||||
const form = new FormData();
|
||||
form.append('ownerType', params.ownerType);
|
||||
form.append('ownerId', params.ownerId);
|
||||
form.append('documentKey', params.documentKey);
|
||||
if (params.title) form.append('title', params.title);
|
||||
if (params.validFrom) form.append('validFrom', params.validFrom);
|
||||
if (params.validTo) form.append('validTo', params.validTo);
|
||||
form.append('files', params.file, params.file.name);
|
||||
form.append("ownerType", params.ownerType);
|
||||
form.append("ownerId", params.ownerId);
|
||||
form.append("documentKey", params.documentKey);
|
||||
if (params.title) form.append("title", params.title);
|
||||
if (params.validFrom) form.append("validFrom", params.validFrom);
|
||||
if (params.validTo) form.append("validTo", params.validTo);
|
||||
form.append("files", params.file, params.file.name);
|
||||
|
||||
const token = resolveTokenFromStorage();
|
||||
const res = await fetch(`${BASE_API_URL}/attachments`, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
body: form,
|
||||
});
|
||||
@@ -55,40 +56,40 @@ export async function uploadDocument(params: {
|
||||
|
||||
/** Human label for a status, in the vocabulary the user stories use. */
|
||||
export const STATUS_LABELS: Record<LicenseStatus, string> = {
|
||||
DRAFT: 'Draft',
|
||||
SUBMITTED: 'Submitted',
|
||||
UNDER_REVIEW: 'Under Review',
|
||||
UNDER_EVALUATION: 'Under Evaluation',
|
||||
RESUBMIT_REQUIRED: 'Resubmit Required',
|
||||
INSPECTION_PENDING: 'Inspection Pending',
|
||||
INSPECTION_COMPLETED: 'Inspection Completed',
|
||||
APPROVED: 'Approved',
|
||||
REJECTED: 'Rejected',
|
||||
ON_HOLD: 'On Hold',
|
||||
PAYMENT_PENDING: 'Payment Pending',
|
||||
PAID: 'Paid',
|
||||
PAYMENT_CONFIRMED: 'Preparing Certificate',
|
||||
CERTIFICATE_ISSUED: 'Certificate Issued',
|
||||
COMPLETED: 'Completed',
|
||||
DRAFT: "Draft",
|
||||
SUBMITTED: "Submitted",
|
||||
UNDER_REVIEW: "Under Review",
|
||||
UNDER_EVALUATION: "Under Evaluation",
|
||||
RESUBMIT_REQUIRED: "Resubmit Required",
|
||||
INSPECTION_PENDING: "Inspection Pending",
|
||||
INSPECTION_COMPLETED: "Inspection Completed",
|
||||
APPROVED: "Approved",
|
||||
REJECTED: "Rejected",
|
||||
ON_HOLD: "On Hold",
|
||||
PAYMENT_PENDING: "Payment Pending",
|
||||
PAID: "Paid",
|
||||
PAYMENT_CONFIRMED: "Preparing Certificate",
|
||||
CERTIFICATE_ISSUED: "Certificate Issued",
|
||||
COMPLETED: "Completed",
|
||||
};
|
||||
|
||||
/** Mantine colour per status — green progresses, orange needs the applicant. */
|
||||
export const STATUS_COLORS: Record<LicenseStatus, string> = {
|
||||
DRAFT: 'gray',
|
||||
SUBMITTED: 'blue',
|
||||
UNDER_REVIEW: 'indigo',
|
||||
UNDER_EVALUATION: 'indigo',
|
||||
RESUBMIT_REQUIRED: 'orange',
|
||||
INSPECTION_PENDING: 'cyan',
|
||||
INSPECTION_COMPLETED: 'cyan',
|
||||
APPROVED: 'teal',
|
||||
REJECTED: 'red',
|
||||
ON_HOLD: 'gray',
|
||||
PAYMENT_PENDING: 'yellow',
|
||||
PAID: 'lime',
|
||||
PAYMENT_CONFIRMED: 'teal',
|
||||
CERTIFICATE_ISSUED: 'green',
|
||||
COMPLETED: 'green',
|
||||
DRAFT: "gray",
|
||||
SUBMITTED: "blue",
|
||||
UNDER_REVIEW: "indigo",
|
||||
UNDER_EVALUATION: "indigo",
|
||||
RESUBMIT_REQUIRED: "orange",
|
||||
INSPECTION_PENDING: "cyan",
|
||||
INSPECTION_COMPLETED: "cyan",
|
||||
APPROVED: "teal",
|
||||
REJECTED: "red",
|
||||
ON_HOLD: "gray",
|
||||
PAYMENT_PENDING: "yellow",
|
||||
PAID: "lime",
|
||||
PAYMENT_CONFIRMED: "teal",
|
||||
CERTIFICATE_ISSUED: "green",
|
||||
COMPLETED: "green",
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -118,46 +119,59 @@ export const STATUS_PROGRESS: Record<LicenseStatus, number> = {
|
||||
|
||||
/** Statuses where nothing moves until the applicant does something. */
|
||||
export const APPLICANT_ACTION_STATUSES: LicenseStatus[] = [
|
||||
'DRAFT',
|
||||
'RESUBMIT_REQUIRED',
|
||||
'PAYMENT_PENDING',
|
||||
"DRAFT",
|
||||
"RESUBMIT_REQUIRED",
|
||||
"PAYMENT_PENDING",
|
||||
];
|
||||
|
||||
/** Statuses that are finished, whichever way they went. */
|
||||
export const TERMINAL_STATUSES: LicenseStatus[] = [
|
||||
'CERTIFICATE_ISSUED',
|
||||
'COMPLETED',
|
||||
'REJECTED',
|
||||
"CERTIFICATE_ISSUED",
|
||||
"COMPLETED",
|
||||
"REJECTED",
|
||||
];
|
||||
|
||||
/** Licence types with no `companyName` — these are filed by a person, not a
|
||||
* business, so display falls back to the applicant name captured in the form. */
|
||||
export const APPLICANT_NAME_TYPE_KEYS = [
|
||||
'SEAFARER_REGISTRATION',
|
||||
'CERTIFICATE_OF_COMPETENCY',
|
||||
'CERTIFICATE_OF_PROFICIENCY',
|
||||
'VESSEL_REGISTRATION',
|
||||
'VESSEL_OWNERSHIP_TRANSFER',
|
||||
'ENDORSEMENT_COC',
|
||||
'ENDORSEMENT_GOC',
|
||||
"SEAFARER_REGISTRATION",
|
||||
"CERTIFICATE_OF_COMPETENCY",
|
||||
"CERTIFICATE_OF_PROFICIENCY",
|
||||
"VESSEL_REGISTRATION",
|
||||
"VESSEL_OWNERSHIP_TRANSFER",
|
||||
"ENDORSEMENT_COC",
|
||||
"ENDORSEMENT_GOC",
|
||||
];
|
||||
|
||||
/** Company name, or applicant name for licence types that have no company. */
|
||||
export function applicantOrCompanyName(app: LicenseApplication): string | undefined {
|
||||
if (!app.licenseType?.key || !APPLICANT_NAME_TYPE_KEYS.includes(app.licenseType.key)) {
|
||||
export function applicantOrCompanyName(
|
||||
app: LicenseApplication,
|
||||
): string | undefined {
|
||||
if (
|
||||
!app.licenseType?.key ||
|
||||
!APPLICANT_NAME_TYPE_KEYS.includes(app.licenseType.key)
|
||||
) {
|
||||
return app.companyName ?? undefined;
|
||||
}
|
||||
const applicantName = (app.formData?.account as Record<string, unknown> | undefined)
|
||||
?.applicantName;
|
||||
return typeof applicantName === 'string' && applicantName ? applicantName : undefined;
|
||||
const applicantName = (
|
||||
app.formData?.account as Record<string, unknown> | undefined
|
||||
)?.applicantName;
|
||||
return typeof applicantName === "string" && applicantName
|
||||
? applicantName
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/** Reads a bilingual value for the active language, falling back to English. */
|
||||
export function localized(value: Bilingual | undefined, language = 'en'): string {
|
||||
if (!value) return '';
|
||||
export function localized(
|
||||
value: Bilingual | undefined,
|
||||
language = "en",
|
||||
): string {
|
||||
if (!value) return "";
|
||||
// `||` not `??`: an empty Amharic string is "not translated", not a value —
|
||||
// editors save `am: ''` freely (ExamPage, CertificationPage, LocationForm).
|
||||
return (language === 'am' ? value.am : value.en) || value.en || value.am || '';
|
||||
return (
|
||||
(language === "am" ? value.am : value.en) || value.en || value.am || ""
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,7 +181,7 @@ export function localized(value: Bilingual | undefined, language = 'en'): string
|
||||
export function extractValidationIssues(error: unknown): ValidationIssue[] {
|
||||
const data = (error as { data?: Record<string, unknown> })?.data;
|
||||
if (!data) return [];
|
||||
const issues = data['issues'];
|
||||
const issues = data["issues"];
|
||||
return Array.isArray(issues) ? (issues as ValidationIssue[]) : [];
|
||||
}
|
||||
|
||||
@@ -179,47 +193,54 @@ export function extractValidationIssues(error: unknown): ValidationIssue[] {
|
||||
*/
|
||||
const ERROR_MESSAGES: Record<string, string> = {
|
||||
capital_verification_required:
|
||||
'Record the verified capital before approving — it must be checked against the bank letter.',
|
||||
"Record the verified capital before approving — it must be checked against the bank letter.",
|
||||
capital_below_threshold:
|
||||
'The verified capital is below the minimum required for this licence type.',
|
||||
"The verified capital is below the minimum required for this licence type.",
|
||||
inspection_required_before_approval:
|
||||
'A physical inspection must be recorded before this application can be approved.',
|
||||
application_already_claimed: 'Another officer has already claimed this application.',
|
||||
"A physical inspection must be recorded before this application can be approved.",
|
||||
application_already_claimed:
|
||||
"Another officer has already claimed this application.",
|
||||
assigned_to_another_officer:
|
||||
'This application is assigned to another officer, so only they can act on it.',
|
||||
not_application_owner: 'This application belongs to another applicant.',
|
||||
application_incomplete: 'The application is missing required information.',
|
||||
unresolved_remarks: 'Every requested correction must be addressed first.',
|
||||
adjustment_items_required: 'Flag at least one item before requesting an adjustment.',
|
||||
remark_text_required: 'Each flagged item needs a remark explaining what to fix.',
|
||||
rejection_reason_required: 'A rejection reason is required.',
|
||||
"This application is assigned to another officer, so only they can act on it.",
|
||||
not_application_owner: "This application belongs to another applicant.",
|
||||
application_incomplete: "The application is missing required information.",
|
||||
unresolved_remarks: "Every requested correction must be addressed first.",
|
||||
adjustment_items_required:
|
||||
"Flag at least one item before requesting an adjustment.",
|
||||
remark_text_required:
|
||||
"Each flagged item needs a remark explaining what to fix.",
|
||||
rejection_reason_required: "A rejection reason is required.",
|
||||
section_not_flagged_for_adjustment:
|
||||
'Only the sections the officer flagged can be changed in this round.',
|
||||
application_not_editable: 'This application can no longer be edited.',
|
||||
invalid_transition: 'That action is not available at this stage.',
|
||||
"Only the sections the officer flagged can be changed in this round.",
|
||||
application_not_editable: "This application can no longer be edited.",
|
||||
invalid_transition: "That action is not available at this stage.",
|
||||
inspection_not_required_use_final_approve:
|
||||
'This licence type needs no inspection — approve it directly.',
|
||||
"This licence type needs no inspection — approve it directly.",
|
||||
application_not_awaiting_inspection:
|
||||
'This application is not waiting for an inspection.',
|
||||
inspection_already_completed: 'This inspection has already been recorded.',
|
||||
license_type_inactive: 'This licence type is not currently accepting applications.',
|
||||
"This application is not waiting for an inspection.",
|
||||
inspection_already_completed: "This inspection has already been recorded.",
|
||||
license_type_inactive:
|
||||
"This licence type is not currently accepting applications.",
|
||||
};
|
||||
|
||||
export function extractErrorMessage(error: unknown, fallback = 'Something went wrong'): string {
|
||||
export function extractErrorMessage(
|
||||
error: unknown,
|
||||
fallback = "Something went wrong",
|
||||
): string {
|
||||
const data = (error as { data?: Record<string, unknown> })?.data;
|
||||
const raw = data?.['message'];
|
||||
const raw = data?.["message"];
|
||||
|
||||
// The filter passes structured errors through as `{ message: { message } }`.
|
||||
const key =
|
||||
typeof raw === 'string'
|
||||
typeof raw === "string"
|
||||
? raw
|
||||
: typeof (raw as { message?: unknown })?.message === 'string'
|
||||
? ((raw as { message: string }).message)
|
||||
: typeof (raw as { message?: unknown })?.message === "string"
|
||||
? (raw as { message: string }).message
|
||||
: undefined;
|
||||
|
||||
if (key && ERROR_MESSAGES[key]) return ERROR_MESSAGES[key];
|
||||
if (typeof raw === 'string') return raw;
|
||||
if (Array.isArray(raw)) return raw.join(', ');
|
||||
if (typeof raw === "string") return raw;
|
||||
if (Array.isArray(raw)) return raw.join(", ");
|
||||
return fallback;
|
||||
}
|
||||
|
||||
@@ -227,7 +248,7 @@ export function extractErrorMessage(error: unknown, fallback = 'Something went w
|
||||
export interface WizardStep {
|
||||
key: string;
|
||||
label: string;
|
||||
kind: 'sections' | 'staff' | 'documents' | 'review';
|
||||
kind: "sections" | "staff" | "documents" | "review";
|
||||
sections: FormSectionConfig[];
|
||||
}
|
||||
|
||||
@@ -266,7 +287,7 @@ export function buildWizardSteps(
|
||||
steps.push({
|
||||
key: `section:${section.key}`,
|
||||
label: localized(section.title, options?.language),
|
||||
kind: 'sections',
|
||||
kind: "sections",
|
||||
sections: [section],
|
||||
});
|
||||
continue;
|
||||
@@ -279,7 +300,7 @@ export function buildWizardSteps(
|
||||
const step: WizardStep = {
|
||||
key: `group:${group}`,
|
||||
label: group,
|
||||
kind: 'sections',
|
||||
kind: "sections",
|
||||
sections: [section],
|
||||
};
|
||||
byGroup.set(group, step);
|
||||
@@ -295,7 +316,7 @@ export function buildWizardSteps(
|
||||
// beside the summary rather than earning a step of their own. It is what
|
||||
// keeps a one-checkbox declaration from costing a whole page.
|
||||
const isReviewGroup = (step: WizardStep) =>
|
||||
step.sections.some((s) => s.group?.trim().toLowerCase() === 'review');
|
||||
step.sections.some((s) => s.group?.trim().toLowerCase() === "review");
|
||||
const reviewSections = steps.filter(isReviewGroup).flatMap((s) => s.sections);
|
||||
const formSteps = steps.filter((step) => !isReviewGroup(step));
|
||||
|
||||
@@ -303,9 +324,21 @@ export function buildWizardSteps(
|
||||
...formSteps,
|
||||
...(options?.hasStaff === false
|
||||
? []
|
||||
: [{ key: 'staff', label: 'Staff', kind: 'staff', sections: [] } as WizardStep]),
|
||||
{ key: 'documents', label: 'Documents', kind: 'documents', sections: [] },
|
||||
{ key: 'review', label: 'Review', kind: 'review', sections: reviewSections },
|
||||
: [
|
||||
{
|
||||
key: "staff",
|
||||
label: "Staff",
|
||||
kind: "staff",
|
||||
sections: [],
|
||||
} as WizardStep,
|
||||
]),
|
||||
{ key: "documents", label: "Documents", kind: "documents", sections: [] },
|
||||
{
|
||||
key: "review",
|
||||
label: "Review",
|
||||
kind: "review",
|
||||
sections: reviewSections,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -320,7 +353,7 @@ export type FieldErrors = Record<string, string>;
|
||||
export function validateSections(
|
||||
sections: FormSectionConfig[],
|
||||
formData: Record<string, Record<string, unknown>>,
|
||||
language = 'en',
|
||||
language = "en",
|
||||
): FieldErrors {
|
||||
const errors: FieldErrors = {};
|
||||
|
||||
@@ -337,13 +370,13 @@ export function validateSections(
|
||||
const empty =
|
||||
value === undefined ||
|
||||
value === null ||
|
||||
(typeof value === 'string' && value.trim() === '') ||
|
||||
(typeof value === 'boolean' && value === false) ||
|
||||
(typeof value === "string" && value.trim() === "") ||
|
||||
(typeof value === "boolean" && value === false) ||
|
||||
(Array.isArray(value) && value.length === 0);
|
||||
|
||||
if (field.required && empty) {
|
||||
errors[`${section.key}.${field.key}`] =
|
||||
field.type === 'BOOLEAN'
|
||||
field.type === "BOOLEAN"
|
||||
? `${localized(field.label, language)} must be accepted`
|
||||
: `${localized(field.label, language)} is required`;
|
||||
continue;
|
||||
@@ -368,23 +401,33 @@ export function validateSections(
|
||||
/** Evaluates a config condition against the current form answers. */
|
||||
export function conditionHolds(
|
||||
condition:
|
||||
| { field: string; equals?: unknown; notEquals?: unknown; in?: (string | number)[]; isSet?: boolean }
|
||||
| {
|
||||
field: string;
|
||||
equals?: unknown;
|
||||
notEquals?: unknown;
|
||||
in?: (string | number)[];
|
||||
isSet?: boolean;
|
||||
}
|
||||
| undefined
|
||||
| null,
|
||||
formData: Record<string, Record<string, unknown>>,
|
||||
): boolean {
|
||||
if (!condition?.field) return true;
|
||||
const value = condition.field
|
||||
.split('.')
|
||||
.split(".")
|
||||
.reduce<unknown>(
|
||||
(acc, key) =>
|
||||
acc && typeof acc === 'object' ? (acc as Record<string, unknown>)[key] : undefined,
|
||||
acc && typeof acc === "object"
|
||||
? (acc as Record<string, unknown>)[key]
|
||||
: undefined,
|
||||
formData as unknown,
|
||||
);
|
||||
const isEmpty = value === undefined || value === null || value === '';
|
||||
if (condition.isSet !== undefined) return condition.isSet ? !isEmpty : isEmpty;
|
||||
const isEmpty = value === undefined || value === null || value === "";
|
||||
if (condition.isSet !== undefined)
|
||||
return condition.isSet ? !isEmpty : isEmpty;
|
||||
if (condition.equals !== undefined) return value === condition.equals;
|
||||
if (condition.notEquals !== undefined) return value !== condition.notEquals;
|
||||
if (condition.in !== undefined) return condition.in.includes(value as string | number);
|
||||
if (condition.in !== undefined)
|
||||
return condition.in.includes(value as string | number);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useEffect, useState, type ReactNode } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { authStorage } from '../utils/auth-storage';
|
||||
import { hydrateAuth, logout, setUser } from '../store/auth.slice';
|
||||
import type { AuthUser } from '../types/auth.types';
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { authStorage } from "../utils/auth-storage";
|
||||
import { hydrateAuth, logout, setUser } from "../store/auth.slice";
|
||||
import type { AuthUser } from "../types/auth.types";
|
||||
|
||||
const BASE_API_URL =
|
||||
(import.meta as { env?: Record<string, string> }).env?.['VITE_BASE_API_URL'] ??
|
||||
'http://localhost:3001/api';
|
||||
(import.meta as { env?: Record<string, string> }).env?.[
|
||||
"VITE_BASE_API_URL"
|
||||
] ?? "http://localhost:3000/api";
|
||||
|
||||
/**
|
||||
* Restores the signed-in session before the router renders.
|
||||
|
||||
@@ -3,7 +3,7 @@ import { authStorage } from "./auth-storage";
|
||||
const BASE_API_URL =
|
||||
(import.meta as { env?: Record<string, string> }).env?.[
|
||||
"VITE_BASE_API_URL"
|
||||
] ?? "http://localhost:3001/api";
|
||||
] ?? "http://localhost:3000/api";
|
||||
|
||||
interface RefreshResponse {
|
||||
token: string;
|
||||
@@ -17,8 +17,8 @@ export async function refreshAccessToken(): Promise<string> {
|
||||
// The IAM service exposes this as `refresh-token`; posting to `/auth/refresh`
|
||||
// 404s, which the catch below turns into a silent logout.
|
||||
const response = await fetch(`${BASE_API_URL}/auth/refresh-token`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
});
|
||||
|
||||
|
||||
123
package-lock.json
generated
123
package-lock.json
generated
@@ -3858,9 +3858,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3881,9 +3878,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3904,9 +3898,6 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3927,9 +3918,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3950,9 +3938,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4013,9 +3998,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4299,9 +4281,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4316,9 +4295,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4333,9 +4309,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4350,9 +4323,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6512,9 +6482,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6528,9 +6495,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6544,9 +6508,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6560,9 +6521,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6576,9 +6534,6 @@
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6592,9 +6547,6 @@
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6608,9 +6560,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6624,9 +6573,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6640,9 +6586,6 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6656,9 +6599,6 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6672,9 +6612,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6688,9 +6625,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6704,9 +6638,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6846,9 +6777,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6863,9 +6791,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6880,9 +6805,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6897,9 +6819,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7515,9 +7434,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7534,9 +7450,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7553,9 +7466,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7572,9 +7482,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8182,6 +8089,18 @@
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tria-plc/iamui/node_modules/@types/react": {
|
||||
"version": "18.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
|
||||
"integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tria-plc/iamui/node_modules/date-fns": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
|
||||
@@ -11232,7 +11151,7 @@
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iconv-lite": "^0.6.2"
|
||||
@@ -12878,7 +12797,7 @@
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
@@ -13620,9 +13539,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -13643,9 +13559,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -13666,9 +13579,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -13689,9 +13599,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -17072,7 +16979,7 @@
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sax": {
|
||||
|
||||
Reference in New Issue
Block a user