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.
176 lines
5.9 KiB
TypeScript
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>
|
|
)
|
|
}
|