Refactor experiment management and update data structures

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

View File

@@ -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

View File

@@ -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
1 Motor Speed (Hz) Soaking Time (hr) Air Drying Time (min) Jig Displacement (in) Spring Stiffness (N/m)
2 33 27 28 -0.307 1800
3 30 37 17 -0.311 2000
4 47 36 50 -0.291 1800
5 42 12 30 -0.314 2000
6 53 34 19 -0.302 1800
7 37 18 40 -0.301 2200
8 40 14 59 -0.286 2000
9 39 18 32 -0.309 1800
10 49 11 31 -0.299 2200
11 47 33 12 -0.295 2000
12 52 23 36 -0.302 2000
13 59 37 35 -0.299 1800
14 41 15 15 -0.312 2000
15 46 24 22 -0.303 1800
16 50 36 15 -0.308 1800
17 36 32 48 -0.306 2200
18 33 28 38 -0.308 2200
19 35 31 51 -0.311 1800
20 55 20 57 -0.304 2000
21 44 10 27 -0.313 2200
22 37 16 43 -0.294 2000
23 56 25 42 -0.31 2200
24 30 13 21 -0.292 2200
25 60 29 46 -0.294 2200
26 41 21 54 -0.306 2000
27 55 29 54 -0.296 1800
28 39 30 48 -0.293 2200
29 34 35 53 -0.285 2200
30 57 32 39 -0.291 1800
31 45 27 38 -0.296 2200
32 52 17 25 -0.297 1800
33 51 13 22 -0.288 2200
34 36 19 11 -0.29 2000
35 44 38 32 -0.315 1800
36 58 26 18 -0.289 1800
37 32 22 52 -0.288 1800
38 43 12 56 -0.287 2200
39 60 16 45 -0.298 2200
40 54 22 25 -0.301 2000
41 48 24 13 -0.305 2000

View File

@@ -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
1 experiment_id run_id run_number 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
2 1 0 34 19 53 28 0.05 -0.09 3 1
3 2 1 24 27 34 29 0.03 0.01 3 3
4 3 12 28 59 37 23 0.06 -0.08 3 1

View File

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

View File

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

View File

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

View File

@@ -108,10 +108,8 @@ export function Experiments() {
try {
const newRepetition = await repetitionManagement.createRepetition({
experiment_id: experiment.id,
repetition_number: repetitionNumber,
schedule_status: 'pending schedule'
repetition_number: repetitionNumber
})
setExperimentRepetitions(prev => ({
...prev,
[experiment.id]: [...(prev[experiment.id] || []), newRepetition].sort((a, b) => a.repetition_number - b.repetition_number)
@@ -141,8 +139,8 @@ export function Experiments() {
}
const getRepetitionStatusSummary = (repetitions: ExperimentRepetition[]) => {
const scheduled = repetitions.filter(r => r.schedule_status === 'scheduled').length
const pending = repetitions.filter(r => r.schedule_status === 'pending schedule').length
const scheduled = repetitions.filter(r => r.scheduled_date).length
const pending = repetitions.filter(r => !r.scheduled_date).length
const completed = repetitions.filter(r => r.completion_status).length
return { scheduled, pending, completed, total: repetitions.length }
@@ -225,7 +223,7 @@ export function Experiments() {
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Experiment #
#
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Reps Required
@@ -311,8 +309,8 @@ export function Experiments() {
<div key={repetition.id} className="flex items-center justify-between p-2 bg-gray-50 rounded">
<span className="text-sm font-medium">Rep #{repetition.repetition_number}</span>
<div className="flex items-center space-x-2">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusBadgeColor(repetition.schedule_status)}`}>
{repetition.schedule_status === 'pending schedule' ? 'Pending' : repetition.schedule_status}
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusBadgeColor(repetition.scheduled_date ? 'scheduled' : 'pending')}`}>
{repetition.scheduled_date ? 'scheduled' : 'pending'}
</span>
<button
onClick={(e) => {
@@ -320,7 +318,7 @@ export function Experiments() {
handleScheduleRepetition(experiment, repetition)
}}
className="text-blue-600 hover:text-blue-900 p-1 rounded hover:bg-blue-50 transition-colors"
title={repetition.schedule_status === 'scheduled' ? 'Reschedule' : 'Schedule'}
title={repetition.scheduled_date ? 'Reschedule' : 'Schedule'}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
@@ -352,7 +350,7 @@ export function Experiments() {
</td>
)}
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusBadgeColor(experiment.results_status)}`}>
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${experiment.results_status}`}>
{experiment.results_status}
</span>
</td>

View File

@@ -114,7 +114,6 @@ export function PhaseExperiments({ phase, onBack }: PhaseExperimentsProps) {
const newRepetition = await repetitionManagement.createRepetition({
experiment_id: experiment.id,
repetition_number: repetitionNumber,
schedule_status: 'pending schedule'
})
setExperimentRepetitions(prev => ({
@@ -146,16 +145,16 @@ export function PhaseExperiments({ phase, onBack }: PhaseExperimentsProps) {
}
const getRepetitionStatusSummary = (repetitions: ExperimentRepetition[]) => {
const scheduled = repetitions.filter(r => r.schedule_status === 'scheduled').length
const pending = repetitions.filter(r => r.schedule_status === 'pending schedule').length
const scheduled = repetitions.filter(r => r.scheduled_date).length
const pending = repetitions.filter(r => !r.scheduled_date).length
const completed = repetitions.filter(r => r.completion_status).length
return { scheduled, pending, completed, total: repetitions.length }
}
const getStatusBadgeColor = (status: ScheduleStatus | ResultsStatus) => {
const getStatusBadgeColor = (status: ScheduleStatus | ResultsStatus | string) => {
switch (status) {
case 'pending schedule':
case 'pending':
return 'bg-yellow-100 text-yellow-800'
case 'scheduled':
return 'bg-blue-100 text-blue-800'
@@ -326,7 +325,7 @@ export function PhaseExperiments({ phase, onBack }: PhaseExperimentsProps) {
<span className="text-sm font-medium">Rep #{repetition.repetition_number}</span>
<div className="flex items-center space-x-2">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusBadgeColor(repetition.schedule_status)}`}>
{repetition.schedule_status === 'pending schedule' ? 'Pending' : repetition.schedule_status}
{repetition.scheduled_date ? 'scheduled' : 'pending'}
</span>
<button
onClick={(e) => {
@@ -334,7 +333,7 @@ export function PhaseExperiments({ phase, onBack }: PhaseExperimentsProps) {
handleScheduleRepetition(experiment, repetition)
}}
className="text-blue-600 hover:text-blue-900 p-1 rounded hover:bg-blue-50 transition-colors"
title={repetition.schedule_status === 'scheduled' ? 'Reschedule' : 'Schedule'}
title={repetition.scheduled_date ? 'Reschedule' : 'Schedule'}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +1 @@
v2.40.7
v2.45.5

View File

@@ -57,7 +57,7 @@ schema_paths = []
enabled = true
# Specifies an ordered list of seed files to load during db reset.
# Supports glob patterns relative to supabase directory: "./seeds/*.sql"
sql_paths = ["./seed.sql"]
sql_paths = ["./seed_01_users.sql", "./seed_04_phase2_jc_experiments.sql", "./seed_05_meyer_experiments.sql"]
[db.network_restrictions]
# Enable management of network restrictions.

View File

@@ -1,196 +0,0 @@
-- Experiments Seed Data
-- This file contains all 50 experiments for Phase 2 of JC Experiments
-- =============================================
-- INSERT ALL 50 EXPERIMENTS
-- =============================================
INSERT INTO public.experiments (
experiment_number,
reps_required,
soaking_duration_hr,
air_drying_time_min,
plate_contact_frequency_hz,
throughput_rate_pecans_sec,
crush_amount_in,
entry_exit_height_diff_in,
results_status,
completion_status,
phase_id,
created_by
) VALUES
-- Experiments 1-10
(1, 3, 2.0, 30, 15.0, 2.5, 0.125, 0.5, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(2, 3, 2.5, 45, 12.0, 2.0, 0.150, 0.75, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(3, 3, 3.0, 60, 10.0, 1.8, 0.175, 1.0, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(4, 3, 1.5, 20, 18.0, 3.0, 0.100, 0.25, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(5, 3, 2.0, 30, 15.0, 2.5, 0.125, 0.5, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(6, 3, 2.5, 45, 12.0, 2.0, 0.150, 0.75, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(7, 3, 3.0, 60, 10.0, 1.8, 0.175, 1.0, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(8, 3, 1.5, 20, 18.0, 3.0, 0.100, 0.25, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(9, 3, 2.0, 30, 15.0, 2.5, 0.125, 0.5, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(10, 3, 2.5, 45, 12.0, 2.0, 0.150, 0.75, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
-- Experiments 11-20
(11, 3, 3.0, 60, 10.0, 1.8, 0.175, 1.0, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(12, 3, 1.5, 20, 18.0, 3.0, 0.100, 0.25, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(13, 3, 2.0, 30, 15.0, 2.5, 0.125, 0.5, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(14, 3, 2.5, 45, 12.0, 2.0, 0.150, 0.75, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(15, 3, 3.0, 60, 10.0, 1.8, 0.175, 1.0, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(16, 3, 1.5, 20, 18.0, 3.0, 0.100, 0.25, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(17, 3, 2.0, 30, 15.0, 2.5, 0.125, 0.5, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(18, 3, 2.5, 45, 12.0, 2.0, 0.150, 0.75, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(19, 3, 3.0, 60, 10.0, 1.8, 0.175, 1.0, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(20, 3, 1.5, 20, 18.0, 3.0, 0.100, 0.25, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
-- Experiments 21-30
(21, 3, 2.0, 30, 15.0, 2.5, 0.125, 0.5, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(22, 3, 2.5, 45, 12.0, 2.0, 0.150, 0.75, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(23, 3, 3.0, 60, 10.0, 1.8, 0.175, 1.0, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(24, 3, 1.5, 20, 18.0, 3.0, 0.100, 0.25, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(25, 3, 2.0, 30, 15.0, 2.5, 0.125, 0.5, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(26, 3, 2.5, 45, 12.0, 2.0, 0.150, 0.75, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(27, 3, 3.0, 60, 10.0, 1.8, 0.175, 1.0, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(28, 3, 1.5, 20, 18.0, 3.0, 0.100, 0.25, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(29, 3, 2.0, 30, 15.0, 2.5, 0.125, 0.5, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(30, 3, 2.5, 45, 12.0, 2.0, 0.150, 0.75, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
-- Experiments 31-40
(31, 3, 3.0, 60, 10.0, 1.8, 0.175, 1.0, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(32, 3, 1.5, 20, 18.0, 3.0, 0.100, 0.25, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(33, 3, 2.0, 30, 15.0, 2.5, 0.125, 0.5, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(34, 3, 2.5, 45, 12.0, 2.0, 0.150, 0.75, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(35, 3, 3.0, 60, 10.0, 1.8, 0.175, 1.0, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(36, 3, 1.5, 20, 18.0, 3.0, 0.100, 0.25, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(37, 3, 2.0, 30, 15.0, 2.5, 0.125, 0.5, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(38, 3, 2.5, 45, 12.0, 2.0, 0.150, 0.75, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(39, 3, 3.0, 60, 10.0, 1.8, 0.175, 1.0, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(40, 3, 1.5, 20, 18.0, 3.0, 0.100, 0.25, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
-- Experiments 41-50
(41, 3, 2.0, 30, 15.0, 2.5, 0.125, 0.5, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(42, 3, 2.5, 45, 12.0, 2.0, 0.150, 0.75, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(43, 3, 3.0, 60, 10.0, 1.8, 0.175, 1.0, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(44, 3, 1.5, 20, 18.0, 3.0, 0.100, 0.25, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(45, 3, 2.0, 30, 15.0, 2.5, 0.125, 0.5, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(46, 3, 2.5, 45, 12.0, 2.0, 0.150, 0.75, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(47, 3, 3.0, 60, 10.0, 1.8, 0.175, 1.0, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(48, 3, 1.5, 20, 18.0, 3.0, 0.100, 0.25, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(49, 3, 2.0, 30, 15.0, 2.5, 0.125, 0.5, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(50, 3, 2.5, 45, 12.0, 2.0, 0.150, 0.75, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com'))
;
-- =============================================
-- CREATE SAMPLE EXPERIMENT REPETITIONS
-- =============================================
-- Create repetitions for first 5 experiments as examples
INSERT INTO public.experiment_repetitions (experiment_id, repetition_number, created_by)
SELECT
e.id,
rep_num,
e.created_by
FROM public.experiments e
CROSS JOIN generate_series(1, 3) AS rep_num
WHERE e.experiment_number <= 5
;

View File

@@ -72,7 +72,78 @@ CREATE INDEX IF NOT EXISTS idx_experiment_phase_assignments_scheduled_start ON p
CREATE INDEX IF NOT EXISTS idx_experiment_phase_assignments_created_by ON public.experiment_phase_assignments(created_by);
-- =============================================
-- 4. FUNCTIONS FOR OVERLAP PREVENTION
-- 4. UTILITY FUNCTIONS
-- =============================================
-- Function to handle updated_at timestamp
CREATE OR REPLACE FUNCTION public.handle_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Helper function to get current user's roles
CREATE OR REPLACE FUNCTION public.get_user_roles()
RETURNS TEXT[] AS $$
BEGIN
RETURN ARRAY(
SELECT r.name
FROM public.user_roles ur
JOIN public.roles r ON ur.role_id = r.id
WHERE ur.user_id = auth.uid()
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Helper function to get current user's first role (for backward compatibility)
CREATE OR REPLACE FUNCTION public.get_user_role()
RETURNS TEXT AS $$
BEGIN
-- Return the first role found (for backward compatibility)
RETURN (
SELECT r.name
FROM public.user_roles ur
JOIN public.roles r ON ur.role_id = r.id
WHERE ur.user_id = auth.uid()
LIMIT 1
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Helper function to check if user is admin
CREATE OR REPLACE FUNCTION public.is_admin()
RETURNS BOOLEAN AS $$
BEGIN
RETURN 'admin' = ANY(public.get_user_roles());
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Helper function to check if user has specific role
CREATE OR REPLACE FUNCTION public.has_role(role_name TEXT)
RETURNS BOOLEAN AS $$
BEGIN
RETURN role_name = ANY(public.get_user_roles());
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Helper function to check if user can manage experiments
CREATE OR REPLACE FUNCTION public.can_manage_experiments()
RETURNS BOOLEAN AS $$
BEGIN
RETURN EXISTS (
SELECT 1
FROM public.user_roles ur
JOIN public.roles r ON ur.role_id = r.id
WHERE ur.user_id = auth.uid()
AND r.name IN ('admin', 'conductor')
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- =============================================
-- 5. FUNCTIONS FOR OVERLAP PREVENTION
-- =============================================
-- Function to check for overlapping availabilities
@@ -156,7 +227,7 @@ END;
$$ LANGUAGE plpgsql;
-- =============================================
-- 5. TRIGGERS
-- 6. TRIGGERS
-- =============================================
-- Create trigger for updated_at on conductor_availability
@@ -239,7 +310,7 @@ END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- =============================================
-- 7. ROW LEVEL SECURITY (RLS)
-- 8. ROW LEVEL SECURITY (RLS)
-- =============================================
-- Enable RLS on new tables

View File

@@ -0,0 +1,185 @@
-- Migration: Change experiments table to use composite primary key (experiment_number, phase_id)
-- This allows each phase to have its own experiment numbering starting from 1
-- =============================================
-- 1. DROP EXISTING FOREIGN KEY CONSTRAINTS
-- =============================================
-- Drop foreign key constraints that reference experiments table
ALTER TABLE public.experiment_repetitions DROP CONSTRAINT IF EXISTS experiment_repetitions_experiment_id_fkey;
ALTER TABLE public.soaking DROP CONSTRAINT IF EXISTS soaking_experiment_id_fkey;
ALTER TABLE public.airdrying DROP CONSTRAINT IF EXISTS airdrying_experiment_id_fkey;
ALTER TABLE public.cracking DROP CONSTRAINT IF EXISTS cracking_experiment_id_fkey;
ALTER TABLE public.shelling DROP CONSTRAINT IF EXISTS shelling_experiment_id_fkey;
ALTER TABLE public.conductor_availability DROP CONSTRAINT IF EXISTS conductor_availability_experiment_id_fkey;
-- =============================================
-- 2. MODIFY EXPERIMENTS TABLE
-- =============================================
-- Drop the existing primary key and unique constraint
ALTER TABLE public.experiments DROP CONSTRAINT IF EXISTS experiments_pkey;
ALTER TABLE public.experiments DROP CONSTRAINT IF EXISTS experiments_experiment_number_key;
-- Make phase_id NOT NULL since it's now part of the primary key
ALTER TABLE public.experiments ALTER COLUMN phase_id SET NOT NULL;
-- Add composite primary key
ALTER TABLE public.experiments ADD CONSTRAINT experiments_pkey PRIMARY KEY (experiment_number, phase_id);
-- =============================================
-- 3. UPDATE FOREIGN KEY COLUMNS
-- =============================================
-- Add phase_id columns to tables that reference experiments
ALTER TABLE public.experiment_repetitions ADD COLUMN IF NOT EXISTS experiment_phase_id UUID;
ALTER TABLE public.soaking ADD COLUMN IF NOT EXISTS experiment_phase_id UUID;
ALTER TABLE public.airdrying ADD COLUMN IF NOT EXISTS experiment_phase_id UUID;
ALTER TABLE public.cracking ADD COLUMN IF NOT EXISTS experiment_phase_id UUID;
ALTER TABLE public.shelling ADD COLUMN IF NOT EXISTS experiment_phase_id UUID;
ALTER TABLE public.conductor_availability ADD COLUMN IF NOT EXISTS experiment_phase_id UUID;
-- Populate the phase_id columns from the experiments table
UPDATE public.experiment_repetitions
SET experiment_phase_id = e.phase_id
FROM public.experiments e
WHERE experiment_repetitions.experiment_id = e.id;
UPDATE public.soaking
SET experiment_phase_id = e.phase_id
FROM public.experiments e
WHERE soaking.experiment_id = e.id;
UPDATE public.airdrying
SET experiment_phase_id = e.phase_id
FROM public.experiments e
WHERE airdrying.experiment_id = e.id;
UPDATE public.cracking
SET experiment_phase_id = e.phase_id
FROM public.experiments e
WHERE cracking.experiment_id = e.id;
UPDATE public.shelling
SET experiment_phase_id = e.phase_id
FROM public.experiments e
WHERE shelling.experiment_id = e.id;
UPDATE public.conductor_availability
SET experiment_phase_id = e.phase_id
FROM public.experiments e
WHERE conductor_availability.experiment_id = e.id;
-- Make the phase_id columns NOT NULL
ALTER TABLE public.experiment_repetitions ALTER COLUMN experiment_phase_id SET NOT NULL;
ALTER TABLE public.soaking ALTER COLUMN experiment_phase_id SET NOT NULL;
ALTER TABLE public.airdrying ALTER COLUMN experiment_phase_id SET NOT NULL;
ALTER TABLE public.cracking ALTER COLUMN experiment_phase_id SET NOT NULL;
ALTER TABLE public.shelling ALTER COLUMN experiment_phase_id SET NOT NULL;
ALTER TABLE public.conductor_availability ALTER COLUMN experiment_phase_id SET NOT NULL;
-- =============================================
-- 4. ADD NEW FOREIGN KEY CONSTRAINTS
-- =============================================
-- Add foreign key constraints using composite key
ALTER TABLE public.experiment_repetitions
ADD CONSTRAINT experiment_repetitions_experiment_fkey
FOREIGN KEY (experiment_id, experiment_phase_id)
REFERENCES public.experiments(experiment_number, phase_id) ON DELETE CASCADE;
ALTER TABLE public.soaking
ADD CONSTRAINT soaking_experiment_fkey
FOREIGN KEY (experiment_id, experiment_phase_id)
REFERENCES public.experiments(experiment_number, phase_id) ON DELETE CASCADE;
ALTER TABLE public.airdrying
ADD CONSTRAINT airdrying_experiment_fkey
FOREIGN KEY (experiment_id, experiment_phase_id)
REFERENCES public.experiments(experiment_number, phase_id) ON DELETE CASCADE;
ALTER TABLE public.cracking
ADD CONSTRAINT cracking_experiment_fkey
FOREIGN KEY (experiment_id, experiment_phase_id)
REFERENCES public.experiments(experiment_number, phase_id) ON DELETE CASCADE;
ALTER TABLE public.shelling
ADD CONSTRAINT shelling_experiment_fkey
FOREIGN KEY (experiment_id, experiment_phase_id)
REFERENCES public.experiments(experiment_number, phase_id) ON DELETE CASCADE;
ALTER TABLE public.conductor_availability
ADD CONSTRAINT conductor_availability_experiment_fkey
FOREIGN KEY (experiment_id, experiment_phase_id)
REFERENCES public.experiments(experiment_number, phase_id) ON DELETE CASCADE;
-- =============================================
-- 5. UPDATE UNIQUE CONSTRAINTS
-- =============================================
-- Update unique constraints to use composite key
ALTER TABLE public.experiment_repetitions
DROP CONSTRAINT IF EXISTS experiment_repetitions_experiment_id_repetition_number_key;
ALTER TABLE public.experiment_repetitions
ADD CONSTRAINT experiment_repetitions_experiment_repetition_key
UNIQUE (experiment_id, experiment_phase_id, repetition_number);
-- Update unique constraints for phase tables
ALTER TABLE public.soaking
DROP CONSTRAINT IF EXISTS unique_soaking_per_experiment;
ALTER TABLE public.soaking
ADD CONSTRAINT unique_soaking_per_experiment
UNIQUE (experiment_id, experiment_phase_id);
ALTER TABLE public.airdrying
DROP CONSTRAINT IF EXISTS unique_airdrying_per_experiment;
ALTER TABLE public.airdrying
ADD CONSTRAINT unique_airdrying_per_experiment
UNIQUE (experiment_id, experiment_phase_id);
ALTER TABLE public.cracking
DROP CONSTRAINT IF EXISTS unique_cracking_per_experiment;
ALTER TABLE public.cracking
ADD CONSTRAINT unique_cracking_per_experiment
UNIQUE (experiment_id, experiment_phase_id);
ALTER TABLE public.shelling
DROP CONSTRAINT IF EXISTS unique_shelling_per_experiment;
ALTER TABLE public.shelling
ADD CONSTRAINT unique_shelling_per_experiment
UNIQUE (experiment_id, experiment_phase_id);
-- =============================================
-- 6. UPDATE INDEXES
-- =============================================
-- Drop old indexes
DROP INDEX IF EXISTS idx_soaking_experiment_id;
DROP INDEX IF EXISTS idx_airdrying_experiment_id;
DROP INDEX IF EXISTS idx_cracking_experiment_id;
DROP INDEX IF EXISTS idx_shelling_experiment_id;
-- Create new composite indexes
CREATE INDEX IF NOT EXISTS idx_soaking_experiment_composite ON public.soaking(experiment_id, experiment_phase_id);
CREATE INDEX IF NOT EXISTS idx_airdrying_experiment_composite ON public.airdrying(experiment_id, experiment_phase_id);
CREATE INDEX IF NOT EXISTS idx_cracking_experiment_composite ON public.cracking(experiment_id, experiment_phase_id);
CREATE INDEX IF NOT EXISTS idx_shelling_experiment_composite ON public.shelling(experiment_id, experiment_phase_id);
CREATE INDEX IF NOT EXISTS idx_experiment_repetitions_experiment_composite ON public.experiment_repetitions(experiment_id, experiment_phase_id);
CREATE INDEX IF NOT EXISTS idx_conductor_availability_experiment_composite ON public.conductor_availability(experiment_id, experiment_phase_id);
-- =============================================
-- 7. UPDATE EXPERIMENTS TABLE FOREIGN KEY REFERENCES
-- =============================================
-- The experiments table has foreign key references to phase tables
-- These need to be updated to use the new composite key structure
-- We'll need to update these after the phase tables are updated
-- Note: The soaking_id, airdrying_id, cracking_id, shelling_id columns in experiments table
-- will need to be updated to reference the new composite structure
-- This will be handled in the seed files update

View File

@@ -0,0 +1,13 @@
-- Convert soaking duration to hours instead of minutes
-- 1) Rename column
ALTER TABLE public.soaking RENAME COLUMN soaking_duration_minutes TO soaking_duration_hours;
-- 2) Change type to double precision to allow fractional hours
ALTER TABLE public.soaking ALTER COLUMN soaking_duration_hours TYPE DOUBLE PRECISION USING soaking_duration_hours::double precision;
-- 3) Convert existing data (currently minutes) to hours
UPDATE public.soaking SET soaking_duration_hours = soaking_duration_hours / 60.0;
-- 4) Ensure CHECK constraint (> 0)
ALTER TABLE public.soaking DROP CONSTRAINT IF EXISTS soaking_soaking_duration_minutes_check;
ALTER TABLE public.soaking ADD CONSTRAINT soaking_soaking_duration_hours_check CHECK (soaking_duration_hours > 0);

View File

@@ -0,0 +1,27 @@
-- Migration: Add cracking_machine_type_id to experiment_phases
-- Adds optional reference to machine_types so a phase can specify the cracking machine
BEGIN;
-- 1) Add column (nullable to avoid breaking existing data)
ALTER TABLE public.experiment_phases
ADD COLUMN IF NOT EXISTS cracking_machine_type_id UUID NULL;
-- 2) Add foreign key to machine_types
ALTER TABLE public.experiment_phases
ADD CONSTRAINT fk_experiment_phases_cracking_machine_type
FOREIGN KEY (cracking_machine_type_id)
REFERENCES public.machine_types(id)
ON DELETE SET NULL;
-- 3) Optional: index for lookup/filtering
CREATE INDEX IF NOT EXISTS idx_experiment_phases_cracking_machine_type_id
ON public.experiment_phases (cracking_machine_type_id);
COMMIT;

View File

@@ -0,0 +1,32 @@
-- Migration: Require cracking_machine_type_id when has_cracking is true
BEGIN;
-- Drop existing constraint if it exists (for re-runs)
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM pg_constraint c
JOIN pg_class t ON c.conrelid = t.oid
WHERE t.relname = 'experiment_phases'
AND c.conname = 'ck_experiment_phases_machine_required_when_cracking'
) THEN
ALTER TABLE public.experiment_phases
DROP CONSTRAINT ck_experiment_phases_machine_required_when_cracking;
END IF;
END $$;
-- Add check: if has_cracking then cracking_machine_type_id must not be null
ALTER TABLE public.experiment_phases
ADD CONSTRAINT ck_experiment_phases_machine_required_when_cracking
CHECK (
(has_cracking = false) OR (cracking_machine_type_id IS NOT NULL)
);
COMMIT;

View File

@@ -0,0 +1,785 @@
-- Complete Database Schema for USDA Vision Pecan Experiments System
-- This migration creates the entire database schema from scratch
-- Enable necessary extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- =============================================
-- 1. ROLES AND USER MANAGEMENT
-- =============================================
-- Create roles table
CREATE TABLE IF NOT EXISTS public.roles (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT UNIQUE NOT NULL CHECK (name IN ('admin', 'conductor', 'analyst', 'data recorder')),
description TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create user_profiles table to extend auth.users
CREATE TABLE IF NOT EXISTS public.user_profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
email TEXT NOT NULL,
status TEXT DEFAULT 'active' CHECK (status IN ('active', 'disabled')),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create user_roles junction table for many-to-many relationship
CREATE TABLE IF NOT EXISTS public.user_roles (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES public.user_profiles(id) ON DELETE CASCADE,
role_id UUID NOT NULL REFERENCES public.roles(id) ON DELETE CASCADE,
assigned_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
assigned_by UUID REFERENCES public.user_profiles(id),
UNIQUE(user_id, role_id)
);
-- =============================================
-- 2. EXPERIMENT PHASES
-- =============================================
-- Create experiment_phases table
CREATE TABLE IF NOT EXISTS public.experiment_phases (
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)
);
-- =============================================
-- 3. EXPERIMENTS
-- =============================================
-- Create experiments table
CREATE TABLE IF NOT EXISTS 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),
soaking_duration_hr FLOAT NOT NULL CHECK (soaking_duration_hr >= 0),
air_drying_time_min INTEGER NOT NULL CHECK (air_drying_time_min >= 0),
plate_contact_frequency_hz FLOAT NOT NULL CHECK (plate_contact_frequency_hz > 0),
throughput_rate_pecans_sec FLOAT NOT NULL CHECK (throughput_rate_pecans_sec > 0),
crush_amount_in FLOAT NOT NULL CHECK (crush_amount_in >= 0),
entry_exit_height_diff_in FLOAT NOT NULL,
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,
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. EXPERIMENT REPETITIONS
-- =============================================
-- Create experiment_repetitions table
CREATE TABLE IF NOT EXISTS public.experiment_repetitions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
experiment_id UUID NOT NULL REFERENCES public.experiments(id) ON DELETE CASCADE,
repetition_number INTEGER NOT NULL CHECK (repetition_number > 0),
scheduled_date TIMESTAMP WITH TIME ZONE,
schedule_status TEXT NOT NULL DEFAULT 'pending schedule'
CHECK (schedule_status IN ('pending schedule', 'scheduled', 'canceled', 'aborted')),
completion_status BOOLEAN NOT NULL DEFAULT false,
is_locked BOOLEAN NOT NULL DEFAULT false,
locked_at TIMESTAMP WITH TIME ZONE,
locked_by UUID REFERENCES public.user_profiles(id),
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),
-- Ensure unique repetition numbers per experiment
CONSTRAINT unique_repetition_per_experiment UNIQUE (experiment_id, repetition_number)
);
-- =============================================
-- 5. DATA ENTRY SYSTEM
-- =============================================
-- Create experiment_phase_drafts table for phase-specific draft management
CREATE TABLE IF NOT EXISTS public.experiment_phase_drafts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
experiment_id UUID NOT NULL REFERENCES public.experiments(id) ON DELETE CASCADE,
repetition_id UUID NOT NULL REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES public.user_profiles(id),
phase_name TEXT NOT NULL CHECK (phase_name IN ('pre-soaking', 'air-drying', 'cracking', 'shelling')),
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'submitted', 'withdrawn')),
draft_name TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
submitted_at TIMESTAMP WITH TIME ZONE,
withdrawn_at TIMESTAMP WITH TIME ZONE
);
-- Create experiment_phase_data table for phase-specific measurements
CREATE TABLE IF NOT EXISTS public.experiment_phase_data (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
phase_draft_id UUID NOT NULL REFERENCES public.experiment_phase_drafts(id) ON DELETE CASCADE,
phase_name TEXT NOT NULL CHECK (phase_name IN ('pre-soaking', 'air-drying', 'cracking', 'shelling')),
-- Pre-soaking phase data
batch_initial_weight_lbs FLOAT CHECK (batch_initial_weight_lbs >= 0),
initial_shell_moisture_pct FLOAT CHECK (initial_shell_moisture_pct >= 0 AND initial_shell_moisture_pct <= 100),
initial_kernel_moisture_pct FLOAT CHECK (initial_kernel_moisture_pct >= 0 AND initial_kernel_moisture_pct <= 100),
soaking_start_time TIMESTAMP WITH TIME ZONE,
-- Air-drying phase data
airdrying_start_time TIMESTAMP WITH TIME ZONE,
post_soak_weight_lbs FLOAT CHECK (post_soak_weight_lbs >= 0),
post_soak_kernel_moisture_pct FLOAT CHECK (post_soak_kernel_moisture_pct >= 0 AND post_soak_kernel_moisture_pct <= 100),
post_soak_shell_moisture_pct FLOAT CHECK (post_soak_shell_moisture_pct >= 0 AND post_soak_shell_moisture_pct <= 100),
avg_pecan_diameter_in FLOAT CHECK (avg_pecan_diameter_in >= 0),
-- Cracking phase data
cracking_start_time TIMESTAMP WITH TIME ZONE,
-- Shelling phase data
shelling_start_time TIMESTAMP WITH TIME ZONE,
bin_1_weight_lbs FLOAT CHECK (bin_1_weight_lbs >= 0),
bin_2_weight_lbs FLOAT CHECK (bin_2_weight_lbs >= 0),
bin_3_weight_lbs FLOAT CHECK (bin_3_weight_lbs >= 0),
discharge_bin_weight_lbs FLOAT CHECK (discharge_bin_weight_lbs >= 0),
bin_1_full_yield_oz FLOAT CHECK (bin_1_full_yield_oz >= 0),
bin_2_full_yield_oz FLOAT CHECK (bin_2_full_yield_oz >= 0),
bin_3_full_yield_oz FLOAT CHECK (bin_3_full_yield_oz >= 0),
bin_1_half_yield_oz FLOAT CHECK (bin_1_half_yield_oz >= 0),
bin_2_half_yield_oz FLOAT CHECK (bin_2_half_yield_oz >= 0),
bin_3_half_yield_oz FLOAT CHECK (bin_3_half_yield_oz >= 0),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- Constraint: One record per phase draft
CONSTRAINT unique_phase_per_draft UNIQUE (phase_draft_id, phase_name)
);
-- Create pecan_diameter_measurements table for individual diameter measurements
CREATE TABLE IF NOT EXISTS public.pecan_diameter_measurements (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
phase_data_id UUID NOT NULL REFERENCES public.experiment_phase_data(id) ON DELETE CASCADE,
measurement_number INTEGER NOT NULL CHECK (measurement_number >= 1 AND measurement_number <= 10),
diameter_in FLOAT NOT NULL CHECK (diameter_in >= 0),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- Constraint: Unique measurement number per phase data
CONSTRAINT unique_measurement_per_phase UNIQUE (phase_data_id, measurement_number)
);
-- =============================================
-- 6. INDEXES FOR PERFORMANCE
-- =============================================
-- User management indexes
CREATE INDEX IF NOT EXISTS idx_user_profiles_email ON public.user_profiles(email);
CREATE INDEX IF NOT EXISTS idx_user_profiles_status ON public.user_profiles(status);
CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON public.user_roles(user_id);
CREATE INDEX IF NOT EXISTS idx_user_roles_role_id ON public.user_roles(role_id);
-- Experiment phases indexes
CREATE INDEX IF NOT EXISTS idx_experiment_phases_name ON public.experiment_phases(name);
CREATE INDEX IF NOT EXISTS idx_experiment_phases_created_by ON public.experiment_phases(created_by);
-- Experiments indexes
CREATE INDEX IF NOT EXISTS idx_experiments_experiment_number ON public.experiments(experiment_number);
CREATE INDEX IF NOT EXISTS idx_experiments_created_by ON public.experiments(created_by);
CREATE INDEX IF NOT EXISTS idx_experiments_results_status ON public.experiments(results_status);
CREATE INDEX IF NOT EXISTS idx_experiments_completion_status ON public.experiments(completion_status);
CREATE INDEX IF NOT EXISTS idx_experiments_created_at ON public.experiments(created_at);
CREATE INDEX IF NOT EXISTS idx_experiments_phase_id ON public.experiments(phase_id);
-- Experiment repetitions indexes
CREATE INDEX IF NOT EXISTS idx_experiment_repetitions_experiment_id ON public.experiment_repetitions(experiment_id);
CREATE INDEX IF NOT EXISTS idx_experiment_repetitions_schedule_status ON public.experiment_repetitions(schedule_status);
CREATE INDEX IF NOT EXISTS idx_experiment_repetitions_completion_status ON public.experiment_repetitions(completion_status);
CREATE INDEX IF NOT EXISTS idx_experiment_repetitions_scheduled_date ON public.experiment_repetitions(scheduled_date);
CREATE INDEX IF NOT EXISTS idx_experiment_repetitions_created_by ON public.experiment_repetitions(created_by);
CREATE INDEX IF NOT EXISTS idx_experiment_repetitions_created_at ON public.experiment_repetitions(created_at);
CREATE INDEX IF NOT EXISTS idx_experiment_repetitions_is_locked ON public.experiment_repetitions(is_locked);
-- Data entry system indexes
CREATE INDEX IF NOT EXISTS idx_experiment_phase_drafts_experiment_id ON public.experiment_phase_drafts(experiment_id);
CREATE INDEX IF NOT EXISTS idx_experiment_phase_drafts_repetition_id ON public.experiment_phase_drafts(repetition_id);
CREATE INDEX IF NOT EXISTS idx_experiment_phase_drafts_user_id ON public.experiment_phase_drafts(user_id);
CREATE INDEX IF NOT EXISTS idx_experiment_phase_drafts_phase_name ON public.experiment_phase_drafts(phase_name);
CREATE INDEX IF NOT EXISTS idx_experiment_phase_drafts_status ON public.experiment_phase_drafts(status);
CREATE INDEX IF NOT EXISTS idx_experiment_phase_data_draft_id ON public.experiment_phase_data(phase_draft_id);
CREATE INDEX IF NOT EXISTS idx_experiment_phase_data_phase_name ON public.experiment_phase_data(phase_name);
CREATE INDEX IF NOT EXISTS idx_pecan_diameter_measurements_phase_data_id ON public.pecan_diameter_measurements(phase_data_id);
-- =============================================
-- 7. TRIGGERS AND FUNCTIONS
-- =============================================
-- Create updated_at trigger function
CREATE OR REPLACE FUNCTION public.handle_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create triggers for updated_at
CREATE TRIGGER set_updated_at_roles
BEFORE UPDATE ON public.roles
FOR EACH ROW
EXECUTE FUNCTION public.handle_updated_at();
CREATE TRIGGER set_updated_at_user_profiles
BEFORE UPDATE ON public.user_profiles
FOR EACH ROW
EXECUTE FUNCTION public.handle_updated_at();
CREATE TRIGGER set_updated_at_experiment_phases
BEFORE UPDATE ON public.experiment_phases
FOR EACH ROW
EXECUTE FUNCTION public.handle_updated_at();
CREATE TRIGGER set_updated_at_experiments
BEFORE UPDATE ON public.experiments
FOR EACH ROW
EXECUTE FUNCTION public.handle_updated_at();
CREATE TRIGGER set_updated_at_experiment_repetitions
BEFORE UPDATE ON public.experiment_repetitions
FOR EACH ROW
EXECUTE FUNCTION public.handle_updated_at();
CREATE TRIGGER set_updated_at_experiment_phase_drafts
BEFORE UPDATE ON public.experiment_phase_drafts
FOR EACH ROW
EXECUTE FUNCTION public.handle_updated_at();
CREATE TRIGGER set_updated_at_experiment_phase_data
BEFORE UPDATE ON public.experiment_phase_data
FOR EACH ROW
EXECUTE FUNCTION public.handle_updated_at();
-- Function to validate repetition number doesn't exceed experiment's reps_required
CREATE OR REPLACE FUNCTION validate_repetition_number()
RETURNS TRIGGER AS $$
DECLARE
max_reps INTEGER;
BEGIN
-- Get the reps_required for this experiment
SELECT reps_required INTO max_reps
FROM public.experiments
WHERE id = NEW.experiment_id;
-- Check if repetition number exceeds the limit
IF NEW.repetition_number > max_reps THEN
RAISE EXCEPTION 'Repetition number % exceeds maximum allowed repetitions % for experiment',
NEW.repetition_number, max_reps;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create trigger to validate repetition number
CREATE TRIGGER trigger_validate_repetition_number
BEFORE INSERT OR UPDATE ON public.experiment_repetitions
FOR EACH ROW
EXECUTE FUNCTION validate_repetition_number();
-- Function to handle phase draft status changes
CREATE OR REPLACE FUNCTION public.handle_phase_draft_status_change()
RETURNS TRIGGER AS $$
BEGIN
-- Set submitted_at when status changes to 'submitted'
IF NEW.status = 'submitted' AND OLD.status != 'submitted' THEN
NEW.submitted_at = NOW();
NEW.withdrawn_at = NULL;
END IF;
-- Set withdrawn_at when status changes to 'withdrawn'
IF NEW.status = 'withdrawn' AND OLD.status = 'submitted' THEN
NEW.withdrawn_at = NOW();
END IF;
-- Clear timestamps when status changes back to 'draft'
IF NEW.status = 'draft' AND OLD.status IN ('submitted', 'withdrawn') THEN
NEW.submitted_at = NULL;
NEW.withdrawn_at = NULL;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER set_timestamps_experiment_phase_drafts
BEFORE UPDATE ON public.experiment_phase_drafts
FOR EACH ROW
EXECUTE FUNCTION public.handle_phase_draft_status_change();
-- =============================================
-- 8. HELPER FUNCTIONS
-- =============================================
-- Helper function to get current user's roles
CREATE OR REPLACE FUNCTION public.get_user_roles()
RETURNS TEXT[] AS $$
BEGIN
RETURN ARRAY(
SELECT r.name
FROM public.user_roles ur
JOIN public.roles r ON ur.role_id = r.id
WHERE ur.user_id = auth.uid()
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Helper function to get current user's first role (for backward compatibility)
CREATE OR REPLACE FUNCTION public.get_user_role()
RETURNS TEXT AS $$
BEGIN
-- Return the first role found (for backward compatibility)
RETURN (
SELECT r.name
FROM public.user_roles ur
JOIN public.roles r ON ur.role_id = r.id
WHERE ur.user_id = auth.uid()
LIMIT 1
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Helper function to check if user is admin
CREATE OR REPLACE FUNCTION public.is_admin()
RETURNS BOOLEAN AS $$
BEGIN
RETURN 'admin' = ANY(public.get_user_roles());
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Helper function to check if user has specific role
CREATE OR REPLACE FUNCTION public.has_role(role_name TEXT)
RETURNS BOOLEAN AS $$
BEGIN
RETURN role_name = ANY(public.get_user_roles());
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Helper function to check if user can manage experiments
CREATE OR REPLACE FUNCTION public.can_manage_experiments()
RETURNS BOOLEAN AS $$
BEGIN
RETURN EXISTS (
SELECT 1
FROM public.user_roles ur
JOIN public.roles r ON ur.role_id = r.id
WHERE ur.user_id = auth.uid()
AND r.name IN ('admin', 'conductor')
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function to generate secure temporary password
CREATE OR REPLACE FUNCTION public.generate_temp_password()
RETURNS TEXT AS $$
DECLARE
chars TEXT := 'ABCDEFGHJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789';
result TEXT := '';
i INTEGER;
BEGIN
FOR i IN 1..12 LOOP
result := result || substr(chars, floor(random() * length(chars) + 1)::integer, 1);
END LOOP;
RETURN result;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function to create user with roles (for admin use)
CREATE OR REPLACE FUNCTION public.create_user_with_roles(
user_email TEXT,
role_names TEXT[],
temp_password TEXT DEFAULT NULL
)
RETURNS JSON AS $$
DECLARE
new_user_id UUID;
role_record RECORD;
generated_password TEXT;
result JSON;
role_count INTEGER;
BEGIN
-- Only admins can create users
IF NOT public.is_admin() THEN
RAISE EXCEPTION 'Only administrators can create users';
END IF;
-- Validate that at least one role is provided
IF array_length(role_names, 1) IS NULL OR array_length(role_names, 1) = 0 THEN
RAISE EXCEPTION 'At least one role must be assigned to the user';
END IF;
-- Validate that all provided roles exist
SELECT COUNT(*) INTO role_count
FROM public.roles
WHERE name = ANY(role_names);
IF role_count != array_length(role_names, 1) THEN
RAISE EXCEPTION 'One or more specified roles do not exist';
END IF;
-- Check if user already exists
IF EXISTS (SELECT 1 FROM auth.users WHERE email = user_email) THEN
RAISE EXCEPTION 'User with email % already exists', user_email;
END IF;
-- Generate password if not provided
IF temp_password IS NULL THEN
generated_password := public.generate_temp_password();
ELSE
generated_password := temp_password;
END IF;
-- Generate new user ID
new_user_id := uuid_generate_v4();
-- Insert into auth.users (simulating user creation)
INSERT INTO auth.users (
instance_id,
id,
aud,
role,
email,
encrypted_password,
email_confirmed_at,
created_at,
updated_at,
confirmation_token,
email_change,
email_change_token_new,
recovery_token
) VALUES (
'00000000-0000-0000-0000-000000000000',
new_user_id,
'authenticated',
'authenticated',
user_email,
crypt(generated_password, gen_salt('bf')),
NOW(),
NOW(),
NOW(),
'',
'',
'',
''
);
-- Insert user profile
INSERT INTO public.user_profiles (id, email, status)
VALUES (new_user_id, user_email, 'active');
-- Assign roles through the user_roles junction table
FOR role_record IN
SELECT id FROM public.roles WHERE name = ANY(role_names)
LOOP
INSERT INTO public.user_roles (user_id, role_id, assigned_by)
VALUES (new_user_id, role_record.id, auth.uid());
END LOOP;
-- Return result
result := json_build_object(
'user_id', new_user_id,
'email', user_email,
'temp_password', generated_password,
'roles', role_names,
'status', 'active'
);
RETURN result;
EXCEPTION
WHEN OTHERS THEN
-- Clean up any partial inserts
DELETE FROM public.user_roles WHERE user_id = new_user_id;
DELETE FROM public.user_profiles WHERE id = new_user_id;
DELETE FROM auth.users WHERE id = new_user_id;
RAISE;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- =============================================
-- 9. ROW LEVEL SECURITY (RLS)
-- =============================================
-- Enable RLS on all tables
ALTER TABLE public.roles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.user_profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.user_roles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.experiment_phases ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.experiments ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.experiment_repetitions ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.experiment_phase_drafts ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.experiment_phase_data ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.pecan_diameter_measurements ENABLE ROW LEVEL SECURITY;
-- Roles table policies
CREATE POLICY "Anyone can read roles" ON public.roles
FOR SELECT USING (true);
CREATE POLICY "Only admins can insert roles" ON public.roles
FOR INSERT WITH CHECK (public.is_admin());
CREATE POLICY "Only admins can update roles" ON public.roles
FOR UPDATE USING (public.is_admin());
CREATE POLICY "Only admins can delete roles" ON public.roles
FOR DELETE USING (public.is_admin());
-- User profiles policies
CREATE POLICY "Users can read own profile, admins can read all" ON public.user_profiles
FOR SELECT USING (
auth.uid() = id OR public.is_admin()
);
CREATE POLICY "Only admins can insert user profiles" ON public.user_profiles
FOR INSERT WITH CHECK (public.is_admin());
CREATE POLICY "Users can update own profile, admins can update any" ON public.user_profiles
FOR UPDATE USING (
auth.uid() = id OR public.is_admin()
);
CREATE POLICY "Only admins can delete user profiles" ON public.user_profiles
FOR DELETE USING (public.is_admin());
-- User roles policies
CREATE POLICY "Users can read own roles, admins can read all" ON public.user_roles
FOR SELECT USING (
user_id = auth.uid() OR public.is_admin()
);
CREATE POLICY "Only admins can assign roles" ON public.user_roles
FOR INSERT WITH CHECK (public.is_admin());
CREATE POLICY "Only admins can update role assignments" ON public.user_roles
FOR UPDATE USING (public.is_admin());
CREATE POLICY "Only admins can remove role assignments" ON public.user_roles
FOR DELETE USING (public.is_admin());
-- Experiment phases policies
CREATE POLICY "experiment_phases_select_policy" ON public.experiment_phases
FOR SELECT
TO authenticated
USING (true);
CREATE POLICY "experiment_phases_insert_policy" ON public.experiment_phases
FOR INSERT
TO authenticated
WITH CHECK (public.can_manage_experiments());
CREATE POLICY "experiment_phases_update_policy" ON public.experiment_phases
FOR UPDATE
TO authenticated
USING (public.can_manage_experiments())
WITH CHECK (public.can_manage_experiments());
CREATE POLICY "experiment_phases_delete_policy" ON public.experiment_phases
FOR DELETE
TO authenticated
USING (public.is_admin());
-- Experiments policies
CREATE POLICY "experiments_select_policy" ON public.experiments
FOR SELECT
TO authenticated
USING (true);
CREATE POLICY "experiments_insert_policy" ON public.experiments
FOR INSERT
TO authenticated
WITH CHECK (public.can_manage_experiments());
CREATE POLICY "experiments_update_policy" ON public.experiments
FOR UPDATE
TO authenticated
USING (public.can_manage_experiments())
WITH CHECK (public.can_manage_experiments());
CREATE POLICY "experiments_delete_policy" ON public.experiments
FOR DELETE
TO authenticated
USING (public.is_admin());
-- Experiment repetitions policies
CREATE POLICY "Users can view experiment repetitions" ON public.experiment_repetitions
FOR SELECT
TO authenticated
USING (true);
CREATE POLICY "Users can create experiment repetitions" ON public.experiment_repetitions
FOR INSERT WITH CHECK (
experiment_id IN (
SELECT id FROM public.experiments
WHERE created_by = auth.uid()
)
OR public.is_admin()
);
CREATE POLICY "Users can update experiment repetitions" ON public.experiment_repetitions
FOR UPDATE USING (
experiment_id IN (
SELECT id FROM public.experiments
WHERE created_by = auth.uid()
)
OR public.is_admin()
);
CREATE POLICY "Users can delete experiment repetitions" ON public.experiment_repetitions
FOR DELETE USING (
experiment_id IN (
SELECT id FROM public.experiments
WHERE created_by = auth.uid()
)
OR public.is_admin()
);
-- Experiment phase drafts policies
CREATE POLICY "experiment_phase_drafts_select_policy" ON public.experiment_phase_drafts
FOR SELECT
TO authenticated
USING (true);
CREATE POLICY "experiment_phase_drafts_insert_policy" ON public.experiment_phase_drafts
FOR INSERT
TO authenticated
WITH CHECK (user_id = auth.uid());
CREATE POLICY "experiment_phase_drafts_update_policy" ON public.experiment_phase_drafts
FOR UPDATE
TO authenticated
USING (
(user_id = auth.uid() AND NOT EXISTS (
SELECT 1 FROM public.experiment_repetitions
WHERE id = repetition_id AND is_locked = true
)) OR public.is_admin()
)
WITH CHECK (
(user_id = auth.uid() AND NOT EXISTS (
SELECT 1 FROM public.experiment_repetitions
WHERE id = repetition_id AND is_locked = true
)) OR public.is_admin()
);
CREATE POLICY "experiment_phase_drafts_delete_policy" ON public.experiment_phase_drafts
FOR DELETE
TO authenticated
USING (
(user_id = auth.uid() AND status = 'draft' AND NOT EXISTS (
SELECT 1 FROM public.experiment_repetitions
WHERE id = repetition_id AND is_locked = true
)) OR public.is_admin()
);
-- Experiment phase data policies
CREATE POLICY "experiment_phase_data_select_policy" ON public.experiment_phase_data
FOR SELECT
TO authenticated
USING (true);
CREATE POLICY "experiment_phase_data_insert_policy" ON public.experiment_phase_data
FOR INSERT
TO authenticated
WITH CHECK (
EXISTS (
SELECT 1 FROM public.experiment_phase_drafts epd
WHERE epd.id = phase_draft_id AND epd.user_id = auth.uid()
)
);
CREATE POLICY "experiment_phase_data_update_policy" ON public.experiment_phase_data
FOR UPDATE
TO authenticated
USING (
EXISTS (
SELECT 1 FROM public.experiment_phase_drafts epd
WHERE epd.id = phase_draft_id AND epd.user_id = auth.uid()
)
)
WITH CHECK (
EXISTS (
SELECT 1 FROM public.experiment_phase_drafts epd
WHERE epd.id = phase_draft_id AND epd.user_id = auth.uid()
)
);
CREATE POLICY "experiment_phase_data_delete_policy" ON public.experiment_phase_data
FOR DELETE
TO authenticated
USING (
EXISTS (
SELECT 1 FROM public.experiment_phase_drafts epd
WHERE epd.id = phase_draft_id AND epd.user_id = auth.uid() AND epd.status = 'draft'
)
);
-- Pecan diameter measurements policies
CREATE POLICY "pecan_diameter_measurements_select_policy" ON public.pecan_diameter_measurements
FOR SELECT
TO authenticated
USING (true);
CREATE POLICY "pecan_diameter_measurements_insert_policy" ON public.pecan_diameter_measurements
FOR INSERT
TO authenticated
WITH CHECK (
EXISTS (
SELECT 1 FROM public.experiment_phase_data epd
JOIN public.experiment_phase_drafts epdr ON epd.phase_draft_id = epdr.id
WHERE epd.id = phase_data_id AND epdr.user_id = auth.uid()
)
);
CREATE POLICY "pecan_diameter_measurements_update_policy" ON public.pecan_diameter_measurements
FOR UPDATE
TO authenticated
USING (
EXISTS (
SELECT 1 FROM public.experiment_phase_data epd
JOIN public.experiment_phase_drafts epdr ON epd.phase_draft_id = epdr.id
WHERE epd.id = phase_data_id AND epdr.user_id = auth.uid()
)
)
WITH CHECK (
EXISTS (
SELECT 1 FROM public.experiment_phase_data epd
JOIN public.experiment_phase_drafts epdr ON epd.phase_draft_id = epdr.id
WHERE epd.id = phase_data_id AND epdr.user_id = auth.uid()
)
);
CREATE POLICY "pecan_diameter_measurements_delete_policy" ON public.pecan_diameter_measurements
FOR DELETE
TO authenticated
USING (
EXISTS (
SELECT 1 FROM public.experiment_phase_data epd
JOIN public.experiment_phase_drafts epdr ON epd.phase_draft_id = epdr.id
WHERE epd.id = phase_data_id AND epdr.user_id = auth.uid() AND epdr.status = 'draft'
)
);
-- =============================================
-- 10. COMMENTS FOR DOCUMENTATION
-- =============================================
COMMENT ON TABLE public.roles IS 'System roles for user access control';
COMMENT ON TABLE public.user_profiles IS 'Extended user profiles linked to auth.users';
COMMENT ON TABLE public.user_roles IS 'Many-to-many relationship between users and roles';
COMMENT ON TABLE public.experiment_phases IS 'Groups experiments into logical phases for better organization and navigation';
COMMENT ON TABLE public.experiments IS 'Stores experiment definitions for pecan processing with parameters and status tracking';
COMMENT ON TABLE public.experiment_repetitions IS 'Individual repetitions of experiment blueprints that can be scheduled and executed';
COMMENT ON TABLE public.experiment_phase_drafts IS 'Phase-specific draft records for experiment data entry with status tracking';
COMMENT ON TABLE public.experiment_phase_data IS 'Phase-specific measurement data for experiments';
COMMENT ON TABLE public.pecan_diameter_measurements IS 'Individual pecan diameter measurements (up to 10 per phase)';

View File

@@ -0,0 +1,341 @@
-- Add Conductor Availability and Experiment Phase Assignment Tables
-- This migration adds tables for conductor availability management and future experiment scheduling
-- =============================================
-- 1. CONDUCTOR AVAILABILITY TABLE
-- =============================================
-- Create conductor_availability table
CREATE TABLE IF NOT EXISTS public.conductor_availability (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES public.user_profiles(id) ON DELETE CASCADE,
available_from TIMESTAMP WITH TIME ZONE NOT NULL,
available_to TIMESTAMP WITH TIME ZONE NOT NULL,
notes TEXT, -- Optional notes about the availability
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'cancelled')),
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),
-- Ensure available_to is after available_from
CONSTRAINT valid_time_range CHECK (available_to > available_from),
-- Ensure availability is in the future (can be modified if needed for past records)
CONSTRAINT future_availability CHECK (available_from >= NOW() - INTERVAL '1 day')
);
-- =============================================
-- 2. EXPERIMENT PHASE ASSIGNMENTS TABLE (Future Scheduling)
-- =============================================
-- Create experiment_phase_assignments table for scheduling conductors to experiment phases
CREATE TABLE IF NOT EXISTS public.experiment_phase_assignments (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
experiment_id UUID NOT NULL REFERENCES public.experiments(id) ON DELETE CASCADE,
repetition_id UUID NOT NULL REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE,
conductor_id UUID NOT NULL REFERENCES public.user_profiles(id) ON DELETE CASCADE,
phase_name TEXT NOT NULL CHECK (phase_name IN ('pre-soaking', 'air-drying', 'cracking', 'shelling')),
scheduled_start_time TIMESTAMP WITH TIME ZONE NOT NULL,
scheduled_end_time TIMESTAMP WITH TIME ZONE NOT NULL,
status TEXT NOT NULL DEFAULT 'scheduled' CHECK (status IN ('scheduled', 'in-progress', 'completed', 'cancelled')),
notes TEXT, -- Optional notes about the assignment
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),
-- Ensure scheduled_end_time is after scheduled_start_time
CONSTRAINT valid_scheduled_time_range CHECK (scheduled_end_time > scheduled_start_time),
-- Ensure unique assignment per conductor per phase per repetition
CONSTRAINT unique_conductor_phase_assignment UNIQUE (repetition_id, conductor_id, phase_name)
);
-- =============================================
-- 3. INDEXES FOR PERFORMANCE
-- =============================================
-- Conductor availability indexes
CREATE INDEX IF NOT EXISTS idx_conductor_availability_user_id ON public.conductor_availability(user_id);
CREATE INDEX IF NOT EXISTS idx_conductor_availability_available_from ON public.conductor_availability(available_from);
CREATE INDEX IF NOT EXISTS idx_conductor_availability_available_to ON public.conductor_availability(available_to);
CREATE INDEX IF NOT EXISTS idx_conductor_availability_status ON public.conductor_availability(status);
CREATE INDEX IF NOT EXISTS idx_conductor_availability_created_by ON public.conductor_availability(created_by);
CREATE INDEX IF NOT EXISTS idx_conductor_availability_time_range ON public.conductor_availability(available_from, available_to);
-- Experiment phase assignments indexes
CREATE INDEX IF NOT EXISTS idx_experiment_phase_assignments_experiment_id ON public.experiment_phase_assignments(experiment_id);
CREATE INDEX IF NOT EXISTS idx_experiment_phase_assignments_repetition_id ON public.experiment_phase_assignments(repetition_id);
CREATE INDEX IF NOT EXISTS idx_experiment_phase_assignments_conductor_id ON public.experiment_phase_assignments(conductor_id);
CREATE INDEX IF NOT EXISTS idx_experiment_phase_assignments_phase_name ON public.experiment_phase_assignments(phase_name);
CREATE INDEX IF NOT EXISTS idx_experiment_phase_assignments_status ON public.experiment_phase_assignments(status);
CREATE INDEX IF NOT EXISTS idx_experiment_phase_assignments_scheduled_start ON public.experiment_phase_assignments(scheduled_start_time);
CREATE INDEX IF NOT EXISTS idx_experiment_phase_assignments_created_by ON public.experiment_phase_assignments(created_by);
-- =============================================
-- 4. FUNCTIONS FOR OVERLAP PREVENTION
-- =============================================
-- Function to check for overlapping availabilities
CREATE OR REPLACE FUNCTION public.check_availability_overlap()
RETURNS TRIGGER AS $$
DECLARE
overlap_count INTEGER;
BEGIN
-- Check for overlapping availabilities for the same user
SELECT COUNT(*) INTO overlap_count
FROM public.conductor_availability
WHERE user_id = NEW.user_id
AND id != COALESCE(NEW.id, '00000000-0000-0000-0000-000000000000'::UUID)
AND status = 'active'
AND (
-- New availability starts during an existing one
(NEW.available_from >= available_from AND NEW.available_from < available_to) OR
-- New availability ends during an existing one
(NEW.available_to > available_from AND NEW.available_to <= available_to) OR
-- New availability completely contains an existing one
(NEW.available_from <= available_from AND NEW.available_to >= available_to) OR
-- Existing availability completely contains the new one
(available_from <= NEW.available_from AND available_to >= NEW.available_to)
);
IF overlap_count > 0 THEN
RAISE EXCEPTION 'Availability overlaps with existing availability for user %. Please adjust the time range or cancel the conflicting availability.', NEW.user_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Function to automatically adjust overlapping availabilities (alternative approach)
CREATE OR REPLACE FUNCTION public.adjust_overlapping_availability()
RETURNS TRIGGER AS $$
DECLARE
overlapping_record RECORD;
BEGIN
-- Find overlapping availabilities and adjust them
FOR overlapping_record IN
SELECT id, available_from, available_to
FROM public.conductor_availability
WHERE user_id = NEW.user_id
AND id != COALESCE(NEW.id, '00000000-0000-0000-0000-000000000000'::UUID)
AND status = 'active'
AND (
(NEW.available_from >= available_from AND NEW.available_from < available_to) OR
(NEW.available_to > available_from AND NEW.available_to <= available_to) OR
(NEW.available_from <= available_from AND NEW.available_to >= available_to) OR
(available_from <= NEW.available_from AND available_to >= NEW.available_to)
)
LOOP
-- Adjust the overlapping record to end where the new one starts
IF overlapping_record.available_from < NEW.available_from AND overlapping_record.available_to > NEW.available_from THEN
UPDATE public.conductor_availability
SET available_to = NEW.available_from,
updated_at = NOW()
WHERE id = overlapping_record.id;
END IF;
-- If the overlapping record starts after the new one, adjust it to start where the new one ends
IF overlapping_record.available_from < NEW.available_to AND overlapping_record.available_to > NEW.available_to THEN
UPDATE public.conductor_availability
SET available_from = NEW.available_to,
updated_at = NOW()
WHERE id = overlapping_record.id;
END IF;
-- If the overlapping record is completely contained within the new one, cancel it
IF overlapping_record.available_from >= NEW.available_from AND overlapping_record.available_to <= NEW.available_to THEN
UPDATE public.conductor_availability
SET status = 'cancelled',
updated_at = NOW()
WHERE id = overlapping_record.id;
END IF;
END LOOP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- =============================================
-- 5. TRIGGERS
-- =============================================
-- Create trigger for updated_at on conductor_availability
CREATE TRIGGER set_updated_at_conductor_availability
BEFORE UPDATE ON public.conductor_availability
FOR EACH ROW
EXECUTE FUNCTION public.handle_updated_at();
-- Create trigger for updated_at on experiment_phase_assignments
CREATE TRIGGER set_updated_at_experiment_phase_assignments
BEFORE UPDATE ON public.experiment_phase_assignments
FOR EACH ROW
EXECUTE FUNCTION public.handle_updated_at();
-- Create trigger to prevent overlapping availabilities (strict approach)
CREATE TRIGGER trigger_check_availability_overlap
BEFORE INSERT OR UPDATE ON public.conductor_availability
FOR EACH ROW
EXECUTE FUNCTION public.check_availability_overlap();
-- Alternative trigger to automatically adjust overlapping availabilities (uncomment if preferred)
-- CREATE TRIGGER trigger_adjust_overlapping_availability
-- BEFORE INSERT OR UPDATE ON public.conductor_availability
-- FOR EACH ROW
-- EXECUTE FUNCTION public.adjust_overlapping_availability();
-- =============================================
-- 6. HELPER FUNCTIONS
-- =============================================
-- Function to get available conductors for a specific time range
CREATE OR REPLACE FUNCTION public.get_available_conductors(
start_time TIMESTAMP WITH TIME ZONE,
end_time TIMESTAMP WITH TIME ZONE
)
RETURNS TABLE (
user_id UUID,
email TEXT,
available_from TIMESTAMP WITH TIME ZONE,
available_to TIMESTAMP WITH TIME ZONE
) AS $$
BEGIN
RETURN QUERY
SELECT
ca.user_id,
up.email,
ca.available_from,
ca.available_to
FROM public.conductor_availability ca
JOIN public.user_profiles up ON ca.user_id = up.id
JOIN public.user_roles ur ON up.id = ur.user_id
JOIN public.roles r ON ur.role_id = r.id
WHERE ca.status = 'active'
AND r.name = 'conductor'
AND ca.available_from <= start_time
AND ca.available_to >= end_time
ORDER BY up.email;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function to check if a conductor is available for a specific time range
CREATE OR REPLACE FUNCTION public.is_conductor_available(
conductor_user_id UUID,
start_time TIMESTAMP WITH TIME ZONE,
end_time TIMESTAMP WITH TIME ZONE
)
RETURNS BOOLEAN AS $$
DECLARE
availability_count INTEGER;
BEGIN
SELECT COUNT(*) INTO availability_count
FROM public.conductor_availability
WHERE user_id = conductor_user_id
AND status = 'active'
AND available_from <= start_time
AND available_to >= end_time;
RETURN availability_count > 0;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- =============================================
-- 7. ROW LEVEL SECURITY (RLS)
-- =============================================
-- Enable RLS on new tables
ALTER TABLE public.conductor_availability ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.experiment_phase_assignments ENABLE ROW LEVEL SECURITY;
-- Conductor availability policies
CREATE POLICY "conductor_availability_select_policy" ON public.conductor_availability
FOR SELECT
TO authenticated
USING (
-- Users can view their own availability, admins can view all
user_id = auth.uid() OR public.is_admin()
);
CREATE POLICY "conductor_availability_insert_policy" ON public.conductor_availability
FOR INSERT
TO authenticated
WITH CHECK (
-- Users can create their own availability, admins can create for anyone
(user_id = auth.uid() AND created_by = auth.uid()) OR public.is_admin()
);
CREATE POLICY "conductor_availability_update_policy" ON public.conductor_availability
FOR UPDATE
TO authenticated
USING (
-- Users can update their own availability, admins can update any
user_id = auth.uid() OR public.is_admin()
)
WITH CHECK (
-- Users can update their own availability, admins can update any
user_id = auth.uid() OR public.is_admin()
);
CREATE POLICY "conductor_availability_delete_policy" ON public.conductor_availability
FOR DELETE
TO authenticated
USING (
-- Users can delete their own availability, admins can delete any
user_id = auth.uid() OR public.is_admin()
);
-- Experiment phase assignments policies
CREATE POLICY "experiment_phase_assignments_select_policy" ON public.experiment_phase_assignments
FOR SELECT
TO authenticated
USING (
-- Conductors can view their own assignments, admins can view all
conductor_id = auth.uid() OR public.is_admin()
);
CREATE POLICY "experiment_phase_assignments_insert_policy" ON public.experiment_phase_assignments
FOR INSERT
TO authenticated
WITH CHECK (
-- Only admins and conductors can create assignments
public.can_manage_experiments()
);
CREATE POLICY "experiment_phase_assignments_update_policy" ON public.experiment_phase_assignments
FOR UPDATE
TO authenticated
USING (
-- Conductors can update their own assignments, admins can update any
conductor_id = auth.uid() OR public.is_admin()
)
WITH CHECK (
-- Conductors can update their own assignments, admins can update any
conductor_id = auth.uid() OR public.is_admin()
);
CREATE POLICY "experiment_phase_assignments_delete_policy" ON public.experiment_phase_assignments
FOR DELETE
TO authenticated
USING (
-- Only admins can delete assignments
public.is_admin()
);
-- =============================================
-- 8. COMMENTS FOR DOCUMENTATION
-- =============================================
COMMENT ON TABLE public.conductor_availability IS 'Stores conductor availability windows for experiment scheduling';
COMMENT ON TABLE public.experiment_phase_assignments IS 'Assigns conductors to specific experiment repetition phases with scheduled times';
COMMENT ON COLUMN public.conductor_availability.available_from IS 'Start time of availability window';
COMMENT ON COLUMN public.conductor_availability.available_to IS 'End time of availability window';
COMMENT ON COLUMN public.conductor_availability.notes IS 'Optional notes about the availability period';
COMMENT ON COLUMN public.conductor_availability.status IS 'Status of the availability (active or cancelled)';
COMMENT ON COLUMN public.experiment_phase_assignments.phase_name IS 'Experiment phase being assigned (pre-soaking, air-drying, cracking, shelling)';
COMMENT ON COLUMN public.experiment_phase_assignments.scheduled_start_time IS 'Planned start time for the phase';
COMMENT ON COLUMN public.experiment_phase_assignments.scheduled_end_time IS 'Planned end time for the phase';
COMMENT ON COLUMN public.experiment_phase_assignments.status IS 'Current status of the assignment';
COMMENT ON COLUMN public.experiment_phase_assignments.notes IS 'Optional notes about the assignment';

View File

@@ -0,0 +1,474 @@
-- Migration: Restructure experiment phases and add machine types
-- This migration restructures the experiment system to support separate phase tables
-- and machine-specific parameters
-- =============================================
-- 1. MACHINE TYPES
-- =============================================
-- Create machine types table
CREATE TABLE IF NOT EXISTS 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)
);
-- Machine types will be inserted in seed.sql after user profiles are created
-- =============================================
-- 2. UPDATE EXPERIMENT_PHASES TABLE
-- =============================================
-- Add phase selection columns to experiment_phases
ALTER TABLE public.experiment_phases
ADD COLUMN IF NOT EXISTS has_soaking BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS has_airdrying BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS has_cracking BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS has_shelling BOOLEAN NOT NULL DEFAULT false;
-- Add constraint to ensure at least one phase is selected
ALTER TABLE public.experiment_phases
ADD CONSTRAINT check_at_least_one_phase
CHECK (has_soaking = true OR has_airdrying = true OR has_cracking = true OR has_shelling = true);
-- =============================================
-- 3. CREATE PHASE-SPECIFIC TABLES
-- =============================================
-- Create soaking table
CREATE TABLE IF NOT EXISTS 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),
-- Ensure only one soaking per experiment or repetition
CONSTRAINT unique_soaking_per_experiment UNIQUE (experiment_id),
CONSTRAINT unique_soaking_per_repetition UNIQUE (repetition_id)
);
-- Create airdrying table
CREATE TABLE IF NOT EXISTS 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),
-- Ensure only one airdrying per experiment or repetition
CONSTRAINT unique_airdrying_per_experiment UNIQUE (experiment_id),
CONSTRAINT unique_airdrying_per_repetition UNIQUE (repetition_id)
);
-- Create cracking table
CREATE TABLE IF NOT EXISTS 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),
-- Ensure only one cracking per experiment or repetition
CONSTRAINT unique_cracking_per_experiment UNIQUE (experiment_id),
CONSTRAINT unique_cracking_per_repetition UNIQUE (repetition_id)
);
-- Create shelling table
CREATE TABLE IF NOT EXISTS 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),
-- Ensure only one shelling per experiment or repetition
CONSTRAINT unique_shelling_per_experiment UNIQUE (experiment_id),
CONSTRAINT unique_shelling_per_repetition UNIQUE (repetition_id)
);
-- =============================================
-- 4. MACHINE-SPECIFIC PARAMETER TABLES
-- =============================================
-- Create JC Cracker parameters table
CREATE TABLE IF NOT EXISTS 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(),
-- Ensure only one parameter set per cracking
CONSTRAINT unique_jc_params_per_cracking UNIQUE (cracking_id)
);
-- Create Meyer Cracker parameters table
CREATE TABLE IF NOT EXISTS 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(),
-- Ensure only one parameter set per cracking
CONSTRAINT unique_meyer_params_per_cracking UNIQUE (cracking_id)
);
-- =============================================
-- 5. UPDATE EXPERIMENTS TABLE
-- =============================================
-- Add weight_per_repetition column
ALTER TABLE public.experiments
ADD COLUMN IF NOT EXISTS weight_per_repetition_lbs DOUBLE PRECISION NOT NULL DEFAULT 0 CHECK (weight_per_repetition_lbs > 0);
-- Remove phase-specific parameters (these will be moved to phase tables)
-- Note: We'll keep these columns for now to avoid data loss, but they should be deprecated
-- Make old columns nullable to support new schema experiments
ALTER TABLE public.experiments
ALTER COLUMN soaking_duration_hr DROP NOT NULL,
ALTER COLUMN air_drying_time_min DROP NOT NULL,
ALTER COLUMN plate_contact_frequency_hz DROP NOT NULL,
ALTER COLUMN throughput_rate_pecans_sec DROP NOT NULL,
ALTER COLUMN crush_amount_in DROP NOT NULL,
ALTER COLUMN entry_exit_height_diff_in DROP NOT NULL;
-- =============================================
-- 6. ADD FOREIGN KEY CONSTRAINTS
-- =============================================
-- Add foreign key constraints to experiments table for phase associations
ALTER TABLE public.experiments
ADD COLUMN IF NOT EXISTS soaking_id UUID REFERENCES public.soaking(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS airdrying_id UUID REFERENCES public.airdrying(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS cracking_id UUID REFERENCES public.cracking(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS shelling_id UUID REFERENCES public.shelling(id) ON DELETE SET NULL;
-- =============================================
-- 7. CREATE INDEXES FOR PERFORMANCE
-- =============================================
-- Create indexes for better query performance
CREATE INDEX IF NOT EXISTS idx_soaking_experiment_id ON public.soaking(experiment_id);
CREATE INDEX IF NOT EXISTS idx_soaking_repetition_id ON public.soaking(repetition_id);
CREATE INDEX IF NOT EXISTS idx_airdrying_experiment_id ON public.airdrying(experiment_id);
CREATE INDEX IF NOT EXISTS idx_airdrying_repetition_id ON public.airdrying(repetition_id);
CREATE INDEX IF NOT EXISTS idx_cracking_experiment_id ON public.cracking(experiment_id);
CREATE INDEX IF NOT EXISTS idx_cracking_repetition_id ON public.cracking(repetition_id);
CREATE INDEX IF NOT EXISTS idx_cracking_machine_type_id ON public.cracking(machine_type_id);
CREATE INDEX IF NOT EXISTS idx_shelling_experiment_id ON public.shelling(experiment_id);
CREATE INDEX IF NOT EXISTS idx_shelling_repetition_id ON public.shelling(repetition_id);
-- =============================================
-- 8. CREATE TRIGGERS FOR AUTOMATIC TIMESTAMP CALCULATIONS
-- =============================================
-- Function to calculate scheduled end time for soaking
CREATE OR REPLACE FUNCTION calculate_soaking_scheduled_end_time()
RETURNS TRIGGER AS $$
BEGIN
NEW.scheduled_end_time = NEW.scheduled_start_time + (NEW.soaking_duration_minutes || ' minutes')::INTERVAL;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Trigger for soaking scheduled end time
DROP TRIGGER IF EXISTS trigger_calculate_soaking_scheduled_end_time ON public.soaking;
CREATE TRIGGER trigger_calculate_soaking_scheduled_end_time
BEFORE INSERT OR UPDATE ON public.soaking
FOR EACH ROW
EXECUTE FUNCTION calculate_soaking_scheduled_end_time();
-- Function to calculate scheduled end time for airdrying
CREATE OR REPLACE FUNCTION calculate_airdrying_scheduled_end_time()
RETURNS TRIGGER AS $$
BEGIN
NEW.scheduled_end_time = NEW.scheduled_start_time + (NEW.duration_minutes || ' minutes')::INTERVAL;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Trigger for airdrying scheduled end time
DROP TRIGGER IF EXISTS trigger_calculate_airdrying_scheduled_end_time ON public.airdrying;
CREATE TRIGGER trigger_calculate_airdrying_scheduled_end_time
BEFORE INSERT OR UPDATE ON public.airdrying
FOR EACH ROW
EXECUTE FUNCTION calculate_airdrying_scheduled_end_time();
-- Function to set airdrying scheduled start time based on soaking end time
CREATE OR REPLACE FUNCTION set_airdrying_scheduled_start_time()
RETURNS TRIGGER AS $$
BEGIN
-- If this is a new airdrying record and no scheduled_start_time is provided,
-- try to get it from the associated soaking's scheduled_end_time
IF NEW.scheduled_start_time IS NULL THEN
SELECT s.scheduled_end_time INTO NEW.scheduled_start_time
FROM public.soaking s
WHERE s.experiment_id = NEW.experiment_id
LIMIT 1;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Trigger for airdrying scheduled start time
DROP TRIGGER IF EXISTS trigger_set_airdrying_scheduled_start_time ON public.airdrying;
CREATE TRIGGER trigger_set_airdrying_scheduled_start_time
BEFORE INSERT ON public.airdrying
FOR EACH ROW
EXECUTE FUNCTION set_airdrying_scheduled_start_time();
-- Function to set cracking scheduled start time based on airdrying end time
CREATE OR REPLACE FUNCTION set_cracking_scheduled_start_time()
RETURNS TRIGGER AS $$
BEGIN
-- If this is a new cracking record and no scheduled_start_time is provided,
-- try to get it from the associated airdrying's scheduled_end_time
IF NEW.scheduled_start_time IS NULL THEN
SELECT a.scheduled_end_time INTO NEW.scheduled_start_time
FROM public.airdrying a
WHERE a.experiment_id = NEW.experiment_id
LIMIT 1;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Trigger for cracking scheduled start time
DROP TRIGGER IF EXISTS trigger_set_cracking_scheduled_start_time ON public.cracking;
CREATE TRIGGER trigger_set_cracking_scheduled_start_time
BEFORE INSERT ON public.cracking
FOR EACH ROW
EXECUTE FUNCTION set_cracking_scheduled_start_time();
-- =============================================
-- 9. CREATE VIEWS FOR EASIER QUERYING
-- =============================================
-- View for experiments with all phase information
CREATE OR REPLACE VIEW public.experiments_with_phases AS
SELECT
e.id,
e.experiment_number,
e.reps_required,
e.weight_per_repetition_lbs,
e.results_status,
e.completion_status,
e.phase_id,
e.soaking_id,
e.airdrying_id,
e.cracking_id,
e.shelling_id,
e.created_at,
e.updated_at,
e.created_by,
ep.name as phase_name,
ep.description as phase_description,
ep.has_soaking,
ep.has_airdrying,
ep.has_cracking,
ep.has_shelling,
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.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.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.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;
-- View for repetitions with phase information
CREATE OR REPLACE 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,
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.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;
-- =============================================
-- 10. GRANT PERMISSIONS
-- =============================================
-- Grant permissions for new tables
GRANT ALL ON public.machine_types TO authenticated;
GRANT ALL ON public.soaking TO authenticated;
GRANT ALL ON public.airdrying TO authenticated;
GRANT ALL ON public.cracking TO authenticated;
GRANT ALL ON public.shelling TO authenticated;
GRANT ALL ON public.jc_cracker_parameters TO authenticated;
GRANT ALL ON public.meyer_cracker_parameters TO authenticated;
-- Grant permissions for views
GRANT SELECT ON public.experiments_with_phases TO authenticated;
GRANT SELECT ON public.repetitions_with_phases TO authenticated;
-- =============================================
-- 11. UPDATE RLS POLICIES
-- =============================================
-- Enable RLS on new tables
ALTER TABLE public.machine_types ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.soaking ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.airdrying ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.cracking ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.shelling ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.jc_cracker_parameters ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.meyer_cracker_parameters ENABLE ROW LEVEL SECURITY;
-- Create RLS policies for machine_types (read-only for all authenticated users)
CREATE POLICY "Machine types are viewable by authenticated users" ON public.machine_types
FOR SELECT USING (auth.role() = 'authenticated');
-- Create RLS policies for phase tables (similar to experiments)
CREATE POLICY "Soaking data is viewable by authenticated users" ON public.soaking
FOR SELECT USING (auth.role() = 'authenticated');
CREATE POLICY "Soaking data is insertable by authenticated users" ON public.soaking
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
CREATE POLICY "Soaking data is updatable by authenticated users" ON public.soaking
FOR UPDATE USING (auth.role() = 'authenticated');
CREATE POLICY "Soaking data is deletable by authenticated users" ON public.soaking
FOR DELETE USING (auth.role() = 'authenticated');
-- Similar policies for other phase tables
CREATE POLICY "Airdrying data is viewable by authenticated users" ON public.airdrying
FOR SELECT USING (auth.role() = 'authenticated');
CREATE POLICY "Airdrying data is insertable by authenticated users" ON public.airdrying
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
CREATE POLICY "Airdrying data is updatable by authenticated users" ON public.airdrying
FOR UPDATE USING (auth.role() = 'authenticated');
CREATE POLICY "Airdrying data is deletable by authenticated users" ON public.airdrying
FOR DELETE USING (auth.role() = 'authenticated');
CREATE POLICY "Cracking data is viewable by authenticated users" ON public.cracking
FOR SELECT USING (auth.role() = 'authenticated');
CREATE POLICY "Cracking data is insertable by authenticated users" ON public.cracking
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
CREATE POLICY "Cracking data is updatable by authenticated users" ON public.cracking
FOR UPDATE USING (auth.role() = 'authenticated');
CREATE POLICY "Cracking data is deletable by authenticated users" ON public.cracking
FOR DELETE USING (auth.role() = 'authenticated');
CREATE POLICY "Shelling data is viewable by authenticated users" ON public.shelling
FOR SELECT USING (auth.role() = 'authenticated');
CREATE POLICY "Shelling data is insertable by authenticated users" ON public.shelling
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
CREATE POLICY "Shelling data is updatable by authenticated users" ON public.shelling
FOR UPDATE USING (auth.role() = 'authenticated');
CREATE POLICY "Shelling data is deletable by authenticated users" ON public.shelling
FOR DELETE USING (auth.role() = 'authenticated');
-- RLS policies for machine parameter tables
CREATE POLICY "JC Cracker parameters are viewable by authenticated users" ON public.jc_cracker_parameters
FOR SELECT USING (auth.role() = 'authenticated');
CREATE POLICY "JC Cracker parameters are insertable by authenticated users" ON public.jc_cracker_parameters
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
CREATE POLICY "JC Cracker parameters are updatable by authenticated users" ON public.jc_cracker_parameters
FOR UPDATE USING (auth.role() = 'authenticated');
CREATE POLICY "JC Cracker parameters are deletable by authenticated users" ON public.jc_cracker_parameters
FOR DELETE USING (auth.role() = 'authenticated');
CREATE POLICY "Meyer Cracker parameters are viewable by authenticated users" ON public.meyer_cracker_parameters
FOR SELECT USING (auth.role() = 'authenticated');
CREATE POLICY "Meyer Cracker parameters are insertable by authenticated users" ON public.meyer_cracker_parameters
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
CREATE POLICY "Meyer Cracker parameters are updatable by authenticated users" ON public.meyer_cracker_parameters
FOR UPDATE USING (auth.role() = 'authenticated');
CREATE POLICY "Meyer Cracker parameters are deletable by authenticated users" ON public.meyer_cracker_parameters
FOR DELETE USING (auth.role() = 'authenticated');

View File

@@ -502,80 +502,45 @@ AND r.name = 'data recorder'
;
-- =============================================
-- 4. CREATE EXPERIMENT PHASES
-- 4. CREATE MACHINE TYPES
-- =============================================
-- Insert default machine types
INSERT INTO public.machine_types (name, description, created_by) VALUES
('JC Cracker', 'JC Cracker machine with plate contact frequency and throughput parameters', (SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
('Meyer Cracker', 'Meyer Cracker machine with motor speed and jig displacement parameters', (SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com'))
ON CONFLICT (name) DO NOTHING;
-- =============================================
-- 5. CREATE EXPERIMENT PHASES
-- =============================================
-- Create "Phase 2 of JC Experiments" phase
INSERT INTO public.experiment_phases (name, description, created_by)
INSERT INTO public.experiment_phases (name, description, has_soaking, has_airdrying, has_cracking, has_shelling, cracking_machine_type_id, created_by)
SELECT
'Phase 2 of JC Experiments',
'Second phase of JC Cracker experiments for pecan processing optimization',
true,
true,
true,
false,
(SELECT id FROM public.machine_types WHERE name = 'JC Cracker'),
up.id
FROM public.user_profiles up
WHERE up.email = 's.alireza.v@gmail.com'
;
-- =============================================
-- 5. INSERT EXPERIMENTS (First 10 as example)
-- =============================================
INSERT INTO public.experiments (
experiment_number,
reps_required,
soaking_duration_hr,
air_drying_time_min,
plate_contact_frequency_hz,
throughput_rate_pecans_sec,
crush_amount_in,
entry_exit_height_diff_in,
results_status,
completion_status,
phase_id,
created_by
) VALUES
(1, 3, 2.0, 30, 15.0, 2.5, 0.125, 0.5, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(2, 3, 2.5, 45, 12.0, 2.0, 0.150, 0.75, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(3, 3, 3.0, 60, 10.0, 1.8, 0.175, 1.0, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(4, 3, 1.5, 20, 18.0, 3.0, 0.100, 0.25, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(5, 3, 2.0, 30, 15.0, 2.5, 0.125, 0.5, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(6, 3, 2.5, 45, 12.0, 2.0, 0.150, 0.75, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(7, 3, 3.0, 60, 10.0, 1.8, 0.175, 1.0, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(8, 3, 1.5, 20, 18.0, 3.0, 0.100, 0.25, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(9, 3, 2.0, 30, 15.0, 2.5, 0.125, 0.5, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(10, 3, 2.5, 45, 12.0, 2.0, 0.150, 0.75, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com'))
;
-- =============================================
-- 6. CREATE SAMPLE EXPERIMENT REPETITIONS
-- =============================================
-- Create repetitions for first 5 experiments as examples
INSERT INTO public.experiment_repetitions (experiment_id, repetition_number, created_by)
-- Create "Post Workshop Meyer Experiments" phase
INSERT INTO public.experiment_phases (name, description, has_soaking, has_airdrying, has_cracking, has_shelling, cracking_machine_type_id, created_by)
SELECT
e.id,
rep_num,
e.created_by
FROM public.experiments e
CROSS JOIN generate_series(1, 3) AS rep_num
WHERE e.experiment_number <= 5
;
'Post Workshop Meyer Experiments',
'Post workshop Meyer Cracker experiments for pecan processing optimization',
true,
true,
true,
false,
(SELECT id FROM public.machine_types WHERE name = 'Meyer Cracker'),
up.id
FROM public.user_profiles up
WHERE up.email = 's.alireza.v@gmail.com'
;

View File

@@ -0,0 +1,372 @@
-- Phase 2 JC Experiments Seed Data
-- This file contains all Phase 2 JC experiments from phase_2_experimental_run_sheet.csv
-- Each experiment has 3 repetitions, and each row represents one repetition
-- Updated to use 1-based numbering per phase and composite primary key
-- =============================================
-- INSERT PHASE 2 JC EXPERIMENTS
-- =============================================
-- First, insert unique experiments (based on experiment_number)
INSERT INTO public.experiments (
experiment_number,
reps_required,
results_status,
completion_status,
phase_id,
created_by
) VALUES
-- Phase 2 JC Experiments 1-20 (1-based numbering)
(1, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(2, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(3, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(4, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(5, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(6, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(7, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(8, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(9, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(10, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(11, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(12, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(13, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(14, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(15, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(16, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(17, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(18, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(19, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(20, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com'))
ON CONFLICT (experiment_number, phase_id) DO NOTHING;
-- =============================================
-- CREATE SOAKING PHASE RECORDS FOR PHASE 2 JC EXPERIMENTS
-- =============================================
-- Create soaking records for Phase 2 JC experiments (1-20)
INSERT INTO public.soaking (
experiment_id,
experiment_phase_id,
scheduled_start_time,
soaking_duration_hours,
scheduled_end_time,
created_by
)
SELECT
e.experiment_number,
e.phase_id,
NOW() + (e.experiment_number) * INTERVAL '1 day',
CASE e.experiment_number
WHEN 1 THEN 34 -- hours
WHEN 2 THEN 24
WHEN 3 THEN 38
WHEN 4 THEN 11
WHEN 5 THEN 13
WHEN 6 THEN 30
WHEN 7 THEN 10
WHEN 8 THEN 15
WHEN 9 THEN 27
WHEN 10 THEN 32
WHEN 11 THEN 26
WHEN 12 THEN 24
WHEN 13 THEN 28
WHEN 14 THEN 21
WHEN 15 THEN 22
WHEN 16 THEN 16
WHEN 17 THEN 20
WHEN 18 THEN 34
WHEN 19 THEN 18
WHEN 20 THEN 11
END,
NOW() + (e.experiment_number) * INTERVAL '1 day' +
CASE e.experiment_number
WHEN 1 THEN 34 * INTERVAL '1 hour'
WHEN 2 THEN 24 * INTERVAL '1 hour'
WHEN 3 THEN 38 * INTERVAL '1 hour'
WHEN 4 THEN 11 * INTERVAL '1 hour'
WHEN 5 THEN 13 * INTERVAL '1 hour'
WHEN 6 THEN 30 * INTERVAL '1 hour'
WHEN 7 THEN 10 * INTERVAL '1 hour'
WHEN 8 THEN 15 * INTERVAL '1 hour'
WHEN 9 THEN 27 * INTERVAL '1 hour'
WHEN 10 THEN 32 * INTERVAL '1 hour'
WHEN 11 THEN 26 * INTERVAL '1 hour'
WHEN 12 THEN 24 * INTERVAL '1 hour'
WHEN 13 THEN 28 * INTERVAL '1 hour'
WHEN 14 THEN 21 * INTERVAL '1 hour'
WHEN 15 THEN 22 * INTERVAL '1 hour'
WHEN 16 THEN 16 * INTERVAL '1 hour'
WHEN 17 THEN 20 * INTERVAL '1 hour'
WHEN 18 THEN 34 * INTERVAL '1 hour'
WHEN 19 THEN 18 * INTERVAL '1 hour'
WHEN 20 THEN 11 * INTERVAL '1 hour'
END,
e.created_by
FROM public.experiments e
WHERE e.experiment_number BETWEEN 1 AND 20
AND e.phase_id = (SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments')
ON CONFLICT DO NOTHING;
-- =============================================
-- CREATE AIRDRYING PHASE RECORDS FOR PHASE 2 JC EXPERIMENTS
-- =============================================
-- Create airdrying records for Phase 2 JC experiments (1-20)
INSERT INTO public.airdrying (
experiment_id,
experiment_phase_id,
scheduled_start_time,
duration_minutes,
scheduled_end_time,
created_by
)
SELECT
e.experiment_number,
e.phase_id,
NOW() + (e.experiment_number) * INTERVAL '1 day' + INTERVAL '2 days', -- Start 2 days after soaking
CASE e.experiment_number
WHEN 1 THEN 19 -- 19 minutes
WHEN 2 THEN 27 -- 27 minutes
WHEN 3 THEN 10 -- 10 minutes
WHEN 4 THEN 36 -- 36 minutes
WHEN 5 THEN 41 -- 41 minutes
WHEN 6 THEN 33 -- 33 minutes
WHEN 7 THEN 22 -- 22 minutes
WHEN 8 THEN 30 -- 30 minutes
WHEN 9 THEN 12 -- 12 minutes
WHEN 10 THEN 26 -- 26 minutes
WHEN 11 THEN 60 -- 60 minutes
WHEN 12 THEN 59 -- 59 minutes
WHEN 13 THEN 59 -- 59 minutes
WHEN 14 THEN 59 -- 59 minutes
WHEN 15 THEN 60 -- 60 minutes
WHEN 16 THEN 59 -- 59 minutes
WHEN 17 THEN 60 -- 60 minutes
WHEN 18 THEN 49 -- 49 minutes
WHEN 19 THEN 25 -- 25 minutes
WHEN 20 THEN 25 -- 25 minutes
END,
NOW() + (e.experiment_number) * INTERVAL '1 day' + INTERVAL '2 days' +
CASE e.experiment_number
WHEN 1 THEN 19 * INTERVAL '1 minute' -- 19 minutes
WHEN 2 THEN 27 * INTERVAL '1 minute' -- 27 minutes
WHEN 3 THEN 10 * INTERVAL '1 minute' -- 10 minutes
WHEN 4 THEN 36 * INTERVAL '1 minute' -- 36 minutes
WHEN 5 THEN 41 * INTERVAL '1 minute' -- 41 minutes
WHEN 6 THEN 33 * INTERVAL '1 minute' -- 33 minutes
WHEN 7 THEN 22 * INTERVAL '1 minute' -- 22 minutes
WHEN 8 THEN 30 * INTERVAL '1 minute' -- 30 minutes
WHEN 9 THEN 12 * INTERVAL '1 minute' -- 12 minutes
WHEN 10 THEN 26 * INTERVAL '1 minute' -- 26 minutes
WHEN 11 THEN 60 * INTERVAL '1 minute' -- 60 minutes
WHEN 12 THEN 59 * INTERVAL '1 minute' -- 59 minutes
WHEN 13 THEN 59 * INTERVAL '1 minute' -- 59 minutes
WHEN 14 THEN 59 * INTERVAL '1 minute' -- 59 minutes
WHEN 15 THEN 60 * INTERVAL '1 minute' -- 60 minutes
WHEN 16 THEN 59 * INTERVAL '1 minute' -- 59 minutes
WHEN 17 THEN 60 * INTERVAL '1 minute' -- 60 minutes
WHEN 18 THEN 49 * INTERVAL '1 minute' -- 49 minutes
WHEN 19 THEN 25 * INTERVAL '1 minute' -- 25 minutes
WHEN 20 THEN 25 * INTERVAL '1 minute' -- 25 minutes
END,
e.created_by
FROM public.experiments e
WHERE e.experiment_number BETWEEN 1 AND 20
AND e.phase_id = (SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments')
ON CONFLICT DO NOTHING;
-- =============================================
-- CREATE CRACKING PHASE RECORDS FOR PHASE 2 JC EXPERIMENTS
-- =============================================
-- Create cracking records for Phase 2 JC experiments (1-20)
INSERT INTO public.cracking (
experiment_id,
experiment_phase_id,
machine_type_id,
scheduled_start_time,
created_by
)
SELECT
e.experiment_number,
e.phase_id,
(SELECT id FROM public.machine_types WHERE name = 'JC Cracker'),
NOW() + (e.experiment_number) * INTERVAL '1 day' + INTERVAL '3 days', -- Start 3 days after soaking
e.created_by
FROM public.experiments e
WHERE e.experiment_number BETWEEN 1 AND 20
AND e.phase_id = (SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments')
ON CONFLICT DO NOTHING;
-- =============================================
-- CREATE JC CRACKER PARAMETERS FOR PHASE 2 JC EXPERIMENTS
-- =============================================
-- Create JC cracker parameters for Phase 2 JC experiments (1-20)
INSERT INTO public.jc_cracker_parameters (
cracking_id,
plate_contact_frequency_hz,
throughput_rate_pecans_sec,
crush_amount_in,
entry_exit_height_diff_in
)
SELECT
c.id,
CASE e.experiment_number
WHEN 1 THEN 53.0
WHEN 2 THEN 34.0
WHEN 3 THEN 60.0
WHEN 4 THEN 42.0
WHEN 5 THEN 41.0
WHEN 6 THEN 30.0
WHEN 7 THEN 37.0
WHEN 8 THEN 35.0
WHEN 9 THEN 55.0
WHEN 10 THEN 47.0
WHEN 11 THEN 44.0
WHEN 12 THEN 42.0
WHEN 13 THEN 37.0
WHEN 14 THEN 41.0
WHEN 15 THEN 45.0
WHEN 16 THEN 30.0
WHEN 17 THEN 41.0
WHEN 18 THEN 34.0
WHEN 19 THEN 38.0
WHEN 20 THEN 56.0
END,
CASE e.experiment_number
WHEN 1 THEN 28.0
WHEN 2 THEN 29.0
WHEN 3 THEN 28.0
WHEN 4 THEN 13.0
WHEN 5 THEN 38.0
WHEN 6 THEN 36.0
WHEN 7 THEN 30.0
WHEN 8 THEN 32.0
WHEN 9 THEN 24.0
WHEN 10 THEN 26.0
WHEN 11 THEN 12.0
WHEN 12 THEN 25.0
WHEN 13 THEN 23.0
WHEN 14 THEN 21.0
WHEN 15 THEN 17.0
WHEN 16 THEN 24.0
WHEN 17 THEN 14.0
WHEN 18 THEN 29.0
WHEN 19 THEN 35.0
WHEN 20 THEN 34.0
END,
CASE e.experiment_number
WHEN 1 THEN 0.05
WHEN 2 THEN 0.03
WHEN 3 THEN 0.06
WHEN 4 THEN 0.07
WHEN 5 THEN 0.05
WHEN 6 THEN 0.05
WHEN 7 THEN 0.06
WHEN 8 THEN 0.05
WHEN 9 THEN 0.04
WHEN 10 THEN 0.07
WHEN 11 THEN 0.08
WHEN 12 THEN 0.07
WHEN 13 THEN 0.06
WHEN 14 THEN 0.06
WHEN 15 THEN 0.07
WHEN 16 THEN 0.07
WHEN 17 THEN 0.07
WHEN 18 THEN 0.07
WHEN 19 THEN 0.07
WHEN 20 THEN 0.06
END,
CASE e.experiment_number
WHEN 1 THEN -0.09
WHEN 2 THEN 0.01
WHEN 3 THEN -0.10
WHEN 4 THEN -0.07
WHEN 5 THEN 0.03
WHEN 6 THEN -0.04
WHEN 7 THEN 0.02
WHEN 8 THEN -0.07
WHEN 9 THEN 0.04
WHEN 10 THEN 0.03
WHEN 11 THEN -0.10
WHEN 12 THEN -0.05
WHEN 13 THEN -0.08
WHEN 14 THEN -0.09
WHEN 15 THEN -0.08
WHEN 16 THEN 0.02
WHEN 17 THEN 0.04
WHEN 18 THEN -0.09
WHEN 19 THEN -0.08
WHEN 20 THEN -0.09
END
FROM public.experiments e
JOIN public.cracking c ON c.experiment_id = e.experiment_number AND c.experiment_phase_id = e.phase_id
WHERE e.experiment_number BETWEEN 1 AND 20
AND e.phase_id = (SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments')
ON CONFLICT DO NOTHING;
-- =============================================
-- CREATE EXPERIMENT REPETITIONS FOR PHASE 2 JC EXPERIMENTS
-- =============================================
-- Create experiment repetitions for Phase 2 JC experiments
-- Each experiment needs 3 repetitions
INSERT INTO public.experiment_repetitions (
experiment_id,
experiment_phase_id,
repetition_number,
status,
created_by
)
SELECT
e.experiment_number,
e.phase_id,
rep_num,
'pending',
e.created_by
FROM public.experiments e
CROSS JOIN generate_series(1, 3) AS rep_num
WHERE e.experiment_number BETWEEN 1 AND 20
AND e.phase_id = (SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments')
ON CONFLICT DO NOTHING;

View File

@@ -0,0 +1,372 @@
-- Phase 2 JC Experiments Seed Data
-- This file contains all Phase 2 JC experiments from phase_2_experimental_run_sheet.csv
-- Each experiment has 3 repetitions, and each row represents one repetition
-- Updated to use 1-based numbering per phase and composite primary key
-- =============================================
-- INSERT PHASE 2 JC EXPERIMENTS
-- =============================================
-- First, insert unique experiments (based on experiment_number)
INSERT INTO public.experiments (
experiment_number,
reps_required,
results_status,
completion_status,
phase_id,
created_by
) VALUES
-- Phase 2 JC Experiments 1-20 (1-based numbering)
(1, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(2, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(3, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(4, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(5, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(6, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(7, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(8, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(9, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(10, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(11, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(12, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(13, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(14, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(15, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(16, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(17, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(18, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(19, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(20, 3, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com'))
ON CONFLICT (experiment_number, phase_id) DO NOTHING;
-- =============================================
-- CREATE SOAKING PHASE RECORDS FOR PHASE 2 JC EXPERIMENTS
-- =============================================
-- Create soaking records for Phase 2 JC experiments (1-20)
INSERT INTO public.soaking (
experiment_id,
experiment_phase_id,
scheduled_start_time,
soaking_duration_minutes,
scheduled_end_time,
created_by
)
SELECT
e.experiment_number,
e.phase_id,
NOW() + (e.experiment_number) * INTERVAL '1 day',
CASE e.experiment_number
WHEN 1 THEN 34 * 60 -- 34 hours = 2040 minutes
WHEN 2 THEN 24 * 60 -- 24 hours = 1440 minutes
WHEN 3 THEN 38 * 60 -- 38 hours = 2280 minutes
WHEN 4 THEN 11 * 60 -- 11 hours = 660 minutes
WHEN 5 THEN 13 * 60 -- 13 hours = 780 minutes
WHEN 6 THEN 30 * 60 -- 30 hours = 1800 minutes
WHEN 7 THEN 10 * 60 -- 10 hours = 600 minutes
WHEN 8 THEN 15 * 60 -- 15 hours = 900 minutes
WHEN 9 THEN 27 * 60 -- 27 hours = 1620 minutes
WHEN 10 THEN 32 * 60 -- 32 hours = 1920 minutes
WHEN 11 THEN 26 * 60 -- 26 hours = 1560 minutes
WHEN 12 THEN 24 * 60 -- 24 hours = 1440 minutes
WHEN 13 THEN 28 * 60 -- 28 hours = 1680 minutes
WHEN 14 THEN 21 * 60 -- 21 hours = 1260 minutes
WHEN 15 THEN 22 * 60 -- 22 hours = 1320 minutes
WHEN 16 THEN 16 * 60 -- 16 hours = 960 minutes
WHEN 17 THEN 20 * 60 -- 20 hours = 1200 minutes
WHEN 18 THEN 34 * 60 -- 34 hours = 2040 minutes
WHEN 19 THEN 18 * 60 -- 18 hours = 1080 minutes
WHEN 20 THEN 11 * 60 -- 11 hours = 660 minutes
END,
NOW() + (e.experiment_number) * INTERVAL '1 day' +
CASE e.experiment_number
WHEN 1 THEN 34 * 60 * INTERVAL '1 minute' -- 34 hours = 2040 minutes
WHEN 2 THEN 24 * 60 * INTERVAL '1 minute' -- 24 hours = 1440 minutes
WHEN 3 THEN 38 * 60 * INTERVAL '1 minute' -- 38 hours = 2280 minutes
WHEN 4 THEN 11 * 60 * INTERVAL '1 minute' -- 11 hours = 660 minutes
WHEN 5 THEN 13 * 60 * INTERVAL '1 minute' -- 13 hours = 780 minutes
WHEN 6 THEN 30 * 60 * INTERVAL '1 minute' -- 30 hours = 1800 minutes
WHEN 7 THEN 10 * 60 * INTERVAL '1 minute' -- 10 hours = 600 minutes
WHEN 8 THEN 15 * 60 * INTERVAL '1 minute' -- 15 hours = 900 minutes
WHEN 9 THEN 27 * 60 * INTERVAL '1 minute' -- 27 hours = 1620 minutes
WHEN 10 THEN 32 * 60 * INTERVAL '1 minute' -- 32 hours = 1920 minutes
WHEN 11 THEN 26 * 60 * INTERVAL '1 minute' -- 26 hours = 1560 minutes
WHEN 12 THEN 24 * 60 * INTERVAL '1 minute' -- 24 hours = 1440 minutes
WHEN 13 THEN 28 * 60 * INTERVAL '1 minute' -- 28 hours = 1680 minutes
WHEN 14 THEN 21 * 60 * INTERVAL '1 minute' -- 21 hours = 1260 minutes
WHEN 15 THEN 22 * 60 * INTERVAL '1 minute' -- 22 hours = 1320 minutes
WHEN 16 THEN 16 * 60 * INTERVAL '1 minute' -- 16 hours = 960 minutes
WHEN 17 THEN 20 * 60 * INTERVAL '1 minute' -- 20 hours = 1200 minutes
WHEN 18 THEN 34 * 60 * INTERVAL '1 minute' -- 34 hours = 2040 minutes
WHEN 19 THEN 18 * 60 * INTERVAL '1 minute' -- 18 hours = 1080 minutes
WHEN 20 THEN 11 * 60 * INTERVAL '1 minute' -- 11 hours = 660 minutes
END,
e.created_by
FROM public.experiments e
WHERE e.experiment_number BETWEEN 1 AND 20
AND e.phase_id = (SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments')
ON CONFLICT DO NOTHING;
-- =============================================
-- CREATE AIRDRYING PHASE RECORDS FOR PHASE 2 JC EXPERIMENTS
-- =============================================
-- Create airdrying records for Phase 2 JC experiments (1-20)
INSERT INTO public.airdrying (
experiment_id,
experiment_phase_id,
scheduled_start_time,
duration_minutes,
scheduled_end_time,
created_by
)
SELECT
e.experiment_number,
e.phase_id,
NOW() + (e.experiment_number) * INTERVAL '1 day' + INTERVAL '2 days', -- Start 2 days after soaking
CASE e.experiment_number
WHEN 1 THEN 19 -- 19 minutes
WHEN 2 THEN 27 -- 27 minutes
WHEN 3 THEN 10 -- 10 minutes
WHEN 4 THEN 36 -- 36 minutes
WHEN 5 THEN 41 -- 41 minutes
WHEN 6 THEN 33 -- 33 minutes
WHEN 7 THEN 22 -- 22 minutes
WHEN 8 THEN 30 -- 30 minutes
WHEN 9 THEN 12 -- 12 minutes
WHEN 10 THEN 26 -- 26 minutes
WHEN 11 THEN 60 -- 60 minutes
WHEN 12 THEN 59 -- 59 minutes
WHEN 13 THEN 59 -- 59 minutes
WHEN 14 THEN 59 -- 59 minutes
WHEN 15 THEN 60 -- 60 minutes
WHEN 16 THEN 59 -- 59 minutes
WHEN 17 THEN 60 -- 60 minutes
WHEN 18 THEN 49 -- 49 minutes
WHEN 19 THEN 25 -- 25 minutes
WHEN 20 THEN 25 -- 25 minutes
END,
NOW() + (e.experiment_number) * INTERVAL '1 day' + INTERVAL '2 days' +
CASE e.experiment_number
WHEN 1 THEN 19 * INTERVAL '1 minute' -- 19 minutes
WHEN 2 THEN 27 * INTERVAL '1 minute' -- 27 minutes
WHEN 3 THEN 10 * INTERVAL '1 minute' -- 10 minutes
WHEN 4 THEN 36 * INTERVAL '1 minute' -- 36 minutes
WHEN 5 THEN 41 * INTERVAL '1 minute' -- 41 minutes
WHEN 6 THEN 33 * INTERVAL '1 minute' -- 33 minutes
WHEN 7 THEN 22 * INTERVAL '1 minute' -- 22 minutes
WHEN 8 THEN 30 * INTERVAL '1 minute' -- 30 minutes
WHEN 9 THEN 12 * INTERVAL '1 minute' -- 12 minutes
WHEN 10 THEN 26 * INTERVAL '1 minute' -- 26 minutes
WHEN 11 THEN 60 * INTERVAL '1 minute' -- 60 minutes
WHEN 12 THEN 59 * INTERVAL '1 minute' -- 59 minutes
WHEN 13 THEN 59 * INTERVAL '1 minute' -- 59 minutes
WHEN 14 THEN 59 * INTERVAL '1 minute' -- 59 minutes
WHEN 15 THEN 60 * INTERVAL '1 minute' -- 60 minutes
WHEN 16 THEN 59 * INTERVAL '1 minute' -- 59 minutes
WHEN 17 THEN 60 * INTERVAL '1 minute' -- 60 minutes
WHEN 18 THEN 49 * INTERVAL '1 minute' -- 49 minutes
WHEN 19 THEN 25 * INTERVAL '1 minute' -- 25 minutes
WHEN 20 THEN 25 * INTERVAL '1 minute' -- 25 minutes
END,
e.created_by
FROM public.experiments e
WHERE e.experiment_number BETWEEN 1 AND 20
AND e.phase_id = (SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments')
ON CONFLICT DO NOTHING;
-- =============================================
-- CREATE CRACKING PHASE RECORDS FOR PHASE 2 JC EXPERIMENTS
-- =============================================
-- Create cracking records for Phase 2 JC experiments (1-20)
INSERT INTO public.cracking (
experiment_id,
experiment_phase_id,
machine_type_id,
scheduled_start_time,
created_by
)
SELECT
e.experiment_number,
e.phase_id,
(SELECT id FROM public.machine_types WHERE name = 'JC Cracker'),
NOW() + (e.experiment_number) * INTERVAL '1 day' + INTERVAL '3 days', -- Start 3 days after soaking
e.created_by
FROM public.experiments e
WHERE e.experiment_number BETWEEN 1 AND 20
AND e.phase_id = (SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments')
ON CONFLICT DO NOTHING;
-- =============================================
-- CREATE JC CRACKER PARAMETERS FOR PHASE 2 JC EXPERIMENTS
-- =============================================
-- Create JC cracker parameters for Phase 2 JC experiments (1-20)
INSERT INTO public.jc_cracker_parameters (
cracking_id,
plate_contact_frequency_hz,
throughput_rate_pecans_sec,
crush_amount_in,
entry_exit_height_diff_in
)
SELECT
c.id,
CASE e.experiment_number
WHEN 1 THEN 53.0
WHEN 2 THEN 34.0
WHEN 3 THEN 60.0
WHEN 4 THEN 42.0
WHEN 5 THEN 41.0
WHEN 6 THEN 30.0
WHEN 7 THEN 37.0
WHEN 8 THEN 35.0
WHEN 9 THEN 55.0
WHEN 10 THEN 47.0
WHEN 11 THEN 44.0
WHEN 12 THEN 42.0
WHEN 13 THEN 37.0
WHEN 14 THEN 41.0
WHEN 15 THEN 45.0
WHEN 16 THEN 30.0
WHEN 17 THEN 41.0
WHEN 18 THEN 34.0
WHEN 19 THEN 38.0
WHEN 20 THEN 56.0
END,
CASE e.experiment_number
WHEN 1 THEN 28.0
WHEN 2 THEN 29.0
WHEN 3 THEN 28.0
WHEN 4 THEN 13.0
WHEN 5 THEN 38.0
WHEN 6 THEN 36.0
WHEN 7 THEN 30.0
WHEN 8 THEN 32.0
WHEN 9 THEN 24.0
WHEN 10 THEN 26.0
WHEN 11 THEN 12.0
WHEN 12 THEN 25.0
WHEN 13 THEN 23.0
WHEN 14 THEN 21.0
WHEN 15 THEN 17.0
WHEN 16 THEN 24.0
WHEN 17 THEN 14.0
WHEN 18 THEN 29.0
WHEN 19 THEN 35.0
WHEN 20 THEN 34.0
END,
CASE e.experiment_number
WHEN 1 THEN 0.05
WHEN 2 THEN 0.03
WHEN 3 THEN 0.06
WHEN 4 THEN 0.07
WHEN 5 THEN 0.05
WHEN 6 THEN 0.05
WHEN 7 THEN 0.06
WHEN 8 THEN 0.05
WHEN 9 THEN 0.04
WHEN 10 THEN 0.07
WHEN 11 THEN 0.08
WHEN 12 THEN 0.07
WHEN 13 THEN 0.06
WHEN 14 THEN 0.06
WHEN 15 THEN 0.07
WHEN 16 THEN 0.07
WHEN 17 THEN 0.07
WHEN 18 THEN 0.07
WHEN 19 THEN 0.07
WHEN 20 THEN 0.06
END,
CASE e.experiment_number
WHEN 1 THEN -0.09
WHEN 2 THEN 0.01
WHEN 3 THEN -0.10
WHEN 4 THEN -0.07
WHEN 5 THEN 0.03
WHEN 6 THEN -0.04
WHEN 7 THEN 0.02
WHEN 8 THEN -0.07
WHEN 9 THEN 0.04
WHEN 10 THEN 0.03
WHEN 11 THEN -0.10
WHEN 12 THEN -0.05
WHEN 13 THEN -0.08
WHEN 14 THEN -0.09
WHEN 15 THEN -0.08
WHEN 16 THEN 0.02
WHEN 17 THEN 0.04
WHEN 18 THEN -0.09
WHEN 19 THEN -0.08
WHEN 20 THEN -0.09
END
FROM public.experiments e
JOIN public.cracking c ON c.experiment_id = e.experiment_number AND c.experiment_phase_id = e.phase_id
WHERE e.experiment_number BETWEEN 1 AND 20
AND e.phase_id = (SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments')
ON CONFLICT DO NOTHING;
-- =============================================
-- CREATE EXPERIMENT REPETITIONS FOR PHASE 2 JC EXPERIMENTS
-- =============================================
-- Create experiment repetitions for Phase 2 JC experiments
-- Each experiment needs 3 repetitions
INSERT INTO public.experiment_repetitions (
experiment_id,
experiment_phase_id,
repetition_number,
status,
created_by
)
SELECT
e.experiment_number,
e.phase_id,
rep_num,
'pending',
e.created_by
FROM public.experiments e
CROSS JOIN generate_series(1, 3) AS rep_num
WHERE e.experiment_number BETWEEN 1 AND 20
AND e.phase_id = (SELECT id FROM public.experiment_phases WHERE name = 'Phase 2 of JC Experiments')
ON CONFLICT DO NOTHING;

View File

@@ -0,0 +1,548 @@
-- Meyer Experiments Seed Data
-- This file contains all 40 Meyer experiments from meyer experiments.csv
-- Each experiment has only 1 repetition required
-- Updated to use 1-based numbering per phase and composite primary key
-- =============================================
-- INSERT MEYER EXPERIMENTS (Post Workshop)
-- =============================================
-- Insert Meyer experiments (experiments 1-40)
INSERT INTO public.experiments (
experiment_number,
reps_required,
results_status,
completion_status,
phase_id,
created_by
) VALUES
-- Meyer Experiments 1-40 (1-based numbering)
(1, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(2, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(3, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(4, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(5, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(6, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(7, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(8, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(9, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(10, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(11, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(12, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(13, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(14, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(15, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(16, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(17, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(18, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(19, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(20, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(21, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(22, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(23, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(24, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(25, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(26, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(27, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(28, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(29, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(30, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(31, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(32, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(33, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(34, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(35, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(36, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(37, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(38, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(39, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(40, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com'))
ON CONFLICT (experiment_number, phase_id) DO NOTHING;
-- =============================================
-- CREATE SOAKING PHASE RECORDS FOR MEYER EXPERIMENTS
-- =============================================
-- Create soaking records for Meyer experiments (1-40)
INSERT INTO public.soaking (
experiment_id,
experiment_phase_id,
scheduled_start_time,
soaking_duration_hours,
scheduled_end_time,
created_by
)
SELECT
e.experiment_number,
e.phase_id,
NOW() + (e.experiment_number) * INTERVAL '1 day',
CASE e.experiment_number
WHEN 1 THEN 27 -- hours
WHEN 2 THEN 37
WHEN 3 THEN 36
WHEN 4 THEN 12
WHEN 5 THEN 34
WHEN 6 THEN 18
WHEN 7 THEN 14
WHEN 8 THEN 18
WHEN 9 THEN 11
WHEN 10 THEN 33
WHEN 11 THEN 23
WHEN 12 THEN 37
WHEN 13 THEN 15
WHEN 14 THEN 24
WHEN 15 THEN 36
WHEN 16 THEN 32
WHEN 17 THEN 28
WHEN 18 THEN 31
WHEN 19 THEN 20
WHEN 20 THEN 10
WHEN 21 THEN 16
WHEN 22 THEN 21
WHEN 23 THEN 42
WHEN 24 THEN 29
WHEN 25 THEN 54
WHEN 26 THEN 29
WHEN 27 THEN 30
WHEN 28 THEN 35
WHEN 29 THEN 27
WHEN 30 THEN 27
WHEN 31 THEN 17
WHEN 32 THEN 13
WHEN 33 THEN 19
WHEN 34 THEN 38
WHEN 35 THEN 26
WHEN 36 THEN 22
WHEN 37 THEN 12
WHEN 38 THEN 16
WHEN 39 THEN 22
WHEN 40 THEN 24
END,
NOW() + (e.experiment_number) * INTERVAL '1 day' +
CASE e.experiment_number
WHEN 1 THEN 27 * INTERVAL '1 hour'
WHEN 2 THEN 37 * INTERVAL '1 hour'
WHEN 3 THEN 36 * INTERVAL '1 hour'
WHEN 4 THEN 12 * INTERVAL '1 hour'
WHEN 5 THEN 34 * INTERVAL '1 hour'
WHEN 6 THEN 18 * INTERVAL '1 hour'
WHEN 7 THEN 14 * INTERVAL '1 hour'
WHEN 8 THEN 18 * INTERVAL '1 hour'
WHEN 9 THEN 11 * INTERVAL '1 hour'
WHEN 10 THEN 33 * INTERVAL '1 hour'
WHEN 11 THEN 23 * INTERVAL '1 hour'
WHEN 12 THEN 37 * INTERVAL '1 hour'
WHEN 13 THEN 15 * INTERVAL '1 hour'
WHEN 14 THEN 24 * INTERVAL '1 hour'
WHEN 15 THEN 36 * INTERVAL '1 hour'
WHEN 16 THEN 32 * INTERVAL '1 hour'
WHEN 17 THEN 28 * INTERVAL '1 hour'
WHEN 18 THEN 31 * INTERVAL '1 hour'
WHEN 19 THEN 20 * INTERVAL '1 hour'
WHEN 20 THEN 10 * INTERVAL '1 hour'
WHEN 21 THEN 16 * INTERVAL '1 hour'
WHEN 22 THEN 21 * INTERVAL '1 hour'
WHEN 23 THEN 42 * INTERVAL '1 hour'
WHEN 24 THEN 29 * INTERVAL '1 hour'
WHEN 25 THEN 54 * INTERVAL '1 hour'
WHEN 26 THEN 29 * INTERVAL '1 hour'
WHEN 27 THEN 30 * INTERVAL '1 hour'
WHEN 28 THEN 35 * INTERVAL '1 hour'
WHEN 29 THEN 27 * INTERVAL '1 hour'
WHEN 30 THEN 27 * INTERVAL '1 hour'
WHEN 31 THEN 17 * INTERVAL '1 hour'
WHEN 32 THEN 13 * INTERVAL '1 hour'
WHEN 33 THEN 19 * INTERVAL '1 hour'
WHEN 34 THEN 38 * INTERVAL '1 hour'
WHEN 35 THEN 26 * INTERVAL '1 hour'
WHEN 36 THEN 22 * INTERVAL '1 hour'
WHEN 37 THEN 12 * INTERVAL '1 hour'
WHEN 38 THEN 16 * INTERVAL '1 hour'
WHEN 39 THEN 22 * INTERVAL '1 hour'
WHEN 40 THEN 24 * INTERVAL '1 hour'
END,
e.created_by
FROM public.experiments e
WHERE e.experiment_number BETWEEN 1 AND 40
AND e.phase_id = (SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments')
ON CONFLICT DO NOTHING;
-- =============================================
-- CREATE AIRDRYING PHASE RECORDS FOR MEYER EXPERIMENTS
-- =============================================
-- Create airdrying records for Meyer experiments (1-40)
INSERT INTO public.airdrying (
experiment_id,
experiment_phase_id,
scheduled_start_time,
duration_minutes,
scheduled_end_time,
created_by
)
SELECT
e.experiment_number,
e.phase_id,
NOW() + (e.experiment_number) * INTERVAL '1 day' + INTERVAL '2 days', -- Start 2 days after soaking
CASE e.experiment_number
WHEN 1 THEN 28 -- 28 minutes
WHEN 2 THEN 17 -- 17 minutes
WHEN 3 THEN 50 -- 50 minutes
WHEN 4 THEN 30 -- 30 minutes
WHEN 5 THEN 19 -- 19 minutes
WHEN 6 THEN 40 -- 40 minutes
WHEN 7 THEN 59 -- 59 minutes
WHEN 8 THEN 32 -- 32 minutes
WHEN 9 THEN 31 -- 31 minutes
WHEN 10 THEN 12 -- 12 minutes
WHEN 11 THEN 36 -- 36 minutes
WHEN 12 THEN 35 -- 35 minutes
WHEN 13 THEN 15 -- 15 minutes
WHEN 14 THEN 22 -- 22 minutes
WHEN 15 THEN 15 -- 15 minutes
WHEN 16 THEN 48 -- 48 minutes
WHEN 17 THEN 38 -- 38 minutes
WHEN 18 THEN 51 -- 51 minutes
WHEN 19 THEN 57 -- 57 minutes
WHEN 20 THEN 27 -- 27 minutes
WHEN 21 THEN 43 -- 43 minutes
WHEN 22 THEN 42 -- 42 minutes
WHEN 23 THEN 21 -- 21 minutes
WHEN 24 THEN 46 -- 46 minutes
WHEN 25 THEN 54 -- 54 minutes
WHEN 26 THEN 54 -- 54 minutes
WHEN 27 THEN 48 -- 48 minutes
WHEN 28 THEN 53 -- 53 minutes
WHEN 29 THEN 39 -- 39 minutes
WHEN 30 THEN 38 -- 38 minutes
WHEN 31 THEN 25 -- 25 minutes
WHEN 32 THEN 22 -- 22 minutes
WHEN 33 THEN 11 -- 11 minutes
WHEN 34 THEN 32 -- 32 minutes
WHEN 35 THEN 18 -- 18 minutes
WHEN 36 THEN 52 -- 52 minutes
WHEN 37 THEN 56 -- 56 minutes
WHEN 38 THEN 45 -- 45 minutes
WHEN 39 THEN 25 -- 25 minutes
WHEN 40 THEN 13 -- 13 minutes
END,
NOW() + (e.experiment_number) * INTERVAL '1 day' + INTERVAL '2 days' +
CASE e.experiment_number
WHEN 1 THEN 28 * INTERVAL '1 minute' -- 28 minutes
WHEN 2 THEN 17 * INTERVAL '1 minute' -- 17 minutes
WHEN 3 THEN 50 * INTERVAL '1 minute' -- 50 minutes
WHEN 4 THEN 30 * INTERVAL '1 minute' -- 30 minutes
WHEN 5 THEN 19 * INTERVAL '1 minute' -- 19 minutes
WHEN 6 THEN 40 * INTERVAL '1 minute' -- 40 minutes
WHEN 7 THEN 59 * INTERVAL '1 minute' -- 59 minutes
WHEN 8 THEN 32 * INTERVAL '1 minute' -- 32 minutes
WHEN 9 THEN 31 * INTERVAL '1 minute' -- 31 minutes
WHEN 10 THEN 12 * INTERVAL '1 minute' -- 12 minutes
WHEN 11 THEN 36 * INTERVAL '1 minute' -- 36 minutes
WHEN 12 THEN 35 * INTERVAL '1 minute' -- 35 minutes
WHEN 13 THEN 15 * INTERVAL '1 minute' -- 15 minutes
WHEN 14 THEN 22 * INTERVAL '1 minute' -- 22 minutes
WHEN 15 THEN 15 * INTERVAL '1 minute' -- 15 minutes
WHEN 16 THEN 48 * INTERVAL '1 minute' -- 48 minutes
WHEN 17 THEN 38 * INTERVAL '1 minute' -- 38 minutes
WHEN 18 THEN 51 * INTERVAL '1 minute' -- 51 minutes
WHEN 19 THEN 57 * INTERVAL '1 minute' -- 57 minutes
WHEN 20 THEN 27 * INTERVAL '1 minute' -- 27 minutes
WHEN 21 THEN 43 * INTERVAL '1 minute' -- 43 minutes
WHEN 22 THEN 42 * INTERVAL '1 minute' -- 42 minutes
WHEN 23 THEN 21 * INTERVAL '1 minute' -- 21 minutes
WHEN 24 THEN 46 * INTERVAL '1 minute' -- 46 minutes
WHEN 25 THEN 54 * INTERVAL '1 minute' -- 54 minutes
WHEN 26 THEN 54 * INTERVAL '1 minute' -- 54 minutes
WHEN 27 THEN 48 * INTERVAL '1 minute' -- 48 minutes
WHEN 28 THEN 53 * INTERVAL '1 minute' -- 53 minutes
WHEN 29 THEN 39 * INTERVAL '1 minute' -- 39 minutes
WHEN 30 THEN 38 * INTERVAL '1 minute' -- 38 minutes
WHEN 31 THEN 25 * INTERVAL '1 minute' -- 25 minutes
WHEN 32 THEN 22 * INTERVAL '1 minute' -- 22 minutes
WHEN 33 THEN 11 * INTERVAL '1 minute' -- 11 minutes
WHEN 34 THEN 32 * INTERVAL '1 minute' -- 32 minutes
WHEN 35 THEN 18 * INTERVAL '1 minute' -- 18 minutes
WHEN 36 THEN 52 * INTERVAL '1 minute' -- 52 minutes
WHEN 37 THEN 56 * INTERVAL '1 minute' -- 56 minutes
WHEN 38 THEN 45 * INTERVAL '1 minute' -- 45 minutes
WHEN 39 THEN 25 * INTERVAL '1 minute' -- 25 minutes
WHEN 40 THEN 13 * INTERVAL '1 minute' -- 13 minutes
END,
e.created_by
FROM public.experiments e
WHERE e.experiment_number BETWEEN 1 AND 40
AND e.phase_id = (SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments')
ON CONFLICT DO NOTHING;
-- =============================================
-- CREATE CRACKING PHASE RECORDS FOR MEYER EXPERIMENTS
-- =============================================
-- Create cracking records for Meyer experiments (1-40)
INSERT INTO public.cracking (
experiment_id,
experiment_phase_id,
machine_type_id,
scheduled_start_time,
created_by
)
SELECT
e.experiment_number,
e.phase_id,
(SELECT id FROM public.machine_types WHERE name = 'Meyer Cracker'),
NOW() + (e.experiment_number) * INTERVAL '1 day' + INTERVAL '3 days', -- Start 3 days after soaking
e.created_by
FROM public.experiments e
WHERE e.experiment_number BETWEEN 1 AND 40
AND e.phase_id = (SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments')
ON CONFLICT DO NOTHING;
-- =============================================
-- CREATE MEYER CRACKER PARAMETERS FOR MEYER EXPERIMENTS
-- =============================================
-- Create Meyer cracker parameters for Meyer experiments (1-40)
INSERT INTO public.meyer_cracker_parameters (
cracking_id,
motor_speed_hz,
jig_displacement_inches,
spring_stiffness_nm
)
SELECT
c.id,
CASE e.experiment_number
WHEN 1 THEN 33.0
WHEN 2 THEN 30.0
WHEN 3 THEN 47.0
WHEN 4 THEN 42.0
WHEN 5 THEN 53.0
WHEN 6 THEN 37.0
WHEN 7 THEN 40.0
WHEN 8 THEN 39.0
WHEN 9 THEN 49.0
WHEN 10 THEN 47.0
WHEN 11 THEN 52.0
WHEN 12 THEN 59.0
WHEN 13 THEN 41.0
WHEN 14 THEN 46.0
WHEN 15 THEN 50.0
WHEN 16 THEN 36.0
WHEN 17 THEN 33.0
WHEN 18 THEN 35.0
WHEN 19 THEN 55.0
WHEN 20 THEN 44.0
WHEN 21 THEN 37.0
WHEN 22 THEN 56.0
WHEN 23 THEN 30.0
WHEN 24 THEN 60.0
WHEN 25 THEN 41.0
WHEN 26 THEN 55.0
WHEN 27 THEN 39.0
WHEN 28 THEN 34.0
WHEN 29 THEN 57.0
WHEN 30 THEN 45.0
WHEN 31 THEN 52.0
WHEN 32 THEN 51.0
WHEN 33 THEN 36.0
WHEN 34 THEN 44.0
WHEN 35 THEN 58.0
WHEN 36 THEN 32.0
WHEN 37 THEN 43.0
WHEN 38 THEN 60.0
WHEN 39 THEN 54.0
WHEN 40 THEN 48.0
END,
CASE e.experiment_number
WHEN 1 THEN -0.307
WHEN 2 THEN -0.311
WHEN 3 THEN -0.291
WHEN 4 THEN -0.314
WHEN 5 THEN -0.302
WHEN 6 THEN -0.301
WHEN 7 THEN -0.286
WHEN 8 THEN -0.309
WHEN 9 THEN -0.299
WHEN 10 THEN -0.295
WHEN 11 THEN -0.302
WHEN 12 THEN -0.299
WHEN 13 THEN -0.312
WHEN 14 THEN -0.303
WHEN 15 THEN -0.308
WHEN 16 THEN -0.306
WHEN 17 THEN -0.308
WHEN 18 THEN -0.311
WHEN 19 THEN -0.304
WHEN 20 THEN -0.313
WHEN 21 THEN -0.294
WHEN 22 THEN -0.310
WHEN 23 THEN -0.292
WHEN 24 THEN -0.294
WHEN 25 THEN -0.306
WHEN 26 THEN -0.296
WHEN 27 THEN -0.293
WHEN 28 THEN -0.285
WHEN 29 THEN -0.291
WHEN 30 THEN -0.296
WHEN 31 THEN -0.297
WHEN 32 THEN -0.288
WHEN 33 THEN -0.290
WHEN 34 THEN -0.315
WHEN 35 THEN -0.289
WHEN 36 THEN -0.288
WHEN 37 THEN -0.287
WHEN 38 THEN -0.298
WHEN 39 THEN -0.301
WHEN 40 THEN -0.305
END,
CASE e.experiment_number
WHEN 1 THEN 1800.0
WHEN 2 THEN 2000.0
WHEN 3 THEN 1800.0
WHEN 4 THEN 2000.0
WHEN 5 THEN 1800.0
WHEN 6 THEN 2200.0
WHEN 7 THEN 2000.0
WHEN 8 THEN 1800.0
WHEN 9 THEN 2200.0
WHEN 10 THEN 2000.0
WHEN 11 THEN 2000.0
WHEN 12 THEN 1800.0
WHEN 13 THEN 2000.0
WHEN 14 THEN 1800.0
WHEN 15 THEN 1800.0
WHEN 16 THEN 2200.0
WHEN 17 THEN 2200.0
WHEN 18 THEN 1800.0
WHEN 19 THEN 2000.0
WHEN 20 THEN 2200.0
WHEN 21 THEN 2000.0
WHEN 22 THEN 2200.0
WHEN 23 THEN 2200.0
WHEN 24 THEN 2200.0
WHEN 25 THEN 2000.0
WHEN 26 THEN 1800.0
WHEN 27 THEN 2200.0
WHEN 28 THEN 2200.0
WHEN 29 THEN 1800.0
WHEN 30 THEN 2200.0
WHEN 31 THEN 1800.0
WHEN 32 THEN 2200.0
WHEN 33 THEN 2000.0
WHEN 34 THEN 1800.0
WHEN 35 THEN 1800.0
WHEN 36 THEN 1800.0
WHEN 37 THEN 2200.0
WHEN 38 THEN 2200.0
WHEN 39 THEN 2000.0
WHEN 40 THEN 2000.0
END
FROM public.experiments e
JOIN public.cracking c ON c.experiment_id = e.experiment_number AND c.experiment_phase_id = e.phase_id
WHERE e.experiment_number BETWEEN 1 AND 40
AND e.phase_id = (SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments')
ON CONFLICT DO NOTHING;
-- =============================================
-- CREATE EXPERIMENT REPETITIONS FOR MEYER EXPERIMENTS
-- =============================================
-- Create experiment repetitions for Meyer experiments
-- Each experiment needs only 1 repetition
INSERT INTO public.experiment_repetitions (
experiment_id,
experiment_phase_id,
repetition_number,
status,
created_by
)
SELECT
e.experiment_number,
e.phase_id,
1,
'pending',
e.created_by
FROM public.experiments e
WHERE e.experiment_number BETWEEN 1 AND 40
AND e.phase_id = (SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments')
ON CONFLICT DO NOTHING;

View File

@@ -0,0 +1,548 @@
-- Meyer Experiments Seed Data
-- This file contains all 40 Meyer experiments from meyer experiments.csv
-- Each experiment has only 1 repetition required
-- Updated to use 1-based numbering per phase and composite primary key
-- =============================================
-- INSERT MEYER EXPERIMENTS (Post Workshop)
-- =============================================
-- Insert Meyer experiments (experiments 1-40)
INSERT INTO public.experiments (
experiment_number,
reps_required,
results_status,
completion_status,
phase_id,
created_by
) VALUES
-- Meyer Experiments 1-40 (1-based numbering)
(1, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(2, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(3, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(4, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(5, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(6, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(7, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(8, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(9, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(10, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(11, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(12, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(13, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(14, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(15, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(16, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(17, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(18, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(19, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(20, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(21, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(22, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(23, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(24, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(25, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(26, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(27, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(28, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(29, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(30, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(31, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(32, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(33, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(34, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(35, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(36, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(37, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(38, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(39, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')),
(40, 1, 'valid', false,
(SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments'),
(SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com'))
ON CONFLICT (experiment_number, phase_id) DO NOTHING;
-- =============================================
-- CREATE SOAKING PHASE RECORDS FOR MEYER EXPERIMENTS
-- =============================================
-- Create soaking records for Meyer experiments (1-40)
INSERT INTO public.soaking (
experiment_id,
experiment_phase_id,
scheduled_start_time,
soaking_duration_minutes,
scheduled_end_time,
created_by
)
SELECT
e.experiment_number,
e.phase_id,
NOW() + (e.experiment_number) * INTERVAL '1 day',
CASE e.experiment_number
WHEN 1 THEN 27 * 60 -- 27 hours = 1620 minutes
WHEN 2 THEN 37 * 60 -- 37 hours = 2220 minutes
WHEN 3 THEN 36 * 60 -- 36 hours = 2160 minutes
WHEN 4 THEN 12 * 60 -- 12 hours = 720 minutes
WHEN 5 THEN 34 * 60 -- 34 hours = 2040 minutes
WHEN 6 THEN 18 * 60 -- 18 hours = 1080 minutes
WHEN 7 THEN 14 * 60 -- 14 hours = 840 minutes
WHEN 8 THEN 18 * 60 -- 18 hours = 1080 minutes
WHEN 9 THEN 11 * 60 -- 11 hours = 660 minutes
WHEN 10 THEN 33 * 60 -- 33 hours = 1980 minutes
WHEN 11 THEN 23 * 60 -- 23 hours = 1380 minutes
WHEN 12 THEN 37 * 60 -- 37 hours = 2220 minutes
WHEN 13 THEN 15 * 60 -- 15 hours = 900 minutes
WHEN 14 THEN 24 * 60 -- 24 hours = 1440 minutes
WHEN 15 THEN 36 * 60 -- 36 hours = 2160 minutes
WHEN 16 THEN 32 * 60 -- 32 hours = 1920 minutes
WHEN 17 THEN 28 * 60 -- 28 hours = 1680 minutes
WHEN 18 THEN 31 * 60 -- 31 hours = 1860 minutes
WHEN 19 THEN 20 * 60 -- 20 hours = 1200 minutes
WHEN 20 THEN 10 * 60 -- 10 hours = 600 minutes
WHEN 21 THEN 16 * 60 -- 16 hours = 960 minutes
WHEN 22 THEN 21 * 60 -- 21 hours = 1260 minutes
WHEN 23 THEN 42 * 60 -- 42 hours = 2520 minutes
WHEN 24 THEN 29 * 60 -- 29 hours = 1740 minutes
WHEN 25 THEN 54 * 60 -- 54 hours = 3240 minutes
WHEN 26 THEN 29 * 60 -- 29 hours = 1740 minutes
WHEN 27 THEN 30 * 60 -- 30 hours = 1800 minutes
WHEN 28 THEN 35 * 60 -- 35 hours = 2100 minutes
WHEN 29 THEN 27 * 60 -- 27 hours = 1620 minutes
WHEN 30 THEN 27 * 60 -- 27 hours = 1620 minutes
WHEN 31 THEN 17 * 60 -- 17 hours = 1020 minutes
WHEN 32 THEN 13 * 60 -- 13 hours = 780 minutes
WHEN 33 THEN 19 * 60 -- 19 hours = 1140 minutes
WHEN 34 THEN 38 * 60 -- 38 hours = 2280 minutes
WHEN 35 THEN 26 * 60 -- 26 hours = 1560 minutes
WHEN 36 THEN 22 * 60 -- 22 hours = 1320 minutes
WHEN 37 THEN 12 * 60 -- 12 hours = 720 minutes
WHEN 38 THEN 16 * 60 -- 16 hours = 960 minutes
WHEN 39 THEN 22 * 60 -- 22 hours = 1320 minutes
WHEN 40 THEN 24 * 60 -- 24 hours = 1440 minutes
END,
NOW() + (e.experiment_number) * INTERVAL '1 day' +
CASE e.experiment_number
WHEN 1 THEN 27 * 60 * INTERVAL '1 minute' -- 27 hours = 1620 minutes
WHEN 2 THEN 37 * 60 * INTERVAL '1 minute' -- 37 hours = 2220 minutes
WHEN 3 THEN 36 * 60 * INTERVAL '1 minute' -- 36 hours = 2160 minutes
WHEN 4 THEN 12 * 60 * INTERVAL '1 minute' -- 12 hours = 720 minutes
WHEN 5 THEN 34 * 60 * INTERVAL '1 minute' -- 34 hours = 2040 minutes
WHEN 6 THEN 18 * 60 * INTERVAL '1 minute' -- 18 hours = 1080 minutes
WHEN 7 THEN 14 * 60 * INTERVAL '1 minute' -- 14 hours = 840 minutes
WHEN 8 THEN 18 * 60 * INTERVAL '1 minute' -- 18 hours = 1080 minutes
WHEN 9 THEN 11 * 60 * INTERVAL '1 minute' -- 11 hours = 660 minutes
WHEN 10 THEN 33 * 60 * INTERVAL '1 minute' -- 33 hours = 1980 minutes
WHEN 11 THEN 23 * 60 * INTERVAL '1 minute' -- 23 hours = 1380 minutes
WHEN 12 THEN 37 * 60 * INTERVAL '1 minute' -- 37 hours = 2220 minutes
WHEN 13 THEN 15 * 60 * INTERVAL '1 minute' -- 15 hours = 900 minutes
WHEN 14 THEN 24 * 60 * INTERVAL '1 minute' -- 24 hours = 1440 minutes
WHEN 15 THEN 36 * 60 * INTERVAL '1 minute' -- 36 hours = 2160 minutes
WHEN 16 THEN 32 * 60 * INTERVAL '1 minute' -- 32 hours = 1920 minutes
WHEN 17 THEN 28 * 60 * INTERVAL '1 minute' -- 28 hours = 1680 minutes
WHEN 18 THEN 31 * 60 * INTERVAL '1 minute' -- 31 hours = 1860 minutes
WHEN 19 THEN 20 * 60 * INTERVAL '1 minute' -- 20 hours = 1200 minutes
WHEN 20 THEN 10 * 60 * INTERVAL '1 minute' -- 10 hours = 600 minutes
WHEN 21 THEN 16 * 60 * INTERVAL '1 minute' -- 16 hours = 960 minutes
WHEN 22 THEN 21 * 60 * INTERVAL '1 minute' -- 21 hours = 1260 minutes
WHEN 23 THEN 42 * 60 * INTERVAL '1 minute' -- 42 hours = 2520 minutes
WHEN 24 THEN 29 * 60 * INTERVAL '1 minute' -- 29 hours = 1740 minutes
WHEN 25 THEN 54 * 60 * INTERVAL '1 minute' -- 54 hours = 3240 minutes
WHEN 26 THEN 29 * 60 * INTERVAL '1 minute' -- 29 hours = 1740 minutes
WHEN 27 THEN 30 * 60 * INTERVAL '1 minute' -- 30 hours = 1800 minutes
WHEN 28 THEN 35 * 60 * INTERVAL '1 minute' -- 35 hours = 2100 minutes
WHEN 29 THEN 27 * 60 * INTERVAL '1 minute' -- 27 hours = 1620 minutes
WHEN 30 THEN 27 * 60 * INTERVAL '1 minute' -- 27 hours = 1620 minutes
WHEN 31 THEN 17 * 60 * INTERVAL '1 minute' -- 17 hours = 1020 minutes
WHEN 32 THEN 13 * 60 * INTERVAL '1 minute' -- 13 hours = 780 minutes
WHEN 33 THEN 19 * 60 * INTERVAL '1 minute' -- 19 hours = 1140 minutes
WHEN 34 THEN 38 * 60 * INTERVAL '1 minute' -- 38 hours = 2280 minutes
WHEN 35 THEN 26 * 60 * INTERVAL '1 minute' -- 26 hours = 1560 minutes
WHEN 36 THEN 22 * 60 * INTERVAL '1 minute' -- 22 hours = 1320 minutes
WHEN 37 THEN 12 * 60 * INTERVAL '1 minute' -- 12 hours = 720 minutes
WHEN 38 THEN 16 * 60 * INTERVAL '1 minute' -- 16 hours = 960 minutes
WHEN 39 THEN 22 * 60 * INTERVAL '1 minute' -- 22 hours = 1320 minutes
WHEN 40 THEN 24 * 60 * INTERVAL '1 minute' -- 24 hours = 1440 minutes
END,
e.created_by
FROM public.experiments e
WHERE e.experiment_number BETWEEN 1 AND 40
AND e.phase_id = (SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments')
ON CONFLICT DO NOTHING;
-- =============================================
-- CREATE AIRDRYING PHASE RECORDS FOR MEYER EXPERIMENTS
-- =============================================
-- Create airdrying records for Meyer experiments (1-40)
INSERT INTO public.airdrying (
experiment_id,
experiment_phase_id,
scheduled_start_time,
duration_minutes,
scheduled_end_time,
created_by
)
SELECT
e.experiment_number,
e.phase_id,
NOW() + (e.experiment_number) * INTERVAL '1 day' + INTERVAL '2 days', -- Start 2 days after soaking
CASE e.experiment_number
WHEN 1 THEN 28 -- 28 minutes
WHEN 2 THEN 17 -- 17 minutes
WHEN 3 THEN 50 -- 50 minutes
WHEN 4 THEN 30 -- 30 minutes
WHEN 5 THEN 19 -- 19 minutes
WHEN 6 THEN 40 -- 40 minutes
WHEN 7 THEN 59 -- 59 minutes
WHEN 8 THEN 32 -- 32 minutes
WHEN 9 THEN 31 -- 31 minutes
WHEN 10 THEN 12 -- 12 minutes
WHEN 11 THEN 36 -- 36 minutes
WHEN 12 THEN 35 -- 35 minutes
WHEN 13 THEN 15 -- 15 minutes
WHEN 14 THEN 22 -- 22 minutes
WHEN 15 THEN 15 -- 15 minutes
WHEN 16 THEN 48 -- 48 minutes
WHEN 17 THEN 38 -- 38 minutes
WHEN 18 THEN 51 -- 51 minutes
WHEN 19 THEN 57 -- 57 minutes
WHEN 20 THEN 27 -- 27 minutes
WHEN 21 THEN 43 -- 43 minutes
WHEN 22 THEN 42 -- 42 minutes
WHEN 23 THEN 21 -- 21 minutes
WHEN 24 THEN 46 -- 46 minutes
WHEN 25 THEN 54 -- 54 minutes
WHEN 26 THEN 54 -- 54 minutes
WHEN 27 THEN 48 -- 48 minutes
WHEN 28 THEN 53 -- 53 minutes
WHEN 29 THEN 39 -- 39 minutes
WHEN 30 THEN 38 -- 38 minutes
WHEN 31 THEN 25 -- 25 minutes
WHEN 32 THEN 22 -- 22 minutes
WHEN 33 THEN 11 -- 11 minutes
WHEN 34 THEN 32 -- 32 minutes
WHEN 35 THEN 18 -- 18 minutes
WHEN 36 THEN 52 -- 52 minutes
WHEN 37 THEN 56 -- 56 minutes
WHEN 38 THEN 45 -- 45 minutes
WHEN 39 THEN 25 -- 25 minutes
WHEN 40 THEN 13 -- 13 minutes
END,
NOW() + (e.experiment_number) * INTERVAL '1 day' + INTERVAL '2 days' +
CASE e.experiment_number
WHEN 1 THEN 28 * INTERVAL '1 minute' -- 28 minutes
WHEN 2 THEN 17 * INTERVAL '1 minute' -- 17 minutes
WHEN 3 THEN 50 * INTERVAL '1 minute' -- 50 minutes
WHEN 4 THEN 30 * INTERVAL '1 minute' -- 30 minutes
WHEN 5 THEN 19 * INTERVAL '1 minute' -- 19 minutes
WHEN 6 THEN 40 * INTERVAL '1 minute' -- 40 minutes
WHEN 7 THEN 59 * INTERVAL '1 minute' -- 59 minutes
WHEN 8 THEN 32 * INTERVAL '1 minute' -- 32 minutes
WHEN 9 THEN 31 * INTERVAL '1 minute' -- 31 minutes
WHEN 10 THEN 12 * INTERVAL '1 minute' -- 12 minutes
WHEN 11 THEN 36 * INTERVAL '1 minute' -- 36 minutes
WHEN 12 THEN 35 * INTERVAL '1 minute' -- 35 minutes
WHEN 13 THEN 15 * INTERVAL '1 minute' -- 15 minutes
WHEN 14 THEN 22 * INTERVAL '1 minute' -- 22 minutes
WHEN 15 THEN 15 * INTERVAL '1 minute' -- 15 minutes
WHEN 16 THEN 48 * INTERVAL '1 minute' -- 48 minutes
WHEN 17 THEN 38 * INTERVAL '1 minute' -- 38 minutes
WHEN 18 THEN 51 * INTERVAL '1 minute' -- 51 minutes
WHEN 19 THEN 57 * INTERVAL '1 minute' -- 57 minutes
WHEN 20 THEN 27 * INTERVAL '1 minute' -- 27 minutes
WHEN 21 THEN 43 * INTERVAL '1 minute' -- 43 minutes
WHEN 22 THEN 42 * INTERVAL '1 minute' -- 42 minutes
WHEN 23 THEN 21 * INTERVAL '1 minute' -- 21 minutes
WHEN 24 THEN 46 * INTERVAL '1 minute' -- 46 minutes
WHEN 25 THEN 54 * INTERVAL '1 minute' -- 54 minutes
WHEN 26 THEN 54 * INTERVAL '1 minute' -- 54 minutes
WHEN 27 THEN 48 * INTERVAL '1 minute' -- 48 minutes
WHEN 28 THEN 53 * INTERVAL '1 minute' -- 53 minutes
WHEN 29 THEN 39 * INTERVAL '1 minute' -- 39 minutes
WHEN 30 THEN 38 * INTERVAL '1 minute' -- 38 minutes
WHEN 31 THEN 25 * INTERVAL '1 minute' -- 25 minutes
WHEN 32 THEN 22 * INTERVAL '1 minute' -- 22 minutes
WHEN 33 THEN 11 * INTERVAL '1 minute' -- 11 minutes
WHEN 34 THEN 32 * INTERVAL '1 minute' -- 32 minutes
WHEN 35 THEN 18 * INTERVAL '1 minute' -- 18 minutes
WHEN 36 THEN 52 * INTERVAL '1 minute' -- 52 minutes
WHEN 37 THEN 56 * INTERVAL '1 minute' -- 56 minutes
WHEN 38 THEN 45 * INTERVAL '1 minute' -- 45 minutes
WHEN 39 THEN 25 * INTERVAL '1 minute' -- 25 minutes
WHEN 40 THEN 13 * INTERVAL '1 minute' -- 13 minutes
END,
e.created_by
FROM public.experiments e
WHERE e.experiment_number BETWEEN 1 AND 40
AND e.phase_id = (SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments')
ON CONFLICT DO NOTHING;
-- =============================================
-- CREATE CRACKING PHASE RECORDS FOR MEYER EXPERIMENTS
-- =============================================
-- Create cracking records for Meyer experiments (1-40)
INSERT INTO public.cracking (
experiment_id,
experiment_phase_id,
machine_type_id,
scheduled_start_time,
created_by
)
SELECT
e.experiment_number,
e.phase_id,
(SELECT id FROM public.machine_types WHERE name = 'Meyer Cracker'),
NOW() + (e.experiment_number) * INTERVAL '1 day' + INTERVAL '3 days', -- Start 3 days after soaking
e.created_by
FROM public.experiments e
WHERE e.experiment_number BETWEEN 1 AND 40
AND e.phase_id = (SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments')
ON CONFLICT DO NOTHING;
-- =============================================
-- CREATE MEYER CRACKER PARAMETERS FOR MEYER EXPERIMENTS
-- =============================================
-- Create Meyer cracker parameters for Meyer experiments (1-40)
INSERT INTO public.meyer_cracker_parameters (
cracking_id,
motor_speed_hz,
jig_displacement_inches,
spring_stiffness_nm
)
SELECT
c.id,
CASE e.experiment_number
WHEN 1 THEN 33.0
WHEN 2 THEN 30.0
WHEN 3 THEN 47.0
WHEN 4 THEN 42.0
WHEN 5 THEN 53.0
WHEN 6 THEN 37.0
WHEN 7 THEN 40.0
WHEN 8 THEN 39.0
WHEN 9 THEN 49.0
WHEN 10 THEN 47.0
WHEN 11 THEN 52.0
WHEN 12 THEN 59.0
WHEN 13 THEN 41.0
WHEN 14 THEN 46.0
WHEN 15 THEN 50.0
WHEN 16 THEN 36.0
WHEN 17 THEN 33.0
WHEN 18 THEN 35.0
WHEN 19 THEN 55.0
WHEN 20 THEN 44.0
WHEN 21 THEN 37.0
WHEN 22 THEN 56.0
WHEN 23 THEN 30.0
WHEN 24 THEN 60.0
WHEN 25 THEN 41.0
WHEN 26 THEN 55.0
WHEN 27 THEN 39.0
WHEN 28 THEN 34.0
WHEN 29 THEN 57.0
WHEN 30 THEN 45.0
WHEN 31 THEN 52.0
WHEN 32 THEN 51.0
WHEN 33 THEN 36.0
WHEN 34 THEN 44.0
WHEN 35 THEN 58.0
WHEN 36 THEN 32.0
WHEN 37 THEN 43.0
WHEN 38 THEN 60.0
WHEN 39 THEN 54.0
WHEN 40 THEN 48.0
END,
CASE e.experiment_number
WHEN 1 THEN -0.307
WHEN 2 THEN -0.311
WHEN 3 THEN -0.291
WHEN 4 THEN -0.314
WHEN 5 THEN -0.302
WHEN 6 THEN -0.301
WHEN 7 THEN -0.286
WHEN 8 THEN -0.309
WHEN 9 THEN -0.299
WHEN 10 THEN -0.295
WHEN 11 THEN -0.302
WHEN 12 THEN -0.299
WHEN 13 THEN -0.312
WHEN 14 THEN -0.303
WHEN 15 THEN -0.308
WHEN 16 THEN -0.306
WHEN 17 THEN -0.308
WHEN 18 THEN -0.311
WHEN 19 THEN -0.304
WHEN 20 THEN -0.313
WHEN 21 THEN -0.294
WHEN 22 THEN -0.310
WHEN 23 THEN -0.292
WHEN 24 THEN -0.294
WHEN 25 THEN -0.306
WHEN 26 THEN -0.296
WHEN 27 THEN -0.293
WHEN 28 THEN -0.285
WHEN 29 THEN -0.291
WHEN 30 THEN -0.296
WHEN 31 THEN -0.297
WHEN 32 THEN -0.288
WHEN 33 THEN -0.290
WHEN 34 THEN -0.315
WHEN 35 THEN -0.289
WHEN 36 THEN -0.288
WHEN 37 THEN -0.287
WHEN 38 THEN -0.298
WHEN 39 THEN -0.301
WHEN 40 THEN -0.305
END,
CASE e.experiment_number
WHEN 1 THEN 1800.0
WHEN 2 THEN 2000.0
WHEN 3 THEN 1800.0
WHEN 4 THEN 2000.0
WHEN 5 THEN 1800.0
WHEN 6 THEN 2200.0
WHEN 7 THEN 2000.0
WHEN 8 THEN 1800.0
WHEN 9 THEN 2200.0
WHEN 10 THEN 2000.0
WHEN 11 THEN 2000.0
WHEN 12 THEN 1800.0
WHEN 13 THEN 2000.0
WHEN 14 THEN 1800.0
WHEN 15 THEN 1800.0
WHEN 16 THEN 2200.0
WHEN 17 THEN 2200.0
WHEN 18 THEN 1800.0
WHEN 19 THEN 2000.0
WHEN 20 THEN 2200.0
WHEN 21 THEN 2000.0
WHEN 22 THEN 2200.0
WHEN 23 THEN 2200.0
WHEN 24 THEN 2200.0
WHEN 25 THEN 2000.0
WHEN 26 THEN 1800.0
WHEN 27 THEN 2200.0
WHEN 28 THEN 2200.0
WHEN 29 THEN 1800.0
WHEN 30 THEN 2200.0
WHEN 31 THEN 1800.0
WHEN 32 THEN 2200.0
WHEN 33 THEN 2000.0
WHEN 34 THEN 1800.0
WHEN 35 THEN 1800.0
WHEN 36 THEN 1800.0
WHEN 37 THEN 2200.0
WHEN 38 THEN 2200.0
WHEN 39 THEN 2000.0
WHEN 40 THEN 2000.0
END
FROM public.experiments e
JOIN public.cracking c ON c.experiment_id = e.experiment_number AND c.experiment_phase_id = e.phase_id
WHERE e.experiment_number BETWEEN 1 AND 40
AND e.phase_id = (SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments')
ON CONFLICT DO NOTHING;
-- =============================================
-- CREATE EXPERIMENT REPETITIONS FOR MEYER EXPERIMENTS
-- =============================================
-- Create experiment repetitions for Meyer experiments
-- Each experiment needs only 1 repetition
INSERT INTO public.experiment_repetitions (
experiment_id,
experiment_phase_id,
repetition_number,
status,
created_by
)
SELECT
e.experiment_number,
e.phase_id,
1,
'pending',
e.created_by
FROM public.experiments e
WHERE e.experiment_number BETWEEN 1 AND 40
AND e.phase_id = (SELECT id FROM public.experiment_phases WHERE name = 'Post Workshop Meyer Experiments')
ON CONFLICT DO NOTHING;