Merge pull request #36 from jasonMPM/pr2

Add files via upload
This commit is contained in:
jasonMPM
2026-03-06 00:28:55 -06:00
committed by GitHub
3 changed files with 45 additions and 67 deletions

View File

@@ -9,7 +9,12 @@ import DeliverableModal from '../Deliverables/DeliverableModal'
const STATUS_KEYS = ['overdue', 'in_progress', 'upcoming', 'completed'] const STATUS_KEYS = ['overdue', 'in_progress', 'upcoming', 'completed']
const STATUS_LABEL = { upcoming: 'Upcoming', in_progress: 'In Progress', completed: 'Completed', overdue: '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_COLOR = { upcoming: 'text-blue-400', in_progress: 'text-amber-400', completed: 'text-green-400', overdue: 'text-red-400' }
const STATUS_BG = { upcoming: 'bg-blue-400/10 border-blue-400/30 hover:bg-blue-400/20', in_progress: 'bg-amber-400/10 border-amber-400/30 hover:bg-amber-400/20', completed: 'bg-green-400/10 border-green-400/30 hover:bg-green-400/20', overdue: 'bg-red-400/10 border-red-400/30 hover:bg-red-400/20' } const STATUS_BG = {
upcoming: 'bg-blue-400/10 border-blue-400/30 hover:bg-blue-400/20',
in_progress: 'bg-amber-400/10 border-amber-400/30 hover:bg-amber-400/20',
completed: 'bg-green-400/10 border-green-400/30 hover:bg-green-400/20',
overdue: 'bg-red-400/10 border-red-400/30 hover:bg-red-400/20',
}
const STATUS_CYCLE = { upcoming: 'in_progress', in_progress: 'completed', completed: 'upcoming', overdue: 'in_progress' } const STATUS_CYCLE = { upcoming: 'in_progress', in_progress: 'completed', completed: 'upcoming', overdue: 'in_progress' }
export default function HeatmapDayPanel({ date, onClose }) { export default function HeatmapDayPanel({ date, onClose }) {
@@ -23,7 +28,7 @@ export default function HeatmapDayPanel({ date, onClose }) {
const dateStr = format(date, 'yyyy-MM-dd') const dateStr = format(date, 'yyyy-MM-dd')
// Derive items live from store so status cycles update instantly // Derive live from store so status cycles re-render instantly
const items = projects.flatMap(p => const items = projects.flatMap(p =>
(p.deliverables || []) (p.deliverables || [])
.filter(d => d.due_date === dateStr) .filter(d => d.due_date === dateStr)
@@ -48,27 +53,19 @@ export default function HeatmapDayPanel({ date, onClose }) {
} }
} }
const handleJumpToCalendar = () => {
jumpToCalendarDate(dateStr)
onClose()
}
return ( return (
<> <>
{/* Backdrop */} {/* Backdrop */}
<div <div className="fixed inset-0 z-40 bg-black/40 backdrop-blur-[1px]" onClick={onClose} />
className="fixed inset-0 z-40 bg-black/40 backdrop-blur-[1px]"
onClick={onClose}
/>
{/* Slide-in panel */} {/* Slide-in panel */}
<div className="fixed right-0 top-0 h-full w-80 z-50 bg-surface-elevated border-l border-surface-border flex flex-col shadow-2xl animate-[slide-up_0.2s_ease-out]"> <div className="fixed right-0 top-0 h-full w-80 z-50 bg-surface-elevated border-l border-surface-border flex flex-col shadow-2xl">
{/* Header */} {/* Header */}
<div className="flex items-start justify-between px-4 pt-4 pb-3 border-b border-surface-border flex-shrink-0"> <div className="flex items-start justify-between px-4 pt-4 pb-3 border-b border-surface-border flex-shrink-0">
<div> <div>
<p className={`text-sm font-bold leading-snug ${isToday(date) ? 'text-gold' : 'text-text-primary'}`}> <p className={`text-sm font-bold leading-snug ${isToday(date) ? 'text-gold' : 'text-text-primary'}`}>
{isToday(date) ? '\u2605 Today' : format(date, 'EEEE')} {isToday(date) ? '★ Today' : format(date, 'EEEE')}
</p> </p>
<p className="text-text-muted text-xs">{format(date, 'MMMM d, yyyy')}</p> <p className="text-text-muted text-xs">{format(date, 'MMMM d, yyyy')}</p>
<p className="text-text-muted/50 text-[10px] mt-0.5"> <p className="text-text-muted/50 text-[10px] mt-0.5">
@@ -77,17 +74,18 @@ export default function HeatmapDayPanel({ date, onClose }) {
</div> </div>
<div className="flex items-center gap-1.5 mt-0.5"> <div className="flex items-center gap-1.5 mt-0.5">
<button <button
onClick={handleJumpToCalendar} onClick={() => { jumpToCalendarDate(dateStr); onClose() }}
className="text-[10px] px-2 py-1.5 rounded-lg border border-surface-border bg-surface text-text-muted hover:text-gold hover:border-gold/40 transition-all" className="text-[10px] px-2 py-1.5 rounded-lg border border-surface-border bg-surface text-text-muted hover:text-gold hover:border-gold/40 transition-all"
title="Switch to calendar and jump to this date" title="Switch to calendar and jump to this date"
> >
\u2192 Calendar &rarr; Calendar
</button> </button>
<button <button
onClick={onClose} onClick={onClose}
className="text-text-muted hover:text-text-primary transition-colors text-base w-7 h-7 flex items-center justify-center rounded hover:bg-surface-border/40" className="text-text-muted hover:text-text-primary transition-colors text-base w-7 h-7 flex items-center justify-center rounded hover:bg-surface-border/40"
title="Close"
> >
\u2715 &times;
</button> </button>
</div> </div>
</div> </div>
@@ -106,14 +104,13 @@ export default function HeatmapDayPanel({ date, onClose }) {
<div className="flex-1 overflow-y-auto px-3 py-3 space-y-4"> <div className="flex-1 overflow-y-auto px-3 py-3 space-y-4">
{items.length === 0 ? ( {items.length === 0 ? (
<div className="flex flex-col items-center justify-center py-10 text-center"> <div className="flex flex-col items-center justify-center py-10 text-center">
<div className="text-3xl mb-3 opacity-20">\u25a1</div> <div className="text-3xl mb-3 opacity-20">&#9633;</div>
<p className="text-text-muted/60 text-sm">Nothing scheduled</p> <p className="text-text-muted/60 text-sm">Nothing scheduled</p>
<p className="text-text-muted/30 text-xs mt-1">Click + Schedule above to add one</p> <p className="text-text-muted/30 text-xs mt-1">Click + Schedule above to add one</p>
</div> </div>
) : ( ) : (
STATUS_KEYS.filter(k => grouped[k].length > 0).map(statusKey => ( STATUS_KEYS.filter(k => grouped[k].length > 0).map(statusKey => (
<div key={statusKey}> <div key={statusKey}>
{/* Status group header */}
<div className="flex items-center gap-2 mb-1.5 px-0.5"> <div className="flex items-center gap-2 mb-1.5 px-0.5">
<span className={`text-[10px] font-bold uppercase tracking-widest ${STATUS_COLOR[statusKey]}`}> <span className={`text-[10px] font-bold uppercase tracking-widest ${STATUS_COLOR[statusKey]}`}>
{STATUS_LABEL[statusKey]} {STATUS_LABEL[statusKey]}
@@ -129,19 +126,11 @@ export default function HeatmapDayPanel({ date, onClose }) {
key={deliverable.id} key={deliverable.id}
className="flex items-center gap-2 bg-surface rounded-lg px-2.5 py-2 border border-surface-border hover:border-gold/20 transition-colors group" className="flex items-center gap-2 bg-surface rounded-lg px-2.5 py-2 border border-surface-border hover:border-gold/20 transition-colors group"
> >
{/* Project color dot */} <div className="w-2 h-2 rounded-full flex-shrink-0" style={{ backgroundColor: project.color }} />
<div
className="w-2 h-2 rounded-full flex-shrink-0"
style={{ backgroundColor: project.color }}
/>
{/* Title + project */}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-xs text-text-primary truncate leading-snug">{deliverable.title}</p> <p className="text-xs text-text-primary truncate leading-snug">{deliverable.title}</p>
<p className="text-[10px] text-text-muted/50 truncate">{project.name}</p> <p className="text-[10px] text-text-muted/50 truncate">{project.name}</p>
</div> </div>
{/* Actions */}
<div className="flex items-center gap-1 flex-shrink-0 opacity-60 group-hover:opacity-100 transition-opacity"> <div className="flex items-center gap-1 flex-shrink-0 opacity-60 group-hover:opacity-100 transition-opacity">
{/* Cycle status */} {/* Cycle status */}
<button <button
@@ -150,7 +139,7 @@ export default function HeatmapDayPanel({ date, onClose }) {
className={`text-[10px] w-6 h-6 flex items-center justify-center rounded border transition-all ${STATUS_COLOR[statusKey]} border-current/30 hover:border-current hover:bg-current/10 disabled:opacity-30`} className={`text-[10px] w-6 h-6 flex items-center justify-center rounded border transition-all ${STATUS_COLOR[statusKey]} border-current/30 hover:border-current hover:bg-current/10 disabled:opacity-30`}
title={`Mark as ${STATUS_LABEL[STATUS_CYCLE[deliverable.status]]}`} title={`Mark as ${STATUS_LABEL[STATUS_CYCLE[deliverable.status]]}`}
> >
{cycling === deliverable.id ? '\u2026' : '\u27f3'} {cycling === deliverable.id ? '' : ''}
</button> </button>
{/* Open Focus View */} {/* Open Focus View */}
<button <button
@@ -158,7 +147,7 @@ export default function HeatmapDayPanel({ date, onClose }) {
className="text-[10px] w-6 h-6 flex items-center justify-center rounded border border-surface-border text-text-muted hover:text-gold hover:border-gold/40 transition-all" className="text-[10px] w-6 h-6 flex items-center justify-center rounded border border-surface-border text-text-muted hover:text-gold hover:border-gold/40 transition-all"
title="Open Focus View" title="Open Focus View"
> >
\u25ce
</button> </button>
</div> </div>
</div> </div>
@@ -169,10 +158,10 @@ export default function HeatmapDayPanel({ date, onClose }) {
)} )}
</div> </div>
{/* Footer: cycle legend */} {/* Footer legend */}
<div className="flex-shrink-0 px-4 py-2.5 border-t border-surface-border/50"> <div className="flex-shrink-0 px-4 py-2.5 border-t border-surface-border/50">
<p className="text-[9px] text-text-muted/30 text-center"> <p className="text-[9px] text-text-muted/30 text-center">
\u27f3 cycles status &nbsp;\u00b7&nbsp; \u25ce opens Focus View &nbsp;\u00b7&nbsp; \u2192 Calendar jumps to date cycles status &nbsp;&middot;&nbsp; opens Focus View &nbsp;&middot;&nbsp; &rarr; Calendar jumps to date
</p> </p>
</div> </div>
</div> </div>

View File

@@ -69,7 +69,6 @@ export default function MainCalendar({ onCalendarReady }) {
openFocus(projectId, deliverableId) openFocus(projectId, deliverableId)
}, [openFocus]) }, [openFocus])
// Drag-and-drop with 30-second undo toast
const handleEventDrop = useCallback(async ({ event, oldEvent }) => { const handleEventDrop = useCallback(async ({ event, oldEvent }) => {
const { deliverableId } = event.extendedProps const { deliverableId } = event.extendedProps
const newDate = event.startStr.substring(0, 10) const newDate = event.startStr.substring(0, 10)
@@ -117,11 +116,11 @@ export default function MainCalendar({ onCalendarReady }) {
setContextMenu({ setContextMenu({
x: e.clientX, y: e.clientY, x: e.clientX, y: e.clientY,
items: [ items: [
{ icon: '\u2714\ufe0e', label: 'Edit Deliverable', action: () => setModal({ open: true, deliverable, defaultDate: '' }) }, { icon: '✔︎', label: 'Edit Deliverable', action: () => setModal({ open: true, deliverable, defaultDate: '' }) },
{ icon: '\u2756', label: 'Open Focus View', action: () => openFocus(projectId, deliverableId) }, { icon: '', label: 'Open Focus View', action: () => openFocus(projectId, deliverableId) },
...(project?.drive_url ? [{ icon: '\u2b21', label: 'Open Drive Folder', action: () => window.open(project.drive_url, '_blank') }] : []), ...(project?.drive_url ? [{ icon: '', label: 'Open Drive Folder', action: () => window.open(project.drive_url, '_blank') }] : []),
{ separator: true }, { separator: true },
{ icon: '\u2715', label: 'Delete Deliverable', danger: true, { icon: '', label: 'Delete Deliverable', danger: true,
action: async () => { action: async () => {
if (window.confirm(`Delete "${deliverable.title}"?`)) { if (window.confirm(`Delete "${deliverable.title}"?`)) {
await deleteDeliverable(deliverableId) await deleteDeliverable(deliverableId)
@@ -147,7 +146,7 @@ export default function MainCalendar({ onCalendarReady }) {
: 'bg-surface-elevated border-surface-border text-text-muted hover:border-gold/40 hover:text-gold' : 'bg-surface-elevated border-surface-border text-text-muted hover:border-gold/40 hover:text-gold'
}`} }`}
> >
{showHeatmap ? '\u2190 Calendar' : '\u2b21 Heatmap'} {showHeatmap ? ' Calendar' : ' Heatmap'}
</button> </button>
</div> </div>

View File

@@ -49,7 +49,6 @@ function getDominantStatus(statusCounts) {
return { dominant, total: Object.values(statusCounts).reduce((a, b) => a + b, 0) } return { dominant, total: Object.values(statusCounts).reduce((a, b) => a + b, 0) }
} }
// 4-cell density legend strip
function DensityLegend({ statusKey }) { function DensityLegend({ statusKey }) {
const c = STATUS_CELL_COLORS[statusKey] const c = STATUS_CELL_COLORS[statusKey]
return ( return (
@@ -67,8 +66,8 @@ function DensityLegend({ statusKey }) {
export default function WorkloadHeatmap() { export default function WorkloadHeatmap() {
const projects = useProjectStore(s => s.projects) const projects = useProjectStore(s => s.projects)
const [tooltip, setTooltip] = useState(null) const [tooltip, setTooltip] = useState(null)
const [selectedDay, setSelectedDay] = useState(null) // Date | null const [selectedDay, setSelectedDay] = useState(null)
const [weekOffset, setWeekOffset] = useState(0) // shifts window in weeks const [weekOffset, setWeekOffset] = useState(0)
const { weeks, stats } = useMemo(() => { const { weeks, stats } = useMemo(() => {
const start = startOfWeek(addWeeks(new Date(), -10 + weekOffset), { weekStartsOn: 1 }) const start = startOfWeek(addWeeks(new Date(), -10 + weekOffset), { weekStartsOn: 1 })
@@ -107,7 +106,7 @@ export default function WorkloadHeatmap() {
const windowStart = weeks[0]?.[0]?.date const windowStart = weeks[0]?.[0]?.date
const windowEnd = weeks[WEEKS - 1]?.[6]?.date const windowEnd = weeks[WEEKS - 1]?.[6]?.date
const windowLabel = windowStart && windowEnd const windowLabel = windowStart && windowEnd
? `${format(windowStart, 'MMM d')} \u2013 ${format(windowEnd, 'MMM d, yyyy')}` ? `${format(windowStart, 'MMM d')} ${format(windowEnd, 'MMM d, yyyy')}`
: '' : ''
const monthLabels = useMemo(() => { const monthLabels = useMemo(() => {
@@ -131,20 +130,18 @@ export default function WorkloadHeatmap() {
return ( return (
<div className="flex flex-col h-full bg-surface overflow-auto"> <div className="flex flex-col h-full bg-surface overflow-auto">
{/* Header */} {/* Header + window navigation */}
<div className="flex items-center justify-between pl-24 pr-8 pt-6 pb-3"> <div className="flex items-center justify-between pl-24 pr-8 pt-6 pb-3">
<div> <div>
<h2 className="text-gold font-bold text-lg tracking-wide">Workload Heatmap</h2> <h2 className="text-gold font-bold text-lg tracking-wide">Workload Heatmap</h2>
<p className="text-text-muted text-xs mt-0.5">20 weeks of deliverable density by status</p> <p className="text-text-muted text-xs mt-0.5">20 weeks of deliverable density by status</p>
</div> </div>
{/* Window navigation */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => setWeekOffset(o => o - 4)} onClick={() => setWeekOffset(o => o - 4)}
className="text-xs px-2.5 py-1.5 rounded-lg border border-surface-border bg-surface-elevated text-text-muted hover:text-gold hover:border-gold/40 transition-all" className="text-xs px-2.5 py-1.5 rounded-lg border border-surface-border bg-surface-elevated text-text-muted hover:text-gold hover:border-gold/40 transition-all"
title="Shift window back 4 weeks" title="Shift window back 4 weeks"
>\u2190 4 wks</button> >&larr; 4 wks</button>
<button <button
onClick={() => setWeekOffset(0)} onClick={() => setWeekOffset(0)}
@@ -160,7 +157,7 @@ export default function WorkloadHeatmap() {
onClick={() => setWeekOffset(o => o + 4)} onClick={() => setWeekOffset(o => o + 4)}
className="text-xs px-2.5 py-1.5 rounded-lg border border-surface-border bg-surface-elevated text-text-muted hover:text-gold hover:border-gold/40 transition-all" className="text-xs px-2.5 py-1.5 rounded-lg border border-surface-border bg-surface-elevated text-text-muted hover:text-gold hover:border-gold/40 transition-all"
title="Shift window forward 4 weeks" title="Shift window forward 4 weeks"
>4 wks \u2192</button> >4 wks &rarr;</button>
<span className="text-[10px] text-text-muted/40 font-mono ml-1 hidden xl:block">{windowLabel}</span> <span className="text-[10px] text-text-muted/40 font-mono ml-1 hidden xl:block">{windowLabel}</span>
</div> </div>
@@ -170,27 +167,21 @@ export default function WorkloadHeatmap() {
<div className="grid grid-cols-4 gap-4 px-8 pb-4"> <div className="grid grid-cols-4 gap-4 px-8 pb-4">
{STATUS_KEYS.map((statusKey) => ( {STATUS_KEYS.map((statusKey) => (
<div key={statusKey} className="flex flex-col gap-3"> <div key={statusKey} className="flex flex-col gap-3">
{/* Stat card */}
<div className="bg-surface-elevated border border-surface-border rounded-xl p-4 text-center"> <div className="bg-surface-elevated border border-surface-border rounded-xl p-4 text-center">
<p className={`text-2xl font-bold ${STATUS_COLOR[statusKey]}`}>{stats[statusKey]}</p> <p className={`text-2xl font-bold ${STATUS_COLOR[statusKey]}`}>{stats[statusKey]}</p>
<p className="text-text-muted text-xs mt-1">{STATUS_LABEL[statusKey]}</p> <p className="text-text-muted text-xs mt-1">{STATUS_LABEL[statusKey]}</p>
</div> </div>
{/* Mini heatmap */}
<div className="bg-surface-elevated/40 border border-surface-border rounded-xl p-3 flex-1 min-h-[160px] flex flex-col gap-2"> <div className="bg-surface-elevated/40 border border-surface-border rounded-xl p-3 flex-1 min-h-[160px] flex flex-col gap-2">
<div className="flex justify-end"> <div className="flex justify-end">
<DensityLegend statusKey={statusKey} /> <DensityLegend statusKey={statusKey} />
</div> </div>
<div className="flex items-center justify-center flex-1"> <div className="flex items-center justify-center flex-1">
<div className="flex gap-2 overflow-x-auto pb-1 justify-center"> <div className="flex gap-2 overflow-x-auto pb-1 justify-center">
{/* Day initials */}
<div className="flex flex-col flex-shrink-0 pt-6" style={{ gap: GAP }}> <div className="flex flex-col flex-shrink-0 pt-6" style={{ gap: GAP }}>
{DAY_INIT.map((d, i) => ( {DAY_INIT.map((d, i) => (
<div key={i} style={{ height: CELL }} className="flex items-center text-[9px] text-text-muted/50 font-mono w-3">{d}</div> <div key={i} style={{ height: CELL }} className="flex items-center text-[9px] text-text-muted/50 font-mono w-3">{d}</div>
))} ))}
</div> </div>
{/* Grid */}
<div className="flex flex-col flex-shrink-0"> <div className="flex flex-col flex-shrink-0">
<div className="relative h-4 mb-1"> <div className="relative h-4 mb-1">
{monthLabels.map(({ wi, label }) => ( {monthLabels.map(({ wi, label }) => (
@@ -247,10 +238,9 @@ export default function WorkloadHeatmap() {
<span className={STATUS_COLOR[sk]}>{STATUS_LABEL[sk]}</span> <span className={STATUS_COLOR[sk]}>{STATUS_LABEL[sk]}</span>
</div> </div>
))} ))}
<span className="ml-1 text-text-muted/40">\u00b7 color = highest priority</span> <span className="ml-1 text-text-muted/40">&middot; color = highest priority</span>
</div> </div>
</div> </div>
<div className="flex gap-3 overflow-x-auto pb-2 justify-center"> <div className="flex gap-3 overflow-x-auto pb-2 justify-center">
<div className="flex flex-col flex-shrink-0 pt-6" style={{ gap: GAP }}> <div className="flex flex-col flex-shrink-0 pt-6" style={{ gap: GAP }}>
{DAY_INIT.map((d, i) => ( {DAY_INIT.map((d, i) => (
@@ -301,7 +291,7 @@ export default function WorkloadHeatmap() {
style={{ left: Math.min(tooltip.x + 14, window.innerWidth - 330), top: Math.max(tooltip.y - 100, 8) }} 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'}`}> <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')} {isToday(tooltip.date) ? 'Today ' : ''}{format(tooltip.date, 'EEE, MMM d, yyyy')}
</p> </p>
{tooltip.combined && tooltip.statusCounts ? ( {tooltip.combined && tooltip.statusCounts ? (
<div className="space-y-1 mb-2"> <div className="space-y-1 mb-2">
@@ -314,7 +304,7 @@ export default function WorkloadHeatmap() {
</div> </div>
) : ( ) : (
<p className={`text-[10px] mb-1.5 ${tooltip.statusKey ? STATUS_COLOR[tooltip.statusKey] : 'text-text-muted/60'}`}> <p className={`text-[10px] mb-1.5 ${tooltip.statusKey ? STATUS_COLOR[tooltip.statusKey] : 'text-text-muted/60'}`}>
{tooltip.statusKey ? STATUS_LABEL[tooltip.statusKey] : ''} \u00b7 {tooltip.items.length} task{tooltip.items.length !== 1 ? 's' : ''} {tooltip.statusKey ? STATUS_LABEL[tooltip.statusKey] : ''} &middot; {tooltip.items.length} task{tooltip.items.length !== 1 ? 's' : ''}
</p> </p>
)} )}
<div className="space-y-1.5"> <div className="space-y-1.5">