Files
emaui/apps/backoffice/src/app/features/license-review/queue-views.ts

164 lines
5.0 KiB
TypeScript

import type { LicenseStatus, QueueCounts, QueueFilter } from '@ema-platform/api';
export type SavedViewId =
| 'unassigned'
| 'mine'
| 'awaitingApplicant'
| 'overdue'
| 'readyToIssue'
| 'all';
export interface SavedView {
id: SavedViewId;
labelKey: string;
/** Which count from `/counts` labels the tab. */
countKey: keyof QueueCounts;
/** Filters this view pins. The user's own facets layer on top. */
filter: Partial<QueueFilter>;
/** Which list endpoint backs it. */
source: 'queue' | 'mine' | 'all';
}
const AWAITING_APPLICANT: LicenseStatus[] = ['RESUBMIT_REQUIRED'];
const READY_TO_ISSUE: LicenseStatus[] = ['PAYMENT_CONFIRMED'];
/**
* The officer's saved views.
*
* Replaces a two-option SegmentedControl (Unclaimed / Mine) that could not
* express the questions officers actually ask — what is late, what is waiting
* on the applicant, what is ready to issue. Each is a filter preset over the
* same grid rather than a separate screen.
*/
export const SAVED_VIEWS: SavedView[] = [
{
id: 'unassigned',
labelKey: 'queue.views.unassigned',
countKey: 'unassigned',
filter: {},
source: 'queue',
},
{
id: 'mine',
labelKey: 'queue.views.mine',
countKey: 'mine',
filter: {},
source: 'mine',
},
{
id: 'awaitingApplicant',
labelKey: 'queue.views.awaitingApplicant',
countKey: 'awaitingApplicant',
filter: { status: AWAITING_APPLICANT },
source: 'all',
},
{
id: 'overdue',
labelKey: 'queue.views.overdue',
countKey: 'overdue',
filter: { overdue: true },
source: 'all',
},
{
id: 'readyToIssue',
labelKey: 'queue.views.readyToIssue',
countKey: 'readyToIssue',
filter: { status: READY_TO_ISSUE },
source: 'all',
},
{
id: 'all',
labelKey: 'queue.views.all',
countKey: 'all',
filter: {},
source: 'all',
},
];
export const DEFAULT_VIEW: SavedViewId = 'unassigned';
/**
* Non-logistics queues (Seafarer Registration, Seaman Book, BTC, CoC, ...)
* have no unclaimed pool to triage — those applications aren't claimed off a
* shared queue — so the tab that lists it doesn't apply there.
*/
export function savedViewsForFamily(isLogistics: boolean): SavedView[] {
return isLogistics
? SAVED_VIEWS
: SAVED_VIEWS.filter((v) => v.id !== 'unassigned');
}
const LAST_VIEW_KEY = 'ema-backoffice-queue-view';
export function readLastView(): SavedViewId {
try {
const stored = localStorage.getItem(LAST_VIEW_KEY) as SavedViewId | null;
return SAVED_VIEWS.some((v) => v.id === stored) ? (stored as SavedViewId) : DEFAULT_VIEW;
} catch {
return DEFAULT_VIEW;
}
}
export function writeLastView(id: SavedViewId): void {
try {
localStorage.setItem(LAST_VIEW_KEY, id);
} catch {
// Not persisting the last view is cosmetic; never break the page for it.
}
}
// ------------------------------------------------------------ URL round-trip
/**
* Reads the user's facets out of the query string.
*
* Filters live in the URL so a filtered queue is a shareable link — "here are
* the six overdue MTO applications" should be something an officer can paste
* into a message, not a state they describe in prose.
*/
export function filterFromSearchParams(params: URLSearchParams): QueueFilter {
const filter: QueueFilter = {};
const search = params.get('q');
if (search) filter.search = search;
const type = params.get('type');
if (type) filter.licenseTypeId = type;
const status = params.get('status');
if (status) filter.status = status.split(',') as LicenseStatus[];
const assignee = params.get('assignee');
if (assignee) filter.assignee = assignee;
const from = params.get('from');
if (from) filter.submittedFrom = from;
const to = params.get('to');
if (to) filter.submittedTo = to;
const sortBy = params.get('sort');
if (sortBy) filter.sortBy = sortBy as QueueFilter['sortBy'];
const sortDir = params.get('dir');
if (sortDir === 'ASC' || sortDir === 'DESC') filter.sortDir = sortDir;
const page = params.get('page');
if (page) {
const parsed = Number(page);
if (Number.isFinite(parsed) && parsed > 0) filter.skip = undefined;
}
return filter;
}
/** Inverse of {@link filterFromSearchParams}. Omits defaults to keep URLs short. */
export function searchParamsFromFilter(
filter: QueueFilter,
view: SavedViewId,
page: number,
): URLSearchParams {
const params = new URLSearchParams();
if (view !== DEFAULT_VIEW) params.set('view', view);
if (filter.search) params.set('q', filter.search);
if (filter.licenseTypeId) params.set('type', filter.licenseTypeId);
if (filter.status?.length) params.set('status', filter.status.join(','));
if (filter.assignee) params.set('assignee', filter.assignee);
if (filter.submittedFrom) params.set('from', filter.submittedFrom);
if (filter.submittedTo) params.set('to', filter.submittedTo);
if (filter.sortBy) params.set('sort', filter.sortBy);
if (filter.sortDir && filter.sortDir !== 'ASC') params.set('dir', filter.sortDir);
if (page > 1) params.set('page', String(page));
return params;
}