Observer platform: mobile fixes, data/UX overhaul, animated nav
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m41s

- Fix dashboard default round selection to target active round instead of R1
- Move edition selector from dashboard header to hamburger menu via shared context
- Add observer-friendly status labels (Not Reviewed / Under Review / Reviewed)
- Fix pipeline completion: closed rounds show 100%, cap all rates at 100%
- Round badge on projects list shows furthest round reached
- Hide scores/evals for projects with zero evaluations
- Enhance project detail round history with pass/reject indicators from ProjectRoundState
- Remove irrelevant fields (Org Type, Budget, Duration) from project detail
- Clickable juror workload with expandable project assignments
- Humanize activity feed with icons and readable messages
- Fix jurors table: responsive card layout on mobile
- Fix criteria chart: horizontal bars for readable labels on mobile
- Animate hamburger menu open/close with CSS grid transition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 22:45:56 +01:00
parent 5eea430ebd
commit 213efdba87
11 changed files with 576 additions and 313 deletions

View File

@@ -1,5 +1,6 @@
import { requireRole } from '@/lib/auth-redirect'
import { ObserverNav } from '@/components/layouts/observer-nav'
import { EditionProvider } from '@/components/observer/observer-edition-context'
export default async function ObserverLayout({
children,
@@ -10,13 +11,15 @@ export default async function ObserverLayout({
return (
<div className="min-h-screen bg-background">
<ObserverNav
user={{
name: session.user.name,
email: session.user.email,
}}
/>
<main className="container-app py-6">{children}</main>
<EditionProvider>
<ObserverNav
user={{
name: session.user.name,
email: session.user.email,
}}
/>
<main className="container-app py-6">{children}</main>
</EditionProvider>
</div>
)
}

View File

@@ -504,47 +504,90 @@ function JurorsTab({ selectedValue }: { selectedValue: string }) {
{isLoading ? (
<Skeleton className="h-[400px]" />
) : jurors.length > 0 ? (
<Card>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Juror</TableHead>
<TableHead className="text-right">Assigned</TableHead>
<TableHead className="text-right">Completed</TableHead>
<TableHead className="min-w-[140px]">Rate</TableHead>
<TableHead className="text-right">Avg Score</TableHead>
<TableHead className="text-right">Std Dev</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{jurors.map((j) => (
<TableRow key={j.userId}>
<TableCell className="font-medium">{j.name}</TableCell>
<TableCell className="text-right">{j.assigned}</TableCell>
<TableCell className="text-right">{j.completed}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Progress value={j.completionRate} className="h-2 w-20" />
<span className="text-sm tabular-nums">{j.completionRate}%</span>
</div>
</TableCell>
<TableCell className="text-right tabular-nums">{j.averageScore.toFixed(2)}</TableCell>
<TableCell className="text-right tabular-nums">{j.stddev.toFixed(2)}</TableCell>
<TableCell>
{j.isOutlier ? (
<Badge variant="destructive">Outlier</Badge>
) : (
<Badge variant="outline">Normal</Badge>
)}
</TableCell>
<>
{/* Desktop Table */}
<Card className="hidden md:block">
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Juror</TableHead>
<TableHead className="text-right">Assigned</TableHead>
<TableHead className="text-right">Completed</TableHead>
<TableHead className="min-w-[140px]">Rate</TableHead>
<TableHead className="text-right">Avg Score</TableHead>
<TableHead className="text-right">Std Dev</TableHead>
<TableHead>Status</TableHead>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</TableHeader>
<TableBody>
{jurors.map((j) => (
<TableRow key={j.userId}>
<TableCell className="font-medium">{j.name}</TableCell>
<TableCell className="text-right">{j.assigned}</TableCell>
<TableCell className="text-right">{j.completed}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Progress value={j.completionRate} className="h-2 w-20" />
<span className="text-sm tabular-nums">{j.completionRate}%</span>
</div>
</TableCell>
<TableCell className="text-right tabular-nums">{j.averageScore.toFixed(2)}</TableCell>
<TableCell className="text-right tabular-nums">{j.stddev.toFixed(2)}</TableCell>
<TableCell>
{j.isOutlier ? (
<Badge variant="destructive">Outlier</Badge>
) : (
<Badge variant="outline">Normal</Badge>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
{/* Mobile Cards */}
<div className="space-y-3 md:hidden">
{jurors.map((j) => (
<Card key={j.userId}>
<CardContent className="pt-4 space-y-3">
<div className="flex items-center justify-between">
<p className="font-medium text-sm truncate">{j.name}</p>
{j.isOutlier ? (
<Badge variant="destructive" className="shrink-0">Outlier</Badge>
) : (
<Badge variant="outline" className="shrink-0">Normal</Badge>
)}
</div>
<div className="flex items-center gap-2">
<Progress value={j.completionRate} className="h-2 flex-1" />
<span className="text-sm tabular-nums shrink-0">{j.completionRate}%</span>
</div>
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
<div className="flex justify-between">
<span className="text-muted-foreground">Assigned</span>
<span className="tabular-nums">{j.assigned}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Completed</span>
<span className="tabular-nums">{j.completed}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Avg Score</span>
<span className="tabular-nums">{j.averageScore.toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Std Dev</span>
<span className="tabular-nums">{j.stddev.toFixed(2)}</span>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</>
) : hasSelection ? (
<Card>
<CardContent className="flex items-center justify-center py-12">