import { SEARCH_SCOPE, DEFAULT_SEARCH_LIMIT, SEARCH_LIMITS, } from "@/lib/frontend/search/urlState"; function isNonEmptyString(value) { return typeof value === "string" && value.trim().length > 0; } function normalizeBranch(value) { return isNonEmptyString(value) ? value.trim() : null; } function normalizeLimit(value) { const n = Number(value); if (!Number.isInteger(n)) return DEFAULT_SEARCH_LIMIT; return SEARCH_LIMITS.includes(n) ? n : DEFAULT_SEARCH_LIMIT; } /** * Normalize URL-derived search state for the current user and route context. * * Why this exists: * - The URL is shareable and can contain stale/foreign params. * - The UI route already defines the "branch context" (/:branch/search). * - Branch users must never get cross-branch scope semantics in the UI. * * Policy: * - branch users: force SINGLE on the current route branch * - admin/dev: SINGLE always uses the current route branch * - MULTI/ALL: do not carry a single-branch value (branch=null) * * @param {{ * q: string|null, * scope: "single"|"multi"|"all", * branch: string|null, * branches: string[], * limit: number, * from: string|null, * to: string|null * }|null|undefined} state * @param {{ routeBranch: string, user: { role: string, branchId: string|null }|null }} options * @returns {{ * q: string|null, * scope: "single"|"multi"|"all", * branch: string|null, * branches: string[], * limit: number, * from: string|null, * to: string|null * }} */ export function normalizeSearchUrlStateForUser( state, { routeBranch, user } = {} ) { const route = normalizeBranch(routeBranch); const base = { q: isNonEmptyString(state?.q) ? state.q.trim() : null, scope: state?.scope || SEARCH_SCOPE.SINGLE, branch: normalizeBranch(state?.branch), branches: Array.isArray(state?.branches) ? state.branches : [], limit: normalizeLimit(state?.limit), from: isNonEmptyString(state?.from) ? state.from.trim() : null, to: isNonEmptyString(state?.to) ? state.to.trim() : null, }; // Defensive: if no routeBranch is available (should not happen in this app), // fall back to the parsed branch value. if (!route) { return { ...base, branch: base.scope === SEARCH_SCOPE.SINGLE ? base.branch : null, branches: base.scope === SEARCH_SCOPE.MULTI ? base.branches : [], }; } const role = user?.role; // Branch users: force SINGLE on the current route branch. if (role === "branch") { return { ...base, scope: SEARCH_SCOPE.SINGLE, branch: route, branches: [], }; } // Admin/dev: SINGLE always uses the current route branch (route context wins over URL param). if (role === "admin" || role === "dev") { if (base.scope === SEARCH_SCOPE.SINGLE) { return { ...base, branch: route, branches: [] }; } if (base.scope === SEARCH_SCOPE.MULTI) { return { ...base, branch: null }; } // ALL return { ...base, scope: SEARCH_SCOPE.ALL, branch: null, branches: [] }; } // Unknown roles: fail-safe to route-branch SINGLE. return { ...base, scope: SEARCH_SCOPE.SINGLE, branch: route, branches: [] }; }