feat: timeline multi-select, demand popover, resource hover card, merged tooltips, dark mode fixes

Major timeline enhancements:
- Right-click drag multi-selection with floating action bar (batch delete/assign)
- DemandPopover for demand strip details (replaces broken "Loading" modal)
- ResourceHoverCard on name hover showing skills, rates, role, chapter
- Merged heatmap+vacation tooltips into unified TimelineTooltip component
- Fixed overbooking blink animation (date normalization, z-index ordering)
- Fixed dark mode sticky column bleed-through in project view
- System roles admin page, notification task management, performance review docs

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-03-18 23:43:51 +01:00
parent d0f04f13f8
commit ddec3a927a
67 changed files with 4930 additions and 1166 deletions
@@ -25,11 +25,11 @@ const proficiencyLabel: Record<number, string> = {
};
const proficiencyColor: Record<number, string> = {
1: "bg-gray-100 text-gray-600",
2: "bg-blue-50 text-blue-600",
3: "bg-brand-50 text-brand-700",
4: "bg-amber-50 text-amber-700",
5: "bg-green-50 text-green-700",
1: "bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300",
2: "bg-blue-50 text-blue-600 dark:bg-blue-900/50 dark:text-blue-300",
3: "bg-brand-50 text-brand-700 dark:bg-brand-900/50 dark:text-brand-200",
4: "bg-amber-50 text-amber-700 dark:bg-amber-900/50 dark:text-amber-300",
5: "bg-green-50 text-green-700 dark:bg-green-900/50 dark:text-green-300",
};
const vacationStatusColor: Record<string, string> = {
@@ -211,10 +211,10 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
<div className="flex items-start justify-between gap-4">
<div className="min-w-0">
<div className="flex items-center gap-3 flex-wrap">
<h1 className="text-2xl font-bold text-gray-900 truncate">{resource.displayName}</h1>
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100 truncate">{resource.displayName}</h1>
<span
className={`flex-shrink-0 px-2.5 py-0.5 text-xs font-medium rounded-full ${
resource.isActive ? "bg-green-100 text-green-700" : "bg-red-100 text-red-700"
resource.isActive ? "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300" : "bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300"
}`}
>
{resource.isActive ? "Active" : "Inactive"}
@@ -359,11 +359,11 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
{/* Profile meta (area role, portfolio, last import) */}
{(resourceWithMeta.areaRole || resourceWithMeta.portfolioUrl || resourceWithMeta.skillMatrixUpdatedAt) && (
<div className="bg-white rounded-xl border border-gray-200 p-4 flex flex-wrap gap-4 text-sm">
<div className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700 p-4 flex flex-wrap gap-4 text-sm">
{resourceWithMeta.areaRole && (
<div className="flex items-center gap-2">
<span className="text-gray-500 text-xs">Area:</span>
<span className="font-medium text-gray-800">{resourceWithMeta.areaRole.name}</span>
<span className="text-gray-500 dark:text-gray-400 text-xs">Area:</span>
<span className="font-medium text-gray-800 dark:text-gray-200">{resourceWithMeta.areaRole.name}</span>
</div>
)}
{resourceWithMeta.portfolioUrl && (
@@ -398,13 +398,13 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
{/* Main Skills Badges */}
{mainSkills.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h2 className="text-sm font-semibold text-gray-800 mb-3 flex items-center">Main Skills<InfoTooltip content="Up to 2 skills flagged as primary strengths. Used for staffing matching priority." /></h2>
<div className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700 p-5">
<h2 className="text-sm font-semibold text-gray-800 dark:text-gray-200 mb-3 flex items-center">Main Skills<InfoTooltip content="Up to 2 skills flagged as primary strengths. Used for staffing matching priority." /></h2>
<div className="flex flex-wrap gap-2">
{mainSkills.map((s) => (
<span
key={s.skill}
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-full bg-amber-50 text-amber-800 border border-amber-200"
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-full bg-amber-50 text-amber-800 border border-amber-200 dark:bg-amber-900/40 dark:text-amber-200 dark:border-amber-700"
>
<span className="text-amber-500"></span>
{s.skill}
@@ -422,8 +422,8 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
{/* Roles */}
{resourceRoles.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h2 className="text-sm font-semibold text-gray-800 mb-3 flex items-center">Roles<InfoTooltip content="Job functions assigned to this resource. The primary role is used in staffing and timeline displays." /></h2>
<div className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700 p-5">
<h2 className="text-sm font-semibold text-gray-800 dark:text-gray-200 mb-3 flex items-center">Roles<InfoTooltip content="Job functions assigned to this resource. The primary role is used in staffing and timeline displays." /></h2>
<div className="flex flex-wrap gap-2">
{resourceRoles.map((rr) => (
<span
@@ -445,13 +445,13 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
{/* Skills */}
{skills.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h2 className="text-sm font-semibold text-gray-800 mb-3 flex items-center">Skills<InfoTooltip content="Full skill inventory with proficiency level (1-5) and years of experience. Imported via skill matrix XLSX." /></h2>
<div className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700 p-5">
<h2 className="text-sm font-semibold text-gray-800 dark:text-gray-200 mb-3 flex items-center">Skills<InfoTooltip content="Full skill inventory with proficiency level (1-5) and years of experience. Imported via skill matrix XLSX." /></h2>
<div className="flex flex-wrap gap-2">
{skills.map((s) => (
<span
key={s.skill}
className="inline-flex items-center gap-1.5 px-3 py-1 text-sm rounded-full bg-gray-100 text-gray-700"
className="inline-flex items-center gap-1.5 px-3 py-1 text-sm rounded-full bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-200"
>
{s.skill}
{s.proficiency != null && (
@@ -464,7 +464,7 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
</span>
)}
{s.yearsExperience != null && (
<span className="text-xs text-gray-400">{s.yearsExperience}y</span>
<span className="text-xs text-gray-400 dark:text-gray-500">{s.yearsExperience}y</span>
)}
</span>
))}