Add project reports section and fix mobile overflow issues
Build and Push Docker Image / build (push) Successful in 11m20s
Details
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:
parent
bd9cd310fc
commit
b5d90d3c26
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue