WIP: integrate-old-refactors-of-github #1
@@ -3,7 +3,7 @@ import type { CreateExperimentRequest, UpdateExperimentRequest, ScheduleStatus,
|
||||
import { experimentPhaseManagement, machineTypeManagement } from '../lib/supabase'
|
||||
|
||||
interface ExperimentFormProps {
|
||||
initialData?: Partial<CreateExperimentRequest & { schedule_status: ScheduleStatus; results_status: ResultsStatus; completion_status: boolean }>
|
||||
initialData?: Partial<CreateExperimentRequest & { schedule_status: ScheduleStatus; results_status: ResultsStatus; completion_status: boolean }> & { phase_id?: string | null }
|
||||
onSubmit: (data: CreateExperimentRequest | UpdateExperimentRequest) => Promise<void>
|
||||
onCancel: () => void
|
||||
isEditing?: boolean
|
||||
@@ -12,31 +12,41 @@ interface ExperimentFormProps {
|
||||
}
|
||||
|
||||
export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = false, loading = false, phaseId }: ExperimentFormProps) {
|
||||
const [formData, setFormData] = useState<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,
|
||||
phase_id: initialData?.phase_id || phaseId
|
||||
const getInitialFormState = (d: any) => ({
|
||||
experiment_number: d?.experiment_number ?? 0,
|
||||
reps_required: d?.reps_required ?? 1,
|
||||
weight_per_repetition_lbs: d?.weight_per_repetition_lbs ?? 1,
|
||||
soaking_duration_hr: d?.soaking?.soaking_duration_hr ?? d?.soaking_duration_hr ?? 0,
|
||||
air_drying_time_min: d?.airdrying?.duration_minutes ?? d?.air_drying_time_min ?? 0,
|
||||
plate_contact_frequency_hz: d?.cracking?.plate_contact_frequency_hz ?? d?.plate_contact_frequency_hz ?? 1,
|
||||
throughput_rate_pecans_sec: d?.cracking?.throughput_rate_pecans_sec ?? d?.throughput_rate_pecans_sec ?? 1,
|
||||
crush_amount_in: d?.cracking?.crush_amount_in ?? d?.crush_amount_in ?? 0,
|
||||
entry_exit_height_diff_in: d?.cracking?.entry_exit_height_diff_in ?? d?.entry_exit_height_diff_in ?? 0,
|
||||
motor_speed_hz: d?.cracking?.motor_speed_hz ?? d?.motor_speed_hz ?? 1,
|
||||
jig_displacement_inches: d?.cracking?.jig_displacement_inches ?? d?.jig_displacement_inches ?? 0,
|
||||
spring_stiffness_nm: d?.cracking?.spring_stiffness_nm ?? d?.spring_stiffness_nm ?? 1,
|
||||
schedule_status: d?.schedule_status ?? 'pending schedule',
|
||||
results_status: d?.results_status ?? 'valid',
|
||||
completion_status: d?.completion_status ?? false,
|
||||
phase_id: d?.phase_id ?? phaseId,
|
||||
ring_gap_inches: d?.shelling?.ring_gap_inches ?? d?.ring_gap_inches ?? null,
|
||||
drum_rpm: d?.shelling?.drum_rpm ?? d?.drum_rpm ?? null
|
||||
})
|
||||
|
||||
const [formData, setFormData] = useState<CreateExperimentRequest & { schedule_status: ScheduleStatus; results_status: ResultsStatus; completion_status: boolean }>(() => getInitialFormState(initialData))
|
||||
|
||||
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)
|
||||
|
||||
// When initialData loads with phase config (edit mode), sync form state
|
||||
useEffect(() => {
|
||||
if ((initialData as any)?.id) {
|
||||
setFormData(prev => ({ ...prev, ...getInitialFormState(initialData) }))
|
||||
}
|
||||
}, [initialData])
|
||||
|
||||
useEffect(() => {
|
||||
const loadMeta = async () => {
|
||||
if (!phaseId) return
|
||||
@@ -76,11 +86,11 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa
|
||||
}
|
||||
|
||||
|
||||
if (formData.soaking_duration_hr < 0) {
|
||||
if ((formData.soaking_duration_hr ?? 0) < 0) {
|
||||
newErrors.soaking_duration_hr = 'Soaking duration cannot be negative'
|
||||
}
|
||||
|
||||
if (formData.air_drying_time_min < 0) {
|
||||
if ((formData.air_drying_time_min ?? 0) < 0) {
|
||||
newErrors.air_drying_time_min = 'Air drying time cannot be negative'
|
||||
}
|
||||
|
||||
@@ -93,7 +103,7 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa
|
||||
if (!formData.throughput_rate_pecans_sec || formData.throughput_rate_pecans_sec <= 0) {
|
||||
newErrors.throughput_rate_pecans_sec = 'Throughput rate must be positive'
|
||||
}
|
||||
if (formData.crush_amount_in < 0) {
|
||||
if ((formData.crush_amount_in ?? 0) < 0) {
|
||||
newErrors.crush_amount_in = 'Crush amount cannot be negative'
|
||||
}
|
||||
}
|
||||
@@ -110,6 +120,16 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa
|
||||
}
|
||||
}
|
||||
|
||||
// Shelling: if provided, must be positive
|
||||
if (phase?.has_shelling) {
|
||||
if (formData.ring_gap_inches != null && (typeof formData.ring_gap_inches !== 'number' || formData.ring_gap_inches <= 0)) {
|
||||
newErrors.ring_gap_inches = 'Ring gap must be positive'
|
||||
}
|
||||
if (formData.drum_rpm != null && (typeof formData.drum_rpm !== 'number' || formData.drum_rpm <= 0)) {
|
||||
newErrors.drum_rpm = 'Drum RPM must be positive'
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
@@ -122,14 +142,25 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa
|
||||
}
|
||||
|
||||
try {
|
||||
// Prepare data for submission
|
||||
// Prepare data: include all phase params so they are stored in experiment_soaking, experiment_airdrying, experiment_cracking, experiment_shelling
|
||||
const submitData = isEditing ? formData : {
|
||||
experiment_number: formData.experiment_number,
|
||||
reps_required: formData.reps_required,
|
||||
weight_per_repetition_lbs: formData.weight_per_repetition_lbs,
|
||||
results_status: formData.results_status,
|
||||
completion_status: formData.completion_status,
|
||||
phase_id: formData.phase_id
|
||||
phase_id: formData.phase_id,
|
||||
soaking_duration_hr: formData.soaking_duration_hr,
|
||||
air_drying_time_min: formData.air_drying_time_min,
|
||||
plate_contact_frequency_hz: formData.plate_contact_frequency_hz,
|
||||
throughput_rate_pecans_sec: formData.throughput_rate_pecans_sec,
|
||||
crush_amount_in: formData.crush_amount_in,
|
||||
entry_exit_height_diff_in: formData.entry_exit_height_diff_in,
|
||||
motor_speed_hz: (formData as any).motor_speed_hz,
|
||||
jig_displacement_inches: (formData as any).jig_displacement_inches,
|
||||
spring_stiffness_nm: (formData as any).spring_stiffness_nm,
|
||||
ring_gap_inches: formData.ring_gap_inches ?? undefined,
|
||||
drum_rpm: formData.drum_rpm ?? undefined
|
||||
}
|
||||
|
||||
await onSubmit(submitData)
|
||||
@@ -138,7 +169,7 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa
|
||||
}
|
||||
}
|
||||
|
||||
const handleInputChange = (field: keyof typeof formData, value: string | number | boolean) => {
|
||||
const handleInputChange = (field: keyof typeof formData, value: string | number | boolean | null | undefined) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
@@ -441,18 +472,40 @@ export function ExperimentForm({ initialData, onSubmit, onCancel, isEditing = fa
|
||||
<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 htmlFor="ring_gap_inches" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Ring gap (inches)
|
||||
</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"
|
||||
id="ring_gap_inches"
|
||||
value={formData.ring_gap_inches ?? ''}
|
||||
onChange={(e) => handleInputChange('ring_gap_inches' as any, e.target.value === '' ? null : parseFloat(e.target.value))}
|
||||
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.ring_gap_inches ? 'border-red-300' : 'border-gray-300'}`}
|
||||
placeholder="e.g. 0.25"
|
||||
min="0"
|
||||
step="0.01"
|
||||
/>
|
||||
{errors.ring_gap_inches && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.ring_gap_inches}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="drum_rpm" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Drum RPM
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="drum_rpm"
|
||||
value={formData.drum_rpm ?? ''}
|
||||
onChange={(e) => handleInputChange('drum_rpm' as any, e.target.value === '' ? null : parseInt(e.target.value, 10))}
|
||||
className={`max-w-xs px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors text-sm ${errors.drum_rpm ? 'border-red-300' : 'border-gray-300'}`}
|
||||
placeholder="e.g. 300"
|
||||
min="1"
|
||||
step="1"
|
||||
/>
|
||||
{errors.drum_rpm && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.drum_rpm}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { ExperimentForm } from './ExperimentForm'
|
||||
import { experimentManagement } from '../lib/supabase'
|
||||
import type { Experiment, CreateExperimentRequest, UpdateExperimentRequest } from '../lib/supabase'
|
||||
@@ -13,9 +13,20 @@ interface ExperimentModalProps {
|
||||
export function ExperimentModal({ experiment, onClose, onExperimentSaved, phaseId }: ExperimentModalProps) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [initialData, setInitialData] = useState<Experiment | (Experiment & { soaking?: any; airdrying?: any; cracking?: any; shelling?: any }) | undefined>(experiment ?? undefined)
|
||||
|
||||
const isEditing = !!experiment
|
||||
|
||||
useEffect(() => {
|
||||
if (experiment) {
|
||||
experimentManagement.getExperimentWithPhaseConfig(experiment.id)
|
||||
.then((data) => setInitialData(data ?? experiment))
|
||||
.catch(() => setInitialData(experiment))
|
||||
} else {
|
||||
setInitialData(undefined)
|
||||
}
|
||||
}, [experiment?.id])
|
||||
|
||||
const handleSubmit = async (data: CreateExperimentRequest | UpdateExperimentRequest) => {
|
||||
setError(null)
|
||||
setLoading(true)
|
||||
@@ -24,22 +35,24 @@ export function ExperimentModal({ experiment, onClose, onExperimentSaved, phaseI
|
||||
let savedExperiment: Experiment
|
||||
|
||||
if (isEditing && experiment) {
|
||||
// Check if experiment number is unique (excluding current experiment)
|
||||
// Check if experiment number is unique within this phase (excluding current experiment)
|
||||
if ('experiment_number' in data && data.experiment_number !== undefined && data.experiment_number !== experiment.experiment_number) {
|
||||
const isUnique = await experimentManagement.isExperimentNumberUnique(data.experiment_number, experiment.id)
|
||||
const phaseIdToCheck = data.phase_id ?? experiment.phase_id ?? phaseId
|
||||
const isUnique = await experimentManagement.isExperimentNumberUnique(data.experiment_number, phaseIdToCheck ?? undefined, experiment.id)
|
||||
if (!isUnique) {
|
||||
setError('Experiment number already exists. Please choose a different number.')
|
||||
setError('Experiment number already exists in this phase. Please choose a different number.')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
savedExperiment = await experimentManagement.updateExperiment(experiment.id, data)
|
||||
} else {
|
||||
// Check if experiment number is unique for new experiments
|
||||
// Check if experiment number is unique within this phase for new experiments
|
||||
const createData = data as CreateExperimentRequest
|
||||
const isUnique = await experimentManagement.isExperimentNumberUnique(createData.experiment_number)
|
||||
const phaseIdToCheck = createData.phase_id ?? phaseId
|
||||
const isUnique = await experimentManagement.isExperimentNumberUnique(createData.experiment_number, phaseIdToCheck ?? undefined)
|
||||
if (!isUnique) {
|
||||
setError('Experiment number already exists. Please choose a different number.')
|
||||
setError('Experiment number already exists in this phase. Please choose a different number.')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -115,7 +128,7 @@ export function ExperimentModal({ experiment, onClose, onExperimentSaved, phaseI
|
||||
|
||||
{/* Form */}
|
||||
<ExperimentForm
|
||||
initialData={experiment}
|
||||
initialData={initialData ? { ...initialData, phase_id: initialData.phase_id ?? undefined } : undefined}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={handleCancel}
|
||||
isEditing={isEditing}
|
||||
|
||||
@@ -31,8 +31,8 @@ export function ExperimentPhases({ onPhaseSelect }: ExperimentPhasesProps) {
|
||||
setPhases(phasesData)
|
||||
setCurrentUser(userData)
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to load experiment phases')
|
||||
console.error('Load experiment phases error:', err)
|
||||
setError(err.message || 'Failed to load experiment books')
|
||||
console.error('Load experiment books error:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -61,16 +61,16 @@ export function ExperimentPhases({ onPhaseSelect }: ExperimentPhasesProps) {
|
||||
<div className="mb-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Experiment Phases</h1>
|
||||
<p className="mt-2 text-gray-600 dark:text-gray-400">Select an experiment phase to view and manage its experiments</p>
|
||||
<p className="mt-2 text-gray-600 dark:text-gray-400">Experiment phases help organize experiments into logical groups for easier navigation and management.</p>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Experiment Books</h1>
|
||||
<p className="mt-2 text-gray-600 dark:text-gray-400">Select an experiment book to view and manage its experiments</p>
|
||||
<p className="mt-2 text-gray-600 dark:text-gray-400">Experiment books help organize experiments into logical groups for easier navigation and management.</p>
|
||||
</div>
|
||||
{canManagePhases && (
|
||||
<button
|
||||
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
|
||||
➕ New Book
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -162,9 +162,9 @@ export function ExperimentPhases({ onPhaseSelect }: ExperimentPhasesProps) {
|
||||
<svg className="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
|
||||
</svg>
|
||||
<h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-white">No experiment phases found</h3>
|
||||
<h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-white">No experiment books found</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Get started by creating your first experiment phase.
|
||||
Get started by creating your first experiment book.
|
||||
</p>
|
||||
{canManagePhases && (
|
||||
<div className="mt-6">
|
||||
@@ -172,7 +172,7 @@ export function ExperimentPhases({ onPhaseSelect }: ExperimentPhasesProps) {
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
➕ Create First Phase
|
||||
➕ Create First Book
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -193,7 +193,7 @@ export function PhaseExperiments({ phase, onBack }: PhaseExperimentsProps) {
|
||||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Back to Phases
|
||||
Back to Books
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -203,7 +203,7 @@ export function PhaseExperiments({ phase, onBack }: PhaseExperimentsProps) {
|
||||
{phase.description && (
|
||||
<p className="mt-2 text-gray-600">{phase.description}</p>
|
||||
)}
|
||||
<p className="mt-2 text-gray-600">Manage experiments within this phase</p>
|
||||
<p className="mt-2 text-gray-600">Manage experiments within this book</p>
|
||||
</div>
|
||||
{canManageExperiments && (
|
||||
<button
|
||||
@@ -417,9 +417,9 @@ export function PhaseExperiments({ phase, onBack }: PhaseExperimentsProps) {
|
||||
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<h3 className="mt-2 text-sm font-medium text-gray-900">No experiments found in this phase</h3>
|
||||
<h3 className="mt-2 text-sm font-medium text-gray-900">No experiments found in this book</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Get started by creating your first experiment in this phase.
|
||||
Get started by creating your first experiment in this book.
|
||||
</p>
|
||||
{canManageExperiments && (
|
||||
<div className="mt-6">
|
||||
|
||||
@@ -147,7 +147,7 @@ export function PhaseForm({ onSubmit, onCancel, loading = false }: PhaseFormProp
|
||||
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"
|
||||
placeholder="Optional description of this experiment book"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@ export function PhaseModal({ onClose, onPhaseCreated }: PhaseModalProps) {
|
||||
onPhaseCreated(newPhase)
|
||||
onClose()
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to create experiment phase')
|
||||
setError(err.message || 'Failed to create experiment book')
|
||||
console.error('Create phase error:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -35,7 +35,7 @@ export function PhaseModal({ onClose, onPhaseCreated }: PhaseModalProps) {
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-lg font-medium text-gray-900">
|
||||
Create New Experiment Phase
|
||||
Create New Experiment Book
|
||||
</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
|
||||
@@ -60,6 +60,8 @@ export interface Experiment {
|
||||
airdrying_id?: string | null
|
||||
cracking_id?: string | null
|
||||
shelling_id?: string | null
|
||||
ring_gap_inches?: number | null
|
||||
drum_rpm?: number | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
created_by: string
|
||||
@@ -170,6 +172,51 @@ export interface UpdateExperimentPhaseRequest {
|
||||
has_shelling?: boolean
|
||||
}
|
||||
|
||||
// Experiment-level phase config (one row per experiment per phase; stored in experiment_soaking, experiment_airdrying, experiment_cracking, experiment_shelling)
|
||||
export interface ExperimentSoakingConfig {
|
||||
id: string
|
||||
experiment_id: string
|
||||
soaking_duration_hr: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
created_by: string
|
||||
}
|
||||
|
||||
export interface ExperimentAirdryingConfig {
|
||||
id: string
|
||||
experiment_id: string
|
||||
duration_minutes: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
created_by: string
|
||||
}
|
||||
|
||||
export interface ExperimentCrackingConfig {
|
||||
id: string
|
||||
experiment_id: string
|
||||
machine_type_id: string
|
||||
plate_contact_frequency_hz?: number | null
|
||||
throughput_rate_pecans_sec?: number | null
|
||||
crush_amount_in?: number | null
|
||||
entry_exit_height_diff_in?: number | null
|
||||
motor_speed_hz?: number | null
|
||||
jig_displacement_inches?: number | null
|
||||
spring_stiffness_nm?: number | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
created_by: string
|
||||
}
|
||||
|
||||
export interface ExperimentShellingConfig {
|
||||
id: string
|
||||
experiment_id: string
|
||||
ring_gap_inches?: number | null
|
||||
drum_rpm?: number | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
created_by: string
|
||||
}
|
||||
|
||||
export interface CreateExperimentRequest {
|
||||
experiment_number: number
|
||||
reps_required: number
|
||||
@@ -177,6 +224,19 @@ export interface CreateExperimentRequest {
|
||||
results_status?: ResultsStatus
|
||||
completion_status?: boolean
|
||||
phase_id?: string
|
||||
// Phase config (stored in experiment_soaking, experiment_airdrying, experiment_cracking, experiment_shelling)
|
||||
soaking_duration_hr?: number
|
||||
air_drying_time_min?: number
|
||||
// Cracking: machine_type comes from book; params below are JC or Meyer specific
|
||||
plate_contact_frequency_hz?: number
|
||||
throughput_rate_pecans_sec?: number
|
||||
crush_amount_in?: number
|
||||
entry_exit_height_diff_in?: number
|
||||
motor_speed_hz?: number
|
||||
jig_displacement_inches?: number
|
||||
spring_stiffness_nm?: number
|
||||
ring_gap_inches?: number | null
|
||||
drum_rpm?: number | null
|
||||
}
|
||||
|
||||
export interface UpdateExperimentRequest {
|
||||
@@ -186,6 +246,17 @@ export interface UpdateExperimentRequest {
|
||||
results_status?: ResultsStatus
|
||||
completion_status?: boolean
|
||||
phase_id?: string
|
||||
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
|
||||
motor_speed_hz?: number
|
||||
jig_displacement_inches?: number
|
||||
spring_stiffness_nm?: number
|
||||
ring_gap_inches?: number | null
|
||||
drum_rpm?: number | null
|
||||
}
|
||||
|
||||
export interface CreateRepetitionRequest {
|
||||
@@ -560,12 +631,12 @@ export const userManagement = {
|
||||
}
|
||||
}
|
||||
|
||||
// Experiment phase management utility functions
|
||||
// Experiment book management (table: experiment_books)
|
||||
export const experimentPhaseManagement = {
|
||||
// Get all experiment phases
|
||||
// Get all experiment books
|
||||
async getAllExperimentPhases(): Promise<ExperimentPhase[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('experiment_phases')
|
||||
.from('experiment_books')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false })
|
||||
|
||||
@@ -573,10 +644,10 @@ export const experimentPhaseManagement = {
|
||||
return data
|
||||
},
|
||||
|
||||
// Get experiment phase by ID
|
||||
// Get experiment book by ID
|
||||
async getExperimentPhaseById(id: string): Promise<ExperimentPhase | null> {
|
||||
const { data, error } = await supabase
|
||||
.from('experiment_phases')
|
||||
.from('experiment_books')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single()
|
||||
@@ -588,13 +659,13 @@ export const experimentPhaseManagement = {
|
||||
return data
|
||||
},
|
||||
|
||||
// Create a new experiment phase
|
||||
// Create a new experiment book
|
||||
async createExperimentPhase(phaseData: CreateExperimentPhaseRequest): Promise<ExperimentPhase> {
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser()
|
||||
if (authError || !user) throw new Error('User not authenticated')
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('experiment_phases')
|
||||
.from('experiment_books')
|
||||
.insert({
|
||||
...phaseData,
|
||||
created_by: user.id
|
||||
@@ -606,10 +677,10 @@ export const experimentPhaseManagement = {
|
||||
return data
|
||||
},
|
||||
|
||||
// Update an experiment phase
|
||||
// Update an experiment book
|
||||
async updateExperimentPhase(id: string, updates: UpdateExperimentPhaseRequest): Promise<ExperimentPhase> {
|
||||
const { data, error } = await supabase
|
||||
.from('experiment_phases')
|
||||
.from('experiment_books')
|
||||
.update(updates)
|
||||
.eq('id', id)
|
||||
.select()
|
||||
@@ -619,10 +690,10 @@ export const experimentPhaseManagement = {
|
||||
return data
|
||||
},
|
||||
|
||||
// Delete an experiment phase
|
||||
// Delete an experiment book
|
||||
async deleteExperimentPhase(id: string): Promise<void> {
|
||||
const { error } = await supabase
|
||||
.from('experiment_phases')
|
||||
.from('experiment_books')
|
||||
.delete()
|
||||
.eq('id', id)
|
||||
|
||||
@@ -670,33 +741,170 @@ export const experimentManagement = {
|
||||
return data
|
||||
},
|
||||
|
||||
// Create a new experiment
|
||||
// Get experiment with its phase config (soaking, airdrying, cracking, shelling) for edit form
|
||||
async getExperimentWithPhaseConfig(id: string): Promise<(Experiment & {
|
||||
soaking?: ExperimentSoakingConfig | null
|
||||
airdrying?: ExperimentAirdryingConfig | null
|
||||
cracking?: ExperimentCrackingConfig | null
|
||||
shelling?: ExperimentShellingConfig | null
|
||||
}) | null> {
|
||||
const experiment = await this.getExperimentById(id)
|
||||
if (!experiment) return null
|
||||
|
||||
const [soakingRes, airdryingRes, crackingRes, shellingRes] = await Promise.all([
|
||||
supabase.from('experiment_soaking').select('*').eq('experiment_id', id).maybeSingle(),
|
||||
supabase.from('experiment_airdrying').select('*').eq('experiment_id', id).maybeSingle(),
|
||||
supabase.from('experiment_cracking').select('*').eq('experiment_id', id).maybeSingle(),
|
||||
supabase.from('experiment_shelling').select('*').eq('experiment_id', id).maybeSingle()
|
||||
])
|
||||
if (soakingRes.error) throw soakingRes.error
|
||||
if (airdryingRes.error) throw airdryingRes.error
|
||||
if (crackingRes.error) throw crackingRes.error
|
||||
if (shellingRes.error) throw shellingRes.error
|
||||
|
||||
return {
|
||||
...experiment,
|
||||
soaking: soakingRes.data ?? null,
|
||||
airdrying: airdryingRes.data ?? null,
|
||||
cracking: crackingRes.data ?? null,
|
||||
shelling: shellingRes.data ?? null
|
||||
}
|
||||
},
|
||||
|
||||
// Create a new experiment and its phase config rows (experiment_soaking, experiment_airdrying, experiment_cracking, experiment_shelling)
|
||||
async createExperiment(experimentData: CreateExperimentRequest): Promise<Experiment> {
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser()
|
||||
if (authError || !user) throw new Error('User not authenticated')
|
||||
|
||||
const { data, error } = await supabase
|
||||
const phaseId = experimentData.phase_id
|
||||
const corePayload = {
|
||||
experiment_number: experimentData.experiment_number,
|
||||
reps_required: experimentData.reps_required,
|
||||
weight_per_repetition_lbs: experimentData.weight_per_repetition_lbs,
|
||||
results_status: experimentData.results_status ?? 'valid',
|
||||
completion_status: experimentData.completion_status ?? false,
|
||||
phase_id: phaseId,
|
||||
created_by: user.id
|
||||
}
|
||||
// phase_id required for phase configs
|
||||
if (!phaseId) {
|
||||
const { data, error } = await supabase.from('experiments').insert(corePayload).select().single()
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
|
||||
const { data: experiment, error } = await supabase
|
||||
.from('experiments')
|
||||
.insert({
|
||||
...experimentData,
|
||||
created_by: user.id
|
||||
})
|
||||
.insert(corePayload)
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
|
||||
const book = await experimentPhaseManagement.getExperimentPhaseById(phaseId)
|
||||
if (!book) return experiment
|
||||
|
||||
if (book.has_soaking && experimentData.soaking_duration_hr != null) {
|
||||
await supabase.from('experiment_soaking').insert({
|
||||
experiment_id: experiment.id,
|
||||
soaking_duration_hr: experimentData.soaking_duration_hr,
|
||||
created_by: user.id
|
||||
})
|
||||
}
|
||||
if (book.has_airdrying && experimentData.air_drying_time_min != null) {
|
||||
await supabase.from('experiment_airdrying').insert({
|
||||
experiment_id: experiment.id,
|
||||
duration_minutes: experimentData.air_drying_time_min,
|
||||
created_by: user.id
|
||||
})
|
||||
}
|
||||
if (book.has_cracking && book.cracking_machine_type_id) {
|
||||
const crackPayload: Record<string, unknown> = {
|
||||
experiment_id: experiment.id,
|
||||
machine_type_id: book.cracking_machine_type_id,
|
||||
created_by: user.id
|
||||
}
|
||||
if (experimentData.plate_contact_frequency_hz != null) crackPayload.plate_contact_frequency_hz = experimentData.plate_contact_frequency_hz
|
||||
if (experimentData.throughput_rate_pecans_sec != null) crackPayload.throughput_rate_pecans_sec = experimentData.throughput_rate_pecans_sec
|
||||
if (experimentData.crush_amount_in != null) crackPayload.crush_amount_in = experimentData.crush_amount_in
|
||||
if (experimentData.entry_exit_height_diff_in != null) crackPayload.entry_exit_height_diff_in = experimentData.entry_exit_height_diff_in
|
||||
if (experimentData.motor_speed_hz != null) crackPayload.motor_speed_hz = experimentData.motor_speed_hz
|
||||
if (experimentData.jig_displacement_inches != null) crackPayload.jig_displacement_inches = experimentData.jig_displacement_inches
|
||||
if (experimentData.spring_stiffness_nm != null) crackPayload.spring_stiffness_nm = experimentData.spring_stiffness_nm
|
||||
await supabase.from('experiment_cracking').insert(crackPayload)
|
||||
}
|
||||
if (book.has_shelling && (experimentData.ring_gap_inches != null || experimentData.drum_rpm != null)) {
|
||||
await supabase.from('experiment_shelling').insert({
|
||||
experiment_id: experiment.id,
|
||||
ring_gap_inches: experimentData.ring_gap_inches ?? null,
|
||||
drum_rpm: experimentData.drum_rpm ?? null,
|
||||
created_by: user.id
|
||||
})
|
||||
}
|
||||
|
||||
return experiment
|
||||
},
|
||||
|
||||
// Update an experiment
|
||||
// Update an experiment and upsert its phase config rows
|
||||
async updateExperiment(id: string, updates: UpdateExperimentRequest): Promise<Experiment> {
|
||||
const { data, error } = await supabase
|
||||
.from('experiments')
|
||||
.update(updates)
|
||||
.eq('id', id)
|
||||
.select()
|
||||
.single()
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser()
|
||||
if (authError || !user) throw new Error('User not authenticated')
|
||||
|
||||
const coreKeys = ['experiment_number', 'reps_required', 'weight_per_repetition_lbs', 'results_status', 'completion_status', 'phase_id'] as const
|
||||
const coreUpdates: Partial<UpdateExperimentRequest> = {}
|
||||
for (const k of coreKeys) {
|
||||
if (updates[k] !== undefined) coreUpdates[k] = updates[k]
|
||||
}
|
||||
if (Object.keys(coreUpdates).length > 0) {
|
||||
const { data, error } = await supabase.from('experiments').update(coreUpdates).eq('id', id).select().single()
|
||||
if (error) throw error
|
||||
}
|
||||
|
||||
if (updates.soaking_duration_hr !== undefined) {
|
||||
const { data: existing } = await supabase.from('experiment_soaking').select('id').eq('experiment_id', id).maybeSingle()
|
||||
if (existing) {
|
||||
await supabase.from('experiment_soaking').update({ soaking_duration_hr: updates.soaking_duration_hr, updated_at: new Date().toISOString() }).eq('experiment_id', id)
|
||||
} else {
|
||||
await supabase.from('experiment_soaking').insert({ experiment_id: id, soaking_duration_hr: updates.soaking_duration_hr, created_by: user.id })
|
||||
}
|
||||
}
|
||||
if (updates.air_drying_time_min !== undefined) {
|
||||
const { data: existing } = await supabase.from('experiment_airdrying').select('id').eq('experiment_id', id).maybeSingle()
|
||||
if (existing) {
|
||||
await supabase.from('experiment_airdrying').update({ duration_minutes: updates.air_drying_time_min, updated_at: new Date().toISOString() }).eq('experiment_id', id)
|
||||
} else {
|
||||
await supabase.from('experiment_airdrying').insert({ experiment_id: id, duration_minutes: updates.air_drying_time_min, created_by: user.id })
|
||||
}
|
||||
}
|
||||
const crackKeys = ['plate_contact_frequency_hz', 'throughput_rate_pecans_sec', 'crush_amount_in', 'entry_exit_height_diff_in', 'motor_speed_hz', 'jig_displacement_inches', 'spring_stiffness_nm'] as const
|
||||
const hasCrackUpdates = crackKeys.some(k => updates[k] !== undefined)
|
||||
if (hasCrackUpdates) {
|
||||
const { data: existing } = await supabase.from('experiment_cracking').select('id').eq('experiment_id', id).maybeSingle()
|
||||
const crackPayload: Record<string, unknown> = {}
|
||||
crackKeys.forEach(k => { if (updates[k] !== undefined) crackPayload[k] = updates[k] })
|
||||
if (Object.keys(crackPayload).length > 0) {
|
||||
if (existing) {
|
||||
await supabase.from('experiment_cracking').update({ ...crackPayload, updated_at: new Date().toISOString() }).eq('experiment_id', id)
|
||||
} else {
|
||||
const exp = await this.getExperimentById(id)
|
||||
const book = exp?.phase_id ? await experimentPhaseManagement.getExperimentPhaseById(exp.phase_id) : null
|
||||
if (book?.has_cracking && book.cracking_machine_type_id) {
|
||||
await supabase.from('experiment_cracking').insert({ experiment_id: id, machine_type_id: book.cracking_machine_type_id, ...crackPayload, created_by: user.id })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (updates.ring_gap_inches !== undefined || updates.drum_rpm !== undefined) {
|
||||
const { data: existing } = await supabase.from('experiment_shelling').select('id').eq('experiment_id', id).maybeSingle()
|
||||
const shellPayload = { ring_gap_inches: updates.ring_gap_inches ?? null, drum_rpm: updates.drum_rpm ?? null }
|
||||
if (existing) {
|
||||
await supabase.from('experiment_shelling').update({ ...shellPayload, updated_at: new Date().toISOString() }).eq('experiment_id', id)
|
||||
} else {
|
||||
await supabase.from('experiment_shelling').insert({ experiment_id: id, ...shellPayload, created_by: user.id })
|
||||
}
|
||||
}
|
||||
|
||||
const { data, error } = await supabase.from('experiments').select('*').eq('id', id).single()
|
||||
if (error) throw error
|
||||
return data
|
||||
},
|
||||
@@ -739,13 +947,16 @@ export const experimentManagement = {
|
||||
|
||||
|
||||
|
||||
// Check if experiment number is unique
|
||||
async isExperimentNumberUnique(experimentNumber: number, excludeId?: string): Promise<boolean> {
|
||||
// Check if experiment number is unique within the same phase (experiment_number + phase_id must be unique)
|
||||
async isExperimentNumberUnique(experimentNumber: number, phaseId?: string, excludeId?: string): Promise<boolean> {
|
||||
let query = supabase
|
||||
.from('experiments')
|
||||
.select('id')
|
||||
.eq('experiment_number', experimentNumber)
|
||||
|
||||
if (phaseId) {
|
||||
query = query.eq('phase_id', phaseId)
|
||||
}
|
||||
if (excludeId) {
|
||||
query = query.neq('id', excludeId)
|
||||
}
|
||||
|
||||
@@ -64,9 +64,9 @@ supabase gen types typescript --local > management-dashboard-web-app/src/types/s
|
||||
|
||||
## Seed Data
|
||||
|
||||
Seed files are run automatically after migrations when using docker-compose. They populate the database with initial data:
|
||||
- `seed_01_users.sql`: Creates admin user and initial user profiles
|
||||
- `seed_02_phase2_experiments.sql`: Creates initial experiment data
|
||||
Seed files are run automatically after migrations when using `supabase db reset` (see `config.toml` → `[db.seed]` → `sql_paths`). Currently only user seed is enabled:
|
||||
- `seed_01_users.sql`: Creates admin user and initial user profiles (enabled)
|
||||
- `seed_02_phase2_experiments.sql`: Initial experiment data (temporarily disabled; add back to `sql_paths` in `config.toml` to re-enable)
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
@@ -57,7 +57,9 @@ 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_01_users.sql", "./seed_02_phase2_experiments.sql"]
|
||||
# Temporarily only user seed; other seeds suppressed.
|
||||
sql_paths = ["./seed_01_users.sql"]
|
||||
# sql_paths = ["./seed_01_users.sql", "./seed_02_phase2_experiments.sql"]
|
||||
# , "./seed_04_phase2_jc_experiments.sql", "./seed_05_meyer_experiments.sql"]
|
||||
|
||||
[db.network_restrictions]
|
||||
|
||||
9
supabase/migrations/00015_experiment_shelling_params.sql
Normal file
9
supabase/migrations/00015_experiment_shelling_params.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
-- Add experiment-level shelling parameters (defaults for repetitions)
|
||||
-- These match the shelling table attributes: ring_gap_inches, drum_rpm
|
||||
|
||||
ALTER TABLE public.experiments
|
||||
ADD COLUMN IF NOT EXISTS ring_gap_inches NUMERIC(6,2) CHECK (ring_gap_inches IS NULL OR ring_gap_inches > 0),
|
||||
ADD COLUMN IF NOT EXISTS drum_rpm INTEGER CHECK (drum_rpm IS NULL OR drum_rpm > 0);
|
||||
|
||||
COMMENT ON COLUMN public.experiments.ring_gap_inches IS 'Default space (inches) between sheller rings for this experiment';
|
||||
COMMENT ON COLUMN public.experiments.drum_rpm IS 'Default sheller drum revolutions per minute for this experiment';
|
||||
@@ -0,0 +1,399 @@
|
||||
-- Rename table experiment_phases to experiment_books
|
||||
-- This migration renames the table and updates all dependent objects (views, functions, triggers, indexes, RLS).
|
||||
|
||||
-- =============================================
|
||||
-- 1. RENAME TABLE
|
||||
-- =============================================
|
||||
|
||||
ALTER TABLE public.experiment_phases RENAME TO experiment_books;
|
||||
|
||||
-- =============================================
|
||||
-- 2. RENAME TRIGGER
|
||||
-- =============================================
|
||||
|
||||
DROP TRIGGER IF EXISTS set_updated_at_experiment_phases ON public.experiment_books;
|
||||
CREATE TRIGGER set_updated_at_experiment_books
|
||||
BEFORE UPDATE ON public.experiment_books
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.handle_updated_at();
|
||||
|
||||
-- =============================================
|
||||
-- 3. RENAME CONSTRAINT
|
||||
-- =============================================
|
||||
|
||||
ALTER TABLE public.experiment_books
|
||||
RENAME CONSTRAINT ck_experiment_phases_machine_required_when_cracking
|
||||
TO ck_experiment_books_machine_required_when_cracking;
|
||||
|
||||
-- =============================================
|
||||
-- 4. RENAME INDEXES
|
||||
-- =============================================
|
||||
|
||||
ALTER INDEX IF EXISTS public.idx_experiment_phases_name RENAME TO idx_experiment_books_name;
|
||||
ALTER INDEX IF EXISTS public.idx_experiment_phases_cracking_machine_type_id RENAME TO idx_experiment_books_cracking_machine_type_id;
|
||||
|
||||
-- =============================================
|
||||
-- 5. RLS POLICIES (drop old, create new with updated names)
|
||||
-- =============================================
|
||||
|
||||
DROP POLICY IF EXISTS "Experiment phases are viewable by authenticated users" ON public.experiment_books;
|
||||
DROP POLICY IF EXISTS "Experiment phases are insertable by authenticated users" ON public.experiment_books;
|
||||
DROP POLICY IF EXISTS "Experiment phases are updatable by authenticated users" ON public.experiment_books;
|
||||
DROP POLICY IF EXISTS "Experiment phases are deletable by authenticated users" ON public.experiment_books;
|
||||
|
||||
CREATE POLICY "Experiment books are viewable by authenticated users" ON public.experiment_books
|
||||
FOR SELECT USING (auth.role() = 'authenticated');
|
||||
|
||||
CREATE POLICY "Experiment books are insertable by authenticated users" ON public.experiment_books
|
||||
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
|
||||
|
||||
CREATE POLICY "Experiment books are updatable by authenticated users" ON public.experiment_books
|
||||
FOR UPDATE USING (auth.role() = 'authenticated');
|
||||
|
||||
CREATE POLICY "Experiment books are deletable by authenticated users" ON public.experiment_books
|
||||
FOR DELETE USING (auth.role() = 'authenticated');
|
||||
|
||||
-- =============================================
|
||||
-- 6. UPDATE FUNCTION: create_phase_executions_for_repetition (references experiment_phases)
|
||||
-- =============================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION create_phase_executions_for_repetition()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
exp_phase_config RECORD;
|
||||
phase_type_list TEXT[] := ARRAY[]::TEXT[];
|
||||
phase_name TEXT;
|
||||
BEGIN
|
||||
SELECT
|
||||
ep.has_soaking,
|
||||
ep.has_airdrying,
|
||||
ep.has_cracking,
|
||||
ep.has_shelling,
|
||||
ep.cracking_machine_type_id
|
||||
INTO exp_phase_config
|
||||
FROM public.experiments e
|
||||
JOIN public.experiment_books ep ON e.phase_id = ep.id
|
||||
WHERE e.id = NEW.experiment_id;
|
||||
|
||||
IF exp_phase_config.has_soaking THEN
|
||||
phase_type_list := array_append(phase_type_list, 'soaking');
|
||||
END IF;
|
||||
IF exp_phase_config.has_airdrying THEN
|
||||
phase_type_list := array_append(phase_type_list, 'airdrying');
|
||||
END IF;
|
||||
IF exp_phase_config.has_cracking THEN
|
||||
phase_type_list := array_append(phase_type_list, 'cracking');
|
||||
END IF;
|
||||
IF exp_phase_config.has_shelling THEN
|
||||
phase_type_list := array_append(phase_type_list, 'shelling');
|
||||
END IF;
|
||||
|
||||
FOREACH phase_name IN ARRAY phase_type_list
|
||||
LOOP
|
||||
INSERT INTO public.experiment_phase_executions (
|
||||
repetition_id,
|
||||
phase_type,
|
||||
scheduled_start_time,
|
||||
status,
|
||||
created_by,
|
||||
soaking_duration_minutes,
|
||||
duration_minutes,
|
||||
machine_type_id
|
||||
)
|
||||
VALUES (
|
||||
NEW.id,
|
||||
phase_name,
|
||||
NOW(),
|
||||
'pending',
|
||||
NEW.created_by,
|
||||
NULL,
|
||||
NULL,
|
||||
CASE WHEN phase_name = 'cracking'
|
||||
THEN exp_phase_config.cracking_machine_type_id
|
||||
ELSE NULL END
|
||||
);
|
||||
END LOOP;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- =============================================
|
||||
-- 7. UPDATE FUNCTION: create_sample_experiment_phases (INSERT into experiment_books)
|
||||
-- =============================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.create_sample_experiment_phases()
|
||||
RETURNS VOID AS $$
|
||||
DECLARE
|
||||
jc_cracker_id UUID;
|
||||
meyer_cracker_id UUID;
|
||||
BEGIN
|
||||
SELECT id INTO jc_cracker_id FROM public.machine_types WHERE name = 'JC Cracker';
|
||||
SELECT id INTO meyer_cracker_id FROM public.machine_types WHERE name = 'Meyer Cracker';
|
||||
|
||||
INSERT INTO public.experiment_books (name, description, has_soaking, has_airdrying, has_cracking, has_shelling, cracking_machine_type_id, created_by) VALUES
|
||||
('Full Process - JC Cracker', 'Complete pecan processing with JC Cracker', true, true, true, true, jc_cracker_id, (SELECT id FROM public.user_profiles LIMIT 1)),
|
||||
('Full Process - Meyer Cracker', 'Complete pecan processing with Meyer Cracker', true, true, true, true, meyer_cracker_id, (SELECT id FROM public.user_profiles LIMIT 1)),
|
||||
('Cracking Only - JC Cracker', 'JC Cracker cracking process only', false, false, true, false, jc_cracker_id, (SELECT id FROM public.user_profiles LIMIT 1)),
|
||||
('Cracking Only - Meyer Cracker', 'Meyer Cracker cracking process only', false, false, true, false, meyer_cracker_id, (SELECT id FROM public.user_profiles LIMIT 1))
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- =============================================
|
||||
-- 8. UPDATE VIEWS (from 00014 - experiments_with_phases, repetitions_with_phases, experiments_with_all_reps_and_phases, get_experiment_with_reps_and_phases)
|
||||
-- =============================================
|
||||
|
||||
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.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,
|
||||
er.id as first_repetition_id,
|
||||
er.repetition_number as first_repetition_number,
|
||||
soaking_e.id as soaking_id,
|
||||
soaking_e.scheduled_start_time as soaking_scheduled_start,
|
||||
soaking_e.actual_start_time as soaking_actual_start,
|
||||
soaking_e.soaking_duration_minutes,
|
||||
soaking_e.scheduled_end_time as soaking_scheduled_end,
|
||||
soaking_e.actual_end_time as soaking_actual_end,
|
||||
airdrying_e.id as airdrying_id,
|
||||
airdrying_e.scheduled_start_time as airdrying_scheduled_start,
|
||||
airdrying_e.actual_start_time as airdrying_actual_start,
|
||||
airdrying_e.duration_minutes as airdrying_duration,
|
||||
airdrying_e.scheduled_end_time as airdrying_scheduled_end,
|
||||
airdrying_e.actual_end_time as airdrying_actual_end,
|
||||
cracking_e.id as cracking_id,
|
||||
cracking_e.scheduled_start_time as cracking_scheduled_start,
|
||||
cracking_e.actual_start_time as cracking_actual_start,
|
||||
cracking_e.actual_end_time as cracking_actual_end,
|
||||
mt.name as machine_type_name,
|
||||
shelling_e.id as shelling_id,
|
||||
shelling_e.scheduled_start_time as shelling_scheduled_start,
|
||||
shelling_e.actual_start_time as shelling_actual_start,
|
||||
shelling_e.actual_end_time as shelling_actual_end
|
||||
FROM public.experiments e
|
||||
LEFT JOIN public.experiment_books ep ON e.phase_id = ep.id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT id, repetition_number
|
||||
FROM public.experiment_repetitions
|
||||
WHERE experiment_id = e.id
|
||||
ORDER BY repetition_number
|
||||
LIMIT 1
|
||||
) er ON true
|
||||
LEFT JOIN public.experiment_phase_executions soaking_e
|
||||
ON soaking_e.repetition_id = er.id AND soaking_e.phase_type = 'soaking'
|
||||
LEFT JOIN public.experiment_phase_executions airdrying_e
|
||||
ON airdrying_e.repetition_id = er.id AND airdrying_e.phase_type = 'airdrying'
|
||||
LEFT JOIN public.experiment_phase_executions cracking_e
|
||||
ON cracking_e.repetition_id = er.id AND cracking_e.phase_type = 'cracking'
|
||||
LEFT JOIN public.experiment_phase_executions shelling_e
|
||||
ON shelling_e.repetition_id = er.id AND shelling_e.phase_type = 'shelling'
|
||||
LEFT JOIN public.machine_types mt ON cracking_e.machine_type_id = mt.id;
|
||||
|
||||
CREATE OR REPLACE VIEW public.repetitions_with_phases AS
|
||||
SELECT
|
||||
er.id,
|
||||
er.experiment_id,
|
||||
er.repetition_number,
|
||||
er.status,
|
||||
er.created_at,
|
||||
er.updated_at,
|
||||
er.created_by,
|
||||
e.experiment_number,
|
||||
e.phase_id,
|
||||
e.weight_per_repetition_lbs,
|
||||
ep.name as phase_name,
|
||||
ep.has_soaking,
|
||||
ep.has_airdrying,
|
||||
ep.has_cracking,
|
||||
ep.has_shelling,
|
||||
soaking_e.scheduled_start_time as soaking_scheduled_start,
|
||||
soaking_e.actual_start_time as soaking_actual_start,
|
||||
soaking_e.soaking_duration_minutes,
|
||||
soaking_e.scheduled_end_time as soaking_scheduled_end,
|
||||
soaking_e.actual_end_time as soaking_actual_end,
|
||||
airdrying_e.scheduled_start_time as airdrying_scheduled_start,
|
||||
airdrying_e.actual_start_time as airdrying_actual_start,
|
||||
airdrying_e.duration_minutes as airdrying_duration,
|
||||
airdrying_e.scheduled_end_time as airdrying_scheduled_end,
|
||||
airdrying_e.actual_end_time as airdrying_actual_end,
|
||||
cracking_e.scheduled_start_time as cracking_scheduled_start,
|
||||
cracking_e.actual_start_time as cracking_actual_start,
|
||||
cracking_e.actual_end_time as cracking_actual_end,
|
||||
mt.name as machine_type_name,
|
||||
shelling_e.scheduled_start_time as shelling_scheduled_start,
|
||||
shelling_e.actual_start_time as shelling_actual_start,
|
||||
shelling_e.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_books ep ON e.phase_id = ep.id
|
||||
LEFT JOIN public.experiment_phase_executions soaking_e
|
||||
ON er.id = soaking_e.repetition_id AND soaking_e.phase_type = 'soaking'
|
||||
LEFT JOIN public.experiment_phase_executions airdrying_e
|
||||
ON er.id = airdrying_e.repetition_id AND airdrying_e.phase_type = 'airdrying'
|
||||
LEFT JOIN public.experiment_phase_executions cracking_e
|
||||
ON er.id = cracking_e.repetition_id AND cracking_e.phase_type = 'cracking'
|
||||
LEFT JOIN public.experiment_phase_executions shelling_e
|
||||
ON er.id = shelling_e.repetition_id AND shelling_e.phase_type = 'shelling'
|
||||
LEFT JOIN public.machine_types mt ON cracking_e.machine_type_id = mt.id;
|
||||
|
||||
-- experiments_with_all_reps_and_phases
|
||||
CREATE OR REPLACE VIEW public.experiments_with_all_reps_and_phases AS
|
||||
SELECT
|
||||
e.id as experiment_id,
|
||||
e.experiment_number,
|
||||
e.reps_required,
|
||||
e.weight_per_repetition_lbs,
|
||||
e.results_status,
|
||||
e.completion_status,
|
||||
e.phase_id,
|
||||
e.created_at as experiment_created_at,
|
||||
e.updated_at as experiment_updated_at,
|
||||
e.created_by as experiment_created_by,
|
||||
ep.name as phase_name,
|
||||
ep.description as phase_description,
|
||||
ep.has_soaking,
|
||||
ep.has_airdrying,
|
||||
ep.has_cracking,
|
||||
ep.has_shelling,
|
||||
ep.cracking_machine_type_id as phase_cracking_machine_type_id,
|
||||
er.id as repetition_id,
|
||||
er.repetition_number,
|
||||
er.status as repetition_status,
|
||||
er.scheduled_date,
|
||||
er.created_at as repetition_created_at,
|
||||
er.updated_at as repetition_updated_at,
|
||||
er.created_by as repetition_created_by,
|
||||
soaking_e.id as soaking_execution_id,
|
||||
soaking_e.scheduled_start_time as soaking_scheduled_start,
|
||||
soaking_e.actual_start_time as soaking_actual_start,
|
||||
soaking_e.soaking_duration_minutes,
|
||||
soaking_e.scheduled_end_time as soaking_scheduled_end,
|
||||
soaking_e.actual_end_time as soaking_actual_end,
|
||||
soaking_e.status as soaking_status,
|
||||
airdrying_e.id as airdrying_execution_id,
|
||||
airdrying_e.scheduled_start_time as airdrying_scheduled_start,
|
||||
airdrying_e.actual_start_time as airdrying_actual_start,
|
||||
airdrying_e.duration_minutes as airdrying_duration_minutes,
|
||||
airdrying_e.scheduled_end_time as airdrying_scheduled_end,
|
||||
airdrying_e.actual_end_time as airdrying_actual_end,
|
||||
airdrying_e.status as airdrying_status,
|
||||
cracking_e.id as cracking_execution_id,
|
||||
cracking_e.scheduled_start_time as cracking_scheduled_start,
|
||||
cracking_e.actual_start_time as cracking_actual_start,
|
||||
cracking_e.scheduled_end_time as cracking_scheduled_end,
|
||||
cracking_e.actual_end_time as cracking_actual_end,
|
||||
cracking_e.machine_type_id as cracking_machine_type_id,
|
||||
cracking_e.status as cracking_status,
|
||||
mt.name as machine_type_name,
|
||||
shelling_e.id as shelling_execution_id,
|
||||
shelling_e.scheduled_start_time as shelling_scheduled_start,
|
||||
shelling_e.actual_start_time as shelling_actual_start,
|
||||
shelling_e.scheduled_end_time as shelling_scheduled_end,
|
||||
shelling_e.actual_end_time as shelling_actual_end,
|
||||
shelling_e.status as shelling_status
|
||||
FROM public.experiments e
|
||||
LEFT JOIN public.experiment_books ep ON e.phase_id = ep.id
|
||||
LEFT JOIN public.experiment_repetitions er ON er.experiment_id = e.id
|
||||
LEFT JOIN public.experiment_phase_executions soaking_e
|
||||
ON soaking_e.repetition_id = er.id AND soaking_e.phase_type = 'soaking'
|
||||
LEFT JOIN public.experiment_phase_executions airdrying_e
|
||||
ON airdrying_e.repetition_id = er.id AND airdrying_e.phase_type = 'airdrying'
|
||||
LEFT JOIN public.experiment_phase_executions cracking_e
|
||||
ON cracking_e.repetition_id = er.id AND cracking_e.phase_type = 'cracking'
|
||||
LEFT JOIN public.experiment_phase_executions shelling_e
|
||||
ON shelling_e.repetition_id = er.id AND shelling_e.phase_type = 'shelling'
|
||||
LEFT JOIN public.machine_types mt ON cracking_e.machine_type_id = mt.id
|
||||
ORDER BY e.experiment_number, er.repetition_number;
|
||||
|
||||
-- get_experiment_with_reps_and_phases function
|
||||
CREATE OR REPLACE FUNCTION public.get_experiment_with_reps_and_phases(p_experiment_id UUID)
|
||||
RETURNS TABLE (
|
||||
experiment_id UUID,
|
||||
experiment_number INTEGER,
|
||||
phase_name TEXT,
|
||||
repetitions JSONB
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
e.id,
|
||||
e.experiment_number,
|
||||
ep.name,
|
||||
COALESCE(
|
||||
jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'repetition_id', er.id,
|
||||
'repetition_number', er.repetition_number,
|
||||
'status', er.status,
|
||||
'scheduled_date', er.scheduled_date,
|
||||
'soaking', jsonb_build_object(
|
||||
'scheduled_start', soaking_e.scheduled_start_time,
|
||||
'actual_start', soaking_e.actual_start_time,
|
||||
'duration_minutes', soaking_e.soaking_duration_minutes,
|
||||
'scheduled_end', soaking_e.scheduled_end_time,
|
||||
'actual_end', soaking_e.actual_end_time,
|
||||
'status', soaking_e.status
|
||||
),
|
||||
'airdrying', jsonb_build_object(
|
||||
'scheduled_start', airdrying_e.scheduled_start_time,
|
||||
'actual_start', airdrying_e.actual_start_time,
|
||||
'duration_minutes', airdrying_e.duration_minutes,
|
||||
'scheduled_end', airdrying_e.scheduled_end_time,
|
||||
'actual_end', airdrying_e.actual_end_time,
|
||||
'status', airdrying_e.status
|
||||
),
|
||||
'cracking', jsonb_build_object(
|
||||
'scheduled_start', cracking_e.scheduled_start_time,
|
||||
'actual_start', cracking_e.actual_start_time,
|
||||
'scheduled_end', cracking_e.scheduled_end_time,
|
||||
'actual_end', cracking_e.actual_end_time,
|
||||
'machine_type_id', cracking_e.machine_type_id,
|
||||
'machine_type_name', mt.name,
|
||||
'status', cracking_e.status
|
||||
),
|
||||
'shelling', jsonb_build_object(
|
||||
'scheduled_start', shelling_e.scheduled_start_time,
|
||||
'actual_start', shelling_e.actual_start_time,
|
||||
'scheduled_end', shelling_e.scheduled_end_time,
|
||||
'actual_end', shelling_e.actual_end_time,
|
||||
'status', shelling_e.status
|
||||
)
|
||||
)
|
||||
ORDER BY er.repetition_number
|
||||
),
|
||||
'[]'::jsonb
|
||||
) as repetitions
|
||||
FROM public.experiments e
|
||||
LEFT JOIN public.experiment_books ep ON e.phase_id = ep.id
|
||||
LEFT JOIN public.experiment_repetitions er ON er.experiment_id = e.id
|
||||
LEFT JOIN public.experiment_phase_executions soaking_e
|
||||
ON soaking_e.repetition_id = er.id AND soaking_e.phase_type = 'soaking'
|
||||
LEFT JOIN public.experiment_phase_executions airdrying_e
|
||||
ON airdrying_e.repetition_id = er.id AND airdrying_e.phase_type = 'airdrying'
|
||||
LEFT JOIN public.experiment_phase_executions cracking_e
|
||||
ON cracking_e.repetition_id = er.id AND cracking_e.phase_type = 'cracking'
|
||||
LEFT JOIN public.experiment_phase_executions shelling_e
|
||||
ON shelling_e.repetition_id = er.id AND shelling_e.phase_type = 'shelling'
|
||||
LEFT JOIN public.machine_types mt ON cracking_e.machine_type_id = mt.id
|
||||
WHERE e.id = p_experiment_id
|
||||
GROUP BY e.id, e.experiment_number, ep.name;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
GRANT SELECT ON public.experiments_with_all_reps_and_phases TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.get_experiment_with_reps_and_phases(UUID) TO authenticated;
|
||||
118
supabase/migrations/00017_experiment_phase_config_tables.sql
Normal file
118
supabase/migrations/00017_experiment_phase_config_tables.sql
Normal file
@@ -0,0 +1,118 @@
|
||||
-- Experiment-level phase config tables
|
||||
-- One row per experiment per phase; linked by experiment_id. Used when creating an experiment
|
||||
-- so soaking, airdrying, cracking, and shelling parameters are stored and can be applied to repetitions.
|
||||
|
||||
-- =============================================
|
||||
-- 1. EXPERIMENT_SOAKING (template for soaking phase)
|
||||
-- =============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.experiment_soaking (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
experiment_id UUID NOT NULL REFERENCES public.experiments(id) ON DELETE CASCADE,
|
||||
soaking_duration_hr DOUBLE PRECISION NOT NULL CHECK (soaking_duration_hr >= 0),
|
||||
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 unique_experiment_soaking_per_experiment UNIQUE (experiment_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_experiment_soaking_experiment_id ON public.experiment_soaking(experiment_id);
|
||||
GRANT ALL ON public.experiment_soaking TO authenticated;
|
||||
ALTER TABLE public.experiment_soaking ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "Experiment soaking config is viewable by authenticated" ON public.experiment_soaking FOR SELECT USING (auth.role() = 'authenticated');
|
||||
CREATE POLICY "Experiment soaking config is insertable by authenticated" ON public.experiment_soaking FOR INSERT WITH CHECK (auth.role() = 'authenticated');
|
||||
CREATE POLICY "Experiment soaking config is updatable by authenticated" ON public.experiment_soaking FOR UPDATE USING (auth.role() = 'authenticated');
|
||||
CREATE POLICY "Experiment soaking config is deletable by authenticated" ON public.experiment_soaking FOR DELETE USING (auth.role() = 'authenticated');
|
||||
|
||||
CREATE TRIGGER set_updated_at_experiment_soaking
|
||||
BEFORE UPDATE ON public.experiment_soaking
|
||||
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
||||
|
||||
-- =============================================
|
||||
-- 2. EXPERIMENT_AIRDRYING (template for airdrying phase)
|
||||
-- =============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.experiment_airdrying (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
experiment_id UUID NOT NULL REFERENCES public.experiments(id) ON DELETE CASCADE,
|
||||
duration_minutes INTEGER NOT NULL CHECK (duration_minutes >= 0),
|
||||
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 unique_experiment_airdrying_per_experiment UNIQUE (experiment_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_experiment_airdrying_experiment_id ON public.experiment_airdrying(experiment_id);
|
||||
GRANT ALL ON public.experiment_airdrying TO authenticated;
|
||||
ALTER TABLE public.experiment_airdrying ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "Experiment airdrying config is viewable by authenticated" ON public.experiment_airdrying FOR SELECT USING (auth.role() = 'authenticated');
|
||||
CREATE POLICY "Experiment airdrying config is insertable by authenticated" ON public.experiment_airdrying FOR INSERT WITH CHECK (auth.role() = 'authenticated');
|
||||
CREATE POLICY "Experiment airdrying config is updatable by authenticated" ON public.experiment_airdrying FOR UPDATE USING (auth.role() = 'authenticated');
|
||||
CREATE POLICY "Experiment airdrying config is deletable by authenticated" ON public.experiment_airdrying FOR DELETE USING (auth.role() = 'authenticated');
|
||||
|
||||
CREATE TRIGGER set_updated_at_experiment_airdrying
|
||||
BEFORE UPDATE ON public.experiment_airdrying
|
||||
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
||||
|
||||
-- =============================================
|
||||
-- 3. EXPERIMENT_CRACKING (template for cracking; supports JC and Meyer params)
|
||||
-- =============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.experiment_cracking (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
experiment_id UUID NOT NULL REFERENCES public.experiments(id) ON DELETE CASCADE,
|
||||
machine_type_id UUID NOT NULL REFERENCES public.machine_types(id) ON DELETE RESTRICT,
|
||||
-- JC Cracker parameters (nullable; used when machine is JC)
|
||||
plate_contact_frequency_hz DOUBLE PRECISION CHECK (plate_contact_frequency_hz IS NULL OR plate_contact_frequency_hz > 0),
|
||||
throughput_rate_pecans_sec DOUBLE PRECISION CHECK (throughput_rate_pecans_sec IS NULL OR throughput_rate_pecans_sec > 0),
|
||||
crush_amount_in DOUBLE PRECISION CHECK (crush_amount_in IS NULL OR crush_amount_in >= 0),
|
||||
entry_exit_height_diff_in DOUBLE PRECISION,
|
||||
-- Meyer Cracker parameters (nullable; used when machine is Meyer)
|
||||
motor_speed_hz DOUBLE PRECISION CHECK (motor_speed_hz IS NULL OR motor_speed_hz > 0),
|
||||
jig_displacement_inches DOUBLE PRECISION CHECK (jig_displacement_inches IS NULL OR jig_displacement_inches >= 0),
|
||||
spring_stiffness_nm DOUBLE PRECISION CHECK (spring_stiffness_nm IS NULL OR spring_stiffness_nm > 0),
|
||||
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 unique_experiment_cracking_per_experiment UNIQUE (experiment_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_experiment_cracking_experiment_id ON public.experiment_cracking(experiment_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_experiment_cracking_machine_type_id ON public.experiment_cracking(machine_type_id);
|
||||
GRANT ALL ON public.experiment_cracking TO authenticated;
|
||||
ALTER TABLE public.experiment_cracking ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "Experiment cracking config is viewable by authenticated" ON public.experiment_cracking FOR SELECT USING (auth.role() = 'authenticated');
|
||||
CREATE POLICY "Experiment cracking config is insertable by authenticated" ON public.experiment_cracking FOR INSERT WITH CHECK (auth.role() = 'authenticated');
|
||||
CREATE POLICY "Experiment cracking config is updatable by authenticated" ON public.experiment_cracking FOR UPDATE USING (auth.role() = 'authenticated');
|
||||
CREATE POLICY "Experiment cracking config is deletable by authenticated" ON public.experiment_cracking FOR DELETE USING (auth.role() = 'authenticated');
|
||||
|
||||
CREATE TRIGGER set_updated_at_experiment_cracking
|
||||
BEFORE UPDATE ON public.experiment_cracking
|
||||
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
||||
|
||||
-- =============================================
|
||||
-- 4. EXPERIMENT_SHELLING (template for shelling phase)
|
||||
-- =============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.experiment_shelling (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
experiment_id UUID NOT NULL REFERENCES public.experiments(id) ON DELETE CASCADE,
|
||||
ring_gap_inches NUMERIC(6,2) CHECK (ring_gap_inches IS NULL OR ring_gap_inches > 0),
|
||||
drum_rpm INTEGER CHECK (drum_rpm IS NULL OR drum_rpm > 0),
|
||||
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 unique_experiment_shelling_per_experiment UNIQUE (experiment_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_experiment_shelling_experiment_id ON public.experiment_shelling(experiment_id);
|
||||
GRANT ALL ON public.experiment_shelling TO authenticated;
|
||||
ALTER TABLE public.experiment_shelling ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "Experiment shelling config is viewable by authenticated" ON public.experiment_shelling FOR SELECT USING (auth.role() = 'authenticated');
|
||||
CREATE POLICY "Experiment shelling config is insertable by authenticated" ON public.experiment_shelling FOR INSERT WITH CHECK (auth.role() = 'authenticated');
|
||||
CREATE POLICY "Experiment shelling config is updatable by authenticated" ON public.experiment_shelling FOR UPDATE USING (auth.role() = 'authenticated');
|
||||
CREATE POLICY "Experiment shelling config is deletable by authenticated" ON public.experiment_shelling FOR DELETE USING (auth.role() = 'authenticated');
|
||||
|
||||
CREATE TRIGGER set_updated_at_experiment_shelling
|
||||
BEFORE UPDATE ON public.experiment_shelling
|
||||
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
||||
@@ -566,11 +566,11 @@ INSERT INTO public.machine_types (name, description, created_by) VALUES
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- =============================================
|
||||
-- 5. CREATE EXPERIMENT PHASES
|
||||
-- 5. CREATE EXPERIMENT BOOKS (table renamed from experiment_phases in migration 00016)
|
||||
-- =============================================
|
||||
|
||||
-- Create "Phase 2 of JC Experiments" phase
|
||||
INSERT INTO public.experiment_phases (name, description, has_soaking, has_airdrying, has_cracking, has_shelling, cracking_machine_type_id, created_by)
|
||||
-- Create "Phase 2 of JC Experiments" book
|
||||
INSERT INTO public.experiment_books (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',
|
||||
@@ -584,8 +584,8 @@ FROM public.user_profiles up
|
||||
WHERE up.email = 's.alireza.v@gmail.com'
|
||||
;
|
||||
|
||||
-- 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)
|
||||
-- Create "Post Workshop Meyer Experiments" book
|
||||
INSERT INTO public.experiment_books (name, description, has_soaking, has_airdrying, has_cracking, has_shelling, cracking_machine_type_id, created_by)
|
||||
SELECT
|
||||
'Post Workshop Meyer Experiments',
|
||||
'Post workshop Meyer Cracker experiments for pecan processing optimization',
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
-- ==============================================
|
||||
-- 6. CREATE EXPERIMENTS FOR PHASE 2
|
||||
-- ==============================================
|
||||
|
||||
-- TEMPORARILY DISABLED (see config.toml sql_paths). When re-enabling, replace
|
||||
-- all "experiment_phases" with "experiment_books" (table renamed in migration 00016).
|
||||
--
|
||||
-- This seed file creates experiments from phase_2_JC_experimental_run_sheet.csv
|
||||
-- Each experiment has 3 repetitions with specific parameters
|
||||
-- Experiment numbers are incremented by 1 (CSV 0-19 becomes DB 1-20)
|
||||
|
||||
Reference in New Issue
Block a user