diff --git a/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx b/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx index c2324581..8b8bf8d6 100644 --- a/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx +++ b/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx @@ -8,21 +8,14 @@ import Link from 'next/link' import { redirect } from 'next/navigation' import { getPayload } from 'payload' -import { CampaignListPagination } from '@/components/campaign/CampaignListPagination' +import { CampaignListEmptyState } from '@/components/campaign/CampaignListEmptyState' +import { CampaignListFooter } from '@/components/campaign/CampaignListFooter' import { CampaignPageShell } from '@/components/campaign/CampaignPageShell' import { CampaignScopeBadge } from '@/components/campaign/CampaignScopeBadge' import { SupporterFilters } from '@/components/campaign/SupporterFilters' import { SupporterList } from '@/components/campaign/SupporterList' import { SupporterListOverview } from '@/components/campaign/SupporterListOverview' import { Button } from '@/components/ui/button' -import { - Empty, - EmptyContent, - EmptyDescription, - EmptyHeader, - EmptyMedia, - EmptyTitle, -} from '@/components/ui/Empty' import { isCampaignCoordinator, isCampaignUnrestricted } from '@/utilities/campaignAccess' import { getCampaignUser } from '@/utilities/campaignAuth' import { campaignRoleLabels } from '@/utilities/campaignUserProfile' @@ -57,35 +50,25 @@ export default async function SupportersPage({ searchParams }: SupportersPagePro toSupporterListItemViewModel(supporter))} /> -
-

- {result.totalDocs}{' '} - {result.totalDocs === 1 ? 'apoiador encontrado' : 'apoiadores encontrados'} -

- buildSupporterListHref(state, page)} - /> -
+ buildSupporterListHref(state, page)} + /> ) : ( - - - - - Nenhum apoiador encontrado - - Ajuste a busca ou os filtros. Você só vê apoiadores dentro do seu escopo. - - - - - - + + + ) return ( diff --git a/src/app/(campaign)/campanha/(app)/assessores/page.tsx b/src/app/(campaign)/campanha/(app)/assessores/page.tsx index 0ae45a1f..574e9d3b 100644 --- a/src/app/(campaign)/campanha/(app)/assessores/page.tsx +++ b/src/app/(campaign)/campanha/(app)/assessores/page.tsx @@ -6,7 +6,7 @@ import { CampaignListPendingBoundary, CampaignListResults, } from '@/components/campaign/CampaignListPending' -import { CampaignListPagination } from '@/components/campaign/CampaignListPagination' +import { CampaignListFooter } from '@/components/campaign/CampaignListFooter' import { CampaignPageShell } from '@/components/campaign/CampaignPageShell' import { CampaignSearchForm } from '@/components/campaign/CampaignSearchForm' import { AdvisorsTable } from '@/components/campaign/AdvisorsTable' @@ -73,16 +73,14 @@ export default async function AdvisorsPage({ searchParams }: AdvisorsPageProps) /> {totalDocs > 0 ? ( -
-

- {totalDocs} {totalDocs === 1 ? 'assessor' : 'assessores'} -

- advisorListHrefForPage(state, page)} - /> -
+ advisorListHrefForPage(state, page)} + /> ) : null} diff --git a/src/app/(campaign)/campanha/(app)/demandas/page.tsx b/src/app/(campaign)/campanha/(app)/demandas/page.tsx index 60de0edc..def7a68e 100644 --- a/src/app/(campaign)/campanha/(app)/demandas/page.tsx +++ b/src/app/(campaign)/campanha/(app)/demandas/page.tsx @@ -5,30 +5,20 @@ import { redirect } from 'next/navigation' import { getPayload } from 'payload' import { CampaignFilterChips } from '@/components/campaign/CampaignFilterChips' -import { CampaignListPagination } from '@/components/campaign/CampaignListPagination' +import { CampaignListEmptyState } from '@/components/campaign/CampaignListEmptyState' +import { CampaignListFooter } from '@/components/campaign/CampaignListFooter' import { CampaignListPendingBoundary, CampaignListResults, } from '@/components/campaign/CampaignListPending' import { CampaignPageShell } from '@/components/campaign/CampaignPageShell' +import { + CampaignTable, + CampaignTableHead, + type CampaignTableColumn, +} from '@/components/campaign/CampaignTable' import { Badge } from '@/components/ui/Badge' import { Button } from '@/components/ui/button' -import { - Empty, - EmptyContent, - EmptyDescription, - EmptyHeader, - EmptyMedia, - EmptyTitle, -} from '@/components/ui/Empty' -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/Table' import { campaignDemandKindLabels, campaignDemandStatusLabels, @@ -37,7 +27,12 @@ import { } from '@/lib/schemas/campaignDemand' import { isCampaignStaff } from '@/utilities/campaignAccess' import { getCampaignUser } from '@/utilities/campaignAuth' -import { loadDemandListPageData, parseDemandListParams } from '@/utilities/campaignDemandData' +import { + buildDemandListHref, + loadDemandListPageData, + parseDemandListParams, + type DemandRowViewModel, +} from '@/utilities/campaignDemandData' type DemandsPageProps = { searchParams: Promise> @@ -56,6 +51,52 @@ const statusVariant: Record< const dateFormatter = new Intl.DateTimeFormat('pt-BR', { dateStyle: 'short' }) +const demandColumns: Array> = [ + { + id: 'title', + mandatory: true, + head: Demanda, + cellClassName: 'max-w-64 whitespace-normal', + cell: (row) => ( + + {row.title} + + ), + }, + { + id: 'kind', + head: Tipo, + cell: (row) => campaignDemandKindLabels[row.kind], + }, + { + id: 'municipality', + head: Praça, + cellClassName: 'text-muted-foreground', + cell: (row) => row.municipalityName, + }, + { + id: 'requester', + head: Solicitante, + cellClassName: 'text-muted-foreground', + cell: (row) => row.requesterName ?? '—', + }, + { + id: 'status', + head: Status, + cell: (row) => ( + {campaignDemandStatusLabels[row.status]} + ), + }, + { + id: 'createdAt', + head: Aberta em, + cell: (row) => dateFormatter.format(new Date(row.createdAt)), + }, +] + export default async function DemandsPage({ searchParams }: DemandsPageProps) { const rawSearchParams = await searchParams const [user, payload] = await Promise.all([getCampaignUser(), getPayload({ config })]) @@ -69,86 +110,8 @@ export default async function DemandsPage({ searchParams }: DemandsPageProps) { state, ) - const hrefFor = (status?: CampaignDemandStatus, page = 1) => { - const params = new URLSearchParams() - if (status) params.set('status', status) - if (state.kind) params.set('kind', state.kind) - if (page > 1) params.set('page', String(page)) - const query = params.toString() - return query ? `/campanha/demandas?${query}` : '/campanha/demandas' - } - - const listBody = rows.length ? ( - <> -
- - - - Demanda - Tipo - Praça - Solicitante - Status - Aberta em - - - - {rows.map((row) => ( - - - - {row.title} - - - {campaignDemandKindLabels[row.kind]} - {row.municipalityName} - {row.requesterName ?? '—'} - - - {campaignDemandStatusLabels[row.status]} - - - {dateFormatter.format(new Date(row.createdAt))} - - ))} - -
-
-
-

- {totalDocs} {totalDocs === 1 ? 'demanda' : 'demandas'} -

- hrefFor(state.status, page)} - /> -
- - ) : ( - - - - - Nenhuma demanda por aqui - - Abra uma demanda quando precisar de material, transporte, espaço ou apoio para uma ação. - - - - - - - ) + const hrefForStatus = (status?: CampaignDemandStatus) => + buildDemandListHref({ ...state, status }, 1) return ( @@ -175,16 +138,43 @@ export default async function DemandsPage({ searchParams }: DemandsPageProps) { ({ - href: hrefFor(status), + href: hrefForStatus(status), label: campaignDemandStatusLabels[status], active: state.status === status, })), ]} /> - {listBody} + + {rows.length ? ( + <> + row.id} /> + buildDemandListHref(state, page)} + /> + + ) : ( + + + + )} + ) diff --git a/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx b/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx index 321525ef..c315c333 100644 --- a/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx +++ b/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx @@ -1,44 +1,70 @@ -import { - CampaignListPendingBoundary, - CampaignListResults, -} from '@/components/campaign/CampaignListPending' -import { CampaignSearchForm } from '@/components/campaign/CampaignSearchForm' import config from '@payload-config' import { PlusIcon, SearchXIcon } from 'lucide-react' import Link from 'next/link' import { redirect } from 'next/navigation' import { getPayload } from 'payload' -import { CampaignListPagination } from '@/components/campaign/CampaignListPagination' +import { CampaignListEmptyState } from '@/components/campaign/CampaignListEmptyState' +import { CampaignListFooter } from '@/components/campaign/CampaignListFooter' +import { + CampaignListPendingBoundary, + CampaignListResults, +} from '@/components/campaign/CampaignListPending' import { CampaignPageShell } from '@/components/campaign/CampaignPageShell' +import { CampaignSearchForm } from '@/components/campaign/CampaignSearchForm' +import { + CampaignTable, + CampaignTableHead, + type CampaignTableColumn, +} from '@/components/campaign/CampaignTable' import { Button } from '@/components/ui/button' -import { - Empty, - EmptyContent, - EmptyDescription, - EmptyHeader, - EmptyMedia, - EmptyTitle, -} from '@/components/ui/Empty' -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/Table' import { isCampaignStaff } from '@/utilities/campaignAccess' import { getCampaignUser } from '@/utilities/campaignAuth' import { + buildStateDeputyListHref, loadStateDeputyListPageData, parseStateDeputyListParams, + type StateDeputyRowViewModel, } from '@/utilities/stateDeputyData' type StateDeputiesPageProps = { searchParams: Promise> } +const stateDeputyColumns: Array> = [ + { + id: 'name', + mandatory: true, + head: Nome, + cell: (row) => ( + + {row.name} + + ), + }, + { + id: 'party', + head: Partido, + cellClassName: 'text-muted-foreground', + cell: (row) => row.party ?? '—', + }, + { + id: 'municipalities', + head: Municípios, + cellClassName: 'tabular-nums', + cell: (row) => row.municipalityCount, + }, + { + id: 'leaderships', + head: Lideranças, + cellClassName: 'tabular-nums', + cell: (row) => row.leadershipCount, + }, +] + export default async function StateDeputiesPage({ searchParams }: StateDeputiesPageProps) { const rawSearchParams = await searchParams const [user, payload] = await Promise.all([getCampaignUser(), getPayload({ config })]) @@ -48,14 +74,6 @@ export default async function StateDeputiesPage({ searchParams }: StateDeputiesP const state = parseStateDeputyListParams(rawSearchParams) const { rows, totalDocs, totalPages } = await loadStateDeputyListPageData(payload, user, state) - const hrefForPage = (page: number) => { - const params = new URLSearchParams() - if (state.q) params.set('q', state.q) - if (page > 1) params.set('page', String(page)) - const query = params.toString() - return query ? `/campanha/dobradinhas?${query}` : '/campanha/dobradinhas' - } - return (
@@ -85,66 +103,29 @@ export default async function StateDeputiesPage({ searchParams }: StateDeputiesP {rows.length ? ( <> -
- - - - Nome - Partido - Municípios - Lideranças - - - - {rows.map((row) => ( - - - - {row.name} - - - {row.party ?? '—'} - {row.municipalityCount} - {row.leadershipCount} - - ))} - -
-
-
-

- {totalDocs} {totalDocs === 1 ? 'dobradinha' : 'dobradinhas'} -

- -
+ row.id} /> + buildStateDeputyListHref(state, page)} + /> ) : ( - - - - - Nenhuma dobradinha cadastrada - - Cadastre deputados estaduais parceiros para vincular a municípios e lideranças. - - - - - - + + + )}
diff --git a/src/app/(campaign)/campanha/(app)/liderancas/page.tsx b/src/app/(campaign)/campanha/(app)/liderancas/page.tsx index f13a69b5..3bdd7503 100644 --- a/src/app/(campaign)/campanha/(app)/liderancas/page.tsx +++ b/src/app/(campaign)/campanha/(app)/liderancas/page.tsx @@ -1,43 +1,80 @@ -import { - CampaignListPendingBoundary, - CampaignListResults, -} from '@/components/campaign/CampaignListPending' -import { CampaignSearchForm } from '@/components/campaign/CampaignSearchForm' import config from '@payload-config' import { PlusIcon, SearchXIcon } from 'lucide-react' import Link from 'next/link' import { redirect } from 'next/navigation' import { getPayload } from 'payload' -import { CampaignListPagination } from '@/components/campaign/CampaignListPagination' +import { CampaignListEmptyState } from '@/components/campaign/CampaignListEmptyState' +import { CampaignListFooter } from '@/components/campaign/CampaignListFooter' +import { + CampaignListPendingBoundary, + CampaignListResults, +} from '@/components/campaign/CampaignListPending' import { CampaignPageShell } from '@/components/campaign/CampaignPageShell' +import { CampaignSearchForm } from '@/components/campaign/CampaignSearchForm' +import { + CampaignTable, + CampaignTableHead, + type CampaignTableColumn, +} from '@/components/campaign/CampaignTable' import { SupportStatusBadge } from '@/components/campaign/SupportStatusBadge' import { Badge } from '@/components/ui/Badge' import { Button } from '@/components/ui/button' -import { - Empty, - EmptyContent, - EmptyDescription, - EmptyHeader, - EmptyMedia, - EmptyTitle, -} from '@/components/ui/Empty' -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/Table' import { isCampaignStaff } from '@/utilities/campaignAccess' import { getCampaignUser } from '@/utilities/campaignAuth' -import { loadLeadershipListPageData, parseLeadershipListParams } from '@/utilities/leadershipData' +import { + buildLeadershipListHref, + loadLeadershipListPageData, + parseLeadershipListParams, + type LeadershipRowViewModel, +} from '@/utilities/leadershipData' type LeadershipsPageProps = { searchParams: Promise> } +const leadershipColumns: Array> = [ + { + id: 'name', + mandatory: true, + head: Nome, + cell: (row) => ( + + {row.name} + + ), + }, + { + id: 'supportStatus', + head: Status, + cell: (row) => (row.supportStatus ? : '—'), + }, + { + id: 'municipalities', + head: Praças, + cellClassName: 'max-w-64 whitespace-normal text-muted-foreground', + cell: (row) => row.municipalityNames.join(', ') || '—', + }, + { + id: 'organizations', + head: Organizações, + cellClassName: 'max-w-56 whitespace-normal text-muted-foreground', + cell: (row) => row.organizationNames.join(', ') || '—', + }, + { + id: 'appAccess', + head: Acesso ao app, + cell: (row) => ( + + {row.hasAppAccess ? 'Com acesso' : 'Sem acesso'} + + ), + }, +] + export default async function LeadershipsPage({ searchParams }: LeadershipsPageProps) { const rawSearchParams = await searchParams const [user, payload] = await Promise.all([getCampaignUser(), getPayload({ config })]) @@ -47,14 +84,6 @@ export default async function LeadershipsPage({ searchParams }: LeadershipsPageP const state = parseLeadershipListParams(rawSearchParams) const { rows, totalDocs, totalPages } = await loadLeadershipListPageData(payload, user, state) - const hrefForPage = (page: number) => { - const params = new URLSearchParams() - if (state.q) params.set('q', state.q) - if (page > 1) params.set('page', String(page)) - const query = params.toString() - return query ? `/campanha/liderancas?${query}` : '/campanha/liderancas' - } - return (
@@ -83,83 +112,29 @@ export default async function LeadershipsPage({ searchParams }: LeadershipsPageP {rows.length ? ( <> -
- - - - Nome - Status - Praças - Organizações - Acesso ao app - - - - {rows.map((row) => ( - - - - {row.name} - - - - {row.supportStatus ? ( - - ) : ( - '—' - )} - - - {row.municipalityNames.join(', ') || '—'} - - - {row.organizationNames.join(', ') || '—'} - - - - {row.hasAppAccess ? 'Com acesso' : 'Sem acesso'} - - - - ))} - -
-
-
-

- {totalDocs} {totalDocs === 1 ? 'liderança' : 'lideranças'} -

- -
+ row.id} /> + buildLeadershipListHref(state, page)} + /> ) : ( - - - - - Nenhuma liderança encontrada - - Cadastre a primeira liderança ou ajuste a busca. Você só vê lideranças das suas - Praças. - - - - - - + + + )}
diff --git a/src/app/(campaign)/campanha/(app)/organizacoes/page.tsx b/src/app/(campaign)/campanha/(app)/organizacoes/page.tsx index efa10de2..df1289b6 100644 --- a/src/app/(campaign)/campanha/(app)/organizacoes/page.tsx +++ b/src/app/(campaign)/campanha/(app)/organizacoes/page.tsx @@ -1,46 +1,71 @@ -import { - CampaignListPendingBoundary, - CampaignListResults, -} from '@/components/campaign/CampaignListPending' -import { CampaignSearchForm } from '@/components/campaign/CampaignSearchForm' import config from '@payload-config' import { PlusIcon, SearchXIcon } from 'lucide-react' import Link from 'next/link' import { redirect } from 'next/navigation' import { getPayload } from 'payload' -import { CampaignListPagination } from '@/components/campaign/CampaignListPagination' +import { CampaignListEmptyState } from '@/components/campaign/CampaignListEmptyState' +import { CampaignListFooter } from '@/components/campaign/CampaignListFooter' +import { + CampaignListPendingBoundary, + CampaignListResults, +} from '@/components/campaign/CampaignListPending' import { CampaignPageShell } from '@/components/campaign/CampaignPageShell' +import { CampaignSearchForm } from '@/components/campaign/CampaignSearchForm' +import { + CampaignTable, + CampaignTableHead, + type CampaignTableColumn, +} from '@/components/campaign/CampaignTable' import { Badge } from '@/components/ui/Badge' import { Button } from '@/components/ui/button' -import { - Empty, - EmptyContent, - EmptyDescription, - EmptyHeader, - EmptyMedia, - EmptyTitle, -} from '@/components/ui/Empty' -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/Table' import { organizationKindLabels } from '@/lib/schemas/organization' import { isCampaignStaff } from '@/utilities/campaignAccess' import { getCampaignUser } from '@/utilities/campaignAuth' import { + buildOrganizationListHref, loadOrganizationListPageData, parseOrganizationListParams, + type OrganizationRowViewModel, } from '@/utilities/organizationData' type OrganizationsPageProps = { searchParams: Promise> } +const organizationColumns: Array> = [ + { + id: 'name', + mandatory: true, + head: Nome, + cell: (row) => ( + + {row.name} + + ), + }, + { + id: 'kind', + head: Tipo, + cell: (row) => {organizationKindLabels[row.kind]}, + }, + { + id: 'municipalities', + head: Praças de atuação, + cellClassName: 'max-w-64 whitespace-normal text-muted-foreground', + cell: (row) => row.municipalityNames.join(', ') || '—', + }, + { + id: 'leaderships', + head: Lideranças, + cellClassName: 'tabular-nums', + cell: (row) => row.leadershipCount, + }, +] + export default async function OrganizationsPage({ searchParams }: OrganizationsPageProps) { const rawSearchParams = await searchParams const [user, payload] = await Promise.all([getCampaignUser(), getPayload({ config })]) @@ -50,15 +75,6 @@ export default async function OrganizationsPage({ searchParams }: OrganizationsP const state = parseOrganizationListParams(rawSearchParams) const { rows, totalDocs, totalPages } = await loadOrganizationListPageData(payload, user, state) - const hrefForPage = (page: number) => { - const params = new URLSearchParams() - if (state.q) params.set('q', state.q) - if (state.kind) params.set('kind', state.kind) - if (page > 1) params.set('page', String(page)) - const query = params.toString() - return query ? `/campanha/organizacoes?${query}` : '/campanha/organizacoes' - } - return (
@@ -88,71 +104,29 @@ export default async function OrganizationsPage({ searchParams }: OrganizationsP {rows.length ? ( <> -
- - - - Nome - Tipo - Praças de atuação - Lideranças - - - - {rows.map((row) => ( - - - - {row.name} - - - - {organizationKindLabels[row.kind]} - - - {row.municipalityNames.join(', ') || '—'} - - {row.leadershipCount} - - ))} - -
-
-
-

- {totalDocs} {totalDocs === 1 ? 'organização' : 'organizações'} -

- -
+ row.id} /> + buildOrganizationListHref(state, page)} + /> ) : ( - - - - - Nenhuma organização cadastrada - - Cadastre sindicatos, associações e movimentos para vincular lideranças e Planos de - Ação. - - - - - - + + + )}
diff --git a/src/app/(campaign)/campanha/(app)/planos/page.tsx b/src/app/(campaign)/campanha/(app)/planos/page.tsx index 62fa7db1..ba9ae72b 100644 --- a/src/app/(campaign)/campanha/(app)/planos/page.tsx +++ b/src/app/(campaign)/campanha/(app)/planos/page.tsx @@ -10,17 +10,10 @@ import { getPayload } from 'payload' import { ActionPlanFilters } from '@/components/campaign/ActionPlanFilters' import { ActionPlanList } from '@/components/campaign/ActionPlanList' -import { CampaignListPagination } from '@/components/campaign/CampaignListPagination' +import { CampaignListEmptyState } from '@/components/campaign/CampaignListEmptyState' +import { CampaignListFooter } from '@/components/campaign/CampaignListFooter' import { CampaignPageShell } from '@/components/campaign/CampaignPageShell' import { Button } from '@/components/ui/button' -import { - Empty, - EmptyContent, - EmptyDescription, - EmptyHeader, - EmptyMedia, - EmptyTitle, -} from '@/components/ui/Empty' import type { ActionPlan } from '@/payload-types' import { loadActionPlanListPageData } from '@/utilities/actionPlanPageData' import { @@ -88,35 +81,25 @@ export default async function ActionPlanListPage({ searchParams }: ActionPlanLis toActionPlanListViewModel(plan as ActionPlan))} /> -
-

- {result.totalDocs}{' '} - {result.totalDocs === 1 ? 'plano encontrado' : 'planos encontrados'} -

- buildActionPlanListHref(state, page)} - /> -
+ buildActionPlanListHref(state, page)} + /> ) : ( - - - - - Nenhum plano encontrado - - Ajuste os filtros ou a janela selecionada. Você só vê planos dentro do seu escopo. - - - - - - + + + )} diff --git a/src/components/campaign/AdvisorsTable.tsx b/src/components/campaign/AdvisorsTable.tsx index dd58ea7c..5264cd02 100644 --- a/src/components/campaign/AdvisorsTable.tsx +++ b/src/components/campaign/AdvisorsTable.tsx @@ -17,15 +17,8 @@ import { toast } from 'sonner' import { AdvisorDebouncedTextCell } from '@/components/campaign/AdvisorDebouncedTextCell' import { AdvisorMunicipalityCell } from '@/components/campaign/AdvisorMunicipalityCell' import { AdvisorPasswordResetButton } from '@/components/campaign/AdvisorPasswordResetButton' +import { CampaignListEmptyState } from '@/components/campaign/CampaignListEmptyState' import { Button } from '@/components/ui/button' -import { - Empty, - EmptyContent, - EmptyDescription, - EmptyHeader, - EmptyMedia, - EmptyTitle, -} from '@/components/ui/Empty' import { Input } from '@/components/ui/input' import { Spinner } from '@/components/ui/Spinner' import { @@ -204,29 +197,22 @@ export const AdvisorsTable = ({ {showEmpty ? ( - - - - {hasQuery ? - - {hasQuery ? 'Nenhum assessor encontrado' : 'Nenhum assessor cadastrado'} - - - {hasQuery - ? 'Ajuste a busca ou limpe o filtro para ver todos.' - : 'Ative a edição e crie a conta na tabela.'} - - + {!hasQuery ? ( - - - + ) : null} - + ) : (
diff --git a/src/components/campaign/CampaignListEmptyState.tsx b/src/components/campaign/CampaignListEmptyState.tsx new file mode 100644 index 00000000..794ed783 --- /dev/null +++ b/src/components/campaign/CampaignListEmptyState.tsx @@ -0,0 +1,40 @@ +import type { LucideIcon } from 'lucide-react' +import type { ReactNode } from 'react' + +import { + Empty, + EmptyContent, + EmptyDescription, + EmptyHeader, + EmptyMedia, + EmptyTitle, +} from '@/components/ui/Empty' + +/** + * The standard campaign list empty state (icon, title, description, optional + * CTA). Rendered inside `CampaignListResults` so it dims with the shared + * transition like the rows it replaces. + */ +export const CampaignListEmptyState = ({ + icon: Icon, + title, + description, + children, +}: { + icon: LucideIcon + title: string + description: ReactNode + /** Call-to-action button(s). */ + children?: ReactNode +}) => ( + + + + + {title} + {description} + + {children ? {children} : null} + +) diff --git a/src/components/campaign/SupporterList.tsx b/src/components/campaign/SupporterList.tsx index fbea5d75..4e38c815 100644 --- a/src/components/campaign/SupporterList.tsx +++ b/src/components/campaign/SupporterList.tsx @@ -1,17 +1,14 @@ import { PhoneIcon } from 'lucide-react' import Link from 'next/link' +import { + CampaignTable, + CampaignTableHead, + type CampaignTableColumn, +} from '@/components/campaign/CampaignTable' import { Badge } from '@/components/ui/Badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/Table' import { formatBrazilianPhoneInput } from '@/utilities/phone' import { supporterVoteIntentionLabels } from '@/utilities/supporterUi' import type { SupporterListItemViewModel } from '@/utilities/supporterViewModels' @@ -85,6 +82,66 @@ const SupporterCard = ({ supporter }: { supporter: SupporterListItemViewModel }) ) } +const supporterColumns: Array> = [ + { + id: 'name', + mandatory: true, + head: Nome, + cellClassName: 'max-w-52 whitespace-normal', + cell: (supporter) => ( + + {supporter.name} + + ), + }, + { + id: 'city', + head: Município, + cell: (supporter) => supporter.city ?? '—', + }, + { + id: 'municipality', + head: Praça, + cellClassName: 'max-w-48 whitespace-normal', + cell: (supporter) => + supporter.municipalityName ? ( + + {supporter.municipalityName} + + ) : ( + Sem Praça vinculada + ), + }, + { + id: 'voteIntention', + head: Intenção, + cell: (supporter) => { + const voteIntentionLabel = supporter.voteIntention + ? supporterVoteIntentionLabels[supporter.voteIntention] + : null + return voteIntentionLabel ? ( + + {voteIntentionLabel} + + ) : ( + Sem intenção + ) + }, + }, + { + id: 'phone', + head: Telefone, + cellClassName: 'tabular-nums', + cell: (supporter) => (supporter.phone ? formatBrazilianPhoneInput(supporter.phone) : '—'), + }, +] + export const SupporterList = ({ supporters }: SupporterListProps) => ( <>
@@ -93,63 +150,11 @@ export const SupporterList = ({ supporters }: SupporterListProps) => ( ))}
-
-
- - - Nome - Município - Praça - Intenção - Telefone - - - - {supporters.map((supporter) => { - const voteIntentionLabel = supporter.voteIntention - ? supporterVoteIntentionLabels[supporter.voteIntention] - : null - - return ( - - - - {supporter.name} - - - {supporter.city ?? '—'} - - {supporter.municipalityName ? ( - - {supporter.municipalityName} - - ) : ( - Sem Praça vinculada - )} - - - {voteIntentionLabel ? ( - - {voteIntentionLabel} - - ) : ( - Sem intenção - )} - - - {supporter.phone ? formatBrazilianPhoneInput(supporter.phone) : '—'} - - - ) - })} - -
-
+ supporter.id} + /> ) diff --git a/src/utilities/campaignDemandData.ts b/src/utilities/campaignDemandData.ts index 32d93e68..5450d5ea 100644 --- a/src/utilities/campaignDemandData.ts +++ b/src/utilities/campaignDemandData.ts @@ -4,6 +4,12 @@ import type { CampaignDemandKind, CampaignDemandStatus } from '@/lib/schemas/cam import { campaignDemandKinds, campaignDemandStatuses } from '@/lib/schemas/campaignDemand' import type { ActionPlan, CampaignDemand, CampaignUser } from '@/payload-types' import { isCampaignStaff } from '@/utilities/campaignAccess' +import { + buildListHref, + firstValue, + strictDecimalInteger, + type RawSearchParams, +} from '@/utilities/campaignListUrl' import { isPopulatedRelationship, relationshipId } from '@/utilities/relationship' export const demandPageSize = 25 @@ -26,16 +32,12 @@ export type DemandListState = { kind?: CampaignDemandKind } -export const parseDemandListParams = ( - searchParams: Record, -): DemandListState => { - const first = (value: string | string[] | undefined) => (Array.isArray(value) ? value[0] : value) - const rawStatus = first(searchParams.status) - const rawKind = first(searchParams.kind) - const rawPage = first(searchParams.page) +export const parseDemandListParams = (searchParams: RawSearchParams): DemandListState => { + const rawStatus = firstValue(searchParams.status) + const rawKind = firstValue(searchParams.kind) return { - page: rawPage && /^[1-9]\d*$/.test(rawPage) ? Number(rawPage) : 1, + page: strictDecimalInteger(firstValue(searchParams.page)) ?? 1, ...(campaignDemandStatuses.includes(rawStatus as CampaignDemandStatus) ? { status: rawStatus as CampaignDemandStatus } : {}), @@ -45,6 +47,17 @@ export const parseDemandListParams = ( } } +const buildDemandListSearchParams = (state: DemandListState, page = state.page): URLSearchParams => { + const params = new URLSearchParams() + if (state.status) params.set('status', state.status) + if (state.kind) params.set('kind', state.kind) + if (page > 1) params.set('page', String(page)) + return params +} + +export const buildDemandListHref = (state: DemandListState, page: number): string => + buildListHref(state, buildDemandListSearchParams, '/campanha/demandas', page) + const resolveMunicipalityAndRequesterNames = async ( payload: Payload, demands: CampaignDemand[], diff --git a/src/utilities/leadershipData.ts b/src/utilities/leadershipData.ts index 28cb10fb..3543a1c6 100644 --- a/src/utilities/leadershipData.ts +++ b/src/utilities/leadershipData.ts @@ -4,6 +4,13 @@ import type { SupportStatus } from '@/lib/schemas/leadership' import { isSupportStatus } from '@/lib/schemas/leadership' import type { CampaignUser, Leadership } from '@/payload-types' import { isCampaignStaff } from '@/utilities/campaignAccess' +import { + buildListHref, + firstValue, + normalizedText, + strictDecimalInteger, + type RawSearchParams, +} from '@/utilities/campaignListUrl' import { relationshipId, requireRelationshipId } from '@/utilities/relationship' import { loadStateDeputySummaries, type StateDeputySummary } from '@/utilities/stateDeputyData' @@ -162,18 +169,28 @@ export type LeadershipListState = { } export const parseLeadershipListParams = ( - searchParams: Record, + searchParams: RawSearchParams, ): LeadershipListState => { - const first = (value: string | string[] | undefined) => (Array.isArray(value) ? value[0] : value) - const q = first(searchParams.q)?.trim() - const rawPage = first(searchParams.page) - + const q = normalizedText(firstValue(searchParams.q)) return { - page: rawPage && /^[1-9]\d*$/.test(rawPage) ? Number(rawPage) : 1, + page: strictDecimalInteger(firstValue(searchParams.page)) ?? 1, ...(q ? { q } : {}), } } +const buildLeadershipListSearchParams = ( + state: LeadershipListState, + page = state.page, +): URLSearchParams => { + const params = new URLSearchParams() + if (state.q) params.set('q', state.q) + if (page > 1) params.set('page', String(page)) + return params +} + +export const buildLeadershipListHref = (state: LeadershipListState, page: number): string => + buildListHref(state, buildLeadershipListSearchParams, '/campanha/liderancas', page) + export const loadLeadershipListPageData = async ( payload: Payload, user: CampaignUser, diff --git a/src/utilities/organizationData.ts b/src/utilities/organizationData.ts index c113ee12..d13c25fd 100644 --- a/src/utilities/organizationData.ts +++ b/src/utilities/organizationData.ts @@ -3,6 +3,13 @@ import type { Payload } from 'payload' import type { OrganizationKind } from '@/lib/schemas/organization' import { organizationKinds } from '@/lib/schemas/organization' import type { CampaignUser, Organization } from '@/payload-types' +import { + buildListHref, + firstValue, + normalizedText, + strictDecimalInteger, + type RawSearchParams, +} from '@/utilities/campaignListUrl' import { relationshipId } from '@/utilities/relationship' export const organizationPageSize = 25 @@ -23,15 +30,13 @@ export type OrganizationListState = { } export const parseOrganizationListParams = ( - searchParams: Record, + searchParams: RawSearchParams, ): OrganizationListState => { - const first = (value: string | string[] | undefined) => (Array.isArray(value) ? value[0] : value) - const q = first(searchParams.q)?.trim() - const rawKind = first(searchParams.kind) - const rawPage = first(searchParams.page) + const q = normalizedText(firstValue(searchParams.q)) + const rawKind = firstValue(searchParams.kind) return { - page: rawPage && /^[1-9]\d*$/.test(rawPage) ? Number(rawPage) : 1, + page: strictDecimalInteger(firstValue(searchParams.page)) ?? 1, ...(q ? { q } : {}), ...(organizationKinds.includes(rawKind as OrganizationKind) ? { kind: rawKind as OrganizationKind } @@ -39,6 +44,20 @@ export const parseOrganizationListParams = ( } } +const buildOrganizationListSearchParams = ( + state: OrganizationListState, + page = state.page, +): URLSearchParams => { + const params = new URLSearchParams() + if (state.q) params.set('q', state.q) + if (state.kind) params.set('kind', state.kind) + if (page > 1) params.set('page', String(page)) + return params +} + +export const buildOrganizationListHref = (state: OrganizationListState, page: number): string => + buildListHref(state, buildOrganizationListSearchParams, '/campanha/organizacoes', page) + const municipalityNamesByIds = async (payload: Payload, ids: number[]): Promise> => { if (ids.length === 0) return new Map() const result = await payload.find({ diff --git a/src/utilities/stateDeputyData.ts b/src/utilities/stateDeputyData.ts index b97bea10..274eba2f 100644 --- a/src/utilities/stateDeputyData.ts +++ b/src/utilities/stateDeputyData.ts @@ -1,6 +1,13 @@ import type { Payload } from 'payload' import type { CampaignUser, Leadership, StateDeputy } from '@/payload-types' +import { + buildListHref, + firstValue, + normalizedText, + strictDecimalInteger, + type RawSearchParams, +} from '@/utilities/campaignListUrl' import { relationshipId } from '@/utilities/relationship' export const stateDeputyPageSize = 25 @@ -20,18 +27,28 @@ export type StateDeputyListState = { } export const parseStateDeputyListParams = ( - searchParams: Record, + searchParams: RawSearchParams, ): StateDeputyListState => { - const first = (value: string | string[] | undefined) => (Array.isArray(value) ? value[0] : value) - const q = first(searchParams.q)?.trim() - const rawPage = first(searchParams.page) - + const q = normalizedText(firstValue(searchParams.q)) return { - page: rawPage && /^[1-9]\d*$/.test(rawPage) ? Number(rawPage) : 1, + page: strictDecimalInteger(firstValue(searchParams.page)) ?? 1, ...(q ? { q } : {}), } } +const buildStateDeputyListSearchParams = ( + state: StateDeputyListState, + page = state.page, +): URLSearchParams => { + const params = new URLSearchParams() + if (state.q) params.set('q', state.q) + if (page > 1) params.set('page', String(page)) + return params +} + +export const buildStateDeputyListHref = (state: StateDeputyListState, page: number): string => + buildListHref(state, buildStateDeputyListSearchParams, '/campanha/dobradinhas', page) + export const loadStateDeputyListPageData = async ( payload: Payload, user: CampaignUser,