Merge pull request #26 from jasonMPM/fix-heatmap-again

Add files via upload
This commit is contained in:
jasonMPM
2026-03-05 17:44:53 -06:00
committed by GitHub

View File

@@ -56,7 +56,7 @@ export default function WorkloadHeatmap() {
}) })
) )
const all = projects.flatMap(p => p.deliverables || []) const all = projects.flatMap(p => p.deliverables || [])
const stats = { const stats = {
total: all.length, total: all.length,
upcoming: all.filter(d => d.status === 'upcoming').length, upcoming: all.filter(d => d.status === 'upcoming').length,
@@ -92,127 +92,128 @@ export default function WorkloadHeatmap() {
</div> </div>
</div> </div>
{/* Status heatmaps */} {/* Stat cards + aligned status heatmaps */}
<div className="flex flex-col gap-6 px-8 pb-8"> <div className="grid grid-cols-4 gap-4 px-8 pb-8">
{STATUS_KEYS.map((statusKey) => ( {STATUS_KEYS.map((statusKey) => (
<div key={statusKey} className="bg-surface-elevated/40 border border-surface-border rounded-xl p-4"> <div key={statusKey} className="flex flex-col gap-3">
<div className="flex items-center justify-between mb-3"> {/* Stat card */}
<div className="flex items-baseline gap-2"> <div className="bg-surface-elevated border border-surface-border rounded-xl p-4 text-center">
<h3 className="text-xs font-semibold tracking-widest uppercase text-text-muted/70"> <p className={`text-2xl font-bold ${STATUS_COLOR[statusKey]}`}>
{STATUS_LABEL[statusKey]} {stats[statusKey]}
</h3> </p>
<span className={`${STATUS_COLOR[statusKey]} text-[10px] font-mono`}> <p className="text-text-muted text-xs mt-1">{STATUS_LABEL[statusKey]}</p>
{stats[statusKey]} tasks
</span>
</div>
<div className="flex items-center gap-2 text-[9px] text-text-muted/60">
<span>Less</span>
{['bg-surface border-surface-border','bg-gold/25 border-gold/40','bg-gold/55 border-gold/70','bg-gold border-gold'].map((c,i) => (
<div key={i} className={`w-3 h-3 rounded-sm border ${c}`} />
))}
<span>More</span>
</div>
</div> </div>
<div className="flex gap-3 overflow-x-auto pb-2"> {/* Heatmap for this status */}
{/* Day labels */} <div className="bg-surface-elevated/40 border border-surface-border rounded-xl p-3 flex-1 min-h-[160px]">
<div className="flex flex-col flex-shrink-0 pt-6" style={{ gap: GAP }}> <div className="flex gap-2 overflow-x-auto pb-1">
{DAY_INIT.map((d, i) => ( {/* Day labels */}
<div key={i} style={{ height: CELL }} className="flex items-center text-[10px] text-text-muted/50 font-mono w-4"> <div className="flex flex-col flex-shrink-0 pt-6" style={{ gap: GAP }}>
{d} {DAY_INIT.map((d, i) => (
</div> <div key={i} style={{ height: CELL }} className="flex items-center text-[9px] text-text-muted/50 font-mono w-3">
))} {d}
</div> </div>
{/* Grid */}
<div className="flex flex-col flex-shrink-0">
{/* Month labels */}
<div className="relative h-5 mb-1">
{monthLabels.map(({ wi, label }) => (
<span
key={label+wi}
className="absolute text-[10px] text-text-muted/60 font-medium"
style={{ left: wi * (CELL + GAP) }}
>
{label}
</span>
))} ))}
</div> </div>
<div className="flex" style={{ gap: GAP }}> {/* Grid */}
{weeks.map((week, wi) => ( <div className="flex flex-col flex-shrink-0">
<div key={wi} className="flex flex-col flex-shrink-0" style={{ gap: GAP }}> {/* Month labels */}
{week.map(({ date, key, items, statusCounts }) => { <div className="relative h-4 mb-1">
const countForStatus = (statusCounts || {})[statusKey] || 0 {monthLabels.map(({ wi, label }) => (
const baseDensity = countForStatus <span
return ( key={label+wi}
<div className="absolute text-[9px] text-text-muted/60 font-medium"
key={key + statusKey} style={{ left: wi * (CELL + GAP) }}
style={{ width: CELL, height: CELL }} >
className={`rounded-sm border cursor-pointer transition-all hover:scale-125 hover:z-10 relative {label}
${getCellClass(baseDensity, statusCounts || {})} </span>
${isToday(date) ? 'ring-1 ring-white/40' : ''} ))}
`} </div>
onClick={() => {
if (!items || !items.length) return <div className="flex" style={{ gap: GAP }}>
const match = items.find(({ deliverable }) => deliverable.status === statusKey) || items[0] {weeks.map((week, wi) => (
if (match) openFocus(match.project.id, match.deliverable.id) <div key={wi} className="flex flex-col flex-shrink-0" style={{ gap: GAP }}>
}} {week.map(({ date, key, items, statusCounts }) => {
onMouseEnter={(e) => { const countForStatus = (statusCounts || {})[statusKey] || 0
const filtered = (items || []).filter(({ deliverable }) => deliverable.status === statusKey) const baseDensity = countForStatus
const showItems = filtered.length ? filtered : items || [] const hoverRing =
setTooltip({ statusKey === 'upcoming' ? 'hover:ring-blue-300/80 hover:shadow-[0_0_0_1px_rgba(147,197,253,0.8)]' :
x: e.clientX, statusKey === 'in_progress' ? 'hover:ring-amber-300/80 hover:shadow-[0_0_0_1px_rgba(252,211,77,0.8)]' :
y: e.clientY, statusKey === 'completed' ? 'hover:ring-green-300/80 hover:shadow-[0_0_0_1px_rgba(74,222,128,0.8)]' :
date, statusKey === 'overdue' ? 'hover:ring-red-400/90 hover:shadow-[0_0_0_1px_rgba(248,113,113,0.9)]' :
statusKey, 'hover:ring-white/60'
items: showItems, return (
}) <div
}} key={key + statusKey}
onMouseLeave={() => setTooltip(null)} style={{ width: CELL, height: CELL }}
/> className={`rounded-sm border cursor-pointer transition-transform duration-100 hover:scale-125 hover:z-10 relative
) ${getCellClass(baseDensity, statusCounts || {})}
})} ${isToday(date) ? 'ring-1 ring-white/60' : ''}
</div> ${hoverRing}
))} `}
onClick={() => {
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)}
/>
)
})}
</div>
))}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
))} ))}
{tooltip && (
<div
className="fixed z-[200] pointer-events-none bg-surface-elevated border border-surface-border rounded-xl shadow-2xl px-3.5 py-3 min-w-[220px] max-w-[320px]"
style={{ left: Math.min(tooltip.x + 14, window.innerWidth - 330), top: Math.max(tooltip.y - 100, 8) }}
>
<p className={`text-xs font-bold mb-1.5 ${isToday(tooltip.date) ? 'text-gold' : 'text-text-primary'}`}>
{isToday(tooltip.date) ? 'Today — ' : ''}{format(tooltip.date, 'EEE, MMM d, yyyy')}
</p>
<p className="text-[10px] text-text-muted/60 mb-1.5">
{STATUS_LABEL[tooltip.statusKey]} · {tooltip.items.length} task{tooltip.items.length !== 1 ? 's' : ''}
</p>
{tooltip.items.length === 0 ? (
<p className="text-text-muted/50 text-xs">No deliverables</p>
) : (
<div className="space-y-1.5">
{tooltip.items.slice(0, 5).map(({ deliverable, project }) => (
<div key={deliverable.id} className="flex items-start gap-1.5">
<div className="w-1.5 h-1.5 rounded-full mt-1 flex-shrink-0" style={{ backgroundColor: project.color }} />
<div className="min-w-0 flex-1">
<p className="text-[11px] text-text-primary truncate">{deliverable.title}</p>
<p className="text-[10px] text-text-muted/60">{project.name}</p>
</div>
</div>
))}
{tooltip.items.length > 5 && (
<p className="text-[10px] text-text-muted/50 pl-3">+{tooltip.items.length - 5} more</p>
)}
</div>
)}
</div>
)}
</div> </div>
{/* Tooltip */}
{tooltip && (
<div
className="fixed z-[200] pointer-events-none bg-surface-elevated border border-surface-border rounded-xl shadow-2xl px-3.5 py-3 min-w-[220px] max-w-[320px]"
style={{ left: Math.min(tooltip.x + 14, window.innerWidth - 330), top: Math.max(tooltip.y - 100, 8) }}
>
<p className={`text-xs font-bold mb-1.5 ${isToday(tooltip.date) ? 'text-gold' : 'text-text-primary'}`}>
{isToday(tooltip.date) ? 'Today \u2014 ' : ''}{format(tooltip.date, 'EEE, MMM d, yyyy')}
</p>
<p className="text-[10px] text-text-muted/60 mb-1.5">
{STATUS_LABEL[tooltip.statusKey]} \u00b7 {tooltip.items.length} task{tooltip.items.length !== 1 ? 's' : ''}
</p>
{tooltip.items.length === 0 ? (
<p className="text-text-muted/50 text-xs">No deliverables</p>
) : (
<div className="space-y-1.5">
{tooltip.items.slice(0, 5).map(({ deliverable, project }) => (
<div key={deliverable.id} className="flex items-start gap-1.5">
<div className="w-1.5 h-1.5 rounded-full mt-1 flex-shrink-0" style={{ backgroundColor: project.color }} />
<div className="min-w-0 flex-1">
<p className="text-[11px] text-text-primary truncate">{deliverable.title}</p>
<p className="text-[10px] text-text-muted/60">{project.name}</p>
</div>
</div>
))}
{tooltip.items.length > 5 && (
<p className="text-[10px] text-text-muted/50 pl-3">+{tooltip.items.length - 5} more</p>
)}
</div>
)}
</div>
)}
</div> </div>
) )
} }