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;
|
action: ResolvedAction | null;
|
||||||
applicantName: string;
|
applicantName: string;
|
||||||
applicationNumber: 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. */
|
/** Populated for Assign and Escalate, which must name a person. */
|
||||||
officers?: Array<{ id: string; name: string | null }>;
|
officers?: Array<{ id: string; name: string | null }>;
|
||||||
submitting?: boolean;
|
submitting?: boolean;
|
||||||
@@ -81,7 +87,7 @@ export function DecisionConfirmModal({
|
|||||||
action,
|
action,
|
||||||
applicantName,
|
applicantName,
|
||||||
applicationNumber,
|
applicationNumber,
|
||||||
flaggedDocuments = [],
|
flaggedItems = [],
|
||||||
officers = [],
|
officers = [],
|
||||||
submitting,
|
submitting,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -97,9 +103,9 @@ export function DecisionConfirmModal({
|
|||||||
const [confirmText, setConfirmText] = useState('');
|
const [confirmText, setConfirmText] = useState('');
|
||||||
|
|
||||||
const codes = action ? (REASON_CODES[action.id] ?? []) : [];
|
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.
|
// 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
|
// Reset per opening, and seed the message the applicant will receive so the
|
||||||
// officer edits real copy rather than composing from nothing.
|
// officer edits real copy rather than composing from nothing.
|
||||||
@@ -107,7 +113,7 @@ export function DecisionConfirmModal({
|
|||||||
if (!action) return;
|
if (!action) return;
|
||||||
setReasonCode(null);
|
setReasonCode(null);
|
||||||
setReason('');
|
setReason('');
|
||||||
setDeficiencies(flaggedDocuments);
|
setDeficiencies(flaggedItems.map((item) => item.key));
|
||||||
setAcknowledged(false);
|
setAcknowledged(false);
|
||||||
setOfficerId(null);
|
setOfficerId(null);
|
||||||
setConfirmText('');
|
setConfirmText('');
|
||||||
@@ -216,7 +222,7 @@ export function DecisionConfirmModal({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 3. Deficiency checklist — the applicant sees exactly this list. */}
|
{/* 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
|
<Checkbox.Group
|
||||||
label={t('review.deficiencies', 'Items the applicant must correct')}
|
label={t('review.deficiencies', 'Items the applicant must correct')}
|
||||||
description={t(
|
description={t(
|
||||||
@@ -227,8 +233,8 @@ export function DecisionConfirmModal({
|
|||||||
onChange={setDeficiencies}
|
onChange={setDeficiencies}
|
||||||
>
|
>
|
||||||
<Stack gap={4} mt="xs">
|
<Stack gap={4} mt="xs">
|
||||||
{flaggedDocuments.map((key) => (
|
{flaggedItems.map((item) => (
|
||||||
<Checkbox key={key} value={key} label={key} />
|
<Checkbox key={item.key} value={item.key} label={item.label} />
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Checkbox.Group>
|
</Checkbox.Group>
|
||||||
|
|||||||
@@ -336,12 +336,16 @@ export function DocumentsTab({
|
|||||||
'Why must this document be corrected?',
|
'Why must this document be corrected?',
|
||||||
)}
|
)}
|
||||||
value={rejecting[attachment.documentKey]}
|
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) => ({
|
setRejecting((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[attachment.documentKey]: e.currentTarget.value,
|
[attachment.documentKey]: reason,
|
||||||
}))
|
}));
|
||||||
}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="compact-sm"
|
size="compact-sm"
|
||||||
|
|||||||
@@ -156,6 +156,35 @@ export function LicenseReviewPage() {
|
|||||||
const pendingInspection = inspections.find((i) => i.status === 'SCHEDULED');
|
const pendingInspection = inspections.find((i) => i.status === 'SCHEDULED');
|
||||||
const flagged = Object.entries(flags);
|
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(() => {
|
const actions = useMemo(() => {
|
||||||
if (!data) return [];
|
if (!data) return [];
|
||||||
return resolveActions({
|
return resolveActions({
|
||||||
@@ -300,29 +329,66 @@ export function LicenseReviewPage() {
|
|||||||
t('review.done.finalApprove', 'Approved'),
|
t('review.done.finalApprove', 'Approved'),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'request-adjustment':
|
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]) =>
|
||||||
|
submission.deficiencies.length
|
||||||
|
? submission.deficiencies.includes(key)
|
||||||
|
: true,
|
||||||
|
)
|
||||||
|
.map(([key, flag]) => ({
|
||||||
|
targetType: flag.targetType,
|
||||||
|
targetKey: key,
|
||||||
|
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 run(async () => {
|
||||||
await requestAdjustment({
|
await requestAdjustment({
|
||||||
id,
|
id,
|
||||||
generalRemark: submission.reason,
|
generalRemark: submission.reason,
|
||||||
notificationBody: submission.notificationBody,
|
notificationBody: submission.notificationBody,
|
||||||
items: flagged
|
items,
|
||||||
// Only the ticked deficiencies are sent, so the applicant can
|
|
||||||
// edit exactly the list they were shown.
|
|
||||||
.filter(([key]) =>
|
|
||||||
submission.deficiencies.length
|
|
||||||
? submission.deficiencies.includes(key)
|
|
||||||
: true,
|
|
||||||
)
|
|
||||||
.map(([key, flag]) => ({
|
|
||||||
targetType: flag.targetType,
|
|
||||||
targetKey: key,
|
|
||||||
remark: flag.remark,
|
|
||||||
})),
|
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
setFlags({});
|
setFlags({});
|
||||||
}, t('review.done.requestAdjustment', 'Adjustment requested'));
|
}, t('review.done.requestAdjustment', 'Adjustment requested'));
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case 'reject':
|
case 'reject':
|
||||||
await run(
|
await run(
|
||||||
() =>
|
() =>
|
||||||
@@ -579,17 +645,33 @@ export function LicenseReviewPage() {
|
|||||||
<TextInput
|
<TextInput
|
||||||
mt="xs"
|
mt="xs"
|
||||||
size="xs"
|
size="xs"
|
||||||
|
withAsterisk
|
||||||
placeholder={t(
|
placeholder={t(
|
||||||
'review.correctionPlaceholder',
|
'review.correctionPlaceholder',
|
||||||
'What must the applicant correct?',
|
'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}
|
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) => ({
|
setFlags((p) => ({
|
||||||
...p,
|
...p,
|
||||||
[sectionKey]: { ...p[sectionKey], remark: e.currentTarget.value },
|
[sectionKey]: { ...p[sectionKey], remark },
|
||||||
}))
|
}));
|
||||||
}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
@@ -660,6 +742,9 @@ export function LicenseReviewPage() {
|
|||||||
<Table.Th>{t('review.role', 'Role')}</Table.Th>
|
<Table.Th>{t('review.role', 'Role')}</Table.Th>
|
||||||
<Table.Th>{t('review.name', 'Name')}</Table.Th>
|
<Table.Th>{t('review.name', 'Name')}</Table.Th>
|
||||||
<Table.Th>{t('review.evidence', 'Evidence')}</Table.Th>
|
<Table.Th>{t('review.evidence', 'Evidence')}</Table.Th>
|
||||||
|
<Table.Th w={260}>
|
||||||
|
{t('review.correction', 'Correction')}
|
||||||
|
</Table.Th>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
</Table.Thead>
|
</Table.Thead>
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
@@ -680,6 +765,46 @@ export function LicenseReviewPage() {
|
|||||||
))}
|
))}
|
||||||
</Group>
|
</Group>
|
||||||
</Table.Td>
|
</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.Tr>
|
||||||
))}
|
))}
|
||||||
</Table.Tbody>
|
</Table.Tbody>
|
||||||
@@ -755,7 +880,7 @@ export function LicenseReviewPage() {
|
|||||||
action={pendingAction}
|
action={pendingAction}
|
||||||
applicantName={app.companyName ?? t('review.theApplicant', 'the applicant')}
|
applicantName={app.companyName ?? t('review.theApplicant', 'the applicant')}
|
||||||
applicationNumber={app.applicationNumber}
|
applicationNumber={app.applicationNumber}
|
||||||
flaggedDocuments={flagged.map(([key]) => key)}
|
flaggedItems={flaggedItems}
|
||||||
officers={officers}
|
officers={officers}
|
||||||
submitting={Boolean(busyAction)}
|
submitting={Boolean(busyAction)}
|
||||||
onClose={() => setPendingAction(null)}
|
onClose={() => setPendingAction(null)}
|
||||||
|
|||||||
Reference in New Issue
Block a user