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; /** 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; }