Refactor experiment management and update data structures

- Renamed columns in the experimental run sheet CSV for clarity.
- Updated the ExperimentForm component to include new fields for weight per repetition and additional parameters specific to Meyer Cracker experiments.
- Enhanced the data entry logic to handle new experiment phases and machine types.
- Refactored repetition scheduling logic to use scheduled_date instead of schedule_status for better clarity in status representation.
- Improved the user interface for displaying experiment phases and their associated statuses.
- Removed outdated seed data and updated database migration scripts to reflect the new schema changes.
This commit is contained in:
salirezav
2025-09-24 14:27:28 -04:00
parent 08538c87c3
commit aaeb164a32
33 changed files with 6489 additions and 1123 deletions

View File

@@ -300,11 +300,11 @@ function RepetitionCard({ experiment, repetition, onSelect, status }: Repetition
</span>
<span className="text-lg">{getStatusIcon()}</span>
</div>
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${repetition.schedule_status === 'scheduled'
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${repetition.scheduled_date
? 'bg-blue-100 text-blue-800'
: 'bg-yellow-100 text-yellow-800'
}`}>
{repetition.schedule_status === 'pending schedule' ? 'Pending' : repetition.schedule_status}
{repetition.scheduled_date ? 'scheduled' : 'pending'}
</span>
</div>

View File

@@ -1,5 +1,6 @@
import { useState } from 'react'
import type { CreateExperimentRequest, UpdateExperimentRequest, ScheduleStatus, ResultsStatus } from '../lib/supabase'
import { useEffect, useState } from 'react'
import type { CreateExperimentRequest, UpdateExperimentRequest, ScheduleStatus, ResultsStatus, ExperimentPhase, MachineType } from '../lib/supabase'
import { experimentPhaseManagement, machineTypeManagement } from '../lib/supabase'
interface ExperimentFormProps {
initialData?: Partial<CreateExperimentRequest & { schedule_status: ScheduleStatus; results_status: ResultsStatus; completion_status: boolean }>
@@ -14,12 +15,17 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa
const [formData, setFormData] = useState<CreateExperimentRequest & { schedule_status: ScheduleStatus; results_status: ResultsStatus; completion_status: boolean }>({
experiment_number: initialData?.experiment_number || 0,
reps_required: initialData?.reps_required || 1,
weight_per_repetition_lbs: (initialData as any)?.weight_per_repetition_lbs || 1,
soaking_duration_hr: initialData?.soaking_duration_hr || 0,
air_drying_time_min: initialData?.air_drying_time_min || 0,
plate_contact_frequency_hz: initialData?.plate_contact_frequency_hz || 1,
throughput_rate_pecans_sec: initialData?.throughput_rate_pecans_sec || 1,
crush_amount_in: initialData?.crush_amount_in || 0,
entry_exit_height_diff_in: initialData?.entry_exit_height_diff_in || 0,
// Meyer-specific (UI only)
motor_speed_hz: (initialData as any)?.motor_speed_hz || 1,
jig_displacement_inches: (initialData as any)?.jig_displacement_inches || 0,
spring_stiffness_nm: (initialData as any)?.spring_stiffness_nm || 1,
schedule_status: initialData?.schedule_status || 'pending schedule',
results_status: initialData?.results_status || 'valid',
completion_status: initialData?.completion_status || false,
@@ -27,6 +33,31 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa
})
const [errors, setErrors] = useState<Record<string, string>>({})
const [phase, setPhase] = useState<ExperimentPhase | null>(null)
const [crackingMachine, setCrackingMachine] = useState<MachineType | null>(null)
const [metaLoading, setMetaLoading] = useState<boolean>(false)
useEffect(() => {
const loadMeta = async () => {
if (!phaseId) return
try {
setMetaLoading(true)
const p = await experimentPhaseManagement.getExperimentPhaseById(phaseId)
setPhase(p)
if (p?.has_cracking && p.cracking_machine_type_id) {
const mt = await machineTypeManagement.getMachineTypeById(p.cracking_machine_type_id)
setCrackingMachine(mt)
} else {
setCrackingMachine(null)
}
} catch (e) {
console.warn('Failed to load phase/machine metadata', e)
} finally {
setMetaLoading(false)
}
}
loadMeta()
}, [phaseId])
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {}
@@ -40,8 +71,9 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa
newErrors.reps_required = 'Repetitions required must be a positive integer'
}
if (!formData.weight_per_repetition_lbs || formData.weight_per_repetition_lbs <= 0) {
newErrors.weight_per_repetition_lbs = 'Weight per repetition must be positive'
}
if (formData.soaking_duration_hr < 0) {
@@ -52,16 +84,30 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa
newErrors.air_drying_time_min = 'Air drying time cannot be negative'
}
if (!formData.plate_contact_frequency_hz || formData.plate_contact_frequency_hz <= 0) {
newErrors.plate_contact_frequency_hz = 'Plate contact frequency must be positive'
}
if (!formData.throughput_rate_pecans_sec || formData.throughput_rate_pecans_sec <= 0) {
newErrors.throughput_rate_pecans_sec = 'Throughput rate must be positive'
}
if (formData.crush_amount_in < 0) {
newErrors.crush_amount_in = 'Crush amount cannot be negative'
// Validate cracking fields depending on machine
if (phase?.has_cracking) {
if (crackingMachine?.name === 'JC Cracker') {
if (!formData.plate_contact_frequency_hz || formData.plate_contact_frequency_hz <= 0) {
newErrors.plate_contact_frequency_hz = 'Plate contact frequency must be positive'
}
if (!formData.throughput_rate_pecans_sec || formData.throughput_rate_pecans_sec <= 0) {
newErrors.throughput_rate_pecans_sec = 'Throughput rate must be positive'
}
if (formData.crush_amount_in < 0) {
newErrors.crush_amount_in = 'Crush amount cannot be negative'
}
}
if (crackingMachine?.name === 'Meyer Cracker') {
if (!(formData as any).motor_speed_hz || (formData as any).motor_speed_hz <= 0) {
newErrors.motor_speed_hz = 'Motor speed must be positive'
}
if ((formData as any).jig_displacement_inches === undefined) {
newErrors.jig_displacement_inches = 'Jig displacement is required'
}
if (!(formData as any).spring_stiffness_nm || (formData as any).spring_stiffness_nm <= 0) {
newErrors.spring_stiffness_nm = 'Spring stiffness must be positive'
}
}
}
setErrors(newErrors)
@@ -80,14 +126,10 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa
const submitData = isEditing ? formData : {
experiment_number: formData.experiment_number,
reps_required: formData.reps_required,
soaking_duration_hr: formData.soaking_duration_hr,
air_drying_time_min: formData.air_drying_time_min,
plate_contact_frequency_hz: formData.plate_contact_frequency_hz,
throughput_rate_pecans_sec: formData.throughput_rate_pecans_sec,
crush_amount_in: formData.crush_amount_in,
entry_exit_height_diff_in: formData.entry_exit_height_diff_in,
schedule_status: formData.schedule_status,
results_status: formData.results_status
weight_per_repetition_lbs: formData.weight_per_repetition_lbs,
results_status: formData.results_status,
completion_status: formData.completion_status,
phase_id: formData.phase_id
}
await onSubmit(submitData)
@@ -157,139 +199,264 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa
)}
</div>
<div>
<label htmlFor="weight_per_repetition_lbs" className="block text-sm font-medium text-gray-700 mb-2">
Weight per Repetition (lbs) *
</label>
<input
type="number"
id="weight_per_repetition_lbs"
value={formData.weight_per_repetition_lbs}
onChange={(e) => handleInputChange('weight_per_repetition_lbs' as any, parseFloat(e.target.value) || 0)}
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.weight_per_repetition_lbs ? 'border-red-300' : 'border-gray-300'}`}
placeholder="e.g. 10.0"
min="0.1"
step="0.1"
required
/>
{errors.weight_per_repetition_lbs && (
<p className="mt-1 text-sm text-red-600">{errors.weight_per_repetition_lbs}</p>
)}
</div>
</div>
{/* Experiment Parameters */}
<div className="border-t pt-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Experiment Parameters</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Dynamic Sections by Phase */}
<div className="border-t pt-6 space-y-8">
{/* Soaking */}
{phase?.has_soaking && (
<div>
<label htmlFor="soaking_duration_hr" className="block text-sm font-medium text-gray-700 mb-2">
Soaking Duration (hours) *
</label>
<input
type="number"
id="soaking_duration_hr"
value={formData.soaking_duration_hr}
onChange={(e) => handleInputChange('soaking_duration_hr', parseFloat(e.target.value) || 0)}
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.soaking_duration_hr ? 'border-red-300' : 'border-gray-300'
}`}
placeholder="0.0"
min="0"
step="0.1"
required
/>
{errors.soaking_duration_hr && (
<p className="mt-1 text-sm text-red-600">{errors.soaking_duration_hr}</p>
)}
<h3 className="text-lg font-medium text-gray-900 mb-4">Soaking</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="soaking_duration_hr" className="block text-sm font-medium text-gray-700 mb-2">
Soaking Duration (hours) *
</label>
<input
type="number"
id="soaking_duration_hr"
value={formData.soaking_duration_hr}
onChange={(e) => handleInputChange('soaking_duration_hr', parseFloat(e.target.value) || 0)}
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.soaking_duration_hr ? 'border-red-300' : 'border-gray-300'}`}
placeholder="0.0"
min="0"
step="0.1"
required
/>
{errors.soaking_duration_hr && (
<p className="mt-1 text-sm text-red-600">{errors.soaking_duration_hr}</p>
)}
</div>
</div>
</div>
)}
{/* Air-Drying */}
{phase?.has_airdrying && (
<div>
<label htmlFor="air_drying_time_min" className="block text-sm font-medium text-gray-700 mb-2">
Air Drying Time (minutes) *
</label>
<input
type="number"
id="air_drying_time_min"
value={formData.air_drying_time_min}
onChange={(e) => handleInputChange('air_drying_time_min', parseInt(e.target.value) || 0)}
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.air_drying_time_min ? 'border-red-300' : 'border-gray-300'
}`}
placeholder="0"
min="0"
step="1"
required
/>
{errors.air_drying_time_min && (
<p className="mt-1 text-sm text-red-600">{errors.air_drying_time_min}</p>
)}
<h3 className="text-lg font-medium text-gray-900 mb-4">Air-Drying</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="air_drying_time_min" className="block text-sm font-medium text-gray-700 mb-2">
Air Drying Time (minutes) *
</label>
<input
type="number"
id="air_drying_time_min"
value={formData.air_drying_time_min}
onChange={(e) => handleInputChange('air_drying_time_min', parseInt(e.target.value) || 0)}
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.air_drying_time_min ? 'border-red-300' : 'border-gray-300'}`}
placeholder="0"
min="0"
step="1"
required
/>
{errors.air_drying_time_min && (
<p className="mt-1 text-sm text-red-600">{errors.air_drying_time_min}</p>
)}
</div>
</div>
</div>
)}
{/* Cracking - machine specific */}
{phase?.has_cracking && (
<div>
<label htmlFor="plate_contact_frequency_hz" className="block text-sm font-medium text-gray-700 mb-2">
Plate Contact Frequency (Hz) *
</label>
<input
type="number"
id="plate_contact_frequency_hz"
value={formData.plate_contact_frequency_hz}
onChange={(e) => handleInputChange('plate_contact_frequency_hz', parseFloat(e.target.value) || 1)}
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.plate_contact_frequency_hz ? 'border-red-300' : 'border-gray-300'
}`}
placeholder="1.0"
min="0.1"
step="0.1"
required
/>
{errors.plate_contact_frequency_hz && (
<p className="mt-1 text-sm text-red-600">{errors.plate_contact_frequency_hz}</p>
)}
</div>
<h3 className="text-lg font-medium text-gray-900 mb-4">Cracking {crackingMachine ? `(${crackingMachine.name})` : ''}</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{crackingMachine?.name === 'JC Cracker' && (
<>
<div>
<label htmlFor="plate_contact_frequency_hz" className="block text-sm font-medium text-gray-700 mb-2">
Plate Contact Frequency (Hz) *
</label>
<input
type="number"
id="plate_contact_frequency_hz"
value={formData.plate_contact_frequency_hz}
onChange={(e) => handleInputChange('plate_contact_frequency_hz', parseFloat(e.target.value) || 1)}
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.plate_contact_frequency_hz ? 'border-red-300' : 'border-gray-300'}`}
placeholder="1.0"
min="0.1"
step="0.1"
required
/>
{errors.plate_contact_frequency_hz && (
<p className="mt-1 text-sm text-red-600">{errors.plate_contact_frequency_hz}</p>
)}
</div>
<div>
<label htmlFor="throughput_rate_pecans_sec" className="block text-sm font-medium text-gray-700 mb-2">
Throughput Rate (pecans/sec) *
</label>
<input
type="number"
id="throughput_rate_pecans_sec"
value={formData.throughput_rate_pecans_sec}
onChange={(e) => handleInputChange('throughput_rate_pecans_sec', parseFloat(e.target.value) || 1)}
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.throughput_rate_pecans_sec ? 'border-red-300' : 'border-gray-300'}`}
placeholder="1.0"
min="0.1"
step="0.1"
required
/>
{errors.throughput_rate_pecans_sec && (
<p className="mt-1 text-sm text-red-600">{errors.throughput_rate_pecans_sec}</p>
)}
</div>
<div>
<label htmlFor="crush_amount_in" className="block text-sm font-medium text-gray-700 mb-2">
Crush Amount (thousandths of inch) *
</label>
<input
type="number"
id="crush_amount_in"
value={formData.crush_amount_in}
onChange={(e) => handleInputChange('crush_amount_in', parseFloat(e.target.value) || 0)}
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.crush_amount_in ? 'border-red-300' : 'border-gray-300'}`}
placeholder="0.0"
min="0"
step="0.001"
required
/>
{errors.crush_amount_in && (
<p className="mt-1 text-sm text-red-600">{errors.crush_amount_in}</p>
)}
</div>
<div className="md:col-span-2">
<label htmlFor="entry_exit_height_diff_in" className="block text-sm font-medium text-gray-700 mb-2">
Entry/Exit Height Difference (inches) *
</label>
<input
type="number"
id="entry_exit_height_diff_in"
value={formData.entry_exit_height_diff_in}
onChange={(e) => handleInputChange('entry_exit_height_diff_in', parseFloat(e.target.value) || 0)}
className={`max-w-sm px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.entry_exit_height_diff_in ? 'border-red-300' : 'border-gray-300'}`}
placeholder="0.0 (can be negative)"
step="0.1"
required
/>
{errors.entry_exit_height_diff_in && (
<p className="mt-1 text-sm text-red-600">{errors.entry_exit_height_diff_in}</p>
)}
<p className="mt-1 text-sm text-gray-500">Positive values indicate entry is higher than exit</p>
</div>
</>
)}
{crackingMachine?.name === 'Meyer Cracker' && (
<>
<div>
<label htmlFor="motor_speed_hz" className="block text-sm font-medium text-gray-700 mb-2">
Motor Speed (Hz) *
</label>
<input
type="number"
id="motor_speed_hz"
value={(formData as any).motor_speed_hz}
onChange={(e) => handleInputChange('motor_speed_hz' as any, parseFloat(e.target.value) || 1)}
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.motor_speed_hz ? 'border-red-300' : 'border-gray-300'}`}
placeholder="1.0"
min="0.1"
step="0.1"
required
/>
{errors.motor_speed_hz && (
<p className="mt-1 text-sm text-red-600">{errors.motor_speed_hz}</p>
)}
</div>
<div>
<label htmlFor="jig_displacement_inches" className="block text-sm font-medium text-gray-700 mb-2">
Jig Displacement (inches) *
</label>
<input
type="number"
id="jig_displacement_inches"
value={(formData as any).jig_displacement_inches}
onChange={(e) => handleInputChange('jig_displacement_inches' as any, parseFloat(e.target.value) || 0)}
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.jig_displacement_inches ? 'border-red-300' : 'border-gray-300'}`}
placeholder="0.0"
min="0"
step="0.01"
required
/>
{errors.jig_displacement_inches && (
<p className="mt-1 text-sm text-red-600">{errors.jig_displacement_inches}</p>
)}
</div>
<div>
<label htmlFor="spring_stiffness_nm" className="block text-sm font-medium text-gray-700 mb-2">
Spring Stiffness (N·m) *
</label>
<input
type="number"
id="spring_stiffness_nm"
value={(formData as any).spring_stiffness_nm}
onChange={(e) => handleInputChange('spring_stiffness_nm' as any, parseFloat(e.target.value) || 1)}
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.spring_stiffness_nm ? 'border-red-300' : 'border-gray-300'}`}
placeholder="1.0"
min="0.1"
step="0.1"
required
/>
{errors.spring_stiffness_nm && (
<p className="mt-1 text-sm text-red-600">{errors.spring_stiffness_nm}</p>
)}
</div>
</>
)}
</div>
</div>
)}
{/* Shelling */}
{phase?.has_shelling && (
<div>
<label htmlFor="throughput_rate_pecans_sec" className="block text-sm font-medium text-gray-700 mb-2">
Throughput Rate (pecans/sec) *
</label>
<input
type="number"
id="throughput_rate_pecans_sec"
value={formData.throughput_rate_pecans_sec}
onChange={(e) => handleInputChange('throughput_rate_pecans_sec', parseFloat(e.target.value) || 1)}
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.throughput_rate_pecans_sec ? 'border-red-300' : 'border-gray-300'
}`}
placeholder="1.0"
min="0.1"
step="0.1"
required
/>
{errors.throughput_rate_pecans_sec && (
<p className="mt-1 text-sm text-red-600">{errors.throughput_rate_pecans_sec}</p>
)}
<h3 className="text-lg font-medium text-gray-900 mb-4">Shelling</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Shelling Start Offset (minutes)
</label>
<input
type="number"
value={(formData as any).shelling_start_offset_min || 0}
onChange={(e) => handleInputChange('shelling_start_offset_min' as any, parseInt(e.target.value) || 0)}
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"
placeholder="0"
min="0"
step="1"
/>
</div>
</div>
</div>
<div>
<label htmlFor="crush_amount_in" className="block text-sm font-medium text-gray-700 mb-2">
Crush Amount (thousandths of inch) *
</label>
<input
type="number"
id="crush_amount_in"
value={formData.crush_amount_in}
onChange={(e) => handleInputChange('crush_amount_in', parseFloat(e.target.value) || 0)}
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.crush_amount_in ? 'border-red-300' : 'border-gray-300'
}`}
placeholder="0.0"
min="0"
step="0.001"
required
/>
{errors.crush_amount_in && (
<p className="mt-1 text-sm text-red-600">{errors.crush_amount_in}</p>
)}
</div>
<div className="md:col-span-2">
<label htmlFor="entry_exit_height_diff_in" className="block text-sm font-medium text-gray-700 mb-2">
Entry/Exit Height Difference (inches) *
</label>
<input
type="number"
id="entry_exit_height_diff_in"
value={formData.entry_exit_height_diff_in}
onChange={(e) => handleInputChange('entry_exit_height_diff_in', parseFloat(e.target.value) || 0)}
className={`max-w-sm px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.entry_exit_height_diff_in ? 'border-red-300' : 'border-gray-300'
}`}
placeholder="0.0 (can be negative)"
step="0.1"
required
/>
{errors.entry_exit_height_diff_in && (
<p className="mt-1 text-sm text-red-600">{errors.entry_exit_height_diff_in}</p>
)}
<p className="mt-1 text-sm text-gray-500">Positive values indicate entry is higher than exit</p>
</div>
</div>
)}
</div>
{/* Status Fields (only show when editing) */}

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react'
import { experimentPhaseManagement, userManagement } from '../lib/supabase'
import type { ExperimentPhase, User } from '../lib/supabase'
import { PhaseModal } from './PhaseModal'
interface ExperimentPhasesProps {
onPhaseSelect: (phase: ExperimentPhase) => void
@@ -11,6 +12,7 @@ export function ExperimentPhases({ onPhaseSelect }: ExperimentPhasesProps) {
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [currentUser, setCurrentUser] = useState<User | null>(null)
const [showCreateModal, setShowCreateModal] = useState(false)
useEffect(() => {
loadData()
@@ -38,6 +40,11 @@ export function ExperimentPhases({ onPhaseSelect }: ExperimentPhasesProps) {
const canManagePhases = currentUser?.roles.includes('admin') || currentUser?.roles.includes('conductor')
const handlePhaseCreated = (newPhase: ExperimentPhase) => {
setPhases(prev => [newPhase, ...prev])
setShowCreateModal(false)
}
if (loading) {
return (
<div className="p-6">
@@ -60,10 +67,7 @@ export function ExperimentPhases({ onPhaseSelect }: ExperimentPhasesProps) {
</div>
{canManagePhases && (
<button
onClick={() => {
// TODO: Implement create phase modal
alert('Create phase functionality will be implemented')
}}
onClick={() => setShowCreateModal(true)}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
New Phase
@@ -113,6 +117,32 @@ export function ExperimentPhases({ onPhaseSelect }: ExperimentPhasesProps) {
</p>
)}
{/* Enabled Phases */}
<div className="mb-4">
<div className="flex flex-wrap gap-1">
{phase.has_soaking && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
🌰 Soaking
</span>
)}
{phase.has_airdrying && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
💨 Air-Drying
</span>
)}
{phase.has_cracking && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
🔨 Cracking
</span>
)}
{phase.has_shelling && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
📊 Shelling
</span>
)}
</div>
</div>
<div className="flex items-center justify-between text-sm text-gray-500">
<span>Created {new Date(phase.created_at).toLocaleDateString()}</span>
<div className="flex items-center text-blue-600 hover:text-blue-800">
@@ -139,10 +169,7 @@ export function ExperimentPhases({ onPhaseSelect }: ExperimentPhasesProps) {
{canManagePhases && (
<div className="mt-6">
<button
onClick={() => {
// TODO: Implement create phase modal
alert('Create phase functionality will be implemented')
}}
onClick={() => setShowCreateModal(true)}
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Create First Phase
@@ -151,6 +178,14 @@ export function ExperimentPhases({ onPhaseSelect }: ExperimentPhasesProps) {
)}
</div>
)}
{/* Create Phase Modal */}
{showCreateModal && (
<PhaseModal
onClose={() => setShowCreateModal(false)}
onPhaseCreated={handlePhaseCreated}
/>
)}
</div>
)
}

View File

@@ -108,10 +108,8 @@ export function Experiments() {
try {
const newRepetition = await repetitionManagement.createRepetition({
experiment_id: experiment.id,
repetition_number: repetitionNumber,
schedule_status: 'pending schedule'
repetition_number: repetitionNumber
})
setExperimentRepetitions(prev => ({
...prev,
[experiment.id]: [...(prev[experiment.id] || []), newRepetition].sort((a, b) => a.repetition_number - b.repetition_number)
@@ -141,8 +139,8 @@ export function Experiments() {
}
const getRepetitionStatusSummary = (repetitions: ExperimentRepetition[]) => {
const scheduled = repetitions.filter(r => r.schedule_status === 'scheduled').length
const pending = repetitions.filter(r => r.schedule_status === 'pending schedule').length
const scheduled = repetitions.filter(r => r.scheduled_date).length
const pending = repetitions.filter(r => !r.scheduled_date).length
const completed = repetitions.filter(r => r.completion_status).length
return { scheduled, pending, completed, total: repetitions.length }
@@ -225,7 +223,7 @@ export function Experiments() {
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Experiment #
#
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Reps Required
@@ -311,8 +309,8 @@ export function Experiments() {
<div key={repetition.id} className="flex items-center justify-between p-2 bg-gray-50 rounded">
<span className="text-sm font-medium">Rep #{repetition.repetition_number}</span>
<div className="flex items-center space-x-2">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusBadgeColor(repetition.schedule_status)}`}>
{repetition.schedule_status === 'pending schedule' ? 'Pending' : repetition.schedule_status}
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusBadgeColor(repetition.scheduled_date ? 'scheduled' : 'pending')}`}>
{repetition.scheduled_date ? 'scheduled' : 'pending'}
</span>
<button
onClick={(e) => {
@@ -320,7 +318,7 @@ export function Experiments() {
handleScheduleRepetition(experiment, repetition)
}}
className="text-blue-600 hover:text-blue-900 p-1 rounded hover:bg-blue-50 transition-colors"
title={repetition.schedule_status === 'scheduled' ? 'Reschedule' : 'Schedule'}
title={repetition.scheduled_date ? 'Reschedule' : '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" />
@@ -352,7 +350,7 @@ export function Experiments() {
</td>
)}
<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 ${experiment.results_status}`}>
{experiment.results_status}
</span>
</td>

View File

@@ -114,7 +114,6 @@ export function PhaseExperiments({ phase, onBack }: PhaseExperimentsProps) {
const newRepetition = await repetitionManagement.createRepetition({
experiment_id: experiment.id,
repetition_number: repetitionNumber,
schedule_status: 'pending schedule'
})
setExperimentRepetitions(prev => ({
@@ -146,16 +145,16 @@ export function PhaseExperiments({ phase, onBack }: PhaseExperimentsProps) {
}
const getRepetitionStatusSummary = (repetitions: ExperimentRepetition[]) => {
const scheduled = repetitions.filter(r => r.schedule_status === 'scheduled').length
const pending = repetitions.filter(r => r.schedule_status === 'pending schedule').length
const scheduled = repetitions.filter(r => r.scheduled_date).length
const pending = repetitions.filter(r => !r.scheduled_date).length
const completed = repetitions.filter(r => r.completion_status).length
return { scheduled, pending, completed, total: repetitions.length }
}
const getStatusBadgeColor = (status: ScheduleStatus | ResultsStatus) => {
const getStatusBadgeColor = (status: ScheduleStatus | ResultsStatus | string) => {
switch (status) {
case 'pending schedule':
case 'pending':
return 'bg-yellow-100 text-yellow-800'
case 'scheduled':
return 'bg-blue-100 text-blue-800'
@@ -326,7 +325,7 @@ export function PhaseExperiments({ phase, onBack }: PhaseExperimentsProps) {
<span className="text-sm font-medium">Rep #{repetition.repetition_number}</span>
<div className="flex items-center space-x-2">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusBadgeColor(repetition.schedule_status)}`}>
{repetition.schedule_status === 'pending schedule' ? 'Pending' : repetition.schedule_status}
{repetition.scheduled_date ? 'scheduled' : 'pending'}
</span>
<button
onClick={(e) => {
@@ -334,7 +333,7 @@ export function PhaseExperiments({ phase, onBack }: PhaseExperimentsProps) {
handleScheduleRepetition(experiment, repetition)
}}
className="text-blue-600 hover:text-blue-900 p-1 rounded hover:bg-blue-50 transition-colors"
title={repetition.schedule_status === 'scheduled' ? 'Reschedule' : 'Schedule'}
title={repetition.scheduled_date ? 'Reschedule' : '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" />

View File

@@ -0,0 +1,288 @@
import { useEffect, useState } from 'react'
import type { CreateExperimentPhaseRequest, MachineType } from '../lib/supabase'
import { machineTypeManagement } from '../lib/supabase'
interface PhaseFormProps {
onSubmit: (data: CreateExperimentPhaseRequest) => void
onCancel: () => void
loading?: boolean
}
export function PhaseForm({ onSubmit, onCancel, loading = false }: PhaseFormProps) {
const [formData, setFormData] = useState<CreateExperimentPhaseRequest>({
name: '',
description: '',
has_soaking: false,
has_airdrying: false,
has_cracking: false,
has_shelling: false
})
const [errors, setErrors] = useState<Record<string, string>>({})
const [machineTypes, setMachineTypes] = useState<MachineType[]>([])
const [machinesLoading, setMachinesLoading] = useState(false)
const [selectedCrackingMachineId, setSelectedCrackingMachineId] = useState<string | undefined>(undefined)
useEffect(() => {
// Preload machine types for cracking selection
const loadMachines = async () => {
try {
setMachinesLoading(true)
const types = await machineTypeManagement.getAllMachineTypes()
setMachineTypes(types)
} catch (e) {
// Non-fatal: we will show no options if it fails
console.warn('Failed to load machine types', e)
} finally {
setMachinesLoading(false)
}
}
loadMachines()
}, [])
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {}
// Name is required
if (!formData.name.trim()) {
newErrors.name = 'Phase name is required'
}
// At least one phase must be selected
if (!formData.has_soaking && !formData.has_airdrying && !formData.has_cracking && !formData.has_shelling) {
newErrors.phases = 'At least one phase must be selected'
}
// If cracking is enabled, require a machine selection
if (formData.has_cracking && !selectedCrackingMachineId) {
newErrors.cracking_machine_type_id = 'Select a cracking machine'
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (validateForm()) {
// Include cracking machine selection in payload when cracking is enabled
const payload: CreateExperimentPhaseRequest = formData.has_cracking
? { ...formData, cracking_machine_type_id: selectedCrackingMachineId }
: formData
onSubmit(payload)
}
}
const handleInputChange = (field: keyof CreateExperimentPhaseRequest, value: string | boolean | undefined) => {
setFormData(prev => ({
...prev,
[field]: value
}))
// Clear error when user starts typing
if (errors[field]) {
setErrors(prev => ({
...prev,
[field]: ''
}))
}
}
const phaseOptions = [
{
key: 'has_soaking' as const,
label: 'Soaking',
description: 'Initial soaking process for pecans',
icon: '🌰'
},
{
key: 'has_airdrying' as const,
label: 'Air-Drying',
description: 'Post-soak air-drying process',
icon: '💨'
},
{
key: 'has_cracking' as const,
label: 'Cracking',
description: 'Pecan shell cracking process',
icon: '🔨'
},
{
key: 'has_shelling' as const,
label: 'Shelling',
description: 'Final shelling and yield measurement',
icon: '📊'
}
]
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Phase Name */}
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-2">
Phase Name *
</label>
<input
type="text"
id="name"
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${errors.name ? 'border-red-300' : 'border-gray-300'
}`}
placeholder="Enter phase name (e.g., 'Full Process Experiment')"
disabled={loading}
/>
{errors.name && (
<p className="mt-1 text-sm text-red-600">{errors.name}</p>
)}
</div>
{/* Description */}
<div>
<label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-2">
Description
</label>
<textarea
id="description"
value={formData.description || ''}
onChange={(e) => handleInputChange('description', e.target.value)}
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Optional description of this experiment phase"
disabled={loading}
/>
</div>
{/* Phase Selection */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
Select Phases *
</label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{phaseOptions.map((option) => (
<div
key={option.key}
className={`relative border rounded-lg p-4 cursor-pointer transition-colors ${formData[option.key]
? 'border-blue-500 bg-blue-50'
: 'border-gray-300 hover:border-gray-400'
}`}
onClick={() => {
const newVal = !formData[option.key]
handleInputChange(option.key, newVal)
if (option.key === 'has_cracking' && !newVal) {
setSelectedCrackingMachineId(undefined)
}
}}
>
<div className="flex items-start">
<div className="flex-shrink-0">
<input
type="checkbox"
checked={formData[option.key]}
onChange={(e) => {
const newVal = e.target.checked
handleInputChange(option.key, newVal)
if (option.key === 'has_cracking' && !newVal) {
setSelectedCrackingMachineId(undefined)
}
}}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
disabled={loading}
/>
</div>
<div className="ml-3 flex-1">
<div className="flex items-center">
<span className="text-2xl mr-2">{option.icon}</span>
<span className="text-sm font-medium text-gray-900">
{option.label}
</span>
</div>
<p className="text-xs text-gray-500 mt-1">
{option.description}
</p>
</div>
</div>
</div>
))}
</div>
{errors.phases && (
<p className="mt-2 text-sm text-red-600">{errors.phases}</p>
)}
</div>
{/* Cracking machine selection */}
{formData.has_cracking && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Select Cracking Machine *
</label>
<div className="max-w-sm">
<select
value={selectedCrackingMachineId || ''}
onChange={(e) => setSelectedCrackingMachineId(e.target.value || undefined)}
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${errors.cracking_machine_type_id ? 'border-red-300' : 'border-gray-300'}`}
disabled={loading || machinesLoading}
>
<option value="">{machinesLoading ? 'Loading...' : 'Select machine'}</option>
{machineTypes.map(mt => (
<option key={mt.id} value={mt.id}>{mt.name}</option>
))}
</select>
</div>
{errors.cracking_machine_type_id && (
<p className="mt-1 text-sm text-red-600">{errors.cracking_machine_type_id}</p>
)}
</div>
)}
{/* Selected Phases Summary */}
{Object.values(formData).some(value => typeof value === 'boolean' && value) && (
<div className="bg-gray-50 rounded-lg p-4">
<h4 className="text-sm font-medium text-gray-900 mb-2">Selected Phases:</h4>
<div className="flex flex-wrap gap-2">
{phaseOptions
.filter(option => formData[option.key])
.map(option => (
<span
key={option.key}
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
>
{option.icon} {option.label}
</span>
))
}
</div>
</div>
)}
{/* Form Actions */}
<div className="flex justify-end space-x-3 pt-6 border-t border-gray-200">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
disabled={loading}
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={loading}
>
{loading ? (
<div className="flex items-center">
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Creating...
</div>
) : (
'Create Phase'
)}
</button>
</div>
</form>
)
}

View File

@@ -0,0 +1,68 @@
import { useState } from 'react'
import { experimentPhaseManagement } from '../lib/supabase'
import type { CreateExperimentPhaseRequest, ExperimentPhase } from '../lib/supabase'
import { PhaseForm } from './PhaseForm'
interface PhaseModalProps {
onClose: () => void
onPhaseCreated: (phase: ExperimentPhase) => void
}
export function PhaseModal({ onClose, onPhaseCreated }: PhaseModalProps) {
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const handleSubmit = async (formData: CreateExperimentPhaseRequest) => {
try {
setLoading(true)
setError(null)
const newPhase = await experimentPhaseManagement.createExperimentPhase(formData)
onPhaseCreated(newPhase)
onClose()
} catch (err: any) {
setError(err.message || 'Failed to create experiment phase')
console.error('Create phase error:', err)
} finally {
setLoading(false)
}
}
return (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 border w-11/12 md:w-2/3 lg:w-1/2 shadow-lg rounded-md bg-white">
<div className="mt-3">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-medium text-gray-900">
Create New Experiment Phase
</h3>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600"
>
<svg className="w-6 h-6" 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>
{/* Error Message */}
{error && (
<div className="mb-4 rounded-md bg-red-50 p-4">
<div className="text-sm text-red-700">{error}</div>
</div>
)}
{/* Form */}
<PhaseForm
onSubmit={handleSubmit}
onCancel={onClose}
loading={loading}
/>
</div>
</div>
</div>
)
}

View File

@@ -33,7 +33,7 @@ export function RepetitionScheduleModal({ experiment, repetition, onClose, onSch
}
const [dateTime, setDateTime] = useState(getInitialDateTime())
const isScheduled = repetition.scheduled_date && repetition.schedule_status === 'scheduled'
const isScheduled = !!repetition.scheduled_date
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()

View File

@@ -2,8 +2,8 @@ import { useEffect, useState } from 'react'
// @ts-ignore - react-big-calendar types not available
import { Calendar, momentLocalizer, Views } from 'react-big-calendar'
import moment from 'moment'
import type { User } from '../lib/supabase'
import { availabilityManagement } from '../lib/supabase'
import type { User, ExperimentPhase, Experiment, ExperimentRepetition, Soaking, Airdrying } from '../lib/supabase'
import { availabilityManagement, userManagement, experimentPhaseManagement, experimentManagement, repetitionManagement, phaseManagement, supabase } from '../lib/supabase'
import 'react-big-calendar/lib/css/react-big-calendar.css'
import './CalendarStyles.css'
@@ -287,7 +287,211 @@ function IndicateAvailability({ user, onBack }: { user: User; onBack: () => void
}
function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }) {
// User context available for future features
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [conductors, setConductors] = useState<User[]>([])
const [conductorIdsWithFutureAvailability, setConductorIdsWithFutureAvailability] = useState<Set<string>>(new Set())
const [selectedConductorIds, setSelectedConductorIds] = useState<Set<string>>(new Set())
const [phases, setPhases] = useState<ExperimentPhase[]>([])
const [expandedPhaseIds, setExpandedPhaseIds] = useState<Set<string>>(new Set())
const [experimentsByPhase, setExperimentsByPhase] = useState<Record<string, Experiment[]>>({})
const [repetitionsByExperiment, setRepetitionsByExperiment] = useState<Record<string, ExperimentRepetition[]>>({})
const [selectedRepetitionIds, setSelectedRepetitionIds] = useState<Set<string>>(new Set())
const [creatingRepetitionsFor, setCreatingRepetitionsFor] = useState<Set<string>>(new Set())
const [soakingByExperiment, setSoakingByExperiment] = useState<Record<string, Soaking | null>>({})
const [airdryingByExperiment, setAirdryingByExperiment] = useState<Record<string, Airdrying | null>>({})
// Calendar state for selected conductors' availability
const localizer = momentLocalizer(moment)
const [calendarView, setCalendarView] = useState(Views.WEEK)
const [currentDate, setCurrentDate] = useState(new Date())
const [availabilityEvents, setAvailabilityEvents] = useState<CalendarEvent[]>([])
// Color map per conductor for calendar events
const [conductorColorMap, setConductorColorMap] = useState<Record<string, string>>({})
const colorPalette = ['#2563eb', '#059669', '#d97706', '#7c3aed', '#dc2626', '#0ea5e9', '#16a34a', '#f59e0b', '#9333ea', '#ef4444']
useEffect(() => {
const load = async () => {
try {
setLoading(true)
setError(null)
const [allUsers, allPhases] = await Promise.all([
userManagement.getAllUsers(),
experimentPhaseManagement.getAllExperimentPhases()
])
// Filter conductors
const conductorsOnly = allUsers.filter(u => u.roles.includes('conductor'))
setConductors(conductorsOnly)
// For each conductor, check if they have any availability in the future
const nowIso = new Date().toISOString()
// Query availability table directly for efficiency
const conductorIds = conductorsOnly.map(c => c.id)
// Fallback: since availabilityManagement is scoped to current user, we query via supabase client here would require direct import.
// To avoid that in UI layer, approximate by marking all conductors as selectable initially.
// In a later iteration, backend endpoint can provide available conductors. For now, consider all conductors as available some time in future.
setConductorIdsWithFutureAvailability(new Set(conductorIds))
setPhases(allPhases)
} catch (e: any) {
setError(e?.message || 'Failed to load scheduling data')
} finally {
setLoading(false)
}
}
load()
}, [])
const toggleConductor = (id: string) => {
setSelectedConductorIds(prev => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}
// 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 selected conductor
const newColorMap: Record<string, string> = {}
selectedIds.forEach((conductorId, index) => {
newColorMap[conductorId] = 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) => ({
id: r.id,
title: `${idToName[r.user_id] || 'Conductor'}`,
start: new Date(r.available_from),
end: new Date(r.available_to),
resource: newColorMap[r.user_id] || '#2563eb'
}))
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 togglePhaseExpand = async (phaseId: string) => {
setExpandedPhaseIds(prev => {
const next = new Set(prev)
if (next.has(phaseId)) next.delete(phaseId)
else next.add(phaseId)
return next
})
// Lazy-load experiments for this phase if not loaded
if (!experimentsByPhase[phaseId]) {
try {
const exps = await experimentManagement.getExperimentsByPhaseId(phaseId)
setExperimentsByPhase(prev => ({ ...prev, [phaseId]: exps }))
// For each experiment, load repetitions and phase data
const repsEntries = await Promise.all(
exps.map(async (exp) => {
const [reps, soaking, airdrying] = await Promise.all([
repetitionManagement.getExperimentRepetitions(exp.id),
phaseManagement.getSoakingByExperimentId(exp.id),
phaseManagement.getAirdryingByExperimentId(exp.id)
])
return [exp.id, reps, soaking, airdrying] as const
})
)
// Update repetitions
setRepetitionsByExperiment(prev => ({
...prev,
...Object.fromEntries(repsEntries.map(([id, reps]) => [id, reps]))
}))
// Update soaking data
setSoakingByExperiment(prev => ({
...prev,
...Object.fromEntries(repsEntries.map(([id, , soaking]) => [id, soaking]))
}))
// Update airdrying data
setAirdryingByExperiment(prev => ({
...prev,
...Object.fromEntries(repsEntries.map(([id, , , airdrying]) => [id, airdrying]))
}))
} catch (e: any) {
setError(e?.message || 'Failed to load experiments or repetitions')
}
}
}
const toggleRepetition = (repId: string) => {
setSelectedRepetitionIds(prev => {
const next = new Set(prev)
if (next.has(repId)) next.delete(repId)
else next.add(repId)
return next
})
}
const createRepetitionsForExperiment = async (experimentId: string) => {
try {
setCreatingRepetitionsFor(prev => new Set(prev).add(experimentId))
setError(null)
// Create all repetitions for this experiment
const newRepetitions = await repetitionManagement.createAllRepetitions(experimentId)
// Update the repetitions state
setRepetitionsByExperiment(prev => ({
...prev,
[experimentId]: newRepetitions
}))
} catch (e: any) {
setError(e?.message || 'Failed to create repetitions')
} finally {
setCreatingRepetitionsFor(prev => {
const next = new Set(prev)
next.delete(experimentId)
return next
})
}
}
return (
<div className="p-6">
<div className="mb-6">
@@ -309,19 +513,212 @@ function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }
</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-purple-100 dark:bg-purple-900/20 rounded-full flex items-center justify-center mb-4">
<svg className="w-8 h-8 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>
{error && (
<div className="mb-4 text-sm text-red-600 dark:text-red-400">{error}</div>
)}
{loading ? (
<div className="text-center py-12">
<div className="mx-auto w-16 h-16 bg-purple-100 dark:bg-purple-900/20 rounded-full flex items-center justify-center mb-4">
<svg className="w-8 h-8 text-purple-600 dark:text-purple-400 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 12a8 8 0 018-8" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">Loading</h3>
<p className="text-gray-500 dark:text-gray-400 max-w-md mx-auto">Fetching conductors, phases, and experiments.</p>
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Left: Conductors with future availability */}
<div>
<div className="flex items-center justify-between mb-3">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Conductors</h2>
<span className="text-xs text-gray-500 dark:text-gray-400">Select to consider for scheduling</span>
</div>
<div className="max-h-[420px] overflow-y-auto divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-md">
{conductors.length === 0 && (
<div className="p-4 text-sm text-gray-500 dark:text-gray-400">No conductors found.</div>
)}
{conductors.map((c, index) => {
const name = [c.first_name, c.last_name].filter(Boolean).join(' ') || c.email
const hasFuture = conductorIdsWithFutureAvailability.has(c.id)
const checked = selectedConductorIds.has(c.id)
const conductorColor = checked ? colorPalette[index % colorPalette.length] : null
return (
<label
key={c.id}
className={`flex items-center justify-between p-3 ${!hasFuture ? 'opacity-50' : ''} ${checked ? 'border-l-4' : ''}`}
style={checked && conductorColor ? { borderLeftColor: conductorColor + '60' } : {}}
>
<div className="flex items-center gap-3">
<input
type="checkbox"
className="h-4 w-4 text-blue-600 border-gray-300 rounded"
checked={checked}
onChange={() => toggleConductor(c.id)}
disabled={!hasFuture}
/>
<div>
<div className="text-sm font-medium text-gray-900 dark:text-white">{name}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">{c.email}</div>
</div>
</div>
<span className={`text-xs ${hasFuture ? 'text-green-600 dark:text-green-400' : 'text-gray-400'}`}>{hasFuture ? 'Available in future' : 'No future availability'}</span>
</label>
)
})}
</div>
</div>
{/* Right: Phases -> Experiments -> Repetitions */}
<div>
<div className="flex items-center justify-between mb-3">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Experiment Phases</h2>
<span className="text-xs text-gray-500 dark:text-gray-400">Expand and select repetitions</span>
</div>
<div className="border border-gray-200 dark:border-gray-700 rounded-md divide-y divide-gray-200 dark:divide-gray-700">
{phases.length === 0 && (
<div className="p-4 text-sm text-gray-500 dark:text-gray-400">No phases defined.</div>
)}
{phases.map(phase => {
const expanded = expandedPhaseIds.has(phase.id)
const experiments = experimentsByPhase[phase.id] || []
return (
<div key={phase.id}>
<button
className="w-full flex items-center justify-between p-3 hover:bg-gray-50 dark:hover:bg-gray-700/50"
onClick={() => togglePhaseExpand(phase.id)}
>
<span className="text-sm font-medium text-gray-900 dark:text-white">{phase.name}</span>
<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>
</button>
{expanded && (
<div className="bg-gray-50 dark:bg-gray-800/50 max-h-96 overflow-y-auto">
{experiments.length === 0 && (
<div className="p-3 text-xs text-gray-500 dark:text-gray-400">No experiments in this phase.</div>
)}
{experiments.map(exp => {
const reps = repetitionsByExperiment[exp.id] || []
const isCreating = creatingRepetitionsFor.has(exp.id)
const allRepsCreated = reps.length >= exp.reps_required
const soaking = soakingByExperiment[exp.id]
const airdrying = airdryingByExperiment[exp.id]
const getSoakDisplay = () => {
if (soaking) return `${soaking.soaking_duration_minutes}min`
return '—'
}
const getAirdryDisplay = () => {
if (airdrying) return `${airdrying.duration_minutes}min`
return '—'
}
return (
<div key={exp.id} className="border-t border-gray-200 dark:border-gray-700">
<div className="px-3 py-2 flex items-center justify-between">
<div className="text-sm text-gray-900 dark:text-white">
<span className="font-medium">Exp #{exp.experiment_number}</span>
<span className="mx-2 text-gray-400"></span>
<span className="text-xs text-gray-600 dark:text-gray-300">Soak: {getSoakDisplay()}</span>
<span className="mx-2 text-gray-400">/</span>
<span className="text-xs text-gray-600 dark:text-gray-300">Air-dry: {getAirdryDisplay()}</span>
</div>
{!allRepsCreated && (
<button
onClick={() => createRepetitionsForExperiment(exp.id)}
disabled={isCreating}
className="text-xs bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white px-2 py-1 rounded transition-colors"
>
{isCreating ? 'Creating...' : 'Add Repetition'}
</button>
)}
</div>
<div className="px-3 pb-2 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2">
{reps.map(rep => {
const checked = selectedRepetitionIds.has(rep.id)
return (
<label key={rep.id} className="flex items-center gap-2 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded p-2">
<input
type="checkbox"
className="h-4 w-4 text-blue-600 border-gray-300 rounded"
checked={checked}
onChange={() => toggleRepetition(rep.id)}
/>
<span className="text-xs text-gray-700 dark:text-gray-300">Rep {rep.repetition_number}</span>
</label>
)
})}
{reps.length === 0 && !isCreating && (
<div className="text-xs text-gray-500 dark:text-gray-400 col-span-full">No repetitions created. Click "Create Reps" to generate them.</div>
)}
{isCreating && (
<div className="text-xs text-blue-600 dark:text-blue-400 col-span-full flex items-center gap-2">
<svg className="w-3 h-3 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 12a8 8 0 018-8" />
</svg>
Creating {exp.reps_required} repetitions...
</div>
)}
</div>
</div>
)
})}
</div>
)}
</div>
)
})}
</div>
</div>
</div>
)}
{/* Week Calendar for selected conductors' availability */}
<div className="mt-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Selected Conductors' Availability</h3>
<div className="h-[80vh] border border-gray-200 dark:border-gray-700 rounded-md overflow-hidden">
<Calendar
localizer={localizer}
events={availabilityEvents}
startAccessor="start"
endAccessor="end"
titleAccessor="title"
style={{ height: '100%' }}
view={calendarView}
onView={setCalendarView}
date={currentDate}
onNavigate={setCurrentDate}
views={[Views.WEEK, Views.DAY]}
dayLayoutAlgorithm="no-overlap"
eventPropGetter={(event) => {
const color = event.resource || '#2563eb'
return {
style: {
backgroundColor: color + '80', // ~50% transparency
borderColor: color,
color: 'white',
borderRadius: '4px',
border: 'none',
opacity: 0.8,
height: 'auto',
minHeight: '20px',
fontSize: '12px',
padding: '2px 4px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}
}
}}
popup
showMultiDayTimes
doShowMore={true}
step={30}
timeslots={2}
/>
</div>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
Experiment Scheduling
</h3>
<p className="text-gray-500 dark:text-gray-400 max-w-md mx-auto">
This interface will allow you to schedule specific experiment runs, assign team members,
and manage the timing of upcoming experimental sessions.
</p>
</div>
</div>
</div>

View File

@@ -38,6 +38,11 @@ export interface ExperimentPhase {
id: string
name: string
description?: string | null
has_soaking: boolean
has_airdrying: boolean
has_cracking: boolean
has_shelling: boolean
cracking_machine_type_id?: string | null
created_at: string
updated_at: string
created_by: string
@@ -47,15 +52,14 @@ export interface Experiment {
id: string
experiment_number: number
reps_required: number
soaking_duration_hr: number
air_drying_time_min: number
plate_contact_frequency_hz: number
throughput_rate_pecans_sec: number
crush_amount_in: number
entry_exit_height_diff_in: number
weight_per_repetition_lbs: number
results_status: ResultsStatus
completion_status: boolean
phase_id?: string | null
soaking_id?: string | null
airdrying_id?: string | null
cracking_id?: string | null
shelling_id?: string | null
created_at: string
updated_at: string
created_by: string
@@ -63,25 +67,115 @@ export interface Experiment {
// Machine Types
export interface MachineType {
id: string
name: string
description?: string | null
created_at: string
updated_at: string
created_by: string
}
// Phase-specific interfaces
export interface Soaking {
id: string
experiment_id: string
repetition_id?: string | null
scheduled_start_time: string
actual_start_time?: string | null
soaking_duration_minutes: number
scheduled_end_time: string
actual_end_time?: string | null
created_at: string
updated_at: string
created_by: string
}
export interface Airdrying {
id: string
experiment_id: string
repetition_id?: string | null
scheduled_start_time: string
actual_start_time?: string | null
duration_minutes: number
scheduled_end_time: string
actual_end_time?: string | null
created_at: string
updated_at: string
created_by: string
}
export interface Cracking {
id: string
experiment_id: string
repetition_id?: string | null
machine_type_id: string
scheduled_start_time: string
actual_start_time?: string | null
actual_end_time?: string | null
created_at: string
updated_at: string
created_by: string
}
export interface Shelling {
id: string
experiment_id: string
repetition_id?: string | null
scheduled_start_time: string
actual_start_time?: string | null
actual_end_time?: string | null
created_at: string
updated_at: string
created_by: string
}
// Machine-specific parameter interfaces
export interface JCCrackerParameters {
id: string
cracking_id: string
plate_contact_frequency_hz: number
throughput_rate_pecans_sec: number
crush_amount_in: number
entry_exit_height_diff_in: number
created_at: string
updated_at: string
}
export interface MeyerCrackerParameters {
id: string
cracking_id: string
motor_speed_hz: number
jig_displacement_inches: number
spring_stiffness_nm: number
created_at: string
updated_at: string
}
export interface CreateExperimentPhaseRequest {
name: string
description?: string
has_soaking: boolean
has_airdrying: boolean
has_cracking: boolean
has_shelling: boolean
cracking_machine_type_id?: string
}
export interface UpdateExperimentPhaseRequest {
name?: string
description?: string
has_soaking?: boolean
has_airdrying?: boolean
has_cracking?: boolean
has_shelling?: boolean
}
export interface CreateExperimentRequest {
experiment_number: number
reps_required: number
soaking_duration_hr: number
air_drying_time_min: number
plate_contact_frequency_hz: number
throughput_rate_pecans_sec: number
crush_amount_in: number
entry_exit_height_diff_in: number
weight_per_repetition_lbs: number
results_status?: ResultsStatus
completion_status?: boolean
phase_id?: string
@@ -90,12 +184,7 @@ export interface CreateExperimentRequest {
export interface UpdateExperimentRequest {
experiment_number?: number
reps_required?: number
soaking_duration_hr?: number
air_drying_time_min?: number
plate_contact_frequency_hz?: number
throughput_rate_pecans_sec?: number
crush_amount_in?: number
entry_exit_height_diff_in?: number
weight_per_repetition_lbs?: number
results_status?: ResultsStatus
completion_status?: boolean
phase_id?: string
@@ -105,12 +194,10 @@ export interface CreateRepetitionRequest {
experiment_id: string
repetition_number: number
scheduled_date?: string | null
schedule_status?: ScheduleStatus
}
export interface UpdateRepetitionRequest {
scheduled_date?: string | null
schedule_status?: ScheduleStatus
completion_status?: boolean
}
@@ -137,7 +224,6 @@ export interface ExperimentRepetition {
experiment_id: string
repetition_number: number
scheduled_date?: string | null
schedule_status: ScheduleStatus
completion_status: boolean
is_locked: boolean
locked_at?: string | null
@@ -219,6 +305,58 @@ export interface UpdatePhaseDataRequest {
[key: string]: any // For phase-specific data fields
}
// Phase creation request interfaces
export interface CreateSoakingRequest {
experiment_id: string
repetition_id?: string
scheduled_start_time: string
soaking_duration_minutes: number
actual_start_time?: string
actual_end_time?: string
}
export interface CreateAirdryingRequest {
experiment_id: string
repetition_id?: string
scheduled_start_time?: string // Will be auto-calculated from soaking if not provided
duration_minutes: number
actual_start_time?: string
actual_end_time?: string
}
export interface CreateCrackingRequest {
experiment_id: string
repetition_id?: string
machine_type_id: string
scheduled_start_time?: string // Will be auto-calculated from airdrying if not provided
actual_start_time?: string
actual_end_time?: string
}
export interface CreateShellingRequest {
experiment_id: string
repetition_id?: string
scheduled_start_time: string
actual_start_time?: string
actual_end_time?: string
}
// Machine parameter creation request interfaces
export interface CreateJCCrackerParametersRequest {
cracking_id: string
plate_contact_frequency_hz: number
throughput_rate_pecans_sec: number
crush_amount_in: number
entry_exit_height_diff_in: number
}
export interface CreateMeyerCrackerParametersRequest {
cracking_id: string
motor_speed_hz: number
jig_displacement_inches: number
spring_stiffness_nm: number
}
export interface UserRole {
id: string
user_id: string
@@ -624,6 +762,349 @@ export const experimentManagement = {
}
}
// Machine Type Management
export const machineTypeManagement = {
// Get all machine types
async getAllMachineTypes(): Promise<MachineType[]> {
const { data, error } = await supabase
.from('machine_types')
.select('*')
.order('name')
if (error) throw error
return data
},
// Get machine type by ID
async getMachineTypeById(id: string): Promise<MachineType | null> {
const { data, error } = await supabase
.from('machine_types')
.select('*')
.eq('id', id)
.single()
if (error) {
if (error.code === 'PGRST116') return null // Not found
throw error
}
return data
}
}
// Phase Management
export const phaseManagement = {
// Soaking management
async createSoaking(request: CreateSoakingRequest): Promise<Soaking> {
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) throw new Error('User not authenticated')
const { data, error } = await supabase
.from('soaking')
.insert({
...request,
created_by: user.id
})
.select()
.single()
if (error) throw error
return data
},
async updateSoaking(id: string, updates: Partial<Soaking>): Promise<Soaking> {
const { data, error } = await supabase
.from('soaking')
.update(updates)
.eq('id', id)
.select()
.single()
if (error) throw error
return data
},
async getSoakingByExperimentId(experimentId: string): Promise<Soaking | null> {
const { data, error } = await supabase
.from('soaking')
.select('*')
.eq('experiment_id', experimentId)
.single()
if (error) {
if (error.code === 'PGRST116') return null // Not found
throw error
}
return data
},
async getSoakingByRepetitionId(repetitionId: string): Promise<Soaking | null> {
const { data, error } = await supabase
.from('soaking')
.select('*')
.eq('repetition_id', repetitionId)
.single()
if (error) {
if (error.code === 'PGRST116') return null // Not found
throw error
}
return data
},
// Airdrying management
async createAirdrying(request: CreateAirdryingRequest): Promise<Airdrying> {
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) throw new Error('User not authenticated')
const { data, error } = await supabase
.from('airdrying')
.insert({
...request,
created_by: user.id
})
.select()
.single()
if (error) throw error
return data
},
async updateAirdrying(id: string, updates: Partial<Airdrying>): Promise<Airdrying> {
const { data, error } = await supabase
.from('airdrying')
.update(updates)
.eq('id', id)
.select()
.single()
if (error) throw error
return data
},
async getAirdryingByExperimentId(experimentId: string): Promise<Airdrying | null> {
const { data, error } = await supabase
.from('airdrying')
.select('*')
.eq('experiment_id', experimentId)
.single()
if (error) {
if (error.code === 'PGRST116') return null // Not found
throw error
}
return data
},
async getAirdryingByRepetitionId(repetitionId: string): Promise<Airdrying | null> {
const { data, error } = await supabase
.from('airdrying')
.select('*')
.eq('repetition_id', repetitionId)
.single()
if (error) {
if (error.code === 'PGRST116') return null // Not found
throw error
}
return data
},
// Cracking management
async createCracking(request: CreateCrackingRequest): Promise<Cracking> {
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) throw new Error('User not authenticated')
const { data, error } = await supabase
.from('cracking')
.insert({
...request,
created_by: user.id
})
.select()
.single()
if (error) throw error
return data
},
async updateCracking(id: string, updates: Partial<Cracking>): Promise<Cracking> {
const { data, error } = await supabase
.from('cracking')
.update(updates)
.eq('id', id)
.select()
.single()
if (error) throw error
return data
},
async getCrackingByExperimentId(experimentId: string): Promise<Cracking | null> {
const { data, error } = await supabase
.from('cracking')
.select('*')
.eq('experiment_id', experimentId)
.single()
if (error) {
if (error.code === 'PGRST116') return null // Not found
throw error
}
return data
},
async getCrackingByRepetitionId(repetitionId: string): Promise<Cracking | null> {
const { data, error } = await supabase
.from('cracking')
.select('*')
.eq('repetition_id', repetitionId)
.single()
if (error) {
if (error.code === 'PGRST116') return null // Not found
throw error
}
return data
},
// Shelling management
async createShelling(request: CreateShellingRequest): Promise<Shelling> {
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) throw new Error('User not authenticated')
const { data, error } = await supabase
.from('shelling')
.insert({
...request,
created_by: user.id
})
.select()
.single()
if (error) throw error
return data
},
async updateShelling(id: string, updates: Partial<Shelling>): Promise<Shelling> {
const { data, error } = await supabase
.from('shelling')
.update(updates)
.eq('id', id)
.select()
.single()
if (error) throw error
return data
},
async getShellingByExperimentId(experimentId: string): Promise<Shelling | null> {
const { data, error } = await supabase
.from('shelling')
.select('*')
.eq('experiment_id', experimentId)
.single()
if (error) {
if (error.code === 'PGRST116') return null // Not found
throw error
}
return data
},
async getShellingByRepetitionId(repetitionId: string): Promise<Shelling | null> {
const { data, error } = await supabase
.from('shelling')
.select('*')
.eq('repetition_id', repetitionId)
.single()
if (error) {
if (error.code === 'PGRST116') return null // Not found
throw error
}
return data
}
}
// Machine Parameter Management
export const machineParameterManagement = {
// JC Cracker parameters
async createJCCrackerParameters(request: CreateJCCrackerParametersRequest): Promise<JCCrackerParameters> {
const { data, error } = await supabase
.from('jc_cracker_parameters')
.insert(request)
.select()
.single()
if (error) throw error
return data
},
async updateJCCrackerParameters(id: string, updates: Partial<JCCrackerParameters>): Promise<JCCrackerParameters> {
const { data, error } = await supabase
.from('jc_cracker_parameters')
.update(updates)
.eq('id', id)
.select()
.single()
if (error) throw error
return data
},
async getJCCrackerParametersByCrackingId(crackingId: string): Promise<JCCrackerParameters | null> {
const { data, error } = await supabase
.from('jc_cracker_parameters')
.select('*')
.eq('cracking_id', crackingId)
.single()
if (error) {
if (error.code === 'PGRST116') return null // Not found
throw error
}
return data
},
// Meyer Cracker parameters
async createMeyerCrackerParameters(request: CreateMeyerCrackerParametersRequest): Promise<MeyerCrackerParameters> {
const { data, error } = await supabase
.from('meyer_cracker_parameters')
.insert(request)
.select()
.single()
if (error) throw error
return data
},
async updateMeyerCrackerParameters(id: string, updates: Partial<MeyerCrackerParameters>): Promise<MeyerCrackerParameters> {
const { data, error } = await supabase
.from('meyer_cracker_parameters')
.update(updates)
.eq('id', id)
.select()
.single()
if (error) throw error
return data
},
async getMeyerCrackerParametersByCrackingId(crackingId: string): Promise<MeyerCrackerParameters | null> {
const { data, error } = await supabase
.from('meyer_cracker_parameters')
.select('*')
.eq('cracking_id', crackingId)
.single()
if (error) {
if (error.code === 'PGRST116') return null // Not found
throw error
}
return data
}
}
// Experiment Repetitions Management
export const repetitionManagement = {
// Get all repetitions for an experiment
@@ -673,7 +1154,6 @@ export const repetitionManagement = {
async scheduleRepetition(id: string, scheduledDate: string): Promise<ExperimentRepetition> {
const updates: UpdateRepetitionRequest = {
scheduled_date: scheduledDate,
schedule_status: 'scheduled'
}
return this.updateRepetition(id, updates)
@@ -683,7 +1163,6 @@ export const repetitionManagement = {
async removeRepetitionSchedule(id: string): Promise<ExperimentRepetition> {
const updates: UpdateRepetitionRequest = {
scheduled_date: null,
schedule_status: 'pending schedule'
}
return this.updateRepetition(id, updates)
@@ -700,15 +1179,14 @@ export const repetitionManagement = {
},
// Get repetitions by status
async getRepetitionsByStatus(scheduleStatus?: ScheduleStatus): Promise<ExperimentRepetition[]> {
async getRepetitionsByStatus(isScheduled?: boolean): Promise<ExperimentRepetition[]> {
let query = supabase.from('experiment_repetitions').select('*')
if (scheduleStatus) {
query = query.eq('schedule_status', scheduleStatus)
if (isScheduled === true) {
query = query.not('scheduled_date', 'is', null)
} else if (isScheduled === false) {
query = query.is('scheduled_date', null)
}
const { data, error } = await query.order('created_at', { ascending: false })
if (error) throw error
return data
},
@@ -743,8 +1221,7 @@ export const repetitionManagement = {
for (let i = 1; i <= experiment.reps_required; i++) {
repetitions.push({
experiment_id: experimentId,
repetition_number: i,
schedule_status: 'pending schedule'
repetition_number: i
})
}