From 78e1092aa918aafea3731add25099e707c7e6fd0 Mon Sep 17 00:00:00 2001 From: jasonMPM Date: Thu, 5 Mar 2026 17:05:47 -0600 Subject: [PATCH] Add files via upload --- .../components/Calendar/WorkloadHeatmap.jsx | 218 ++++++++++++------ 1 file changed, 143 insertions(+), 75 deletions(-) diff --git a/frontend/src/components/Calendar/WorkloadHeatmap.jsx b/frontend/src/components/Calendar/WorkloadHeatmap.jsx index 3cd383f..27226fa 100644 --- a/frontend/src/components/Calendar/WorkloadHeatmap.jsx +++ b/frontend/src/components/Calendar/WorkloadHeatmap.jsx @@ -1,16 +1,30 @@ import { useMemo, useState } from 'react' -import { format, startOfWeek, addDays, addWeeks, isSameDay, parseISO, isToday } from 'date-fns' +import { format, startOfWeek, addDays, addWeeks, parseISO, isToday } from 'date-fns' import useProjectStore from '../../store/useProjectStore' import useFocusStore from '../../store/useFocusStore' -import Badge from '../UI/Badge' -const WEEKS = 20 -const DAY_INIT = ['M','T','W','T','F','S','S'] +const WEEKS = 20 +const DAY_INIT = ['M','T','W','T','F','S','S'] -function getCellStyle(count) { - if (count === 0) return 'bg-surface border-surface-border' - if (count === 1) return 'bg-gold/25 border-gold/40' - if (count === 2) return 'bg-gold/55 border-gold/70' +const STATUS_KEYS = ['upcoming','in_progress','completed','overdue'] +const STATUS_LABEL = { + upcoming: 'Upcoming', + in_progress: 'In Progress', + completed: 'Completed', + overdue: 'Overdue', +} +const STATUS_COLOR = { + upcoming: 'text-blue-400', + in_progress: 'text-amber-400', + completed: 'text-green-400', + overdue: 'text-red-400', +} + +function getCellClass(baseDensity, statusCounts) { + const total = Object.values(statusCounts).reduce((a, b) => a + b, 0) + if (total === 0) return 'bg-surface border-surface-border' + if (baseDensity === 1) return 'bg-gold/25 border-gold/40' + if (baseDensity === 2) return 'bg-gold/55 border-gold/70' return 'bg-gold border-gold shadow-gold' } @@ -22,10 +36,14 @@ export default function WorkloadHeatmap() { const { weeks, stats } = useMemo(() => { const start = startOfWeek(addWeeks(new Date(), -10), { weekStartsOn: 1 }) const map = {} + projects.forEach(p => { (p.deliverables || []).forEach(d => { - if (!map[d.due_date]) map[d.due_date] = [] - map[d.due_date].push({ deliverable: d, project: p }) + const key = d.due_date + if (!map[key]) map[key] = { items: [], statusCounts: { upcoming: 0, in_progress: 0, completed: 0, overdue: 0 } } + map[key].items.push({ deliverable: d, project: p }) + const s = (d.status || 'upcoming') + if (map[key].statusCounts[s] !== undefined) map[key].statusCounts[s]++ }) }) @@ -33,17 +51,18 @@ export default function WorkloadHeatmap() { Array.from({ length: 7 }, (_, di) => { const date = addDays(start, wi * 7 + di) const key = format(date, 'yyyy-MM-dd') - return { date, key, items: map[key] || [] } + const entry = map[key] || { items: [], statusCounts: { upcoming: 0, in_progress: 0, completed: 0, overdue: 0 } } + return { date, key, ...entry } }) ) const all = projects.flatMap(p => p.deliverables || []) const stats = { total: all.length, - overdue: all.filter(d => d.status === 'overdue').length, + upcoming: all.filter(d => d.status === 'upcoming').length, in_progress: all.filter(d => d.status === 'in_progress').length, completed: all.filter(d => d.status === 'completed').length, - upcoming: all.filter(d => d.status === 'upcoming').length, + overdue: all.filter(d => d.status === 'overdue').length, } return { weeks: grid, stats } }, [projects]) @@ -57,90 +76,138 @@ export default function WorkloadHeatmap() { return labels }, [weeks]) - const CELL = 20, GAP = 3 + const CELL = 18 + const GAP = 3 return ( -
- {/* Header */} -
+
+ {/* Header with spacing that clears FABDASH corner logo */} +

Workload Heatmap

-

{WEEKS} weeks of deliverable density

+

20 weeks of deliverable density by status

-
- Less - {['bg-surface border-surface-border','bg-gold/25 border-gold/40','bg-gold/55 border-gold/70','bg-gold border-gold'].map((c,i) => ( -
- ))} - More +
+ FABDASH
- {/* Stat cards */} -
+ {/* Stat cards by status */} +
{[ - { label: 'Total', value: stats.total, color: 'text-text-primary' }, - { label: 'Upcoming', value: stats.upcoming, color: 'text-blue-400' }, - { label: 'In Progress', value: stats.in_progress, color: 'text-amber-400' }, - { label: 'Completed', value: stats.completed, color: 'text-green-400' }, - { label: 'Overdue', value: stats.overdue, color: 'text-red-400' }, - ].map(({ label, value, color }) => ( -
-

{value}

+ { key: 'total', label: 'Total', color: 'text-text-primary' }, + { key: 'upcoming', label: 'Upcoming', color: 'text-blue-400' }, + { key: 'in_progress', label: 'In Progress', color: 'text-amber-400' }, + { key: 'completed', label: 'Completed', color: 'text-green-400' }, + { key: 'overdue', label: 'Overdue', color: 'text-red-400' }, + ].map(({ key, label, color }) => ( +
+

{stats[key]}

{label}

))}
- {/* Heatmap grid */} -
- {/* Day labels */} -
- {DAY_INIT.map((d, i) => ( -
{d}
- ))} -
+ {/* Multi-row heatmaps by status */} +
+ {STATUS_KEYS.map((statusKey) => ( +
+
+
+

+ {STATUS_LABEL[statusKey]} +

+ + {stats[statusKey]} tasks + +
+
+ Less + {['bg-surface border-surface-border','bg-gold/25 border-gold/40','bg-gold/55 border-gold/70','bg-gold border-gold'].map((c,i) => ( +
+ ))} + More +
+
- {/* Grid */} -
- {/* Month labels */} -
- {monthLabels.map(({ wi, label }) => ( - - {label} - - ))} -
- {/* Week columns */} -
- {weeks.map((week, wi) => ( -
- {week.map(({ date, key, items }) => ( -
items.length > 0 && openFocus(items[0].project.id, items[0].deliverable.id)} - onMouseEnter={(e) => setTooltip({ x: e.clientX, y: e.clientY, date, items })} - onMouseLeave={() => setTooltip(null)} - /> +
+ {/* Day labels */} +
+ {DAY_INIT.map((d, i) => ( +
+ {d} +
))}
- ))} -
-
-
- {/* Tooltip */} + {/* Grid */} +
+ {/* Month labels */} +
+ {monthLabels.map(({ wi, label }) => ( + + {label} + + ))} +
+ +
+ {weeks.map((week, wi) => ( +
+ {week.map(({ date, key, items, statusCounts }) => { + const countForStatus = (statusCounts || {})[statusKey] || 0 + const baseDensity = countForStatus + return ( +
{ + if (!items || !items.length) return + const match = items.find(({ deliverable }) => deliverable.status === statusKey) || items[0] + if (match) openFocus(match.project.id, match.deliverable.id) + }} + onMouseEnter={(e) => { + const filtered = (items || []).filter(({ deliverable }) => deliverable.status === statusKey) + const showItems = filtered.length ? filtered : items || [] + setTooltip({ + x: e.clientX, + y: e.clientY, + date, + statusKey, + items: showItems, + }) + }} + onMouseLeave={() => setTooltip(null)} + /> + ) + })} +
+ ))} +
+
+
+
+ ))} + {tooltip && ( -
+

{isToday(tooltip.date) ? 'Today — ' : ''}{format(tooltip.date, 'EEE, MMM d, yyyy')}

+

+ {STATUS_LABEL[tooltip.statusKey]} · {tooltip.items.length} task{tooltip.items.length !== 1 ? 's' : ''} +

{tooltip.items.length === 0 ? (

No deliverables

) : ( @@ -161,6 +228,7 @@ export default function WorkloadHeatmap() { )}
)} +
) }