diff --git a/management-dashboard-web-app/src/components/ExperimentForm.tsx b/management-dashboard-web-app/src/components/ExperimentForm.tsx index 9e32b60..8329411 100755 --- a/management-dashboard-web-app/src/components/ExperimentForm.tsx +++ b/management-dashboard-web-app/src/components/ExperimentForm.tsx @@ -3,7 +3,7 @@ import type { CreateExperimentRequest, UpdateExperimentRequest, ScheduleStatus, import { experimentPhaseManagement, machineTypeManagement } from '../lib/supabase' interface ExperimentFormProps { - initialData?: Partial + initialData?: Partial & { phase_id?: string | null } onSubmit: (data: CreateExperimentRequest | UpdateExperimentRequest) => Promise onCancel: () => void isEditing?: boolean @@ -12,31 +12,41 @@ interface ExperimentFormProps { } export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = false, loading = false, phaseId }: ExperimentFormProps) { - const [formData, setFormData] = useState({ - 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, - phase_id: initialData?.phase_id || phaseId + const getInitialFormState = (d: any) => ({ + experiment_number: d?.experiment_number ?? 0, + reps_required: d?.reps_required ?? 1, + weight_per_repetition_lbs: d?.weight_per_repetition_lbs ?? 1, + soaking_duration_hr: d?.soaking?.soaking_duration_hr ?? d?.soaking_duration_hr ?? 0, + air_drying_time_min: d?.airdrying?.duration_minutes ?? d?.air_drying_time_min ?? 0, + plate_contact_frequency_hz: d?.cracking?.plate_contact_frequency_hz ?? d?.plate_contact_frequency_hz ?? 1, + throughput_rate_pecans_sec: d?.cracking?.throughput_rate_pecans_sec ?? d?.throughput_rate_pecans_sec ?? 1, + crush_amount_in: d?.cracking?.crush_amount_in ?? d?.crush_amount_in ?? 0, + entry_exit_height_diff_in: d?.cracking?.entry_exit_height_diff_in ?? d?.entry_exit_height_diff_in ?? 0, + motor_speed_hz: d?.cracking?.motor_speed_hz ?? d?.motor_speed_hz ?? 1, + jig_displacement_inches: d?.cracking?.jig_displacement_inches ?? d?.jig_displacement_inches ?? 0, + spring_stiffness_nm: d?.cracking?.spring_stiffness_nm ?? d?.spring_stiffness_nm ?? 1, + schedule_status: d?.schedule_status ?? 'pending schedule', + results_status: d?.results_status ?? 'valid', + completion_status: d?.completion_status ?? false, + phase_id: d?.phase_id ?? phaseId, + ring_gap_inches: d?.shelling?.ring_gap_inches ?? d?.ring_gap_inches ?? null, + drum_rpm: d?.shelling?.drum_rpm ?? d?.drum_rpm ?? null }) + const [formData, setFormData] = useState(() => getInitialFormState(initialData)) + const [errors, setErrors] = useState>({}) const [phase, setPhase] = useState(null) const [crackingMachine, setCrackingMachine] = useState(null) const [metaLoading, setMetaLoading] = useState(false) + // When initialData loads with phase config (edit mode), sync form state + useEffect(() => { + if ((initialData as any)?.id) { + setFormData(prev => ({ ...prev, ...getInitialFormState(initialData) })) + } + }, [initialData]) + useEffect(() => { const loadMeta = async () => { if (!phaseId) return @@ -76,11 +86,11 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa } - if (formData.soaking_duration_hr < 0) { + if ((formData.soaking_duration_hr ?? 0) < 0) { newErrors.soaking_duration_hr = 'Soaking duration cannot be negative' } - if (formData.air_drying_time_min < 0) { + if ((formData.air_drying_time_min ?? 0) < 0) { newErrors.air_drying_time_min = 'Air drying time cannot be negative' } @@ -93,7 +103,7 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa 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) { + if ((formData.crush_amount_in ?? 0) < 0) { newErrors.crush_amount_in = 'Crush amount cannot be negative' } } @@ -110,6 +120,16 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa } } + // Shelling: if provided, must be positive + if (phase?.has_shelling) { + if (formData.ring_gap_inches != null && (typeof formData.ring_gap_inches !== 'number' || formData.ring_gap_inches <= 0)) { + newErrors.ring_gap_inches = 'Ring gap must be positive' + } + if (formData.drum_rpm != null && (typeof formData.drum_rpm !== 'number' || formData.drum_rpm <= 0)) { + newErrors.drum_rpm = 'Drum RPM must be positive' + } + } + setErrors(newErrors) return Object.keys(newErrors).length === 0 } @@ -122,14 +142,25 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa } try { - // Prepare data for submission + // Prepare data: include all phase params so they are stored in experiment_soaking, experiment_airdrying, experiment_cracking, experiment_shelling const submitData = isEditing ? formData : { experiment_number: formData.experiment_number, reps_required: formData.reps_required, weight_per_repetition_lbs: formData.weight_per_repetition_lbs, results_status: formData.results_status, completion_status: formData.completion_status, - phase_id: formData.phase_id + phase_id: formData.phase_id, + 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, + motor_speed_hz: (formData as any).motor_speed_hz, + jig_displacement_inches: (formData as any).jig_displacement_inches, + spring_stiffness_nm: (formData as any).spring_stiffness_nm, + ring_gap_inches: formData.ring_gap_inches ?? undefined, + drum_rpm: formData.drum_rpm ?? undefined } await onSubmit(submitData) @@ -138,7 +169,7 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa } } - const handleInputChange = (field: keyof typeof formData, value: string | number | boolean) => { + const handleInputChange = (field: keyof typeof formData, value: string | number | boolean | null | undefined) => { setFormData(prev => ({ ...prev, [field]: value @@ -441,18 +472,40 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa

Shelling

-
+
+ + handleInputChange('drum_rpm' as any, e.target.value === '' ? null : parseInt(e.target.value, 10))} + 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.drum_rpm ? 'border-red-300' : 'border-gray-300'}`} + placeholder="e.g. 300" + min="1" step="1" /> + {errors.drum_rpm && ( +

{errors.drum_rpm}

+ )}
diff --git a/management-dashboard-web-app/src/components/ExperimentModal.tsx b/management-dashboard-web-app/src/components/ExperimentModal.tsx index b4d9a82..34797b2 100755 --- a/management-dashboard-web-app/src/components/ExperimentModal.tsx +++ b/management-dashboard-web-app/src/components/ExperimentModal.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import { ExperimentForm } from './ExperimentForm' import { experimentManagement } from '../lib/supabase' import type { Experiment, CreateExperimentRequest, UpdateExperimentRequest } from '../lib/supabase' @@ -13,9 +13,20 @@ interface ExperimentModalProps { export function ExperimentModal({ experiment, onClose, onExperimentSaved, phaseId }: ExperimentModalProps) { const [loading, setLoading] = useState(false) const [error, setError] = useState(null) + const [initialData, setInitialData] = useState(experiment ?? undefined) const isEditing = !!experiment + useEffect(() => { + if (experiment) { + experimentManagement.getExperimentWithPhaseConfig(experiment.id) + .then((data) => setInitialData(data ?? experiment)) + .catch(() => setInitialData(experiment)) + } else { + setInitialData(undefined) + } + }, [experiment?.id]) + const handleSubmit = async (data: CreateExperimentRequest | UpdateExperimentRequest) => { setError(null) setLoading(true) @@ -24,22 +35,24 @@ export function ExperimentModal({ experiment, onClose, onExperimentSaved, phaseI let savedExperiment: Experiment if (isEditing && experiment) { - // Check if experiment number is unique (excluding current experiment) + // Check if experiment number is unique within this phase (excluding current experiment) if ('experiment_number' in data && data.experiment_number !== undefined && data.experiment_number !== experiment.experiment_number) { - const isUnique = await experimentManagement.isExperimentNumberUnique(data.experiment_number, experiment.id) + const phaseIdToCheck = data.phase_id ?? experiment.phase_id ?? phaseId + const isUnique = await experimentManagement.isExperimentNumberUnique(data.experiment_number, phaseIdToCheck ?? undefined, experiment.id) if (!isUnique) { - setError('Experiment number already exists. Please choose a different number.') + setError('Experiment number already exists in this phase. Please choose a different number.') return } } savedExperiment = await experimentManagement.updateExperiment(experiment.id, data) } else { - // Check if experiment number is unique for new experiments + // Check if experiment number is unique within this phase for new experiments const createData = data as CreateExperimentRequest - const isUnique = await experimentManagement.isExperimentNumberUnique(createData.experiment_number) + const phaseIdToCheck = createData.phase_id ?? phaseId + const isUnique = await experimentManagement.isExperimentNumberUnique(createData.experiment_number, phaseIdToCheck ?? undefined) if (!isUnique) { - setError('Experiment number already exists. Please choose a different number.') + setError('Experiment number already exists in this phase. Please choose a different number.') return } @@ -115,7 +128,7 @@ export function ExperimentModal({ experiment, onClose, onExperimentSaved, phaseI {/* Form */}
-

Experiment Phases

-

Select an experiment phase to view and manage its experiments

-

Experiment phases help organize experiments into logical groups for easier navigation and management.

+

Experiment Books

+

Select an experiment book to view and manage its experiments

+

Experiment books help organize experiments into logical groups for easier navigation and management.

{canManagePhases && ( )}
@@ -162,9 +162,9 @@ export function ExperimentPhases({ onPhaseSelect }: ExperimentPhasesProps) { -

No experiment phases found

+

No experiment books found

- Get started by creating your first experiment phase. + Get started by creating your first experiment book.

{canManagePhases && (
@@ -172,7 +172,7 @@ export function ExperimentPhases({ onPhaseSelect }: ExperimentPhasesProps) { 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 + ➕ Create First Book
)} diff --git a/management-dashboard-web-app/src/components/PhaseExperiments.tsx b/management-dashboard-web-app/src/components/PhaseExperiments.tsx index 624d7a8..c2ae719 100644 --- a/management-dashboard-web-app/src/components/PhaseExperiments.tsx +++ b/management-dashboard-web-app/src/components/PhaseExperiments.tsx @@ -193,7 +193,7 @@ export function PhaseExperiments({ phase, onBack }: PhaseExperimentsProps) { - Back to Phases + Back to Books @@ -203,7 +203,7 @@ export function PhaseExperiments({ phase, onBack }: PhaseExperimentsProps) { {phase.description && (

{phase.description}

)} -

Manage experiments within this phase

+

Manage experiments within this book

{canManageExperiments && (