teqo/src/components/campaign/VoteIntentionControl.tsx
Francisco Ignácio Fontoura Solla b079daa9bb Add C2 nominal supporter registry for /campanha.
Ship the supporter join collection, consent-keyed create/import/remove actions, and apoiadores UI so staff can build a phone-required nominal base before mass field ops.
2026-07-18 20:32:54 -03:00

133 lines
4.8 KiB
TypeScript

'use client'
import { useActionState, useEffect, useState } from 'react'
import { toast } from 'sonner'
import type { SupporterVoteIntentionFormState } from '@/app/(campaign)/campanha/(app)/apoiadores/[id]/formActions'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/Alert'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/Checkbox'
import {
Field,
FieldContent,
FieldDescription,
FieldLabel,
} from '@/components/ui/field'
import { Spinner } from '@/components/ui/Spinner'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/ToggleGroup'
import type { SupporterVoteIntention } from '@/lib/schemas/supporter'
import { supporterVoteIntentionLabels } from '@/utilities/supporterUi'
export type VoteIntentionControlAction = (
state: SupporterVoteIntentionFormState,
formData: FormData,
) => Promise<SupporterVoteIntentionFormState>
export const VoteIntentionControl = ({
supporterId,
currentValue,
hasVoteIntentionConsent,
voteIntentionConsentConfigured,
action,
}: {
supporterId: number
currentValue: SupporterVoteIntention | null
hasVoteIntentionConsent: boolean
voteIntentionConsentConfigured: boolean
action: VoteIntentionControlAction
}) => {
const [state, formAction, pending] = useActionState(action, {})
const [localConsentAccepted, setLocalConsentAccepted] = useState(false)
const [selected, setSelected] = useState<SupporterVoteIntention | ''>(currentValue ?? '')
const canEdit =
hasVoteIntentionConsent || (localConsentAccepted && voteIntentionConsentConfigured)
useEffect(() => {
if (state.status !== 'success') return
toast.success(state.message)
if (state.voteIntention) setSelected(state.voteIntention)
}, [state.message, state.status, state.voteIntention])
return (
<section className="rounded-[6px] border bg-card p-4" aria-labelledby="vote-intention-title">
<h2 id="vote-intention-title" className="font-medium">
Intenção de voto
</h2>
<p className="mt-1 text-sm text-muted-foreground">
Dado sensível — disponível somente após confirmação do consentimento LGPD.
</p>
{!hasVoteIntentionConsent ? (
voteIntentionConsentConfigured ? (
<Field orientation="horizontal" className="mt-4">
<Checkbox
id="vote-intention-consent-accepted"
checked={localConsentAccepted}
onCheckedChange={(checked) => setLocalConsentAccepted(checked === true)}
/>
<FieldContent>
<FieldLabel htmlFor="vote-intention-consent-accepted">
Confirmo o consentimento destacado para registrar intenção de voto
</FieldLabel>
<FieldDescription>
Marque somente se o titular autorizou expressamente este tratamento.
</FieldDescription>
</FieldContent>
</Field>
) : (
<Alert variant="destructive" className="mt-4">
<AlertTitle>Consentimento não configurado</AlertTitle>
<AlertDescription>
O texto de intenção de voto ainda não foi cadastrado no admin.
</AlertDescription>
</Alert>
)
) : null}
<form action={formAction} className="mt-4 flex flex-col gap-4">
<input type="hidden" name="id" value={supporterId} />
<input type="hidden" name="voteIntention" value={selected} />
{hasVoteIntentionConsent || localConsentAccepted ? (
<input type="hidden" name="voteIntentionConsentAccepted" value="true" />
) : null}
<ToggleGroup
type="single"
value={selected}
onValueChange={(value) => setSelected((value as SupporterVoteIntention) ?? '')}
variant="outline"
className="flex w-full flex-wrap"
disabled={!canEdit || pending}
>
{Object.entries(supporterVoteIntentionLabels).map(([value, label]) => (
<ToggleGroupItem key={value} value={value} className="min-h-11 flex-1">
{label}
</ToggleGroupItem>
))}
</ToggleGroup>
{!canEdit ? (
<p className="text-sm text-muted-foreground">
Disponível somente após confirmação do consentimento LGPD.
</p>
) : null}
{state.message && state.status !== 'success' ? (
<Alert variant="destructive">
<AlertDescription>{state.message}</AlertDescription>
</Alert>
) : null}
<Button
type="submit"
className="min-h-11 w-fit"
disabled={!canEdit || !selected || pending}
>
{pending ? <Spinner data-icon="inline-start" /> : null}
{pending ? 'Salvando…' : 'Salvar intenção'}
</Button>
</form>
</section>
)
}