Files
usda-vision/management-dashboard-web-app/src/lib/supabase.ts

1670 lines
44 KiB
TypeScript
Executable File

import { createClient } from '@supabase/supabase-js'
// Supabase configuration from environment (Vite)
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
if (!supabaseUrl || !supabaseAnonKey) {
throw new Error('Supabase is not configured. Please set VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY in your environment (.env)')
}
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
// Database types for TypeScript
export type RoleName = 'admin' | 'conductor' | 'analyst' | 'data recorder'
export type UserStatus = 'active' | 'disabled'
export type ScheduleStatus = 'pending schedule' | 'scheduled' | 'canceled' | 'aborted'
export type ResultsStatus = 'valid' | 'invalid'
export interface User {
id: string
email: string
first_name?: string
last_name?: string
roles: RoleName[]
status: UserStatus
created_at: string
updated_at: string
}
export interface Role {
id: string
name: RoleName
description: string
created_at: string
}
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
}
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
}
// 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
repetition_id: string
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
repetition_id: string
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
weight_per_repetition_lbs: number
results_status?: ResultsStatus
completion_status?: boolean
phase_id?: string
}
export interface UpdateExperimentRequest {
experiment_number?: number
reps_required?: number
weight_per_repetition_lbs?: number
results_status?: ResultsStatus
completion_status?: boolean
phase_id?: string
}
export interface CreateRepetitionRequest {
experiment_id: string
repetition_number: number
scheduled_date?: string | null
}
export interface UpdateRepetitionRequest {
scheduled_date?: string | null
completion_status?: boolean
}
// Data Entry System Interfaces
export type PhaseDraftStatus = 'draft' | 'submitted' | 'withdrawn'
export type ExperimentPhase = 'pre-soaking' | 'air-drying' | 'cracking' | 'shelling'
export interface ExperimentPhaseDraft {
id: string
experiment_id: string
repetition_id: string
user_id: string
phase_name: ExperimentPhase
status: PhaseDraftStatus
draft_name?: string | null
created_at: string
updated_at: string
submitted_at?: string | null
withdrawn_at?: string | null
}
export interface ExperimentRepetition {
id: string
experiment_id: string
repetition_number: number
scheduled_date?: string | null
completion_status: boolean
is_locked: boolean
locked_at?: string | null
locked_by?: string | null
created_at: string
updated_at: string
created_by: string
}
export interface PecanDiameterMeasurement {
id: string
phase_data_id: string
measurement_number: number
diameter_in: number
created_at: string
}
export interface ExperimentPhaseData {
id: string
phase_draft_id: string
phase_name: ExperimentPhase
// Pre-soaking phase
batch_initial_weight_lbs?: number | null
initial_shell_moisture_pct?: number | null
initial_kernel_moisture_pct?: number | null
soaking_start_time?: string | null
// Air-drying phase
airdrying_start_time?: string | null
post_soak_weight_lbs?: number | null
post_soak_kernel_moisture_pct?: number | null
post_soak_shell_moisture_pct?: number | null
avg_pecan_diameter_in?: number | null
// Cracking phase
cracking_start_time?: string | null
// Shelling phase
shelling_start_time?: string | null
bin_1_weight_lbs?: number | null
bin_2_weight_lbs?: number | null
bin_3_weight_lbs?: number | null
discharge_bin_weight_lbs?: number | null
bin_1_full_yield_oz?: number | null
bin_2_full_yield_oz?: number | null
bin_3_full_yield_oz?: number | null
bin_1_half_yield_oz?: number | null
bin_2_half_yield_oz?: number | null
bin_3_half_yield_oz?: number | null
created_at: string
updated_at: string
// Related data
diameter_measurements?: PecanDiameterMeasurement[]
}
export interface CreatePhaseDraftRequest {
experiment_id: string
repetition_id: string
phase_name: ExperimentPhase
draft_name?: string
status?: PhaseDraftStatus
}
export interface UpdatePhaseDraftRequest {
draft_name?: string
status?: PhaseDraftStatus
}
export interface CreatePhaseDataRequest {
data_entry_id: string
phase_name: ExperimentPhase
[key: string]: any // For phase-specific data fields
}
export interface UpdatePhaseDataRequest {
[key: string]: any // For phase-specific data fields
}
// Phase creation request interfaces
export interface CreateSoakingRequest {
repetition_id: string
scheduled_start_time: string
soaking_duration_minutes: number
actual_start_time?: string
actual_end_time?: string
}
export interface CreateAirdryingRequest {
repetition_id: string
scheduled_start_time: string
duration_minutes: number
actual_start_time?: string
actual_end_time?: string
}
export interface CreateCrackingRequest {
repetition_id: string
machine_type_id: string
scheduled_start_time: string
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
role_id: string
assigned_at: string
assigned_by?: string
}
export interface UserProfile {
id: string
email: string
status: UserStatus
created_at: string
updated_at: string
role_id?: string // Legacy field, will be deprecated
}
export interface CreateUserRequest {
email: string
roles: RoleName[]
tempPassword?: string
}
export interface CreateUserResponse {
user_id: string
email: string
temp_password: string
roles: RoleName[]
status: UserStatus
}
// User management utility functions
export const userManagement = {
// Get all users with their roles
async getAllUsers(): Promise<User[]> {
const { data: profiles, error: profilesError } = await supabase
.from('user_profiles')
.select(`
id,
email,
first_name,
last_name,
status,
created_at,
updated_at
`)
if (profilesError) throw profilesError
// Get roles for each user
const usersWithRoles = await Promise.all(
profiles.map(async (profile) => {
const { data: userRoles, error: rolesError } = await supabase
.from('user_roles')
.select(`
roles!inner (
name
)
`)
.eq('user_id', profile.id)
if (rolesError) throw rolesError
return {
...profile,
roles: userRoles.map(ur => (ur.roles as any).name as RoleName)
}
})
)
return usersWithRoles
},
// Get all available roles
async getAllRoles(): Promise<Role[]> {
const { data, error } = await supabase
.from('roles')
.select('*')
.order('name')
if (error) throw error
return data
},
// Create a new user with roles
async createUser(userData: CreateUserRequest): Promise<CreateUserResponse> {
const { data, error } = await supabase.rpc('create_user_with_roles', {
user_email: userData.email,
role_names: userData.roles,
temp_password: userData.tempPassword
})
if (error) throw error
return data
},
// Update user status (enable/disable)
async updateUserStatus(userId: string, status: UserStatus): Promise<void> {
const { error } = await supabase
.from('user_profiles')
.update({ status })
.eq('id', userId)
if (error) throw error
},
// Update user roles
async updateUserRoles(userId: string, roleNames: RoleName[]): Promise<void> {
// First, remove all existing roles for the user
const { error: deleteError } = await supabase
.from('user_roles')
.delete()
.eq('user_id', userId)
if (deleteError) throw deleteError
// Get role IDs for the new roles
const { data: roles, error: rolesError } = await supabase
.from('roles')
.select('id, name')
.in('name', roleNames)
if (rolesError) throw rolesError
// Insert new role assignments
const roleAssignments = roles.map(role => ({
user_id: userId,
role_id: role.id
}))
const { error: insertError } = await supabase
.from('user_roles')
.insert(roleAssignments)
if (insertError) throw insertError
},
// Update user email
async updateUserEmail(userId: string, email: string): Promise<void> {
const { error } = await supabase
.from('user_profiles')
.update({ email })
.eq('id', userId)
if (error) throw error
},
// Get current user with roles
async getCurrentUser(): Promise<User | null> {
const { data: { user: authUser }, error: authError } = await supabase.auth.getUser()
if (authError || !authUser) return null
const { data: profile, error: profileError } = await supabase
.from('user_profiles')
.select(`
id,
email,
first_name,
last_name,
status,
created_at,
updated_at
`)
.eq('id', authUser.id)
.single()
if (profileError) throw profileError
const { data: userRoles, error: rolesError } = await supabase
.from('user_roles')
.select(`
roles!inner (
name
)
`)
.eq('user_id', authUser.id)
if (rolesError) throw rolesError
return {
...profile,
roles: userRoles.map(ur => (ur.roles as any).name as RoleName)
}
},
// Reset user password to default (admin only)
async resetUserPassword(userId: string): Promise<{ user_id: string; email: string; new_password: string; reset_at: string }> {
const { data, error } = await supabase.rpc('reset_user_password', {
target_user_id: userId
})
if (error) throw error
return data
},
// Change user password (user can only change their own password)
async changeUserPassword(currentPassword: string, newPassword: string): Promise<{ user_id: string; email: string; password_changed_at: string }> {
const { data, error } = await supabase.rpc('change_user_password', {
current_password: currentPassword,
new_password: newPassword
})
if (error) throw error
return data
},
// Sync OAuth user - ensures user profile exists for OAuth-authenticated users
async syncOAuthUser(): Promise<void> {
try {
const { data: { user: authUser }, error: authError } = await supabase.auth.getUser()
if (authError || !authUser) {
console.warn('No authenticated user found for OAuth sync')
return
}
// Check if user profile already exists
const { data: existingProfile, error: checkError } = await supabase
.from('user_profiles')
.select('id')
.eq('id', authUser.id)
.single()
// If profile already exists, no need to create it
if (existingProfile && !checkError) {
console.log('User profile already exists for user:', authUser.id)
return
}
// If error is not "no rows returned", it's a real error
if (checkError && checkError.code !== 'PGRST116') {
console.error('Error checking for existing profile:', checkError)
return
}
// Create user profile for new OAuth user
const { error: insertError } = await supabase
.from('user_profiles')
.insert({
id: authUser.id,
email: authUser.email || '',
status: 'active'
})
if (insertError) {
// Ignore "duplicate key value" errors in case of race condition
if (insertError.code === '23505') {
console.log('User profile was already created (race condition handled)')
return
}
console.error('Error creating user profile for OAuth user:', insertError)
return
}
console.log('Successfully created user profile for OAuth user:', authUser.id)
} catch (error) {
console.error('Unexpected error in syncOAuthUser:', error)
}
}
}
// Experiment phase management utility functions
export const experimentPhaseManagement = {
// Get all experiment phases
async getAllExperimentPhases(): Promise<ExperimentPhase[]> {
const { data, error } = await supabase
.from('experiment_phases')
.select('*')
.order('created_at', { ascending: false })
if (error) throw error
return data
},
// Get experiment phase by ID
async getExperimentPhaseById(id: string): Promise<ExperimentPhase | null> {
const { data, error } = await supabase
.from('experiment_phases')
.select('*')
.eq('id', id)
.single()
if (error) {
if (error.code === 'PGRST116') return null // Not found
throw error
}
return data
},
// Create a new experiment phase
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')
.insert({
...phaseData,
created_by: user.id
})
.select()
.single()
if (error) throw error
return data
},
// Update an experiment phase
async updateExperimentPhase(id: string, updates: UpdateExperimentPhaseRequest): Promise<ExperimentPhase> {
const { data, error } = await supabase
.from('experiment_phases')
.update(updates)
.eq('id', id)
.select()
.single()
if (error) throw error
return data
},
// Delete an experiment phase
async deleteExperimentPhase(id: string): Promise<void> {
const { error } = await supabase
.from('experiment_phases')
.delete()
.eq('id', id)
if (error) throw error
}
}
// Experiment management utility functions
export const experimentManagement = {
// Get all experiments
async getAllExperiments(): Promise<Experiment[]> {
const { data, error } = await supabase
.from('experiments')
.select('*')
.order('created_at', { ascending: false })
if (error) throw error
return data
},
// Get experiments by phase ID
async getExperimentsByPhaseId(phaseId: string): Promise<Experiment[]> {
const { data, error } = await supabase
.from('experiments')
.select('*')
.eq('phase_id', phaseId)
.order('created_at', { ascending: false })
if (error) throw error
return data
},
// Get experiment by ID
async getExperimentById(id: string): Promise<Experiment | null> {
const { data, error } = await supabase
.from('experiments')
.select('*')
.eq('id', id)
.single()
if (error) {
if (error.code === 'PGRST116') return null // Not found
throw error
}
return data
},
// Create a new experiment
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
.from('experiments')
.insert({
...experimentData,
created_by: user.id
})
.select()
.single()
if (error) throw error
return data
},
// Update an experiment
async updateExperiment(id: string, updates: UpdateExperimentRequest): Promise<Experiment> {
const { data, error } = await supabase
.from('experiments')
.update(updates)
.eq('id', id)
.select()
.single()
if (error) throw error
return data
},
// Delete an experiment (admin only)
async deleteExperiment(id: string): Promise<void> {
const { error } = await supabase
.from('experiments')
.delete()
.eq('id', id)
if (error) throw error
},
// Update experiment status
async updateExperimentStatus(id: string, scheduleStatus?: ScheduleStatus, resultsStatus?: ResultsStatus): Promise<Experiment> {
const updates: Partial<UpdateExperimentRequest> = {}
if (scheduleStatus) updates.schedule_status = scheduleStatus
if (resultsStatus) updates.results_status = resultsStatus
return this.updateExperiment(id, updates)
},
// Get experiments by status
async getExperimentsByStatus(scheduleStatus?: ScheduleStatus, resultsStatus?: ResultsStatus): Promise<Experiment[]> {
let query = supabase.from('experiments').select('*')
if (scheduleStatus) {
query = query.eq('schedule_status', scheduleStatus)
}
if (resultsStatus) {
query = query.eq('results_status', resultsStatus)
}
const { data, error } = await query.order('created_at', { ascending: false })
if (error) throw error
return data
},
// Check if experiment number is unique
async isExperimentNumberUnique(experimentNumber: number, excludeId?: string): Promise<boolean> {
let query = supabase
.from('experiments')
.select('id')
.eq('experiment_number', experimentNumber)
if (excludeId) {
query = query.neq('id', excludeId)
}
const { data, error } = await query
if (error) throw error
return data.length === 0
}
}
// 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')
if (!request.repetition_id) {
throw new Error('repetition_id is required')
}
const scheduledEndTime = new Date(new Date(request.scheduled_start_time).getTime() + request.soaking_duration_minutes * 60000).toISOString()
const { data, error } = await supabase
.from('soaking')
.upsert({
repetition_id: request.repetition_id,
scheduled_start_time: request.scheduled_start_time,
soaking_duration_minutes: request.soaking_duration_minutes,
scheduled_end_time: scheduledEndTime,
created_by: user.id
}, {
onConflict: 'repetition_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> {
// Get the first repetition for this experiment
const { data: repetitions, error: repsError } = await supabase
.from('experiment_repetitions')
.select('id')
.eq('experiment_id', experimentId)
.order('repetition_number', { ascending: true })
.limit(1)
if (repsError || !repetitions || repetitions.length === 0) {
return null
}
// Get soaking from unified experiment_phase_executions table
const { data, error } = await supabase
.from('experiment_phase_executions')
.select('*')
.eq('repetition_id', repetitions[0].id)
.eq('phase_type', 'soaking')
.single()
if (error) {
if (error.code === 'PGRST116') return null // Not found
throw error
}
// Map unified table data to Soaking interface format
if (data) {
return {
id: data.id,
repetition_id: data.repetition_id,
scheduled_start_time: data.scheduled_start_time,
actual_start_time: data.actual_start_time || null,
soaking_duration_minutes: data.soaking_duration_minutes || 0,
scheduled_end_time: data.scheduled_end_time || '',
actual_end_time: data.actual_end_time || null,
created_at: data.created_at,
updated_at: data.updated_at,
created_by: data.created_by
} as Soaking
}
return null
},
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')
if (!request.repetition_id) {
throw new Error('repetition_id is required')
}
if (!request.scheduled_start_time) {
throw new Error('scheduled_start_time is required')
}
const scheduledEndTime = new Date(new Date(request.scheduled_start_time).getTime() + request.duration_minutes * 60000).toISOString()
const { data, error } = await supabase
.from('airdrying')
.upsert({
repetition_id: request.repetition_id,
scheduled_start_time: request.scheduled_start_time,
duration_minutes: request.duration_minutes,
scheduled_end_time: scheduledEndTime,
created_by: user.id
}, {
onConflict: 'repetition_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> {
// Get the first repetition for this experiment
const { data: repetitions, error: repsError } = await supabase
.from('experiment_repetitions')
.select('id')
.eq('experiment_id', experimentId)
.order('repetition_number', { ascending: true })
.limit(1)
if (repsError || !repetitions || repetitions.length === 0) {
return null
}
// Get airdrying from unified experiment_phase_executions table
const { data, error } = await supabase
.from('experiment_phase_executions')
.select('*')
.eq('repetition_id', repetitions[0].id)
.eq('phase_type', 'airdrying')
.single()
if (error) {
if (error.code === 'PGRST116') return null // Not found
throw error
}
// Map unified table data to Airdrying interface format
if (data) {
return {
id: data.id,
repetition_id: data.repetition_id,
scheduled_start_time: data.scheduled_start_time,
actual_start_time: data.actual_start_time || null,
duration_minutes: data.duration_minutes || 0,
scheduled_end_time: data.scheduled_end_time || '',
actual_end_time: data.actual_end_time || null,
created_at: data.created_at,
updated_at: data.updated_at,
created_by: data.created_by
} as Airdrying
}
return null
},
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')
if (!request.repetition_id) {
throw new Error('repetition_id is required')
}
if (!request.scheduled_start_time) {
throw new Error('scheduled_start_time is required')
}
const { data, error } = await supabase
.from('cracking')
.upsert({
repetition_id: request.repetition_id,
machine_type_id: request.machine_type_id,
scheduled_start_time: request.scheduled_start_time,
created_by: user.id
}, {
onConflict: 'repetition_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
async getExperimentRepetitions(experimentId: string): Promise<ExperimentRepetition[]> {
const { data, error } = await supabase
.from('experiment_repetitions')
.select('*')
.eq('experiment_id', experimentId)
.order('repetition_number', { ascending: true })
if (error) throw error
return data
},
// Create a new repetition
async createRepetition(repetitionData: CreateRepetitionRequest): Promise<ExperimentRepetition> {
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_repetitions')
.insert({
...repetitionData,
created_by: user.id
})
.select()
.single()
if (error) throw error
return data
},
// Update a repetition
async updateRepetition(id: string, updates: UpdateRepetitionRequest): Promise<ExperimentRepetition> {
const { data, error } = await supabase
.from('experiment_repetitions')
.update(updates)
.eq('id', id)
.select()
.single()
if (error) throw error
return data
},
// Schedule a repetition
async scheduleRepetition(id: string, scheduledDate: string): Promise<ExperimentRepetition> {
const updates: UpdateRepetitionRequest = {
scheduled_date: scheduledDate,
}
return this.updateRepetition(id, updates)
},
// Remove repetition schedule
async removeRepetitionSchedule(id: string): Promise<ExperimentRepetition> {
const updates: UpdateRepetitionRequest = {
scheduled_date: null,
}
return this.updateRepetition(id, updates)
},
// Delete a repetition
async deleteRepetition(id: string): Promise<void> {
const { error } = await supabase
.from('experiment_repetitions')
.delete()
.eq('id', id)
if (error) throw error
},
// Get repetitions by status
async getRepetitionsByStatus(isScheduled?: boolean): Promise<ExperimentRepetition[]> {
let query = supabase.from('experiment_repetitions').select('*')
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
},
// Get repetitions with experiment details
async getRepetitionsWithExperiments(): Promise<(ExperimentRepetition & { experiment: Experiment })[]> {
const { data, error } = await supabase
.from('experiment_repetitions')
.select(`
*,
experiment:experiments(*)
`)
.order('created_at', { ascending: false })
if (error) throw error
return data
},
// Create all repetitions for an experiment
async createAllRepetitions(experimentId: string): Promise<ExperimentRepetition[]> {
// First get the experiment to know how many reps are required
const { data: experiment, error: expError } = await supabase
.from('experiments')
.select('reps_required')
.eq('id', experimentId)
.single()
if (expError) throw expError
// Create repetitions for each required rep
const repetitions: CreateRepetitionRequest[] = []
for (let i = 1; i <= experiment.reps_required; i++) {
repetitions.push({
experiment_id: experimentId,
repetition_number: i
})
}
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_repetitions')
.insert(repetitions.map(rep => ({
...rep,
created_by: user.id
})))
.select()
if (error) throw error
return data
},
// Lock a repetition (admin only)
async lockRepetition(repetitionId: string): Promise<ExperimentRepetition> {
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_repetitions')
.update({
is_locked: true,
locked_at: new Date().toISOString(),
locked_by: user.id
})
.eq('id', repetitionId)
.select()
.single()
if (error) throw error
return data
},
// Unlock a repetition (admin only)
async unlockRepetition(repetitionId: string): Promise<ExperimentRepetition> {
const { data, error } = await supabase
.from('experiment_repetitions')
.update({
is_locked: false,
locked_at: null,
locked_by: null
})
.eq('id', repetitionId)
.select()
.single()
if (error) throw error
return data
}
}
// Phase Draft Management
export const phaseDraftManagement = {
// Get all phase drafts for a repetition
async getPhaseDraftsForRepetition(repetitionId: string): Promise<ExperimentPhaseDraft[]> {
const { data, error } = await supabase
.from('experiment_phase_drafts')
.select('*')
.eq('repetition_id', repetitionId)
.order('created_at', { ascending: false })
if (error) throw error
return data
},
// Get user's phase drafts for a repetition
async getUserPhaseDraftsForRepetition(repetitionId: string): Promise<ExperimentPhaseDraft[]> {
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_phase_drafts')
.select('*')
.eq('repetition_id', repetitionId)
.eq('user_id', user.id)
.order('created_at', { ascending: false })
if (error) throw error
return data
},
// Get user's phase drafts for a specific phase and repetition
async getUserPhaseDraftsForPhase(repetitionId: string, phase: ExperimentPhase): Promise<ExperimentPhaseDraft[]> {
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_phase_drafts')
.select('*')
.eq('repetition_id', repetitionId)
.eq('user_id', user.id)
.eq('phase_name', phase)
.order('created_at', { ascending: false })
if (error) throw error
return data
},
// Create a new phase draft
async createPhaseDraft(request: CreatePhaseDraftRequest): Promise<ExperimentPhaseDraft> {
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_phase_drafts')
.insert({
...request,
user_id: user.id
})
.select()
.single()
if (error) throw error
return data
},
// Update a phase draft
async updatePhaseDraft(id: string, updates: UpdatePhaseDraftRequest): Promise<ExperimentPhaseDraft> {
const { data, error } = await supabase
.from('experiment_phase_drafts')
.update(updates)
.eq('id', id)
.select()
.single()
if (error) throw error
return data
},
// Delete a phase draft (only drafts)
async deletePhaseDraft(id: string): Promise<void> {
const { error } = await supabase
.from('experiment_phase_drafts')
.delete()
.eq('id', id)
if (error) throw error
},
// Submit a phase draft (change status from draft to submitted)
async submitPhaseDraft(id: string): Promise<ExperimentPhaseDraft> {
return this.updatePhaseDraft(id, { status: 'submitted' })
},
// Withdraw a phase draft (change status from submitted to withdrawn)
async withdrawPhaseDraft(id: string): Promise<ExperimentPhaseDraft> {
return this.updatePhaseDraft(id, { status: 'withdrawn' })
},
// Get phase data for a phase draft
async getPhaseDataForDraft(phaseDraftId: string): Promise<ExperimentPhaseData | null> {
const { data, error } = await supabase
.from('experiment_phase_data')
.select(`
*,
diameter_measurements:pecan_diameter_measurements(*)
`)
.eq('phase_draft_id', phaseDraftId)
.single()
if (error) {
if (error.code === 'PGRST116') return null // No rows found
throw error
}
return data
},
// Create or update phase data for a draft
async upsertPhaseData(phaseDraftId: string, phaseData: Partial<ExperimentPhaseData>): Promise<ExperimentPhaseData> {
const { data, error } = await supabase
.from('experiment_phase_data')
.upsert({
phase_draft_id: phaseDraftId,
...phaseData
}, {
onConflict: 'phase_draft_id,phase_name'
})
.select()
.single()
if (error) throw error
return data
},
// Save diameter measurements
async saveDiameterMeasurements(phaseDataId: string, measurements: number[]): Promise<PecanDiameterMeasurement[]> {
// First, delete existing measurements
await supabase
.from('pecan_diameter_measurements')
.delete()
.eq('phase_data_id', phaseDataId)
// Then insert new measurements
const measurementData = measurements.map((diameter, index) => ({
phase_data_id: phaseDataId,
measurement_number: index + 1,
diameter_in: diameter
}))
const { data, error } = await supabase
.from('pecan_diameter_measurements')
.insert(measurementData)
.select()
if (error) throw error
return data
},
// Calculate average diameter from measurements
calculateAverageDiameter(measurements: number[]): number {
if (measurements.length === 0) return 0
const validMeasurements = measurements.filter(m => m > 0)
if (validMeasurements.length === 0) return 0
return validMeasurements.reduce((sum, m) => sum + m, 0) / validMeasurements.length
},
// Auto-save draft data (for periodic saves)
async autoSaveDraft(phaseDraftId: string, phaseData: Partial<ExperimentPhaseData>): Promise<void> {
try {
await this.upsertPhaseData(phaseDraftId, phaseData)
} catch (error) {
console.warn('Auto-save failed:', error)
// Don't throw error for auto-save failures
}
}
}
// Conductor Availability Management
export interface ConductorAvailability {
id: string
user_id: string
available_from: string
available_to: string
notes?: string | null
status: 'active' | 'cancelled'
created_at: string
updated_at: string
created_by: string
}
export interface CreateAvailabilityRequest {
available_from: string
available_to: string
notes?: string
}
export const availabilityManagement = {
async getMyAvailability(): Promise<ConductorAvailability[]> {
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) throw new Error('User not authenticated')
const { data, error } = await supabase
.from('conductor_availability')
.select('*')
.eq('user_id', user.id)
.eq('status', 'active')
.order('available_from', { ascending: true })
if (error) throw error
return data
},
async createAvailability(request: CreateAvailabilityRequest): Promise<ConductorAvailability> {
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) throw new Error('User not authenticated')
const { data, error } = await supabase
.from('conductor_availability')
.insert({
user_id: user.id,
available_from: request.available_from,
available_to: request.available_to,
notes: request.notes,
created_by: user.id
})
.select()
.single()
if (error) throw error
return data
},
async deleteAvailability(id: string): Promise<void> {
const { error } = await supabase
.from('conductor_availability')
.update({ status: 'cancelled' })
.eq('id', id)
if (error) throw error
}
}