diff --git a/frontend/src/components/Calendar/MainCalendar.jsx b/frontend/src/components/Calendar/MainCalendar.jsx
index cb0d169..72df263 100644
--- a/frontend/src/components/Calendar/MainCalendar.jsx
+++ b/frontend/src/components/Calendar/MainCalendar.jsx
@@ -5,14 +5,17 @@ import timeGridPlugin from '@fullcalendar/timegrid'
import interactionPlugin from '@fullcalendar/interaction'
import useProjectStore from '../../store/useProjectStore'
import useFocusStore from '../../store/useFocusStore'
-import { updateDeliverable } from '../../api/deliverables'
+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 } = useProjectStore()
+ const { projects, updateDeliverable: storeUpdate, removeDeliverable } = useProjectStore()
const openFocus = useFocusStore(s => s.openFocus)
- const [modal, setModal] = useState({ open: false, deliverable: null, defaultDate: '' })
+
+ const [modal, setModal] = useState({ open: false, deliverable: null, defaultDate: '' })
+ const [contextMenu, setContextMenu] = useState(null)
const events = projects.flatMap(p =>
(p.deliverables || []).map(d => ({
@@ -26,22 +29,78 @@ export default function MainCalendar() {
}))
)
- const handleEventDrop = useCallback(async ({ event }) => {
- const { deliverableId } = event.extendedProps
- storeUpdate(await updateDeliverable(deliverableId, { due_date: event.startStr.substring(0,10) }))
- }, [storeUpdate])
+ 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])
- const handleDateClick = useCallback(({ dateStr }) => {
- setModal({ open: true, deliverable: null, defaultDate: dateStr.substring(0,10) })
+ // 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 (
-
+
e.preventDefault()}>
+
setModal({ open: false, deliverable: null, defaultDate: '' })}
deliverable={modal.deliverable}
defaultDate={modal.defaultDate}
/>
+
+ {contextMenu && (
+ setContextMenu(null)}
+ />
+ )}
)
}
diff --git a/frontend/src/components/FocusView/DeliverableCard.jsx b/frontend/src/components/FocusView/DeliverableCard.jsx
index 0554a49..10fb059 100644
--- a/frontend/src/components/FocusView/DeliverableCard.jsx
+++ b/frontend/src/components/FocusView/DeliverableCard.jsx
@@ -1,43 +1,89 @@
+import { useState } from 'react'
import Badge from '../UI/Badge'
import { formatDate } from '../../utils/dateHelpers'
+import ContextMenu from '../UI/ContextMenu'
+import { updateDeliverable, deleteDeliverable } from '../../api/deliverables'
+import useProjectStore from '../../store/useProjectStore'
+import { STATUS_OPTIONS } from '../../utils/statusHelpers'
export default function DeliverableCard({ deliverable, isActive, index, projectColor, onSelect, onEdit }) {
+ const { updateDeliverable: storeUpdate, removeDeliverable } = useProjectStore()
+ const [ctxMenu, setCtxMenu] = useState(null)
+
+ const handleContextMenu = (e) => {
+ e.preventDefault()
+ e.stopPropagation()
+ setCtxMenu({
+ x: e.clientX, y: e.clientY,
+ items: [
+ { icon: '✎', label: 'Edit Deliverable', highlight: true, action: () => onEdit(deliverable) },
+ { separator: true },
+ ...STATUS_OPTIONS.map(s => ({
+ icon: s.value === deliverable.status ? '●' : '○',
+ label: `Mark ${s.label}`,
+ action: async () => {
+ storeUpdate(await updateDeliverable(deliverable.id, { status: s.value }))
+ },
+ })),
+ { separator: true },
+ {
+ icon: '✕', label: 'Delete Deliverable', danger: true,
+ action: async () => {
+ if (window.confirm(`Delete "${deliverable.title}"?`)) {
+ await deleteDeliverable(deliverable.id)
+ removeDeliverable(deliverable.id)
+ }
+ },
+ },
+ ],
+ })
+ }
+
return (
-
onSelect(deliverable.id)}
- className={`relative flex flex-col gap-2 rounded-xl border p-4 min-w-[190px] max-w-[230px] flex-shrink-0 cursor-pointer
- transition-all duration-200 select-none mt-4
- ${isActive
- ? 'border-gold bg-surface-elevated shadow-gold scale-105 ring-2 ring-gold/30'
- : 'border-surface-border bg-surface hover:border-gold/40 hover:bg-surface-elevated/60'
- }`}
- >
- {isActive && (
-
- Selected
+ <>
+
onSelect(deliverable.id)}
+ onDoubleClick={(e) => { e.stopPropagation(); onEdit(deliverable) }}
+ onContextMenu={handleContextMenu}
+ title="Click: Select · Double-click: Edit · Right-click: Menu"
+ className={`relative flex flex-col gap-2 rounded-xl border p-4 min-w-[190px] max-w-[230px] flex-shrink-0 cursor-pointer
+ transition-all duration-200 select-none mt-4
+ ${isActive
+ ? 'border-gold bg-surface-elevated shadow-gold scale-105 ring-2 ring-gold/30'
+ : 'border-surface-border bg-surface hover:border-gold/40 hover:bg-surface-elevated/60'
+ }`}
+ >
+ {isActive && (
+
+ Selected
+
+ )}
+
+
+
+ Deliverable {index + 1}
+
- )}
-
-
-
- Deliverable {index + 1}
-
+
+ {deliverable.title}
+
+
+ {formatDate(deliverable.due_date)}
+
+
+ {isActive && (
+
+ )}
-
- {deliverable.title}
-
-
- {formatDate(deliverable.due_date)}
-
-
- {isActive && (
-
+
+ {ctxMenu && (
+
setCtxMenu(null)} />
)}
-
+ >
)
}
diff --git a/frontend/src/components/Projects/ProjectCard.jsx b/frontend/src/components/Projects/ProjectCard.jsx
index baa42d9..65d5d49 100644
--- a/frontend/src/components/Projects/ProjectCard.jsx
+++ b/frontend/src/components/Projects/ProjectCard.jsx
@@ -1,6 +1,11 @@
+import { useState } from 'react'
import Badge from '../UI/Badge'
import { formatDate } from '../../utils/dateHelpers'
import useFocusStore from '../../store/useFocusStore'
+import useProjectStore from '../../store/useProjectStore'
+import { deleteDeliverable } from '../../api/deliverables'
+import DeliverableModal from '../Deliverables/DeliverableModal'
+import ContextMenu from '../UI/ContextMenu'
function DriveIcon() {
return (
@@ -16,35 +21,74 @@ function DriveIcon() {
}
export default function ProjectCard({ project, onEdit, onDelete }) {
- const openFocus = useFocusStore(s => s.openFocus)
+ const openFocus = useFocusStore(s => s.openFocus)
+ const { removeDeliverable } = useProjectStore()
+ const [delModal, setDelModal] = useState({ open: false, deliverable: null })
+ const [ctxMenu, setCtxMenu] = useState(null)
+
+ const openDelEdit = (d) => setDelModal({ open: true, deliverable: d })
+
+ const handleRowCtx = (e, d) => {
+ e.preventDefault()
+ e.stopPropagation()
+ setCtxMenu({
+ x: e.clientX, y: e.clientY,
+ items: [
+ { icon: '✎', label: 'Edit Deliverable', action: () => openDelEdit(d) },
+ { icon: '◎', label: 'Open Focus View', action: () => openFocus(project.id, d.id) },
+ { separator: true },
+ {
+ icon: '✕', label: 'Delete Deliverable', danger: true,
+ action: async () => {
+ if (window.confirm(`Delete "${d.title}"?`)) {
+ await deleteDeliverable(d.id)
+ removeDeliverable(d.id)
+ }
+ },
+ },
+ ],
+ })
+ }
+
+ const handleHeaderCtx = (e) => {
+ e.preventDefault()
+ setCtxMenu({
+ x: e.clientX, y: e.clientY,
+ items: [
+ { icon: '✎', label: 'Edit Project', action: () => onEdit(project) },
+ ...(project.drive_url ? [{ icon: '⬡', label: 'Open Drive', action: () => window.open(project.drive_url, '_blank') }] : []),
+ { separator: true },
+ { icon: '✕', label: 'Delete Project', danger: true, action: () => onDelete(project) },
+ ],
+ })
+ }
return (
- {/* Header row */}
-
+ {/* Header — double-click to edit, right-click for menu */}
+
onEdit(project)}
+ onContextMenu={handleHeaderCtx}
+ title="Double-click to edit project"
+ >
@@ -55,8 +99,14 @@ export default function ProjectCard({ project, onEdit, onDelete }) {
{/* Deliverable rows */}
{(project.deliverables || []).map(d => (
-
+
+ {/* Local deliverable edit modal */}
+
setDelModal({ open: false, deliverable: null })}
+ deliverable={delModal.deliverable}
+ projectId={project.id}
+ />
+
+ {ctxMenu && (
+ setCtxMenu(null)} />
+ )}
)
}
diff --git a/frontend/src/components/UI/ContextMenu.jsx b/frontend/src/components/UI/ContextMenu.jsx
new file mode 100644
index 0000000..fe97c5a
--- /dev/null
+++ b/frontend/src/components/UI/ContextMenu.jsx
@@ -0,0 +1,55 @@
+import { useEffect, useRef } from 'react'
+
+export default function ContextMenu({ x, y, items, onClose }) {
+ const ref = useRef(null)
+
+ useEffect(() => {
+ const onMouseDown = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose() }
+ const onKey = (e) => { if (e.key === 'Escape') onClose() }
+ document.addEventListener('mousedown', onMouseDown)
+ document.addEventListener('keydown', onKey)
+ return () => {
+ document.removeEventListener('mousedown', onMouseDown)
+ document.removeEventListener('keydown', onKey)
+ }
+ }, [onClose])
+
+ // Keep menu inside viewport
+ const W = 192
+ const H = items.length * 34
+ const adjX = Math.min(x, window.innerWidth - W - 8)
+ const adjY = Math.min(y, window.innerHeight - H - 8)
+
+ return (
+
+ {items.map((item, i) =>
+ item.separator ? (
+
+ ) : (
+
{ item.action(); onClose() }}
+ className={`w-full flex items-center gap-2.5 px-3 py-2 text-xs text-left transition-colors disabled:opacity-40 disabled:cursor-not-allowed
+ ${item.danger
+ ? 'text-red-400 hover:bg-red-500/10'
+ : item.highlight
+ ? 'text-gold hover:bg-gold/10'
+ : 'text-text-primary hover:bg-surface-border/40'
+ }`}
+ >
+ {item.icon}
+ {item.label}
+ {item.shortcut && (
+ {item.shortcut}
+ )}
+
+ )
+ )}
+
+ )
+}