teqo-backup/src/components/campaign/LeadershipForm.tsx
Francisco Ignácio Fontoura Solla e2724c10da Remodel the campaign vertical from Praça to Município
Rename the operational unit "Praça"/"Núcleo" to "Município" (code identifier
`municipality`) and land the 2026-07-23 course changes:

- New `candidate` role with unrestricted visibility (`isCampaignStaff` now
  includes it) alongside coordinator/advisor; leader is locked down to the
  supporter contact tool only.
- Demandas become staff-only; dobradinhas modeled via a `stateDeputy` entity.
- Camaçari consolidated to a single município (ZE 170/171 aggregated);
  Salvador keeps its 19 zone municípios.
- Map moves to the staff dashboard; `/campanha/municipios` is list/overview.
- Vote pledges keep the declared vs estimated (scenario) asymmetry; leaders
  never see estimates.

Destructive migration for the campaign vertical (drops nucleus/plaza-era
tables, wipes campaign rows, seeds 435 municípios). Public-site data untouched.

Includes the post-delivery /simplify cleanups: shared reloadStaffActor /
reloadCoordinatorActor helpers, req.context cache for getFreshCampaignUser,
parallelized dashboard loads, candidate dashboard-copy fix, and dead
leader-dashboard / roleKind code removal.
2026-07-23 21:36:20 -03:00

176 lines
5.9 KiB
TypeScript

'use client'
import { useActionState } from 'react'
import { RelationMultiSelect, type RelationOption } from '@/components/campaign/RelationMultiSelect'
import { Alert, AlertDescription } from '@/components/ui/Alert'
import { Button } from '@/components/ui/button'
import { Field, FieldError, FieldLabel } from '@/components/ui/field'
import { Input } from '@/components/ui/input'
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
import { Spinner } from '@/components/ui/Spinner'
import { Textarea } from '@/components/ui/textarea'
import { leadershipSectors, leadershipSupportStatuses } from '@/lib/schemas/leadership'
import type { CampaignFormActionState } from '@/utilities/campaignFormActionError'
import { fieldError } from '@/utilities/campaignFormFields'
const sectorLabels: Record<(typeof leadershipSectors)[number], string> = {
religioso: 'Religioso',
sindical: 'Sindical',
comunitario: 'Comunitário',
rural: 'Rural',
empresarial: 'Empresarial',
juventude: 'Juventude',
saude: 'Saúde',
educacao: 'Educação',
cultura: 'Cultura',
outro: 'Outro',
}
const supportStatusLabels: Record<(typeof leadershipSupportStatuses)[number], string> = {
engajado: 'Engajado',
a_abordar: 'A abordar',
em_disputa: 'Em disputa',
negativo: 'Negativo',
}
type LeadershipFormProps = {
municipalityOptions: RelationOption[]
organizationOptions: RelationOption[]
stateDeputyOptions: RelationOption[]
initialMunicipalityIDs: number[]
formAction: (
state: CampaignFormActionState,
formData: FormData,
) => Promise<CampaignFormActionState>
}
export const LeadershipForm = ({
municipalityOptions,
organizationOptions,
stateDeputyOptions,
initialMunicipalityIDs,
formAction,
}: LeadershipFormProps) => {
const [state, submitAction, isPending] = useActionState(formAction, {})
return (
<form action={submitAction} className="flex max-w-2xl flex-col gap-4">
<Field>
<FieldLabel htmlFor="leadership-name">Nome completo</FieldLabel>
<Input
id="leadership-name"
name="name"
required
minLength={2}
maxLength={120}
className="min-h-11"
/>
{fieldError(state.fieldErrors, 'name') ? (
<FieldError>{fieldError(state.fieldErrors, 'name')}</FieldError>
) : null}
</Field>
<div className="grid gap-4 sm:grid-cols-2">
<Field>
<FieldLabel htmlFor="leadership-phone">Celular (com DDD)</FieldLabel>
<Input
id="leadership-phone"
name="phone"
type="tel"
required
autoComplete="tel-national"
className="min-h-11"
/>
{fieldError(state.fieldErrors, 'phone') ? (
<FieldError>{fieldError(state.fieldErrors, 'phone')}</FieldError>
) : null}
</Field>
<Field>
<FieldLabel htmlFor="leadership-email">E-mail (opcional)</FieldLabel>
<Input id="leadership-email" name="email" type="email" className="min-h-11" />
{fieldError(state.fieldErrors, 'email') ? (
<FieldError>{fieldError(state.fieldErrors, 'email')}</FieldError>
) : null}
</Field>
</div>
<RelationMultiSelect
name="municipalities"
label="Praças em que atua"
options={municipalityOptions}
initialSelectedIDs={initialMunicipalityIDs}
error={fieldError(state.fieldErrors, 'municipalities')}
placeholder="Adicionar Praça…"
/>
<RelationMultiSelect
name="organizations"
label="Organizações (sindicatos, associações…)"
options={organizationOptions}
error={fieldError(state.fieldErrors, 'organizations')}
placeholder="Adicionar organização…"
/>
<RelationMultiSelect
name="stateDeputies"
label="Dobradinhas"
options={stateDeputyOptions}
error={fieldError(state.fieldErrors, 'stateDeputies')}
placeholder="Adicionar dobradinha…"
/>
<div className="grid gap-4 sm:grid-cols-2">
<Field>
<FieldLabel htmlFor="leadership-sector">Setor</FieldLabel>
<NativeSelect
id="leadership-sector"
name="sector"
defaultValue=""
className="min-h-11 w-full"
>
<NativeSelectOption value="">Não informado</NativeSelectOption>
{leadershipSectors.map((sector) => (
<NativeSelectOption key={sector} value={sector}>
{sectorLabels[sector]}
</NativeSelectOption>
))}
</NativeSelect>
</Field>
<Field>
<FieldLabel htmlFor="leadership-status">Status de apoio</FieldLabel>
<NativeSelect
id="leadership-status"
name="supportStatus"
defaultValue="a_abordar"
className="min-h-11 w-full"
>
{leadershipSupportStatuses.map((status) => (
<NativeSelectOption key={status} value={status}>
{supportStatusLabels[status]}
</NativeSelectOption>
))}
</NativeSelect>
</Field>
</div>
<Field>
<FieldLabel htmlFor="leadership-notes">Observações internas</FieldLabel>
<Textarea id="leadership-notes" name="notes" rows={3} maxLength={3000} />
</Field>
<Field>
<FieldLabel htmlFor="leadership-consent-note">Registro de consentimento externo</FieldLabel>
<Textarea id="leadership-consent-note" name="consentNote" rows={2} maxLength={2000} />
</Field>
{state.message && state.status !== 'success' ? (
<Alert variant="destructive">
<AlertDescription>{state.message}</AlertDescription>
</Alert>
) : null}
<Button type="submit" disabled={isPending} className="min-h-11 self-start">
{isPending ? <Spinner data-icon="inline-start" aria-hidden="true" /> : null}
Cadastrar liderança
</Button>
</form>
)
}