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 && (
)}
+ {/* Enabled Phases */}
+
+
+ {phase.has_soaking && (
+
+ ๐ฐ Soaking
+
+ )}
+ {phase.has_airdrying && (
+
+ ๐จ Air-Drying
+
+ )}
+ {phase.has_cracking && (
+
+ ๐จ Cracking
+
+ )}
+ {phase.has_shelling && (
+
+ ๐ Shelling
+
+ )}
+
+
+
Created {new Date(phase.created_at).toLocaleDateString()}
@@ -139,10 +169,7 @@ export function ExperimentPhases({ onPhaseSelect }: ExperimentPhasesProps) {
{canManagePhases && (
)}
+
+ {/* Create Phase Modal */}
+ {showCreateModal && (
+
setShowCreateModal(false)}
+ onPhaseCreated={handlePhaseCreated}
+ />
+ )}
)
}
diff --git a/management-dashboard-web-app/src/components/Experiments.tsx b/management-dashboard-web-app/src/components/Experiments.tsx
index ea11e53..fa1d0eb 100755
--- a/management-dashboard-web-app/src/components/Experiments.tsx
+++ b/management-dashboard-web-app/src/components/Experiments.tsx
@@ -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() {
|
- Experiment #
+ #
|
Reps Required
@@ -311,8 +309,8 @@ export function Experiments() {
Rep #{repetition.repetition_number}
-
- {repetition.schedule_status === 'pending schedule' ? 'Pending' : repetition.schedule_status}
+
+ {repetition.scheduled_date ? 'scheduled' : 'pending'}
|