diff --git a/src/lib/services/reports/operational-filters.ts b/src/lib/services/reports/operational-filters.ts new file mode 100644 index 00000000..333c43aa --- /dev/null +++ b/src/lib/services/reports/operational-filters.ts @@ -0,0 +1,27 @@ +/** + * Operational report filters. Mirrors `sales-filters.ts`: the parser is a + * pure, unit-testable function so the route just hands it the query params. + * + * Beta scope is Area only (a berth-area scope). The shape is intentionally + * an object so a Status dimension can be added later without a rename. + */ +export interface OperationalFilters { + areas?: string[]; +} + +/** + * Parse the `area` CSV query param into a free list of port-defined area + * strings. Empty / whitespace entries are dropped. Drizzle parameterises + * the downstream `inArray`, so unvalidated values are injection-safe. + * Returns `undefined` when no areas are active (→ no filter). + */ +export function parseOperationalFilters(params: URLSearchParams): OperationalFilters | undefined { + const raw = params.get('area'); + if (!raw) return undefined; + const areas = raw + .split(',') + .map((s) => s.trim()) + .filter((s) => s.length > 0); + if (areas.length === 0) return undefined; + return { areas }; +} diff --git a/tests/unit/services/reports/operational-filters.test.ts b/tests/unit/services/reports/operational-filters.test.ts new file mode 100644 index 00000000..96b0db25 --- /dev/null +++ b/tests/unit/services/reports/operational-filters.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest'; + +import { parseOperationalFilters } from '@/lib/services/reports/operational-filters'; + +function params(qs: string): URLSearchParams { + return new URLSearchParams(qs); +} + +describe('parseOperationalFilters', () => { + it('returns undefined when no area param is present', () => { + expect(parseOperationalFilters(params(''))).toBeUndefined(); + expect(parseOperationalFilters(params('from=x&to=y'))).toBeUndefined(); + }); + + it('parses a single area', () => { + expect(parseOperationalFilters(params('area=A'))).toEqual({ areas: ['A'] }); + }); + + it('parses a CSV of areas and trims whitespace', () => { + expect(parseOperationalFilters(params('area=A,%20B%20,C'))).toEqual({ + areas: ['A', 'B', 'C'], + }); + }); + + it('drops empty / whitespace-only entries, returning undefined when nothing is left', () => { + expect(parseOperationalFilters(params('area=%20,%20'))).toBeUndefined(); + expect(parseOperationalFilters(params('area='))).toBeUndefined(); + }); +});