141 lines
5.0 KiB
JavaScript
141 lines
5.0 KiB
JavaScript
import { useRef, useState, useCallback } 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 { updateDeliverable, deleteDeliverable } from '../../api/deliverables'
|
|
import DeliverableModal from '../Deliverables/DeliverableModal'
|
|
import ContextMenu from '../UI/ContextMenu'
|
|
|
|
export default function MainCalendar() {
|
|
const calRef = useRef(null)
|
|
const { projects, updateDeliverable: storeUpdate, removeDeliverable } = useProjectStore()
|
|
const openFocus = useFocusStore(s => s.openFocus)
|
|
|
|
const [modal, setModal] = useState({ open: false, deliverable: null, defaultDate: '' })
|
|
const [contextMenu, setContextMenu] = useState(null)
|
|
|
|
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 getDeliverable = (projectId, deliverableId) => {
|
|
const p = projects.find(p => p.id === projectId)
|
|
return { project: p, deliverable: p?.deliverables.find(d => d.id === deliverableId) }
|
|
}
|
|
|
|
// Single click → Focus View
|
|
const handleEventClick = useCallback(({ event }) => {
|
|
const { deliverableId, projectId } = event.extendedProps
|
|
openFocus(projectId, deliverableId)
|
|
}, [openFocus])
|
|
|
|
// Drag-and-drop → patch date
|
|
const handleEventDrop = useCallback(async ({ event }) => {
|
|
const { deliverableId } = event.extendedProps
|
|
storeUpdate(await updateDeliverable(deliverableId, { due_date: event.startStr.substring(0, 10) }))
|
|
}, [storeUpdate])
|
|
|
|
// Click empty date → add deliverable
|
|
const handleDateClick = useCallback(({ dateStr }) => {
|
|
setModal({ open: true, deliverable: null, defaultDate: dateStr.substring(0, 10) })
|
|
}, [])
|
|
|
|
// Attach dblclick + contextmenu to each event element after mount
|
|
const handleEventDidMount = useCallback(({ event, el }) => {
|
|
const { deliverableId, projectId } = event.extendedProps
|
|
|
|
// Double-click → open edit modal directly
|
|
el.addEventListener('dblclick', (e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
const { deliverable } = getDeliverable(projectId, deliverableId)
|
|
if (deliverable) setModal({ open: true, deliverable, defaultDate: '' })
|
|
})
|
|
|
|
// Right-click → context menu
|
|
el.addEventListener('contextmenu', (e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
const { project, deliverable } = getDeliverable(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 (
|
|
<div className="h-full flex flex-col bg-surface p-4" onContextMenu={e => e.preventDefault()}>
|
|
<div className="flex-1 overflow-hidden">
|
|
<FullCalendar
|
|
ref={calRef}
|
|
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
|
|
initialView="dayGridMonth"
|
|
headerToolbar={{ left: 'prev,next today', center: 'title', right: 'dayGridMonth,timeGridWeek,timeGridDay' }}
|
|
events={events}
|
|
editable={true}
|
|
eventDrop={handleEventDrop}
|
|
eventClick={handleEventClick}
|
|
dateClick={handleDateClick}
|
|
eventDidMount={handleEventDidMount}
|
|
height="100%"
|
|
dayMaxEvents={4}
|
|
eventDisplay="block"
|
|
displayEventTime={false}
|
|
/>
|
|
</div>
|
|
|
|
<DeliverableModal
|
|
isOpen={modal.open}
|
|
onClose={() => setModal({ open: false, deliverable: null, defaultDate: '' })}
|
|
deliverable={modal.deliverable}
|
|
defaultDate={modal.defaultDate}
|
|
/>
|
|
|
|
{contextMenu && (
|
|
<ContextMenu
|
|
x={contextMenu.x}
|
|
y={contextMenu.y}
|
|
items={contextMenu.items}
|
|
onClose={() => setContextMenu(null)}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|