mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
Merge pull request #8 from Tria-plc/mulufeatures
Adding Cors rule in the environment variable
This commit is contained in:
@@ -57,8 +57,14 @@ interface DecisionConfirmModalProps {
|
||||
action: ResolvedAction | null;
|
||||
applicantName: string;
|
||||
applicationNumber: string;
|
||||
/** Document keys the officer flagged, for the deficiency checklist. */
|
||||
flaggedDocuments?: string[];
|
||||
/**
|
||||
* What the officer flagged, for the deficiency checklist.
|
||||
*
|
||||
* Carries a label as well as the key because the key is only human-readable
|
||||
* for documents: a form section is camelCase and a flagged staff member is a
|
||||
* uuid, which is not something to put in front of an officer.
|
||||
*/
|
||||
flaggedItems?: Array<{ key: string; label: string }>;
|
||||
/** Populated for Assign and Escalate, which must name a person. */
|
||||
officers?: Array<{ id: string; name: string | null }>;
|
||||
submitting?: boolean;
|
||||
@@ -81,7 +87,7 @@ export function DecisionConfirmModal({
|
||||
action,
|
||||
applicantName,
|
||||
applicationNumber,
|
||||
flaggedDocuments = [],
|
||||
flaggedItems = [],
|
||||
officers = [],
|
||||
submitting,
|
||||
onClose,
|
||||
@@ -97,9 +103,9 @@ export function DecisionConfirmModal({
|
||||
const [confirmText, setConfirmText] = useState('');
|
||||
|
||||
const codes = action ? (REASON_CODES[action.id] ?? []) : [];
|
||||
// `flaggedDocuments` is a fresh array on every parent render, so keying the
|
||||
// `flaggedItems` is a fresh array on every parent render, so keying the
|
||||
// reset effect on its identity would wipe the officer's edits continuously.
|
||||
const flaggedKey = flaggedDocuments.join('|');
|
||||
const flaggedKey = flaggedItems.map((item) => item.key).join('|');
|
||||
|
||||
// Reset per opening, and seed the message the applicant will receive so the
|
||||
// officer edits real copy rather than composing from nothing.
|
||||
@@ -107,7 +113,7 @@ export function DecisionConfirmModal({
|
||||
if (!action) return;
|
||||
setReasonCode(null);
|
||||
setReason('');
|
||||
setDeficiencies(flaggedDocuments);
|
||||
setDeficiencies(flaggedItems.map((item) => item.key));
|
||||
setAcknowledged(false);
|
||||
setOfficerId(null);
|
||||
setConfirmText('');
|
||||
@@ -216,7 +222,7 @@ export function DecisionConfirmModal({
|
||||
)}
|
||||
|
||||
{/* 3. Deficiency checklist — the applicant sees exactly this list. */}
|
||||
{action.id === 'request-adjustment' && flaggedDocuments.length > 0 && (
|
||||
{action.id === 'request-adjustment' && flaggedItems.length > 0 && (
|
||||
<Checkbox.Group
|
||||
label={t('review.deficiencies', 'Items the applicant must correct')}
|
||||
description={t(
|
||||
@@ -227,8 +233,8 @@ export function DecisionConfirmModal({
|
||||
onChange={setDeficiencies}
|
||||
>
|
||||
<Stack gap={4} mt="xs">
|
||||
{flaggedDocuments.map((key) => (
|
||||
<Checkbox key={key} value={key} label={key} />
|
||||
{flaggedItems.map((item) => (
|
||||
<Checkbox key={item.key} value={item.key} label={item.label} />
|
||||
))}
|
||||
</Stack>
|
||||
</Checkbox.Group>
|
||||
|
||||
@@ -336,12 +336,16 @@ export function DocumentsTab({
|
||||
'Why must this document be corrected?',
|
||||
)}
|
||||
value={rejecting[attachment.documentKey]}
|
||||
onChange={(e) =>
|
||||
onChange={(e) => {
|
||||
// Read before the updater: React nulls `currentTarget`
|
||||
// once the handler returns, and the updater runs later,
|
||||
// during the re-render.
|
||||
const reason = e.currentTarget.value;
|
||||
setRejecting((prev) => ({
|
||||
...prev,
|
||||
[attachment.documentKey]: e.currentTarget.value,
|
||||
}))
|
||||
}
|
||||
[attachment.documentKey]: reason,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="compact-sm"
|
||||
|
||||
@@ -156,6 +156,35 @@ export function LicenseReviewPage() {
|
||||
const pendingInspection = inspections.find((i) => i.status === 'SCHEDULED');
|
||||
const flagged = Object.entries(flags);
|
||||
|
||||
/**
|
||||
* The flagged items as the officer should see them in the confirmation.
|
||||
*
|
||||
* Only a document key reads acceptably on its own. A form section is
|
||||
* camelCase, and a staff flag is keyed by the ApplicationStaff id — showing
|
||||
* a uuid in the checklist would make the list impossible to tick with
|
||||
* confidence.
|
||||
*/
|
||||
const flaggedItems = useMemo(
|
||||
() =>
|
||||
flagged.map(([key, flag]) => {
|
||||
if (flag.targetType === 'STAFF') {
|
||||
const member = data?.staff.find((s) => s.id === key);
|
||||
return {
|
||||
key,
|
||||
label: member
|
||||
? `${member.roleKey} — ${member.fullName}`
|
||||
: t('review.staffMember', 'Staff member'),
|
||||
};
|
||||
}
|
||||
if (flag.targetType === 'FORM_SECTION') {
|
||||
return { key, label: key.replace(/([A-Z])/g, ' $1').trim() };
|
||||
}
|
||||
return { key, label: key };
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[flags, data?.staff, t],
|
||||
);
|
||||
|
||||
const actions = useMemo(() => {
|
||||
if (!data) return [];
|
||||
return resolveActions({
|
||||
@@ -300,13 +329,8 @@ export function LicenseReviewPage() {
|
||||
t('review.done.finalApprove', 'Approved'),
|
||||
);
|
||||
break;
|
||||
case 'request-adjustment':
|
||||
await run(async () => {
|
||||
await requestAdjustment({
|
||||
id,
|
||||
generalRemark: submission.reason,
|
||||
notificationBody: submission.notificationBody,
|
||||
items: flagged
|
||||
case 'request-adjustment': {
|
||||
const items = flagged
|
||||
// Only the ticked deficiencies are sent, so the applicant can
|
||||
// edit exactly the list they were shown.
|
||||
.filter(([key]) =>
|
||||
@@ -317,12 +341,54 @@ export function LicenseReviewPage() {
|
||||
.map(([key, flag]) => ({
|
||||
targetType: flag.targetType,
|
||||
targetKey: key,
|
||||
remark: flag.remark,
|
||||
})),
|
||||
remark: flag.remark.trim(),
|
||||
}));
|
||||
|
||||
// Each item is an instruction the applicant has to act on, so the
|
||||
// API requires wording for every one. Catching it here names the
|
||||
// items that need it; letting it through produced a bare 400 saying
|
||||
// "items.0.remark should not be empty", which tells an officer
|
||||
// nothing about which box to go and fill in.
|
||||
const unexplained = items.filter((item) => !item.remark);
|
||||
if (items.length === 0 || unexplained.length > 0) {
|
||||
notifications.show({
|
||||
color: 'orange',
|
||||
title:
|
||||
items.length === 0
|
||||
? t(
|
||||
'review.adjustment.nothingFlagged',
|
||||
'Nothing has been flagged',
|
||||
)
|
||||
: t(
|
||||
'review.adjustment.reasonsMissing',
|
||||
'Every flagged item needs a reason',
|
||||
),
|
||||
message:
|
||||
items.length === 0
|
||||
? t(
|
||||
'review.adjustment.nothingFlaggedHint',
|
||||
'Tick what the applicant must correct before requesting an adjustment.',
|
||||
)
|
||||
: `${t(
|
||||
'review.adjustment.reasonsMissingHint',
|
||||
'Say what must be corrected for:',
|
||||
)} ${unexplained.map((item) => item.targetKey).join(', ')}`,
|
||||
});
|
||||
// `finally` closes the modal and clears the busy flag.
|
||||
return;
|
||||
}
|
||||
|
||||
await run(async () => {
|
||||
await requestAdjustment({
|
||||
id,
|
||||
generalRemark: submission.reason,
|
||||
notificationBody: submission.notificationBody,
|
||||
items,
|
||||
}).unwrap();
|
||||
setFlags({});
|
||||
}, t('review.done.requestAdjustment', 'Adjustment requested'));
|
||||
break;
|
||||
}
|
||||
case 'reject':
|
||||
await run(
|
||||
() =>
|
||||
@@ -579,17 +645,33 @@ export function LicenseReviewPage() {
|
||||
<TextInput
|
||||
mt="xs"
|
||||
size="xs"
|
||||
withAsterisk
|
||||
placeholder={t(
|
||||
'review.correctionPlaceholder',
|
||||
'What must the applicant correct?',
|
||||
)}
|
||||
// Flagging without saying why is what the applicant
|
||||
// would receive: "fix this section", and nothing else.
|
||||
error={
|
||||
flags[sectionKey].remark.trim()
|
||||
? null
|
||||
: t(
|
||||
'review.correctionRequired',
|
||||
'Say what must be corrected',
|
||||
)
|
||||
}
|
||||
value={flags[sectionKey].remark}
|
||||
onChange={(e) =>
|
||||
onChange={(e) => {
|
||||
// Read here, not inside the updater: React nulls
|
||||
// `currentTarget` when the handler returns, and the
|
||||
// updater runs afterwards during the re-render —
|
||||
// which crashed the page on the first keystroke.
|
||||
const remark = e.currentTarget.value;
|
||||
setFlags((p) => ({
|
||||
...p,
|
||||
[sectionKey]: { ...p[sectionKey], remark: e.currentTarget.value },
|
||||
}))
|
||||
}
|
||||
[sectionKey]: { ...p[sectionKey], remark },
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
@@ -660,6 +742,9 @@ export function LicenseReviewPage() {
|
||||
<Table.Th>{t('review.role', 'Role')}</Table.Th>
|
||||
<Table.Th>{t('review.name', 'Name')}</Table.Th>
|
||||
<Table.Th>{t('review.evidence', 'Evidence')}</Table.Th>
|
||||
<Table.Th w={260}>
|
||||
{t('review.correction', 'Correction')}
|
||||
</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
@@ -680,6 +765,46 @@ export function LicenseReviewPage() {
|
||||
))}
|
||||
</Group>
|
||||
</Table.Td>
|
||||
{/* A person's papers are as returnable as a document
|
||||
or a form section: an ERB certificate for the wrong
|
||||
person is a defect the applicant has to fix, and
|
||||
until now the officer had to describe it under some
|
||||
unrelated document. */}
|
||||
<Table.Td>
|
||||
<Checkbox
|
||||
size="xs"
|
||||
label={t('review.needsCorrection', 'Needs correction')}
|
||||
checked={Boolean(flags[member.id])}
|
||||
onChange={() => toggleFlag('STAFF', member.id)}
|
||||
/>
|
||||
{flags[member.id] && (
|
||||
<TextInput
|
||||
mt={6}
|
||||
size="xs"
|
||||
withAsterisk
|
||||
placeholder={t(
|
||||
'review.correctionPlaceholder',
|
||||
'What must the applicant correct?',
|
||||
)}
|
||||
error={
|
||||
flags[member.id].remark.trim()
|
||||
? null
|
||||
: t(
|
||||
'review.correctionRequired',
|
||||
'Say what must be corrected',
|
||||
)
|
||||
}
|
||||
value={flags[member.id].remark}
|
||||
onChange={(e) => {
|
||||
const remark = e.currentTarget.value;
|
||||
setFlags((p) => ({
|
||||
...p,
|
||||
[member.id]: { ...p[member.id], remark },
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
@@ -755,7 +880,7 @@ export function LicenseReviewPage() {
|
||||
action={pendingAction}
|
||||
applicantName={app.companyName ?? t('review.theApplicant', 'the applicant')}
|
||||
applicationNumber={app.applicationNumber}
|
||||
flaggedDocuments={flagged.map(([key]) => key)}
|
||||
flaggedItems={flaggedItems}
|
||||
officers={officers}
|
||||
submitting={Boolean(busyAction)}
|
||||
onClose={() => setPendingAction(null)}
|
||||
|
||||
Reference in New Issue
Block a user