import { useMemo, useState } from 'react' import { format, startOfWeek, addDays, addWeeks, isSameDay, 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'] 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' return 'bg-gold border-gold shadow-gold' } 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 => { if (!map[d.due_date]) map[d.due_date] = [] map[d.due_date].push({ deliverable: d, project: p }) }) }) 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') return { date, key, items: map[key] || [] } }) ) const all = projects.flatMap(p => p.deliverables || []) const stats = { total: all.length, overdue: all.filter(d => d.status === 'overdue').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, } 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 CELL = 20, GAP = 3 return (
{/* Header */}

Workload Heatmap

{WEEKS} weeks of deliverable density

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
{/* Stat cards */}
{[ { 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}

{label}

))}
{/* Heatmap grid */}
{/* Day labels */}
{DAY_INIT.map((d, i) => (
{d}
))}
{/* 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)} /> ))}
))}
{/* Tooltip */} {tooltip && (

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

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

No deliverables

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

{deliverable.title}

{project.name}

))} {tooltip.items.length > 5 && (

+{tooltip.items.length - 5} more

)}
)}
)}
) }