Refactor Experiment components to support new experiment book structure
- Updated ExperimentForm to handle additional phase parameters and improved initial state management. - Modified ExperimentModal to fetch experiment data with phase configuration and ensure unique experiment numbers within the same phase. - Renamed references from "phases" to "books" across ExperimentPhases, PhaseExperiments, and related components for consistency with the new terminology. - Enhanced error handling and validation for new shelling parameters in ExperimentForm. - Updated Supabase interface definitions to reflect changes in experiment and phase data structures.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user