import { useEffect, useState } from 'react' import { Check, FileDown, Layers, Loader2, X } from 'lucide-react' import { useSlateCandidates, useSlateRequirements, useGenerateSlateTool, useDecideMember, useSlateReadiness, useSlateDetail, useSlatesFor, } from './queries' import { slateApi } from './api' import { slateBrand } from './brand' import { Badge, Panel } from '../../components/ui' import { ToolHeader } from '../../components/ToolHeader' import { bandColor, num } from '../../lib/format' import type { ProposalReadinessView, SlateMemberDetailView } from '../../lib/types' const selectCls = 'rounded-lg border border-line/70 bg-navy-900 px-3 py-2 text-sm text-ink outline-none focus:border-slate-accent min-w-[16rem]' export function Slate() { const requirements = useSlateRequirements() const candidates = useSlateCandidates() const gen = useGenerateSlateTool() const [reqId, setReqId] = useState('') const [name, setName] = useState('Proposal Candidate Slate') // The active slate is whichever was just generated or selected from the recent list. Its members, // decisions, and notes are read from the stored slate, so a reload or revisit rehydrates them. const [slateId, setSlateId] = useState(null) const slates = useSlatesFor(reqId || null) const detail = useSlateDetail(slateId) const readiness = useSlateReadiness(slateId) useEffect(() => { if (!reqId && requirements.data?.length) setReqId(requirements.data[0].id) }, [requirements.data, reqId]) // A new requirement clears the open slate until one is generated or picked. useEffect(() => { setSlateId(null) }, [reqId]) const names = new Map((candidates.data ?? []).map((c) => [c.id, c.full_name])) return (
{!!slates.data?.length && (
{slates.data.map((s) => ( ))}
)} {gen.isError && (

Slate generation failed: {(gen.error as Error).message}

)} {slateId && ( <> {detail.isLoading ? (

Loading the slate…

) : (
{detail.data?.members.map((m) => ( ))}
)}

A human approves or rejects every candidate, optionally with a note. The engine never auto rejects. Each decision is recorded with the actor and the time for the AEDT audit trail.

)}
) } function MemberRow({ m, slateId, fallbackName, }: { m: SlateMemberDetailView slateId: string fallbackName?: string }) { const decide = useDecideMember() const [note, setNote] = useState('') const decided = m.decision === 'approved' || m.decision === 'rejected' return (
{m.rank}
{m.full_name || fallbackName || m.candidate_id.slice(0, 8)}
{m.explanation}
{num(m.total_score).toFixed(0)}%
{m.band_label}
{decided ? ( <> {m.decision === 'approved' ? : } {m.decision === 'approved' ? 'Approved' : 'Rejected'} {m.decision_note ? {m.decision_note} : null} ) : ( <> setNote(e.target.value)} placeholder="Reviewer note (optional)" className="w-56 rounded-lg border border-line/70 bg-navy-900 px-2.5 py-1.5 text-xs text-ink outline-none focus:border-slate-accent" />
)}
) } function ReadinessPanel({ readiness, slateId }: { readiness: ProposalReadinessView | undefined; slateId: string }) { return (
Status
{readiness?.status ?? '...'}
Approved share
{readiness ? `${readiness.readiness_pct}%` : '...'}
Export CSV
) } function Stat({ label, value, color }: { label: string; value: number | undefined; color?: string }) { return (
{label}
{value ?? '...'}
) }