import { SEARCH_SCOPE } from "@/lib/frontend/search/urlState"; import { ApiClientError } from "@/lib/frontend/apiClient"; import { getSearchDateRangeValidation } from "@/lib/frontend/search/searchDateValidation"; function isNonEmptyString(value) { return typeof value === "string" && value.trim().length > 0; } function toTrimmedOrNull(value) { return isNonEmptyString(value) ? value.trim() : null; } function buildValidationError(code, message, details) { return new ApiClientError({ status: 400, code, message, details, }); } /** * Build the apiClient.search(...) input from URL state + current user context. * * Return shape: * - input: object for apiClient.search(...) or null (no search yet / not ready) * - error: ApiClientError or null (local validation / fast-fail) * * UX policy for MULTI without branches: * - Treat it as "not ready" (input=null, error=null) instead of an error. * * @param {{ * urlState: { * q: string|null, * scope: "single"|"multi"|"all", * branch: string|null, * branches: string[], * from: string|null, * to: string|null * }, * routeBranch: string, * user: { role: string, branchId: string|null }|null, * cursor?: string|null, * limit?: number * }} args * @returns {{ input: any|null, error: any|null }} */ export function buildSearchApiInput({ urlState, routeBranch, user, cursor = null, limit = 100, }) { const q = isNonEmptyString(urlState?.q) ? urlState.q.trim() : null; // UI policy (RHL-024): q is required to trigger a search. if (!q) return { input: null, error: null }; // --- Date range validation (RHL-025) ------------------------------------ const from = toTrimmedOrNull(urlState?.from); const to = toTrimmedOrNull(urlState?.to); const dateValidation = getSearchDateRangeValidation(from, to); if (dateValidation) { return { input: null, error: buildValidationError( dateValidation.code, dateValidation.message, dateValidation.details ), }; } // --- Build input --------------------------------------------------------- const input = { q, limit }; if (from) input.from = from; if (to) input.to = to; if (isNonEmptyString(cursor)) input.cursor = cursor.trim(); const role = user?.role; // Branch users: always restricted to the current route branch. if (role === "branch") { input.branch = routeBranch; return { input, error: null }; } // Admin/dev: respect scope rules from URL state. if (role === "admin" || role === "dev") { if (urlState.scope === SEARCH_SCOPE.ALL) { input.scope = "all"; return { input, error: null }; } if (urlState.scope === SEARCH_SCOPE.MULTI) { const branches = Array.isArray(urlState.branches) ? urlState.branches : []; // UX: missing branches is not an error, it's "not ready". if (branches.length === 0) { return { input: null, error: null }; } input.scope = "multi"; input.branches = branches; return { input, error: null }; } // SINGLE input.branch = routeBranch; return { input, error: null }; } // Unknown role: fail-safe to single-branch using the route context. input.branch = routeBranch; return { input, error: null }; }