Add 'assign-reviewer' action and related functionality for license review process

This commit is contained in:
Nati
2026-08-26 13:34:06 +00:00
parent 207994df69
commit b5f1ffa6b8
6 changed files with 63 additions and 3 deletions

View File

@@ -134,7 +134,10 @@ export function DecisionConfirmModal({
if (!action) return null; if (!action) return null;
const needsOfficer = action.id === 'assign' || action.id === 'escalate'; const needsOfficer =
action.id === 'assign' ||
action.id === 'assign-reviewer' ||
action.id === 'escalate';
const reasonMissing = action.requiresReason && !reason.trim() && !reasonCode; const reasonMissing = action.requiresReason && !reason.trim() && !reasonCode;
const blocked = const blocked =
reasonMissing || reasonMissing ||

View File

@@ -18,6 +18,7 @@ export type ActionTier =
export type ActionId = export type ActionId =
| 'claim' | 'claim'
| 'assign' | 'assign'
| 'assign-reviewer'
| 'escalate' | 'escalate'
| 'hold' | 'hold'
| 'resume' | 'resume'
@@ -68,6 +69,18 @@ export const ACTIONS: ActionDefinition[] = [
// Claim is deliberately absent here: an officer claims from the queue // Claim is deliberately absent here: an officer claims from the queue
// (LicenseQueuePage), not from this detail page. That implementation is // (LicenseQueuePage), not from this detail page. That implementation is
// separate — see LicenseQueuePage/actions.tsx — and is unaffected by this. // separate — see LicenseQueuePage/actions.tsx — and is unaffected by this.
{
// Starts the review under the push model: the team leader hands a freshly
// submitted (or eligibility-paid) file to an employee. Without it a CoC/CoP
// sitting in ELIGIBILITY_PAID had no action at all — the server offers only
// `claim` (a permission no seeded role holds) and this.
id: 'assign-reviewer',
tier: 'primary',
labelKey: 'review.actions.assignReviewer',
from: ['SUBMITTED', 'ELIGIBILITY_PAID'],
permissions: ['can:assign:license-application'],
emphasis: 'filled',
},
{ {
id: 'assign', id: 'assign',
tier: 'workflow', tier: 'workflow',
@@ -302,6 +315,7 @@ export interface ResolveContext {
const WORKFLOW_EVENT_IDS = new Set<ActionId>([ const WORKFLOW_EVENT_IDS = new Set<ActionId>([
'claim', 'claim',
'assign', 'assign',
'assign-reviewer',
'escalate', 'escalate',
'hold', 'hold',
'resume', 'resume',
@@ -364,9 +378,13 @@ export function resolveActions(ctx: ResolveContext): ResolvedAction[] {
disabledReason: reason, disabledReason: reason,
}); });
// Decisions belong to whoever holds the application. // Decisions belong to whoever holds the application. Assigning a
// reviewer is the team leader handing work out, so it is exempt — the
// leader is by definition not the officer who will hold it.
const needsOwnership = const needsOwnership =
action.tier === 'primary' && action.id !== 'confirm-payment'; action.tier === 'primary' &&
action.id !== 'confirm-payment' &&
action.id !== 'assign-reviewer';
if ( if (
needsOwnership && needsOwnership &&
app.assignedOfficerId && app.assignedOfficerId &&

View File

@@ -39,6 +39,7 @@ import {
useLocalized, useLocalized,
useApproveDocumentsMutation, useApproveDocumentsMutation,
useAssignApplicationMutation, useAssignApplicationMutation,
useAssignReviewerMutation,
useClaimApplicationMutation, useClaimApplicationMutation,
useCompleteReviewMutation, useCompleteReviewMutation,
useConfirmPaymentMutation, useConfirmPaymentMutation,
@@ -217,6 +218,7 @@ export function LicenseReviewPage() {
const [resumeApplication] = useResumeApplicationMutation(); const [resumeApplication] = useResumeApplicationMutation();
const [escalateApplication] = useEscalateApplicationMutation(); const [escalateApplication] = useEscalateApplicationMutation();
const [assignApplication] = useAssignApplicationMutation(); const [assignApplication] = useAssignApplicationMutation();
const [assignReviewer] = useAssignReviewerMutation();
// Real officer list, so Assign and Escalate name a person instead of // Real officer list, so Assign and Escalate name a person instead of
// silently reassigning to whoever already held the application. // silently reassigning to whoever already held the application.
const { data: officers = [] } = useGetAssignableOfficersQuery(); const { data: officers = [] } = useGetAssignableOfficersQuery();
@@ -670,6 +672,18 @@ export function LicenseReviewPage() {
t("review.done.escalate", "Escalated"), t("review.done.escalate", "Escalated"),
); );
break; break;
case "assign-reviewer":
if (!submission.officerId) return;
await run(
() =>
assignReviewer({
id,
officerId: submission.officerId as string,
remark: submission.reason,
}).unwrap(),
t("review.done.assignReviewer", "Review assigned"),
);
break;
case "assign": case "assign":
if (!submission.officerId) return; if (!submission.officerId) return;
await run( await run(

View File

@@ -998,6 +998,7 @@ export const am: Translations = {
actions: { actions: {
claim: "ውሰድ", claim: "ውሰድ",
assign: "መድብ", assign: "መድብ",
assignReviewer: "ገምጋሚ መድብ",
escalate: "ወደ ላይ አሳድግ", escalate: "ወደ ላይ አሳድግ",
hold: "አግድ", hold: "አግድ",
resume: "ቀጥል", resume: "ቀጥል",
@@ -1056,6 +1057,7 @@ export const am: Translations = {
hold: "ለ{{applicant}} ማመልከቻ {{number}} ያግዳል። ለእርስዎ ተመድቦ ይቆያል።", hold: "ለ{{applicant}} ማመልከቻ {{number}} ያግዳል። ለእርስዎ ተመድቦ ይቆያል።",
resume: "ማመልከቻ {{number}} ወደ ታገደበት ደረጃ ይመልሳል።", resume: "ማመልከቻ {{number}} ወደ ታገደበት ደረጃ ይመልሳል።",
escalate: "ማመልከቻ {{number}} ለውሳኔ ወደ የበላይ ኃላፊ ያሳድጋል።", escalate: "ማመልከቻ {{number}} ለውሳኔ ወደ የበላይ ኃላፊ ያሳድጋል።",
"assign-reviewer": "ማመልከቻ {{number}} ለተመረጠው ሹም ሰጥቶ ግምገማውን ያስጀምራል።",
"confirm-payment": "ለማመልከቻ {{number}} ክፍያ ያረጋግጣል።", "confirm-payment": "ለማመልከቻ {{number}} ክፍያ ያረጋግጣል።",
"schedule-exam": "ለማመልከቻ {{number}} {{applicant}}ን ለፈተና ክፍለ ጊዜ ይመድባል።", "schedule-exam": "ለማመልከቻ {{number}} {{applicant}}ን ለፈተና ክፍለ ጊዜ ይመድባል።",
}, },
@@ -1150,6 +1152,7 @@ export const am: Translations = {
resume: "ማመልከቻው ቀጥሏል", resume: "ማመልከቻው ቀጥሏል",
escalate: "ወደ ላይ አድጓል", escalate: "ወደ ላይ አድጓል",
assign: "እንደገና ተመድቧል", assign: "እንደገና ተመድቧል",
assignReviewer: "ግምገማ ተመድቧል",
scheduled: "ምርመራ ተይዟል", scheduled: "ምርመራ ተይዟል",
inspectionPassed: "ምርመራ አልፏል", inspectionPassed: "ምርመራ አልፏል",
inspectionFailed: "ምርመራ ወድቋል", inspectionFailed: "ምርመራ ወድቋል",

View File

@@ -1007,6 +1007,7 @@ export const en = {
actions: { actions: {
claim: 'Claim', claim: 'Claim',
assign: 'Assign', assign: 'Assign',
assignReviewer: 'Assign reviewer',
escalate: 'Escalate', escalate: 'Escalate',
hold: 'Put on hold', hold: 'Put on hold',
resume: 'Resume', resume: 'Resume',
@@ -1064,6 +1065,8 @@ export const en = {
hold: 'Parks application {{number}} for {{applicant}}. It stays assigned to you.', hold: 'Parks application {{number}} for {{applicant}}. It stays assigned to you.',
resume: 'Returns application {{number}} to the stage it was held from.', resume: 'Returns application {{number}} to the stage it was held from.',
escalate: 'Raises application {{number}} to a supervisor for a decision.', escalate: 'Raises application {{number}} to a supervisor for a decision.',
'assign-reviewer':
'Hands application {{number}} to the chosen officer and starts the review.',
'confirm-payment': 'Confirms settlement for application {{number}}.', 'confirm-payment': 'Confirms settlement for application {{number}}.',
'schedule-exam': 'Assigns {{applicant}} to an exam session for application {{number}}.', 'schedule-exam': 'Assigns {{applicant}} to an exam session for application {{number}}.',
}, },
@@ -1155,6 +1158,7 @@ export const en = {
resume: 'Application resumed', resume: 'Application resumed',
escalate: 'Escalated', escalate: 'Escalated',
assign: 'Reassigned', assign: 'Reassigned',
assignReviewer: 'Review assigned',
scheduled: 'Inspection scheduled', scheduled: 'Inspection scheduled',
inspectionPassed: 'Inspection passed', inspectionPassed: 'Inspection passed',
inspectionFailed: 'Inspection failed', inspectionFailed: 'Inspection failed',

View File

@@ -856,6 +856,23 @@ export const licensingApi = baseApi
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')], error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
}), }),
/**
* Starts the review: the team leader hands the file to an employee.
* `assign` above only re-points an application already in flight.
*/
assignReviewer: builder.mutation<
LicenseApplication,
{ id: string; officerId: string; remark?: string }
>({
query: ({ id, ...body }) => ({
url: `/license-application-review/${id}/assign-reviewer`,
method: 'POST',
body,
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
}),
holdApplication: builder.mutation< holdApplication: builder.mutation<
LicenseApplication, LicenseApplication,
{ id: string; reason: string } { id: string; reason: string }
@@ -1050,6 +1067,7 @@ export const {
useRevokeLicenseMutation, useRevokeLicenseMutation,
useReinstateLicenseMutation, useReinstateLicenseMutation,
useAssignApplicationMutation, useAssignApplicationMutation,
useAssignReviewerMutation,
useHoldApplicationMutation, useHoldApplicationMutation,
useResumeApplicationMutation, useResumeApplicationMutation,
useEscalateApplicationMutation, useEscalateApplicationMutation,