mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
logestics chnage
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Modal, Select, Stack, Text, Textarea } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useGetAssignableOfficersQuery } from '@ema-platform/api';
|
||||
import { ModalFooter } from '@ema-platform/ui';
|
||||
import { Button } from '@mantine/core';
|
||||
|
||||
export type AssignKind = 'review' | 'inspection';
|
||||
|
||||
interface AssignDialogProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
/** Which stage is being handed out — changes the wording, not the mechanics. */
|
||||
kind: AssignKind;
|
||||
/** Reference of the application being dispatched, shown for confirmation. */
|
||||
applicationNumber?: string;
|
||||
loading?: boolean;
|
||||
onConfirm: (officerId: string, remark?: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The team leader handing work to an employee.
|
||||
*
|
||||
* One dialog for both stages because the decision is identical — pick a person,
|
||||
* optionally say why — and two near-identical modals would drift apart. The
|
||||
* `kind` only selects wording.
|
||||
*
|
||||
* Confirm stays disabled until someone is picked: an assignment with no
|
||||
* assignee is the one mistake this dialog exists to prevent.
|
||||
*/
|
||||
export function AssignDialog({
|
||||
opened,
|
||||
onClose,
|
||||
kind,
|
||||
applicationNumber,
|
||||
loading,
|
||||
onConfirm,
|
||||
}: AssignDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const { data: officers = [], isLoading } = useGetAssignableOfficersQuery();
|
||||
const [officerId, setOfficerId] = useState<string | null>(null);
|
||||
const [remark, setRemark] = useState('');
|
||||
|
||||
// Reopening for a different application must not offer the previous
|
||||
// dialog's answers as if they had been chosen for this one.
|
||||
useEffect(() => {
|
||||
if (opened) {
|
||||
setOfficerId(null);
|
||||
setRemark('');
|
||||
}
|
||||
}, [opened]);
|
||||
|
||||
const title =
|
||||
kind === 'review'
|
||||
? t('queue.assignReview', 'Assign document review')
|
||||
: t('queue.assignInspection', 'Assign inspection');
|
||||
|
||||
return (
|
||||
<Modal opened={opened} onClose={onClose} title={title} size="md">
|
||||
<Stack gap="md">
|
||||
{applicationNumber && (
|
||||
<Text size="sm" c="dimmed">
|
||||
{applicationNumber}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Select
|
||||
label={
|
||||
kind === 'review'
|
||||
? t('queue.assignReviewTo', 'Employee to review the documents')
|
||||
: t('queue.assignInspectionTo', 'Employee to conduct the inspection')
|
||||
}
|
||||
placeholder={t('queue.selectEmployee', 'Select an employee')}
|
||||
data={officers.map((o) => ({
|
||||
value: o.id,
|
||||
label: o.name ?? o.id,
|
||||
}))}
|
||||
value={officerId}
|
||||
onChange={setOfficerId}
|
||||
disabled={isLoading}
|
||||
searchable
|
||||
withAsterisk
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label={t('queue.assignRemark', 'Instructions')}
|
||||
description={t(
|
||||
'queue.assignRemarkHint',
|
||||
'Optional. Sent with the assignment notification.',
|
||||
)}
|
||||
value={remark}
|
||||
onChange={(e) => setRemark(e.currentTarget.value)}
|
||||
autosize
|
||||
minRows={2}
|
||||
/>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant="default" onClick={onClose} size="sm">
|
||||
{t('common.cancel', 'Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
loading={loading}
|
||||
disabled={!officerId}
|
||||
onClick={() => officerId && onConfirm(officerId, remark || undefined)}
|
||||
>
|
||||
{t('queue.assignConfirm', 'Assign')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -134,7 +134,13 @@ export function DecisionConfirmModal({
|
||||
|
||||
if (!action) return null;
|
||||
|
||||
const needsOfficer = action.id === 'assign' || action.id === 'escalate';
|
||||
// The push-model assignments name a person the same way Assign does, so they
|
||||
// reuse this picker rather than each carrying a dialog of their own.
|
||||
const needsOfficer =
|
||||
action.id === 'assign' ||
|
||||
action.id === 'escalate' ||
|
||||
action.id === 'assign-reviewer' ||
|
||||
action.id === 'assign-inspector';
|
||||
const reasonMissing = action.requiresReason && !reason.trim() && !reasonCode;
|
||||
const blocked =
|
||||
reasonMissing ||
|
||||
@@ -177,6 +183,10 @@ export function DecisionConfirmModal({
|
||||
label={
|
||||
action.id === 'escalate'
|
||||
? t('review.supervisor', 'Supervisor')
|
||||
: action.id === 'assign-inspector'
|
||||
? t('review.inspector', 'Inspector')
|
||||
: action.id === 'assign-reviewer'
|
||||
? t('review.reviewer', 'Reviewer')
|
||||
: t('review.officer', 'Officer')
|
||||
}
|
||||
placeholder={t('review.officerPlaceholder', 'Select who takes this on')}
|
||||
|
||||
@@ -18,6 +18,10 @@ export type ActionTier =
|
||||
export type ActionId =
|
||||
| 'claim'
|
||||
| 'assign'
|
||||
| 'assign-reviewer'
|
||||
| 'report-review'
|
||||
| 'assign-inspector'
|
||||
| 'report-inspection'
|
||||
| 'escalate'
|
||||
| 'hold'
|
||||
| 'resume'
|
||||
@@ -75,6 +79,46 @@ export const ACTIONS: ActionDefinition[] = [
|
||||
permissions: ['can:claim:license-application'],
|
||||
emphasis: 'light',
|
||||
},
|
||||
/**
|
||||
* The push-model actions.
|
||||
*
|
||||
* `assign-reviewer` and `assign-inspector` start a stage; `report-review`
|
||||
* and `report-inspection` hand it back. An employee sees only the two report
|
||||
* actions — the assign pair is gated on ASSIGN_APPLICATION, which their
|
||||
* position type does not carry.
|
||||
*/
|
||||
{
|
||||
id: 'assign-reviewer',
|
||||
tier: 'workflow',
|
||||
labelKey: 'review.actions.assignReviewer',
|
||||
from: ['SUBMITTED', 'ELIGIBILITY_PAID'],
|
||||
permissions: ['can:assign:license-application'],
|
||||
emphasis: 'filled',
|
||||
},
|
||||
{
|
||||
id: 'report-review',
|
||||
tier: 'workflow',
|
||||
labelKey: 'review.actions.reportReview',
|
||||
from: ['UNDER_REVIEW', 'UNDER_EVALUATION'],
|
||||
permissions: ['can:review:license-application'],
|
||||
emphasis: 'filled',
|
||||
},
|
||||
{
|
||||
id: 'assign-inspector',
|
||||
tier: 'workflow',
|
||||
labelKey: 'review.actions.assignInspector',
|
||||
from: ['REVIEW_REPORTED', 'UNDER_EVALUATION'],
|
||||
permissions: ['can:assign:license-application'],
|
||||
emphasis: 'filled',
|
||||
},
|
||||
{
|
||||
id: 'report-inspection',
|
||||
tier: 'workflow',
|
||||
labelKey: 'review.actions.reportInspection',
|
||||
from: ['INSPECTION_COMPLETED'],
|
||||
permissions: ['can:update:inspection'],
|
||||
emphasis: 'filled',
|
||||
},
|
||||
{
|
||||
id: 'assign',
|
||||
tier: 'workflow',
|
||||
@@ -162,7 +206,9 @@ export const ACTIONS: ActionDefinition[] = [
|
||||
id: 'final-approve',
|
||||
tier: 'primary',
|
||||
labelKey: 'review.actions.finalApprove',
|
||||
from: ['INSPECTION_COMPLETED', 'UNDER_EVALUATION'],
|
||||
from: ['INSPECTION_COMPLETED',
|
||||
'REVIEW_REPORTED',
|
||||
'INSPECTION_REPORTED', 'UNDER_EVALUATION'],
|
||||
permissions: ['can:approve:license-application'],
|
||||
emphasis: 'filled',
|
||||
color: 'teal',
|
||||
@@ -172,7 +218,9 @@ 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',
|
||||
'REVIEW_REPORTED',
|
||||
'INSPECTION_REPORTED'],
|
||||
permissions: ['can:request-adjustment:license-application'],
|
||||
emphasis: 'light',
|
||||
color: 'orange',
|
||||
@@ -187,6 +235,8 @@ export const ACTIONS: ActionDefinition[] = [
|
||||
'UNDER_EVALUATION',
|
||||
'INSPECTION_PENDING',
|
||||
'INSPECTION_COMPLETED',
|
||||
'REVIEW_REPORTED',
|
||||
'INSPECTION_REPORTED',
|
||||
],
|
||||
permissions: ['can:reject:license-application'],
|
||||
emphasis: 'light',
|
||||
|
||||
@@ -7,11 +7,12 @@ import { LICENSE_PERMISSIONS, RequirePermission } from "@ema-platform/auth";
|
||||
export function licenseQueueActionsColumn(
|
||||
t: TFunction,
|
||||
handlers: {
|
||||
claiming: boolean;
|
||||
onClaim: (id: string) => void;
|
||||
assigning: boolean;
|
||||
/** Opens the assign dialog. Team leaders only — see below. */
|
||||
onAssign: (application: LicenseApplication) => void;
|
||||
onOpen: (id: string) => void;
|
||||
/** False for a non-logistics queue — there's no unclaimed pool to claim from. */
|
||||
claimable?: boolean;
|
||||
/** False for a non-logistics queue — nothing there is dispatched. */
|
||||
assignable?: boolean;
|
||||
},
|
||||
): AdvancedColumn<LicenseApplication> {
|
||||
return {
|
||||
@@ -19,24 +20,32 @@ export function licenseQueueActionsColumn(
|
||||
label: t("queue.actionsColumn", "Actions"),
|
||||
align: "right",
|
||||
size: 140,
|
||||
/**
|
||||
* "Assign", not "Claim".
|
||||
*
|
||||
* Work is pushed, not picked up: an unassigned application is waiting for
|
||||
* the team leader to hand it to someone, and no seeded position type holds
|
||||
* `CLAIM_APPLICATION` any more. Guarded on `ASSIGN_APPLICATION`, so an
|
||||
* employee sees only "Review" on the files that are already theirs.
|
||||
*/
|
||||
cell: ({ row }) =>
|
||||
handlers.claimable !== false &&
|
||||
handlers.assignable !== false &&
|
||||
row.original.assignedOfficerId === null &&
|
||||
// Mirrors the CLAIM transition's `from` list: an examined cert
|
||||
// Mirrors the ASSIGN_REVIEWER transition's `from` list: an examined cert
|
||||
// (CoC/CoP) sits in ELIGIBILITY_PAID once its eligibility fee clears,
|
||||
// not SUBMITTED.
|
||||
(row.original.status === "SUBMITTED" ||
|
||||
row.original.status === "ELIGIBILITY_PAID") ? (
|
||||
<RequirePermission
|
||||
anyOf={[LICENSE_PERMISSIONS.CLAIM_APPLICATION]}
|
||||
anyOf={[LICENSE_PERMISSIONS.ASSIGN_APPLICATION]}
|
||||
hideOnly
|
||||
>
|
||||
<Button
|
||||
size="xs"
|
||||
loading={handlers.claiming}
|
||||
onClick={() => handlers.onClaim(row.original.id)}
|
||||
loading={handlers.assigning}
|
||||
onClick={() => handlers.onAssign(row.original)}
|
||||
>
|
||||
{t("queue.claim", "Claim")}
|
||||
{t("queue.assign", "Assign")}
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
) : (
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
familyLabels,
|
||||
localized,
|
||||
resolveFamilyKind,
|
||||
useClaimApplicationMutation,
|
||||
useAssignReviewerMutation,
|
||||
useGetAllApplicationsQuery,
|
||||
useGetAssignedToMeQuery,
|
||||
useGetLicenseTypesQuery,
|
||||
@@ -54,7 +54,6 @@ import {
|
||||
AmharicDatePicker,
|
||||
type AdvancedColumn,
|
||||
} from "@ema-platform/ui";
|
||||
import { LICENSE_PERMISSIONS, RequirePermission } from "@ema-platform/auth";
|
||||
import {
|
||||
DEFAULT_VIEW,
|
||||
SAVED_VIEWS,
|
||||
@@ -70,6 +69,7 @@ import { setDensity } from "../../../../store/preferences.slice";
|
||||
import { useAppDispatch, useAppSelector } from "../../../../store/hooks";
|
||||
import { KEYBOARD_SHORTCUTS, useQueueKeyboard } from "../../useQueueKeyboard";
|
||||
import { licenseQueueColumns } from "./columns";
|
||||
import { AssignDialog } from "../../components/AssignDialog";
|
||||
import { licenseQueueActionsColumn } from "./actions";
|
||||
import { PageHeader } from '@ema-platform/ui';
|
||||
|
||||
@@ -267,7 +267,8 @@ export function LicenseQueuePage() {
|
||||
? mineQuery
|
||||
: allQuery;
|
||||
|
||||
const [claim, { isLoading: claiming }] = useClaimApplicationMutation();
|
||||
const [assignReviewer, { isLoading: assigning }] = useAssignReviewerMutation();
|
||||
const [assignTarget, setAssignTarget] = useState<LicenseApplication | null>(null);
|
||||
const [runExport, { isFetching: exporting }] =
|
||||
useLazyExportApplicationsQuery();
|
||||
|
||||
@@ -354,58 +355,40 @@ export function LicenseQueuePage() {
|
||||
setFacet({ sortBy: field, sortDir: dir });
|
||||
};
|
||||
|
||||
async function handleClaim(id: string) {
|
||||
/**
|
||||
* The team leader dispatching one application.
|
||||
*
|
||||
* Replaces the old self-service claim: nothing is picked up any more, so the
|
||||
* queue's primary action is handing a file to an employee. The dialog holds
|
||||
* the choice; this only sends it.
|
||||
*/
|
||||
async function handleAssign(officerId: string, remark?: string) {
|
||||
if (!assignTarget) return;
|
||||
try {
|
||||
await claim(id).unwrap();
|
||||
await assignReviewer({ id: assignTarget.id, officerId, remark }).unwrap();
|
||||
notifications.show({
|
||||
color: "teal",
|
||||
title: t("queue.claimed", "Claimed"),
|
||||
title: t("queue.assigned", "Assigned"),
|
||||
message: t(
|
||||
"queue.claimedBody",
|
||||
"The application is now assigned to you.",
|
||||
"queue.assignedBody",
|
||||
"The employee has been notified and the review has started.",
|
||||
),
|
||||
});
|
||||
changeView("mine");
|
||||
setAssignTarget(null);
|
||||
active.refetch();
|
||||
} catch (err) {
|
||||
// A 409 means another officer got there first — refresh so the queue
|
||||
// stops showing work that is no longer available.
|
||||
notifications.show({
|
||||
color: "red",
|
||||
title: t("queue.claimFailed", "Could not claim"),
|
||||
title: t("queue.assignFailed", "Could not assign"),
|
||||
message: extractErrorMessage(
|
||||
err,
|
||||
t("queue.claimRace", "Another officer already claimed it."),
|
||||
t("queue.assignError", "The application could not be assigned."),
|
||||
),
|
||||
});
|
||||
active.refetch();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBulkClaim() {
|
||||
const results = await Promise.allSettled(
|
||||
selected.map((id) => claim(id).unwrap()),
|
||||
);
|
||||
const claimed = results.filter((r) => r.status === "fulfilled").length;
|
||||
const lost = results.length - claimed;
|
||||
notifications.show({
|
||||
color: lost ? "yellow" : "teal",
|
||||
title: t("queue.bulkClaimed", {
|
||||
count: claimed,
|
||||
defaultValue: "{{count}} claimed",
|
||||
}),
|
||||
// Partial success is the normal case in a shared queue, so it is
|
||||
// reported rather than swallowed or treated as total failure.
|
||||
message: lost
|
||||
? t("queue.bulkClaimPartial", {
|
||||
count: lost,
|
||||
defaultValue: "{{count}} were already taken by another officer.",
|
||||
})
|
||||
: "",
|
||||
});
|
||||
setSelected([]);
|
||||
active.refetch();
|
||||
}
|
||||
|
||||
const cursorRow = items[cursor];
|
||||
useQueueKeyboard({
|
||||
enabled: !helpOpen,
|
||||
@@ -414,10 +397,12 @@ export function LicenseQueuePage() {
|
||||
onPrevious: () => setCursor((c) => Math.max(c - 1, 0)),
|
||||
onOpen: () => cursorRow && navigate(`/licence-review/${cursorRow.id}`),
|
||||
onClaim: () => {
|
||||
// Only unclaimed rows on a logistics queue can be claimed; pressing c
|
||||
// elsewhere is a no-op rather than an error the officer has to read.
|
||||
// "c" now opens the assign dialog on an undispatched row. Kept on the
|
||||
// same key: it is still "do the queue's primary action to this row",
|
||||
// and rebinding a shortcut officers have in their fingers costs more
|
||||
// than the name mismatch.
|
||||
if (isLogistics !== false && cursorRow && cursorRow.assignedOfficerId === null)
|
||||
handleClaim(cursorRow.id);
|
||||
setAssignTarget(cursorRow);
|
||||
},
|
||||
onEscape: () => setSelected([]),
|
||||
onHelp: () => setHelpOpen(true),
|
||||
@@ -478,12 +463,12 @@ export function LicenseQueuePage() {
|
||||
isLogistics,
|
||||
}),
|
||||
licenseQueueActionsColumn(t, {
|
||||
claiming,
|
||||
onClaim: handleClaim,
|
||||
assigning,
|
||||
onAssign: setAssignTarget,
|
||||
onOpen: (id) => navigate(`/licence-review/${id}`),
|
||||
// Non-logistics applications aren't claimed off a shared queue (see
|
||||
// Non-logistics applications aren't dispatched off a shared queue (see
|
||||
// `savedViewsForFamily`) — every row opens straight to Review.
|
||||
claimable: isLogistics !== false,
|
||||
assignable: isLogistics !== false,
|
||||
}),
|
||||
],
|
||||
[
|
||||
@@ -494,7 +479,7 @@ export function LicenseQueuePage() {
|
||||
selected,
|
||||
allSelected,
|
||||
items,
|
||||
claiming,
|
||||
assigning,
|
||||
isLogistics,
|
||||
],
|
||||
);
|
||||
@@ -763,23 +748,18 @@ export function LicenseQueuePage() {
|
||||
>
|
||||
{t("queue.export", "Export CSV")}
|
||||
</Button>
|
||||
{isLogistics !== false && (
|
||||
<RequirePermission
|
||||
anyOf={[LICENSE_PERMISSIONS.CLAIM_APPLICATION]}
|
||||
hideOnly
|
||||
>
|
||||
<Button loading={claiming} onClick={handleBulkClaim}>
|
||||
{t("queue.bulkClaim", {
|
||||
count: selected.length,
|
||||
defaultValue: "Claim {{count}}",
|
||||
})}
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
</Paper>
|
||||
)}
|
||||
<AssignDialog
|
||||
opened={assignTarget !== null}
|
||||
onClose={() => setAssignTarget(null)}
|
||||
kind="review"
|
||||
applicationNumber={assignTarget?.applicationNumber}
|
||||
loading={assigning}
|
||||
onConfirm={handleAssign}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,10 @@ import {
|
||||
useLocalized,
|
||||
useApproveDocumentsMutation,
|
||||
useAssignApplicationMutation,
|
||||
useAssignReviewerMutation,
|
||||
useAssignInspectorMutation,
|
||||
useReportReviewMutation,
|
||||
useReportInspectionMutation,
|
||||
useCompleteReviewMutation,
|
||||
useConfirmPaymentMutation,
|
||||
useScheduleIssuanceMutation,
|
||||
@@ -215,6 +219,13 @@ export function LicenseReviewPage() {
|
||||
const [resumeApplication] = useResumeApplicationMutation();
|
||||
const [escalateApplication] = useEscalateApplicationMutation();
|
||||
const [assignApplication] = useAssignApplicationMutation();
|
||||
const [assignReviewer, { isLoading: assigningReviewer }] =
|
||||
useAssignReviewerMutation();
|
||||
const [assignInspector, { isLoading: assigningInspector }] =
|
||||
useAssignInspectorMutation();
|
||||
const [reportReview] = useReportReviewMutation();
|
||||
const [reportInspection] = useReportInspectionMutation();
|
||||
|
||||
// Real officer list, so Assign and Escalate name a person instead of
|
||||
// silently reassigning to whoever already held the application.
|
||||
const { data: officers = [] } = useGetAssignableOfficersQuery();
|
||||
@@ -534,6 +545,46 @@ export function LicenseReviewPage() {
|
||||
// Claim is fired from the queue in practice; kept here for the case
|
||||
// where an officer opens an unclaimed application directly.
|
||||
break;
|
||||
// Handing work out. Reuses the decision modal's officer picker rather
|
||||
// than a dialog of its own — it already resolves the real officer list.
|
||||
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-inspector":
|
||||
if (!submission.officerId) return;
|
||||
await run(
|
||||
() =>
|
||||
assignInspector({
|
||||
id,
|
||||
inspectorId: submission.officerId as string,
|
||||
remark: submission.reason,
|
||||
}).unwrap(),
|
||||
t("review.done.assignInspector", "Inspection assigned"),
|
||||
);
|
||||
break;
|
||||
// Handing work back. Parks the file with the team leader — an employee
|
||||
// finishing their task decides nothing.
|
||||
case "report-review":
|
||||
await run(
|
||||
() => reportReview({ id, remark: submission.reason }).unwrap(),
|
||||
t("review.done.reportReview", "Sent to your team leader"),
|
||||
);
|
||||
break;
|
||||
case "report-inspection":
|
||||
await run(
|
||||
() => reportInspection({ id, remark: submission.reason }).unwrap(),
|
||||
t("review.done.reportInspection", "Inspection result sent"),
|
||||
);
|
||||
break;
|
||||
case "complete-review":
|
||||
await run(
|
||||
() =>
|
||||
|
||||
@@ -867,6 +867,11 @@ export const am: Translations = {
|
||||
submitted: "የቀረበበት",
|
||||
sla: "ዕድሜ / የጊዜ ገደብ",
|
||||
claim: "ውሰድ",
|
||||
assign: "መድብ",
|
||||
assigned: "ተመድቧል",
|
||||
assignedBody: "ባለሙያው ተነግሮታል፤ ግምገማው ተጀምሯል።",
|
||||
assignFailed: "መመደብ አልተቻለም",
|
||||
assignError: "ማመልከቻውን መመደብ አልተቻለም።",
|
||||
review: "ገምግም",
|
||||
claimed: "ተወስዷል",
|
||||
claimedBody: "ማመልከቻው አሁን ለእርስዎ ተመድቧል።",
|
||||
@@ -966,6 +971,10 @@ export const am: Translations = {
|
||||
actions: {
|
||||
claim: "ውሰድ",
|
||||
assign: "መድብ",
|
||||
assignReviewer: "ግምገማ መድብ",
|
||||
reportReview: "ለቡድን መሪ አሳውቅ",
|
||||
assignInspector: "ምርመራ መድብ",
|
||||
reportInspection: "የምርመራ ውጤት አሳውቅ",
|
||||
escalate: "ወደ ላይ አሳድግ",
|
||||
hold: "አግድ",
|
||||
resume: "ቀጥል",
|
||||
|
||||
@@ -875,6 +875,11 @@ export const en = {
|
||||
submitted: 'Submitted',
|
||||
sla: 'Age / SLA',
|
||||
claim: 'Claim',
|
||||
assign: 'Assign',
|
||||
assigned: 'Assigned',
|
||||
assignedBody: 'The employee has been notified and the review has started.',
|
||||
assignFailed: 'Could not assign',
|
||||
assignError: 'The application could not be assigned.',
|
||||
review: 'Review',
|
||||
claimed: 'Claimed',
|
||||
claimedBody: 'The application is now assigned to you.',
|
||||
@@ -974,6 +979,10 @@ export const en = {
|
||||
actions: {
|
||||
claim: 'Claim',
|
||||
assign: 'Assign',
|
||||
assignReviewer: 'Assign review',
|
||||
reportReview: 'Report to team leader',
|
||||
assignInspector: 'Assign inspection',
|
||||
reportInspection: 'Report inspection result',
|
||||
escalate: 'Escalate',
|
||||
hold: 'Put on hold',
|
||||
resume: 'Resume',
|
||||
|
||||
@@ -755,6 +755,66 @@ export const licensingApi = baseApi
|
||||
query: () => ({ url: '/license-application-review/officers' }),
|
||||
}),
|
||||
|
||||
// ------------------------------------------------- team-leader dispatch
|
||||
/**
|
||||
* Handing work out, and handing it back.
|
||||
*
|
||||
* `assignApplication` below only re-points an application already in
|
||||
* flight; these two *start* a stage, because under the push model
|
||||
* assignment is how work begins — nothing is claimed from a queue.
|
||||
*/
|
||||
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')],
|
||||
}),
|
||||
|
||||
assignInspector: builder.mutation<
|
||||
LicenseApplication,
|
||||
{ id: string; inspectorId: string; remark?: string }
|
||||
>({
|
||||
query: ({ id, ...body }) => ({
|
||||
url: `/license-application-review/${id}/assign-inspector`,
|
||||
method: 'POST',
|
||||
body,
|
||||
}),
|
||||
invalidatesTags: (_r, error, { id }) =>
|
||||
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
|
||||
}),
|
||||
|
||||
reportReview: builder.mutation<
|
||||
LicenseApplication,
|
||||
{ id: string; remark?: string }
|
||||
>({
|
||||
query: ({ id, ...body }) => ({
|
||||
url: `/license-application-review/${id}/report-review`,
|
||||
method: 'POST',
|
||||
body,
|
||||
}),
|
||||
invalidatesTags: (_r, error, { id }) =>
|
||||
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
|
||||
}),
|
||||
|
||||
reportInspection: builder.mutation<
|
||||
LicenseApplication,
|
||||
{ id: string; remark?: string }
|
||||
>({
|
||||
query: ({ id, ...body }) => ({
|
||||
url: `/license-application-review/${id}/report-inspection`,
|
||||
method: 'POST',
|
||||
body,
|
||||
}),
|
||||
invalidatesTags: (_r, error, { id }) =>
|
||||
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
|
||||
}),
|
||||
|
||||
// ----------------------------------------------------- workflow controls
|
||||
assignApplication: builder.mutation<
|
||||
LicenseApplication,
|
||||
@@ -953,6 +1013,10 @@ export const {
|
||||
useRevokeLicenseMutation,
|
||||
useReinstateLicenseMutation,
|
||||
useAssignApplicationMutation,
|
||||
useAssignReviewerMutation,
|
||||
useAssignInspectorMutation,
|
||||
useReportReviewMutation,
|
||||
useReportInspectionMutation,
|
||||
useHoldApplicationMutation,
|
||||
useResumeApplicationMutation,
|
||||
useEscalateApplicationMutation,
|
||||
|
||||
@@ -65,7 +65,9 @@ export const STATUS_LABELS: Record<LicenseStatus, string> = {
|
||||
UNDER_EVALUATION: 'Under Evaluation',
|
||||
RESUBMIT_REQUIRED: 'Resubmit Required',
|
||||
INSPECTION_PENDING: 'Inspection Pending',
|
||||
REVIEW_REPORTED: 'Review Reported',
|
||||
INSPECTION_COMPLETED: 'Inspection Completed',
|
||||
INSPECTION_REPORTED: 'Inspection Reported',
|
||||
APPROVED: 'Approved',
|
||||
REJECTED: 'Rejected',
|
||||
ON_HOLD: 'On Hold',
|
||||
@@ -92,7 +94,11 @@ export const STATUS_COLORS: Record<LicenseStatus, string> = {
|
||||
UNDER_EVALUATION: 'indigo',
|
||||
RESUBMIT_REQUIRED: 'orange',
|
||||
INSPECTION_PENDING: 'cyan',
|
||||
// Both 'reported' states are waiting on the team leader, so they carry the
|
||||
// same tone as anything else awaiting an officer decision.
|
||||
REVIEW_REPORTED: 'orange',
|
||||
INSPECTION_COMPLETED: 'cyan',
|
||||
INSPECTION_REPORTED: 'orange',
|
||||
APPROVED: 'teal',
|
||||
REJECTED: 'red',
|
||||
ON_HOLD: 'gray',
|
||||
@@ -125,7 +131,9 @@ export const STATUS_PROGRESS: Record<LicenseStatus, number> = {
|
||||
UNDER_EVALUATION: 45,
|
||||
RESUBMIT_REQUIRED: 30,
|
||||
INSPECTION_PENDING: 55,
|
||||
REVIEW_REPORTED: 50,
|
||||
INSPECTION_COMPLETED: 65,
|
||||
INSPECTION_REPORTED: 70,
|
||||
APPROVED: 75,
|
||||
// Parked, so it keeps the progress of wherever it was held from.
|
||||
ON_HOLD: 45,
|
||||
|
||||
@@ -26,9 +26,13 @@ export type LicenseStatus =
|
||||
| "SUBMITTED"
|
||||
| "UNDER_REVIEW"
|
||||
| "UNDER_EVALUATION"
|
||||
// Employee filed their review; parked with the team leader for a decision.
|
||||
| "REVIEW_REPORTED"
|
||||
| "RESUBMIT_REQUIRED"
|
||||
| "INSPECTION_PENDING"
|
||||
| "INSPECTION_COMPLETED"
|
||||
// Inspector filed the result; parked with the team leader for a decision.
|
||||
| "INSPECTION_REPORTED"
|
||||
| "APPROVED"
|
||||
| "REJECTED"
|
||||
| "ON_HOLD"
|
||||
|
||||
Reference in New Issue
Block a user