From b5f1ffa6b8a9937879601969cb454fdcf0b7b8a2 Mon Sep 17 00:00:00 2001 From: Nati Date: Wed, 26 Aug 2026 13:34:06 +0000 Subject: [PATCH 1/2] Add 'assign-reviewer' action and related functionality for license review process --- .../components/DecisionConfirmModal.tsx | 5 ++++- .../features/license-review/config/actions.ts | 22 +++++++++++++++++-- .../pages/LicenseReviewPage/index.tsx | 14 ++++++++++++ apps/backoffice/src/app/i18n/locales/am.ts | 3 +++ apps/backoffice/src/app/i18n/locales/en.ts | 4 ++++ .../lib/features/licensing/licensing-api.ts | 18 +++++++++++++++ 6 files changed, 63 insertions(+), 3 deletions(-) diff --git a/apps/backoffice/src/app/features/license-review/components/DecisionConfirmModal.tsx b/apps/backoffice/src/app/features/license-review/components/DecisionConfirmModal.tsx index 2c34fc9cd..47265f5c9 100644 --- a/apps/backoffice/src/app/features/license-review/components/DecisionConfirmModal.tsx +++ b/apps/backoffice/src/app/features/license-review/components/DecisionConfirmModal.tsx @@ -134,7 +134,10 @@ export function DecisionConfirmModal({ 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 blocked = reasonMissing || diff --git a/apps/backoffice/src/app/features/license-review/config/actions.ts b/apps/backoffice/src/app/features/license-review/config/actions.ts index 2ce63ea65..d641a7689 100644 --- a/apps/backoffice/src/app/features/license-review/config/actions.ts +++ b/apps/backoffice/src/app/features/license-review/config/actions.ts @@ -18,6 +18,7 @@ export type ActionTier = export type ActionId = | 'claim' | 'assign' + | 'assign-reviewer' | 'escalate' | 'hold' | 'resume' @@ -68,6 +69,18 @@ export const ACTIONS: ActionDefinition[] = [ // Claim is deliberately absent here: an officer claims from the queue // (LicenseQueuePage), not from this detail page. That implementation is // 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', tier: 'workflow', @@ -302,6 +315,7 @@ export interface ResolveContext { const WORKFLOW_EVENT_IDS = new Set([ 'claim', 'assign', + 'assign-reviewer', 'escalate', 'hold', 'resume', @@ -364,9 +378,13 @@ export function resolveActions(ctx: ResolveContext): ResolvedAction[] { 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 = - action.tier === 'primary' && action.id !== 'confirm-payment'; + action.tier === 'primary' && + action.id !== 'confirm-payment' && + action.id !== 'assign-reviewer'; if ( needsOwnership && app.assignedOfficerId && diff --git a/apps/backoffice/src/app/features/license-review/pages/LicenseReviewPage/index.tsx b/apps/backoffice/src/app/features/license-review/pages/LicenseReviewPage/index.tsx index c26ec2cc8..c6bc05dd5 100644 --- a/apps/backoffice/src/app/features/license-review/pages/LicenseReviewPage/index.tsx +++ b/apps/backoffice/src/app/features/license-review/pages/LicenseReviewPage/index.tsx @@ -39,6 +39,7 @@ import { useLocalized, useApproveDocumentsMutation, useAssignApplicationMutation, + useAssignReviewerMutation, useClaimApplicationMutation, useCompleteReviewMutation, useConfirmPaymentMutation, @@ -217,6 +218,7 @@ export function LicenseReviewPage() { const [resumeApplication] = useResumeApplicationMutation(); const [escalateApplication] = useEscalateApplicationMutation(); const [assignApplication] = useAssignApplicationMutation(); + const [assignReviewer] = useAssignReviewerMutation(); // Real officer list, so Assign and Escalate name a person instead of // silently reassigning to whoever already held the application. const { data: officers = [] } = useGetAssignableOfficersQuery(); @@ -670,6 +672,18 @@ export function LicenseReviewPage() { t("review.done.escalate", "Escalated"), ); 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": if (!submission.officerId) return; await run( diff --git a/apps/backoffice/src/app/i18n/locales/am.ts b/apps/backoffice/src/app/i18n/locales/am.ts index 458b638e6..ae6c4d533 100644 --- a/apps/backoffice/src/app/i18n/locales/am.ts +++ b/apps/backoffice/src/app/i18n/locales/am.ts @@ -998,6 +998,7 @@ export const am: Translations = { actions: { claim: "ውሰድ", assign: "መድብ", + assignReviewer: "ገምጋሚ መድብ", escalate: "ወደ ላይ አሳድግ", hold: "አግድ", resume: "ቀጥል", @@ -1056,6 +1057,7 @@ export const am: Translations = { hold: "ለ{{applicant}} ማመልከቻ {{number}} ያግዳል። ለእርስዎ ተመድቦ ይቆያል።", resume: "ማመልከቻ {{number}} ወደ ታገደበት ደረጃ ይመልሳል።", escalate: "ማመልከቻ {{number}} ለውሳኔ ወደ የበላይ ኃላፊ ያሳድጋል።", + "assign-reviewer": "ማመልከቻ {{number}} ለተመረጠው ሹም ሰጥቶ ግምገማውን ያስጀምራል።", "confirm-payment": "ለማመልከቻ {{number}} ክፍያ ያረጋግጣል።", "schedule-exam": "ለማመልከቻ {{number}} {{applicant}}ን ለፈተና ክፍለ ጊዜ ይመድባል።", }, @@ -1150,6 +1152,7 @@ export const am: Translations = { resume: "ማመልከቻው ቀጥሏል", escalate: "ወደ ላይ አድጓል", assign: "እንደገና ተመድቧል", + assignReviewer: "ግምገማ ተመድቧል", scheduled: "ምርመራ ተይዟል", inspectionPassed: "ምርመራ አልፏል", inspectionFailed: "ምርመራ ወድቋል", diff --git a/apps/backoffice/src/app/i18n/locales/en.ts b/apps/backoffice/src/app/i18n/locales/en.ts index 73a9d96d7..b217be98c 100644 --- a/apps/backoffice/src/app/i18n/locales/en.ts +++ b/apps/backoffice/src/app/i18n/locales/en.ts @@ -1007,6 +1007,7 @@ export const en = { actions: { claim: 'Claim', assign: 'Assign', + assignReviewer: 'Assign reviewer', escalate: 'Escalate', hold: 'Put on hold', resume: 'Resume', @@ -1064,6 +1065,8 @@ export const en = { hold: 'Parks application {{number}} for {{applicant}}. It stays assigned to you.', resume: 'Returns application {{number}} to the stage it was held from.', 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}}.', 'schedule-exam': 'Assigns {{applicant}} to an exam session for application {{number}}.', }, @@ -1155,6 +1158,7 @@ export const en = { resume: 'Application resumed', escalate: 'Escalated', assign: 'Reassigned', + assignReviewer: 'Review assigned', scheduled: 'Inspection scheduled', inspectionPassed: 'Inspection passed', inspectionFailed: 'Inspection failed', diff --git a/libs/api/src/lib/features/licensing/licensing-api.ts b/libs/api/src/lib/features/licensing/licensing-api.ts index 3e17464ff..665aaf8ab 100644 --- a/libs/api/src/lib/features/licensing/licensing-api.ts +++ b/libs/api/src/lib/features/licensing/licensing-api.ts @@ -856,6 +856,23 @@ export const licensingApi = baseApi 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< LicenseApplication, { id: string; reason: string } @@ -1050,6 +1067,7 @@ export const { useRevokeLicenseMutation, useReinstateLicenseMutation, useAssignApplicationMutation, + useAssignReviewerMutation, useHoldApplicationMutation, useResumeApplicationMutation, useEscalateApplicationMutation, From 580ceaa139f412d23754c393ec17636055f1cc4f Mon Sep 17 00:00:00 2001 From: Nati Date: Wed, 26 Aug 2026 14:08:09 +0000 Subject: [PATCH 2/2] Remove 'assign-reviewer' action and related functionality from license review process --- .../components/DecisionConfirmModal.tsx | 5 +--- .../features/license-review/config/actions.ts | 29 +++++-------------- .../pages/LicenseReviewPage/index.tsx | 14 --------- 3 files changed, 8 insertions(+), 40 deletions(-) diff --git a/apps/backoffice/src/app/features/license-review/components/DecisionConfirmModal.tsx b/apps/backoffice/src/app/features/license-review/components/DecisionConfirmModal.tsx index 47265f5c9..2c34fc9cd 100644 --- a/apps/backoffice/src/app/features/license-review/components/DecisionConfirmModal.tsx +++ b/apps/backoffice/src/app/features/license-review/components/DecisionConfirmModal.tsx @@ -134,10 +134,7 @@ export function DecisionConfirmModal({ if (!action) return null; - const needsOfficer = - action.id === 'assign' || - action.id === 'assign-reviewer' || - action.id === 'escalate'; + const needsOfficer = action.id === 'assign' || action.id === 'escalate'; const reasonMissing = action.requiresReason && !reason.trim() && !reasonCode; const blocked = reasonMissing || diff --git a/apps/backoffice/src/app/features/license-review/config/actions.ts b/apps/backoffice/src/app/features/license-review/config/actions.ts index 6ed6d30c7..4bd1d9446 100644 --- a/apps/backoffice/src/app/features/license-review/config/actions.ts +++ b/apps/backoffice/src/app/features/license-review/config/actions.ts @@ -18,7 +18,6 @@ export type ActionTier = export type ActionId = | 'claim' | 'assign' - | 'assign-reviewer' | 'escalate' | 'hold' | 'resume' @@ -69,18 +68,6 @@ export const ACTIONS: ActionDefinition[] = [ // Claim is deliberately absent here: an officer claims from the queue // (LicenseQueuePage), not from this detail page. That implementation is // 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', tier: 'workflow', @@ -168,7 +155,9 @@ export const ACTIONS: ActionDefinition[] = [ id: 'final-approve', tier: 'primary', labelKey: 'review.actions.finalApprove', - from: ['INSPECTION_COMPLETED', 'UNDER_EVALUATION'], + // ELIGIBILITY_PAID: an examined certificate (CoC/CoP) is decided straight + // off the eligibility queue — no assignment step. + from: ['INSPECTION_COMPLETED', 'UNDER_EVALUATION', 'ELIGIBILITY_PAID'], permissions: ['can:approve:license-application'], emphasis: 'filled', color: 'teal', @@ -178,7 +167,7 @@ export const ACTIONS: ActionDefinition[] = [ id: 'request-adjustment', tier: 'primary', labelKey: 'review.actions.requestAdjustment', - from: ['UNDER_REVIEW', 'UNDER_EVALUATION', 'INSPECTION_COMPLETED'], + from: ['UNDER_REVIEW', 'UNDER_EVALUATION', 'INSPECTION_COMPLETED', 'ELIGIBILITY_PAID'], permissions: ['can:request-adjustment:license-application'], emphasis: 'light', color: 'orange', @@ -193,6 +182,7 @@ export const ACTIONS: ActionDefinition[] = [ 'UNDER_EVALUATION', 'INSPECTION_PENDING', 'INSPECTION_COMPLETED', + 'ELIGIBILITY_PAID', ], permissions: ['can:reject:license-application'], emphasis: 'light', @@ -315,7 +305,6 @@ export interface ResolveContext { const WORKFLOW_EVENT_IDS = new Set([ 'claim', 'assign', - 'assign-reviewer', 'escalate', 'hold', 'resume', @@ -390,13 +379,9 @@ export function resolveActions(ctx: ResolveContext): ResolvedAction[] { disabledReason: reason, }); - // 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. + // Decisions belong to whoever holds the application. const needsOwnership = - action.tier === 'primary' && - action.id !== 'confirm-payment' && - action.id !== 'assign-reviewer'; + action.tier === 'primary' && action.id !== 'confirm-payment'; if ( needsOwnership && app.assignedOfficerId && diff --git a/apps/backoffice/src/app/features/license-review/pages/LicenseReviewPage/index.tsx b/apps/backoffice/src/app/features/license-review/pages/LicenseReviewPage/index.tsx index 60658db63..166b512eb 100644 --- a/apps/backoffice/src/app/features/license-review/pages/LicenseReviewPage/index.tsx +++ b/apps/backoffice/src/app/features/license-review/pages/LicenseReviewPage/index.tsx @@ -39,7 +39,6 @@ import { useLocalized, useApproveDocumentsMutation, useAssignApplicationMutation, - useAssignReviewerMutation, useClaimApplicationMutation, useCompleteReviewMutation, useConfirmPaymentMutation, @@ -218,7 +217,6 @@ export function LicenseReviewPage() { const [resumeApplication] = useResumeApplicationMutation(); const [escalateApplication] = useEscalateApplicationMutation(); const [assignApplication] = useAssignApplicationMutation(); - const [assignReviewer] = useAssignReviewerMutation(); // Real officer list, so Assign and Escalate name a person instead of // silently reassigning to whoever already held the application. const { data: officers = [] } = useGetAssignableOfficersQuery(); @@ -672,18 +670,6 @@ export function LicenseReviewPage() { t("review.done.escalate", "Escalated"), ); 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": if (!submission.officerId) return; await run(