import { useMemo, useState } from 'react' import { format, startOfWeek, addDays, addWeeks, isToday } from 'date-fns' import useProjectStore from '../../store/useProjectStore' import useFocusStore from '../../store/useFocusStore' const WEEKS = 20 const DAY_INIT = ['M','T','W','T','F','S','S'] const CELL = 16 const CELL_LG = 40 const GAP_LG = 4 const GAP = 2 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', } const STATUS_CELL_COLORS = { upcoming: [ 'bg-blue-400/20 border-blue-400/30', 'bg-blue-400/55 border-blue-400/70', 'bg-blue-400 border-blue-400 shadow-[0_0_4px_rgba(96,165,250,0.6)]', ], in_progress: [ 'bg-amber-400/20 border-amber-400/30', 'bg-amber-400/55 border-amber-400/70', 'bg-amber-400 border-amber-400 shadow-[0_0_4px_rgba(251,191,36,0.6)]', ], completed: [ 'bg-green-400/20 border-green-400/30', 'bg-green-400/55 border-green-400/70', 'bg-green-400 border-green-400 shadow-[0_0_4px_rgba(74,222,128,0.6)]', ], overdue: [ 'bg-red-400/20 border-red-400/30', 'bg-red-400/55 border-red-400/70', 'bg-red-400 border-red-400 shadow-[0_0_4px_rgba(248,113,113,0.6)]', ], } const STATUS_HOVER_RING = { upcoming: 'hover:ring-1 hover:ring-blue-300/80', in_progress: 'hover:ring-1 hover:ring-amber-300/80', completed: 'hover:ring-1 hover:ring-green-300/80', overdue: 'hover:ring-1 hover:ring-red-400/90', } // Tie-break priority: overdue > in_progress > upcoming > completed const STATUS_PRIORITY = { overdue: 4, in_progress: 3, upcoming: 2, completed: 1 } function getCellClass(count, statusKey) { if (count === 0) return 'bg-surface border-surface-border' const colors = STATUS_CELL_COLORS[statusKey] || STATUS_CELL_COLORS.upcoming if (count === 1) return colors[0] if (count === 2) return colors[1] return colors[2] } function getDominantStatus(statusCounts) { let dominant = null let maxCount = 0 let maxPriority = 0 for (const [sk, count] of Object.entries(statusCounts)) { if (count === 0) continue if ( count > maxCount || (count === maxCount && (STATUS_PRIORITY[sk] || 0) > maxPriority) ) { dominant = sk maxCount = count maxPriority = STATUS_PRIORITY[sk] || 0 } } return { dominant, total: Object.values(statusCounts).reduce((a, b) => a + b, 0) } } export default function WorkloadHeatmap() { const projects = useProjectStore(s => s.projects) const openFocus = useFocusStore(s => s.openFocus) const [tooltip, setTooltip] = useState(null) const { weeks, stats } = useMemo(() => { const start = startOfWeek(addWeeks(new Date(), -10), { weekStartsOn: 1 }) const map = {} projects.forEach(p => { (p.deliverables || []).forEach(d => { 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]++ }) }) const grid = Array.from({ length: WEEKS }, (_, wi) => Array.from({ length: 7 }, (_, di) => { const date = addDays(start, wi * 7 + di) const key = format(date, 'yyyy-MM-dd') 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, 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, overdue: all.filter(d => d.status === 'overdue').length, } return { weeks: grid, stats } }, [projects]) const monthLabels = useMemo(() => { const labels = []; let last = -1 weeks.forEach((week, wi) => { const m = week[0].date.getMonth() if (m !== last) { labels.push({ wi, label: format(week[0].date, 'MMM') }); last = m } }) return labels }, [weeks]) const monthLabelsBig = useMemo(() => { const labels = []; let last = -1 weeks.forEach((week, wi) => { const m = week[0].date.getMonth() if (m !== last) { labels.push({ wi, label: format(week[0].date, 'MMM') }); last = m } }) return labels }, [weeks]) return (
{/* Header — pl-24 clears the fixed 64px navbar button at left-4 */}

Workload Heatmap

20 weeks of deliverable density by status

FABDASH
{/* Stat cards + per-status heatmaps */}
{STATUS_KEYS.map((statusKey) => (

{stats[statusKey]}

{STATUS_LABEL[statusKey]}

{DAY_INIT.map((d, i) => (
{d}
))}
{monthLabels.map(({ wi, label }) => ( {label} ))}
{weeks.map((week, wi) => (
{week.map(({ date, key, items, statusCounts }) => { const count = (statusCounts || {})[statusKey] || 0 return (
0 ? STATUS_HOVER_RING[statusKey] : ''} `} onClick={() => { if (!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) if (!filtered.length) return setTooltip({ x: e.clientX, y: e.clientY, date, statusKey, items: filtered }) }} onMouseLeave={() => setTooltip(null)} /> ) })}
))}
))}
{/* Combined heatmap */}

All Tasks

{stats.total} tasks
{STATUS_KEYS.map(sk => (
{STATUS_LABEL[sk]}
))} · color = highest count
{/* Day labels */}
{DAY_INIT.map((d, i) => (
{d}
))}
{/* Grid */}
{monthLabelsBig.map(({ wi, label }) => ( {label} ))}
{weeks.map((week, wi) => (
{week.map(({ date, key, items, statusCounts }) => { const { dominant, total } = getDominantStatus(statusCounts || {}) return (
{ if (!items?.length) return openFocus(items[0].project.id, items[0].deliverable.id) }} onMouseEnter={(e) => { if (!items?.length) return setTooltip({ x: e.clientX, y: e.clientY, date, statusKey: dominant, items, combined: true, statusCounts, }) }} onMouseLeave={() => setTooltip(null)} /> ) })}
))}
{/* Tooltip */} {tooltip && (

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

{tooltip.combined && tooltip.statusCounts ? (
{STATUS_KEYS.filter(sk => (tooltip.statusCounts[sk] || 0) > 0).map(sk => (
{STATUS_LABEL[sk]} {tooltip.statusCounts[sk]}
))}
) : (

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

)}
{(tooltip.items || []).slice(0, 5).map(({ deliverable, project }) => (

{deliverable.title}

{project.name}

))} {(tooltip.items || []).length > 5 && (

+{tooltip.items.length - 5} more

)}
)}
) }