feat(bench): add Resource Bench Board page

Shows resources with available capacity in a selected date window.
- Filter by date range (with DateRangePresets), min hours/day slider, and free-text search
- Cards show role, chapter, available h/day with color-coded capacity bar
- Links to individual resource profiles
- "Bench" nav entry added to Resources section in AppShell

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 13:30:44 +02:00
parent 1df208dbcc
commit 607af1a857
4 changed files with 297 additions and 0 deletions
@@ -0,0 +1,101 @@
"use client";
import Link from "next/link";
interface BenchResourceCardProps {
id: string;
name: string;
eid: string;
role: string | null;
chapter: string | null;
availableHours: number;
availableHoursPerDay: number;
workingDays: number;
}
export function BenchResourceCard({
id,
name,
eid,
role,
chapter,
availableHours,
availableHoursPerDay,
}: BenchResourceCardProps) {
const initials = name
.split(" ")
.slice(0, 2)
.map((w) => w[0]?.toUpperCase() ?? "")
.join("");
const availabilityLevel =
availableHoursPerDay >= 6
? "high"
: availableHoursPerDay >= 3
? "medium"
: "low";
const levelClass =
availabilityLevel === "high"
? "border-emerald-300 dark:border-emerald-700 bg-emerald-50 dark:bg-emerald-950/20"
: availabilityLevel === "medium"
? "border-amber-300 dark:border-amber-700 bg-amber-50 dark:bg-amber-950/20"
: "border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900/20";
const barColor =
availabilityLevel === "high"
? "bg-emerald-500"
: availabilityLevel === "medium"
? "bg-amber-500"
: "bg-gray-400";
const barWidth = Math.min(100, Math.round((availableHoursPerDay / 8) * 100));
return (
<div className={`rounded-xl border p-4 space-y-3 ${levelClass}`}>
<div className="flex items-start gap-3">
<div className="h-10 w-10 shrink-0 rounded-full bg-brand-100 dark:bg-brand-900/40 flex items-center justify-center">
<span className="text-sm font-semibold text-brand-700 dark:text-brand-300">{initials}</span>
</div>
<div className="min-w-0 flex-1">
<div className="font-medium text-sm text-gray-900 dark:text-gray-100 truncate">{name}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">{eid}</div>
</div>
</div>
{(role ?? chapter) && (
<div className="flex flex-wrap gap-1.5">
{role && (
<span className="rounded-full bg-brand-100 dark:bg-brand-900/40 px-2 py-0.5 text-[11px] font-medium text-brand-700 dark:text-brand-300">
{role}
</span>
)}
{chapter && (
<span className="rounded-full bg-gray-100 dark:bg-gray-800 px-2 py-0.5 text-[11px] text-gray-600 dark:text-gray-400">
{chapter}
</span>
)}
</div>
)}
<div>
<div className="flex items-center justify-between text-xs mb-1">
<span className="text-gray-500 dark:text-gray-400">Available capacity</span>
<span className="font-semibold text-gray-800 dark:text-gray-200">
{availableHoursPerDay.toFixed(1)}h/day · {availableHours.toFixed(0)}h total
</span>
</div>
<div className="h-1.5 rounded-full bg-gray-200 dark:bg-gray-700 overflow-hidden">
<div className={`h-full rounded-full ${barColor}`} style={{ width: `${barWidth}%` }} />
</div>
</div>
<Link
href={`/resources/${id}`}
className="block w-full rounded-lg border border-gray-200 dark:border-gray-700 py-1.5 text-center text-xs font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
View Profile
</Link>
</div>
);
}