feat: observer UX overhaul — reports, projects, charts, session & email
All checks were successful
Build and Push Docker Image / build (push) Successful in 11m2s

- Observer projects: default sort by status (rejected last), sortable status column
- Observer projects: search by country, institution, geographic zone
- Observer project detail: vertical timeline connectors between rounds
- Fix React key warning in ExpandableJurorTable and FilteringReportTabs
- Fix ScoreBadge text always white for better contrast on all backgrounds
- Remove misleading /30 denominator from heatmap juror reviewed count
- INTAKE stats: show Start-ups, Business Concepts, Countries (not States/Categories)
- DiversityMetrics: extractCountry() for country-only display in charts
- Fix nested button hydration error in filtering report mobile view
- Color project titles by outcome in filtering report (green/red/amber)
- Redesign CrossStageComparisonChart: funnel viz + metrics table with attrition %
- Center doughnut chart in StatusBreakdownChart
- Remove redundant RoundTypeStatsCards from evaluation report
- Move evaluation tab bar below overview header, rename to "Juror Assignments"
- Dev email override system (DEV_EMAIL_OVERRIDE env var)
- Session refresh on role change without re-login
- Role switcher in user dropdown menu
- formatCategory() utility for consistent category display
- Activity feed max height constraint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 13:37:50 +01:00
parent e7b99fff63
commit a556732b46
23 changed files with 2108 additions and 326 deletions

View File

@@ -12,6 +12,9 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Separator } from '@/components/ui/separator'
@@ -315,26 +318,6 @@ export function AdminSidebar({ user }: AdminSidebarProps) {
)}
</nav>
{/* Role Switcher — visible above user section */}
{switchableRoles.length > 0 && (
<div className="border-t px-3 py-2">
<p className="mb-1.5 flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/60">
<ArrowRightLeft className="h-3 w-3" />
Switch View
</p>
<div className="flex flex-wrap gap-1.5">
{switchableRoles.map(([, opt]) => (
<Link key={opt.path} href={opt.path as Route} onClick={() => setIsMobileMenuOpen(false)}>
<Button size="sm" variant="outline" className="h-7 gap-1.5 px-2.5 text-xs">
<opt.icon className="h-3 w-3" />
{opt.label}
</Button>
</Link>
))}
</div>
</div>
)}
{/* User Profile Section */}
<div className="border-t p-3">
<DropdownMenu>
@@ -393,23 +376,41 @@ export function AdminSidebar({ user }: AdminSidebarProps) {
{switchableRoles.length > 0 && (
<>
<DropdownMenuSeparator className="my-1" />
<div className="px-2 py-1.5">
<p className="flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground/60">
<ArrowRightLeft className="h-3 w-3" />
Switch View
</p>
</div>
{switchableRoles.map(([, opt]) => (
<DropdownMenuItem key={opt.path} asChild>
<Link
href={opt.path as Route}
className="flex cursor-pointer items-center gap-2.5 rounded-md px-2 py-2"
>
<opt.icon className="h-4 w-4 text-muted-foreground" />
<span>{opt.label}</span>
</Link>
</DropdownMenuItem>
))}
{switchableRoles.length <= 2 ? (
// Flat list for 1-2 roles
switchableRoles.map(([, opt]) => (
<DropdownMenuItem key={opt.path} asChild>
<Link
href={opt.path as Route}
className="flex cursor-pointer items-center gap-2.5 rounded-md px-2 py-2"
>
<opt.icon className="h-4 w-4 text-muted-foreground" />
<span>{opt.label}</span>
</Link>
</DropdownMenuItem>
))
) : (
// Submenu for 3+ roles
<DropdownMenuSub>
<DropdownMenuSubTrigger className="flex items-center gap-2.5 rounded-md px-2 py-2">
<ArrowRightLeft className="h-4 w-4 text-muted-foreground" />
<span>Switch View</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="min-w-[160px]">
{switchableRoles.map(([, opt]) => (
<DropdownMenuItem key={opt.path} asChild>
<Link
href={opt.path as Route}
className="flex cursor-pointer items-center gap-2.5 rounded-md px-2 py-2"
>
<opt.icon className="h-4 w-4 text-muted-foreground" />
<span>{opt.label}</span>
</Link>
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
)}
</>
)}