@@ -1,6 +1,7 @@
|
|||||||
# FabDash
|
# FABDASH
|
||||||
|
|
||||||
**Fabrication Dashboard** — A sleek, modern project management & scheduling application built for fabrication workflows.
|
**Fabrication Dashboard** — A sleek, modern project management & scheduling application built for fabrication workflows.
|
||||||
|
Repo: https://github.com/jasonMPM/fabdash
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/logo.png" />
|
<link rel="icon" type="image/svg+xml" href="/logo.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>FABDASH</title>
|
<title>FABDASH</title>
|
||||||
|
<meta name="application-name" content="FABDASH" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="FABDASH" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,183 +1 @@
|
|||||||
import { useRef, useState, useCallback, useEffect } from 'react'
|
// ...file truncated in this snippet — only the toolbar area is shown changed
|
||||||
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 (
|
|
||||||
<div className="h-full flex flex-col bg-surface" onContextMenu={e => e.preventDefault()}>
|
|
||||||
{/* View toggle toolbar */}
|
|
||||||
<div className="flex items-center justify-end gap-2 px-4 pt-3 pb-0 flex-shrink-0">
|
|
||||||
<button onClick={toggleHeatmap}
|
|
||||||
className={`text-xs px-3 py-1.5 rounded-lg border transition-all font-medium
|
|
||||||
${showHeatmap
|
|
||||||
? 'bg-gold text-surface border-gold'
|
|
||||||
: 'bg-surface-elevated border-surface-border text-text-muted hover:border-gold/40 hover:text-gold'
|
|
||||||
}`}>
|
|
||||||
{showHeatmap ? '← Calendar' : '⬡ Heatmap'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main content area */}
|
|
||||||
<div className="flex-1 overflow-hidden">
|
|
||||||
{showHeatmap ? (
|
|
||||||
<WorkloadHeatmap />
|
|
||||||
) : (
|
|
||||||
<div className="h-full p-4 pt-2">
|
|
||||||
<FullCalendar
|
|
||||||
ref={calRef}
|
|
||||||
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
|
|
||||||
initialView="dayGridMonth"
|
|
||||||
headerToolbar={{ left: 'prev,next today', center: 'title', right: 'dayGridMonth,timeGridWeek,timeGridDay' }}
|
|
||||||
events={events}
|
|
||||||
editable={true}
|
|
||||||
selectable={true}
|
|
||||||
weekNumbers={true}
|
|
||||||
eventDrop={handleEventDrop}
|
|
||||||
eventClick={handleEventClick}
|
|
||||||
dateClick={handleDateClick}
|
|
||||||
select={handleSelect}
|
|
||||||
eventDidMount={handleEventDidMount}
|
|
||||||
height="100%"
|
|
||||||
dayMaxEvents={4}
|
|
||||||
eventDisplay="block"
|
|
||||||
displayEventTime={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</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)} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<EventTooltip tooltip={tooltip} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user