mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-29 21:08:13 +00:00
feat: add INSPECTION_FAILED state, enforce inspection scheduling locks, and improve adjustment round UI for section locking and remark labeling
This commit is contained in:
@@ -78,6 +78,7 @@ export const ACTIONS: ActionDefinition[] = [
|
||||
'UNDER_EVALUATION',
|
||||
'INSPECTION_PENDING',
|
||||
'INSPECTION_COMPLETED',
|
||||
'INSPECTION_FAILED',
|
||||
'ON_HOLD',
|
||||
],
|
||||
permissions: ['can:assign:license-application'],
|
||||
@@ -101,6 +102,7 @@ export const ACTIONS: ActionDefinition[] = [
|
||||
'UNDER_EVALUATION',
|
||||
'INSPECTION_PENDING',
|
||||
'INSPECTION_COMPLETED',
|
||||
'INSPECTION_FAILED',
|
||||
],
|
||||
permissions: ['can:hold:license-application'],
|
||||
emphasis: 'subtle',
|
||||
@@ -139,7 +141,8 @@ export const ACTIONS: ActionDefinition[] = [
|
||||
id: 'schedule-inspection',
|
||||
tier: 'primary',
|
||||
labelKey: 'review.actions.scheduleInspection',
|
||||
from: ['INSPECTION_PENDING'],
|
||||
// INSPECTION_FAILED: booking the re-inspection after a failed visit.
|
||||
from: ['INSPECTION_PENDING', 'INSPECTION_FAILED'],
|
||||
permissions: ['can:create:inspection'],
|
||||
emphasis: 'filled',
|
||||
},
|
||||
@@ -147,7 +150,7 @@ export const ACTIONS: ActionDefinition[] = [
|
||||
id: 'record-inspection',
|
||||
tier: 'primary',
|
||||
labelKey: 'review.actions.recordInspection',
|
||||
from: ['INSPECTION_PENDING'],
|
||||
from: ['INSPECTION_PENDING', 'INSPECTION_FAILED'],
|
||||
permissions: ['can:update:inspection'],
|
||||
emphasis: 'filled',
|
||||
},
|
||||
@@ -167,7 +170,13 @@ export const ACTIONS: ActionDefinition[] = [
|
||||
id: 'request-adjustment',
|
||||
tier: 'primary',
|
||||
labelKey: 'review.actions.requestAdjustment',
|
||||
from: ['UNDER_REVIEW', 'UNDER_EVALUATION', 'INSPECTION_COMPLETED', 'ELIGIBILITY_PAID'],
|
||||
from: [
|
||||
'UNDER_REVIEW',
|
||||
'UNDER_EVALUATION',
|
||||
'INSPECTION_COMPLETED',
|
||||
'INSPECTION_FAILED',
|
||||
'ELIGIBILITY_PAID',
|
||||
],
|
||||
permissions: ['can:request-adjustment:license-application'],
|
||||
emphasis: 'light',
|
||||
color: 'orange',
|
||||
@@ -182,6 +191,7 @@ export const ACTIONS: ActionDefinition[] = [
|
||||
'UNDER_EVALUATION',
|
||||
'INSPECTION_PENDING',
|
||||
'INSPECTION_COMPLETED',
|
||||
'INSPECTION_FAILED',
|
||||
'ELIGIBILITY_PAID',
|
||||
],
|
||||
permissions: ['can:reject:license-application'],
|
||||
@@ -282,11 +292,18 @@ export interface ResolveContext {
|
||||
needsCapital: string;
|
||||
needsInspection: string;
|
||||
needsDocumentReviews: string;
|
||||
inspectionNotYetDue: string;
|
||||
};
|
||||
/** Number of sections/documents the officer has flagged for correction. */
|
||||
flaggedCount: number;
|
||||
/** True when an inspection is scheduled and awaiting a result. */
|
||||
hasPendingInspection: boolean;
|
||||
/**
|
||||
* True while the booked inspection's scheduled instant is still in the
|
||||
* future — a visit cannot have an outcome before it happens, so the record
|
||||
* button waits (the server refuses early results the same way).
|
||||
*/
|
||||
inspectionNotYetDue: boolean;
|
||||
/**
|
||||
* False while any uploaded document is still unjudged or rejected. Approving
|
||||
* is a statement that every document was checked, so the button stays dead
|
||||
@@ -379,6 +396,12 @@ export function resolveActions(ctx: ResolveContext): ResolvedAction[] {
|
||||
disabledReason: reason,
|
||||
});
|
||||
|
||||
// Booked but not yet due: keep the button visible so the officer sees
|
||||
// the next step, disabled with the scheduled time as the reason.
|
||||
if (action.id === 'record-inspection' && ctx.inspectionNotYetDue) {
|
||||
return disabled(reasons.inspectionNotYetDue);
|
||||
}
|
||||
|
||||
// Decisions belong to whoever holds the application.
|
||||
const needsOwnership =
|
||||
action.tier === 'primary' && action.id !== 'confirm-payment';
|
||||
|
||||
@@ -279,6 +279,12 @@ export function LicenseReviewPage() {
|
||||
}, [flags]);
|
||||
|
||||
const pendingInspection = inspections.find((i) => i.status === 'SCHEDULED');
|
||||
// A visit cannot have an outcome before it happens — mirror of the server's
|
||||
// inspection_not_yet_due guard, compared instant-to-instant.
|
||||
const inspectionNotYetDue = Boolean(
|
||||
pendingInspection?.scheduledDate &&
|
||||
new Date(pendingInspection.scheduledDate) > new Date(),
|
||||
);
|
||||
|
||||
// Approving means every uploaded document was accepted — one unjudged or
|
||||
// rejected file is enough to keep the decision buttons dead. The counts feed
|
||||
@@ -335,6 +341,7 @@ export function LicenseReviewPage() {
|
||||
can,
|
||||
flaggedCount: flagged.length,
|
||||
hasPendingInspection: Boolean(pendingInspection),
|
||||
inspectionNotYetDue,
|
||||
allDocumentsAccepted,
|
||||
reasons: {
|
||||
wrongStatus: t('review.disabled.wrongStatus', 'Not available at this stage'),
|
||||
@@ -343,6 +350,13 @@ export function LicenseReviewPage() {
|
||||
needsFlags: t('review.disabled.needsFlags', 'Flag at least one item to request a correction'),
|
||||
needsCapital: t('review.disabled.needsCapital', 'Record the verified capital first'),
|
||||
needsInspection: t('review.disabled.needsInspection', 'Requires an inspection result'),
|
||||
inspectionNotYetDue: t('review.disabled.inspectionNotYetDue', {
|
||||
date: pendingInspection?.scheduledDate
|
||||
? showDate(pendingInspection.scheduledDate)
|
||||
: '',
|
||||
defaultValue:
|
||||
'Inspection scheduled for {{date}}. Results can be recorded after the scheduled time.',
|
||||
}),
|
||||
needsDocumentReviews:
|
||||
documentProgress.total === 0
|
||||
? t(
|
||||
@@ -357,7 +371,7 @@ export function LicenseReviewPage() {
|
||||
}),
|
||||
},
|
||||
});
|
||||
}, [data, currentUserId, can, flagged.length, pendingInspection, t]);
|
||||
}, [data, currentUserId, can, flagged.length, pendingInspection, inspectionNotYetDue, showDate, t]);
|
||||
|
||||
// Location answers are tree ids. The picker the applicant used resolves them
|
||||
// client-side from the same list, so the reviewer reads the place rather than
|
||||
@@ -1063,6 +1077,18 @@ export function LicenseReviewPage() {
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="inspection">
|
||||
{status === "INSPECTION_FAILED" && (
|
||||
<Alert
|
||||
mb="md"
|
||||
color="red"
|
||||
icon={<IconAlertTriangle size={16} />}
|
||||
>
|
||||
{t(
|
||||
"review.inspectionFailedBlocked",
|
||||
"Approval is unavailable because the inspection failed. Schedule a re-inspection, request corrections, or reject the application.",
|
||||
)}
|
||||
</Alert>
|
||||
)}
|
||||
<Paper withBorder p="md">
|
||||
{inspections.length === 0 ? (
|
||||
<Text size="sm" c="dimmed">
|
||||
@@ -1386,12 +1412,23 @@ export function LicenseReviewPage() {
|
||||
autosize
|
||||
minRows={3}
|
||||
/>
|
||||
{inspectionNotYetDue && (
|
||||
<Alert color="yellow" icon={<IconAlertTriangle size={16} />}>
|
||||
{t("review.disabled.inspectionNotYetDue", {
|
||||
date: pendingInspection?.scheduledDate
|
||||
? showDate(pendingInspection.scheduledDate)
|
||||
: "",
|
||||
defaultValue:
|
||||
"Inspection scheduled for {{date}}. Results can be recorded after the scheduled time.",
|
||||
})}
|
||||
</Alert>
|
||||
)}
|
||||
<ModalFooter grow>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
color="teal"
|
||||
size="lg"
|
||||
disabled={!findings.trim() || !pendingInspection}
|
||||
disabled={!findings.trim() || !pendingInspection || inspectionNotYetDue}
|
||||
aria-label={t("review.passed", "Passed")}
|
||||
onClick={() =>
|
||||
run(
|
||||
@@ -1418,7 +1455,7 @@ export function LicenseReviewPage() {
|
||||
variant="light"
|
||||
color="red"
|
||||
size="lg"
|
||||
disabled={!findings.trim() || !pendingInspection}
|
||||
disabled={!findings.trim() || !pendingInspection || inspectionNotYetDue}
|
||||
aria-label={t("review.failed", "Failed")}
|
||||
onClick={() =>
|
||||
run(
|
||||
|
||||
@@ -1027,10 +1027,14 @@ export const am: Translations = {
|
||||
needsFlags: "ማስተካከያ ለመጠየቅ ቢያንስ አንድ ነገር ምልክት ያድርጉ",
|
||||
needsCapital: "መጀመሪያ የተረጋገጠውን ካፒታል ይመዝግቡ",
|
||||
needsInspection: "የምርመራ ውጤት ያስፈልጋል",
|
||||
inspectionNotYetDue:
|
||||
"ምርመራ ለ{{date}} ተይዟል። ውጤቶች ከተያዘው ሰዓት በኋላ መመዝገብ ይችላሉ።",
|
||||
needsDocumentReviews:
|
||||
"መጀመሪያ ሁሉንም ሰነዶች ይቀበሉ — ከ{{total}} {{accepted}} ተቀብለዋል። የሰነዶች ትር ከፍተው ቀሪዎቹን ይቀበሉ።",
|
||||
needsDocumentsUploaded: "እስካሁን የሚገመገም ሰነድ አልተጫነም",
|
||||
},
|
||||
inspectionFailedBlocked:
|
||||
"ምርመራው ስላልተሳካ ማጽደቅ አይቻልም። ድጋሚ ምርመራ ያስይዙ፣ ማስተካከያ ይጠይቁ ወይም ማመልከቻውን ውድቅ ያድርጉ።",
|
||||
reasons: {
|
||||
incompleteDocuments: "ያልተሟሉ ሰነዶች",
|
||||
belowCapital: "ካፒታል ከሚያስፈልገው በታች",
|
||||
|
||||
@@ -1036,10 +1036,14 @@ export const en = {
|
||||
needsFlags: 'Flag at least one item to request a correction',
|
||||
needsCapital: 'Record the verified capital first',
|
||||
needsInspection: 'Requires an inspection result',
|
||||
inspectionNotYetDue:
|
||||
'Inspection scheduled for {{date}}. Results can be recorded after the scheduled time.',
|
||||
needsDocumentReviews:
|
||||
'Accept all documents first — {{accepted}} of {{total}} accepted. Open the Documents tab and accept the rest.',
|
||||
needsDocumentsUploaded: 'No documents uploaded to review yet',
|
||||
},
|
||||
inspectionFailedBlocked:
|
||||
'Approval is unavailable because the inspection failed. Schedule a re-inspection, request corrections, or reject the application.',
|
||||
reasons: {
|
||||
incompleteDocuments: 'Incomplete documents',
|
||||
belowCapital: 'Capital below the required minimum',
|
||||
|
||||
Reference in New Issue
Block a user