mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
Pass 2 W1-D2: migra tripé de entidades, demandas, apoiadores, planos e assessores para o sistema de listas
- liderancas/organizacoes/dobradinhas/demandas: CampaignTable + column defs + CampaignListFooter + CampaignListEmptyState; parsers locais de first/page substituidos pelos helpers de campaignListUrl (fecha C8 F3); buildXListHref por modulo (URLs byte-identicas) - SupporterList: tabela desktop em column defs (cards mobile intactos); apoiadores adota footer + empty state compartilhados - planos: cards continuam (dominio de cards); adota footer + empty state - assessores: tabela client bespoke (edit-where-you-see); pagina adota CampaignListFooter; empty state compartilhado no AdvisorsTable - fora por decisao D5: TerritoryOverviewTable (client-sort, dataset <=27), comparacao de candidatos, LeaderContacts, preview de import
This commit is contained in:
parent
03bf54dea2
commit
23e1f1e83d
14 changed files with 574 additions and 593 deletions
|
|
@ -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
|
|||
<SupporterList
|
||||
supporters={result.docs.map((supporter) => toSupporterListItemViewModel(supporter))}
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{result.totalDocs}{' '}
|
||||
{result.totalDocs === 1 ? 'apoiador encontrado' : 'apoiadores encontrados'}
|
||||
</p>
|
||||
<CampaignListPagination
|
||||
page={state.page}
|
||||
totalPages={result.totalPages}
|
||||
hrefForPage={(page) => buildSupporterListHref(state, page)}
|
||||
/>
|
||||
</div>
|
||||
<CampaignListFooter
|
||||
totalDocs={result.totalDocs}
|
||||
singular="apoiador encontrado"
|
||||
plural="apoiadores encontrados"
|
||||
page={state.page}
|
||||
totalPages={result.totalPages}
|
||||
hrefForPage={(page) => buildSupporterListHref(state, page)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Empty className="min-h-72 border">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<SearchXIcon aria-hidden="true" />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Nenhum apoiador encontrado</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Ajuste a busca ou os filtros. Você só vê apoiadores dentro do seu escopo.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button asChild variant="outline" className="min-h-11">
|
||||
<Link href="/campanha/apoiadores">Limpar busca e filtros</Link>
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<CampaignListEmptyState
|
||||
icon={SearchXIcon}
|
||||
title="Nenhum apoiador encontrado"
|
||||
description="Ajuste a busca ou os filtros. Você só vê apoiadores dentro do seu escopo."
|
||||
>
|
||||
<Button asChild variant="outline" className="min-h-11">
|
||||
<Link href="/campanha/apoiadores">Limpar busca e filtros</Link>
|
||||
</Button>
|
||||
</CampaignListEmptyState>
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{totalDocs} {totalDocs === 1 ? 'assessor' : 'assessores'}
|
||||
</p>
|
||||
<CampaignListPagination
|
||||
page={state.page}
|
||||
totalPages={totalPages}
|
||||
hrefForPage={(page) => advisorListHrefForPage(state, page)}
|
||||
/>
|
||||
</div>
|
||||
<CampaignListFooter
|
||||
totalDocs={totalDocs}
|
||||
singular="assessor"
|
||||
plural="assessores"
|
||||
page={state.page}
|
||||
totalPages={totalPages}
|
||||
hrefForPage={(page) => advisorListHrefForPage(state, page)}
|
||||
/>
|
||||
) : null}
|
||||
</CampaignListResults>
|
||||
</CampaignListPendingBoundary>
|
||||
|
|
|
|||
|
|
@ -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<Record<string, string | string[] | undefined>>
|
||||
|
|
@ -56,6 +51,52 @@ const statusVariant: Record<
|
|||
|
||||
const dateFormatter = new Intl.DateTimeFormat('pt-BR', { dateStyle: 'short' })
|
||||
|
||||
const demandColumns: Array<CampaignTableColumn<DemandRowViewModel>> = [
|
||||
{
|
||||
id: 'title',
|
||||
mandatory: true,
|
||||
head: <CampaignTableHead>Demanda</CampaignTableHead>,
|
||||
cellClassName: 'max-w-64 whitespace-normal',
|
||||
cell: (row) => (
|
||||
<Link
|
||||
href={`/campanha/demandas/${row.slug}`}
|
||||
className="inline-flex min-h-11 items-center font-medium text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
{row.title}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'kind',
|
||||
head: <CampaignTableHead>Tipo</CampaignTableHead>,
|
||||
cell: (row) => campaignDemandKindLabels[row.kind],
|
||||
},
|
||||
{
|
||||
id: 'municipality',
|
||||
head: <CampaignTableHead>Praça</CampaignTableHead>,
|
||||
cellClassName: 'text-muted-foreground',
|
||||
cell: (row) => row.municipalityName,
|
||||
},
|
||||
{
|
||||
id: 'requester',
|
||||
head: <CampaignTableHead>Solicitante</CampaignTableHead>,
|
||||
cellClassName: 'text-muted-foreground',
|
||||
cell: (row) => row.requesterName ?? '—',
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
head: <CampaignTableHead>Status</CampaignTableHead>,
|
||||
cell: (row) => (
|
||||
<Badge variant={statusVariant[row.status]}>{campaignDemandStatusLabels[row.status]}</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'createdAt',
|
||||
head: <CampaignTableHead>Aberta em</CampaignTableHead>,
|
||||
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 ? (
|
||||
<>
|
||||
<div className="overflow-hidden rounded-xl border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Demanda</TableHead>
|
||||
<TableHead>Tipo</TableHead>
|
||||
<TableHead>Praça</TableHead>
|
||||
<TableHead>Solicitante</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Aberta em</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
<TableCell className="max-w-64 whitespace-normal">
|
||||
<Link
|
||||
href={`/campanha/demandas/${row.slug}`}
|
||||
className="inline-flex min-h-11 items-center font-medium text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
{row.title}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>{campaignDemandKindLabels[row.kind]}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{row.municipalityName}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{row.requesterName ?? '—'}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={statusVariant[row.status]}>
|
||||
{campaignDemandStatusLabels[row.status]}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{dateFormatter.format(new Date(row.createdAt))}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{totalDocs} {totalDocs === 1 ? 'demanda' : 'demandas'}
|
||||
</p>
|
||||
<CampaignListPagination
|
||||
page={state.page}
|
||||
totalPages={totalPages}
|
||||
hrefForPage={(page) => hrefFor(state.status, page)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Empty className="min-h-72 border">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<InboxIcon aria-hidden="true" />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Nenhuma demanda por aqui</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Abra uma demanda quando precisar de material, transporte, espaço ou apoio para uma ação.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button asChild className="min-h-11">
|
||||
<Link href="/campanha/demandas/nova">
|
||||
<PlusIcon data-icon="inline-start" aria-hidden="true" />
|
||||
Nova demanda
|
||||
</Link>
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
)
|
||||
const hrefForStatus = (status?: CampaignDemandStatus) =>
|
||||
buildDemandListHref({ ...state, status }, 1)
|
||||
|
||||
return (
|
||||
<CampaignPageShell>
|
||||
|
|
@ -175,16 +138,43 @@ export default async function DemandsPage({ searchParams }: DemandsPageProps) {
|
|||
<CampaignFilterChips
|
||||
ariaLabel="Filtrar por status"
|
||||
chips={[
|
||||
{ href: hrefFor(undefined), label: 'Todas', active: state.status === undefined },
|
||||
{ href: hrefForStatus(undefined), label: 'Todas', active: state.status === undefined },
|
||||
...campaignDemandStatuses.map((status) => ({
|
||||
href: hrefFor(status),
|
||||
href: hrefForStatus(status),
|
||||
label: campaignDemandStatusLabels[status],
|
||||
active: state.status === status,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
|
||||
<CampaignListResults>{listBody}</CampaignListResults>
|
||||
<CampaignListResults>
|
||||
{rows.length ? (
|
||||
<>
|
||||
<CampaignTable columns={demandColumns} rows={rows} rowKey={(row) => row.id} />
|
||||
<CampaignListFooter
|
||||
totalDocs={totalDocs}
|
||||
singular="demanda"
|
||||
plural="demandas"
|
||||
page={state.page}
|
||||
totalPages={totalPages}
|
||||
hrefForPage={(page) => buildDemandListHref(state, page)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<CampaignListEmptyState
|
||||
icon={InboxIcon}
|
||||
title="Nenhuma demanda por aqui"
|
||||
description="Abra uma demanda quando precisar de material, transporte, espaço ou apoio para uma ação."
|
||||
>
|
||||
<Button asChild className="min-h-11">
|
||||
<Link href="/campanha/demandas/nova">
|
||||
<PlusIcon data-icon="inline-start" aria-hidden="true" />
|
||||
Nova demanda
|
||||
</Link>
|
||||
</Button>
|
||||
</CampaignListEmptyState>
|
||||
)}
|
||||
</CampaignListResults>
|
||||
</CampaignListPendingBoundary>
|
||||
</CampaignPageShell>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<Record<string, string | string[] | undefined>>
|
||||
}
|
||||
|
||||
const stateDeputyColumns: Array<CampaignTableColumn<StateDeputyRowViewModel>> = [
|
||||
{
|
||||
id: 'name',
|
||||
mandatory: true,
|
||||
head: <CampaignTableHead>Nome</CampaignTableHead>,
|
||||
cell: (row) => (
|
||||
<Link
|
||||
href={`/campanha/dobradinhas/${row.slug}`}
|
||||
className="inline-flex min-h-11 items-center font-medium text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
{row.name}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'party',
|
||||
head: <CampaignTableHead>Partido</CampaignTableHead>,
|
||||
cellClassName: 'text-muted-foreground',
|
||||
cell: (row) => row.party ?? '—',
|
||||
},
|
||||
{
|
||||
id: 'municipalities',
|
||||
head: <CampaignTableHead>Municípios</CampaignTableHead>,
|
||||
cellClassName: 'tabular-nums',
|
||||
cell: (row) => row.municipalityCount,
|
||||
},
|
||||
{
|
||||
id: 'leaderships',
|
||||
head: <CampaignTableHead>Lideranças</CampaignTableHead>,
|
||||
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 (
|
||||
<CampaignPageShell>
|
||||
<header className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
|
|
@ -85,66 +103,29 @@ export default async function StateDeputiesPage({ searchParams }: StateDeputiesP
|
|||
<CampaignListResults>
|
||||
{rows.length ? (
|
||||
<>
|
||||
<div className="overflow-hidden rounded-xl border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Nome</TableHead>
|
||||
<TableHead>Partido</TableHead>
|
||||
<TableHead>Municípios</TableHead>
|
||||
<TableHead>Lideranças</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
<TableCell>
|
||||
<Link
|
||||
href={`/campanha/dobradinhas/${row.slug}`}
|
||||
className="inline-flex min-h-11 items-center font-medium text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
{row.name}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">{row.party ?? '—'}</TableCell>
|
||||
<TableCell className="tabular-nums">{row.municipalityCount}</TableCell>
|
||||
<TableCell className="tabular-nums">{row.leadershipCount}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{totalDocs} {totalDocs === 1 ? 'dobradinha' : 'dobradinhas'}
|
||||
</p>
|
||||
<CampaignListPagination
|
||||
page={state.page}
|
||||
totalPages={totalPages}
|
||||
hrefForPage={hrefForPage}
|
||||
/>
|
||||
</div>
|
||||
<CampaignTable columns={stateDeputyColumns} rows={rows} rowKey={(row) => row.id} />
|
||||
<CampaignListFooter
|
||||
totalDocs={totalDocs}
|
||||
singular="dobradinha"
|
||||
plural="dobradinhas"
|
||||
page={state.page}
|
||||
totalPages={totalPages}
|
||||
hrefForPage={(page) => buildStateDeputyListHref(state, page)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Empty className="min-h-72 border">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<SearchXIcon aria-hidden="true" />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Nenhuma dobradinha cadastrada</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Cadastre deputados estaduais parceiros para vincular a municípios e lideranças.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button asChild className="min-h-11">
|
||||
<Link href="/campanha/dobradinhas/nova">
|
||||
<PlusIcon data-icon="inline-start" aria-hidden="true" />
|
||||
Nova dobradinha
|
||||
</Link>
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<CampaignListEmptyState
|
||||
icon={SearchXIcon}
|
||||
title="Nenhuma dobradinha cadastrada"
|
||||
description="Cadastre deputados estaduais parceiros para vincular a municípios e lideranças."
|
||||
>
|
||||
<Button asChild className="min-h-11">
|
||||
<Link href="/campanha/dobradinhas/nova">
|
||||
<PlusIcon data-icon="inline-start" aria-hidden="true" />
|
||||
Nova dobradinha
|
||||
</Link>
|
||||
</Button>
|
||||
</CampaignListEmptyState>
|
||||
)}
|
||||
</CampaignListResults>
|
||||
</CampaignListPendingBoundary>
|
||||
|
|
|
|||
|
|
@ -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<Record<string, string | string[] | undefined>>
|
||||
}
|
||||
|
||||
const leadershipColumns: Array<CampaignTableColumn<LeadershipRowViewModel>> = [
|
||||
{
|
||||
id: 'name',
|
||||
mandatory: true,
|
||||
head: <CampaignTableHead>Nome</CampaignTableHead>,
|
||||
cell: (row) => (
|
||||
<Link
|
||||
href={`/campanha/liderancas/${row.id}`}
|
||||
className="inline-flex min-h-11 items-center font-medium text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
{row.name}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'supportStatus',
|
||||
head: <CampaignTableHead>Status</CampaignTableHead>,
|
||||
cell: (row) => (row.supportStatus ? <SupportStatusBadge status={row.supportStatus} /> : '—'),
|
||||
},
|
||||
{
|
||||
id: 'municipalities',
|
||||
head: <CampaignTableHead>Praças</CampaignTableHead>,
|
||||
cellClassName: 'max-w-64 whitespace-normal text-muted-foreground',
|
||||
cell: (row) => row.municipalityNames.join(', ') || '—',
|
||||
},
|
||||
{
|
||||
id: 'organizations',
|
||||
head: <CampaignTableHead>Organizações</CampaignTableHead>,
|
||||
cellClassName: 'max-w-56 whitespace-normal text-muted-foreground',
|
||||
cell: (row) => row.organizationNames.join(', ') || '—',
|
||||
},
|
||||
{
|
||||
id: 'appAccess',
|
||||
head: <CampaignTableHead>Acesso ao app</CampaignTableHead>,
|
||||
cell: (row) => (
|
||||
<Badge variant={row.hasAppAccess ? 'estimate-confirmed' : 'outline'}>
|
||||
{row.hasAppAccess ? 'Com acesso' : 'Sem acesso'}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
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 (
|
||||
<CampaignPageShell>
|
||||
<header className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
|
|
@ -83,83 +112,29 @@ export default async function LeadershipsPage({ searchParams }: LeadershipsPageP
|
|||
<CampaignListResults>
|
||||
{rows.length ? (
|
||||
<>
|
||||
<div className="overflow-hidden rounded-xl border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Nome</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Praças</TableHead>
|
||||
<TableHead>Organizações</TableHead>
|
||||
<TableHead>Acesso ao app</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
<TableCell>
|
||||
<Link
|
||||
href={`/campanha/liderancas/${row.id}`}
|
||||
className="inline-flex min-h-11 items-center font-medium text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
{row.name}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{row.supportStatus ? (
|
||||
<SupportStatusBadge status={row.supportStatus} />
|
||||
) : (
|
||||
'—'
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="max-w-64 whitespace-normal text-muted-foreground">
|
||||
{row.municipalityNames.join(', ') || '—'}
|
||||
</TableCell>
|
||||
<TableCell className="max-w-56 whitespace-normal text-muted-foreground">
|
||||
{row.organizationNames.join(', ') || '—'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={row.hasAppAccess ? 'estimate-confirmed' : 'outline'}>
|
||||
{row.hasAppAccess ? 'Com acesso' : 'Sem acesso'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{totalDocs} {totalDocs === 1 ? 'liderança' : 'lideranças'}
|
||||
</p>
|
||||
<CampaignListPagination
|
||||
page={state.page}
|
||||
totalPages={totalPages}
|
||||
hrefForPage={hrefForPage}
|
||||
/>
|
||||
</div>
|
||||
<CampaignTable columns={leadershipColumns} rows={rows} rowKey={(row) => row.id} />
|
||||
<CampaignListFooter
|
||||
totalDocs={totalDocs}
|
||||
singular="liderança"
|
||||
plural="lideranças"
|
||||
page={state.page}
|
||||
totalPages={totalPages}
|
||||
hrefForPage={(page) => buildLeadershipListHref(state, page)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Empty className="min-h-72 border">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<SearchXIcon aria-hidden="true" />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Nenhuma liderança encontrada</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Cadastre a primeira liderança ou ajuste a busca. Você só vê lideranças das suas
|
||||
Praças.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button asChild className="min-h-11">
|
||||
<Link href="/campanha/liderancas/nova">
|
||||
<PlusIcon data-icon="inline-start" aria-hidden="true" />
|
||||
Nova liderança
|
||||
</Link>
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<CampaignListEmptyState
|
||||
icon={SearchXIcon}
|
||||
title="Nenhuma liderança encontrada"
|
||||
description="Cadastre a primeira liderança ou ajuste a busca. Você só vê lideranças das suas Praças."
|
||||
>
|
||||
<Button asChild className="min-h-11">
|
||||
<Link href="/campanha/liderancas/nova">
|
||||
<PlusIcon data-icon="inline-start" aria-hidden="true" />
|
||||
Nova liderança
|
||||
</Link>
|
||||
</Button>
|
||||
</CampaignListEmptyState>
|
||||
)}
|
||||
</CampaignListResults>
|
||||
</CampaignListPendingBoundary>
|
||||
|
|
|
|||
|
|
@ -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<Record<string, string | string[] | undefined>>
|
||||
}
|
||||
|
||||
const organizationColumns: Array<CampaignTableColumn<OrganizationRowViewModel>> = [
|
||||
{
|
||||
id: 'name',
|
||||
mandatory: true,
|
||||
head: <CampaignTableHead>Nome</CampaignTableHead>,
|
||||
cell: (row) => (
|
||||
<Link
|
||||
href={`/campanha/organizacoes/${row.slug}`}
|
||||
className="inline-flex min-h-11 items-center font-medium text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
{row.name}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'kind',
|
||||
head: <CampaignTableHead>Tipo</CampaignTableHead>,
|
||||
cell: (row) => <Badge variant="secondary">{organizationKindLabels[row.kind]}</Badge>,
|
||||
},
|
||||
{
|
||||
id: 'municipalities',
|
||||
head: <CampaignTableHead>Praças de atuação</CampaignTableHead>,
|
||||
cellClassName: 'max-w-64 whitespace-normal text-muted-foreground',
|
||||
cell: (row) => row.municipalityNames.join(', ') || '—',
|
||||
},
|
||||
{
|
||||
id: 'leaderships',
|
||||
head: <CampaignTableHead>Lideranças</CampaignTableHead>,
|
||||
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 (
|
||||
<CampaignPageShell>
|
||||
<header className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
|
|
@ -88,71 +104,29 @@ export default async function OrganizationsPage({ searchParams }: OrganizationsP
|
|||
<CampaignListResults>
|
||||
{rows.length ? (
|
||||
<>
|
||||
<div className="overflow-hidden rounded-xl border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Nome</TableHead>
|
||||
<TableHead>Tipo</TableHead>
|
||||
<TableHead>Praças de atuação</TableHead>
|
||||
<TableHead>Lideranças</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
<TableCell>
|
||||
<Link
|
||||
href={`/campanha/organizacoes/${row.slug}`}
|
||||
className="inline-flex min-h-11 items-center font-medium text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
{row.name}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary">{organizationKindLabels[row.kind]}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="max-w-64 whitespace-normal text-muted-foreground">
|
||||
{row.municipalityNames.join(', ') || '—'}
|
||||
</TableCell>
|
||||
<TableCell className="tabular-nums">{row.leadershipCount}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{totalDocs} {totalDocs === 1 ? 'organização' : 'organizações'}
|
||||
</p>
|
||||
<CampaignListPagination
|
||||
page={state.page}
|
||||
totalPages={totalPages}
|
||||
hrefForPage={hrefForPage}
|
||||
/>
|
||||
</div>
|
||||
<CampaignTable columns={organizationColumns} rows={rows} rowKey={(row) => row.id} />
|
||||
<CampaignListFooter
|
||||
totalDocs={totalDocs}
|
||||
singular="organização"
|
||||
plural="organizações"
|
||||
page={state.page}
|
||||
totalPages={totalPages}
|
||||
hrefForPage={(page) => buildOrganizationListHref(state, page)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Empty className="min-h-72 border">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<SearchXIcon aria-hidden="true" />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Nenhuma organização cadastrada</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Cadastre sindicatos, associações e movimentos para vincular lideranças e Planos de
|
||||
Ação.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button asChild className="min-h-11">
|
||||
<Link href="/campanha/organizacoes/nova">
|
||||
<PlusIcon data-icon="inline-start" aria-hidden="true" />
|
||||
Nova organização
|
||||
</Link>
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<CampaignListEmptyState
|
||||
icon={SearchXIcon}
|
||||
title="Nenhuma organização cadastrada"
|
||||
description="Cadastre sindicatos, associações e movimentos para vincular lideranças e Planos de Ação."
|
||||
>
|
||||
<Button asChild className="min-h-11">
|
||||
<Link href="/campanha/organizacoes/nova">
|
||||
<PlusIcon data-icon="inline-start" aria-hidden="true" />
|
||||
Nova organização
|
||||
</Link>
|
||||
</Button>
|
||||
</CampaignListEmptyState>
|
||||
)}
|
||||
</CampaignListResults>
|
||||
</CampaignListPendingBoundary>
|
||||
|
|
|
|||
|
|
@ -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
|
|||
<ActionPlanList
|
||||
plans={result.docs.map((plan) => toActionPlanListViewModel(plan as ActionPlan))}
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{result.totalDocs}{' '}
|
||||
{result.totalDocs === 1 ? 'plano encontrado' : 'planos encontrados'}
|
||||
</p>
|
||||
<CampaignListPagination
|
||||
page={state.page}
|
||||
totalPages={result.totalPages}
|
||||
hrefForPage={(page) => buildActionPlanListHref(state, page)}
|
||||
/>
|
||||
</div>
|
||||
<CampaignListFooter
|
||||
totalDocs={result.totalDocs}
|
||||
singular="plano encontrado"
|
||||
plural="planos encontrados"
|
||||
page={state.page}
|
||||
totalPages={result.totalPages}
|
||||
hrefForPage={(page) => buildActionPlanListHref(state, page)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Empty className="min-h-72 border">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<SearchXIcon aria-hidden="true" />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Nenhum plano encontrado</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Ajuste os filtros ou a janela selecionada. Você só vê planos dentro do seu escopo.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button asChild variant="outline" className="min-h-11">
|
||||
<Link href="/campanha/planos">Limpar filtros</Link>
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<CampaignListEmptyState
|
||||
icon={SearchXIcon}
|
||||
title="Nenhum plano encontrado"
|
||||
description="Ajuste os filtros ou a janela selecionada. Você só vê planos dentro do seu escopo."
|
||||
>
|
||||
<Button asChild variant="outline" className="min-h-11">
|
||||
<Link href="/campanha/planos">Limpar filtros</Link>
|
||||
</Button>
|
||||
</CampaignListEmptyState>
|
||||
)}
|
||||
</CampaignListResults>
|
||||
</CampaignListPendingBoundary>
|
||||
|
|
|
|||
|
|
@ -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 = ({
|
|||
</div>
|
||||
|
||||
{showEmpty ? (
|
||||
<Empty className="min-h-72 border">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
{hasQuery ? <SearchXIcon aria-hidden="true" /> : <UserCogIcon aria-hidden="true" />}
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>
|
||||
{hasQuery ? 'Nenhum assessor encontrado' : 'Nenhum assessor cadastrado'}
|
||||
</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
{hasQuery
|
||||
? 'Ajuste a busca ou limpe o filtro para ver todos.'
|
||||
: 'Ative a edição e crie a conta na tabela.'}
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<CampaignListEmptyState
|
||||
icon={hasQuery ? SearchXIcon : UserCogIcon}
|
||||
title={hasQuery ? 'Nenhum assessor encontrado' : 'Nenhum assessor cadastrado'}
|
||||
description={
|
||||
hasQuery
|
||||
? 'Ajuste a busca ou limpe o filtro para ver todos.'
|
||||
: 'Ative a edição e crie a conta na tabela.'
|
||||
}
|
||||
>
|
||||
{!hasQuery ? (
|
||||
<EmptyContent>
|
||||
<Button type="button" className="min-h-11" onClick={startDraft}>
|
||||
<PlusIcon data-icon="inline-start" aria-hidden="true" />
|
||||
Novo assessor
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
<Button type="button" className="min-h-11" onClick={startDraft}>
|
||||
<PlusIcon data-icon="inline-start" aria-hidden="true" />
|
||||
Novo assessor
|
||||
</Button>
|
||||
) : null}
|
||||
</Empty>
|
||||
</CampaignListEmptyState>
|
||||
) : (
|
||||
<div className="overflow-x-auto rounded-xl border">
|
||||
<Table className="min-w-[56rem] table-fixed">
|
||||
|
|
|
|||
40
src/components/campaign/CampaignListEmptyState.tsx
Normal file
40
src/components/campaign/CampaignListEmptyState.tsx
Normal file
|
|
@ -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
|
||||
}) => (
|
||||
<Empty className="min-h-72 border">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<Icon aria-hidden="true" />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>{title}</EmptyTitle>
|
||||
<EmptyDescription>{description}</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
{children ? <EmptyContent>{children}</EmptyContent> : null}
|
||||
</Empty>
|
||||
)
|
||||
|
|
@ -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<CampaignTableColumn<SupporterListItemViewModel>> = [
|
||||
{
|
||||
id: 'name',
|
||||
mandatory: true,
|
||||
head: <CampaignTableHead>Nome</CampaignTableHead>,
|
||||
cellClassName: 'max-w-52 whitespace-normal',
|
||||
cell: (supporter) => (
|
||||
<Link
|
||||
href={`/campanha/apoiadores/${supporter.id}`}
|
||||
className="inline-flex min-h-11 items-center font-medium text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
{supporter.name}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'city',
|
||||
head: <CampaignTableHead>Município</CampaignTableHead>,
|
||||
cell: (supporter) => supporter.city ?? '—',
|
||||
},
|
||||
{
|
||||
id: 'municipality',
|
||||
head: <CampaignTableHead>Praça</CampaignTableHead>,
|
||||
cellClassName: 'max-w-48 whitespace-normal',
|
||||
cell: (supporter) =>
|
||||
supporter.municipalityName ? (
|
||||
<Link
|
||||
href={`/campanha/municipios/${supporter.municipalitySlug}`}
|
||||
className="text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
{supporter.municipalityName}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-muted-foreground">Sem Praça vinculada</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'voteIntention',
|
||||
head: <CampaignTableHead>Intenção</CampaignTableHead>,
|
||||
cell: (supporter) => {
|
||||
const voteIntentionLabel = supporter.voteIntention
|
||||
? supporterVoteIntentionLabels[supporter.voteIntention]
|
||||
: null
|
||||
return voteIntentionLabel ? (
|
||||
<Badge variant={voteIntentionBadgeVariant(supporter.voteIntention)}>
|
||||
{voteIntentionLabel}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline">Sem intenção</Badge>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'phone',
|
||||
head: <CampaignTableHead>Telefone</CampaignTableHead>,
|
||||
cellClassName: 'tabular-nums',
|
||||
cell: (supporter) => (supporter.phone ? formatBrazilianPhoneInput(supporter.phone) : '—'),
|
||||
},
|
||||
]
|
||||
|
||||
export const SupporterList = ({ supporters }: SupporterListProps) => (
|
||||
<>
|
||||
<div data-view="mobile-cards" className="flex flex-col gap-4 md:hidden">
|
||||
|
|
@ -93,63 +150,11 @@ export const SupporterList = ({ supporters }: SupporterListProps) => (
|
|||
))}
|
||||
</div>
|
||||
|
||||
<div data-view="desktop-table" className="hidden overflow-hidden rounded-xl border md:block">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Nome</TableHead>
|
||||
<TableHead>Município</TableHead>
|
||||
<TableHead>Praça</TableHead>
|
||||
<TableHead>Intenção</TableHead>
|
||||
<TableHead>Telefone</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{supporters.map((supporter) => {
|
||||
const voteIntentionLabel = supporter.voteIntention
|
||||
? supporterVoteIntentionLabels[supporter.voteIntention]
|
||||
: null
|
||||
|
||||
return (
|
||||
<TableRow key={supporter.id}>
|
||||
<TableCell className="max-w-52 whitespace-normal">
|
||||
<Link
|
||||
href={`/campanha/apoiadores/${supporter.id}`}
|
||||
className="inline-flex min-h-11 items-center font-medium text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
{supporter.name}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>{supporter.city ?? '—'}</TableCell>
|
||||
<TableCell className="max-w-48 whitespace-normal">
|
||||
{supporter.municipalityName ? (
|
||||
<Link
|
||||
href={`/campanha/municipios/${supporter.municipalitySlug}`}
|
||||
className="text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
{supporter.municipalityName}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-muted-foreground">Sem Praça vinculada</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{voteIntentionLabel ? (
|
||||
<Badge variant={voteIntentionBadgeVariant(supporter.voteIntention)}>
|
||||
{voteIntentionLabel}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline">Sem intenção</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="tabular-nums">
|
||||
{supporter.phone ? formatBrazilianPhoneInput(supporter.phone) : '—'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<CampaignTable
|
||||
className="hidden md:block"
|
||||
columns={supporterColumns}
|
||||
rows={supporters}
|
||||
rowKey={(supporter) => supporter.id}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<string, string | string[] | undefined>,
|
||||
): 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[],
|
||||
|
|
|
|||
|
|
@ -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<string, string | string[] | undefined>,
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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<string, string | string[] | undefined>,
|
||||
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<Map<number, string>> => {
|
||||
if (ids.length === 0) return new Map()
|
||||
const result = await payload.find({
|
||||
|
|
|
|||
|
|
@ -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<string, string | string[] | undefined>,
|
||||
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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue