import type { MonitorViewport } from '../hooks/useMonitorData'; import { normalizeMonitorBounds } from '../domain/monitor'; export const MONITOR_RETURN_PARAM = 'monitorReturn'; export type MonitorRouteContext = { mode: 'map' | 'list'; keyword: string; protocol: string; status: string; selectedVin: string; detailOpen: boolean; viewport: MonitorViewport; listOffset: number; listLimit: number; hasViewport: boolean; }; const DEFAULT_CONTEXT: MonitorRouteContext = { mode: 'map', keyword: '', protocol: '', status: '', selectedVin: '', detailOpen: false, viewport: { zoom: 5, bounds: '' }, listOffset: 0, listLimit: 50, hasViewport: false }; function integer(value: string | null, fallback: number, minimum: number, maximum: number) { const parsed = Number(value); return Number.isInteger(parsed) && parsed >= minimum && parsed <= maximum ? parsed : fallback; } function decimal(value: string | null, fallback: number, minimum: number, maximum: number) { const parsed = Number(value); return Number.isFinite(parsed) && parsed >= minimum && parsed <= maximum ? parsed : fallback; } export function parseMonitorRouteContext(search: string | URLSearchParams): MonitorRouteContext { const params = typeof search === 'string' ? new URLSearchParams(search) : search; const selectedVin = params.get('selectedVin')?.trim().toUpperCase() ?? ''; const bounds = normalizeMonitorBounds(params.get('bounds')); const hasViewport = params.has('zoom') || Boolean(bounds); return { mode: params.get('mode') === 'list' ? 'list' : 'map', keyword: params.get('keyword')?.trim() ?? '', protocol: params.get('protocol')?.trim() ?? '', status: params.get('status')?.trim() ?? '', selectedVin, detailOpen: Boolean(selectedVin) && params.get('detail') !== 'collapsed', viewport: { zoom: decimal(params.get('zoom'), DEFAULT_CONTEXT.viewport.zoom, 3, 20), bounds }, listOffset: integer(params.get('listOffset'), 0, 0, 1_000_000), listLimit: [20, 50, 100].includes(Number(params.get('listLimit'))) ? Number(params.get('listLimit')) : 50, hasViewport }; } export function buildMonitorPath(context: Omit | MonitorRouteContext) { const params = new URLSearchParams(); if (context.mode !== 'map') params.set('mode', context.mode); if (context.keyword.trim()) params.set('keyword', context.keyword.trim()); if (context.protocol) params.set('protocol', context.protocol); if (context.status) params.set('status', context.status); if (context.selectedVin) { params.set('selectedVin', context.selectedVin); params.set('detail', context.detailOpen ? 'open' : 'collapsed'); } params.set('zoom', String(Math.round(context.viewport.zoom * 100) / 100)); const bounds = normalizeMonitorBounds(context.viewport.bounds); if (bounds) params.set('bounds', bounds); if (context.listOffset) params.set('listOffset', String(context.listOffset)); if (context.listLimit !== 50) params.set('listLimit', String(context.listLimit)); const query = params.toString(); return query ? `/monitor?${query}` : '/monitor'; } export function validatedMonitorReturn(value?: string | null) { if (!value) return ''; try { const parsed = new URL(value, 'https://vehicle-platform.invalid'); if (parsed.origin !== 'https://vehicle-platform.invalid' || parsed.pathname !== '/monitor') return ''; return buildMonitorPath(parseMonitorRouteContext(parsed.searchParams)); } catch { return ''; } } export function withMonitorReturn(path: string, monitorPath: string) { const target = new URL(path, 'https://vehicle-platform.invalid'); const validated = validatedMonitorReturn(monitorPath); if (validated) target.searchParams.set(MONITOR_RETURN_PARAM, validated); return `${target.pathname}${target.search}${target.hash}`; } export function monitorReturnFromParams(params: URLSearchParams) { return validatedMonitorReturn(params.get(MONITOR_RETURN_PARAM)); } export function preserveMonitorReturn(params: URLSearchParams, monitorReturn: string) { if (monitorReturn) params.set(MONITOR_RETURN_PARAM, monitorReturn); else params.delete(MONITOR_RETURN_PARAM); return params; }