Add files via upload
This commit is contained in:
@@ -6,16 +6,21 @@ import useUIStore from '../../store/useUIStore'
|
|||||||
import { updateDeliverable as apiUpdate } from '../../api/deliverables'
|
import { updateDeliverable as apiUpdate } from '../../api/deliverables'
|
||||||
import DeliverableModal from '../Deliverables/DeliverableModal'
|
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 = {
|
||||||
const STATUS_CYCLE = { upcoming: 'in_progress', in_progress: 'completed', completed: 'upcoming', overdue: 'in_progress' }
|
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' }
|
||||||
|
|
||||||
export default function HeatmapDayPanel({ date, onClose }) {
|
export default function HeatmapDayPanel({ date, onClose }) {
|
||||||
const projects = useProjectStore(s => s.projects)
|
const projects = useProjectStore(s => s.projects)
|
||||||
const storeUpdate = useProjectStore(s => s.updateDeliverable)
|
const storeUpdate = useProjectStore(s => s.updateDeliverable)
|
||||||
const openFocus = useFocusStore(s => s.openFocus)
|
const openFocus = useFocusStore(s => s.openFocus)
|
||||||
const { jumpToCalendarDate } = useUIStore()
|
const { jumpToCalendarDate } = useUIStore()
|
||||||
|
|
||||||
const [addModal, setAddModal] = useState(false)
|
const [addModal, setAddModal] = useState(false)
|
||||||
@@ -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
|
→ 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
|
×
|
||||||
</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">□</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 \u00b7 \u25ce opens Focus View \u00b7 \u2192 Calendar jumps to date
|
⟳ cycles status · ◎ opens Focus View · → Calendar jumps to date
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { format, startOfWeek, addDays, addWeeks, isToday } from 'date-fns'
|
|||||||
import useProjectStore from '../../store/useProjectStore'
|
import useProjectStore from '../../store/useProjectStore'
|
||||||
import HeatmapDayPanel from './HeatmapDayPanel'
|
import HeatmapDayPanel from './HeatmapDayPanel'
|
||||||
|
|
||||||
const WEEKS = 20
|
const WEEKS = 20
|
||||||
const DAY_INIT = ['M','T','W','T','F','S','S']
|
const DAY_INIT = ['M','T','W','T','F','S','S']
|
||||||
const CELL = 16
|
const CELL = 16
|
||||||
const CELL_LG = 40
|
const CELL_LG = 40
|
||||||
const GAP = 2
|
const GAP = 2
|
||||||
const GAP_LG = 4
|
const GAP_LG = 4
|
||||||
|
|
||||||
const STATUS_KEYS = ['upcoming','in_progress','completed','overdue']
|
const STATUS_KEYS = ['upcoming','in_progress','completed','overdue']
|
||||||
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' }
|
||||||
@@ -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>
|
>← 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 →</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">· 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] : ''} · {tooltip.items.length} task{tooltip.items.length !== 1 ? 's' : ''}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
|
|||||||
Reference in New Issue
Block a user