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

View File

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

View File

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