mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
feat: add workflow profiles to restrict status filtering and sync queue counts by license type
This commit is contained in:
@@ -43,6 +43,7 @@ import {
|
||||
useLazyExportApplicationsQuery,
|
||||
type LicenseApplication,
|
||||
type LicenseStatus,
|
||||
type LicenseType,
|
||||
type QueueFilter,
|
||||
} from "@ema-platform/api";
|
||||
import {
|
||||
@@ -72,6 +73,17 @@ import { licenseQueueActionsColumn } from "./actions";
|
||||
const PAGE_SIZE = 10;
|
||||
const SEARCH_DEBOUNCE_MS = 300;
|
||||
|
||||
/**
|
||||
* Statuses only the STANDARD course can reach. A registration goes straight
|
||||
* review → approval, so offering these in its facet would be offering filters
|
||||
* that can only ever match nothing.
|
||||
*/
|
||||
const STANDARD_ONLY_STATUSES: LicenseStatus[] = [
|
||||
"UNDER_EVALUATION",
|
||||
"INSPECTION_PENDING",
|
||||
"INSPECTION_COMPLETED",
|
||||
];
|
||||
|
||||
const ALL_STATUSES: LicenseStatus[] = [
|
||||
"SUBMITTED",
|
||||
"UNDER_REVIEW",
|
||||
@@ -89,6 +101,24 @@ const ALL_STATUSES: LicenseStatus[] = [
|
||||
"REJECTED",
|
||||
];
|
||||
|
||||
/** Statuses an application of this type can actually occupy. */
|
||||
function statusesFor(type: LicenseType | undefined): LicenseStatus[] {
|
||||
if (!type) return ALL_STATUSES;
|
||||
return ALL_STATUSES.filter((status) => {
|
||||
if (
|
||||
type.workflowProfile === "REGISTRATION" &&
|
||||
STANDARD_ONLY_STATUSES.includes(status)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (status === "INSPECTION_PENDING" || status === "INSPECTION_COMPLETED") {
|
||||
return type.inspectionRequired;
|
||||
}
|
||||
if (status === "CERTIFICATE_ISSUED") return type.issuesCertificate;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The officer work pool.
|
||||
*
|
||||
@@ -123,14 +153,37 @@ export function LicenseQueuePage() {
|
||||
const activeView = SAVED_VIEWS.find((v) => v.id === view) ?? SAVED_VIEWS[0];
|
||||
|
||||
const { data: licenseTypes } = useGetLicenseTypesQuery();
|
||||
const { data: counts } = useGetQueueCountsQuery();
|
||||
|
||||
// A `/licence-review/type/:typeCode` deep link pins the type facet.
|
||||
const pinnedTypeId = useMemo(() => {
|
||||
if (!typeCode) return undefined;
|
||||
return licenseTypes?.items?.find((type) => type.key === typeCode)?.id;
|
||||
}, [typeCode, licenseTypes]);
|
||||
|
||||
// Counts endpoint keys off `key`, the facet off `id` — resolve whichever the
|
||||
// route or the dropdown set, so the tab badges always count the same rows
|
||||
// the grid is showing rather than system-wide totals.
|
||||
const countsKey = useMemo(
|
||||
() =>
|
||||
typeCode ??
|
||||
licenseTypes?.items?.find((type) => type.id === urlFilter.licenseTypeId)
|
||||
?.key,
|
||||
[typeCode, urlFilter.licenseTypeId, licenseTypes],
|
||||
);
|
||||
const { data: counts } = useGetQueueCountsQuery(countsKey);
|
||||
|
||||
const selectedType = useMemo(() => {
|
||||
const typeId = pinnedTypeId ?? urlFilter.licenseTypeId;
|
||||
if (!typeId) return undefined;
|
||||
return licenseTypes?.items?.find((type) => type.id === typeId);
|
||||
}, [pinnedTypeId, urlFilter.licenseTypeId, licenseTypes]);
|
||||
|
||||
// The facet offers what the chosen type can actually reach. With no type
|
||||
// chosen the queue spans every course, so the full list is correct.
|
||||
const statusOptions = useMemo(
|
||||
() => statusesFor(selectedType),
|
||||
[selectedType],
|
||||
);
|
||||
|
||||
const filter: QueueFilter = useMemo(
|
||||
() => ({
|
||||
...activeView.filter,
|
||||
@@ -234,6 +287,21 @@ export function LicenseQueuePage() {
|
||||
updateUrl(next, view, 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Switching type drops any selected status the new type cannot reach —
|
||||
* otherwise the facet keeps an invisible filter that matches nothing and the
|
||||
* grid looks empty for no reason the officer can see.
|
||||
*/
|
||||
const changeType = (typeId: string | undefined) => {
|
||||
const allowed = statusesFor(
|
||||
licenseTypes?.items?.find((type) => type.id === typeId),
|
||||
);
|
||||
setFacet({
|
||||
licenseTypeId: typeId,
|
||||
status: urlFilter.status?.filter((s) => allowed.includes(s)),
|
||||
});
|
||||
};
|
||||
|
||||
const toggleSort = (field: NonNullable<QueueFilter["sortBy"]>) => {
|
||||
const dir =
|
||||
urlFilter.sortBy === field && urlFilter.sortDir !== "DESC"
|
||||
@@ -448,7 +516,7 @@ export function LicenseQueuePage() {
|
||||
<MultiSelect
|
||||
label={t("queue.status", "Status")}
|
||||
placeholder={t("queue.anyStatus", "Any")}
|
||||
data={ALL_STATUSES.map((s) => ({
|
||||
data={statusOptions.map((s) => ({
|
||||
value: s,
|
||||
label: t(`queue.statusValues.${s}`, STATUS_LABELS[s]),
|
||||
}))}
|
||||
@@ -466,7 +534,7 @@ export function LicenseQueuePage() {
|
||||
label: localized(type.name, i18n.language) || type.key,
|
||||
}))}
|
||||
value={urlFilter.licenseTypeId ?? null}
|
||||
onChange={(v) => setFacet({ licenseTypeId: v ?? undefined })}
|
||||
onChange={(v) => changeType(v ?? undefined)}
|
||||
clearable
|
||||
w={220}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user