Commit changes before merging to main

This commit is contained in:
salirezav
2025-12-18 14:33:05 -05:00
parent 9159ab68f3
commit 6cf67822dc
20 changed files with 3571 additions and 1311 deletions

View File

@@ -0,0 +1,95 @@
# Scheduling Component Refactoring Summary
## Overview
The `Scheduling.tsx` file (originally 1832 lines) has been refactored into a modular structure with reusable components, hooks, and better organization.
## New Structure
```
scheduling-remote/src/components/
├── Scheduling.tsx # Backward compatibility re-export
└── scheduling/
├── types.ts # Shared types and interfaces
├── Scheduling.tsx # Main router component (~100 lines)
├── AvailabilityCalendar.tsx # Availability calendar component
├── ui/ # Reusable UI components
│ ├── BackButton.tsx # Back navigation button
│ ├── SchedulingCard.tsx # Card component for main view
│ ├── TimeSlotModal.tsx # Modal for adding time slots
│ └── DropdownCurtain.tsx # Reusable dropdown/accordion
├── views/ # Main view components
│ ├── ViewSchedule.tsx # View schedule component
│ ├── IndicateAvailability.tsx # Indicate availability view
│ └── ScheduleExperimentImpl.tsx # Temporary wrapper (TODO: further refactor)
└── hooks/ # Custom hooks
└── useConductors.ts # Conductor management hook
```
## What Was Extracted
### 1. **Shared Types** (`types.ts`)
- `CalendarEvent` interface
- `SchedulingProps` interface
- `SchedulingView` type
- `ScheduledRepetition` interface
- Re-exports of service types
### 2. **Reusable UI Components** (`ui/`)
- **BackButton**: Consistent back navigation
- **SchedulingCard**: Reusable card component for the main scheduling view
- **TimeSlotModal**: Modal for adding/editing time slots
- **DropdownCurtain**: Reusable accordion/dropdown component
### 3. **View Components** (`views/`)
- **ViewSchedule**: Complete schedule view (simplified, placeholder)
- **IndicateAvailability**: Wrapper for availability calendar
- **ScheduleExperimentImpl**: Temporary wrapper (original implementation still in old file)
### 4. **Custom Hooks** (`hooks/`)
- **useConductors**: Manages conductor data, selection, colors, and availability
### 5. **Main Components**
- **Scheduling.tsx**: Main router component (reduced from ~220 lines to ~100 lines)
- **AvailabilityCalendar.tsx**: Extracted availability calendar (moved from inline component)
## Benefits
1. **Maintainability**: Each component has a single, clear responsibility
2. **Reusability**: UI components can be reused across different views
3. **Testability**: Smaller units are easier to test in isolation
4. **Readability**: Easier to understand and navigate the codebase
5. **Organization**: Clear separation of concerns
## File Size Reduction
- **Original Scheduling.tsx**: 1832 lines
- **New main Scheduling.tsx**: ~100 lines (95% reduction)
- **Total new structure**: Better organized across multiple focused files
## Backward Compatibility
The original `Scheduling.tsx` file is maintained for backward compatibility and re-exports the new modular component. The `ScheduleExperiment` function is still exported from the original file location.
## TODO / Future Improvements
1. **Further refactor ScheduleExperiment**:
- Extract into smaller subcomponents (ConductorPanel, ExperimentPhasePanel, etc.)
- Create additional hooks (useExperimentPhases, useScheduling, useCalendarEvents)
- Move implementation from old file to new structure
2. **Additional reusable components**:
- Experiment item component
- Repetition item component
- Calendar controls component
3. **Testing**:
- Add unit tests for extracted components
- Add integration tests for hooks
## Migration Notes
- All imports from `./components/Scheduling` continue to work
- The new structure is in `./components/scheduling/`
- No breaking changes to the public API

View File

@@ -4,6 +4,9 @@
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);
height: 100%;
display: flex;
flex-direction: column;
}
/* Dark mode support */
@@ -65,6 +68,8 @@
.rbc-month-view {
border: 1px solid #e2e8f0;
border-radius: 8px;
flex: 1;
min-height: 0;
}
.dark .rbc-month-view {
@@ -73,6 +78,8 @@
.rbc-month-row {
border-bottom: 1px solid #e2e8f0;
flex: 1;
min-height: 100px;
}
.dark .rbc-month-row {
@@ -87,6 +94,10 @@
.rbc-time-view {
border: 1px solid #e2e8f0;
border-radius: 8px;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.dark .rbc-time-view {
@@ -105,6 +116,9 @@
.rbc-time-content {
background: white;
flex: 1;
min-height: 0;
overflow-y: auto;
}
.dark .rbc-time-content {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,218 @@
import { useEffect, useState } from 'react'
import { Calendar, momentLocalizer, Views } from 'react-big-calendar'
import moment from 'moment'
import type { User, CalendarEvent } from './types'
import { availabilityManagement } from '../../services/supabase'
import { BackButton } from './ui/BackButton'
import { TimeSlotModal } from './ui/TimeSlotModal'
import 'react-big-calendar/lib/css/react-big-calendar.css'
import '../CalendarStyles.css'
interface AvailabilityCalendarProps {
user: User
onBack?: () => void
}
export function AvailabilityCalendar({ user, onBack }: AvailabilityCalendarProps) {
const localizer = momentLocalizer(moment)
const [events, setEvents] = useState<CalendarEvent[]>([])
const [selectedDate, setSelectedDate] = useState<Date | null>(null)
const [showTimeSlotForm, setShowTimeSlotForm] = useState(false)
const [newTimeSlot, setNewTimeSlot] = useState({
startTime: '09:00',
endTime: '17:00'
})
const [currentView, setCurrentView] = useState(Views.MONTH)
const [currentDate, setCurrentDate] = useState(new Date())
// Load availability from DB on mount
useEffect(() => {
const loadAvailability = async () => {
try {
const records = await availabilityManagement.getMyAvailability()
const mapped: CalendarEvent[] = records.map(r => ({
id: r.id,
title: 'Available',
start: new Date(r.available_from),
end: new Date(r.available_to),
resource: 'available'
}))
setEvents(mapped)
} catch (e) {
console.error('Failed to load availability', e)
}
}
loadAvailability()
}, [])
const eventStyleGetter = (event: CalendarEvent) => {
return {
style: {
backgroundColor: '#10b981', // green-500 for available
borderColor: '#10b981',
color: 'white',
borderRadius: '4px',
border: 'none',
display: 'block'
}
}
}
const handleSelectSlot = ({ start, end }: { start: Date; end: Date }) => {
// Set the selected date and show the time slot form
setSelectedDate(start)
setShowTimeSlotForm(true)
// Pre-fill the form with the selected time range
const startTime = moment(start).format('HH:mm')
const endTime = moment(end).format('HH:mm')
setNewTimeSlot({
startTime,
endTime
})
}
const handleSelectEvent = async (event: CalendarEvent) => {
if (!window.confirm('Do you want to remove this availability?')) {
return
}
try {
if (typeof event.id === 'string') {
await availabilityManagement.deleteAvailability(event.id)
}
setEvents(events.filter(e => e.id !== event.id))
} catch (e: any) {
alert(e?.message || 'Failed to delete availability.')
console.error('Failed to delete availability', e)
}
}
const handleAddTimeSlot = async () => {
if (!selectedDate) return
const [startHour, startMinute] = newTimeSlot.startTime.split(':').map(Number)
const [endHour, endMinute] = newTimeSlot.endTime.split(':').map(Number)
const startDateTime = new Date(selectedDate)
startDateTime.setHours(startHour, startMinute, 0, 0)
const endDateTime = new Date(selectedDate)
endDateTime.setHours(endHour, endMinute, 0, 0)
// Check for overlapping events
const hasOverlap = events.some(event => {
const eventStart = new Date(event.start)
const eventEnd = new Date(event.end)
return (
eventStart.toDateString() === selectedDate.toDateString() &&
((startDateTime >= eventStart && startDateTime < eventEnd) ||
(endDateTime > eventStart && endDateTime <= eventEnd) ||
(startDateTime <= eventStart && endDateTime >= eventEnd))
)
})
if (hasOverlap) {
alert('This time slot overlaps with an existing availability. Please choose a different time.')
return
}
try {
// Persist to DB first to get real ID and server validation
const created = await availabilityManagement.createAvailability({
available_from: startDateTime.toISOString(),
available_to: endDateTime.toISOString()
})
const newEvent: CalendarEvent = {
id: created.id,
title: 'Available',
start: new Date(created.available_from),
end: new Date(created.available_to),
resource: 'available'
}
setEvents([...events, newEvent])
setShowTimeSlotForm(false)
setSelectedDate(null)
} catch (e: any) {
alert(e?.message || 'Failed to save availability. Please try again.')
console.error('Failed to create availability', e)
}
}
const handleCancelTimeSlot = () => {
setShowTimeSlotForm(false)
setSelectedDate(null)
}
const getEventsForDate = (date: Date) => {
return events.filter(event => {
const eventDate = new Date(event.start)
return eventDate.toDateString() === date.toDateString()
})
}
return (
<div className="space-y-6">
{/* Calendar Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
Availability Calendar
</h2>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
Click and drag to add availability slots, or click on existing events to remove them. You can add multiple time slots per day.
</p>
</div>
{/* Legend */}
<div className="flex items-center space-x-4 mt-4 sm:mt-0">
<div className="flex items-center">
<div className="w-3 h-3 bg-green-500 rounded mr-2"></div>
<span className="text-sm text-gray-600 dark:text-gray-400">Available</span>
</div>
</div>
</div>
{/* Time Slot Form Modal */}
{showTimeSlotForm && selectedDate && (
<TimeSlotModal
selectedDate={selectedDate}
newTimeSlot={newTimeSlot}
existingEvents={events}
onTimeSlotChange={setNewTimeSlot}
onAdd={handleAddTimeSlot}
onCancel={handleCancelTimeSlot}
/>
)}
{/* Calendar */}
<div className="h-[600px] flex flex-col">
<Calendar
localizer={localizer}
events={events}
startAccessor="start"
endAccessor="end"
style={{ height: '100%', minHeight: '600px' }}
view={currentView}
onView={setCurrentView}
date={currentDate}
onNavigate={setCurrentDate}
views={[Views.MONTH, Views.WEEK, Views.DAY]}
selectable
onSelectSlot={handleSelectSlot}
onSelectEvent={handleSelectEvent}
eventPropGetter={eventStyleGetter}
popup
showMultiDayTimes
step={30}
timeslots={2}
min={new Date(2024, 0, 1, 6, 0)} // 6:00 AM
max={new Date(2024, 0, 1, 22, 0)} // 10:00 PM
/>
</div>
</div>
)
}

View File

@@ -0,0 +1,120 @@
import type { SchedulingProps, SchedulingView } from './types'
import { ViewSchedule } from './views/ViewSchedule'
import { IndicateAvailability } from './views/IndicateAvailability'
import { SchedulingCard } from './ui/SchedulingCard'
// Import the original ScheduleExperiment for now
// TODO: Further refactor ScheduleExperiment into smaller components
import { ScheduleExperiment } from '../Scheduling'
export function Scheduling({ user, currentRoute }: SchedulingProps) {
// Extract current view from route
const getCurrentView = (): SchedulingView => {
if (currentRoute === '/scheduling') {
return 'main'
}
const match = currentRoute.match(/^\/scheduling\/(.+)$/)
if (match) {
const subRoute = match[1]
switch (subRoute) {
case 'view-schedule':
return 'view-schedule'
case 'indicate-availability':
return 'indicate-availability'
case 'schedule-experiment':
return 'schedule-experiment'
default:
return 'main'
}
}
return 'main'
}
const currentView = getCurrentView()
const handleCardClick = (view: SchedulingView) => {
const newPath = view === 'main' ? '/scheduling' : `/scheduling/${view}`
window.history.pushState({}, '', newPath)
// Trigger a popstate event to update the route
window.dispatchEvent(new PopStateEvent('popstate'))
}
const handleBackToMain = () => {
window.history.pushState({}, '', '/scheduling')
// Trigger a popstate event to update the route
window.dispatchEvent(new PopStateEvent('popstate'))
}
// Render different views based on currentView
if (currentView === 'view-schedule') {
return <ViewSchedule user={user} onBack={handleBackToMain} />
}
if (currentView === 'indicate-availability') {
return <IndicateAvailability user={user} onBack={handleBackToMain} />
}
if (currentView === 'schedule-experiment') {
return <ScheduleExperiment user={user} onBack={handleBackToMain} />
}
// Main view with cards
return (
<div className="p-6">
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
Scheduling
</h1>
<p className="text-gray-600 dark:text-gray-400">
This is the scheduling module of the dashboard. Here you can indicate your availability for upcoming experiment runs.
</p>
</div>
{/* Scheduling Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* View Complete Schedule Card */}
<SchedulingCard
title="View Complete Schedule"
description="View the complete schedule of all upcoming experiment runs and their current status."
icon={
<svg className="w-6 h-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
}
status={{ label: 'Available', color: 'green' }}
footer={{ left: 'All experiments', right: 'View Schedule' }}
onClick={() => handleCardClick('view-schedule')}
/>
{/* Indicate Availability Card */}
<SchedulingCard
title="Indicate Your Availability"
description="Set your availability preferences and time slots for upcoming experiment runs."
icon={
<svg className="w-6 h-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
}
status={{ label: 'Active', color: 'blue' }}
footer={{ left: 'Personal settings', right: 'Set Availability' }}
onClick={() => handleCardClick('indicate-availability')}
/>
{/* Schedule Experiment Card */}
<SchedulingCard
title="Schedule Experiment"
description="Schedule specific experiment runs and assign team members to upcoming sessions."
icon={
<svg className="w-6 h-6 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
}
status={{ label: 'Planning', color: 'yellow' }}
footer={{ left: 'Experiment planning', right: 'Schedule Now' }}
onClick={() => handleCardClick('schedule-experiment')}
/>
</div>
</div>
)
}

View File

@@ -0,0 +1,123 @@
import { useState, useEffect } from 'react'
import type { User, CalendarEvent } from '../types'
import { userManagement, supabase } from '../../../services/supabase'
const colorPalette = ['#2563eb', '#059669', '#d97706', '#7c3aed', '#dc2626', '#0ea5e9', '#16a34a', '#f59e0b', '#9333ea', '#ef4444']
export function useConductors() {
const [conductors, setConductors] = useState<User[]>([])
const [conductorIdsWithFutureAvailability, setConductorIdsWithFutureAvailability] = useState<Set<string>>(new Set())
const [selectedConductorIds, setSelectedConductorIds] = useState<Set<string>>(new Set())
const [availabilityEvents, setAvailabilityEvents] = useState<CalendarEvent[]>([])
const [conductorColorMap, setConductorColorMap] = useState<Record<string, string>>({})
useEffect(() => {
const loadConductors = async () => {
try {
const allUsers = await userManagement.getAllUsers()
const conductorsOnly = allUsers.filter(u => u.roles.includes('conductor'))
setConductors(conductorsOnly)
const conductorIds = conductorsOnly.map(c => c.id)
setConductorIdsWithFutureAvailability(new Set(conductorIds))
} catch (e) {
console.error('Failed to load conductors', e)
}
}
loadConductors()
}, [])
// Fetch availability for selected conductors and build calendar events
useEffect(() => {
const loadSelectedAvailability = async () => {
try {
const selectedIds = Array.from(selectedConductorIds)
if (selectedIds.length === 0) {
setAvailabilityEvents([])
return
}
// Assign consistent colors per conductor based on their position in the full conductors array
const newColorMap: Record<string, string> = {}
conductors.forEach((conductor, index) => {
if (selectedIds.includes(conductor.id)) {
newColorMap[conductor.id] = colorPalette[index % colorPalette.length]
}
})
setConductorColorMap(newColorMap)
// Fetch availability for selected conductors in a single query
const { data, error } = await supabase
.from('conductor_availability')
.select('*')
.in('user_id', selectedIds)
.eq('status', 'active')
.gt('available_to', new Date().toISOString())
.order('available_from', { ascending: true })
if (error) throw error
// Map user_id -> display name
const idToName: Record<string, string> = {}
conductors.forEach(c => {
const name = [c.first_name, c.last_name].filter(Boolean).join(' ') || c.email
idToName[c.id] = name
})
const events: CalendarEvent[] = (data || []).map((r: any) => {
const conductorId = r.user_id
return {
id: r.id,
title: `${idToName[conductorId] || 'Conductor'}`,
start: new Date(r.available_from),
end: new Date(r.available_to),
resource: conductorId
}
})
setAvailabilityEvents(events)
} catch (e) {
// Fail silently for calendar, do not break main UI
setAvailabilityEvents([])
}
}
loadSelectedAvailability()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedConductorIds, conductors])
const toggleConductor = (id: string) => {
setSelectedConductorIds(prev => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}
const toggleAllConductors = () => {
const availableConductorIds = conductors
.filter(c => conductorIdsWithFutureAvailability.has(c.id))
.map(c => c.id)
setSelectedConductorIds(prev => {
const allSelected = availableConductorIds.every(id => prev.has(id))
if (allSelected) {
return new Set()
} else {
return new Set(availableConductorIds)
}
})
}
return {
conductors,
conductorIdsWithFutureAvailability,
selectedConductorIds,
availabilityEvents,
conductorColorMap,
colorPalette,
toggleConductor,
toggleAllConductors
}
}

View File

@@ -0,0 +1,29 @@
import type { User, ExperimentPhase, Experiment, ExperimentRepetition, Soaking, Airdrying } from '../../services/supabase'
// Type definitions for calendar events
export interface CalendarEvent {
id: number | string
title: string
start: Date
end: Date
resource?: string
}
export interface SchedulingProps {
user: User
currentRoute: string
}
export type SchedulingView = 'main' | 'view-schedule' | 'indicate-availability' | 'schedule-experiment'
export interface ScheduledRepetition {
repetitionId: string
experimentId: string
soakingStart: Date | null
airdryingStart: Date | null
crackingStart: Date | null
}
// Re-export types for convenience
export type { User, ExperimentPhase, Experiment, ExperimentRepetition, Soaking, Airdrying }

View File

@@ -0,0 +1,19 @@
interface BackButtonProps {
onClick: () => void
label?: string
}
export function BackButton({ onClick, label = 'Back to Scheduling' }: BackButtonProps) {
return (
<button
onClick={onClick}
className="flex items-center text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 mb-4"
>
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
{label}
</button>
)
}

View File

@@ -0,0 +1,35 @@
import { ReactNode } from 'react'
interface DropdownCurtainProps {
title: string
expanded: boolean
onToggle: () => void
children: ReactNode
headerAction?: ReactNode
className?: string
}
export function DropdownCurtain({ title, expanded, onToggle, children, headerAction, className = '' }: DropdownCurtainProps) {
return (
<div className={`border border-gray-200 dark:border-gray-700 rounded-md divide-y divide-gray-200 dark:divide-gray-700 ${className}`}>
<button
className="w-full flex items-center justify-between p-3 hover:bg-gray-50 dark:hover:bg-gray-700/50"
onClick={onToggle}
>
<span className="text-sm font-medium text-gray-900 dark:text-white">{title}</span>
<div className="flex items-center gap-2">
{headerAction}
<svg className={`w-4 h-4 text-gray-500 transition-transform ${expanded ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
</button>
{expanded && (
<div className="bg-gray-50 dark:bg-gray-800/50">
{children}
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,65 @@
import type { SchedulingView } from '../types'
interface SchedulingCardProps {
title: string
description: string
icon: React.ReactNode
status: {
label: string
color: 'green' | 'blue' | 'yellow'
}
footer: {
left: string
right: string
}
onClick: () => void
}
export function SchedulingCard({ title, description, icon, status, footer, onClick }: SchedulingCardProps) {
const statusColors = {
green: 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-400',
blue: 'bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-400',
yellow: 'bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-400',
}
return (
<div
onClick={onClick}
className="bg-white dark:bg-gray-800 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200 cursor-pointer border border-gray-200 dark:border-gray-700 hover:border-blue-300"
>
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center">
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/20 rounded-lg flex items-center justify-center">
{icon}
</div>
</div>
<div className="text-right">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusColors[status.color]}`}>
{status.label}
</span>
</div>
</div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
{title}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-2">
{description}
</p>
<div className="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
<span>{footer.left}</span>
<div className="flex items-center text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300">
<span className="mr-1">{footer.right}</span>
<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>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,123 @@
import moment from 'moment'
import type { CalendarEvent } from '../types'
interface TimeSlotModalProps {
selectedDate: Date
newTimeSlot: {
startTime: string
endTime: string
}
existingEvents: CalendarEvent[]
onTimeSlotChange: (timeSlot: { startTime: string; endTime: string }) => void
onAdd: () => void
onCancel: () => void
}
export function TimeSlotModal({
selectedDate,
newTimeSlot,
existingEvents,
onTimeSlotChange,
onAdd,
onCancel
}: TimeSlotModalProps) {
const getEventsForDate = (date: Date) => {
return existingEvents.filter(event => {
const eventDate = new Date(event.start)
return eventDate.toDateString() === date.toDateString()
})
}
return (
<div className="fixed inset-0 flex items-center justify-center overflow-y-auto modal z-999999">
<div
className="fixed inset-0 h-full w-full bg-gray-400/50 backdrop-blur-[2px]"
onClick={onCancel}
/>
<div className="relative w-full rounded-2xl bg-white shadow-theme-xl dark:bg-gray-900 max-w-md mx-auto p-4" onClick={(e) => e.stopPropagation()}>
{/* Close Button */}
<button
onClick={onCancel}
className="absolute right-3 top-3 z-999 flex h-9.5 w-9.5 items-center justify-center rounded-full bg-gray-100 text-gray-400 transition-colors hover:bg-gray-200 hover:text-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white sm:right-6 sm:top-6 sm:h-11 sm:w-11"
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.04289 16.5413C5.65237 16.9318 5.65237 17.565 6.04289 17.9555C6.43342 18.346 7.06658 18.346 7.45711 17.9555L11.9987 13.4139L16.5408 17.956C16.9313 18.3466 17.5645 18.3466 17.955 17.956C18.3455 17.5655 18.3455 16.9323 17.955 16.5418L13.4129 11.9997L17.955 7.4576C18.3455 7.06707 18.3455 6.43391 17.955 6.04338C17.5645 5.65286 16.9313 5.65286 16.5408 6.04338L11.9987 10.5855L7.45711 6.0439C7.06658 5.65338 6.43342 5.65338 6.04289 6.0439C5.65237 6.43442 5.65237 7.06759 6.04289 7.45811L10.5845 11.9997L6.04289 16.5413Z"
fill="currentColor"
/>
</svg>
</button>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Add Availability for {moment(selectedDate).format('MMMM D, YYYY')}
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Start Time
</label>
<input
type="time"
value={newTimeSlot.startTime}
onChange={(e) => onTimeSlotChange({ ...newTimeSlot, startTime: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
End Time
</label>
<input
type="time"
value={newTimeSlot.endTime}
onChange={(e) => onTimeSlotChange({ ...newTimeSlot, endTime: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
/>
</div>
</div>
{/* Show existing time slots for this date */}
{getEventsForDate(selectedDate).length > 0 && (
<div className="mt-4">
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Existing time slots:
</h4>
<div className="space-y-1">
{getEventsForDate(selectedDate).map(event => (
<div key={event.id} className="text-sm text-gray-600 dark:text-gray-400">
{moment(event.start).format('HH:mm')} - {moment(event.end).format('HH:mm')} ({event.title})
</div>
))}
</div>
</div>
)}
<div className="flex justify-end space-x-3 mt-6">
<button
onClick={onCancel}
className="px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 transition-colors"
>
Cancel
</button>
<button
onClick={onAdd}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Add Time Slot
</button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,29 @@
import type { User } from '../types'
import { BackButton } from '../ui/BackButton'
import { AvailabilityCalendar } from '../AvailabilityCalendar'
interface IndicateAvailabilityProps {
user: User
onBack: () => void
}
export function IndicateAvailability({ user, onBack }: IndicateAvailabilityProps) {
return (
<div className="p-6">
<div className="mb-6">
<BackButton onClick={onBack} />
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
Indicate Availability
</h1>
<p className="text-gray-600 dark:text-gray-400">
Set your availability preferences and time slots for upcoming experiment runs.
</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<AvailabilityCalendar user={user} />
</div>
</div>
)
}

View File

@@ -0,0 +1,6 @@
// This is a temporary wrapper that imports the original ScheduleExperiment logic
// TODO: Further refactor this component into smaller subcomponents and hooks
// For now, we're keeping the original implementation but moving it to its own file
export { ScheduleExperiment } from './ScheduleExperimentImpl'

View File

@@ -0,0 +1,9 @@
// Temporary implementation - imports the original ScheduleExperiment from the old file
// TODO: Further refactor this into smaller components and hooks
// For now, we're keeping it in the original file but will move it here later
import type { User } from '../types'
// Re-export the original implementation from the old file location
export { ScheduleExperiment } from '../../Scheduling'

View File

@@ -0,0 +1,41 @@
import type { User } from '../types'
import { BackButton } from '../ui/BackButton'
interface ViewScheduleProps {
user: User
onBack: () => void
}
export function ViewSchedule({ user, onBack }: ViewScheduleProps) {
return (
<div className="p-6">
<div className="mb-6">
<BackButton onClick={onBack} />
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
Complete Schedule
</h1>
<p className="text-gray-600 dark:text-gray-400">
View all scheduled experiment runs and their current status.
</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<div className="text-center py-12">
<div className="mx-auto w-16 h-16 bg-blue-100 dark:bg-blue-900/20 rounded-full flex items-center justify-center mb-4">
<svg className="w-8 h-8 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
Complete Schedule View
</h3>
<p className="text-gray-500 dark:text-gray-400 max-w-md mx-auto">
This view will show a comprehensive calendar and list of all scheduled experiment runs,
including dates, times, assigned team members, and current status.
</p>
</div>
</div>
</div>
)
}