Merge remote-tracking branch 'old-github/main' into integrate-old-refactors-of-github

This commit is contained in:
Alireza Vaezi
2026-03-09 13:10:08 -04:00
35 changed files with 2878 additions and 688 deletions

View File

@@ -70,8 +70,11 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
// Track repetitions that have been dropped/moved and should show time points
const [repetitionsWithTimes, setRepetitionsWithTimes] = useState<Set<string>>(new Set())
<<<<<<< HEAD
// Track which repetitions are locked (prevent dragging)
const [lockedSchedules, setLockedSchedules] = useState<Set<string>>(new Set())
=======
>>>>>>> old-github/main
// Track which repetitions are currently being scheduled
const [schedulingRepetitions, setSchedulingRepetitions] = useState<Set<string>>(new Set())
// Track conductor assignments for each phase marker (markerId -> conductorIds[])
@@ -253,6 +256,7 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
}
const toggleRepetition = (repId: string) => {
<<<<<<< HEAD
setSelectedRepetitionIds(prev => {
const next = new Set(prev)
if (next.has(repId)) {
@@ -291,6 +295,24 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
// Re-stagger all existing repetitions to prevent overlap
// Note: reStaggerRepetitions will automatically skip locked repetitions
reStaggerRepetitions([...next, repId])
=======
// Checking/unchecking should only control visibility on the timeline.
// It must NOT clear scheduling info or conductor assignments.
setSelectedRepetitionIds(prev => {
const next = new Set(prev)
if (next.has(repId)) {
// Hide this repetition from the timeline
next.delete(repId)
// Keep scheduledRepetitions and repetitionsWithTimes intact so that
// re-checking the box restores the repetition in the correct spot.
} else {
// Show this repetition on the timeline
next.add(repId)
// Auto-spawn when checked - pass the updated set to ensure correct stagger calculation
// spawnSingleRepetition will position the new repetition relative to existing ones
// without resetting existing positions
spawnSingleRepetition(repId, next)
>>>>>>> old-github/main
}
return next
})
@@ -305,6 +327,7 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
const allSelected = allRepetitions.every(rep => prev.has(rep.id))
if (allSelected) {
<<<<<<< HEAD
// Deselect all repetitions in this phase
const next = new Set(prev)
allRepetitions.forEach(rep => {
@@ -319,6 +342,16 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
return next
} else {
// Select all repetitions in this phase
=======
// Deselect all repetitions in this phase (hide from timeline only)
const next = new Set(prev)
allRepetitions.forEach(rep => {
next.delete(rep.id)
})
return next
} else {
// Select all repetitions in this phase (show on timeline)
>>>>>>> old-github/main
const next = new Set(prev)
allRepetitions.forEach(rep => {
next.add(rep.id)
@@ -356,7 +389,11 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
// Re-stagger all repetitions to prevent overlap
// IMPORTANT: Skip locked repetitions to prevent them from moving
<<<<<<< HEAD
const reStaggerRepetitions = useCallback((repIds: string[]) => {
=======
const reStaggerRepetitions = useCallback((repIds: string[], onlyResetWithoutCustomTimes: boolean = false) => {
>>>>>>> old-github/main
const tomorrow = new Date()
tomorrow.setDate(tomorrow.getDate() + 1)
tomorrow.setHours(9, 0, 0, 0)
@@ -364,8 +401,16 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
setScheduledRepetitions(prev => {
const newScheduled = { ...prev }
<<<<<<< HEAD
// Filter out locked repetitions - they should not be moved
const unlockedRepIds = repIds.filter(repId => !lockedSchedules.has(repId))
=======
// If onlyResetWithoutCustomTimes is true, filter out repetitions that have custom times set
let unlockedRepIds = repIds
if (onlyResetWithoutCustomTimes) {
unlockedRepIds = unlockedRepIds.filter(repId => !repetitionsWithTimes.has(repId))
}
>>>>>>> old-github/main
// Calculate stagger index only for unlocked repetitions
let staggerIndex = 0
@@ -407,7 +452,11 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
return newScheduled
})
<<<<<<< HEAD
}, [lockedSchedules, repetitionsByExperiment, experimentsByPhase, soakingByExperiment, airdryingByExperiment])
=======
}, [repetitionsByExperiment, experimentsByPhase, soakingByExperiment, airdryingByExperiment, repetitionsWithTimes])
>>>>>>> old-github/main
// Spawn a single repetition in calendar
const spawnSingleRepetition = (repId: string, updatedSelectedIds?: Set<string>) => {
@@ -477,10 +526,18 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
let newScheduled = { ...prev }
const clampToReasonableHours = (d: Date) => {
<<<<<<< HEAD
const min = new Date(d)
min.setHours(5, 0, 0, 0)
const max = new Date(d)
max.setHours(23, 0, 0, 0)
=======
// Allow full 24 hours (midnight to midnight)
const min = new Date(d)
min.setHours(0, 0, 0, 0)
const max = new Date(d)
max.setHours(23, 59, 59, 999)
>>>>>>> old-github/main
const t = d.getTime()
return new Date(Math.min(Math.max(t, min.getTime()), max.getTime()))
}
@@ -536,6 +593,7 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
const repetition = repetitionsByExperiment[scheduled.experimentId]?.find(r => r.id === scheduled.repetitionId)
if (experiment && repetition && scheduled.soakingStart) {
<<<<<<< HEAD
const isLocked = lockedSchedules.has(scheduled.repetitionId)
const lockIcon = isLocked ? '🔒' : '🔓'
@@ -543,6 +601,12 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
events.push({
id: `${scheduled.repetitionId}-soaking`,
title: `${lockIcon} 💧 Soaking - Exp ${experiment.experiment_number} Rep ${repetition.repetition_number}`,
=======
// Soaking marker
events.push({
id: `${scheduled.repetitionId}-soaking`,
title: `💧 Soaking - Exp ${experiment.experiment_number} Rep ${repetition.repetition_number}`,
>>>>>>> old-github/main
start: scheduled.soakingStart,
end: new Date(scheduled.soakingStart.getTime() + 15 * 60000), // 15 minute duration for better visibility
resource: 'soaking'
@@ -552,7 +616,11 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
if (scheduled.airdryingStart) {
events.push({
id: `${scheduled.repetitionId}-airdrying`,
<<<<<<< HEAD
title: `${lockIcon} 🌬️ Airdrying - Exp ${experiment.experiment_number} Rep ${repetition.repetition_number}`,
=======
title: `🌬️ Airdrying - Exp ${experiment.experiment_number} Rep ${repetition.repetition_number}`,
>>>>>>> old-github/main
start: scheduled.airdryingStart,
end: new Date(scheduled.airdryingStart.getTime() + 15 * 60000), // 15 minute duration for better visibility
resource: 'airdrying'
@@ -563,7 +631,11 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
if (scheduled.crackingStart) {
events.push({
id: `${scheduled.repetitionId}-cracking`,
<<<<<<< HEAD
title: `${lockIcon} ⚡ Cracking - Exp ${experiment.experiment_number} Rep ${repetition.repetition_number}`,
=======
title: `⚡ Cracking - Exp ${experiment.experiment_number} Rep ${repetition.repetition_number}`,
>>>>>>> old-github/main
start: scheduled.crackingStart,
end: new Date(scheduled.crackingStart.getTime() + 15 * 60000), // 15 minute duration for better visibility
resource: 'cracking'
@@ -573,7 +645,11 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
})
return events
<<<<<<< HEAD
}, [scheduledRepetitions, experimentsByPhase, repetitionsByExperiment, lockedSchedules])
=======
}, [scheduledRepetitions, experimentsByPhase, repetitionsByExperiment])
>>>>>>> old-github/main
// Memoize the calendar events
const calendarEvents = useMemo(() => {
@@ -609,6 +685,7 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
return moment(date).format('MMM D, h:mm A')
}
<<<<<<< HEAD
const toggleScheduleLock = (repId: string) => {
setLockedSchedules(prev => {
const next = new Set(prev)
@@ -618,6 +695,18 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
next.add(repId)
}
return next
=======
// Remove all conductor assignments from a repetition
const removeRepetitionAssignments = (repId: string) => {
const markerIdPrefix = repId
setConductorAssignments(prev => {
const newAssignments = { ...prev }
// Remove assignments for all three phases
delete newAssignments[`${markerIdPrefix}-soaking`]
delete newAssignments[`${markerIdPrefix}-airdrying`]
delete newAssignments[`${markerIdPrefix}-cracking`]
return newAssignments
>>>>>>> old-github/main
})
}
@@ -625,6 +714,7 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
// Only make repetition markers draggable, not availability events
const resource = event.resource as string
if (resource === 'soaking' || resource === 'airdrying' || resource === 'cracking') {
<<<<<<< HEAD
// Check if the repetition is locked
const eventId = event.id as string
const repId = eventId.split('-')[0]
@@ -633,16 +723,25 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
}
return false
}, [lockedSchedules])
=======
return true
}
return false
}, [])
>>>>>>> old-github/main
const eventPropGetter = useCallback((event: any) => {
const resource = event.resource as string
// Styling for repetition markers (foreground events)
if (resource === 'soaking' || resource === 'airdrying' || resource === 'cracking') {
<<<<<<< HEAD
const eventId = event.id as string
const repId = eventId.split('-')[0]
const isLocked = lockedSchedules.has(repId)
=======
>>>>>>> old-github/main
const colors = {
soaking: '#3b82f6', // blue
airdrying: '#10b981', // green
@@ -652,8 +751,13 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
return {
style: {
<<<<<<< HEAD
backgroundColor: isLocked ? '#9ca3af' : color, // gray if locked
borderColor: isLocked ? color : color, // border takes original color when locked
=======
backgroundColor: color,
borderColor: color,
>>>>>>> old-github/main
color: 'white',
borderRadius: '8px',
border: '2px solid',
@@ -674,17 +778,28 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
<<<<<<< HEAD
cursor: isLocked ? 'not-allowed' : 'grab',
boxShadow: isLocked ? '0 1px 2px rgba(0,0,0,0.1)' : '0 2px 4px rgba(0,0,0,0.2)',
transition: 'all 0.2s ease',
opacity: isLocked ? 0.7 : 1
=======
cursor: 'grab',
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
transition: 'all 0.2s ease',
opacity: 1
>>>>>>> old-github/main
}
}
}
// Default styling for other events
return {}
<<<<<<< HEAD
}, [lockedSchedules])
=======
}, [])
>>>>>>> old-github/main
const scheduleRepetition = async (repId: string, experimentId: string) => {
setSchedulingRepetitions(prev => new Set(prev).add(repId))
@@ -756,6 +871,54 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
}
}
<<<<<<< HEAD
=======
// Unschedule a repetition: clear its scheduling info and unassign all conductors.
const unscheduleRepetition = async (repId: string, experimentId: string) => {
setSchedulingRepetitions(prev => new Set(prev).add(repId))
try {
// Remove all conductor assignments for this repetition
removeRepetitionAssignments(repId)
// Clear scheduled_date on the repetition in local state
setRepetitionsByExperiment(prev => ({
...prev,
[experimentId]: prev[experimentId]?.map(r =>
r.id === repId ? { ...r, scheduled_date: null } : r
) || []
}))
// Clear scheduled times for this repetition so it disappears from the timeline
setScheduledRepetitions(prev => {
const next = { ...prev }
delete next[repId]
return next
})
// This repetition no longer has active times
setRepetitionsWithTimes(prev => {
const next = new Set(prev)
next.delete(repId)
return next
})
// Also clear scheduled_date in the database for this repetition
await repetitionManagement.updateRepetition(repId, {
scheduled_date: null
})
} catch (error: any) {
setError(error?.message || 'Failed to unschedule repetition')
} finally {
setSchedulingRepetitions(prev => {
const next = new Set(prev)
next.delete(repId)
return next
})
}
}
>>>>>>> old-github/main
// Restore scroll position after scheduledRepetitions changes
useEffect(() => {
if (scrollPositionRef.current) {
@@ -806,11 +969,22 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
phase: 'soaking' | 'airdrying' | 'cracking'
startTime: Date
assignedConductors: string[]
<<<<<<< HEAD
locked: boolean
=======
>>>>>>> old-github/main
}> = []
Object.values(scheduledRepetitions).forEach(scheduled => {
const repId = scheduled.repetitionId
<<<<<<< HEAD
=======
// Only include markers for repetitions that are checked (selected)
if (!selectedRepetitionIds.has(repId)) {
return
}
>>>>>>> old-github/main
const markerIdPrefix = repId
if (scheduled.soakingStart) {
@@ -820,8 +994,12 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
experimentId: scheduled.experimentId,
phase: 'soaking',
startTime: scheduled.soakingStart,
<<<<<<< HEAD
assignedConductors: conductorAssignments[`${markerIdPrefix}-soaking`] || [],
locked: lockedSchedules.has(repId)
=======
assignedConductors: conductorAssignments[`${markerIdPrefix}-soaking`] || []
>>>>>>> old-github/main
})
}
@@ -832,8 +1010,12 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
experimentId: scheduled.experimentId,
phase: 'airdrying',
startTime: scheduled.airdryingStart,
<<<<<<< HEAD
assignedConductors: conductorAssignments[`${markerIdPrefix}-airdrying`] || [],
locked: lockedSchedules.has(repId)
=======
assignedConductors: conductorAssignments[`${markerIdPrefix}-airdrying`] || []
>>>>>>> old-github/main
})
}
@@ -844,8 +1026,12 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
experimentId: scheduled.experimentId,
phase: 'cracking',
startTime: scheduled.crackingStart,
<<<<<<< HEAD
assignedConductors: conductorAssignments[`${markerIdPrefix}-cracking`] || [],
locked: lockedSchedules.has(repId)
=======
assignedConductors: conductorAssignments[`${markerIdPrefix}-cracking`] || []
>>>>>>> old-github/main
})
}
})
@@ -856,7 +1042,70 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
conductorAvailabilities,
phaseMarkers
}
<<<<<<< HEAD
}, [selectedConductorIds, conductors, conductorColorMap, colorPalette, availabilityEvents, scheduledRepetitions, conductorAssignments, lockedSchedules, calendarStartDate, calendarZoom])
=======
}, [selectedConductorIds, conductors, conductorColorMap, colorPalette, availabilityEvents, scheduledRepetitions, conductorAssignments, calendarStartDate, calendarZoom, selectedRepetitionIds])
// Build repetition metadata mapping for timeline display
const repetitionMetadata = useMemo(() => {
const metadata: Record<string, { phaseName: string; experimentNumber: number; repetitionNumber: number; experimentId: string; isScheduledInDb: boolean }> = {}
Object.values(scheduledRepetitions).forEach(scheduled => {
const repId = scheduled.repetitionId
// Only include metadata for repetitions that are checked (selected)
if (!selectedRepetitionIds.has(repId)) {
return
}
const experiment = Object.values(experimentsByPhase).flat().find(e => e.id === scheduled.experimentId)
const repetition = Object.values(repetitionsByExperiment).flat().find(r => r.id === repId)
const phase = phases.find(p =>
Object.values(experimentsByPhase[p.id] || []).some(e => e.id === scheduled.experimentId)
)
if (experiment && repetition && phase) {
metadata[repId] = {
phaseName: phase.name,
experimentNumber: experiment.experiment_number,
repetitionNumber: repetition.repetition_number,
experimentId: scheduled.experimentId,
// Consider a repetition \"scheduled\" in DB if it has a non-null scheduled_date
isScheduledInDb: Boolean(repetition.scheduled_date)
}
}
})
return metadata
}, [scheduledRepetitions, experimentsByPhase, repetitionsByExperiment, phases, selectedRepetitionIds])
// Scroll to repetition in accordion
const handleScrollToRepetition = useCallback(async (repetitionId: string) => {
// First, expand the phase if it's collapsed
const repetition = Object.values(repetitionsByExperiment).flat().find(r => r.id === repetitionId)
if (repetition) {
const experiment = Object.values(experimentsByPhase).flat().find(e =>
(repetitionsByExperiment[e.id] || []).some(r => r.id === repetitionId)
)
if (experiment) {
const phase = phases.find(p =>
(experimentsByPhase[p.id] || []).some(e => e.id === experiment.id)
)
if (phase && !expandedPhaseIds.has(phase.id)) {
await togglePhaseExpand(phase.id)
// Wait a bit for the accordion to expand
await new Promise(resolve => setTimeout(resolve, 300))
}
}
}
// Then scroll to the element
const element = document.getElementById(`repetition-${repetitionId}`)
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
}, [repetitionsByExperiment, experimentsByPhase, phases, expandedPhaseIds, togglePhaseExpand])
>>>>>>> old-github/main
// Handlers for horizontal calendar
const handleHorizontalMarkerDrag = useCallback((markerId: string, newTime: Date) => {
@@ -878,6 +1127,7 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
}))
}, [])
<<<<<<< HEAD
const handleHorizontalMarkerLockToggle = useCallback((markerId: string) => {
// Marker ID format: ${repId}-${phase} where repId is a UUID with hyphens
// Split by '-' and take all but the last segment as repId
@@ -893,6 +1143,8 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
return next
})
}, [])
=======
>>>>>>> old-github/main
return (
@@ -1027,7 +1279,13 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
phaseMarkers={horizontalCalendarData.phaseMarkers}
onMarkerDrag={handleHorizontalMarkerDrag}
onMarkerAssignConductors={handleHorizontalMarkerAssignConductors}
<<<<<<< HEAD
onMarkerLockToggle={handleHorizontalMarkerLockToggle}
=======
repetitionMetadata={repetitionMetadata}
onScrollToRepetition={handleScrollToRepetition}
onScheduleRepetition={scheduleRepetition}
>>>>>>> old-github/main
timeStep={15}
minHour={6}
maxHour={22}
@@ -1196,11 +1454,29 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
const checked = selectedRepetitionIds.has(rep.id)
const hasTimes = repetitionsWithTimes.has(rep.id)
const scheduled = scheduledRepetitions[rep.id]
<<<<<<< HEAD
const isLocked = lockedSchedules.has(rep.id)
const isScheduling = schedulingRepetitions.has(rep.id)
return (
<div key={rep.id} className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded p-3">
=======
const isScheduling = schedulingRepetitions.has(rep.id)
// Check if there are any conductor assignments
const markerIdPrefix = rep.id
const soakingConductors = conductorAssignments[`${markerIdPrefix}-soaking`] || []
const airdryingConductors = conductorAssignments[`${markerIdPrefix}-airdrying`] || []
const crackingConductors = conductorAssignments[`${markerIdPrefix}-cracking`] || []
const hasAssignments = soakingConductors.length > 0 || airdryingConductors.length > 0 || crackingConductors.length > 0
return (
<div
key={rep.id}
id={`repetition-${rep.id}`}
className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded p-3"
>
>>>>>>> old-github/main
{/* Checkbox row */}
<label className="flex items-center gap-2">
<input
@@ -1212,6 +1488,7 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Rep {rep.repetition_number}</span>
</label>
<<<<<<< HEAD
{/* Time points (shown only if has been dropped/moved) */}
{hasTimes && scheduled && (
<div className="mt-2 ml-6 text-xs space-y-1">
@@ -1250,6 +1527,91 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
>
{isScheduling ? 'Scheduling...' : 'Schedule'}
</button>
=======
{/* Time points (shown whenever the repetition has scheduled times) */}
{scheduled && (
<div className="mt-2 ml-6 text-xs space-y-1">
{(() => {
const repId = rep.id
const markerIdPrefix = repId
// Get assigned conductors for each phase
const soakingConductors = conductorAssignments[`${markerIdPrefix}-soaking`] || []
const airdryingConductors = conductorAssignments[`${markerIdPrefix}-airdrying`] || []
const crackingConductors = conductorAssignments[`${markerIdPrefix}-cracking`] || []
// Helper to get conductor names
const getConductorNames = (conductorIds: string[]) => {
return conductorIds.map(id => {
const conductor = conductors.find(c => c.id === id)
if (!conductor) return null
return [conductor.first_name, conductor.last_name].filter(Boolean).join(' ') || conductor.email
}).filter(Boolean).join(', ')
}
return (
<>
<div className="flex items-center gap-2 flex-wrap">
<span>💧</span>
<span>Soaking: {formatTime(scheduled.soakingStart)}</span>
{soakingConductors.length > 0 && (
<span className="text-blue-600 dark:text-blue-400">
({getConductorNames(soakingConductors)})
</span>
)}
</div>
<div className="flex items-center gap-2 flex-wrap">
<span>🌬️</span>
<span>Airdrying: {formatTime(scheduled.airdryingStart)}</span>
{airdryingConductors.length > 0 && (
<span className="text-green-600 dark:text-green-400">
({getConductorNames(airdryingConductors)})
</span>
)}
</div>
<div className="flex items-center gap-2 flex-wrap">
<span>⚡</span>
<span>Cracking: {formatTime(scheduled.crackingStart)}</span>
{crackingConductors.length > 0 && (
<span className="text-orange-600 dark:text-orange-400">
({getConductorNames(crackingConductors)})
</span>
)}
</div>
</>
)
})()}
{/* Remove Assignments button and Schedule/Unschedule button */}
<div className="flex items-center gap-3 mt-3 pt-2 border-t border-gray-200 dark:border-gray-600">
{hasAssignments && (
<button
onClick={() => removeRepetitionAssignments(rep.id)}
disabled={Boolean(rep.scheduled_date)}
className="px-3 py-1 bg-red-600 hover:bg-red-700 disabled:bg-gray-400 text-white rounded text-xs transition-colors"
title={rep.scheduled_date ? "Unschedule the repetition first before removing assignments" : "Remove all conductor assignments from this repetition"}
>
Remove Assignments
</button>
)}
{rep.scheduled_date ? (
<button
onClick={() => unscheduleRepetition(rep.id, exp.id)}
disabled={isScheduling}
className="px-3 py-1 bg-yellow-600 hover:bg-yellow-700 disabled:bg-gray-400 text-white rounded text-xs transition-colors"
>
{isScheduling ? 'Unscheduling...' : 'Unschedule'}
</button>
) : (
<button
onClick={() => scheduleRepetition(rep.id, exp.id)}
disabled={isScheduling}
className="px-3 py-1 bg-green-600 hover:bg-green-700 disabled:bg-gray-400 text-white rounded text-xs transition-colors"
>
{isScheduling ? 'Scheduling...' : 'Schedule'}
</button>
)}
>>>>>>> old-github/main
</div>
</div>
)}