teqo-backup/src/utilities/leadershipListUrl.ts
Francisco Ignácio Fontoura Solla 3536eb02d3 Deliver B29: header sort and filters on the leaderships list.
Bring B15/B16-style URL sort/filter to /campanha/liderancas on the B33 mold, with shared CampaignTableHead filter slot and B18 exhaustive enum canonicalize from /simplify.
2026-07-28 01:43:44 -03:00

245 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Leadership list URL contract: state, param parse/canonicalize, Payload
* `where`, sort and hrefs. Own module (same shape as `stateDeputyListUrl`) —
* does not import municipality/territory list modules.
*/
import type { Where } from 'payload'
import {
leadershipSectors,
leadershipSupportStatuses,
type SupportStatus,
} from '@/lib/schemas/leadership'
import {
allParamValues,
buildListHref,
firstValue,
normalizedText,
parseExhaustiveEnumParam,
resolveListUrl,
strictDecimalInteger,
type RawSearchParams,
} from '@/utilities/campaignListUrl'
export const leadershipPageSize = 25
export type LeadershipListSortKey = 'name' | 'supportStatus' | 'sector' | 'updatedAt'
export type LeadershipListSortDirection = 'asc' | 'desc'
/** Acesso ao app — exclusive filter (com | sem), same shape as municipality coverage. */
export type LeadershipListAccessFilter = 'com' | 'sem'
type LeadershipSector = (typeof leadershipSectors)[number]
export type LeadershipListState = {
page: number
q?: string
statuses?: SupportStatus[]
sectors?: LeadershipSector[]
municipalities?: number[]
access?: LeadershipListAccessFilter
sort?: LeadershipListSortKey
dir?: LeadershipListSortDirection
}
export type LeadershipListSearchParams = RawSearchParams
const leadershipListParamNames = [
'q',
'status',
'sector',
'municipality',
'access',
'sort',
'dir',
'page',
] as const
const leadershipListParamNameSet = new Set<string>(leadershipListParamNames)
const DEFAULT_LEADERSHIP_LIST_SORT_KEY: LeadershipListSortKey = 'updatedAt'
const DEFAULT_LEADERSHIP_LIST_SORT_DIR: LeadershipListSortDirection = 'desc'
export const leadershipListSortLabels: Record<LeadershipListSortKey, string> = {
name: 'Nome',
supportStatus: 'Status',
sector: 'Setor',
updatedAt: 'Última atualização',
}
const leadershipListSortKeySet = new Set<string>(Object.keys(leadershipListSortLabels))
const leadershipListSortDirSet = new Set<LeadershipListSortDirection>(['asc', 'desc'])
const supportStatusSet = new Set<string>(leadershipSupportStatuses)
const sectorSet = new Set<string>(leadershipSectors)
const accessFilterSet = new Set<LeadershipListAccessFilter>(['com', 'sem'])
/** Temporal = newest first; categorical/name = A–Z. */
export const defaultLeadershipListSortDir = (
key: LeadershipListSortKey,
): LeadershipListSortDirection => (key === 'updatedAt' ? 'desc' : 'asc')
export const resolveLeadershipListSort = (
state: LeadershipListState,
): { sort: LeadershipListSortKey; dir: LeadershipListSortDirection } => {
const sort = state.sort ?? DEFAULT_LEADERSHIP_LIST_SORT_KEY
return { sort, dir: state.dir ?? defaultLeadershipListSortDir(sort) }
}
/** Dotted path for Contact name join; `-` prefix for descending. */
export const resolveLeadershipListPayloadSort = (
sort: LeadershipListSortKey,
dir: LeadershipListSortDirection,
): string => {
const path = sort === 'name' ? 'contact.name' : sort
return dir === 'desc' ? `-${path}` : path
}
export const formatLeadershipListSortSummary = (
sort: LeadershipListSortKey,
dir: LeadershipListSortDirection,
): string => {
const label = leadershipListSortLabels[sort]
if (sort === 'updatedAt') {
return dir === 'desc'
? `Ordenado por ${label} (mais recente)`
: `Ordenado por ${label} (mais antigo)`
}
return dir === 'asc' ? `Ordenado por ${label} (A–Z)` : `Ordenado por ${label} (Z–A)`
}
export const leadershipListStateToRawParams = (
state: LeadershipListState,
page = state.page,
): LeadershipListSearchParams => ({
page: String(page),
q: state.q,
status: state.statuses,
sector: state.sectors,
municipality: state.municipalities?.map(String),
access: state.access,
sort: state.sort,
dir: state.dir,
})
export const parseLeadershipListParams = (
params: LeadershipListSearchParams,
): LeadershipListState => {
const rawPage = strictDecimalInteger(firstValue(params.page))
const q = normalizedText(firstValue(params.q))
const statuses = parseExhaustiveEnumParam<SupportStatus>(params.status, supportStatusSet)
const sectors = parseExhaustiveEnumParam<LeadershipSector>(params.sector, sectorSet)
const municipalities = allParamValues(params.municipality)
.map((token) => strictDecimalInteger(token))
.filter((id): id is number => typeof id === 'number' && id > 0)
const rawAccess = firstValue(params.access)
const access =
rawAccess && accessFilterSet.has(rawAccess as LeadershipListAccessFilter)
? (rawAccess as LeadershipListAccessFilter)
: undefined
const rawSort = firstValue(params.sort)
const sort =
rawSort && leadershipListSortKeySet.has(rawSort)
? (rawSort as LeadershipListSortKey)
: undefined
const rawDir = firstValue(params.dir)
const dir =
rawDir && leadershipListSortDirSet.has(rawDir as LeadershipListSortDirection)
? (rawDir as LeadershipListSortDirection)
: undefined
return {
page: rawPage ?? 1,
...(q ? { q } : {}),
...(statuses.length ? { statuses } : {}),
...(sectors.length ? { sectors } : {}),
...(municipalities.length ? { municipalities } : {}),
...(access ? { access } : {}),
...(sort ? { sort } : {}),
...(dir ? { dir } : {}),
}
}
export const buildLeadershipListWhere = (state: LeadershipListState): Where => {
const filters: Where[] = []
if (state.q) filters.push({ 'contact.name': { contains: state.q } })
if (state.statuses?.length) filters.push({ supportStatus: { in: state.statuses } })
if (state.sectors?.length) filters.push({ sector: { in: state.sectors } })
if (state.municipalities?.length) {
filters.push({ municipalities: { in: state.municipalities } })
}
if (state.access) {
filters.push({ user: { exists: state.access === 'com' } })
}
return filters.length ? { and: filters } : {}
}
/** Expects already-canonical state (from parse or a rule-preserving toggle). */
export const serializeCanonicalLeadershipListSearchParams = (
canonicalState: LeadershipListState,
): URLSearchParams => {
const params = new URLSearchParams()
const resolvedSort = canonicalState.sort ?? DEFAULT_LEADERSHIP_LIST_SORT_KEY
const resolvedDir = canonicalState.dir ?? defaultLeadershipListSortDir(resolvedSort)
const isSortDefault =
resolvedSort === DEFAULT_LEADERSHIP_LIST_SORT_KEY &&
resolvedDir === DEFAULT_LEADERSHIP_LIST_SORT_DIR
if (canonicalState.q) params.set('q', canonicalState.q)
for (const status of canonicalState.statuses ?? []) params.append('status', status)
for (const sector of canonicalState.sectors ?? []) params.append('sector', sector)
for (const municipality of canonicalState.municipalities ?? []) {
params.append('municipality', String(municipality))
}
if (canonicalState.access) params.set('access', canonicalState.access)
if (!isSortDefault) {
params.set('sort', resolvedSort)
if (resolvedDir !== defaultLeadershipListSortDir(resolvedSort)) {
params.set('dir', resolvedDir)
}
}
if (canonicalState.page > 1) params.set('page', String(canonicalState.page))
return params
}
const buildLeadershipListSearchParams = (
state: LeadershipListState,
page = state.page,
): URLSearchParams =>
serializeCanonicalLeadershipListSearchParams(
parseLeadershipListParams(leadershipListStateToRawParams(state, page)),
)
export const buildLeadershipListHref = (state: LeadershipListState, page: number): string =>
buildListHref(state, buildLeadershipListSearchParams, '/campanha/liderancas', page)
export const buildLeadershipSortHref = (
state: LeadershipListState,
nextKey: LeadershipListSortKey,
): string => {
const current = resolveLeadershipListSort(state)
const dir =
current.sort === nextKey
? current.dir === 'asc'
? 'desc'
: 'asc'
: defaultLeadershipListSortDir(nextKey)
return buildLeadershipListHref({ ...state, sort: nextKey, dir, page: 1 }, 1)
}
export const resolveLeadershipListUrl = (
params: LeadershipListSearchParams,
totalPages?: number,
): {
state: LeadershipListState
href: string
redirectHref?: string
} =>
resolveListUrl({
params,
paramNameSet: leadershipListParamNameSet,
parse: parseLeadershipListParams,
buildSearchParams: buildLeadershipListSearchParams,
basePath: '/campanha/liderancas',
totalPages,
})