From 3d9084c94b8704c75ea740b02c068b0690b06831 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 2 Jun 2026 10:08:16 +0200 Subject: [PATCH] feat(reports): parseOperationalFilters pure parser (Area scope) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../services/reports/operational-filters.ts | 27 +++++++++++++++++ .../reports/operational-filters.test.ts | 29 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/lib/services/reports/operational-filters.ts create mode 100644 tests/unit/services/reports/operational-filters.test.ts 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(); + }); +});