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:
@@ -23,10 +23,10 @@ function getDefaultDateRange(): { start: string; end: string } {
|
||||
function heatColor(hours: number, maxHours: number): string {
|
||||
if (hours === 0 || maxHours === 0) return "";
|
||||
const ratio = Math.min(hours / maxHours, 1);
|
||||
if (ratio < 0.25) return "bg-blue-50";
|
||||
if (ratio < 0.5) return "bg-blue-100";
|
||||
if (ratio < 0.75) return "bg-blue-200";
|
||||
return "bg-blue-300";
|
||||
if (ratio < 0.25) return "bg-blue-50 dark:bg-blue-900/20";
|
||||
if (ratio < 0.5) return "bg-blue-100 dark:bg-blue-900/30";
|
||||
if (ratio < 0.75) return "bg-blue-200 dark:bg-blue-900/40";
|
||||
return "bg-blue-300 dark:bg-blue-900/50";
|
||||
}
|
||||
|
||||
export function WeeklyPhasingView({ estimateId, canEdit }: WeeklyPhasingViewProps) {
|
||||
@@ -116,43 +116,43 @@ export function WeeklyPhasingView({ estimateId, canEdit }: WeeklyPhasingViewProp
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header / Controls */}
|
||||
<div className="rounded-3xl border border-gray-200 bg-white p-6">
|
||||
<h3 className="mb-4 text-lg font-semibold text-gray-900">
|
||||
<div className="rounded-3xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-6">
|
||||
<h3 className="mb-4 text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
Weekly Phasing (4Dispo)
|
||||
</h3>
|
||||
|
||||
{canEdit && (
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Start Date
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={data?.hasPhasing ? effectiveStart : startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
className="rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
||||
className="rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
End Date
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={data?.hasPhasing ? effectiveEnd : endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
className="rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
||||
className="rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Pattern
|
||||
</label>
|
||||
<select
|
||||
value={pattern}
|
||||
onChange={(e) => setPattern(e.target.value as PhasingPattern)}
|
||||
className="rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
||||
className="rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="even">Even Distribution</option>
|
||||
<option value="front_loaded">Front Loaded (60/40)</option>
|
||||
@@ -198,8 +198,8 @@ export function WeeklyPhasingView({ estimateId, canEdit }: WeeklyPhasingViewProp
|
||||
className={clsx(
|
||||
"rounded-lg px-3 py-1.5 text-sm font-medium",
|
||||
viewMode === "by_line"
|
||||
? "bg-sky-100 text-sky-700"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200",
|
||||
? "bg-sky-100 dark:bg-sky-900/40 text-sky-700 dark:text-sky-300"
|
||||
: "bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600",
|
||||
)}
|
||||
>
|
||||
By Line
|
||||
@@ -210,8 +210,8 @@ export function WeeklyPhasingView({ estimateId, canEdit }: WeeklyPhasingViewProp
|
||||
className={clsx(
|
||||
"rounded-lg px-3 py-1.5 text-sm font-medium",
|
||||
viewMode === "by_chapter"
|
||||
? "bg-sky-100 text-sky-700"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200",
|
||||
? "bg-sky-100 dark:bg-sky-900/40 text-sky-700 dark:text-sky-300"
|
||||
: "bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600",
|
||||
)}
|
||||
>
|
||||
By Chapter
|
||||
@@ -221,25 +221,25 @@ export function WeeklyPhasingView({ estimateId, canEdit }: WeeklyPhasingViewProp
|
||||
|
||||
{/* Phasing Grid */}
|
||||
{phasingQuery.isLoading && (
|
||||
<div className="rounded-3xl border border-gray-200 bg-white p-8 text-center text-gray-500">
|
||||
<div className="rounded-3xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-8 text-center text-gray-500 dark:text-gray-400">
|
||||
Loading phasing data...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data && !data.hasPhasing && (
|
||||
<div className="rounded-3xl border border-gray-200 bg-white p-8 text-center text-gray-500">
|
||||
<div className="rounded-3xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-8 text-center text-gray-500 dark:text-gray-400">
|
||||
No weekly phasing generated yet. Use the controls above to generate a
|
||||
phasing distribution.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data?.hasPhasing && viewMode === "by_line" && (
|
||||
<div className="rounded-3xl border border-gray-200 bg-white overflow-hidden">
|
||||
<div className="rounded-3xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200 bg-gray-50">
|
||||
<th className="sticky left-0 z-10 bg-gray-50 px-4 py-3 text-left font-medium text-gray-700 min-w-[200px]">
|
||||
<tr className="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
|
||||
<th className="sticky left-0 z-10 bg-gray-50 dark:bg-gray-900 px-4 py-3 text-left font-medium text-gray-700 dark:text-gray-300 min-w-[200px]">
|
||||
Demand Line
|
||||
</th>
|
||||
{data.weeks.map((week) => {
|
||||
@@ -247,13 +247,13 @@ export function WeeklyPhasingView({ estimateId, canEdit }: WeeklyPhasingViewProp
|
||||
return (
|
||||
<th
|
||||
key={key}
|
||||
className="px-3 py-3 text-right font-medium text-gray-600 min-w-[80px] whitespace-nowrap"
|
||||
className="px-3 py-3 text-right font-medium text-gray-600 dark:text-gray-400 min-w-[80px] whitespace-nowrap"
|
||||
>
|
||||
{week.label}
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
<th className="sticky right-0 z-10 bg-gray-50 px-4 py-3 text-right font-semibold text-gray-700 min-w-[90px]">
|
||||
<th className="sticky right-0 z-10 bg-gray-50 dark:bg-gray-900 px-4 py-3 text-right font-semibold text-gray-700 dark:text-gray-300 min-w-[90px]">
|
||||
Total
|
||||
</th>
|
||||
</tr>
|
||||
@@ -267,14 +267,14 @@ export function WeeklyPhasingView({ estimateId, canEdit }: WeeklyPhasingViewProp
|
||||
return (
|
||||
<tr
|
||||
key={line.id}
|
||||
className="border-b border-gray-100 hover:bg-gray-50/50"
|
||||
className="border-b border-gray-100 dark:border-gray-700/50 hover:bg-gray-50/50 dark:hover:bg-gray-700/30"
|
||||
>
|
||||
<td className="sticky left-0 z-10 bg-white px-4 py-2 font-medium text-gray-900">
|
||||
<td className="sticky left-0 z-10 bg-white dark:bg-gray-800 px-4 py-2 font-medium text-gray-900 dark:text-gray-100">
|
||||
<div className="truncate max-w-[200px]" title={line.name}>
|
||||
{line.name}
|
||||
</div>
|
||||
{line.chapter && (
|
||||
<div className="text-xs text-gray-500">
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{line.chapter}
|
||||
</div>
|
||||
)}
|
||||
@@ -286,7 +286,7 @@ export function WeeklyPhasingView({ estimateId, canEdit }: WeeklyPhasingViewProp
|
||||
<td
|
||||
key={key}
|
||||
className={clsx(
|
||||
"px-3 py-2 text-right tabular-nums",
|
||||
"px-3 py-2 text-right tabular-nums text-gray-700 dark:text-gray-200",
|
||||
heatColor(hours, maxHours),
|
||||
)}
|
||||
>
|
||||
@@ -294,7 +294,7 @@ export function WeeklyPhasingView({ estimateId, canEdit }: WeeklyPhasingViewProp
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
<td className="sticky right-0 z-10 bg-white px-4 py-2 text-right font-semibold tabular-nums text-gray-900">
|
||||
<td className="sticky right-0 z-10 bg-white dark:bg-gray-800 px-4 py-2 text-right font-semibold tabular-nums text-gray-900 dark:text-gray-100">
|
||||
{lineTotal.toFixed(1)}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -302,8 +302,8 @@ export function WeeklyPhasingView({ estimateId, canEdit }: WeeklyPhasingViewProp
|
||||
})}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="border-t-2 border-gray-300 bg-gray-50 font-semibold">
|
||||
<td className="sticky left-0 z-10 bg-gray-50 px-4 py-3 text-gray-700">
|
||||
<tr className="border-t-2 border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 font-semibold">
|
||||
<td className="sticky left-0 z-10 bg-gray-50 dark:bg-gray-900 px-4 py-3 text-gray-700 dark:text-gray-300">
|
||||
Total
|
||||
</td>
|
||||
{data.weeks.map((week) => {
|
||||
@@ -312,13 +312,13 @@ export function WeeklyPhasingView({ estimateId, canEdit }: WeeklyPhasingViewProp
|
||||
return (
|
||||
<td
|
||||
key={key}
|
||||
className="px-3 py-3 text-right tabular-nums text-gray-900"
|
||||
className="px-3 py-3 text-right tabular-nums text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
{total > 0 ? total.toFixed(1) : "-"}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
<td className="sticky right-0 z-10 bg-gray-50 px-4 py-3 text-right tabular-nums text-gray-900">
|
||||
<td className="sticky right-0 z-10 bg-gray-50 dark:bg-gray-900 px-4 py-3 text-right tabular-nums text-gray-900 dark:text-gray-100">
|
||||
{Object.values(columnTotals)
|
||||
.reduce((sum, h) => sum + h, 0)
|
||||
.toFixed(1)}
|
||||
@@ -331,12 +331,12 @@ export function WeeklyPhasingView({ estimateId, canEdit }: WeeklyPhasingViewProp
|
||||
)}
|
||||
|
||||
{data?.hasPhasing && viewMode === "by_chapter" && (
|
||||
<div className="rounded-3xl border border-gray-200 bg-white overflow-hidden">
|
||||
<div className="rounded-3xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200 bg-gray-50">
|
||||
<th className="sticky left-0 z-10 bg-gray-50 px-4 py-3 text-left font-medium text-gray-700 min-w-[200px]">
|
||||
<tr className="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
|
||||
<th className="sticky left-0 z-10 bg-gray-50 dark:bg-gray-900 px-4 py-3 text-left font-medium text-gray-700 dark:text-gray-300 min-w-[200px]">
|
||||
Chapter
|
||||
</th>
|
||||
{data.weeks.map((week) => {
|
||||
@@ -344,13 +344,13 @@ export function WeeklyPhasingView({ estimateId, canEdit }: WeeklyPhasingViewProp
|
||||
return (
|
||||
<th
|
||||
key={key}
|
||||
className="px-3 py-3 text-right font-medium text-gray-600 min-w-[80px] whitespace-nowrap"
|
||||
className="px-3 py-3 text-right font-medium text-gray-600 dark:text-gray-400 min-w-[80px] whitespace-nowrap"
|
||||
>
|
||||
{week.label}
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
<th className="sticky right-0 z-10 bg-gray-50 px-4 py-3 text-right font-semibold text-gray-700 min-w-[90px]">
|
||||
<th className="sticky right-0 z-10 bg-gray-50 dark:bg-gray-900 px-4 py-3 text-right font-semibold text-gray-700 dark:text-gray-300 min-w-[90px]">
|
||||
Total
|
||||
</th>
|
||||
</tr>
|
||||
@@ -366,9 +366,9 @@ export function WeeklyPhasingView({ estimateId, canEdit }: WeeklyPhasingViewProp
|
||||
return (
|
||||
<tr
|
||||
key={chapter}
|
||||
className="border-b border-gray-100 hover:bg-gray-50/50"
|
||||
className="border-b border-gray-100 dark:border-gray-700/50 hover:bg-gray-50/50 dark:hover:bg-gray-700/30"
|
||||
>
|
||||
<td className="sticky left-0 z-10 bg-white px-4 py-2 font-medium text-gray-900">
|
||||
<td className="sticky left-0 z-10 bg-white dark:bg-gray-800 px-4 py-2 font-medium text-gray-900 dark:text-gray-100">
|
||||
{chapter}
|
||||
</td>
|
||||
{data.weeks.map((week) => {
|
||||
@@ -378,7 +378,7 @@ export function WeeklyPhasingView({ estimateId, canEdit }: WeeklyPhasingViewProp
|
||||
<td
|
||||
key={key}
|
||||
className={clsx(
|
||||
"px-3 py-2 text-right tabular-nums",
|
||||
"px-3 py-2 text-right tabular-nums text-gray-700 dark:text-gray-200",
|
||||
heatColor(hours, maxChapterHours),
|
||||
)}
|
||||
>
|
||||
@@ -386,7 +386,7 @@ export function WeeklyPhasingView({ estimateId, canEdit }: WeeklyPhasingViewProp
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
<td className="sticky right-0 z-10 bg-white px-4 py-2 text-right font-semibold tabular-nums text-gray-900">
|
||||
<td className="sticky right-0 z-10 bg-white dark:bg-gray-800 px-4 py-2 text-right font-semibold tabular-nums text-gray-900 dark:text-gray-100">
|
||||
{chapterTotal.toFixed(1)}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -394,8 +394,8 @@ export function WeeklyPhasingView({ estimateId, canEdit }: WeeklyPhasingViewProp
|
||||
})}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="border-t-2 border-gray-300 bg-gray-50 font-semibold">
|
||||
<td className="sticky left-0 z-10 bg-gray-50 px-4 py-3 text-gray-700">
|
||||
<tr className="border-t-2 border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 font-semibold">
|
||||
<td className="sticky left-0 z-10 bg-gray-50 dark:bg-gray-900 px-4 py-3 text-gray-700 dark:text-gray-300">
|
||||
Total
|
||||
</td>
|
||||
{data.weeks.map((week) => {
|
||||
@@ -404,13 +404,13 @@ export function WeeklyPhasingView({ estimateId, canEdit }: WeeklyPhasingViewProp
|
||||
return (
|
||||
<td
|
||||
key={key}
|
||||
className="px-3 py-3 text-right tabular-nums text-gray-900"
|
||||
className="px-3 py-3 text-right tabular-nums text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
{total > 0 ? total.toFixed(1) : "-"}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
<td className="sticky right-0 z-10 bg-gray-50 px-4 py-3 text-right tabular-nums text-gray-900">
|
||||
<td className="sticky right-0 z-10 bg-gray-50 dark:bg-gray-900 px-4 py-3 text-right tabular-nums text-gray-900 dark:text-gray-100">
|
||||
{Object.values(chapterColumnTotals)
|
||||
.reduce((sum, h) => sum + h, 0)
|
||||
.toFixed(1)}
|
||||
@@ -424,8 +424,8 @@ export function WeeklyPhasingView({ estimateId, canEdit }: WeeklyPhasingViewProp
|
||||
|
||||
{/* Info about current phasing config */}
|
||||
{data?.hasPhasing && data.config && (
|
||||
<div className="rounded-3xl border border-gray-200 bg-white p-4">
|
||||
<p className="text-sm text-gray-600">
|
||||
<div className="rounded-3xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
<span className="font-medium">Current phasing:</span>{" "}
|
||||
{data.config.pattern.replace("_", " ")} distribution from{" "}
|
||||
{data.config.startDate} to {data.config.endDate} across{" "}
|
||||
|
||||
Reference in New Issue
Block a user