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
|
||||
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) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
|
@ -203,17 +219,101 @@ function ReportsOverview() {
|
|||
</Card>
|
||||
)}
|
||||
|
||||
{/* Rounds Table */}
|
||||
{/* Project Reports (default: all projects, filterable by round) */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<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>
|
||||
Round Reports
|
||||
Project Reports
|
||||
</CardTitle>
|
||||
<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>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
|
@ -221,7 +321,7 @@ function ReportsOverview() {
|
|||
<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.
|
||||
No rounds created yet.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -230,7 +330,6 @@ function ReportsOverview() {
|
|||
<TableRow>
|
||||
<TableHead>Round</TableHead>
|
||||
<TableHead>Program</TableHead>
|
||||
<TableHead>Projects</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right">Export</TableHead>
|
||||
</TableRow>
|
||||
|
|
@ -249,7 +348,6 @@ function ReportsOverview() {
|
|||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{round.programName}</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={
|
||||
|
|
@ -264,7 +362,7 @@ function ReportsOverview() {
|
|||
</Badge>
|
||||
</TableCell>
|
||||
<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>
|
||||
<a
|
||||
href={`/api/export/evaluations?roundId=${round.id}`}
|
||||
|
|
@ -754,7 +852,7 @@ export default function ReportsPage() {
|
|||
{/* Tabs */}
|
||||
<Tabs defaultValue="overview" className="space-y-6">
|
||||
<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">
|
||||
<FileSpreadsheet className="h-4 w-4" />
|
||||
Overview
|
||||
|
|
@ -776,7 +874,7 @@ export default function ReportsPage() {
|
|||
Diversity
|
||||
</TabsTrigger>
|
||||
</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}>
|
||||
<SelectTrigger className="w-[220px]">
|
||||
<SelectValue placeholder="Select round for PDF" />
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@
|
|||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
@apply bg-background text-foreground overflow-x-hidden;
|
||||
font-feature-settings: "rlig" 1, "calt" 1;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -148,14 +148,14 @@ export function MembersContent() {
|
|||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-center justify-between flex-wrap gap-4">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Members</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Manage jury members, mentors, observers, and admins
|
||||
</p>
|
||||
</div>
|
||||
<Button asChild>
|
||||
<Button asChild className="w-full sm:w-auto shrink-0">
|
||||
<Link href="/admin/members/invite">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Member
|
||||
|
|
|
|||
|
|
@ -167,8 +167,8 @@ export function AdminSidebar({ user }: AdminSidebarProps) {
|
|||
return (
|
||||
<>
|
||||
{/* 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">
|
||||
<Logo showText textSuffix="Admin" />
|
||||
<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" className="min-w-0" />
|
||||
<div className="flex items-center gap-2">
|
||||
<NotificationBell />
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -43,10 +43,10 @@ export function Logo({
|
|||
priority
|
||||
/>
|
||||
{showText && (
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-1 min-w-0">
|
||||
<span className="font-semibold">MOPC</span>
|
||||
{textSuffix && (
|
||||
<span className="text-xs text-muted-foreground">{textSuffix}</span>
|
||||
<span className="text-xs text-muted-foreground truncate">{textSuffix}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue