diff --git a/frontend/src/components/Calendar/MainCalendar.jsx b/frontend/src/components/Calendar/MainCalendar.jsx index 636aeef..2e90d98 100644 --- a/frontend/src/components/Calendar/MainCalendar.jsx +++ b/frontend/src/components/Calendar/MainCalendar.jsx @@ -1 +1,183 @@ -// ...file truncated in this snippet — only the toolbar area is shown changed +import { useRef, useState, useCallback, useEffect } from 'react' +import FullCalendar from '@fullcalendar/react' +import dayGridPlugin from '@fullcalendar/daygrid' +import timeGridPlugin from '@fullcalendar/timegrid' +import interactionPlugin from '@fullcalendar/interaction' +import useProjectStore from '../../store/useProjectStore' +import useFocusStore from '../../store/useFocusStore' +import useUIStore from '../../store/useUIStore' +import useToastStore from '../../store/useToastStore' +import { updateDeliverable, deleteDeliverable } from '../../api/deliverables' +import DeliverableModal from '../Deliverables/DeliverableModal' +import ContextMenu from '../UI/ContextMenu' +import EventTooltip from './EventTooltip' +import WorkloadHeatmap from './WorkloadHeatmap' + +export default function MainCalendar({ onCalendarReady }) { + const calRef = useRef(null) + const { projects, updateDeliverable: storeUpdate, removeDeliverable } = useProjectStore() + const openFocus = useFocusStore(s => s.openFocus) + const { showHeatmap, toggleHeatmap } = useUIStore() + const addToast = useToastStore(s => s.addToast) + + const [modal, setModal] = useState({ open: false, deliverable: null, defaultDate: '' }) + const [contextMenu, setContextMenu] = useState(null) + const [tooltip, setTooltip] = useState(null) + + // Expose calendar API to App.jsx for keyboard shortcuts + useEffect(() => { + if (calRef.current && onCalendarReady) { + onCalendarReady(calRef.current.getApi()) + } + }, []) + + const events = projects.flatMap(p => + (p.deliverables || []).map(d => ({ + id: String(d.id), + title: `${p.name}: ${d.title}`, + start: d.due_date, + allDay: true, + backgroundColor: p.color, + borderColor: p.color, + extendedProps: { deliverableId: d.id, projectId: p.id }, + })) + ) + + const getCtx = (projectId, deliverableId) => { + const project = projects.find(p => p.id === projectId) + const deliverable = project?.deliverables.find(d => d.id === deliverableId) + return { project, deliverable } + } + + const handleEventClick = useCallback(({ event }) => { + const { deliverableId, projectId } = event.extendedProps + openFocus(projectId, deliverableId) + }, [openFocus]) + + // Drag-and-drop with 30-second undo toast + const handleEventDrop = useCallback(async ({ event, oldEvent }) => { + const { deliverableId } = event.extendedProps + const newDate = event.startStr.substring(0, 10) + const oldDate = oldEvent.startStr.substring(0, 10) + storeUpdate(await updateDeliverable(deliverableId, { due_date: newDate })) + addToast({ + message: `Moved to ${newDate}`, + duration: 30, + undoFn: async () => { + storeUpdate(await updateDeliverable(deliverableId, { due_date: oldDate })) + }, + }) + }, [storeUpdate, addToast]) + + // Click empty date — open add modal + const handleDateClick = useCallback(({ dateStr }) => { + setModal({ open: true, deliverable: null, defaultDate: dateStr.substring(0, 10) }) + }, []) + + // Date range drag-select — pre-fill modal with start date + const handleSelect = useCallback(({ startStr }) => { + setModal({ open: true, deliverable: null, defaultDate: startStr.substring(0, 10) }) + }, []) + + // Attach dblclick + contextmenu + tooltip via eventDidMount + const handleEventDidMount = useCallback(({ event, el }) => { + const { deliverableId, projectId } = event.extendedProps + + el.addEventListener('mouseenter', (e) => { + const { project, deliverable } = getCtx(projectId, deliverableId) + setTooltip({ x: e.clientX, y: e.clientY, project, deliverable }) + }) + el.addEventListener('mouseleave', () => setTooltip(null)) + el.addEventListener('mousemove', (e) => { + setTooltip(prev => prev ? { ...prev, x: e.clientX, y: e.clientY } : null) + }) + + el.addEventListener('dblclick', (e) => { + e.preventDefault(); e.stopPropagation() + setTooltip(null) + const { deliverable } = getCtx(projectId, deliverableId) + if (deliverable) setModal({ open: true, deliverable, defaultDate: '' }) + }) + + el.addEventListener('contextmenu', (e) => { + e.preventDefault(); e.stopPropagation() + setTooltip(null) + const { project, deliverable } = getCtx(projectId, deliverableId) + if (!deliverable) return + setContextMenu({ + x: e.clientX, y: e.clientY, + items: [ + { icon: '✎', label: 'Edit Deliverable', action: () => setModal({ open: true, deliverable, defaultDate: '' }) }, + { icon: '◎', label: 'Open Focus View', action: () => openFocus(projectId, deliverableId) }, + ...(project?.drive_url ? [{ icon: '⬡', label: 'Open Drive Folder', action: () => window.open(project.drive_url, '_blank') }] : []), + { separator: true }, + { icon: '✕', label: 'Delete Deliverable', danger: true, + action: async () => { + if (window.confirm(`Delete "${deliverable.title}"?`)) { + await deleteDeliverable(deliverableId); removeDeliverable(deliverableId) + } + } + }, + ], + }) + }) + }, [projects, openFocus]) + + return ( +