Remove CalendarStyles.css and Scheduling.tsx components, updating the project structure for improved maintainability. Update Supabase CLI version and modify experiment_repetitions SQL migration to include scheduled_date. Enhance scheduling component in the remote app with improved drag-and-drop functionality and UI adjustments for better user experience.
This commit is contained in:
@@ -1,250 +0,0 @@
|
||||
/* Custom styles for React Big Calendar to match dashboard theme */
|
||||
|
||||
.rbc-calendar {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
.dark .rbc-calendar {
|
||||
background: #1f2937;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
/* Header styling */
|
||||
.rbc-header {
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
color: #374151;
|
||||
font-weight: 600;
|
||||
padding: 12px 8px;
|
||||
}
|
||||
|
||||
.dark .rbc-header {
|
||||
background: #374151;
|
||||
border-bottom: 1px solid #4b5563;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
/* Today styling */
|
||||
.rbc-today {
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.dark .rbc-today {
|
||||
background: #1e3a8a;
|
||||
}
|
||||
|
||||
/* Date cells */
|
||||
.rbc-date-cell {
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dark .rbc-date-cell {
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
/* Event styling */
|
||||
.rbc-event {
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.rbc-event-content {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Month view specific */
|
||||
.rbc-month-view {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dark .rbc-month-view {
|
||||
border: 1px solid #4b5563;
|
||||
}
|
||||
|
||||
.rbc-month-row {
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.dark .rbc-month-row {
|
||||
border-bottom: 1px solid #4b5563;
|
||||
}
|
||||
|
||||
.rbc-date-cell {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* Week and day view specific */
|
||||
.rbc-time-view {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dark .rbc-time-view {
|
||||
border: 1px solid #4b5563;
|
||||
}
|
||||
|
||||
.rbc-time-header {
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.dark .rbc-time-header {
|
||||
background: #374151;
|
||||
border-bottom: 1px solid #4b5563;
|
||||
}
|
||||
|
||||
.rbc-time-content {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.dark .rbc-time-content {
|
||||
background: #1f2937;
|
||||
}
|
||||
|
||||
/* Time slots */
|
||||
.rbc-time-slot {
|
||||
border-top: 1px solid #f1f5f9;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.dark .rbc-time-slot {
|
||||
border-top: 1px solid #374151;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* Toolbar */
|
||||
.rbc-toolbar {
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.dark .rbc-toolbar {
|
||||
background: #374151;
|
||||
border-bottom: 1px solid #4b5563;
|
||||
}
|
||||
|
||||
.rbc-toolbar button {
|
||||
background: white;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
padding: 6px 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.rbc-toolbar button:hover {
|
||||
background: #f3f4f6;
|
||||
border-color: #9ca3af;
|
||||
}
|
||||
|
||||
.rbc-toolbar button:active,
|
||||
.rbc-toolbar button.rbc-active {
|
||||
background: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark .rbc-toolbar button {
|
||||
background: #1f2937;
|
||||
border: 1px solid #4b5563;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
.dark .rbc-toolbar button:hover {
|
||||
background: #374151;
|
||||
border-color: #6b7280;
|
||||
}
|
||||
|
||||
.dark .rbc-toolbar button:active,
|
||||
.dark .rbc-toolbar button.rbc-active {
|
||||
background: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Labels */
|
||||
.rbc-toolbar-label {
|
||||
color: #111827;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dark .rbc-toolbar-label {
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
/* Drag and drop improvements */
|
||||
.rbc-event {
|
||||
cursor: grab !important;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.rbc-event:active {
|
||||
cursor: grabbing !important;
|
||||
transform: scale(1.05);
|
||||
z-index: 1000 !important;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
/* Improve event spacing and visibility */
|
||||
.rbc-event-content {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Better visual feedback for dragging */
|
||||
.rbc-addons-dnd-dragging {
|
||||
opacity: 0.8;
|
||||
transform: rotate(2deg);
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
|
||||
.rbc-addons-dnd-drag-preview {
|
||||
background: rgba(255, 255, 255, 0.9) !important;
|
||||
border: 2px dashed #3b82f6 !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 8px 12px !important;
|
||||
font-weight: bold !important;
|
||||
color: #1f2937 !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
/* Improve event hover states */
|
||||
.rbc-event:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
|
||||
/* Better spacing between events */
|
||||
.rbc-time-slot {
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.rbc-toolbar {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rbc-toolbar button {
|
||||
font-size: 14px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.rbc-toolbar-label {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
||||
v2.65.2
|
||||
v2.65.5
|
||||
@@ -9,6 +9,7 @@ CREATE TABLE IF NOT EXISTS public.experiment_repetitions (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
experiment_id UUID NOT NULL REFERENCES public.experiments(id) ON DELETE CASCADE,
|
||||
repetition_number INTEGER NOT NULL CHECK (repetition_number > 0),
|
||||
scheduled_date TIMESTAMP WITH TIME ZONE,
|
||||
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'in_progress', 'completed', 'cancelled')),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
@@ -36,3 +36,6 @@ ADD CONSTRAINT unique_meyer_cracker_parameters_per_repetition
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
156
scheduling-remote/REFACTORING_PROPOSAL.md
Normal file
156
scheduling-remote/REFACTORING_PROPOSAL.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# Scheduling Component Refactoring Proposal
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
The `ScheduleExperiment` component is approximately **1,140 lines** and handles multiple responsibilities:
|
||||
|
||||
1. **Conductor Management** (~150 lines)
|
||||
- Fetching conductors
|
||||
- Selection state
|
||||
- Color mapping
|
||||
- Availability fetching
|
||||
|
||||
2. **Phase/Experiment/Repetition Management** (~300 lines)
|
||||
- Phase expansion/collapse
|
||||
- Experiment loading
|
||||
- Repetition creation/selection
|
||||
- Soaking/airdrying data fetching
|
||||
|
||||
3. **Calendar Event Management** (~200 lines)
|
||||
- Event generation
|
||||
- Drag and drop handling
|
||||
- Availability events
|
||||
- Styling and theming
|
||||
|
||||
4. **Scheduling Logic** (~300 lines)
|
||||
- Repetition spawning
|
||||
- Staggering calculations
|
||||
- Phase timing updates
|
||||
- Lock/unlock functionality
|
||||
- Database persistence
|
||||
|
||||
5. **UI Rendering** (~200 lines)
|
||||
- Conductor panel
|
||||
- Phase/Experiment/Repetition tree
|
||||
- Calendar component
|
||||
- Loading/error states
|
||||
|
||||
## Proposed Structure
|
||||
|
||||
### Component Hierarchy
|
||||
|
||||
```
|
||||
ScheduleExperiment (Main Container - ~150 lines)
|
||||
├── ConductorPanel (UI Component - ~100 lines)
|
||||
├── ExperimentPhasePanel (UI Component - ~150 lines)
|
||||
│ ├── ExperimentItem (UI Component - ~80 lines)
|
||||
│ │ └── RepetitionItem (UI Component - ~120 lines)
|
||||
└── SchedulingCalendar (UI Component - ~150 lines)
|
||||
```
|
||||
|
||||
### Custom Hooks
|
||||
|
||||
```
|
||||
hooks/
|
||||
├── useConductors.ts (~100 lines)
|
||||
│ - Conductor fetching
|
||||
│ - Selection state
|
||||
│ - Color mapping
|
||||
│ - Availability fetching
|
||||
│
|
||||
├── useExperimentPhases.ts (~150 lines)
|
||||
│ - Phase/Experiment/Repetition data fetching
|
||||
│ - Expansion state
|
||||
│ - Selection state
|
||||
│ - Soaking/airdrying data
|
||||
│
|
||||
├── useScheduling.ts (~200 lines)
|
||||
│ - Repetition scheduling state
|
||||
│ - Spawn/stagger logic
|
||||
│ - Phase timing updates
|
||||
│ - Lock/unlock functionality
|
||||
│ - Database persistence
|
||||
│
|
||||
└── useCalendarEvents.ts (~150 lines)
|
||||
- Calendar event generation
|
||||
- Drag and drop handlers
|
||||
- Event styling
|
||||
- Scroll preservation
|
||||
```
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
scheduling-remote/src/components/
|
||||
├── Scheduling.tsx (Main router - unchanged)
|
||||
├── ScheduleExperiment/
|
||||
│ ├── index.tsx (Main container - ~150 lines)
|
||||
│ ├── ConductorPanel.tsx (~100 lines)
|
||||
│ ├── ExperimentPhasePanel.tsx (~150 lines)
|
||||
│ ├── ExperimentItem.tsx (~80 lines)
|
||||
│ ├── RepetitionItem.tsx (~120 lines)
|
||||
│ └── SchedulingCalendar.tsx (~150 lines)
|
||||
└── hooks/
|
||||
├── useConductors.ts (~100 lines)
|
||||
├── useExperimentPhases.ts (~150 lines)
|
||||
├── useScheduling.ts (~200 lines)
|
||||
└── useCalendarEvents.ts (~150 lines)
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Maintainability**: Each component has a single, clear responsibility
|
||||
2. **Testability**: Smaller units are easier to test in isolation
|
||||
3. **Reusability**: Components and hooks can be reused elsewhere
|
||||
4. **Readability**: Easier to understand and navigate
|
||||
5. **Performance**: Better optimization opportunities with smaller components
|
||||
6. **Collaboration**: Multiple developers can work on different parts simultaneously
|
||||
|
||||
## Refactoring Strategy
|
||||
|
||||
### Phase 1: Extract Custom Hooks (Low Risk)
|
||||
1. Extract `useConductors` hook
|
||||
2. Extract `useExperimentPhases` hook
|
||||
3. Extract `useScheduling` hook
|
||||
4. Extract `useCalendarEvents` hook
|
||||
5. Test each hook independently
|
||||
|
||||
### Phase 2: Extract UI Components (Medium Risk)
|
||||
1. Extract `ConductorPanel` component
|
||||
2. Extract `ExperimentPhasePanel` component
|
||||
3. Extract `ExperimentItem` component
|
||||
4. Extract `RepetitionItem` component
|
||||
5. Extract `SchedulingCalendar` component
|
||||
6. Test component integration
|
||||
|
||||
### Phase 3: Refactor Main Component (Low Risk)
|
||||
1. Update `ScheduleExperiment` to use extracted hooks and components
|
||||
2. Remove duplicate code
|
||||
3. Final integration testing
|
||||
|
||||
## Best Practices Applied
|
||||
|
||||
1. **Single Responsibility Principle**: Each component/hook has one clear purpose
|
||||
2. **Custom Hooks for Logic**: Business logic separated from UI
|
||||
3. **Props Drilling vs Context**: Use props for direct parent-child, context only if needed
|
||||
4. **Memoization**: Preserve existing `useMemo`/`useCallback` optimizations
|
||||
5. **Type Safety**: Maintain all TypeScript types
|
||||
6. **State Management**: Keep related state together in hooks
|
||||
|
||||
## Migration Path
|
||||
|
||||
1. **Backward Compatible**: All functionality preserved
|
||||
2. **Incremental**: Can be done in phases
|
||||
3. **Testable**: Each phase can be tested independently
|
||||
4. **Reversible**: Changes can be rolled back if needed
|
||||
|
||||
## Estimated Impact
|
||||
|
||||
- **Main Component**: 1,140 lines → ~150 lines (87% reduction)
|
||||
- **Total Lines**: ~1,140 → ~1,200 (slight increase due to imports/exports, but much better organized)
|
||||
- **Complexity**: Significantly reduced per file
|
||||
- **Maintainability**: Dramatically improved
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -186,45 +186,23 @@
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
/* Drag and drop improvements */
|
||||
/* Basic drag and drop styles - minimal and safe */
|
||||
.rbc-event {
|
||||
cursor: grab !important;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.rbc-event:active {
|
||||
cursor: grabbing !important;
|
||||
transform: scale(1.05);
|
||||
z-index: 1000 !important;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3) !important;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* Improve event spacing and visibility */
|
||||
.rbc-event-content {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Better visual feedback for dragging */
|
||||
/* Simple drag feedback */
|
||||
.rbc-addons-dnd-dragging {
|
||||
opacity: 0.8;
|
||||
transform: rotate(2deg);
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
|
||||
.rbc-addons-dnd-drag-preview {
|
||||
background: rgba(255, 255, 255, 0.9) !important;
|
||||
border: 2px dashed #3b82f6 !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 8px 12px !important;
|
||||
font-weight: bold !important;
|
||||
color: #1f2937 !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
/* Improve event hover states */
|
||||
.rbc-event:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2) !important;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Better spacing between events */
|
||||
|
||||
@@ -335,8 +335,6 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
// Track which repetitions are currently being scheduled
|
||||
const [schedulingRepetitions, setSchedulingRepetitions] = useState<Set<string>>(new Set())
|
||||
|
||||
// Visual style for repetition markers
|
||||
const [markerStyle, setMarkerStyle] = useState<'circles' | 'dots' | 'icons' | 'lines'>('lines')
|
||||
|
||||
// Ref for calendar container to preserve scroll position
|
||||
const calendarRef = useRef<HTMLDivElement>(null)
|
||||
@@ -435,13 +433,16 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
idToName[c.id] = name
|
||||
})
|
||||
|
||||
const events: CalendarEvent[] = (data || []).map((r: any) => ({
|
||||
const events: CalendarEvent[] = (data || []).map((r: any) => {
|
||||
const conductorId = r.user_id
|
||||
return {
|
||||
id: r.id,
|
||||
title: `${idToName[r.user_id] || 'Conductor'}`,
|
||||
title: `${idToName[conductorId] || 'Conductor'}`,
|
||||
start: new Date(r.available_from),
|
||||
end: new Date(r.available_to),
|
||||
resource: r.user_id // Store conductor ID, not color
|
||||
}))
|
||||
resource: conductorId // Store conductor ID for color mapping
|
||||
}
|
||||
})
|
||||
|
||||
setAvailabilityEvents(events)
|
||||
} catch (e) {
|
||||
@@ -540,6 +541,7 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
// Auto-spawn when checked - pass the updated set to ensure correct stagger calculation
|
||||
spawnSingleRepetition(repId, next)
|
||||
// Re-stagger all existing repetitions to prevent overlap
|
||||
// Note: reStaggerRepetitions will automatically skip locked repetitions
|
||||
reStaggerRepetitions([...next, repId])
|
||||
}
|
||||
return next
|
||||
@@ -605,7 +607,8 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
}
|
||||
|
||||
// Re-stagger all repetitions to prevent overlap
|
||||
const reStaggerRepetitions = (repIds: string[]) => {
|
||||
// IMPORTANT: Skip locked repetitions to prevent them from moving
|
||||
const reStaggerRepetitions = useCallback((repIds: string[]) => {
|
||||
const tomorrow = new Date()
|
||||
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||
tomorrow.setHours(9, 0, 0, 0)
|
||||
@@ -613,9 +616,14 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
setScheduledRepetitions(prev => {
|
||||
const newScheduled = { ...prev }
|
||||
|
||||
repIds.forEach((repId, index) => {
|
||||
// Filter out locked repetitions - they should not be moved
|
||||
const unlockedRepIds = repIds.filter(repId => !lockedSchedules.has(repId))
|
||||
|
||||
// Calculate stagger index only for unlocked repetitions
|
||||
let staggerIndex = 0
|
||||
unlockedRepIds.forEach((repId) => {
|
||||
if (newScheduled[repId]) {
|
||||
const staggerMinutes = index * 15 // 15 minutes between each repetition
|
||||
const staggerMinutes = staggerIndex * 15 // 15 minutes between each repetition
|
||||
const baseTime = new Date(tomorrow.getTime() + (staggerMinutes * 60000))
|
||||
|
||||
// Find the experiment for this repetition
|
||||
@@ -643,6 +651,7 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
airdryingStart,
|
||||
crackingStart
|
||||
}
|
||||
staggerIndex++
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -650,7 +659,7 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
|
||||
return newScheduled
|
||||
})
|
||||
}
|
||||
}, [lockedSchedules, repetitionsByExperiment, experimentsByPhase, soakingByExperiment, airdryingByExperiment])
|
||||
|
||||
// Spawn a single repetition in calendar
|
||||
const spawnSingleRepetition = (repId: string, updatedSelectedIds?: Set<string>) => {
|
||||
@@ -769,10 +778,13 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
const repetition = repetitionsByExperiment[scheduled.experimentId]?.find(r => r.id === scheduled.repetitionId)
|
||||
|
||||
if (experiment && repetition && scheduled.soakingStart) {
|
||||
const isLocked = lockedSchedules.has(scheduled.repetitionId)
|
||||
const lockIcon = isLocked ? '🔒' : '🔓'
|
||||
|
||||
// Soaking marker
|
||||
events.push({
|
||||
id: `${scheduled.repetitionId}-soaking`,
|
||||
title: `💧 Soaking - Exp ${experiment.experiment_number} Rep ${repetition.repetition_number}`,
|
||||
title: `${lockIcon} 💧 Soaking - Exp ${experiment.experiment_number} Rep ${repetition.repetition_number}`,
|
||||
start: scheduled.soakingStart,
|
||||
end: new Date(scheduled.soakingStart.getTime() + 15 * 60000), // 15 minute duration for better visibility
|
||||
resource: 'soaking'
|
||||
@@ -782,7 +794,7 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
if (scheduled.airdryingStart) {
|
||||
events.push({
|
||||
id: `${scheduled.repetitionId}-airdrying`,
|
||||
title: `🌬️ Airdrying - Exp ${experiment.experiment_number} Rep ${repetition.repetition_number}`,
|
||||
title: `${lockIcon} 🌬️ Airdrying - Exp ${experiment.experiment_number} Rep ${repetition.repetition_number}`,
|
||||
start: scheduled.airdryingStart,
|
||||
end: new Date(scheduled.airdryingStart.getTime() + 15 * 60000), // 15 minute duration for better visibility
|
||||
resource: 'airdrying'
|
||||
@@ -793,7 +805,7 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
if (scheduled.crackingStart) {
|
||||
events.push({
|
||||
id: `${scheduled.repetitionId}-cracking`,
|
||||
title: `⚡ Cracking - Exp ${experiment.experiment_number} Rep ${repetition.repetition_number}`,
|
||||
title: `${lockIcon} ⚡ Cracking - Exp ${experiment.experiment_number} Rep ${repetition.repetition_number}`,
|
||||
start: scheduled.crackingStart,
|
||||
end: new Date(scheduled.crackingStart.getTime() + 15 * 60000), // 15 minute duration for better visibility
|
||||
resource: 'cracking'
|
||||
@@ -803,10 +815,12 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
})
|
||||
|
||||
return events
|
||||
}, [scheduledRepetitions, experimentsByPhase, repetitionsByExperiment])
|
||||
}, [scheduledRepetitions, experimentsByPhase, repetitionsByExperiment, lockedSchedules])
|
||||
|
||||
// Memoize the calendar events to prevent unnecessary re-renders
|
||||
const calendarEvents = useMemo(() => generateRepetitionEvents(), [generateRepetitionEvents])
|
||||
// Memoize the calendar events
|
||||
const calendarEvents = useMemo(() => {
|
||||
return generateRepetitionEvents()
|
||||
}, [generateRepetitionEvents])
|
||||
|
||||
// Functions to preserve and restore scroll position
|
||||
const preserveScrollPosition = useCallback(() => {
|
||||
@@ -1232,9 +1246,13 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
type="checkbox"
|
||||
className="h-3 w-3 text-blue-600 border-gray-300 rounded"
|
||||
checked={isLocked}
|
||||
onChange={() => toggleScheduleLock(rep.id)}
|
||||
onChange={() => {
|
||||
toggleScheduleLock(rep.id)
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">Lock</span>
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{isLocked ? '🔒 Locked' : '🔓 Unlocked'}
|
||||
</span>
|
||||
</label>
|
||||
<button
|
||||
onClick={() => scheduleRepetition(rep.id, exp.id)}
|
||||
@@ -1275,79 +1293,8 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
)}
|
||||
{/* Week Calendar for selected conductors' availability */}
|
||||
<div className="mt-6 flex flex-col flex-1 min-h-0 overflow-hidden">
|
||||
<div className="flex justify-between items-center mb-3 flex-shrink-0">
|
||||
<div className="mb-3 flex-shrink-0">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Selected Conductors' Availability & Experiment Scheduling</h3>
|
||||
<div className="flex gap-2 items-center">
|
||||
{/* Day-by-day navigation buttons (only show in week view) */}
|
||||
{calendarView === Views.WEEK && (
|
||||
<div className="flex items-center gap-1 mr-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
const newDate = new Date(currentDate)
|
||||
newDate.setDate(newDate.getDate() - 1)
|
||||
setCurrentDate(newDate)
|
||||
}}
|
||||
className="px-2 py-1 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded border border-gray-300 dark:border-gray-600"
|
||||
title="Previous day"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const newDate = new Date(currentDate)
|
||||
newDate.setDate(newDate.getDate() + 1)
|
||||
setCurrentDate(newDate)
|
||||
}}
|
||||
className="px-2 py-1 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded border border-gray-300 dark:border-gray-600"
|
||||
title="Next day"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 ml-1">Day</span>
|
||||
</div>
|
||||
)}
|
||||
<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 ref={calendarRef} className="flex-1 min-h-0 border border-gray-200 dark:border-gray-700 rounded-md overflow-hidden relative">
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
@@ -1366,6 +1313,18 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
views={[Views.WEEK, Views.DAY]}
|
||||
dayLayoutAlgorithm="no-overlap"
|
||||
draggableAccessor={draggableAccessor}
|
||||
onSelectEvent={(event: any) => {
|
||||
// Handle clicking on repetition markers to toggle lock
|
||||
const resource = event.resource as string
|
||||
if (resource === 'soaking' || resource === 'airdrying' || resource === 'cracking') {
|
||||
const eventId = event.id as string
|
||||
const repId = eventId.split('-')[0]
|
||||
// Toggle lock for this repetition - this will update both checkbox and marker icons
|
||||
toggleScheduleLock(repId)
|
||||
// Prevent default popup behavior
|
||||
return false
|
||||
}
|
||||
}}
|
||||
onEventDrop={({ event, start }: { event: any, start: Date }) => {
|
||||
// Preserve scroll position before updating
|
||||
preserveScrollPosition()
|
||||
@@ -1408,17 +1367,18 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
|
||||
eventPropGetter={eventPropGetter}
|
||||
backgroundEventPropGetter={(event: any) => {
|
||||
// Styling for background events (conductor availability)
|
||||
const conductorId = event.resource
|
||||
const conductorId = event.resource as string
|
||||
const color = conductorColorMap[conductorId] || '#2563eb'
|
||||
// Use more visible colors - higher opacity for better visibility
|
||||
return {
|
||||
style: {
|
||||
backgroundColor: color + '40', // ~25% transparency for background
|
||||
backgroundColor: color + '60', // ~37% opacity for better visibility
|
||||
borderColor: color,
|
||||
borderWidth: '1px',
|
||||
borderWidth: '2px',
|
||||
borderStyle: 'solid',
|
||||
color: 'transparent',
|
||||
borderRadius: '4px',
|
||||
opacity: 0.6,
|
||||
opacity: 0.8,
|
||||
height: 'auto',
|
||||
minHeight: '20px',
|
||||
fontSize: '0px',
|
||||
|
||||
Reference in New Issue
Block a user