Add project reports section and fix mobile overflow issues
Build and Push Docker Image / build (push) Successful in 11m20s Details

- Add project-wide reporting table with scope selector (all rounds / per round)
- Fix horizontal overflow on mobile (body, admin sidebar, logo truncation)
- Make members header and reports tabs responsive with flex-wrap

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt 2026-02-11 19:08:04 +01:00
parent bd9cd310fc
commit b5d90d3c26
5 changed files with 116 additions and 18 deletions

View File

@ -65,6 +65,22 @@ function ReportsOverview() {
// Flatten rounds from all programs // Flatten rounds from all programs
const rounds = programs?.flatMap(p => p.rounds.map(r => ({ ...r, programId: p.id, programName: `${p.year} Edition` }))) || [] const rounds = programs?.flatMap(p => p.rounds.map(r => ({ ...r, programId: p.id, programName: `${p.year} Edition` }))) || []
// Project reporting scope (default: latest program, all rounds)
const [selectedValue, setSelectedValue] = useState<string | null>(null)
if (programs?.length && !selectedValue) {
setSelectedValue(`all:${programs[0].id}`)
}
const scopeInput = parseSelection(selectedValue)
const hasScope = !!scopeInput.roundId || !!scopeInput.programId
const { data: projectRankings, isLoading: projectsLoading } =
trpc.analytics.getProjectRankings.useQuery(
{ ...scopeInput, limit: 5000 },
{ enabled: hasScope }
)
if (isLoading || statsLoading) { if (isLoading || statsLoading) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@ -203,17 +219,101 @@ function ReportsOverview() {
</Card> </Card>
)} )}
{/* Rounds Table */} {/* Project Reports (default: all projects, filterable by round) */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<div className="rounded-lg bg-emerald-500/10 p-1.5"> <div className="rounded-lg bg-emerald-500/10 p-1.5">
<FileSpreadsheet className="h-4 w-4 text-emerald-600" /> <ClipboardList className="h-4 w-4 text-emerald-600" />
</div> </div>
Round Reports Project Reports
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
View progress and export data for each round Project-wide reporting across all projects optionally filter to a specific round
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* Scope selector */}
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
<span className="text-sm font-medium">Scope:</span>
<Select value={selectedValue || ''} onValueChange={setSelectedValue}>
<SelectTrigger className="w-full sm:w-[360px]">
<SelectValue placeholder="All projects" />
</SelectTrigger>
<SelectContent>
{programs?.map((p) => (
<SelectItem key={`all:${p.id}`} value={`all:${p.id}`}>
{p.year} Edition All Rounds
</SelectItem>
))}
{rounds.map((round) => (
<SelectItem key={round.id} value={round.id}>
{round.programName} - {round.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{projectsLoading ? (
<Skeleton className="h-[400px]" />
) : projectRankings?.length ? (
<div className="rounded-lg border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Project</TableHead>
<TableHead className="hidden sm:table-cell">Team</TableHead>
<TableHead className="text-right">Avg</TableHead>
<TableHead className="text-right">Evals</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{projectRankings.map((p) => (
<TableRow key={p.id}>
<TableCell className="font-medium">
<Link href={`/admin/projects/${p.id}`} className="hover:underline">
{p.title}
</Link>
</TableCell>
<TableCell className="hidden sm:table-cell text-muted-foreground">
{p.teamName || '-'}
</TableCell>
<TableCell className="text-right tabular-nums">
{p.averageScore === null ? '-' : p.averageScore.toFixed(2)}
</TableCell>
<TableCell className="text-right tabular-nums">{p.evaluationCount}</TableCell>
<TableCell>
<Badge variant="outline">{p.status}</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : (
<div className="flex flex-col items-center justify-center py-10 text-center">
<ClipboardList className="h-10 w-10 text-muted-foreground/50" />
<p className="mt-2 text-sm text-muted-foreground">
No project report data available for the selected scope yet.
</p>
</div>
)}
</CardContent>
</Card>
{/* Round exports (still available) */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<div className="rounded-lg bg-blue-500/10 p-1.5">
<FileSpreadsheet className="h-4 w-4 text-blue-600" />
</div>
Round Exports
</CardTitle>
<CardDescription>
Download round-level evaluations and results
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@ -221,7 +321,7 @@ function ReportsOverview() {
<div className="flex flex-col items-center justify-center py-8 text-center"> <div className="flex flex-col items-center justify-center py-8 text-center">
<FileSpreadsheet className="h-10 w-10 text-muted-foreground/50" /> <FileSpreadsheet className="h-10 w-10 text-muted-foreground/50" />
<p className="mt-2 text-sm text-muted-foreground"> <p className="mt-2 text-sm text-muted-foreground">
No rounds created yet. Round-specific reports will appear here once rounds are set up. No rounds created yet.
</p> </p>
</div> </div>
) : ( ) : (
@ -230,7 +330,6 @@ function ReportsOverview() {
<TableRow> <TableRow>
<TableHead>Round</TableHead> <TableHead>Round</TableHead>
<TableHead>Program</TableHead> <TableHead>Program</TableHead>
<TableHead>Projects</TableHead>
<TableHead>Status</TableHead> <TableHead>Status</TableHead>
<TableHead className="text-right">Export</TableHead> <TableHead className="text-right">Export</TableHead>
</TableRow> </TableRow>
@ -249,7 +348,6 @@ function ReportsOverview() {
</div> </div>
</TableCell> </TableCell>
<TableCell>{round.programName}</TableCell> <TableCell>{round.programName}</TableCell>
<TableCell>-</TableCell>
<TableCell> <TableCell>
<Badge <Badge
variant={ variant={
@ -264,7 +362,7 @@ function ReportsOverview() {
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2 flex-wrap">
<Button variant="outline" size="sm" asChild> <Button variant="outline" size="sm" asChild>
<a <a
href={`/api/export/evaluations?roundId=${round.id}`} href={`/api/export/evaluations?roundId=${round.id}`}
@ -754,7 +852,7 @@ export default function ReportsPage() {
{/* Tabs */} {/* Tabs */}
<Tabs defaultValue="overview" className="space-y-6"> <Tabs defaultValue="overview" className="space-y-6">
<div className="flex items-center justify-between flex-wrap gap-4"> <div className="flex items-center justify-between flex-wrap gap-4">
<TabsList> <TabsList className="w-full sm:w-auto flex-wrap justify-start overflow-x-auto scrollbar-hide">
<TabsTrigger value="overview" className="gap-2"> <TabsTrigger value="overview" className="gap-2">
<FileSpreadsheet className="h-4 w-4" /> <FileSpreadsheet className="h-4 w-4" />
Overview Overview
@ -776,7 +874,7 @@ export default function ReportsPage() {
Diversity Diversity
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 w-full sm:w-auto justify-between sm:justify-end">
<Select value={pdfRoundId || ''} onValueChange={setPdfRoundId}> <Select value={pdfRoundId || ''} onValueChange={setPdfRoundId}>
<SelectTrigger className="w-[220px]"> <SelectTrigger className="w-[220px]">
<SelectValue placeholder="Select round for PDF" /> <SelectValue placeholder="Select round for PDF" />

View File

@ -217,7 +217,7 @@
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground overflow-x-hidden;
font-feature-settings: "rlig" 1, "calt" 1; font-feature-settings: "rlig" 1, "calt" 1;
} }

View File

@ -148,14 +148,14 @@ export function MembersContent() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between flex-wrap gap-4">
<div> <div className="min-w-0">
<h1 className="text-2xl font-semibold tracking-tight">Members</h1> <h1 className="text-2xl font-semibold tracking-tight">Members</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Manage jury members, mentors, observers, and admins Manage jury members, mentors, observers, and admins
</p> </p>
</div> </div>
<Button asChild> <Button asChild className="w-full sm:w-auto shrink-0">
<Link href="/admin/members/invite"> <Link href="/admin/members/invite">
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
Add Member Add Member

View File

@ -167,8 +167,8 @@ export function AdminSidebar({ user }: AdminSidebarProps) {
return ( return (
<> <>
{/* Mobile menu button */} {/* Mobile menu button */}
<div className="fixed top-0 left-0 right-0 z-40 flex h-16 items-center justify-between border-b bg-card px-4 lg:hidden"> <div className="fixed top-0 left-0 right-0 z-40 flex h-16 items-center justify-between border-b bg-card px-4 lg:hidden overflow-x-hidden">
<Logo showText textSuffix="Admin" /> <Logo showText textSuffix="Admin" className="min-w-0" />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<NotificationBell /> <NotificationBell />
<Button <Button

View File

@ -43,10 +43,10 @@ export function Logo({
priority priority
/> />
{showText && ( {showText && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1 min-w-0">
<span className="font-semibold">MOPC</span> <span className="font-semibold">MOPC</span>
{textSuffix && ( {textSuffix && (
<span className="text-xs text-muted-foreground">{textSuffix}</span> <span className="text-xs text-muted-foreground truncate">{textSuffix}</span>
)} )}
</div> </div>
)} )}