Merge pull request #8 from Tria-plc/mulufeatures

Adding Cors rule in the environment variable
This commit is contained in:
mulish77
2026-08-03 15:34:33 +03:00
committed by GitHub
3 changed files with 167 additions and 32 deletions

View File

@@ -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>

View File

@@ -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"

View File

@@ -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)}