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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user