mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-15 18:53:06 +00:00
Omnibox de /campanha/municipios ganha as dimensões Dobradinha (nome do contato), Liderança (nome do contato) e Partido (sigla), com recortes "Sem dobradinha"/"Sem liderança"/"Sem partido" no precedente de "Sem nível". Params aditivos stateDeputy/leadership/party na URL congelada; buildMunicipalityListWhere ganha catálogo request-scoped fail-closed (municipalityRelationSets) que vira as leituras reversa e cruzada em where — sem migration, paginação nativa mantida; facets estendidas e o mapa compartilha o catálogo. Coluna Dobradinhas liberada ao staff (servidor já scoped B37/B157; só o gate de UI mudou). Extrato do par sentinela+exists:false em campaignListUrl (5 call sites).
226 lines
7.9 KiB
TypeScript
226 lines
7.9 KiB
TypeScript
/**
|
||
* State deputy ("dobradinha") list URL contract: state type, param
|
||
* parsing/canonicalization, Payload `where`, sort resolution and hrefs.
|
||
* Mirrors `municipalityListUrl.ts` (paginated, `resolveListUrl` clamp) and
|
||
* `territoryListUrl.ts` / `leadershipListUrl.ts` (lean sort-key/label shape)
|
||
* without importing either.
|
||
*/
|
||
import type { Where } from 'payload'
|
||
|
||
import {
|
||
allParamValues,
|
||
buildListHref,
|
||
collapseListWhereOrBranches,
|
||
createSortToggleHref,
|
||
firstValue,
|
||
NO_PARTY_FILTER_VALUE,
|
||
normalizedText,
|
||
resolveListUrl,
|
||
splitAbsenceFilterValues,
|
||
strictDecimalInteger,
|
||
type RawSearchParams,
|
||
} from '@/utilities/campaignListUrl'
|
||
|
||
export const stateDeputyPageSize = 25
|
||
|
||
export type StateDeputyListSortKey = 'name' | 'party'
|
||
export type StateDeputyListSortDirection = 'asc' | 'desc'
|
||
|
||
export type StateDeputyListState = {
|
||
page: number
|
||
q?: string
|
||
/** Multi-select (OR) party names, plus the `NO_PARTY_FILTER_VALUE` sentinel. */
|
||
parties?: string[]
|
||
sort?: StateDeputyListSortKey
|
||
dir?: StateDeputyListSortDirection
|
||
}
|
||
|
||
export type StateDeputyListSearchParams = RawSearchParams
|
||
|
||
const stateDeputyListParamNames = ['q', 'party', 'sort', 'dir', 'page'] as const
|
||
const stateDeputyListParamNameSet = new Set<string>(stateDeputyListParamNames)
|
||
|
||
const DEFAULT_STATE_DEPUTY_LIST_SORT_KEY: StateDeputyListSortKey = 'name'
|
||
const DEFAULT_STATE_DEPUTY_LIST_SORT_DIR: StateDeputyListSortDirection = 'asc'
|
||
|
||
export const stateDeputyListSortLabels: Record<StateDeputyListSortKey, string> = {
|
||
name: 'Nome',
|
||
party: 'Partido',
|
||
}
|
||
|
||
const stateDeputyListSortKeySet = new Set<string>(Object.keys(stateDeputyListSortLabels))
|
||
const stateDeputyListSortDirSet = new Set<StateDeputyListSortDirection>(['asc', 'desc'])
|
||
|
||
/** Both sort keys default to ascending (A–Z) — no "biggest first" semantics here. */
|
||
export const defaultStateDeputyListSortDir = (): StateDeputyListSortDirection =>
|
||
DEFAULT_STATE_DEPUTY_LIST_SORT_DIR
|
||
|
||
export const resolveStateDeputyListSort = (
|
||
state: StateDeputyListState,
|
||
): { sort: StateDeputyListSortKey; dir: StateDeputyListSortDirection } => {
|
||
const sort = state.sort ?? DEFAULT_STATE_DEPUTY_LIST_SORT_KEY
|
||
return { sort, dir: state.dir ?? defaultStateDeputyListSortDir() }
|
||
}
|
||
|
||
/** `payload.find({ sort })` string — `-` prefix for descending. */
|
||
export const resolveStateDeputyListPayloadSort = (
|
||
sort: StateDeputyListSortKey,
|
||
dir: StateDeputyListSortDirection,
|
||
): string => {
|
||
const path = sort === 'name' ? 'contact.name' : sort
|
||
return dir === 'desc' ? `-${path}` : path
|
||
}
|
||
|
||
export const formatStateDeputyListSortSummary = (
|
||
sort: StateDeputyListSortKey,
|
||
dir: StateDeputyListSortDirection,
|
||
): string => {
|
||
const label = stateDeputyListSortLabels[sort]
|
||
return dir === 'asc' ? `Ordenado por ${label} (A–Z)` : `Ordenado por ${label} (Z–A)`
|
||
}
|
||
|
||
export const stateDeputyListStateToRawParams = (
|
||
state: StateDeputyListState,
|
||
page = state.page,
|
||
): StateDeputyListSearchParams => ({
|
||
page: String(page),
|
||
q: state.q,
|
||
party: state.parties,
|
||
sort: state.sort,
|
||
dir: state.dir,
|
||
})
|
||
|
||
export const parseStateDeputyListParams = (
|
||
params: StateDeputyListSearchParams,
|
||
): StateDeputyListState => {
|
||
const rawPage = strictDecimalInteger(firstValue(params.page))
|
||
const q = normalizedText(firstValue(params.q))
|
||
// `allParamValues` already trims and dedupes; only the length cap is ours.
|
||
const parties = allParamValues(params.party).filter((token) => token.length <= 32)
|
||
const rawSort = firstValue(params.sort)
|
||
const sort =
|
||
rawSort && stateDeputyListSortKeySet.has(rawSort)
|
||
? (rawSort as StateDeputyListSortKey)
|
||
: undefined
|
||
const rawDir = firstValue(params.dir)
|
||
const dir =
|
||
rawDir && stateDeputyListSortDirSet.has(rawDir as StateDeputyListSortDirection)
|
||
? (rawDir as StateDeputyListSortDirection)
|
||
: undefined
|
||
|
||
return {
|
||
page: rawPage ?? 1,
|
||
...(q ? { q } : {}),
|
||
...(parties.length ? { parties } : {}),
|
||
...(sort ? { sort } : {}),
|
||
...(dir ? { dir } : {}),
|
||
}
|
||
}
|
||
|
||
export const buildStateDeputyListWhere = (state: StateDeputyListState): Where => {
|
||
const filters: Where[] = []
|
||
if (state.q) filters.push({ 'contact.name': { contains: state.q } })
|
||
|
||
if (state.parties?.length) {
|
||
const { named: namedParties, hasAbsence } = splitAbsenceFilterValues(
|
||
state.parties,
|
||
(party) => party === NO_PARTY_FILTER_VALUE,
|
||
)
|
||
const partyFilters: Where[] = []
|
||
if (namedParties.length) partyFilters.push({ party: { in: namedParties } })
|
||
if (hasAbsence) partyFilters.push({ party: { exists: false } })
|
||
// Every entry in a non-empty `parties` is named or the sentinel (or both),
|
||
// so `partyFilters` always has at least one member here.
|
||
const branch = collapseListWhereOrBranches(partyFilters)
|
||
if (branch) filters.push(branch)
|
||
}
|
||
|
||
return filters.length ? { and: filters } : {}
|
||
}
|
||
|
||
/**
|
||
* Serializes a state that is ALREADY canonical (came out of
|
||
* `parseStateDeputyListParams`, or was derived from a canonical state by a
|
||
* rule-preserving toggle) — same private-ish contract as
|
||
* `serializeCanonicalMunicipalityListSearchParams`.
|
||
*/
|
||
export const serializeCanonicalStateDeputyListSearchParams = (
|
||
canonicalState: StateDeputyListState,
|
||
): URLSearchParams => {
|
||
const params = new URLSearchParams()
|
||
const resolvedSort = canonicalState.sort ?? DEFAULT_STATE_DEPUTY_LIST_SORT_KEY
|
||
const resolvedDir = canonicalState.dir ?? defaultStateDeputyListSortDir()
|
||
const isSortDefault =
|
||
resolvedSort === DEFAULT_STATE_DEPUTY_LIST_SORT_KEY &&
|
||
resolvedDir === DEFAULT_STATE_DEPUTY_LIST_SORT_DIR
|
||
|
||
if (canonicalState.q) params.set('q', canonicalState.q)
|
||
for (const party of canonicalState.parties ?? []) params.append('party', party)
|
||
if (!isSortDefault) {
|
||
params.set('sort', resolvedSort)
|
||
if (resolvedDir !== DEFAULT_STATE_DEPUTY_LIST_SORT_DIR) params.set('dir', resolvedDir)
|
||
}
|
||
if (canonicalState.page > 1) params.set('page', String(canonicalState.page))
|
||
|
||
return params
|
||
}
|
||
|
||
const buildStateDeputyListSearchParams = (
|
||
state: StateDeputyListState,
|
||
page = state.page,
|
||
): URLSearchParams =>
|
||
serializeCanonicalStateDeputyListSearchParams(
|
||
parseStateDeputyListParams(stateDeputyListStateToRawParams(state, page)),
|
||
)
|
||
|
||
export const buildStateDeputyListHref = (state: StateDeputyListState, page: number): string =>
|
||
buildListHref(state, buildStateDeputyListSearchParams, '/campanha/dobradinhas', page)
|
||
|
||
export const buildStateDeputySortHref = createSortToggleHref<
|
||
StateDeputyListState,
|
||
StateDeputyListSortKey
|
||
>({
|
||
resolveCurrentSort: resolveStateDeputyListSort,
|
||
defaultDir: () => DEFAULT_STATE_DEPUTY_LIST_SORT_DIR,
|
||
buildHref: (state) => buildStateDeputyListHref(state, 1),
|
||
})
|
||
|
||
const sortOptionLabel = (key: StateDeputyListSortKey, dir: StateDeputyListSortDirection): string =>
|
||
`${stateDeputyListSortLabels[key]} (${dir === 'asc' ? 'A–Z' : 'Z–A'})`
|
||
|
||
export const stateDeputyListSortOptions = (
|
||
Object.keys(stateDeputyListSortLabels) as StateDeputyListSortKey[]
|
||
).flatMap((key) =>
|
||
(['asc', 'desc'] as const).map((dir) => ({ key, dir, label: sortOptionLabel(key, dir) })),
|
||
)
|
||
|
||
export const serializeStateDeputySortValue = (
|
||
key: StateDeputyListSortKey,
|
||
dir: StateDeputyListSortDirection,
|
||
): string => `${key}|${dir}`
|
||
|
||
export const parseStateDeputySortValue = (
|
||
value: string,
|
||
): { key: StateDeputyListSortKey; dir: StateDeputyListSortDirection } | null => {
|
||
const [key, dir] = value.split('|')
|
||
if (!stateDeputyListSortKeySet.has(key)) return null
|
||
if (!stateDeputyListSortDirSet.has(dir as StateDeputyListSortDirection)) return null
|
||
return { key: key as StateDeputyListSortKey, dir: dir as StateDeputyListSortDirection }
|
||
}
|
||
|
||
export const resolveStateDeputyListUrl = (
|
||
params: StateDeputyListSearchParams,
|
||
totalPages?: number,
|
||
): {
|
||
state: StateDeputyListState
|
||
href: string
|
||
redirectHref?: string
|
||
} =>
|
||
resolveListUrl({
|
||
params,
|
||
paramNameSet: stateDeputyListParamNameSet,
|
||
parse: parseStateDeputyListParams,
|
||
buildSearchParams: buildStateDeputyListSearchParams,
|
||
basePath: '/campanha/dobradinhas',
|
||
totalPages,
|
||
})
|