mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
Merge pull request #323 from fsolla/cursor/pool-i311-f0d9725a-dfcb
B143 — Lideranças — filtrar por organização e dobradinha na omnibox
This commit is contained in:
commit
c90c7d3722
10 changed files with 444 additions and 16 deletions
63
docs/plans/liderancas-omnibox-org-dobradinha-impl.md
Normal file
63
docs/plans/liderancas-omnibox-org-dobradinha-impl.md
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# Impl: Lideranças — filtrar por organização e dobradinha na omnibox
|
||||
|
||||
Status: aprovado
|
||||
Atualizado em: 2026-08-02
|
||||
Issue: #311
|
||||
Intenção: docs/plans/liderancas-omnibox-org-dobradinha.md
|
||||
Appetite restante: herdado (~0,5–1 dia eng)
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** Staff filtra a lista `/campanha/liderancas` por organização e/ou dobradinha via omnibox (sugestões tipadas + chips), com semântica OR inclusiva por dimensão, sem expor estimativas.
|
||||
- **O que NÃO negociar:** lista continua staff-only; sem criação de org/dobradinha na barra; sem filtro “sem organização/dobradinha”; sem estimatedVotes.
|
||||
- **O que reavaliar:** facet cross-filtered (como município) em vez de carregar catálogo inteiro; params URL `organization` / `stateDeputy` (singular, espelho de `municipality`).
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
URL["leadershipListUrl"] --> Where["buildLeadershipListWhere"]
|
||||
URL --> Omnibox["leadershipOmnibox"]
|
||||
Data["leadershipData facets"] --> Page["liderancas/page"]
|
||||
Page --> Filters["LeadershipFilters"]
|
||||
Filters --> Omnibox
|
||||
```
|
||||
|
||||
**Opções consideradas:** A) facet cross-filtered + omnibox · B) catálogo completo no client · C) só uma dimensão
|
||||
**Recomendação:** A — precedente município/status; busca tipada escala; URL canônica reutiliza `campaignListUrl` helpers.
|
||||
**Rejeitadas:** B (lista grande, sem cross-filter); C (aceite pede ambas).
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`leadershipListUrl.ts`:** state `organizations` / `stateDeputies`; parse/serialize `organization` / `stateDeputy`; `where` com `in` OR.
|
||||
- **`leadershipListFilters.ts`:** toggles multi-valor (mesmo contrato de município).
|
||||
- **`leadershipData.ts`:** facets `organizationIDs` / `stateDeputyIDs` (ignoram próprio filtro, unem seleção).
|
||||
- **`leadershipOmnibox.ts`:** chips, seeds, apply/remove para `organization:` / `stateDeputy:`.
|
||||
- **`LeadershipFilters.tsx` + `page.tsx`:** opções de facet com rótulos; placeholder atualizado.
|
||||
- **Migration:** sem migration.
|
||||
- **Access / Consent:** inalterado (staff guard existente).
|
||||
- **UI:** Impeccable B — encaixe na omnibox existente; sem header filter novo.
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **URL + where + filters** — parsers, toggles, `buildLeadershipListWhere`; unit `leadershipListUrl`.
|
||||
2. **Facets + page wiring** — `leadershipData`; int filter por organização.
|
||||
3. **Omnibox** — adapter + `LeadershipFilters`; unit B128.
|
||||
4. **Gates** — `pnpm gate:fast`; push via `pnpm push`.
|
||||
|
||||
## Rabbit holes / Não escopo (engenharia)
|
||||
|
||||
- Header filter popover para org/dobradinha (só omnibox neste item).
|
||||
- Saved filters; sentinela “sem vínculo”.
|
||||
- Alterar omnibox de organizações/dobradinhas (Issues irmãs).
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **Facet pesado:** `select` só do campo relacional, `limit: 0` — mesmo padrão município.
|
||||
- **Rótulo de chip ausente:** unir ID selecionado no facet mesmo fora do recorte atual.
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [x] Aceite de produto da intenção ainda coberto
|
||||
- [x] Invariantes AGENTS/engineering-standards
|
||||
- [x] Testes de domínio previstos (unit URL/omnibox; int filter org)
|
||||
|
|
@ -73,7 +73,9 @@ import {
|
|||
resolveLeadershipListUrl,
|
||||
type LeadershipListState,
|
||||
} from '@/utilities/leadership/leadershipListUrl'
|
||||
import { loadOrganizationNamesByIds } from '@/utilities/loadNamesByIds'
|
||||
import { loadMunicipalityPortfolioIndex } from '@/utilities/municipality/municipalityPortfolioIndex'
|
||||
import { loadStateDeputySummaries } from '@/utilities/stateDeputyData'
|
||||
|
||||
import {
|
||||
setLeadershipMunicipalitiesFormAction,
|
||||
|
|
@ -352,6 +354,30 @@ export default async function LeadershipsPage({ searchParams }: LeadershipsPageP
|
|||
.filter((option): option is LeadershipFilterOption => option !== null)
|
||||
.sort((left, right) => left.label.localeCompare(right.label, 'pt-BR'))
|
||||
|
||||
const [organizationNames, stateDeputySummaries] = await Promise.all([
|
||||
loadOrganizationNamesByIds(payload, filterFacets.organizationIDs),
|
||||
loadStateDeputySummaries(payload, filterFacets.stateDeputyIDs),
|
||||
])
|
||||
|
||||
const organizationFilterOptions: LeadershipFilterOption[] = filterFacets.organizationIDs
|
||||
.map((id) => {
|
||||
const label = organizationNames.get(id)
|
||||
return label ? { value: String(id), label } : null
|
||||
})
|
||||
.filter((option): option is LeadershipFilterOption => option !== null)
|
||||
.sort((left, right) => left.label.localeCompare(right.label, 'pt-BR'))
|
||||
|
||||
const stateDeputyById = new Map(stateDeputySummaries.map((summary) => [summary.id, summary]))
|
||||
const stateDeputyFilterOptions: LeadershipFilterOption[] = filterFacets.stateDeputyIDs
|
||||
.map((id) => {
|
||||
const summary = stateDeputyById.get(id)
|
||||
if (!summary) return null
|
||||
const label = summary.party ? `${summary.name} (${summary.party})` : summary.name
|
||||
return { value: String(id), label }
|
||||
})
|
||||
.filter((option): option is LeadershipFilterOption => option !== null)
|
||||
.sort((left, right) => left.label.localeCompare(right.label, 'pt-BR'))
|
||||
|
||||
const { sort, dir } = resolveLeadershipListSort(state)
|
||||
const sortSummary = formatLeadershipListSortSummary(sort, dir)
|
||||
const columns = leadershipColumns({
|
||||
|
|
@ -384,7 +410,12 @@ export default async function LeadershipsPage({ searchParams }: LeadershipsPageP
|
|||
</div>
|
||||
|
||||
<CampaignListPendingBoundary>
|
||||
<LeadershipFilters state={state} municipalityFilterOptions={municipalityFilterOptions} />
|
||||
<LeadershipFilters
|
||||
state={state}
|
||||
municipalityFilterOptions={municipalityFilterOptions}
|
||||
organizationFilterOptions={organizationFilterOptions}
|
||||
stateDeputyFilterOptions={stateDeputyFilterOptions}
|
||||
/>
|
||||
|
||||
<CampaignListResults>
|
||||
<p className="text-sm text-muted-foreground" aria-live="polite">
|
||||
|
|
|
|||
|
|
@ -22,9 +22,13 @@ import {
|
|||
export const LeadershipFilters = ({
|
||||
state,
|
||||
municipalityFilterOptions,
|
||||
organizationFilterOptions,
|
||||
stateDeputyFilterOptions,
|
||||
}: {
|
||||
state: LeadershipListState
|
||||
municipalityFilterOptions: LeadershipFilterOption[]
|
||||
organizationFilterOptions: LeadershipFilterOption[]
|
||||
stateDeputyFilterOptions: LeadershipFilterOption[]
|
||||
}) => {
|
||||
const { navigate, isPending } = useCampaignListFilterNavigation({
|
||||
state,
|
||||
|
|
@ -41,14 +45,49 @@ export const LeadershipFilters = ({
|
|||
return map
|
||||
}, [municipalityFilterOptions])
|
||||
|
||||
const organizationLabelsById = useMemo(() => {
|
||||
const map = new Map<number, string>()
|
||||
for (const option of organizationFilterOptions) {
|
||||
const id = Number(option.value)
|
||||
if (Number.isSafeInteger(id) && id > 0) map.set(id, option.label)
|
||||
}
|
||||
for (const organizationId of state.organizations ?? []) {
|
||||
if (!map.has(organizationId)) map.set(organizationId, `Organização #${organizationId}`)
|
||||
}
|
||||
return map
|
||||
}, [organizationFilterOptions, state.organizations])
|
||||
|
||||
const stateDeputyLabelsById = useMemo(() => {
|
||||
const map = new Map<number, string>()
|
||||
for (const option of stateDeputyFilterOptions) {
|
||||
const id = Number(option.value)
|
||||
if (Number.isSafeInteger(id) && id > 0) map.set(id, option.label)
|
||||
}
|
||||
for (const stateDeputyId of state.stateDeputies ?? []) {
|
||||
if (!map.has(stateDeputyId)) map.set(stateDeputyId, `Dobradinha #${stateDeputyId}`)
|
||||
}
|
||||
return map
|
||||
}, [stateDeputyFilterOptions, state.stateDeputies])
|
||||
|
||||
const chips = useMemo(
|
||||
() => buildLeadershipOmniboxChips({ state, municipalityLabelsById }),
|
||||
[state, municipalityLabelsById],
|
||||
() =>
|
||||
buildLeadershipOmniboxChips({
|
||||
state,
|
||||
municipalityLabelsById,
|
||||
organizationLabelsById,
|
||||
stateDeputyLabelsById,
|
||||
}),
|
||||
[state, municipalityLabelsById, organizationLabelsById, stateDeputyLabelsById],
|
||||
)
|
||||
|
||||
const suggestionSeeds = useMemo(
|
||||
() => buildLeadershipOmniboxSuggestionSeeds({ municipalityFilterOptions }),
|
||||
[municipalityFilterOptions],
|
||||
() =>
|
||||
buildLeadershipOmniboxSuggestionSeeds({
|
||||
municipalityFilterOptions,
|
||||
organizationFilterOptions,
|
||||
stateDeputyFilterOptions,
|
||||
}),
|
||||
[municipalityFilterOptions, organizationFilterOptions, stateDeputyFilterOptions],
|
||||
)
|
||||
|
||||
const suggestions = useMemo(
|
||||
|
|
@ -75,7 +114,7 @@ export const LeadershipFilters = ({
|
|||
<CampaignListOmnibox
|
||||
id="leadership-omnibox"
|
||||
label="Filtrar lideranças"
|
||||
placeholder="Digite para filtrar (status, município, acesso…)"
|
||||
placeholder="Digite para filtrar (status, município, organização, dobradinha…)"
|
||||
chips={chips}
|
||||
suggestions={suggestions}
|
||||
query={query}
|
||||
|
|
|
|||
|
|
@ -189,6 +189,10 @@ export const loadFreshestMunicipalityLeaderships = async (
|
|||
export type LeadershipListFilterFacets = {
|
||||
/** Municipality ids present under the other active filters (cross-filtered). */
|
||||
municipalityIDs: number[]
|
||||
/** Organization ids present under the other active filters (cross-filtered). */
|
||||
organizationIDs: number[]
|
||||
/** State deputy ids present under the other active filters (cross-filtered). */
|
||||
stateDeputyIDs: number[]
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -200,7 +204,7 @@ const loadLeadershipMunicipalityFacet = async (
|
|||
payload: Payload,
|
||||
user: CampaignUser,
|
||||
state: LeadershipListState,
|
||||
): Promise<LeadershipListFilterFacets> => {
|
||||
): Promise<number[]> => {
|
||||
const result = await payload.find({
|
||||
collection: 'leadership',
|
||||
where: buildLeadershipListWhere({ ...state, municipalities: undefined }),
|
||||
|
|
@ -220,9 +224,74 @@ const loadLeadershipMunicipalityFacet = async (
|
|||
}
|
||||
}
|
||||
|
||||
return {
|
||||
municipalityIDs: [...available].sort((left, right) => left - right),
|
||||
return [...available].sort((left, right) => left - right)
|
||||
}
|
||||
|
||||
const loadLeadershipOrganizationFacet = async (
|
||||
payload: Payload,
|
||||
user: CampaignUser,
|
||||
state: LeadershipListState,
|
||||
): Promise<number[]> => {
|
||||
const result = await payload.find({
|
||||
collection: 'leadership',
|
||||
where: buildLeadershipListWhere({ ...state, organizations: undefined }),
|
||||
depth: 0,
|
||||
limit: 0,
|
||||
pagination: false,
|
||||
select: { organizations: true },
|
||||
user,
|
||||
overrideAccess: false,
|
||||
})
|
||||
|
||||
const available = new Set<number>(state.organizations ?? [])
|
||||
for (const doc of result.docs) {
|
||||
for (const organization of doc.organizations ?? []) {
|
||||
const id = relationshipId(organization)
|
||||
if (id !== null) available.add(id)
|
||||
}
|
||||
}
|
||||
|
||||
return [...available].sort((left, right) => left - right)
|
||||
}
|
||||
|
||||
const loadLeadershipStateDeputyFacet = async (
|
||||
payload: Payload,
|
||||
user: CampaignUser,
|
||||
state: LeadershipListState,
|
||||
): Promise<number[]> => {
|
||||
const result = await payload.find({
|
||||
collection: 'leadership',
|
||||
where: buildLeadershipListWhere({ ...state, stateDeputies: undefined }),
|
||||
depth: 0,
|
||||
limit: 0,
|
||||
pagination: false,
|
||||
select: { stateDeputies: true },
|
||||
user,
|
||||
overrideAccess: false,
|
||||
})
|
||||
|
||||
const available = new Set<number>(state.stateDeputies ?? [])
|
||||
for (const doc of result.docs) {
|
||||
for (const stateDeputy of doc.stateDeputies ?? []) {
|
||||
const id = relationshipId(stateDeputy)
|
||||
if (id !== null) available.add(id)
|
||||
}
|
||||
}
|
||||
|
||||
return [...available].sort((left, right) => left - right)
|
||||
}
|
||||
|
||||
const loadLeadershipFilterFacets = async (
|
||||
payload: Payload,
|
||||
user: CampaignUser,
|
||||
state: LeadershipListState,
|
||||
): Promise<LeadershipListFilterFacets> => {
|
||||
const [municipalityIDs, organizationIDs, stateDeputyIDs] = await Promise.all([
|
||||
loadLeadershipMunicipalityFacet(payload, user, state),
|
||||
loadLeadershipOrganizationFacet(payload, user, state),
|
||||
loadLeadershipStateDeputyFacet(payload, user, state),
|
||||
])
|
||||
return { municipalityIDs, organizationIDs, stateDeputyIDs }
|
||||
}
|
||||
|
||||
export const loadLeadershipListPageData = async (
|
||||
|
|
@ -236,7 +305,12 @@ export const loadLeadershipListPageData = async (
|
|||
filterFacets: LeadershipListFilterFacets
|
||||
}> => {
|
||||
if (!isCampaignStaff(user)) {
|
||||
return { rows: [], totalDocs: 0, totalPages: 0, filterFacets: { municipalityIDs: [] } }
|
||||
return {
|
||||
rows: [],
|
||||
totalDocs: 0,
|
||||
totalPages: 0,
|
||||
filterFacets: { municipalityIDs: [], organizationIDs: [], stateDeputyIDs: [] },
|
||||
}
|
||||
}
|
||||
|
||||
const { sort, dir } = resolveLeadershipListSort(state)
|
||||
|
|
@ -251,7 +325,7 @@ export const loadLeadershipListPageData = async (
|
|||
user,
|
||||
overrideAccess: false,
|
||||
}),
|
||||
loadLeadershipMunicipalityFacet(payload, user, state),
|
||||
loadLeadershipFilterFacets(payload, user, state),
|
||||
])
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export type LeadershipFilterOption = {
|
|||
label: string
|
||||
}
|
||||
|
||||
type LeadershipMultiFilterParam = 'status' | 'municipality'
|
||||
type LeadershipMultiFilterParam = 'status' | 'municipality' | 'organization' | 'stateDeputy'
|
||||
|
||||
export const leadershipStatusFilterOptions: LeadershipFilterOption[] =
|
||||
leadershipSupportStatuses.map((value) => ({ value, label: supportStatusLabels[value] }))
|
||||
|
|
@ -53,6 +53,8 @@ const getLeadershipMultiFilterValues = (
|
|||
param: LeadershipMultiFilterParam,
|
||||
): string[] => {
|
||||
if (param === 'status') return state.statuses ?? []
|
||||
if (param === 'organization') return (state.organizations ?? []).map(String)
|
||||
if (param === 'stateDeputy') return (state.stateDeputies ?? []).map(String)
|
||||
return (state.municipalities ?? []).map(String)
|
||||
}
|
||||
|
||||
|
|
@ -89,6 +91,29 @@ export const clearLeadershipMunicipalityFilter = (
|
|||
state: LeadershipListState,
|
||||
): LeadershipListState => setLeadershipMultiFilterValues(state, 'municipality', [])
|
||||
|
||||
export const toggleLeadershipOrganizationFilter = (
|
||||
state: LeadershipListState,
|
||||
value: string,
|
||||
): LeadershipListState => {
|
||||
if (strictDecimalInteger(value) === undefined) return state
|
||||
return toggleLeadershipMultiFilterValue(state, 'organization', value)
|
||||
}
|
||||
|
||||
export const clearLeadershipOrganizationFilter = (
|
||||
state: LeadershipListState,
|
||||
): LeadershipListState => setLeadershipMultiFilterValues(state, 'organization', [])
|
||||
|
||||
export const toggleLeadershipStateDeputyFilter = (
|
||||
state: LeadershipListState,
|
||||
value: string,
|
||||
): LeadershipListState => {
|
||||
if (strictDecimalInteger(value) === undefined) return state
|
||||
return toggleLeadershipMultiFilterValue(state, 'stateDeputy', value)
|
||||
}
|
||||
|
||||
export const clearLeadershipStateDeputyFilter = (state: LeadershipListState): LeadershipListState =>
|
||||
setLeadershipMultiFilterValues(state, 'stateDeputy', [])
|
||||
|
||||
/** Exclusive toggle — selecting the active value clears; selecting the other replaces. */
|
||||
export const toggleLeadershipAccessFilter = (
|
||||
state: LeadershipListState,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ export type LeadershipListState = {
|
|||
q?: string
|
||||
statuses?: SupportStatus[]
|
||||
municipalities?: number[]
|
||||
organizations?: number[]
|
||||
stateDeputies?: number[]
|
||||
access?: LeadershipListAccessFilter
|
||||
sort?: LeadershipListSortKey
|
||||
dir?: LeadershipListSortDirection
|
||||
|
|
@ -42,6 +44,8 @@ const leadershipListParamNames = [
|
|||
'q',
|
||||
'status',
|
||||
'municipality',
|
||||
'organization',
|
||||
'stateDeputy',
|
||||
'access',
|
||||
'sort',
|
||||
'dir',
|
||||
|
|
@ -105,6 +109,8 @@ export const leadershipListStateToRawParams = (
|
|||
q: state.q,
|
||||
status: state.statuses,
|
||||
municipality: state.municipalities?.map(String),
|
||||
organization: state.organizations?.map(String),
|
||||
stateDeputy: state.stateDeputies?.map(String),
|
||||
access: state.access,
|
||||
sort: state.sort,
|
||||
dir: state.dir,
|
||||
|
|
@ -119,6 +125,12 @@ export const parseLeadershipListParams = (
|
|||
const municipalities = allParamValues(params.municipality)
|
||||
.map((token) => strictDecimalInteger(token))
|
||||
.filter((id): id is number => typeof id === 'number' && id > 0)
|
||||
const organizations = allParamValues(params.organization)
|
||||
.map((token) => strictDecimalInteger(token))
|
||||
.filter((id): id is number => typeof id === 'number' && id > 0)
|
||||
const stateDeputies = allParamValues(params.stateDeputy)
|
||||
.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)
|
||||
|
|
@ -140,6 +152,8 @@ export const parseLeadershipListParams = (
|
|||
...(q ? { q } : {}),
|
||||
...(statuses.length ? { statuses } : {}),
|
||||
...(municipalities.length ? { municipalities } : {}),
|
||||
...(organizations.length ? { organizations } : {}),
|
||||
...(stateDeputies.length ? { stateDeputies } : {}),
|
||||
...(access ? { access } : {}),
|
||||
...(sort ? { sort } : {}),
|
||||
...(dir ? { dir } : {}),
|
||||
|
|
@ -153,6 +167,12 @@ export const buildLeadershipListWhere = (state: LeadershipListState): Where => {
|
|||
if (state.municipalities?.length) {
|
||||
filters.push({ municipalities: { in: state.municipalities } })
|
||||
}
|
||||
if (state.organizations?.length) {
|
||||
filters.push({ organizations: { in: state.organizations } })
|
||||
}
|
||||
if (state.stateDeputies?.length) {
|
||||
filters.push({ stateDeputies: { in: state.stateDeputies } })
|
||||
}
|
||||
if (state.access) {
|
||||
filters.push({ user: { exists: state.access === 'com' } })
|
||||
}
|
||||
|
|
@ -175,6 +195,12 @@ export const serializeCanonicalLeadershipListSearchParams = (
|
|||
for (const municipality of canonicalState.municipalities ?? []) {
|
||||
params.append('municipality', String(municipality))
|
||||
}
|
||||
for (const organization of canonicalState.organizations ?? []) {
|
||||
params.append('organization', String(organization))
|
||||
}
|
||||
for (const stateDeputy of canonicalState.stateDeputies ?? []) {
|
||||
params.append('stateDeputy', String(stateDeputy))
|
||||
}
|
||||
if (canonicalState.access) params.set('access', canonicalState.access)
|
||||
if (!isSortDefault) {
|
||||
params.set('sort', resolvedSort)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import {
|
|||
leadershipStatusFilterOptions,
|
||||
toggleLeadershipAccessFilter,
|
||||
toggleLeadershipMunicipalityFilter,
|
||||
toggleLeadershipOrganizationFilter,
|
||||
toggleLeadershipStateDeputyFilter,
|
||||
toggleLeadershipStatusFilter,
|
||||
type LeadershipFilterOption,
|
||||
} from '@/utilities/leadership/leadershipListFilters'
|
||||
|
|
@ -64,9 +66,13 @@ const leadershipListSortOptions = (
|
|||
export const buildLeadershipOmniboxChips = ({
|
||||
state,
|
||||
municipalityLabelsById,
|
||||
organizationLabelsById,
|
||||
stateDeputyLabelsById,
|
||||
}: {
|
||||
state: LeadershipListState
|
||||
municipalityLabelsById: ReadonlyMap<number, string>
|
||||
organizationLabelsById: ReadonlyMap<number, string>
|
||||
stateDeputyLabelsById: ReadonlyMap<number, string>
|
||||
}): CampaignListOmniboxChip[] => {
|
||||
const chips: CampaignListOmniboxChip[] = []
|
||||
|
||||
|
|
@ -86,6 +92,26 @@ export const buildLeadershipOmniboxChips = ({
|
|||
})
|
||||
}
|
||||
|
||||
for (const organizationId of state.organizations ?? []) {
|
||||
chips.push({
|
||||
id: `organization:${organizationId}`,
|
||||
label: chipLabel(
|
||||
'Organização',
|
||||
organizationLabelsById.get(organizationId) ?? `Organização #${organizationId}`,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
for (const stateDeputyId of state.stateDeputies ?? []) {
|
||||
chips.push({
|
||||
id: `stateDeputy:${stateDeputyId}`,
|
||||
label: chipLabel(
|
||||
'Dobradinha',
|
||||
stateDeputyLabelsById.get(stateDeputyId) ?? `Dobradinha #${stateDeputyId}`,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
if (state.access) {
|
||||
chips.push({
|
||||
id: `access:${state.access}`,
|
||||
|
|
@ -109,8 +135,12 @@ export const buildLeadershipOmniboxChips = ({
|
|||
|
||||
export const buildLeadershipOmniboxSuggestionSeeds = ({
|
||||
municipalityFilterOptions,
|
||||
organizationFilterOptions,
|
||||
stateDeputyFilterOptions,
|
||||
}: {
|
||||
municipalityFilterOptions: readonly LeadershipFilterOption[]
|
||||
organizationFilterOptions: readonly LeadershipFilterOption[]
|
||||
stateDeputyFilterOptions: readonly LeadershipFilterOption[]
|
||||
}) => {
|
||||
const seeds = []
|
||||
|
||||
|
|
@ -139,6 +169,28 @@ export const buildLeadershipOmniboxSuggestionSeeds = ({
|
|||
)
|
||||
}
|
||||
|
||||
for (const option of organizationFilterOptions) {
|
||||
seeds.push(
|
||||
createOmniboxSuggestionSeed({
|
||||
id: `organization:${option.value}`,
|
||||
group: 'Organização',
|
||||
label: option.label,
|
||||
keywords: ['organizacao', 'org', 'sindicato'],
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
for (const option of stateDeputyFilterOptions) {
|
||||
seeds.push(
|
||||
createOmniboxSuggestionSeed({
|
||||
id: `stateDeputy:${option.value}`,
|
||||
group: 'Dobradinha',
|
||||
label: option.label,
|
||||
keywords: ['dobradinha', 'deputado'],
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
for (const option of leadershipAccessFilterOptions) {
|
||||
seeds.push(
|
||||
createOmniboxSuggestionSeed(
|
||||
|
|
@ -198,6 +250,20 @@ export const applyLeadershipOmniboxSuggestion = ({
|
|||
}
|
||||
}
|
||||
|
||||
if (suggestionId.startsWith('organization:')) {
|
||||
return {
|
||||
kind: 'url',
|
||||
state: toggleLeadershipOrganizationFilter(state, suggestionId.slice(13)),
|
||||
}
|
||||
}
|
||||
|
||||
if (suggestionId.startsWith('stateDeputy:')) {
|
||||
return {
|
||||
kind: 'url',
|
||||
state: toggleLeadershipStateDeputyFilter(state, suggestionId.slice(12)),
|
||||
}
|
||||
}
|
||||
|
||||
if (suggestionId.startsWith('access:')) {
|
||||
return {
|
||||
kind: 'url',
|
||||
|
|
@ -252,6 +318,20 @@ export const removeLeadershipOmniboxChip = ({
|
|||
}
|
||||
}
|
||||
|
||||
if (chipId.startsWith('organization:')) {
|
||||
return {
|
||||
kind: 'url',
|
||||
state: toggleLeadershipOrganizationFilter(state, chipId.slice(13)),
|
||||
}
|
||||
}
|
||||
|
||||
if (chipId.startsWith('stateDeputy:')) {
|
||||
return {
|
||||
kind: 'url',
|
||||
state: toggleLeadershipStateDeputyFilter(state, chipId.slice(12)),
|
||||
}
|
||||
}
|
||||
|
||||
if (chipId.startsWith('access:')) {
|
||||
return { kind: 'url', state: toggleLeadershipAccessFilter(state, chipId.slice(7)) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ describe('loadLeadershipListPageData', () => {
|
|||
rows: [],
|
||||
totalDocs: 0,
|
||||
totalPages: 0,
|
||||
filterFacets: { municipalityIDs: [] },
|
||||
filterFacets: { municipalityIDs: [], organizationIDs: [], stateDeputyIDs: [] },
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -82,10 +82,40 @@ describe('loadLeadershipListPageData', () => {
|
|||
rows: [],
|
||||
totalDocs: 0,
|
||||
totalPages: 0,
|
||||
filterFacets: { municipalityIDs: [] },
|
||||
filterFacets: { municipalityIDs: [], organizationIDs: [], stateDeputyIDs: [] },
|
||||
})
|
||||
})
|
||||
|
||||
it('filters by organization with inclusive OR within the dimension', async () => {
|
||||
const fixtures = campaignFixtures()
|
||||
const coordinator = await fixtures.createCampaignUser('coordinator')
|
||||
const municipality = await fixtures.getMunicipality()
|
||||
const marker = fixtures.value('lead-org-filter')
|
||||
const organizationA = await fixtures.createOrganization({ name: `Org A ${marker}` })
|
||||
const organizationB = await fixtures.createOrganization({ name: `Org B ${marker}` })
|
||||
const matchContact = await fixtures.createContact({ name: `Ana ${marker}` })
|
||||
const missContact = await fixtures.createContact({ name: `Bruno ${marker}` })
|
||||
const match = await fixtures.createLeadership({
|
||||
contact: matchContact.id,
|
||||
municipalities: [municipality.id],
|
||||
organizations: [organizationA.id],
|
||||
})
|
||||
await fixtures.createLeadership({
|
||||
contact: missContact.id,
|
||||
municipalities: [municipality.id],
|
||||
organizations: [organizationB.id],
|
||||
})
|
||||
|
||||
const byOrganization = await loadLeadershipListPageData(payload, coordinator, {
|
||||
page: 1,
|
||||
q: marker,
|
||||
organizations: [organizationA.id],
|
||||
})
|
||||
expect(byOrganization.rows.map((row) => row.id)).toEqual([match.id])
|
||||
expect(byOrganization.filterFacets.organizationIDs).toContain(organizationA.id)
|
||||
expect(byOrganization.filterFacets.organizationIDs).toContain(organizationB.id)
|
||||
})
|
||||
|
||||
it('filters by supportStatus, access and municipality', async () => {
|
||||
const fixtures = campaignFixtures()
|
||||
const coordinator = await fixtures.createCampaignUser('coordinator')
|
||||
|
|
|
|||
|
|
@ -4,10 +4,14 @@ import {
|
|||
clearLeadershipAccessFilter,
|
||||
clearLeadershipListFilters,
|
||||
clearLeadershipMunicipalityFilter,
|
||||
clearLeadershipOrganizationFilter,
|
||||
clearLeadershipStateDeputyFilter,
|
||||
clearLeadershipStatusFilter,
|
||||
formatLeadershipActiveFiltersSummary,
|
||||
toggleLeadershipAccessFilter,
|
||||
toggleLeadershipMunicipalityFilter,
|
||||
toggleLeadershipOrganizationFilter,
|
||||
toggleLeadershipStateDeputyFilter,
|
||||
toggleLeadershipStatusFilter,
|
||||
} from '@/utilities/leadership/leadershipListFilters'
|
||||
import {
|
||||
|
|
@ -44,11 +48,15 @@ describe('leadership list URL contract', () => {
|
|||
status: ['engajado', 'a_abordar', 'engajado', 'unknown'],
|
||||
sector: ['religioso'],
|
||||
municipality: ['12', '12', '0', 'abc', '3'],
|
||||
organization: ['5', '5', '0', 'abc', '8'],
|
||||
stateDeputy: ['21', '21', '0', 'abc', '34'],
|
||||
}),
|
||||
).toEqual({
|
||||
page: 1,
|
||||
statuses: ['engajado', 'a_abordar'],
|
||||
municipalities: [12, 3],
|
||||
organizations: [5, 8],
|
||||
stateDeputies: [21, 34],
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -84,12 +92,14 @@ describe('leadership list URL contract', () => {
|
|||
status: ['a_abordar'],
|
||||
sector: ['religioso'],
|
||||
municipality: ['7'],
|
||||
organization: ['5', '8'],
|
||||
stateDeputy: ['21'],
|
||||
access: 'sem',
|
||||
sort: 'name',
|
||||
dir: 'asc',
|
||||
})
|
||||
expect(buildLeadershipListHref(state, 1)).toBe(
|
||||
'/campanha/liderancas?q=ana&status=a_abordar&municipality=7&access=sem&sort=name',
|
||||
'/campanha/liderancas?q=ana&status=a_abordar&municipality=7&organization=5&organization=8&stateDeputy=21&access=sem&sort=name',
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -155,6 +165,16 @@ describe('leadership list filter state', () => {
|
|||
expect(clearLeadershipAccessFilter(withAccess)).toEqual({ page: 1 })
|
||||
})
|
||||
|
||||
it('toggles organization and stateDeputy filters and resets the page', () => {
|
||||
const withOrganization = toggleLeadershipOrganizationFilter({ page: 2 }, '15')
|
||||
expect(withOrganization).toEqual({ page: 1, organizations: [15] })
|
||||
expect(toggleLeadershipOrganizationFilter(withOrganization, '15')).toEqual({ page: 1 })
|
||||
|
||||
const withStateDeputy = toggleLeadershipStateDeputyFilter({ page: 2 }, '9')
|
||||
expect(withStateDeputy).toEqual({ page: 1, stateDeputies: [9] })
|
||||
expect(toggleLeadershipStateDeputyFilter(withStateDeputy, '9')).toEqual({ page: 1 })
|
||||
})
|
||||
|
||||
it('clears only one filter, keeping q and sort', () => {
|
||||
expect(
|
||||
clearLeadershipStatusFilter({
|
||||
|
|
@ -172,6 +192,21 @@ describe('leadership list filter state', () => {
|
|||
access: 'sem',
|
||||
}),
|
||||
).toEqual({ page: 1, access: 'sem' })
|
||||
expect(
|
||||
clearLeadershipOrganizationFilter({
|
||||
page: 1,
|
||||
organizations: [5],
|
||||
q: 'ana',
|
||||
}),
|
||||
).toEqual({ page: 1, q: 'ana' })
|
||||
expect(
|
||||
clearLeadershipStateDeputyFilter({
|
||||
page: 1,
|
||||
stateDeputies: [3],
|
||||
sort: 'name',
|
||||
dir: 'asc',
|
||||
}),
|
||||
).toEqual({ page: 1, sort: 'name', dir: 'asc' })
|
||||
})
|
||||
|
||||
it('clears every filter and the search, keeping sort', () => {
|
||||
|
|
|
|||
|
|
@ -78,11 +78,36 @@ describe('list omnibox adapters (B128)', () => {
|
|||
expect(applied.state.statuses).toContain('engajado')
|
||||
}
|
||||
|
||||
const seeds = buildLeadershipOmniboxSuggestionSeeds({ municipalityFilterOptions: [] })
|
||||
const seeds = buildLeadershipOmniboxSuggestionSeeds({
|
||||
municipalityFilterOptions: [],
|
||||
organizationFilterOptions: [],
|
||||
stateDeputyFilterOptions: [],
|
||||
})
|
||||
const suggestions = filterLeadershipOmniboxSuggestions(seeds, 'ordenar')
|
||||
expect(suggestions.some((entry) => entry.group === 'Ordenação')).toBe(true)
|
||||
})
|
||||
|
||||
it('leadership applies organization and stateDeputy toggles', () => {
|
||||
const base = parseLeadershipListParams({})
|
||||
const organization = applyLeadershipOmniboxSuggestion({
|
||||
state: base,
|
||||
suggestionId: 'organization:12',
|
||||
})
|
||||
expect(organization.kind).toBe('url')
|
||||
if (organization.kind === 'url') {
|
||||
expect(organization.state.organizations).toEqual([12])
|
||||
}
|
||||
|
||||
const stateDeputy = applyLeadershipOmniboxSuggestion({
|
||||
state: base,
|
||||
suggestionId: 'stateDeputy:7',
|
||||
})
|
||||
expect(stateDeputy.kind).toBe('url')
|
||||
if (stateDeputy.kind === 'url') {
|
||||
expect(stateDeputy.state.stateDeputies).toEqual([7])
|
||||
}
|
||||
})
|
||||
|
||||
it('state deputy toggles party filter', () => {
|
||||
const base = parseStateDeputyListParams({})
|
||||
const applied = applyStateDeputyOmniboxSuggestion({
|
||||
|
|
|
|||
Loading…
Reference in a new issue