mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
refactor: introduce hasUnclaimedPool helper to allow claim workflows for specific document-family queues
This commit is contained in:
@@ -61,6 +61,7 @@ import {
|
|||||||
SAVED_VIEWS,
|
SAVED_VIEWS,
|
||||||
filterFromSearchParams,
|
filterFromSearchParams,
|
||||||
readLastView,
|
readLastView,
|
||||||
|
hasUnclaimedPool,
|
||||||
savedViewsForFamily,
|
savedViewsForFamily,
|
||||||
searchParamsFromFilter,
|
searchParamsFromFilter,
|
||||||
writeLastView,
|
writeLastView,
|
||||||
@@ -159,12 +160,16 @@ export function LicenseQueuePage() {
|
|||||||
const isLogistics = typeCode
|
const isLogistics = typeCode
|
||||||
? resolveFamilyKind(typeCode) === "LOGISTICS_LICENSE"
|
? resolveFamilyKind(typeCode) === "LOGISTICS_LICENSE"
|
||||||
: undefined;
|
: undefined;
|
||||||
const visibleViews = savedViewsForFamily(isLogistics !== false);
|
// Claiming is a workflow property, not a label one — Vessel Registration is
|
||||||
|
// a DOCUMENT family but is still triaged off a shared unclaimed pool, so it
|
||||||
|
// keeps the Unassigned tab and the claim actions.
|
||||||
|
const claimable = hasUnclaimedPool(typeCode);
|
||||||
|
const visibleViews = savedViewsForFamily(claimable);
|
||||||
|
|
||||||
const [view, setView] = useState<SavedViewId>(
|
const [view, setView] = useState<SavedViewId>(
|
||||||
() =>
|
() =>
|
||||||
(searchParams.get("view") as SavedViewId) ||
|
(searchParams.get("view") as SavedViewId) ||
|
||||||
(isLogistics === false ? "all" : readLastView()),
|
(claimable ? readLastView() : "all"),
|
||||||
);
|
);
|
||||||
const [page, setPage] = useState(() => Number(searchParams.get("page")) || 1);
|
const [page, setPage] = useState(() => Number(searchParams.get("page")) || 1);
|
||||||
const [pageSize, setPageSize] = useState(PAGE_SIZE);
|
const [pageSize, setPageSize] = useState(PAGE_SIZE);
|
||||||
@@ -174,21 +179,17 @@ export function LicenseQueuePage() {
|
|||||||
const [helpOpen, setHelpOpen] = useState(false);
|
const [helpOpen, setHelpOpen] = useState(false);
|
||||||
const [debouncedSearch] = useDebouncedValue(searchInput, SEARCH_DEBOUNCE_MS);
|
const [debouncedSearch] = useDebouncedValue(searchInput, SEARCH_DEBOUNCE_MS);
|
||||||
|
|
||||||
// Non-logistics queues have no unassigned/unclaimed pool (see
|
// Queues with no unclaimed pool (see `hasUnclaimedPool`) have no unassigned
|
||||||
// `savedViewsForFamily`), so a stale "unassigned" view — e.g. restored from
|
// tab, so a stale "unassigned" view — e.g. restored from
|
||||||
// `readLastView()` — must fall back to "all" rather than land on a tab that
|
// `readLastView()` — must fall back to "all" rather than land on a tab that
|
||||||
// no longer exists. Auto-created BTC requests specifically start at
|
// no longer exists. Auto-created BTC requests specifically start at
|
||||||
// PAYMENT_PENDING, outside "mine" too, so "all" is the one view guaranteed
|
// PAYMENT_PENDING, outside "mine" too, so "all" is the one view guaranteed
|
||||||
// to show them.
|
// to show them.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (!claimable && !searchParams.has("view") && view === "unassigned") {
|
||||||
isLogistics === false &&
|
|
||||||
!searchParams.has("view") &&
|
|
||||||
view === "unassigned"
|
|
||||||
) {
|
|
||||||
setView("all");
|
setView("all");
|
||||||
}
|
}
|
||||||
}, [isLogistics, searchParams, view]);
|
}, [claimable, searchParams, view]);
|
||||||
|
|
||||||
const urlFilter = useMemo(
|
const urlFilter = useMemo(
|
||||||
() => filterFromSearchParams(searchParams),
|
() => filterFromSearchParams(searchParams),
|
||||||
@@ -414,9 +415,9 @@ export function LicenseQueuePage() {
|
|||||||
onPrevious: () => setCursor((c) => Math.max(c - 1, 0)),
|
onPrevious: () => setCursor((c) => Math.max(c - 1, 0)),
|
||||||
onOpen: () => cursorRow && navigate(`/licence-review/${cursorRow.id}`),
|
onOpen: () => cursorRow && navigate(`/licence-review/${cursorRow.id}`),
|
||||||
onClaim: () => {
|
onClaim: () => {
|
||||||
// Only unclaimed rows on a logistics queue can be claimed; pressing c
|
// Only unclaimed rows on a claimable queue can be claimed; pressing c
|
||||||
// elsewhere is a no-op rather than an error the officer has to read.
|
// elsewhere is a no-op rather than an error the officer has to read.
|
||||||
if (isLogistics !== false && cursorRow && cursorRow.assignedOfficerId === null)
|
if (claimable && cursorRow && cursorRow.assignedOfficerId === null)
|
||||||
handleClaim(cursorRow.id);
|
handleClaim(cursorRow.id);
|
||||||
},
|
},
|
||||||
onEscape: () => setSelected([]),
|
onEscape: () => setSelected([]),
|
||||||
@@ -481,9 +482,9 @@ export function LicenseQueuePage() {
|
|||||||
claiming,
|
claiming,
|
||||||
onClaim: handleClaim,
|
onClaim: handleClaim,
|
||||||
onOpen: (id) => navigate(`/licence-review/${id}`),
|
onOpen: (id) => navigate(`/licence-review/${id}`),
|
||||||
// Non-logistics applications aren't claimed off a shared queue (see
|
// Applications with no unclaimed pool (see `hasUnclaimedPool`) are
|
||||||
// `savedViewsForFamily`) — every row opens straight to Review.
|
// never claimed — every row opens straight to Review.
|
||||||
claimable: isLogistics !== false,
|
claimable,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -496,6 +497,7 @@ export function LicenseQueuePage() {
|
|||||||
items,
|
items,
|
||||||
claiming,
|
claiming,
|
||||||
isLogistics,
|
isLogistics,
|
||||||
|
claimable,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -767,7 +769,7 @@ export function LicenseQueuePage() {
|
|||||||
>
|
>
|
||||||
{t("queue.export", "Export CSV")}
|
{t("queue.export", "Export CSV")}
|
||||||
</Button>
|
</Button>
|
||||||
{isLogistics !== false && (
|
{claimable && (
|
||||||
<RequirePermission
|
<RequirePermission
|
||||||
anyOf={[LICENSE_PERMISSIONS.CLAIM_APPLICATION]}
|
anyOf={[LICENSE_PERMISSIONS.CLAIM_APPLICATION]}
|
||||||
hideOnly
|
hideOnly
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { resolveFamilyKind } from "@ema-platform/api";
|
import { resolveFamilyKind } from "@ema-platform/api";
|
||||||
|
import { hasUnclaimedPool, savedViewsForFamily } from "./queue-views";
|
||||||
|
|
||||||
describe("resolveFamilyKind", () => {
|
describe("resolveFamilyKind", () => {
|
||||||
it("treats person-centric seafarer applications as document queues", () => {
|
it("treats person-centric seafarer applications as document queues", () => {
|
||||||
@@ -8,3 +9,28 @@ describe("resolveFamilyKind", () => {
|
|||||||
expect(resolveFamilyKind("BTC_BASIC_TRAINING")).toBe("CERTIFICATE");
|
expect(resolveFamilyKind("BTC_BASIC_TRAINING")).toBe("CERTIFICATE");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("hasUnclaimedPool", () => {
|
||||||
|
it("keeps the unassigned pool for logistics licences", () => {
|
||||||
|
expect(hasUnclaimedPool("FREIGHT_FORWARDER")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps it for vessel registration, a DOCUMENT queue that is still claimed", () => {
|
||||||
|
expect(resolveFamilyKind("VESSEL_REGISTRATION")).toBe("DOCUMENT");
|
||||||
|
expect(hasUnclaimedPool("VESSEL_REGISTRATION")).toBe(true);
|
||||||
|
expect(savedViewsForFamily(hasUnclaimedPool("VESSEL_REGISTRATION")).map((v) => v.id)).toContain(
|
||||||
|
"unassigned",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops it for the person-centric services", () => {
|
||||||
|
expect(hasUnclaimedPool("SEAMAN_BOOK")).toBe(false);
|
||||||
|
expect(savedViewsForFamily(hasUnclaimedPool("SEAMAN_BOOK")).map((v) => v.id)).not.toContain(
|
||||||
|
"unassigned",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps it for the mixed All/Mine grids, where no type is pinned", () => {
|
||||||
|
expect(hasUnclaimedPool(undefined)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { resolveFamilyKind } from '@ema-platform/api';
|
||||||
import type { LicenseStatus, QueueCounts, QueueFilter } from '@ema-platform/api';
|
import type { LicenseStatus, QueueCounts, QueueFilter } from '@ema-platform/api';
|
||||||
|
|
||||||
export type SavedViewId =
|
export type SavedViewId =
|
||||||
@@ -78,12 +79,33 @@ export const SAVED_VIEWS: SavedView[] = [
|
|||||||
export const DEFAULT_VIEW: SavedViewId = 'unassigned';
|
export const DEFAULT_VIEW: SavedViewId = 'unassigned';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Non-logistics queues (Seafarer Registration, Seaman Book, BTC, CoC, ...)
|
* Type keys reviewed off a shared unclaimed pool despite not being logistics
|
||||||
* have no unclaimed pool to triage — those applications aren't claimed off a
|
* licences. Whether a queue is claimed is an officer-workflow property, not a
|
||||||
* shared queue — so the tab that lists it doesn't apply there.
|
* label one: vessel registrations arrive unassigned and officers claim them,
|
||||||
|
* even though the family kind is DOCUMENT because the certificate they produce
|
||||||
|
* is a document rather than a licence.
|
||||||
*/
|
*/
|
||||||
export function savedViewsForFamily(isLogistics: boolean): SavedView[] {
|
const CLAIMABLE_NON_LOGISTICS = new Set(['VESSEL_REGISTRATION']);
|
||||||
return isLogistics
|
|
||||||
|
/**
|
||||||
|
* Does this queue have an unclaimed pool to triage?
|
||||||
|
*
|
||||||
|
* True for the mixed All/Mine grids (no type pinned) — nothing is hidden when
|
||||||
|
* the queue spans every type. False for the person-centric services (Seafarer
|
||||||
|
* Registration, Seaman Book, BTC, CoC, ...), whose applications aren't claimed
|
||||||
|
* off a shared queue, so the tab and the claim actions don't apply there.
|
||||||
|
*/
|
||||||
|
export function hasUnclaimedPool(typeCode: string | undefined): boolean {
|
||||||
|
if (!typeCode) return true;
|
||||||
|
return (
|
||||||
|
resolveFamilyKind(typeCode) === 'LOGISTICS_LICENSE' ||
|
||||||
|
CLAIMABLE_NON_LOGISTICS.has(typeCode)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Drops the Unassigned tab on queues with no unclaimed pool. */
|
||||||
|
export function savedViewsForFamily(claimable: boolean): SavedView[] {
|
||||||
|
return claimable
|
||||||
? SAVED_VIEWS
|
? SAVED_VIEWS
|
||||||
: SAVED_VIEWS.filter((v) => v.id !== 'unassigned');
|
: SAVED_VIEWS.filter((v) => v.id !== 'unassigned');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user