teqo/src/components/campaign/NucleusCard.tsx
Francisco Ignácio Fontoura Solla e5e051d175 Deliver E1 vote goals and E3 strategy fields on nuclei.
Wire schema, UI, aggregates, and tests for scenario goals, priority filter, staff-only dobradinha notes and next steps; register E6 scale follow-ups from simplify.
2026-07-19 04:38:07 -03:00

110 lines
3.2 KiB
TypeScript

import { AlertTriangleIcon, Clock3Icon } from 'lucide-react'
import { Badge } from '@/components/ui/Badge'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { TseZoneBadge } from '@/components/campaign/TseZoneBadge'
import { cn } from '@/lib/utils'
import { nucleusPriorityLabels } from '@/utilities/nucleusUi'
const voteFormatter = new Intl.NumberFormat('pt-BR')
export type NucleusCardProps = {
name: string
territory: string
organization?: string
tseZones?: number[]
confirmedVoteEstimate?: number | null
hasPendingEstimate?: boolean
isHighPriority?: boolean
lastUpdateLabel?: string
isUpdateOverdue?: boolean
leadershipCounts?: {
engaged: number
toApproach: number
disputed?: number
}
actions?: React.ReactNode
className?: string
}
export const NucleusCard = ({
name,
territory,
organization,
tseZones = [],
confirmedVoteEstimate,
hasPendingEstimate = false,
isHighPriority = false,
lastUpdateLabel,
isUpdateOverdue = false,
leadershipCounts,
actions,
className,
}: NucleusCardProps) => (
<Card className={className}>
<CardHeader>
<div className="flex flex-wrap items-start justify-between gap-2">
<CardTitle>{name}</CardTitle>
{isHighPriority ? <Badge variant="destructive">{nucleusPriorityLabels.alta}</Badge> : null}
</div>
<CardDescription>{[organization, territory].filter(Boolean).join(' · ')}</CardDescription>
{tseZones.length > 0 ? (
<div className="flex flex-wrap gap-1">
{tseZones.map((zoneNumber) => (
<TseZoneBadge key={zoneNumber} zoneNumber={zoneNumber} />
))}
</div>
) : (
<Badge variant="tse">Sem Zona TSE</Badge>
)}
</CardHeader>
<CardContent className="flex flex-col gap-3">
<div className="flex flex-wrap items-center gap-2">
<span className="text-2xl font-bold tabular-nums">
{confirmedVoteEstimate == null
? 'Sem estimativa confirmada'
: `${voteFormatter.format(confirmedVoteEstimate)} votos`}
</span>
{hasPendingEstimate ? <Badge variant="estimate-pending">Sugestão pendente</Badge> : null}
</div>
{leadershipCounts ? (
<p className="m-0 text-sm">
<strong>{leadershipCounts.engaged} engajadas</strong>
{' · '}
{leadershipCounts.toApproach} a abordar
{leadershipCounts.disputed != null ? ` · ${leadershipCounts.disputed} em disputa` : null}
</p>
) : null}
{lastUpdateLabel ? (
<div
className={cn(
'flex items-center gap-1.5 text-xs text-muted-foreground',
isUpdateOverdue && 'font-medium text-cadence-overdue',
)}
>
{isUpdateOverdue ? (
<AlertTriangleIcon aria-hidden="true" />
) : (
<Clock3Icon aria-hidden="true" />
)}
<span>
{isUpdateOverdue ? 'Atualização atrasada · ' : null}
{lastUpdateLabel}
</span>
</div>
) : null}
</CardContent>
{actions ? <CardFooter className="flex-wrap gap-2">{actions}</CardFooter> : null}
</Card>
)