import { useMemo, useState } from 'react' import { format, startOfWeek, addDays, addWeeks, parseISO, 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 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' } 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 CELL = 16 const GAP = 2 return (
20 weeks of deliverable density by status
{stats[statusKey]}
{STATUS_LABEL[statusKey]}
{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
) : ({deliverable.title}
{project.name}
+{tooltip.items.length - 5} more
)}