mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
feat: implement 18+ age validation, enforce document review completion for approvals, and refactor DecisionBar UI
This commit is contained in:
@@ -76,9 +76,13 @@ export function DecisionBar({
|
||||
role="region"
|
||||
aria-label={t('review.decisionBar', 'Decision bar')}
|
||||
>
|
||||
<Group justify="space-between" wrap="nowrap" gap="md">
|
||||
{/* Wraps rather than overflows: at narrow widths the nowrap row pushed
|
||||
the workflow buttons past the viewport edge, so Assign, Escalate and
|
||||
Hold were simply not there. Wrapping drops them onto a second line
|
||||
instead of off the screen. */}
|
||||
<Group justify="space-between" wrap="wrap" gap="sm">
|
||||
{/* Left: where the application stands, and who has it. */}
|
||||
<Group gap="sm" wrap="nowrap" style={{ minWidth: 0 }}>
|
||||
<Group gap="sm" wrap="wrap" style={{ minWidth: 0, flex: '1 1 auto' }}>
|
||||
<Badge color={STATUS_COLORS[status]} variant="light" size="lg">
|
||||
{t(`queue.statusValues.${status}`, STATUS_LABELS[status])}
|
||||
</Badge>
|
||||
@@ -124,7 +128,7 @@ export function DecisionBar({
|
||||
</Group>
|
||||
|
||||
{/* Right: the decision. */}
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Group gap="xs" wrap="wrap" justify="flex-end" style={{ flex: '0 1 auto' }}>
|
||||
{primary.map((action) => (
|
||||
<ActionButton
|
||||
key={action.id}
|
||||
@@ -194,10 +198,17 @@ function ActionButton({ action, busy, size, onAction }: ActionButtonProps) {
|
||||
const button = (
|
||||
<Button
|
||||
size={size}
|
||||
variant={action.emphasis === 'filled' ? 'filled' : action.emphasis ?? 'light'}
|
||||
variant={
|
||||
action.emphasis === 'filled'
|
||||
? 'filled'
|
||||
: action.emphasis === 'subtle'
|
||||
? 'default'
|
||||
: 'light'
|
||||
}
|
||||
color={action.color}
|
||||
loading={busy}
|
||||
disabled={!action.enabled}
|
||||
style={{ flexShrink: 0 }}
|
||||
onClick={() => onAction(action)}
|
||||
>
|
||||
{t(action.labelKey)}
|
||||
|
||||
@@ -253,11 +253,18 @@ export interface ResolveContext {
|
||||
needsFlags: string;
|
||||
needsCapital: string;
|
||||
needsInspection: string;
|
||||
needsDocumentReviews: 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;
|
||||
/**
|
||||
* False while any uploaded document is still unjudged or rejected. Approving
|
||||
* is a statement that every document was checked, so the button stays dead
|
||||
* until the officer has actually judged each one.
|
||||
*/
|
||||
allDocumentsAccepted: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -300,6 +307,13 @@ export function resolveActions(ctx: ResolveContext): ResolvedAction[] {
|
||||
return disabled(reasons.notAssigned);
|
||||
}
|
||||
|
||||
if (
|
||||
(action.id === 'approve-documents' || action.id === 'final-approve') &&
|
||||
!ctx.allDocumentsAccepted
|
||||
) {
|
||||
return disabled(reasons.needsDocumentReviews);
|
||||
}
|
||||
|
||||
if (action.id === 'request-adjustment' && ctx.flaggedCount === 0) {
|
||||
return disabled(reasons.needsFlags);
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
useFinalApproveMutation,
|
||||
useGetApplicationForReviewQuery,
|
||||
useGetAttachmentsQuery,
|
||||
useGetDocumentReviewsQuery,
|
||||
useGetInspectionsQuery,
|
||||
useGetAssignableOfficersQuery,
|
||||
useGetLicenseTypeRequirementsQuery,
|
||||
@@ -162,6 +163,9 @@ export function LicenseReviewPage() {
|
||||
// Real officer list, so Assign and Escalate name a person instead of
|
||||
// silently reassigning to whoever already held the application.
|
||||
const { data: officers = [] } = useGetAssignableOfficersQuery();
|
||||
// Same cached query the Documents tab reads, so the decision bar reacts the
|
||||
// moment a verdict is saved.
|
||||
const { data: documentReviews = [] } = useGetDocumentReviewsQuery(id, { skip: !id });
|
||||
|
||||
const staffTable = useServerTable();
|
||||
const [flags, setFlags] = useState<FlagMap>({});
|
||||
@@ -208,6 +212,19 @@ export function LicenseReviewPage() {
|
||||
}, [flags]);
|
||||
|
||||
const pendingInspection = inspections.find((i) => i.status === 'SCHEDULED');
|
||||
|
||||
// Approving means every uploaded document was accepted — one unjudged or
|
||||
// rejected file is enough to keep the decision buttons dead.
|
||||
const allDocumentsAccepted = useMemo(() => {
|
||||
const attachments = data?.attachments ?? [];
|
||||
if (attachments.length === 0) return false;
|
||||
const accepted = new Set(
|
||||
documentReviews
|
||||
.filter((review) => review.decision === 'ACCEPTED')
|
||||
.map((review) => review.documentKey),
|
||||
);
|
||||
return attachments.every((a) => accepted.has(a.documentKey));
|
||||
}, [data?.attachments, documentReviews]);
|
||||
const flagged = Object.entries(flags);
|
||||
|
||||
/**
|
||||
@@ -247,6 +264,7 @@ export function LicenseReviewPage() {
|
||||
can,
|
||||
flaggedCount: flagged.length,
|
||||
hasPendingInspection: Boolean(pendingInspection),
|
||||
allDocumentsAccepted,
|
||||
reasons: {
|
||||
wrongStatus: t('review.disabled.wrongStatus', 'Not available at this stage'),
|
||||
notAssigned: t('review.disabled.notAssigned', 'Assigned to another officer'),
|
||||
@@ -254,9 +272,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'),
|
||||
needsDocumentReviews: t(
|
||||
'review.disabled.needsDocumentReviews',
|
||||
'Accept every document first',
|
||||
),
|
||||
},
|
||||
});
|
||||
}, [data, currentUserId, can, flagged.length, pendingInspection, t]);
|
||||
}, [data, currentUserId, can, flagged.length, pendingInspection, allDocumentsAccepted, t]);
|
||||
|
||||
if (isLoading) {
|
||||
// Skeleton mirrors the real three-zone layout so nothing jumps on load.
|
||||
|
||||
@@ -949,6 +949,7 @@ export const am: Translations = {
|
||||
needsFlags: "ማስተካከያ ለመጠየቅ ቢያንስ አንድ ነገር ምልክት ያድርጉ",
|
||||
needsCapital: "መጀመሪያ የተረጋገጠውን ካፒታል ይመዝግቡ",
|
||||
needsInspection: "የምርመራ ውጤት ያስፈልጋል",
|
||||
needsDocumentReviews: "መጀመሪያ ሁሉንም ሰነዶች ተቀበል",
|
||||
},
|
||||
reasons: {
|
||||
incompleteDocuments: "ያልተሟሉ ሰነዶች",
|
||||
|
||||
@@ -950,6 +950,7 @@ export const en = {
|
||||
needsFlags: 'Flag at least one item to request a correction',
|
||||
needsCapital: 'Record the verified capital first',
|
||||
needsInspection: 'Requires an inspection result',
|
||||
needsDocumentReviews: 'Accept every document first',
|
||||
},
|
||||
reasons: {
|
||||
incompleteDocuments: 'Incomplete documents',
|
||||
|
||||
Reference in New Issue
Block a user