Reports general data, projects import fix, and Docker entrypoint cleanup

- Reports page now shows platform-wide stats even when no rounds exist
- Fix missing getCountryFlag import on projects page
- Clean up Docker entrypoint: remove hardcoded migrate resolve

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 00:29:21 +01:00
parent 7e3d600eed
commit 74515768f5
4 changed files with 157 additions and 306 deletions

View File

@@ -59,11 +59,12 @@ import { ExportPdfButton } from '@/components/shared/export-pdf-button'
function ReportsOverview() {
const { data: programs, isLoading } = trpc.program.list.useQuery({ includeRounds: true })
const { data: dashStats, isLoading: statsLoading } = trpc.analytics.getDashboardStats.useQuery()
// Flatten rounds from all programs
const rounds = programs?.flatMap(p => p.rounds.map(r => ({ ...r, programId: p.id, programName: `${p.year} Edition` }))) || []
if (isLoading) {
if (isLoading || statsLoading) {
return (
<div className="space-y-6">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
@@ -83,24 +84,13 @@ function ReportsOverview() {
)
}
if (!rounds || rounds.length === 0) {
return (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
<FileSpreadsheet className="h-12 w-12 text-muted-foreground/50" />
<p className="mt-2 font-medium">No data to report</p>
<p className="text-sm text-muted-foreground">
Create rounds and assign jury members to generate reports
</p>
</CardContent>
</Card>
)
}
// Calculate totals
const totalProjects = programs?.reduce((acc, p) => acc + (p._count?.rounds || 0), 0) || 0
const totalPrograms = programs?.length || 0
const activeRounds = rounds.filter((r) => r.status === 'ACTIVE').length
const totalPrograms = dashStats?.programCount ?? programs?.length ?? 0
const totalProjects = dashStats?.projectCount ?? 0
const activeRounds = dashStats?.activeRoundCount ?? rounds.filter((r) => r.status === 'ACTIVE').length
const jurorCount = dashStats?.jurorCount ?? 0
const submittedEvaluations = dashStats?.submittedEvaluations ?? 0
const totalEvaluations = dashStats?.totalEvaluations ?? 0
const completionRate = dashStats?.completionRate ?? 0
return (
<div className="space-y-6">
@@ -108,13 +98,13 @@ function ReportsOverview() {
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Rounds</CardTitle>
<BarChart3 className="h-4 w-4 text-muted-foreground" />
<CardTitle className="text-sm font-medium">Programs</CardTitle>
<CheckCircle2 className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{rounds.length}</div>
<div className="text-2xl font-bold">{totalPrograms}</div>
<p className="text-xs text-muted-foreground">
{activeRounds} active
{activeRounds} active round{activeRounds !== 1 ? 's' : ''}
</p>
</CardContent>
</Card>
@@ -126,33 +116,63 @@ function ReportsOverview() {
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totalProjects}</div>
<p className="text-xs text-muted-foreground">Across all rounds</p>
<p className="text-xs text-muted-foreground">Across all programs</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Active Rounds</CardTitle>
<CardTitle className="text-sm font-medium">Jury Members</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{activeRounds}</div>
<p className="text-xs text-muted-foreground">Currently active</p>
<div className="text-2xl font-bold">{jurorCount}</div>
<p className="text-xs text-muted-foreground">Active jurors</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Programs</CardTitle>
<CheckCircle2 className="h-4 w-4 text-muted-foreground" />
<CardTitle className="text-sm font-medium">Evaluations</CardTitle>
<BarChart3 className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totalPrograms}</div>
<p className="text-xs text-muted-foreground">Total programs</p>
<div className="text-2xl font-bold">{submittedEvaluations}</div>
<p className="text-xs text-muted-foreground">
{totalEvaluations > 0
? `${completionRate}% completion rate`
: 'No assignments yet'}
</p>
</CardContent>
</Card>
</div>
{/* Score Distribution (if any evaluations exist) */}
{dashStats?.scoreDistribution && dashStats.scoreDistribution.some(b => b.count > 0) && (
<Card>
<CardHeader>
<CardTitle>Score Distribution</CardTitle>
<CardDescription>Overall score distribution across all evaluations</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{dashStats.scoreDistribution.map((bucket) => {
const maxCount = Math.max(...dashStats.scoreDistribution.map(b => b.count), 1)
return (
<div key={bucket.label} className="flex items-center gap-3">
<span className="w-10 text-sm font-medium text-right">{bucket.label}</span>
<div className="flex-1">
<Progress value={(bucket.count / maxCount) * 100} className="h-6" />
</div>
<span className="w-8 text-sm text-muted-foreground text-right">{bucket.count}</span>
</div>
)
})}
</div>
</CardContent>
</Card>
)}
{/* Rounds Table */}
<Card>
<CardHeader>
@@ -162,72 +182,81 @@ function ReportsOverview() {
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Round</TableHead>
<TableHead>Program</TableHead>
<TableHead>Projects</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Export</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rounds.map((round) => (
<TableRow key={round.id}>
<TableCell>
<div>
<p className="font-medium">{round.name}</p>
{round.votingEndAt && (
<p className="text-sm text-muted-foreground">
Ends: {formatDateOnly(round.votingEndAt)}
</p>
)}
</div>
</TableCell>
<TableCell>{round.programName}</TableCell>
<TableCell>-</TableCell>
<TableCell>
<Badge
variant={
round.status === 'ACTIVE'
? 'default'
: round.status === 'CLOSED'
? 'secondary'
: 'outline'
}
>
{round.status}
</Badge>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button variant="outline" size="sm" asChild>
<a
href={`/api/export/evaluations?roundId=${round.id}`}
target="_blank"
rel="noopener noreferrer"
>
<Download className="mr-2 h-4 w-4" />
Evaluations
</a>
</Button>
<Button variant="outline" size="sm" asChild>
<a
href={`/api/export/results?roundId=${round.id}`}
target="_blank"
rel="noopener noreferrer"
>
<Download className="mr-2 h-4 w-4" />
Results
</a>
</Button>
</div>
</TableCell>
{rounds.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-center">
<FileSpreadsheet className="h-10 w-10 text-muted-foreground/50" />
<p className="mt-2 text-sm text-muted-foreground">
No rounds created yet. Round-specific reports will appear here once rounds are set up.
</p>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Round</TableHead>
<TableHead>Program</TableHead>
<TableHead>Projects</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Export</TableHead>
</TableRow>
))}
</TableBody>
</Table>
</TableHeader>
<TableBody>
{rounds.map((round) => (
<TableRow key={round.id}>
<TableCell>
<div>
<p className="font-medium">{round.name}</p>
{round.votingEndAt && (
<p className="text-sm text-muted-foreground">
Ends: {formatDateOnly(round.votingEndAt)}
</p>
)}
</div>
</TableCell>
<TableCell>{round.programName}</TableCell>
<TableCell>-</TableCell>
<TableCell>
<Badge
variant={
round.status === 'ACTIVE'
? 'default'
: round.status === 'CLOSED'
? 'secondary'
: 'outline'
}
>
{round.status}
</Badge>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button variant="outline" size="sm" asChild>
<a
href={`/api/export/evaluations?roundId=${round.id}`}
target="_blank"
rel="noopener noreferrer"
>
<Download className="mr-2 h-4 w-4" />
Evaluations
</a>
</Button>
<Button variant="outline" size="sm" asChild>
<a
href={`/api/export/results?roundId=${round.id}`}
target="_blank"
rel="noopener noreferrer"
>
<Download className="mr-2 h-4 w-4" />
Results
</a>
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>