mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
153 lines
4.6 KiB
TypeScript
153 lines
4.6 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';
|
|
|
|
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;
|
|
}
|