add scheduling functionality for experiments with new ScheduleModal component
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { ExperimentModal } from './ExperimentModal'
|
import { ExperimentModal } from './ExperimentModal'
|
||||||
|
import { ScheduleModal } from './ScheduleModal'
|
||||||
import { experimentManagement, userManagement } from '../lib/supabase'
|
import { experimentManagement, userManagement } from '../lib/supabase'
|
||||||
import type { Experiment, User, ScheduleStatus, ResultsStatus } from '../lib/supabase'
|
import type { Experiment, User, ScheduleStatus, ResultsStatus } from '../lib/supabase'
|
||||||
|
|
||||||
@@ -11,6 +12,8 @@ export function Experiments() {
|
|||||||
const [editingExperiment, setEditingExperiment] = useState<Experiment | undefined>(undefined)
|
const [editingExperiment, setEditingExperiment] = useState<Experiment | undefined>(undefined)
|
||||||
const [currentUser, setCurrentUser] = useState<User | null>(null)
|
const [currentUser, setCurrentUser] = useState<User | null>(null)
|
||||||
const [filterStatus, setFilterStatus] = useState<ScheduleStatus | 'all'>('all')
|
const [filterStatus, setFilterStatus] = useState<ScheduleStatus | 'all'>('all')
|
||||||
|
const [showScheduleModal, setShowScheduleModal] = useState(false)
|
||||||
|
const [schedulingExperiment, setSchedulingExperiment] = useState<Experiment | undefined>(undefined)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData()
|
loadData()
|
||||||
@@ -56,6 +59,21 @@ export function Experiments() {
|
|||||||
// Add new experiment
|
// Add new experiment
|
||||||
setExperiments(prev => [experiment, ...prev])
|
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) => {
|
const handleDeleteExperiment = async (experiment: Experiment) => {
|
||||||
@@ -198,6 +216,11 @@ export function Experiments() {
|
|||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Schedule Status
|
Schedule Status
|
||||||
</th>
|
</th>
|
||||||
|
{canManageExperiments && (
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Scheduled Date/Time
|
||||||
|
</th>
|
||||||
|
)}
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Results Status
|
Results Status
|
||||||
</th>
|
</th>
|
||||||
@@ -236,6 +259,29 @@ export function Experiments() {
|
|||||||
{experiment.schedule_status}
|
{experiment.schedule_status}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
{canManageExperiments && (
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleScheduleExperiment(experiment)
|
||||||
|
}}
|
||||||
|
className="text-blue-600 hover:text-blue-900 p-1 rounded hover:bg-blue-50 transition-colors"
|
||||||
|
title="Schedule"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" 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>
|
||||||
|
</button>
|
||||||
|
{experiment.scheduled_date && (
|
||||||
|
<span className="text-xs">
|
||||||
|
{new Date(experiment.scheduled_date).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusBadgeColor(experiment.results_status)}`}>
|
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusBadgeColor(experiment.results_status)}`}>
|
||||||
{experiment.results_status}
|
{experiment.results_status}
|
||||||
@@ -301,7 +347,7 @@ export function Experiments() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal */}
|
{/* Experiment Modal */}
|
||||||
{showModal && (
|
{showModal && (
|
||||||
<ExperimentModal
|
<ExperimentModal
|
||||||
experiment={editingExperiment}
|
experiment={editingExperiment}
|
||||||
@@ -309,6 +355,15 @@ export function Experiments() {
|
|||||||
onExperimentSaved={handleExperimentSaved}
|
onExperimentSaved={handleExperimentSaved}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Schedule Modal */}
|
||||||
|
{showScheduleModal && schedulingExperiment && (
|
||||||
|
<ScheduleModal
|
||||||
|
experiment={schedulingExperiment}
|
||||||
|
onClose={() => setShowScheduleModal(false)}
|
||||||
|
onScheduleUpdated={handleScheduleUpdated}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
206
src/components/ScheduleModal.tsx
Normal file
206
src/components/ScheduleModal.tsx
Normal file
@@ -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<string | null>(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 (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-25 overflow-y-auto h-full w-full z-50 flex items-center justify-center p-4">
|
||||||
|
<div className="relative bg-white rounded-xl shadow-2xl w-full max-w-md mx-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900">
|
||||||
|
{isScheduled ? 'Update Schedule' : 'Schedule Experiment'}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg p-2 transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6">
|
||||||
|
{/* Experiment Info */}
|
||||||
|
<div className="mb-6 p-4 bg-gray-50 rounded-lg">
|
||||||
|
<h4 className="font-medium text-gray-900 mb-2">Experiment #{experiment.experiment_number}</h4>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
{experiment.reps_required} reps required • {experiment.soaking_duration_hr}h soaking
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
{error && (
|
||||||
|
<div className="mb-4 rounded-md bg-red-50 p-4">
|
||||||
|
<div className="text-sm text-red-700">{error}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Current Schedule (if exists) */}
|
||||||
|
{isScheduled && (
|
||||||
|
<div className="mb-6 p-4 bg-blue-50 rounded-lg">
|
||||||
|
<h5 className="font-medium text-blue-900 mb-1">Currently Scheduled</h5>
|
||||||
|
<p className="text-sm text-blue-700">
|
||||||
|
{new Date(experiment.scheduled_date!).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Schedule Form */}
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="date" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Date *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="date"
|
||||||
|
value={dateTime.date}
|
||||||
|
onChange={(e) => 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
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="time" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Time *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
id="time"
|
||||||
|
value={dateTime.time}
|
||||||
|
onChange={(e) => 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
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex justify-between pt-4">
|
||||||
|
<div>
|
||||||
|
{isScheduled && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleRemoveSchedule}
|
||||||
|
disabled={loading}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-red-600 hover:text-red-700 hover:bg-red-50 rounded-lg transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Remove Schedule
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex space-x-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCancel}
|
||||||
|
disabled={loading}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? 'Saving...' : (isScheduled ? 'Update Schedule' : 'Schedule Experiment')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -40,6 +40,7 @@ export interface Experiment {
|
|||||||
entry_exit_height_diff_in: number
|
entry_exit_height_diff_in: number
|
||||||
schedule_status: ScheduleStatus
|
schedule_status: ScheduleStatus
|
||||||
results_status: ResultsStatus
|
results_status: ResultsStatus
|
||||||
|
scheduled_date?: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
created_by: string
|
created_by: string
|
||||||
@@ -56,6 +57,7 @@ export interface CreateExperimentRequest {
|
|||||||
entry_exit_height_diff_in: number
|
entry_exit_height_diff_in: number
|
||||||
schedule_status?: ScheduleStatus
|
schedule_status?: ScheduleStatus
|
||||||
results_status?: ResultsStatus
|
results_status?: ResultsStatus
|
||||||
|
scheduled_date?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateExperimentRequest {
|
export interface UpdateExperimentRequest {
|
||||||
@@ -69,6 +71,7 @@ export interface UpdateExperimentRequest {
|
|||||||
entry_exit_height_diff_in?: number
|
entry_exit_height_diff_in?: number
|
||||||
schedule_status?: ScheduleStatus
|
schedule_status?: ScheduleStatus
|
||||||
results_status?: ResultsStatus
|
results_status?: ResultsStatus
|
||||||
|
scheduled_date?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserRole {
|
export interface UserRole {
|
||||||
@@ -349,6 +352,26 @@ export const experimentManagement = {
|
|||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Schedule an experiment
|
||||||
|
async scheduleExperiment(id: string, scheduledDate: string): Promise<Experiment> {
|
||||||
|
const updates: UpdateExperimentRequest = {
|
||||||
|
scheduled_date: scheduledDate,
|
||||||
|
schedule_status: 'scheduled'
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.updateExperiment(id, updates)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Remove experiment schedule
|
||||||
|
async removeExperimentSchedule(id: string): Promise<Experiment> {
|
||||||
|
const updates: UpdateExperimentRequest = {
|
||||||
|
scheduled_date: null,
|
||||||
|
schedule_status: 'pending schedule'
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.updateExperiment(id, updates)
|
||||||
|
},
|
||||||
|
|
||||||
// Check if experiment number is unique
|
// Check if experiment number is unique
|
||||||
async isExperimentNumberUnique(experimentNumber: number, excludeId?: string): Promise<boolean> {
|
async isExperimentNumberUnique(experimentNumber: number, excludeId?: string): Promise<boolean> {
|
||||||
let query = supabase
|
let query = supabase
|
||||||
|
|||||||
12
supabase/migrations/20250721000001_add_scheduled_date.sql
Normal file
12
supabase/migrations/20250721000001_add_scheduled_date.sql
Normal file
@@ -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';
|
||||||
60
supabase/seed.sql
Normal file
60
supabase/seed.sql
Normal file
@@ -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;
|
||||||
Reference in New Issue
Block a user