import { createClient } from '@supabase/supabase-js' // Local development configuration const supabaseUrl = 'http://127.0.0.1:54321' const supabaseAnonKey = '[REDACTED]' 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 roles: RoleName[] status: UserStatus created_at: string updated_at: string } export interface Role { id: string name: RoleName description: string created_at: string } export interface Experiment { id: string experiment_number: number reps_required: number soaking_duration_hr: number air_drying_time_min: number plate_contact_frequency_hz: number throughput_rate_pecans_sec: number crush_amount_in: number entry_exit_height_diff_in: number results_status: ResultsStatus completion_status: boolean created_at: string updated_at: string created_by: string } export interface CreateExperimentRequest { experiment_number: number reps_required: number soaking_duration_hr: number air_drying_time_min: number plate_contact_frequency_hz: number throughput_rate_pecans_sec: number crush_amount_in: number entry_exit_height_diff_in: number results_status?: ResultsStatus completion_status?: boolean } export interface UpdateExperimentRequest { experiment_number?: number reps_required?: number soaking_duration_hr?: number air_drying_time_min?: number plate_contact_frequency_hz?: number throughput_rate_pecans_sec?: number crush_amount_in?: number entry_exit_height_diff_in?: number results_status?: ResultsStatus completion_status?: boolean } export interface CreateRepetitionRequest { experiment_id: string repetition_number: number scheduled_date?: string | null schedule_status?: ScheduleStatus } export interface UpdateRepetitionRequest { scheduled_date?: string | null schedule_status?: ScheduleStatus completion_status?: boolean } // 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 schedule_status: ScheduleStatus 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 } 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 { const { data: profiles, error: profilesError } = await supabase .from('user_profiles') .select(` id, email, 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 { 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 { 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 { 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 { // 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 { const { error } = await supabase .from('user_profiles') .update({ email }) .eq('id', userId) if (error) throw error }, // Get current user with roles async getCurrentUser(): Promise { 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, 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) } } } // Experiment management utility functions export const experimentManagement = { // Get all experiments async getAllExperiments(): Promise { const { data, error } = await supabase .from('experiments') .select('*') .order('created_at', { ascending: false }) if (error) throw error return data }, // Get experiment by ID async getExperimentById(id: string): Promise { 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 { 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 { 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 { 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 { const updates: Partial = {} 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 { 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 { 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 } } // Experiment Repetitions Management export const repetitionManagement = { // Get all repetitions for an experiment async getExperimentRepetitions(experimentId: string): Promise { 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 { 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 { 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 { const updates: UpdateRepetitionRequest = { scheduled_date: scheduledDate, schedule_status: 'scheduled' } return this.updateRepetition(id, updates) }, // Remove repetition schedule async removeRepetitionSchedule(id: string): Promise { const updates: UpdateRepetitionRequest = { scheduled_date: null, schedule_status: 'pending schedule' } return this.updateRepetition(id, updates) }, // Delete a repetition async deleteRepetition(id: string): Promise { const { error } = await supabase .from('experiment_repetitions') .delete() .eq('id', id) if (error) throw error }, // Get repetitions by status async getRepetitionsByStatus(scheduleStatus?: ScheduleStatus): Promise { let query = supabase.from('experiment_repetitions').select('*') if (scheduleStatus) { query = query.eq('schedule_status', scheduleStatus) } 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 { // 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, schedule_status: 'pending schedule' }) } 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { return this.updatePhaseDraft(id, { status: 'submitted' }) }, // Withdraw a phase draft (change status from submitted to withdrawn) async withdrawPhaseDraft(id: string): Promise { return this.updatePhaseDraft(id, { status: 'withdrawn' }) }, // Get phase data for a phase draft async getPhaseDataForDraft(phaseDraftId: string): Promise { 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): Promise { 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 { // 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): Promise { try { await this.upsertPhaseData(phaseDraftId, phaseData) } catch (error) { console.warn('Auto-save failed:', error) // Don't throw error for auto-save failures } } }