diff --git a/src/components/Experiments.tsx b/src/components/Experiments.tsx index 0079984..da1edf5 100644 --- a/src/components/Experiments.tsx +++ b/src/components/Experiments.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react' import { ExperimentModal } from './ExperimentModal' +import { ScheduleModal } from './ScheduleModal' import { experimentManagement, userManagement } from '../lib/supabase' import type { Experiment, User, ScheduleStatus, ResultsStatus } from '../lib/supabase' @@ -11,6 +12,8 @@ export function Experiments() { const [editingExperiment, setEditingExperiment] = useState(undefined) const [currentUser, setCurrentUser] = useState(null) const [filterStatus, setFilterStatus] = useState('all') + const [showScheduleModal, setShowScheduleModal] = useState(false) + const [schedulingExperiment, setSchedulingExperiment] = useState(undefined) useEffect(() => { loadData() @@ -56,6 +59,21 @@ export function Experiments() { // Add new experiment setExperiments(prev => [experiment, ...prev]) } + setShowModal(false) + setEditingExperiment(undefined) + } + + const handleScheduleExperiment = (experiment: Experiment) => { + setSchedulingExperiment(experiment) + setShowScheduleModal(true) + } + + const handleScheduleUpdated = (updatedExperiment: Experiment) => { + setExperiments(prev => prev.map(exp => + exp.id === updatedExperiment.id ? updatedExperiment : exp + )) + setShowScheduleModal(false) + setSchedulingExperiment(undefined) } const handleDeleteExperiment = async (experiment: Experiment) => { @@ -198,6 +216,11 @@ export function Experiments() { Schedule Status + {canManageExperiments && ( + + Scheduled Date/Time + + )} Results Status @@ -236,6 +259,29 @@ export function Experiments() { {experiment.schedule_status} + {canManageExperiments && ( + +
+ + {experiment.scheduled_date && ( + + {new Date(experiment.scheduled_date).toLocaleString()} + + )} +
+ + )} {experiment.results_status} @@ -301,7 +347,7 @@ export function Experiments() { )} - {/* Modal */} + {/* Experiment Modal */} {showModal && ( )} + + {/* Schedule Modal */} + {showScheduleModal && schedulingExperiment && ( + setShowScheduleModal(false)} + onScheduleUpdated={handleScheduleUpdated} + /> + )} ) } diff --git a/src/components/ScheduleModal.tsx b/src/components/ScheduleModal.tsx new file mode 100644 index 0000000..27bbc27 --- /dev/null +++ b/src/components/ScheduleModal.tsx @@ -0,0 +1,206 @@ +import { useState } from 'react' +import { experimentManagement } from '../lib/supabase' +import type { Experiment } from '../lib/supabase' + +interface ScheduleModalProps { + experiment: Experiment + onClose: () => void + onScheduleUpdated: (experiment: Experiment) => void +} + +export function ScheduleModal({ experiment, onClose, onScheduleUpdated }: ScheduleModalProps) { + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + // Initialize with existing scheduled date or current date/time + const getInitialDateTime = () => { + if (experiment.scheduled_date) { + const date = new Date(experiment.scheduled_date) + return { + date: date.toISOString().split('T')[0], + time: date.toTimeString().slice(0, 5) + } + } + + const now = new Date() + // Set to next hour by default + now.setHours(now.getHours() + 1, 0, 0, 0) + return { + date: now.toISOString().split('T')[0], + time: now.toTimeString().slice(0, 5) + } + } + + const [dateTime, setDateTime] = useState(getInitialDateTime()) + + const isScheduled = !!experiment.scheduled_date + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError(null) + setLoading(true) + + try { + // Validate date/time + const selectedDateTime = new Date(`${dateTime.date}T${dateTime.time}`) + const now = new Date() + + if (selectedDateTime <= now) { + setError('Scheduled date and time must be in the future') + setLoading(false) + return + } + + // Schedule the experiment + const updatedExperiment = await experimentManagement.scheduleExperiment( + experiment.id, + selectedDateTime.toISOString() + ) + + onScheduleUpdated(updatedExperiment) + onClose() + } catch (err: any) { + setError(err.message || 'Failed to schedule experiment') + console.error('Schedule experiment error:', err) + } finally { + setLoading(false) + } + } + + const handleRemoveSchedule = async () => { + if (!confirm('Are you sure you want to remove the schedule for this experiment?')) { + return + } + + setError(null) + setLoading(true) + + try { + const updatedExperiment = await experimentManagement.removeExperimentSchedule(experiment.id) + onScheduleUpdated(updatedExperiment) + onClose() + } catch (err: any) { + setError(err.message || 'Failed to remove schedule') + console.error('Remove schedule error:', err) + } finally { + setLoading(false) + } + } + + const handleCancel = () => { + onClose() + } + + return ( +
+
+ {/* Header */} +
+

+ {isScheduled ? 'Update Schedule' : 'Schedule Experiment'} +

+ +
+ +
+ {/* Experiment Info */} +
+

Experiment #{experiment.experiment_number}

+

+ {experiment.reps_required} reps required • {experiment.soaking_duration_hr}h soaking +

+
+ + {/* Error Message */} + {error && ( +
+
{error}
+
+ )} + + {/* Current Schedule (if exists) */} + {isScheduled && ( +
+
Currently Scheduled
+

+ {new Date(experiment.scheduled_date!).toLocaleString()} +

+
+ )} + + {/* Schedule Form */} +
+
+ + setDateTime({ ...dateTime, date: e.target.value })} + className="max-w-xs px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm" + required + /> +
+ +
+ + setDateTime({ ...dateTime, time: e.target.value })} + className="max-w-xs px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm" + required + /> +
+ + {/* Action Buttons */} +
+
+ {isScheduled && ( + + )} +
+ +
+ + +
+
+
+
+
+
+ ) +} diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts index 660209b..74b1eee 100644 --- a/src/lib/supabase.ts +++ b/src/lib/supabase.ts @@ -40,6 +40,7 @@ export interface Experiment { entry_exit_height_diff_in: number schedule_status: ScheduleStatus results_status: ResultsStatus + scheduled_date?: string | null created_at: string updated_at: string created_by: string @@ -56,6 +57,7 @@ export interface CreateExperimentRequest { entry_exit_height_diff_in: number schedule_status?: ScheduleStatus results_status?: ResultsStatus + scheduled_date?: string | null } export interface UpdateExperimentRequest { @@ -69,6 +71,7 @@ export interface UpdateExperimentRequest { entry_exit_height_diff_in?: number schedule_status?: ScheduleStatus results_status?: ResultsStatus + scheduled_date?: string | null } export interface UserRole { @@ -349,6 +352,26 @@ export const experimentManagement = { return data }, + // Schedule an experiment + async scheduleExperiment(id: string, scheduledDate: string): Promise { + const updates: UpdateExperimentRequest = { + scheduled_date: scheduledDate, + schedule_status: 'scheduled' + } + + return this.updateExperiment(id, updates) + }, + + // Remove experiment schedule + async removeExperimentSchedule(id: string): Promise { + const updates: UpdateExperimentRequest = { + scheduled_date: null, + schedule_status: 'pending schedule' + } + + return this.updateExperiment(id, updates) + }, + // Check if experiment number is unique async isExperimentNumberUnique(experimentNumber: number, excludeId?: string): Promise { let query = supabase diff --git a/supabase/migrations/20250721000001_add_scheduled_date.sql b/supabase/migrations/20250721000001_add_scheduled_date.sql new file mode 100644 index 0000000..f34e5f0 --- /dev/null +++ b/supabase/migrations/20250721000001_add_scheduled_date.sql @@ -0,0 +1,12 @@ +-- Add scheduled_date field to experiments table +-- This migration adds support for storing when experiments are scheduled to run + +-- Add scheduled_date column to experiments table +ALTER TABLE public.experiments +ADD COLUMN IF NOT EXISTS scheduled_date TIMESTAMP WITH TIME ZONE; + +-- Create index for better performance when querying by scheduled date +CREATE INDEX IF NOT EXISTS idx_experiments_scheduled_date ON public.experiments(scheduled_date); + +-- Add comment for documentation +COMMENT ON COLUMN public.experiments.scheduled_date IS 'Date and time when the experiment is scheduled to run'; diff --git a/supabase/seed.sql b/supabase/seed.sql new file mode 100644 index 0000000..80a4faa --- /dev/null +++ b/supabase/seed.sql @@ -0,0 +1,60 @@ +-- Seed data for testing experiment scheduling functionality + +-- Insert some sample experiments for testing +INSERT INTO public.experiments ( + experiment_number, + reps_required, + soaking_duration_hr, + air_drying_time_min, + plate_contact_frequency_hz, + throughput_rate_pecans_sec, + crush_amount_in, + entry_exit_height_diff_in, + schedule_status, + results_status, + created_by +) VALUES +( + 1001, + 5, + 2.5, + 30, + 50.0, + 2.5, + 0.005, + 1.2, + 'pending schedule', + 'valid', + (SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com') +), +( + 1002, + 3, + 1.0, + 15, + 45.0, + 3.0, + 0.003, + 0.8, + 'pending schedule', + 'valid', + (SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com') +), +( + 1003, + 4, + 3.0, + 45, + 55.0, + 2.0, + 0.007, + 1.5, + 'scheduled', + 'valid', + (SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com') +); + +-- Update one experiment to have a scheduled date for testing +UPDATE public.experiments +SET scheduled_date = NOW() + INTERVAL '2 days' +WHERE experiment_number = 1003;