'use client' import { useEffect, useState } from 'react' import { trpc } from '@/lib/trpc/client' import { toast } from 'sonner' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' import { Loader2, Save, Trophy } from 'lucide-react' import { formatEnumLabel } from '@/lib/utils' import type { CompetitionCategory } from '@prisma/client' interface Props { programId: string } const CATEGORIES: CompetitionCategory[] = ['STARTUP', 'BUSINESS_CONCEPT'] type Row = { category: CompetitionCategory quota: number confirmed: number pending: number } export function FinalistSlotsCard({ programId }: Props) { const utils = trpc.useUtils() const { data: quotas, isLoading: loadingQuotas } = trpc.finalist.listQuotas.useQuery({ programId, }) const { data: counts, isLoading: loadingCounts } = trpc.finalist.listCategoryCounts.useQuery({ programId, }) const [draft, setDraft] = useState>({ STARTUP: '', BUSINESS_CONCEPT: '', }) // Sync draft from server response on first load / after save useEffect(() => { if (!quotas) return const next: Record = { STARTUP: '', BUSINESS_CONCEPT: '' } for (const cat of CATEGORIES) { const found = quotas.find((q) => q.category === cat) next[cat] = found ? String(found.quota) : '' } setDraft(next) }, [quotas]) const setQuotaMutation = trpc.finalist.setQuota.useMutation({ onSuccess: (_, vars) => { toast.success(`${formatEnumLabel(vars.category)} quota saved`) utils.finalist.listQuotas.invalidate({ programId }) utils.finalist.listCategoryCounts.invalidate({ programId }) }, onError: (err) => toast.error(err.message), }) if (loadingQuotas || loadingCounts) { return } const rows: Row[] = CATEGORIES.map((cat) => { const q = quotas?.find((x) => x.category === cat) const c = counts?.find((x) => x.category === cat) return { category: cat, quota: q?.quota ?? 0, confirmed: c?.confirmed ?? 0, pending: c?.pending ?? 0, } }) const handleSave = (category: CompetitionCategory) => { const raw = draft[category] const n = Number.parseInt(raw, 10) if (Number.isNaN(n) || n < 0) { toast.error('Quota must be a non-negative integer') return } setQuotaMutation.mutate({ programId, category, quota: n }) } return (
Finalist slots
Per-category quotas. Reductions blocked when {`> `}confirmed count — un-confirm a team first if you need to shrink a category.
{rows.map((r) => { const isPending = setQuotaMutation.isPending && setQuotaMutation.variables?.category === r.category const dirty = String(r.quota) !== draft[r.category] return (
{formatEnumLabel(r.category)}
{r.confirmed} {' '} confirmed {r.pending} {' '} pending
setDraft((d) => ({ ...d, [r.category]: e.target.value })) } />
) })}
) }