From aaeb164a32a37e395a69a9aab2c0bffe49a0168a Mon Sep 17 00:00:00 2001 From: salirezav Date: Wed, 24 Sep 2025 14:27:28 -0400 Subject: [PATCH] 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. --- .../NEW_DATABASE_SCHEMA.md | 510 ++++++++ .../meyer experiments.csv | 41 + .../phase_2_experimental_run_sheet.csv | 2 +- .../src/components/DataEntry.tsx | 4 +- .../src/components/ExperimentForm.tsx | 451 ++++--- .../src/components/ExperimentPhases.tsx | 51 +- .../src/components/Experiments.tsx | 18 +- .../src/components/PhaseExperiments.tsx | 13 +- .../src/components/PhaseForm.tsx | 288 +++++ .../src/components/PhaseModal.tsx | 68 + .../components/RepetitionScheduleModal.tsx | 2 +- .../src/components/Scheduling.tsx | 427 ++++++- .../src/lib/supabase.ts | 539 +++++++- .../supabase/.temp/cli-latest | 2 +- .../supabase/config.toml | 2 +- .../supabase/experiments_seed.sql | 196 --- .../20250101000001_complete_schema.sql | 1129 +++++++---------- ...50102000001_add_conductor_availability.sql | 77 +- ...0250103000001_composite_experiment_key.sql | 185 +++ ...00002_update_soaking_duration_to_hours.sql | 13 + ..._cracking_machine_to_experiment_phases.sql | 27 + ...23000002_require_machine_when_cracking.sql | 32 + .../20250101000001_complete_schema.sql | 785 ++++++++++++ ...50102000001_add_conductor_availability.sql | 341 +++++ ...02000002_restructure_experiment_phases.sql | 474 +++++++ ...0103000001_add_password_reset_function.sql | 0 .../20250103000002_add_user_names.sql | 0 ...103000003_add_change_password_function.sql | 0 .../supabase/{seed.sql => seed_01_users.sql} | 95 +- .../seed_04_phase2_jc_experiments.sql | 372 ++++++ .../seed_04_phase2_jc_experiments_updated.sql | 372 ++++++ .../supabase/seed_05_meyer_experiments.sql | 548 ++++++++ .../seed_05_meyer_experiments_updated.sql | 548 ++++++++ 33 files changed, 6489 insertions(+), 1123 deletions(-) create mode 100644 management-dashboard-web-app/NEW_DATABASE_SCHEMA.md create mode 100644 management-dashboard-web-app/meyer experiments.csv create mode 100644 management-dashboard-web-app/src/components/PhaseForm.tsx create mode 100644 management-dashboard-web-app/src/components/PhaseModal.tsx delete mode 100644 management-dashboard-web-app/supabase/experiments_seed.sql create mode 100644 management-dashboard-web-app/supabase/migrations/20250103000001_composite_experiment_key.sql create mode 100644 management-dashboard-web-app/supabase/migrations/20250103000002_update_soaking_duration_to_hours.sql create mode 100644 management-dashboard-web-app/supabase/migrations/20250923000001_add_cracking_machine_to_experiment_phases.sql create mode 100644 management-dashboard-web-app/supabase/migrations/20250923000002_require_machine_when_cracking.sql create mode 100644 management-dashboard-web-app/supabase/migrations_backup/20250101000001_complete_schema.sql create mode 100644 management-dashboard-web-app/supabase/migrations_backup/20250102000001_add_conductor_availability.sql create mode 100644 management-dashboard-web-app/supabase/migrations_backup/20250102000002_restructure_experiment_phases.sql rename management-dashboard-web-app/supabase/{migrations => migrations_backup}/20250103000001_add_password_reset_function.sql (100%) rename management-dashboard-web-app/supabase/{migrations => migrations_backup}/20250103000002_add_user_names.sql (100%) rename management-dashboard-web-app/supabase/{migrations => migrations_backup}/20250103000003_add_change_password_function.sql (100%) rename management-dashboard-web-app/supabase/{seed.sql => seed_01_users.sql} (78%) create mode 100644 management-dashboard-web-app/supabase/seed_04_phase2_jc_experiments.sql create mode 100644 management-dashboard-web-app/supabase/seed_04_phase2_jc_experiments_updated.sql create mode 100644 management-dashboard-web-app/supabase/seed_05_meyer_experiments.sql create mode 100644 management-dashboard-web-app/supabase/seed_05_meyer_experiments_updated.sql diff --git a/management-dashboard-web-app/NEW_DATABASE_SCHEMA.md b/management-dashboard-web-app/NEW_DATABASE_SCHEMA.md new file mode 100644 index 0000000..ef36359 --- /dev/null +++ b/management-dashboard-web-app/NEW_DATABASE_SCHEMA.md @@ -0,0 +1,510 @@ +# New Database Schema Documentation + +## Overview + +The database has been restructured to support a more flexible experiment phase system. Each experiment can now have different combinations of phases (soaking, airdrying, cracking, shelling), and each phase has its own parameters stored in separate tables. + +## Key Changes + +### 1. Experiment Phases Table +The `experiment_phases` table now includes boolean flags to indicate which phases are enabled: + +```sql +CREATE TABLE public.experiment_phases ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name TEXT NOT NULL UNIQUE, + description TEXT, + has_soaking BOOLEAN NOT NULL DEFAULT false, + has_airdrying BOOLEAN NOT NULL DEFAULT false, + has_cracking BOOLEAN NOT NULL DEFAULT false, + has_shelling BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + created_by UUID NOT NULL REFERENCES public.user_profiles(id) +); +``` + +**Constraint**: At least one phase must be selected (`has_soaking = true OR has_airdrying = true OR has_cracking = true OR has_shelling = true`). + +### 2. Machine Types Table +New table to support different cracking machines: + +```sql +CREATE TABLE public.machine_types ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name TEXT NOT NULL UNIQUE, + description TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + created_by UUID NOT NULL REFERENCES public.user_profiles(id) +); +``` + +**Default machine types**: +- JC Cracker +- Meyer Cracker + +### 3. Phase-Specific Tables + +#### Soaking Table +```sql +CREATE TABLE public.soaking ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + experiment_id UUID NOT NULL REFERENCES public.experiments(id) ON DELETE CASCADE, + repetition_id UUID REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE, + scheduled_start_time TIMESTAMP WITH TIME ZONE NOT NULL, + actual_start_time TIMESTAMP WITH TIME ZONE, + soaking_duration_minutes INTEGER NOT NULL CHECK (soaking_duration_minutes > 0), + scheduled_end_time TIMESTAMP WITH TIME ZONE NOT NULL, + actual_end_time TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + created_by UUID NOT NULL REFERENCES public.user_profiles(id) +); +``` + +**Features**: +- `scheduled_end_time` is automatically calculated from `scheduled_start_time + soaking_duration_minutes` +- Can be associated with either an experiment (template) or a specific repetition + +#### Airdrying Table +```sql +CREATE TABLE public.airdrying ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + experiment_id UUID NOT NULL REFERENCES public.experiments(id) ON DELETE CASCADE, + repetition_id UUID REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE, + scheduled_start_time TIMESTAMP WITH TIME ZONE NOT NULL, + actual_start_time TIMESTAMP WITH TIME ZONE, + duration_minutes INTEGER NOT NULL CHECK (duration_minutes > 0), + scheduled_end_time TIMESTAMP WITH TIME ZONE NOT NULL, + actual_end_time TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + created_by UUID NOT NULL REFERENCES public.user_profiles(id) +); +``` + +**Features**: +- `scheduled_start_time` is automatically set to the soaking's `scheduled_end_time` if not provided +- `scheduled_end_time` is automatically calculated from `scheduled_start_time + duration_minutes` + +#### Cracking Table +```sql +CREATE TABLE public.cracking ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + experiment_id UUID NOT NULL REFERENCES public.experiments(id) ON DELETE CASCADE, + repetition_id UUID REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE, + machine_type_id UUID NOT NULL REFERENCES public.machine_types(id), + scheduled_start_time TIMESTAMP WITH TIME ZONE NOT NULL, + actual_start_time TIMESTAMP WITH TIME ZONE, + actual_end_time TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + created_by UUID NOT NULL REFERENCES public.user_profiles(id) +); +``` + +**Features**: +- `scheduled_start_time` is automatically set to the airdrying's `scheduled_end_time` if not provided +- No duration or scheduled end time (user sets actual end time) +- Requires selection of machine type + +#### Shelling Table +```sql +CREATE TABLE public.shelling ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + experiment_id UUID NOT NULL REFERENCES public.experiments(id) ON DELETE CASCADE, + repetition_id UUID REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE, + scheduled_start_time TIMESTAMP WITH TIME ZONE NOT NULL, + actual_start_time TIMESTAMP WITH TIME ZONE, + actual_end_time TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + created_by UUID NOT NULL REFERENCES public.user_profiles(id) +); +``` + +### 4. Machine-Specific Parameter Tables + +#### JC Cracker Parameters +```sql +CREATE TABLE public.jc_cracker_parameters ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + cracking_id UUID NOT NULL REFERENCES public.cracking(id) ON DELETE CASCADE, + plate_contact_frequency_hz DOUBLE PRECISION NOT NULL CHECK (plate_contact_frequency_hz > 0), + throughput_rate_pecans_sec DOUBLE PRECISION NOT NULL CHECK (throughput_rate_pecans_sec > 0), + crush_amount_in DOUBLE PRECISION NOT NULL CHECK (crush_amount_in >= 0), + entry_exit_height_diff_in DOUBLE PRECISION NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); +``` + +#### Meyer Cracker Parameters +```sql +CREATE TABLE public.meyer_cracker_parameters ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + cracking_id UUID NOT NULL REFERENCES public.cracking(id) ON DELETE CASCADE, + motor_speed_hz DOUBLE PRECISION NOT NULL CHECK (motor_speed_hz > 0), + jig_displacement_inches DOUBLE PRECISION NOT NULL CHECK (jig_displacement_inches >= 0), + spring_stiffness_nm DOUBLE PRECISION NOT NULL CHECK (spring_stiffness_nm > 0), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); +``` + +### 5. Updated Experiments Table +```sql +CREATE TABLE public.experiments ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + experiment_number INTEGER UNIQUE NOT NULL, + reps_required INTEGER NOT NULL CHECK (reps_required > 0), + weight_per_repetition_lbs DOUBLE PRECISION NOT NULL DEFAULT 0 CHECK (weight_per_repetition_lbs > 0), + results_status TEXT NOT NULL DEFAULT 'valid' CHECK (results_status IN ('valid', 'invalid')), + completion_status BOOLEAN NOT NULL DEFAULT false, + phase_id UUID REFERENCES public.experiment_phases(id) ON DELETE SET NULL, + soaking_id UUID REFERENCES public.soaking(id) ON DELETE SET NULL, + airdrying_id UUID REFERENCES public.airdrying(id) ON DELETE SET NULL, + cracking_id UUID REFERENCES public.cracking(id) ON DELETE SET NULL, + shelling_id UUID REFERENCES public.shelling(id) ON DELETE SET NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + created_by UUID NOT NULL REFERENCES public.user_profiles(id) +); +``` + +**Changes**: +- Added `weight_per_repetition_lbs` field +- Added foreign key references to phase tables +- Removed phase-specific parameters (moved to phase tables) + +## Database Views + +### experiments_with_phases +A comprehensive view that joins experiments with all their phase information: + +```sql +CREATE VIEW public.experiments_with_phases AS +SELECT + e.*, + ep.name as phase_name, + ep.description as phase_description, + ep.has_soaking, + ep.has_airdrying, + ep.has_cracking, + ep.has_shelling, + s.id as soaking_id, + s.scheduled_start_time as soaking_scheduled_start, + s.actual_start_time as soaking_actual_start, + s.soaking_duration_minutes, + s.scheduled_end_time as soaking_scheduled_end, + s.actual_end_time as soaking_actual_end, + ad.id as airdrying_id, + ad.scheduled_start_time as airdrying_scheduled_start, + ad.actual_start_time as airdrying_actual_start, + ad.duration_minutes as airdrying_duration, + ad.scheduled_end_time as airdrying_scheduled_end, + ad.actual_end_time as airdrying_actual_end, + c.id as cracking_id, + c.scheduled_start_time as cracking_scheduled_start, + c.actual_start_time as cracking_actual_start, + c.actual_end_time as cracking_actual_end, + mt.name as machine_type_name, + sh.id as shelling_id, + sh.scheduled_start_time as shelling_scheduled_start, + sh.actual_start_time as shelling_actual_start, + sh.actual_end_time as shelling_actual_end +FROM public.experiments e +LEFT JOIN public.experiment_phases ep ON e.phase_id = ep.id +LEFT JOIN public.soaking s ON e.soaking_id = s.id +LEFT JOIN public.airdrying ad ON e.airdrying_id = ad.id +LEFT JOIN public.cracking c ON e.cracking_id = c.id +LEFT JOIN public.machine_types mt ON c.machine_type_id = mt.id +LEFT JOIN public.shelling sh ON e.shelling_id = sh.id; +``` + +### repetitions_with_phases +A view for repetitions with their phase information: + +```sql +CREATE VIEW public.repetitions_with_phases AS +SELECT + er.*, + e.experiment_number, + e.weight_per_repetition_lbs, + ep.name as phase_name, + ep.has_soaking, + ep.has_airdrying, + ep.has_cracking, + ep.has_shelling, + -- ... (similar phase fields as above) +FROM public.experiment_repetitions er +JOIN public.experiments e ON er.experiment_id = e.id +LEFT JOIN public.experiment_phases ep ON e.phase_id = ep.id +LEFT JOIN public.soaking s ON er.id = s.repetition_id +LEFT JOIN public.airdrying ad ON er.id = ad.repetition_id +LEFT JOIN public.cracking c ON er.id = c.repetition_id +LEFT JOIN public.machine_types mt ON c.machine_type_id = mt.id +LEFT JOIN public.shelling sh ON er.id = sh.repetition_id; +``` + +## TypeScript Interfaces + +### New Interfaces Added + +```typescript +// 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 +} +``` + +### Updated Interfaces + +```typescript +export interface ExperimentPhase { + id: string + name: string + description?: string | null + has_soaking: boolean + has_airdrying: boolean + has_cracking: boolean + has_shelling: boolean + created_at: string + updated_at: string + created_by: string +} + +export interface Experiment { + id: string + experiment_number: number + reps_required: 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 +} +``` + +## API Management Functions + +### New Management Objects + +1. **machineTypeManagement**: Functions to manage machine types +2. **phaseManagement**: Functions to manage all phase-specific data +3. **machineParameterManagement**: Functions to manage machine-specific parameters + +### Key Functions + +```typescript +// Machine Type Management +machineTypeManagement.getAllMachineTypes() +machineTypeManagement.getMachineTypeById(id) + +// Phase Management +phaseManagement.createSoaking(request) +phaseManagement.createAirdrying(request) +phaseManagement.createCracking(request) +phaseManagement.createShelling(request) + +// Machine Parameter Management +machineParameterManagement.createJCCrackerParameters(request) +machineParameterManagement.createMeyerCrackerParameters(request) +``` + +## Usage Examples + +### Creating an Experiment with Phases + +1. **Create Experiment Phase Definition**: +```typescript +const phaseData = { + name: "Full Process Experiment", + description: "Complete pecan processing with all phases", + has_soaking: true, + has_airdrying: true, + has_cracking: true, + has_shelling: true +}; +const phase = await experimentPhaseManagement.createExperimentPhase(phaseData); +``` + +2. **Create Experiment**: +```typescript +const experimentData = { + experiment_number: 1001, + reps_required: 3, + weight_per_repetition_lbs: 50.0, + phase_id: phase.id +}; +const experiment = await experimentManagement.createExperiment(experimentData); +``` + +3. **Create Phase Data** (if phases are enabled): +```typescript +// Soaking +const soakingData = { + experiment_id: experiment.id, + scheduled_start_time: "2025-01-15T08:00:00Z", + soaking_duration_minutes: 120 +}; +const soaking = await phaseManagement.createSoaking(soakingData); + +// Airdrying (scheduled start will be auto-calculated from soaking end) +const airdryingData = { + experiment_id: experiment.id, + duration_minutes: 180 +}; +const airdrying = await phaseManagement.createAirdrying(airdryingData); + +// Cracking +const crackingData = { + experiment_id: experiment.id, + machine_type_id: "jc-cracker-id", // or meyer-cracker-id + // scheduled start will be auto-calculated from airdrying end +}; +const cracking = await phaseManagement.createCracking(crackingData); + +// Add machine-specific parameters +const jcParams = { + cracking_id: cracking.id, + plate_contact_frequency_hz: 60.0, + throughput_rate_pecans_sec: 2.5, + crush_amount_in: 0.25, + entry_exit_height_diff_in: 0.5 +}; +await machineParameterManagement.createJCCrackerParameters(jcParams); +``` + +4. **Update Experiment with Phase References**: +```typescript +await experimentManagement.updateExperiment(experiment.id, { + soaking_id: soaking.id, + airdrying_id: airdrying.id, + cracking_id: cracking.id +}); +``` + +### Creating Repetitions with Phase Data + +When creating repetitions, you can create phase-specific data for each repetition: + +```typescript +// Create repetition +const repetition = await repetitionManagement.createRepetition({ + experiment_id: experiment.id, + repetition_number: 1 +}); + +// Create phase data for this specific repetition +const repetitionSoaking = await phaseManagement.createSoaking({ + experiment_id: experiment.id, + repetition_id: repetition.id, + scheduled_start_time: "2025-01-15T08:00:00Z", + soaking_duration_minutes: 120 +}); +``` + +## Migration Notes + +- The old phase-specific parameters in the `experiments` table are preserved for backward compatibility +- New experiments should use the new phase-specific tables +- The existing draft system continues to work with the new schema +- All RLS policies are properly configured for the new tables + +## Benefits of New Schema + +1. **Flexibility**: Each experiment can have different combinations of phases +2. **Machine Support**: Easy to add new machine types with different parameters +3. **Separation of Concerns**: Phase parameters are stored in dedicated tables +4. **Automatic Calculations**: Scheduled times are automatically calculated based on phase dependencies +5. **Scalability**: Easy to add new phases or modify existing ones +6. **Data Integrity**: Strong constraints ensure data consistency diff --git a/management-dashboard-web-app/meyer experiments.csv b/management-dashboard-web-app/meyer experiments.csv new file mode 100644 index 0000000..40e952f --- /dev/null +++ b/management-dashboard-web-app/meyer experiments.csv @@ -0,0 +1,41 @@ +Motor Speed (Hz),Soaking Time (hr),Air Drying Time (min),Jig Displacement (in),Spring Stiffness (N/m) +33,27,28,-0.307,1800 +30,37,17,-0.311,2000 +47,36,50,-0.291,1800 +42,12,30,-0.314,2000 +53,34,19,-0.302,1800 +37,18,40,-0.301,2200 +40,14,59,-0.286,2000 +39,18,32,-0.309,1800 +49,11,31,-0.299,2200 +47,33,12,-0.295,2000 +52,23,36,-0.302,2000 +59,37,35,-0.299,1800 +41,15,15,-0.312,2000 +46,24,22,-0.303,1800 +50,36,15,-0.308,1800 +36,32,48,-0.306,2200 +33,28,38,-0.308,2200 +35,31,51,-0.311,1800 +55,20,57,-0.304,2000 +44,10,27,-0.313,2200 +37,16,43,-0.294,2000 +56,25,42,-0.31,2200 +30,13,21,-0.292,2200 +60,29,46,-0.294,2200 +41,21,54,-0.306,2000 +55,29,54,-0.296,1800 +39,30,48,-0.293,2200 +34,35,53,-0.285,2200 +57,32,39,-0.291,1800 +45,27,38,-0.296,2200 +52,17,25,-0.297,1800 +51,13,22,-0.288,2200 +36,19,11,-0.29,2000 +44,38,32,-0.315,1800 +58,26,18,-0.289,1800 +32,22,52,-0.288,1800 +43,12,56,-0.287,2200 +60,16,45,-0.298,2200 +54,22,25,-0.301,2000 +48,24,13,-0.305,2000 \ No newline at end of file diff --git a/management-dashboard-web-app/phase_2_experimental_run_sheet.csv b/management-dashboard-web-app/phase_2_experimental_run_sheet.csv index 6555955..d9e8aa7 100755 --- a/management-dashboard-web-app/phase_2_experimental_run_sheet.csv +++ b/management-dashboard-web-app/phase_2_experimental_run_sheet.csv @@ -1,4 +1,4 @@ -experiment_id,run_number,soaking_duration_hr,air_drying_time_min,plate_contact_frequency_hz,throughput_rate_pecans_sec,crush_amount_in,entry_exit_height_diff_in,reps,rep +run_id,experiment_number,soaking_duration_hr,air_drying_time_min,plate_contact_frequency_hz,throughput_rate_pecans_sec,crush_amount_in,entry_exit_height_diff_in,reps,rep 1,0,34,19,53,28,0.05,-0.09,3,1 2,1,24,27,34,29,0.03,0.01,3,3 3,12,28,59,37,23,0.06,-0.08,3,1 diff --git a/management-dashboard-web-app/src/components/DataEntry.tsx b/management-dashboard-web-app/src/components/DataEntry.tsx index aacc103..0ae5672 100755 --- a/management-dashboard-web-app/src/components/DataEntry.tsx +++ b/management-dashboard-web-app/src/components/DataEntry.tsx @@ -300,11 +300,11 @@ function RepetitionCard({ experiment, repetition, onSelect, status }: Repetition {getStatusIcon()} - - {repetition.schedule_status === 'pending schedule' ? 'Pending' : repetition.schedule_status} + {repetition.scheduled_date ? 'scheduled' : 'pending'} diff --git a/management-dashboard-web-app/src/components/ExperimentForm.tsx b/management-dashboard-web-app/src/components/ExperimentForm.tsx index c6afc51..9e32b60 100755 --- a/management-dashboard-web-app/src/components/ExperimentForm.tsx +++ b/management-dashboard-web-app/src/components/ExperimentForm.tsx @@ -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 @@ -14,12 +15,17 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa 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, @@ -27,6 +33,31 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa }) const [errors, setErrors] = useState>({}) + const [phase, setPhase] = useState(null) + const [crackingMachine, setCrackingMachine] = useState(null) + const [metaLoading, setMetaLoading] = useState(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 = {} @@ -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 )} +
+ + 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 && ( +

{errors.weight_per_repetition_lbs}

+ )} +
- {/* Experiment Parameters */} -
-

Experiment Parameters

-
+ {/* Dynamic Sections by Phase */} +
+ {/* Soaking */} + {phase?.has_soaking && (
- - 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 && ( -

{errors.soaking_duration_hr}

- )} +

Soaking

+
+
+ + 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 && ( +

{errors.soaking_duration_hr}

+ )} +
+
+ )} + {/* Air-Drying */} + {phase?.has_airdrying && (
- - 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 && ( -

{errors.air_drying_time_min}

- )} +

Air-Drying

+
+
+ + 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 && ( +

{errors.air_drying_time_min}

+ )} +
+
+ )} + {/* Cracking - machine specific */} + {phase?.has_cracking && (
- - 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 && ( -

{errors.plate_contact_frequency_hz}

- )} -
+

Cracking {crackingMachine ? `(${crackingMachine.name})` : ''}

+
+ {crackingMachine?.name === 'JC Cracker' && ( + <> +
+ + 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 && ( +

{errors.plate_contact_frequency_hz}

+ )} +
+
+ + 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 && ( +

{errors.throughput_rate_pecans_sec}

+ )} +
+ +
+ + 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 && ( +

{errors.crush_amount_in}

+ )} +
+ +
+ + 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 && ( +

{errors.entry_exit_height_diff_in}

+ )} +

Positive values indicate entry is higher than exit

+
+ + )} + + {crackingMachine?.name === 'Meyer Cracker' && ( + <> +
+ + 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 && ( +

{errors.motor_speed_hz}

+ )} +
+ +
+ + 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 && ( +

{errors.jig_displacement_inches}

+ )} +
+ +
+ + 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 && ( +

{errors.spring_stiffness_nm}

+ )} +
+ + )} +
+
+ )} + + {/* Shelling */} + {phase?.has_shelling && (
- - 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 && ( -

{errors.throughput_rate_pecans_sec}

- )} +

Shelling

+
+
+ + 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" + /> +
+
- -
- - 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 && ( -

{errors.crush_amount_in}

- )} -
- -
- - 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 && ( -

{errors.entry_exit_height_diff_in}

- )} -

Positive values indicate entry is higher than exit

-
-
+ )}
{/* Status Fields (only show when editing) */} diff --git a/management-dashboard-web-app/src/components/ExperimentPhases.tsx b/management-dashboard-web-app/src/components/ExperimentPhases.tsx index 9c80576..a9aac80 100644 --- a/management-dashboard-web-app/src/components/ExperimentPhases.tsx +++ b/management-dashboard-web-app/src/components/ExperimentPhases.tsx @@ -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(null) const [currentUser, setCurrentUser] = useState(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 (
@@ -60,10 +67,7 @@ export function ExperimentPhases({ onPhaseSelect }: ExperimentPhasesProps) {
{canManagePhases && (