Round system redesign: Phases 1-7 complete
Full pipeline/track/stage architecture replacing the legacy round system. Schema: 11 new models (Pipeline, Track, Stage, StageTransition, ProjectStageState, RoutingRule, Cohort, CohortProject, LiveProgressCursor, OverrideAction, AudienceVoter) + 8 new enums. Backend: 9 new routers (pipeline, stage, routing, stageFiltering, stageAssignment, cohort, live, decision, award) + 6 new services (stage-engine, routing-engine, stage-filtering, stage-assignment, stage-notifications, live-control). Frontend: Pipeline wizard (17 components), jury stage pages (7), applicant pipeline pages (3), public stage pages (2), admin pipeline pages (5), shared stage components (3), SSE route, live hook. Phase 6 refit: 23 routers/services migrated from roundId to stageId, all frontend components refitted. Deleted round.ts (985 lines), roundTemplate.ts, round-helpers.ts, round-settings.ts, round-type-settings.tsx, 10 legacy admin pages, 7 legacy jury pages, 3 legacy dialogs. Phase 7 validation: 36 tests (10 unit + 8 integration files) all passing, TypeScript 0 errors, Next.js build succeeds, 13 integrity checks, legacy symbol sweep clean, auto-seed on first Docker startup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -48,7 +48,7 @@ import { useDebouncedCallback } from 'use-debounce'
|
||||
const PER_PAGE_OPTIONS = [10, 20, 50]
|
||||
|
||||
export function ObserverDashboardContent({ userName }: { userName?: string }) {
|
||||
const [selectedRoundId, setSelectedRoundId] = useState<string>('all')
|
||||
const [selectedStageId, setSelectedStageId] = useState<string>('all')
|
||||
const [search, setSearch] = useState('')
|
||||
const [debouncedSearch, setDebouncedSearch] = useState('')
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all')
|
||||
@@ -65,8 +65,8 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
|
||||
debouncedSetSearch(value)
|
||||
}
|
||||
|
||||
const handleRoundChange = (value: string) => {
|
||||
setSelectedRoundId(value)
|
||||
const handleStageChange = (value: string) => {
|
||||
setSelectedStageId(value)
|
||||
setPage(1)
|
||||
}
|
||||
|
||||
@@ -75,38 +75,38 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
|
||||
setPage(1)
|
||||
}
|
||||
|
||||
// Fetch programs/rounds for the filter dropdown
|
||||
const { data: programs } = trpc.program.list.useQuery({ includeRounds: true })
|
||||
// Fetch programs/stages for the filter dropdown
|
||||
const { data: programs } = trpc.program.list.useQuery({ includeStages: true })
|
||||
|
||||
const rounds = programs?.flatMap((p) =>
|
||||
p.rounds.map((r) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
const stages = programs?.flatMap((p) =>
|
||||
(p.stages as { id: string; name: string; status: string; windowCloseAt: Date | null; _count: { projects: number; assignments: number } }[]).map((s) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
programName: `${p.year} Edition`,
|
||||
status: r.status,
|
||||
status: s.status,
|
||||
}))
|
||||
) || []
|
||||
|
||||
// Fetch dashboard stats
|
||||
const roundIdParam = selectedRoundId !== 'all' ? selectedRoundId : undefined
|
||||
const stageIdParam = selectedStageId !== 'all' ? selectedStageId : undefined
|
||||
const { data: stats, isLoading: statsLoading } = trpc.analytics.getDashboardStats.useQuery(
|
||||
{ roundId: roundIdParam }
|
||||
{ stageId: stageIdParam }
|
||||
)
|
||||
|
||||
// Fetch projects
|
||||
const { data: projectsData, isLoading: projectsLoading } = trpc.analytics.getAllProjects.useQuery({
|
||||
roundId: roundIdParam,
|
||||
stageId: stageIdParam,
|
||||
search: debouncedSearch || undefined,
|
||||
status: statusFilter !== 'all' ? statusFilter : undefined,
|
||||
page,
|
||||
perPage,
|
||||
})
|
||||
|
||||
// Fetch recent rounds for jury completion
|
||||
const { data: recentRoundsData } = trpc.program.list.useQuery({ includeRounds: true })
|
||||
const recentRounds = recentRoundsData?.flatMap((p) =>
|
||||
p.rounds.map((r) => ({
|
||||
...r,
|
||||
// Fetch recent stages for jury completion
|
||||
const { data: recentStagesData } = trpc.program.list.useQuery({ includeStages: true })
|
||||
const recentStages = recentStagesData?.flatMap((p) =>
|
||||
(p.stages as { id: string; name: string; status: string; windowCloseAt: Date | null; _count: { projects: number; assignments: number } }[]).map((s) => ({
|
||||
...s,
|
||||
programName: `${p.year} Edition`,
|
||||
}))
|
||||
)?.slice(0, 5) || []
|
||||
@@ -141,18 +141,18 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Round Filter */}
|
||||
{/* Stage Filter */}
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
|
||||
<label className="text-sm font-medium">Filter by Round:</label>
|
||||
<Select value={selectedRoundId} onValueChange={handleRoundChange}>
|
||||
<label className="text-sm font-medium">Filter by Stage:</label>
|
||||
<Select value={selectedStageId} onValueChange={handleStageChange}>
|
||||
<SelectTrigger className="w-full sm:w-[300px]">
|
||||
<SelectValue placeholder="All Rounds" />
|
||||
<SelectValue placeholder="All Stages" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Rounds</SelectItem>
|
||||
{rounds.map((round) => (
|
||||
<SelectItem key={round.id} value={round.id}>
|
||||
{round.programName} - {round.name}
|
||||
<SelectItem value="all">All Stages</SelectItem>
|
||||
{stages.map((stage) => (
|
||||
<SelectItem key={stage.id} value={stage.id}>
|
||||
{stage.programName} - {stage.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -184,7 +184,7 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
|
||||
<p className="text-sm font-medium text-muted-foreground">Programs</p>
|
||||
<p className="text-2xl font-bold mt-1">{stats.programCount}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{stats.activeRoundCount} active round{stats.activeRoundCount !== 1 ? 's' : ''}
|
||||
{stats.activeStageCount} active round{stats.activeStageCount !== 1 ? 's' : ''}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl bg-blue-50 p-3">
|
||||
@@ -203,7 +203,7 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
|
||||
<p className="text-sm font-medium text-muted-foreground">Projects</p>
|
||||
<p className="text-2xl font-bold mt-1">{stats.projectCount}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{selectedRoundId !== 'all' ? 'In selected round' : 'Across all rounds'}
|
||||
{selectedStageId !== 'all' ? 'In selected stage' : 'Across all stages'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl bg-emerald-50 p-3">
|
||||
@@ -341,7 +341,7 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
|
||||
<TableCell className="max-w-[150px] truncate">{project.teamName || '-'}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" className="text-xs whitespace-nowrap">
|
||||
{project.roundName}
|
||||
{project.stageName}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
@@ -375,7 +375,7 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
|
||||
)}
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{project.roundName}
|
||||
{project.stageName}
|
||||
</Badge>
|
||||
<div className="flex gap-3">
|
||||
<span>Score: {project.averageScore !== null ? project.averageScore.toFixed(2) : '-'}</span>
|
||||
@@ -465,8 +465,8 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
|
||||
</AnimatedCard>
|
||||
)}
|
||||
|
||||
{/* Recent Rounds */}
|
||||
{recentRounds.length > 0 && (
|
||||
{/* Recent Stages */}
|
||||
{recentStages.length > 0 && (
|
||||
<AnimatedCard index={6}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -474,40 +474,40 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
|
||||
<div className="rounded-lg bg-violet-500/10 p-1.5">
|
||||
<BarChart3 className="h-4 w-4 text-violet-500" />
|
||||
</div>
|
||||
Recent Rounds
|
||||
Recent Stages
|
||||
</CardTitle>
|
||||
<CardDescription>Overview of the latest voting rounds</CardDescription>
|
||||
<CardDescription>Overview of the latest evaluation stages</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{recentRounds.map((round) => (
|
||||
{recentStages.map((stage) => (
|
||||
<div
|
||||
key={round.id}
|
||||
key={stage.id}
|
||||
className="flex items-center justify-between rounded-lg border p-4 transition-all hover:shadow-sm"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-medium">{round.name}</p>
|
||||
<p className="font-medium">{stage.name}</p>
|
||||
<Badge
|
||||
variant={
|
||||
round.status === 'ACTIVE'
|
||||
stage.status === 'STAGE_ACTIVE'
|
||||
? 'default'
|
||||
: round.status === 'CLOSED'
|
||||
: stage.status === 'STAGE_CLOSED'
|
||||
? 'secondary'
|
||||
: 'outline'
|
||||
}
|
||||
>
|
||||
{round.status}
|
||||
{stage.status === 'STAGE_ACTIVE' ? 'Active' : stage.status === 'STAGE_CLOSED' ? 'Closed' : stage.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{round.programName}
|
||||
{stage.programName}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right text-sm">
|
||||
<p>{round._count?.projects || 0} projects</p>
|
||||
<p>{stage._count?.projects || 0} projects</p>
|
||||
<p className="text-muted-foreground">
|
||||
{round._count?.assignments || 0} assignments
|
||||
{stage._count?.assignments || 0} assignments
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user