Add drag-and-drop scheduling functionality to the Scheduling component

- Integrated react-dnd and react-dnd-html5-backend for drag-and-drop capabilities.
- Enhanced the Scheduling component to allow users to visually manage experiment repetitions on the calendar.
- Added state management for scheduled repetitions and their timing.
- Implemented select-all checkboxes for conductors and repetitions for improved user experience.
- Updated calendar event generation to include new repetition markers with distinct styles.
- Refactored event handling to support draggable repetition markers and update their timing dynamically.
This commit is contained in:
salirezav
2025-09-24 21:23:38 -04:00
parent aaeb164a32
commit 853cec1b13
3 changed files with 478 additions and 46 deletions

View File

@@ -14,6 +14,8 @@
"moment": "^2.30.1",
"react": "^19.1.0",
"react-big-calendar": "^1.19.4",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^19.1.0",
"react-router-dom": "^6.28.0",
"tailwindcss": "^4.1.11"
@@ -1060,6 +1062,24 @@
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@react-dnd/asap": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz",
"integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==",
"license": "MIT"
},
"node_modules/@react-dnd/invariant": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz",
"integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==",
"license": "MIT"
},
"node_modules/@react-dnd/shallowequal": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz",
"integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==",
"license": "MIT"
},
"node_modules/@remix-run/router": {
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
@@ -2440,6 +2460,17 @@
"node": ">=8"
}
},
"node_modules/dnd-core": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz",
"integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==",
"license": "MIT",
"dependencies": {
"@react-dnd/asap": "^5.0.1",
"@react-dnd/invariant": "^4.0.1",
"redux": "^4.2.0"
}
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
@@ -2716,7 +2747,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-glob": {
@@ -2915,6 +2945,15 @@
"node": ">=8"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"license": "BSD-3-Clause",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -3770,6 +3809,45 @@
"react-dom": "^16.14.0 || ^17 || ^18 || ^19"
}
},
"node_modules/react-dnd": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",
"integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==",
"license": "MIT",
"dependencies": {
"@react-dnd/invariant": "^4.0.1",
"@react-dnd/shallowequal": "^4.0.1",
"dnd-core": "^16.0.1",
"fast-deep-equal": "^3.1.3",
"hoist-non-react-statics": "^3.3.2"
},
"peerDependencies": {
"@types/hoist-non-react-statics": ">= 3.3.1",
"@types/node": ">= 12",
"@types/react": ">= 16",
"react": ">= 16.14"
},
"peerDependenciesMeta": {
"@types/hoist-non-react-statics": {
"optional": true
},
"@types/node": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/react-dnd-html5-backend": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz",
"integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==",
"license": "MIT",
"dependencies": {
"dnd-core": "^16.0.1"
}
},
"node_modules/react-dom": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
@@ -3856,6 +3934,15 @@
"react-dom": ">=16.8"
}
},
"node_modules/redux": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.9.2"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",

View File

@@ -16,6 +16,8 @@
"moment": "^2.30.1",
"react": "^19.1.0",
"react-big-calendar": "^1.19.4",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^19.1.0",
"react-router-dom": "^6.28.0",
"tailwindcss": "^4.1.11"
@@ -34,4 +36,4 @@
"typescript-eslint": "^8.28.1",
"vite": "^7.0.4"
}
}
}

View File

@@ -1,6 +1,10 @@
import { useEffect, useState } from 'react'
// @ts-ignore - react-big-calendar types not available
import { Calendar, momentLocalizer, Views } from 'react-big-calendar'
// @ts-ignore - react-big-calendar dragAndDrop types not available
import withDragAndDrop from 'react-big-calendar/lib/addons/dragAndDrop'
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import moment from 'moment'
import type { User, ExperimentPhase, Experiment, ExperimentRepetition, Soaking, Airdrying } from '../lib/supabase'
import { availabilityManagement, userManagement, experimentPhaseManagement, experimentManagement, repetitionManagement, phaseManagement, supabase } from '../lib/supabase'
@@ -305,6 +309,7 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
// Calendar state for selected conductors' availability
const localizer = momentLocalizer(moment)
const DnDCalendar = withDragAndDrop(Calendar)
const [calendarView, setCalendarView] = useState(Views.WEEK)
const [currentDate, setCurrentDate] = useState(new Date())
const [availabilityEvents, setAvailabilityEvents] = useState<CalendarEvent[]>([])
@@ -313,6 +318,18 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
const [conductorColorMap, setConductorColorMap] = useState<Record<string, string>>({})
const colorPalette = ['#2563eb', '#059669', '#d97706', '#7c3aed', '#dc2626', '#0ea5e9', '#16a34a', '#f59e0b', '#9333ea', '#ef4444']
// Repetition scheduling state
const [scheduledRepetitions, setScheduledRepetitions] = useState<Record<string, {
repetitionId: string
experimentId: string
soakingStart: Date | null
airdryingStart: Date | null
crackingStart: Date | null
}>>({})
// Visual style for repetition markers
const [markerStyle, setMarkerStyle] = useState<'circles' | 'dots' | 'icons' | 'lines'>('lines')
useEffect(() => {
const load = async () => {
try {
@@ -329,7 +346,6 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
setConductors(conductorsOnly)
// For each conductor, check if they have any availability in the future
const nowIso = new Date().toISOString()
// Query availability table directly for efficiency
const conductorIds = conductorsOnly.map(c => c.id)
// Fallback: since availabilityManagement is scoped to current user, we query via supabase client here would require direct import.
@@ -356,6 +372,23 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
})
}
const toggleAllConductors = () => {
const availableConductorIds = conductors
.filter(c => conductorIdsWithFutureAvailability.has(c.id))
.map(c => c.id)
setSelectedConductorIds(prev => {
// If all available conductors are selected, deselect all
const allSelected = availableConductorIds.every(id => prev.has(id))
if (allSelected) {
return new Set()
} else {
// Select all available conductors
return new Set(availableConductorIds)
}
})
}
// Fetch availability for selected conductors and build calendar events
useEffect(() => {
const loadSelectedAvailability = async () => {
@@ -396,7 +429,7 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
title: `${idToName[r.user_id] || 'Conductor'}`,
start: new Date(r.available_from),
end: new Date(r.available_to),
resource: newColorMap[r.user_id] || '#2563eb'
resource: r.user_id // Store conductor ID, not color
}))
setAvailabilityEvents(events)
@@ -462,12 +495,57 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
const toggleRepetition = (repId: string) => {
setSelectedRepetitionIds(prev => {
const next = new Set(prev)
if (next.has(repId)) next.delete(repId)
else next.add(repId)
if (next.has(repId)) {
next.delete(repId)
// Remove from scheduled repetitions when unchecked
setScheduledRepetitions(prevScheduled => {
const newScheduled = { ...prevScheduled }
delete newScheduled[repId]
return newScheduled
})
} else {
next.add(repId)
// Auto-spawn when checked
spawnSingleRepetition(repId)
}
return next
})
}
const toggleAllRepetitionsInPhase = (phaseId: string) => {
const experiments = experimentsByPhase[phaseId] || []
const allRepetitions = experiments.flatMap(exp => repetitionsByExperiment[exp.id] || [])
setSelectedRepetitionIds(prev => {
// Check if all repetitions in this phase are selected
const allSelected = allRepetitions.every(rep => prev.has(rep.id))
if (allSelected) {
// Deselect all repetitions in this phase
const next = new Set(prev)
allRepetitions.forEach(rep => {
next.delete(rep.id)
// Remove from scheduled repetitions
setScheduledRepetitions(prevScheduled => {
const newScheduled = { ...prevScheduled }
delete newScheduled[rep.id]
return newScheduled
})
})
return next
} else {
// Select all repetitions in this phase
const next = new Set(prev)
allRepetitions.forEach(rep => {
next.add(rep.id)
// Auto-spawn when checked
spawnSingleRepetition(rep.id)
})
return next
}
})
}
const createRepetitionsForExperiment = async (experimentId: string) => {
try {
setCreatingRepetitionsFor(prev => new Set(prev).add(experimentId))
@@ -492,6 +570,140 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
}
}
// Spawn a single repetition in calendar
const spawnSingleRepetition = (repId: string) => {
const tomorrow = new Date()
tomorrow.setDate(tomorrow.getDate() + 1)
tomorrow.setHours(9, 0, 0, 0) // Default to 9 AM tomorrow
// Find the experiment for this repetition
let experimentId = ''
for (const [expId, reps] of Object.entries(repetitionsByExperiment)) {
if (reps.find(r => r.id === repId)) {
experimentId = expId
break
}
}
if (experimentId) {
const experiment = Object.values(experimentsByPhase).flat().find(e => e.id === experimentId)
const soaking = soakingByExperiment[experimentId]
const airdrying = airdryingByExperiment[experimentId]
if (experiment && soaking && airdrying) {
const soakingStart = new Date(tomorrow)
const airdryingStart = new Date(soakingStart.getTime() + (soaking.soaking_duration_minutes * 60000))
const crackingStart = new Date(airdryingStart.getTime() + (airdrying.duration_minutes * 60000))
setScheduledRepetitions(prev => ({
...prev,
[repId]: {
repetitionId: repId,
experimentId,
soakingStart,
airdryingStart,
crackingStart
}
}))
}
}
}
// Update phase timing when a marker is moved
const updatePhaseTiming = (repId: string, phase: 'soaking' | 'airdrying' | 'cracking', newTime: Date) => {
setScheduledRepetitions(prev => {
const current = prev[repId]
if (!current) return prev
const experimentId = current.experimentId
const soaking = soakingByExperiment[experimentId]
const airdrying = airdryingByExperiment[experimentId]
if (!soaking || !airdrying) return prev
let newScheduled = { ...prev }
if (phase === 'soaking') {
const airdryingStart = new Date(newTime.getTime() + (soaking.soaking_duration_minutes * 60000))
const crackingStart = new Date(airdryingStart.getTime() + (airdrying.duration_minutes * 60000))
newScheduled[repId] = {
...current,
soakingStart: newTime,
airdryingStart,
crackingStart
}
} else if (phase === 'airdrying') {
const soakingStart = new Date(newTime.getTime() - (soaking.soaking_duration_minutes * 60000))
const crackingStart = new Date(newTime.getTime() + (airdrying.duration_minutes * 60000))
newScheduled[repId] = {
...current,
soakingStart,
airdryingStart: newTime,
crackingStart
}
} else if (phase === 'cracking') {
const airdryingStart = new Date(newTime.getTime() - (airdrying.duration_minutes * 60000))
const soakingStart = new Date(airdryingStart.getTime() - (soaking.soaking_duration_minutes * 60000))
newScheduled[repId] = {
...current,
soakingStart,
airdryingStart,
crackingStart: newTime
}
}
return newScheduled
})
}
// Generate calendar events for scheduled repetitions
const generateRepetitionEvents = (): CalendarEvent[] => {
const events: CalendarEvent[] = []
Object.values(scheduledRepetitions).forEach(scheduled => {
const experiment = Object.values(experimentsByPhase).flat().find(e => e.id === scheduled.experimentId)
const repetition = repetitionsByExperiment[scheduled.experimentId]?.find(r => r.id === scheduled.repetitionId)
if (experiment && repetition && scheduled.soakingStart) {
// Soaking marker
events.push({
id: `${scheduled.repetitionId}-soaking`,
title: `Exp ${experiment.experiment_number} Rep ${repetition.repetition_number}`,
start: scheduled.soakingStart,
end: new Date(scheduled.soakingStart.getTime() + 30 * 60000), // 30 minute duration for visibility
resource: 'soaking'
})
// Airdrying marker
if (scheduled.airdryingStart) {
events.push({
id: `${scheduled.repetitionId}-airdrying`,
title: `Exp ${experiment.experiment_number} Rep ${repetition.repetition_number}`,
start: scheduled.airdryingStart,
end: new Date(scheduled.airdryingStart.getTime() + 30 * 60000),
resource: 'airdrying'
})
}
// Cracking marker
if (scheduled.crackingStart) {
events.push({
id: `${scheduled.repetitionId}-cracking`,
title: `Exp ${experiment.experiment_number} Rep ${repetition.repetition_number}`,
start: scheduled.crackingStart,
end: new Date(scheduled.crackingStart.getTime() + 30 * 60000),
resource: 'cracking'
})
}
}
})
return events
}
return (
<div className="p-6">
<div className="mb-6">
@@ -534,6 +746,18 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Conductors</h2>
<span className="text-xs text-gray-500 dark:text-gray-400">Select to consider for scheduling</span>
</div>
{/* Select All checkbox for conductors */}
<div className="mb-3">
<label className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-700/50 rounded border border-gray-200 dark:border-gray-600">
<input
type="checkbox"
className="h-4 w-4 text-blue-600 border-gray-300 rounded"
checked={conductors.filter(c => conductorIdsWithFutureAvailability.has(c.id)).every(c => selectedConductorIds.has(c.id)) && conductors.filter(c => conductorIdsWithFutureAvailability.has(c.id)).length > 0}
onChange={toggleAllConductors}
/>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Select All Available Conductors</span>
</label>
</div>
<div className="max-h-[420px] overflow-y-auto divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-md">
{conductors.length === 0 && (
<div className="p-4 text-sm text-gray-500 dark:text-gray-400">No conductors found.</div>
@@ -596,6 +820,20 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
</button>
{expanded && (
<div className="bg-gray-50 dark:bg-gray-800/50 max-h-96 overflow-y-auto">
{/* Select All checkbox for this phase */}
{experiments.length > 0 && (
<div className="p-3 border-b border-gray-200 dark:border-gray-600">
<label className="flex items-center gap-2">
<input
type="checkbox"
className="h-4 w-4 text-blue-600 border-gray-300 rounded"
checked={experiments.flatMap(exp => repetitionsByExperiment[exp.id] || []).every(rep => selectedRepetitionIds.has(rep.id)) && experiments.flatMap(exp => repetitionsByExperiment[exp.id] || []).length > 0}
onChange={() => toggleAllRepetitionsInPhase(phase.id)}
/>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Select All Repetitions in {phase.name}</span>
</label>
</div>
)}
{experiments.length === 0 && (
<div className="p-3 text-xs text-gray-500 dark:text-gray-400">No experiments in this phase.</div>
)}
@@ -677,47 +915,152 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
)}
{/* Week Calendar for selected conductors' availability */}
<div className="mt-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Selected Conductors' Availability</h3>
<div className="h-[80vh] border border-gray-200 dark:border-gray-700 rounded-md overflow-hidden">
<Calendar
localizer={localizer}
events={availabilityEvents}
startAccessor="start"
endAccessor="end"
titleAccessor="title"
style={{ height: '100%' }}
view={calendarView}
onView={setCalendarView}
date={currentDate}
onNavigate={setCurrentDate}
views={[Views.WEEK, Views.DAY]}
dayLayoutAlgorithm="no-overlap"
eventPropGetter={(event) => {
const color = event.resource || '#2563eb'
return {
style: {
backgroundColor: color + '80', // ~50% transparency
borderColor: color,
color: 'white',
borderRadius: '4px',
border: 'none',
opacity: 0.8,
height: 'auto',
minHeight: '20px',
fontSize: '12px',
padding: '2px 4px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
<div className="flex justify-between items-center mb-3">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Selected Conductors' Availability & Experiment Scheduling</h3>
<div className="flex gap-2">
<span className="text-sm text-gray-600 dark:text-gray-400">Marker Style:</span>
<button
onClick={() => setMarkerStyle('lines')}
className={`px-3 py-1 text-xs rounded ${markerStyle === 'lines'
? 'bg-blue-500 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
}`}
>
Lines
</button>
<button
onClick={() => setMarkerStyle('circles')}
className={`px-3 py-1 text-xs rounded ${markerStyle === 'circles'
? 'bg-blue-500 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
}`}
>
Circles
</button>
<button
onClick={() => setMarkerStyle('dots')}
className={`px-3 py-1 text-xs rounded ${markerStyle === 'dots'
? 'bg-blue-500 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
}`}
>
Dots
</button>
<button
onClick={() => setMarkerStyle('icons')}
className={`px-3 py-1 text-xs rounded ${markerStyle === 'icons'
? 'bg-blue-500 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
}`}
>
Icons
</button>
</div>
</div>
<div className="h-[80vh] border border-gray-200 dark:border-gray-700 rounded-md overflow-hidden relative">
<DndProvider backend={HTML5Backend}>
<DnDCalendar
localizer={localizer}
events={generateRepetitionEvents()}
backgroundEvents={availabilityEvents}
startAccessor="start"
endAccessor="end"
titleAccessor="title"
style={{ height: '100%' }}
view={calendarView}
onView={setCalendarView}
date={currentDate}
onNavigate={setCurrentDate}
views={[Views.WEEK, Views.DAY]}
dayLayoutAlgorithm="no-overlap"
draggableAccessor={(event: any) => {
// Only make repetition markers draggable, not availability events
const resource = event.resource as string
return resource === 'soaking' || resource === 'airdrying' || resource === 'cracking'
}}
onEventDrop={({ event, start }: { event: any, start: Date }) => {
// Handle dragging repetition markers
const eventId = event.id as string
if (eventId.includes('-soaking')) {
const repId = eventId.replace('-soaking', '')
updatePhaseTiming(repId, 'soaking', start)
} else if (eventId.includes('-airdrying')) {
const repId = eventId.replace('-airdrying', '')
updatePhaseTiming(repId, 'airdrying', start)
} else if (eventId.includes('-cracking')) {
const repId = eventId.replace('-cracking', '')
updatePhaseTiming(repId, 'cracking', start)
}
}
}}
popup
showMultiDayTimes
doShowMore={true}
step={30}
timeslots={2}
/>
}}
eventPropGetter={(event: any) => {
const resource = event.resource as string
// Styling for repetition markers (foreground events)
if (resource === 'soaking' || resource === 'airdrying' || resource === 'cracking') {
const colors = {
soaking: '#3b82f6', // blue
airdrying: '#10b981', // green
cracking: '#f59e0b' // orange
}
const color = colors[resource as keyof typeof colors] || '#6b7280'
return {
style: {
backgroundColor: color,
borderColor: color,
color: 'white',
borderRadius: '4px',
border: 'none',
height: '8px',
minHeight: '8px',
fontSize: '10px',
padding: '2px 4px',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'space-between',
fontWeight: 'bold',
zIndex: 10,
position: 'relative',
lineHeight: '1.2',
textShadow: '1px 1px 2px rgba(0,0,0,0.7)'
}
}
}
// Default styling for other events
return {}
}}
backgroundEventPropGetter={(event: any) => {
// Styling for background events (conductor availability)
const conductorId = event.resource
const color = conductorColorMap[conductorId] || '#2563eb'
return {
style: {
backgroundColor: color + '40', // ~25% transparency for background
borderColor: color,
borderWidth: '1px',
borderStyle: 'solid',
color: 'transparent',
borderRadius: '4px',
opacity: 0.6,
height: 'auto',
minHeight: '20px',
fontSize: '0px',
padding: '0px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}
}
}}
popup
showMultiDayTimes
doShowMore={true}
step={30}
timeslots={2}
/>
</DndProvider>
</div>
</div>
</div>