diff --git a/.env.example b/.env.example
index d3e1525..e2ce5f2 100644
--- a/.env.example
+++ b/.env.example
@@ -1,5 +1,5 @@
# Web environment (Vite)
-VITE_SUPABASE_URL=http://localhost:54321
+VITE_SUPABASE_URL=http://exp-dash:54321
VITE_SUPABASE_ANON_KEY=[REDACTED]
# API config (optional)
diff --git a/.gitignore b/.gitignore
index 3c3dbed..0937301 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,3 +31,4 @@ camera-management-api/usda_vision_system.log
camera-management-api/camera_sdk/
camera-management-api/core
+management-dashboard-web-app/users.txt
diff --git a/camera-management-api/ai_agent/guides/AI_INTEGRATION_GUIDE.md b/camera-management-api/ai_agent/guides/AI_INTEGRATION_GUIDE.md
index a10f096..64e0172 100644
--- a/camera-management-api/ai_agent/guides/AI_INTEGRATION_GUIDE.md
+++ b/camera-management-api/ai_agent/guides/AI_INTEGRATION_GUIDE.md
@@ -474,11 +474,11 @@ const apiConfig = {
### Hostname Setup Guide
```bash
# Option 1: Add to /etc/hosts (Linux/Mac)
-echo "127.0.0.1 vision" | sudo tee -a /etc/hosts
+echo "exp-dash vision" | sudo tee -a /etc/hosts
# Option 2: Add to hosts file (Windows)
# Add to C:\Windows\System32\drivers\etc\hosts:
-# 127.0.0.1 vision
+# exp-dash vision
# Option 3: Configure DNS
# Point 'vision' hostname to your server's IP address
diff --git a/camera-management-api/ai_agent/references/api-endpoints.http b/camera-management-api/ai_agent/references/api-endpoints.http
index 0d3336d..0e23aa6 100644
--- a/camera-management-api/ai_agent/references/api-endpoints.http
+++ b/camera-management-api/ai_agent/references/api-endpoints.http
@@ -8,7 +8,7 @@
#
# HOSTNAME SETUP:
# To use 'vision' hostname instead of 'localhost':
-# 1. Add to /etc/hosts: 127.0.0.1 vision
+# 1. Add to /etc/hosts: exp-dash vision
# 2. Or configure DNS to point 'vision' to the server IP
# 3. Update camera_preview.html: API_BASE = 'http://localhost:8000'
###############################################################################
@@ -28,7 +28,7 @@
# Option 1: Using 'vision' hostname (recommended for production)
# - Requires hostname resolution setup
-# - Add to /etc/hosts: 127.0.0.1 vision
+# - Add to /etc/hosts: exp-dash vision
# - Or configure DNS: vision -> server IP address
# - Update camera_preview.html: API_BASE = 'http://localhost:8000'
# - Set @baseUrl = http://localhost:8000
diff --git a/camera-management-api/setup_timezone.sh b/camera-management-api/setup_timezone.sh
index 83a5e49..3fbfcc8 100755
--- a/camera-management-api/setup_timezone.sh
+++ b/camera-management-api/setup_timezone.sh
@@ -100,7 +100,7 @@ server 3.us.pool.ntp.org iburst
# Security settings
restrict default kod notrap nomodify nopeer noquery
restrict -6 default kod notrap nomodify nopeer noquery
-restrict 127.0.0.1
+restrict exp-dash
restrict -6 ::1
# Local clock as fallback
diff --git a/database_schema.md b/database_schema.md
index e36da75..0fffca0 100644
--- a/database_schema.md
+++ b/database_schema.md
@@ -201,6 +201,57 @@ CREATE TABLE public.pecan_diameter_measurements (
**Purpose**: Individual pecan diameter measurements (up to 10 per phase).
+### 3. Conductor Availability Management
+
+#### `public.conductor_availability`
+```sql
+CREATE TABLE public.conductor_availability (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id UUID NOT NULL REFERENCES public.user_profiles(id) ON DELETE CASCADE,
+ available_from TIMESTAMP WITH TIME ZONE NOT NULL,
+ available_to TIMESTAMP WITH TIME ZONE NOT NULL,
+ notes TEXT, -- Optional notes about the availability
+ status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'cancelled')),
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ created_by UUID NOT NULL REFERENCES public.user_profiles(id),
+
+ -- Ensure available_to is after available_from
+ CONSTRAINT valid_time_range CHECK (available_to > available_from),
+
+ -- Ensure availability is in the future (can be modified if needed for past records)
+ CONSTRAINT future_availability CHECK (available_from >= NOW() - INTERVAL '1 day')
+);
+```
+
+**Purpose**: Stores conductor availability windows for experiment scheduling with overlap prevention.
+
+#### `public.experiment_phase_assignments`
+```sql
+CREATE TABLE public.experiment_phase_assignments (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ experiment_id UUID NOT NULL REFERENCES public.experiments(id) ON DELETE CASCADE,
+ repetition_id UUID NOT NULL REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE,
+ conductor_id UUID NOT NULL REFERENCES public.user_profiles(id) ON DELETE CASCADE,
+ phase_name TEXT NOT NULL CHECK (phase_name IN ('pre-soaking', 'air-drying', 'cracking', 'shelling')),
+ scheduled_start_time TIMESTAMP WITH TIME ZONE NOT NULL,
+ scheduled_end_time TIMESTAMP WITH TIME ZONE NOT NULL,
+ status TEXT NOT NULL DEFAULT 'scheduled' CHECK (status IN ('scheduled', 'in-progress', 'completed', 'cancelled')),
+ notes TEXT, -- Optional notes about the assignment
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ created_by UUID NOT NULL REFERENCES public.user_profiles(id),
+
+ -- Ensure scheduled_end_time is after scheduled_start_time
+ CONSTRAINT valid_scheduled_time_range CHECK (scheduled_end_time > scheduled_start_time),
+
+ -- Ensure unique assignment per conductor per phase per repetition
+ CONSTRAINT unique_conductor_phase_assignment UNIQUE (repetition_id, conductor_id, phase_name)
+);
+```
+
+**Purpose**: Assigns conductors to specific experiment repetition phases with scheduled times for future scheduling functionality.
+
## Key Functions
### Authentication & Authorization
@@ -218,6 +269,12 @@ CREATE TABLE public.pecan_diameter_measurements (
### Data Validation
- `validate_repetition_number()`: Ensures repetition numbers don't exceed experiment requirements
- `public.check_repetition_lock_before_withdrawal()`: Prevents withdrawal of locked repetitions
+- `public.check_availability_overlap()`: Prevents overlapping availabilities for the same conductor
+- `public.adjust_overlapping_availability()`: Automatically adjusts overlapping availabilities (alternative approach)
+
+### Availability Management
+- `public.get_available_conductors(start_time, end_time)`: Returns conductors available for a specific time range
+- `public.is_conductor_available(conductor_id, start_time, end_time)`: Checks if a conductor is available for a specific time range
### Timestamp Management
- `public.handle_updated_at()`: Updates the updated_at timestamp
@@ -227,7 +284,7 @@ CREATE TABLE public.pecan_diameter_measurements (
### Access Control Summary
- **Admin**: Full access to all tables and operations
-- **Conductor**: Can manage experiments, experiment phases, and repetitions, view all data
+- **Conductor**: Can manage experiments, experiment phases, and repetitions, view all data, manage their own availability
- **Analyst**: Read-only access to all data
- **Data Recorder**: Can create and manage their own phase drafts and data
@@ -235,6 +292,8 @@ CREATE TABLE public.pecan_diameter_measurements (
- All authenticated users can view experiments, experiment phases, and repetitions
- Admin and conductor roles can manage experiment phases
- Users can only modify their own phase drafts (unless admin)
+- Users can only manage their own availability (unless admin)
+- Conductors can view their own experiment phase assignments
- Locked repetitions prevent draft modifications
- Submitted drafts cannot be withdrawn if repetition is locked
- Role-based access control for user management
@@ -260,6 +319,10 @@ The database includes comprehensive indexing for performance:
- Only one submitted draft per user per phase per repetition
- Moisture percentages must be between 0-100
- Weights and measurements must be non-negative
+- Conductor availabilities cannot overlap for the same user
+- Availability windows must have valid time ranges (end > start)
+- Only one conductor assignment per phase per repetition
+- Scheduled times must have valid ranges (end > start)
## Migration History
@@ -271,5 +334,6 @@ The schema has evolved through several migrations:
5. **Data Entry System**: Phase-specific draft and data management
6. **Constraint Fixes**: Refined business rules and constraints
7. **Experiment Phases**: Added experiment grouping for better organization
+8. **Conductor Availability**: Added availability management and experiment phase assignments for scheduling
This schema supports a comprehensive pecan processing experiment management system with robust security, data integrity, and flexible role-based access control.
diff --git a/management-dashboard-web-app/.env.backup b/management-dashboard-web-app/.env.backup
new file mode 100755
index 0000000..4ee0560
--- /dev/null
+++ b/management-dashboard-web-app/.env.backup
@@ -0,0 +1,4 @@
+# Local Supabase config for Vite dev server
+VITE_SUPABASE_URL=http://127.0.0.1:54321
+VITE_SUPABASE_ANON_KEY=[REDACTED]
+
diff --git a/management-dashboard-web-app/CAMERA_ROUTE_IMPLEMENTATION.md b/management-dashboard-web-app/CAMERA_ROUTE_IMPLEMENTATION.md
index 66ee35d..a515d80 100755
--- a/management-dashboard-web-app/CAMERA_ROUTE_IMPLEMENTATION.md
+++ b/management-dashboard-web-app/CAMERA_ROUTE_IMPLEMENTATION.md
@@ -216,3 +216,5 @@ If you encounter issues:
+
+
diff --git a/management-dashboard-web-app/public/camera-test.html b/management-dashboard-web-app/public/camera-test.html
index 9404112..93ef557 100755
--- a/management-dashboard-web-app/public/camera-test.html
+++ b/management-dashboard-web-app/public/camera-test.html
@@ -125,3 +125,5 @@
+
+
diff --git a/management-dashboard-web-app/src/components/CameraRoute.tsx b/management-dashboard-web-app/src/components/CameraRoute.tsx
index 9172e8b..aa0d688 100755
--- a/management-dashboard-web-app/src/components/CameraRoute.tsx
+++ b/management-dashboard-web-app/src/components/CameraRoute.tsx
@@ -31,3 +31,5 @@ export function CameraRoute({ cameraNumber }: CameraRouteProps) {
+
+
diff --git a/management-dashboard-web-app/src/components/DashboardLayout.tsx b/management-dashboard-web-app/src/components/DashboardLayout.tsx
index 9e1723d..778d05a 100755
--- a/management-dashboard-web-app/src/components/DashboardLayout.tsx
+++ b/management-dashboard-web-app/src/components/DashboardLayout.tsx
@@ -8,6 +8,7 @@ import { DataEntry } from './DataEntry'
import { VisionSystem } from './VisionSystem'
import { Scheduling } from './Scheduling'
import { VideoStreamingPage } from '../features/video-streaming'
+import { UserProfile } from './UserProfile'
import { userManagement, type User } from '../lib/supabase'
interface DashboardLayoutProps {
@@ -25,7 +26,7 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps
const [isHovered, setIsHovered] = useState(false)
// Valid dashboard views
- const validViews = ['dashboard', 'user-management', 'experiments', 'analytics', 'data-entry', 'vision-system', 'scheduling', 'video-library']
+ const validViews = ['dashboard', 'user-management', 'experiments', 'analytics', 'data-entry', 'vision-system', 'scheduling', 'video-library', 'profile']
// Save current view to localStorage
const saveCurrentView = (view: string) => {
@@ -194,6 +195,8 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps
return
case 'video-library':
return
+ case 'profile':
+ return
default:
return
}
@@ -274,6 +277,7 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps
currentView={currentView}
onToggleSidebar={handleToggleSidebar}
isSidebarOpen={isMobileOpen}
+ onNavigateToProfile={() => handleViewChange('profile')}
/>
{renderCurrentView()}
diff --git a/management-dashboard-web-app/src/components/LiveCameraView.tsx b/management-dashboard-web-app/src/components/LiveCameraView.tsx
index 990a8f4..72c4867 100755
--- a/management-dashboard-web-app/src/components/LiveCameraView.tsx
+++ b/management-dashboard-web-app/src/components/LiveCameraView.tsx
@@ -140,3 +140,5 @@ export function LiveCameraView({ cameraName }: LiveCameraViewProps) {
+
+
diff --git a/management-dashboard-web-app/src/components/TopNavbar.tsx b/management-dashboard-web-app/src/components/TopNavbar.tsx
index b5cb594..e5f10c6 100755
--- a/management-dashboard-web-app/src/components/TopNavbar.tsx
+++ b/management-dashboard-web-app/src/components/TopNavbar.tsx
@@ -7,6 +7,7 @@ interface TopNavbarProps {
currentView?: string
onToggleSidebar?: () => void
isSidebarOpen?: boolean
+ onNavigateToProfile?: () => void
}
export function TopNavbar({
@@ -14,7 +15,8 @@ export function TopNavbar({
onLogout,
currentView = 'dashboard',
onToggleSidebar,
- isSidebarOpen = false
+ isSidebarOpen = false,
+ onNavigateToProfile
}: TopNavbarProps) {
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false)
@@ -145,11 +147,16 @@ export function TopNavbar({
>
- {user.email.charAt(0).toUpperCase()}
+ {(user.first_name || user.email).charAt(0).toUpperCase()}
-
{user.email.split('@')[0]}
+
+ {user.first_name && user.last_name
+ ? `${user.first_name} ${user.last_name}`
+ : user.email.split('@')[0]
+ }
+
- {user.email.split('@')[0]}
+ {user.first_name && user.last_name
+ ? `${user.first_name} ${user.last_name}`
+ : user.email.split('@')[0]
+ }
{user.email}
@@ -183,7 +193,13 @@ export function TopNavbar({
-
+ {
+ setIsUserMenuOpen(false)
+ onNavigateToProfile?.()
+ }}
+ className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg text-theme-sm hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300 w-full text-left"
+ >
Profile
-
+
diff --git a/management-dashboard-web-app/src/components/UserManagement.tsx b/management-dashboard-web-app/src/components/UserManagement.tsx
index a315a24..93083fa 100755
--- a/management-dashboard-web-app/src/components/UserManagement.tsx
+++ b/management-dashboard-web-app/src/components/UserManagement.tsx
@@ -81,6 +81,20 @@ export function UserManagement() {
}
}
+ const handlePasswordReset = async (userId: string, userEmail: string) => {
+ if (!confirm(`Are you sure you want to reset the password for ${userEmail}? The password will be reset to "password123".`)) {
+ return
+ }
+
+ try {
+ const result = await userManagement.resetUserPassword(userId)
+ alert(`Password reset successfully for ${result.email}. New password: ${result.new_password}`)
+ } catch (err) {
+ console.error('Password reset error:', err)
+ alert('Failed to reset user password')
+ }
+ }
+
const handleUserCreated = (newUser: User) => {
setUsers([...users, newUser])
setShowCreateModal(false)
@@ -260,6 +274,7 @@ export function UserManagement() {
onStatusToggle={handleStatusToggle}
onRoleUpdate={handleRoleUpdate}
onEmailUpdate={handleEmailUpdate}
+ onPasswordReset={handlePasswordReset}
getRoleBadgeColor={getRoleBadgeColor}
/>
))}
@@ -290,6 +305,7 @@ interface UserRowProps {
onStatusToggle: (userId: string, currentStatus: UserStatus) => void
onRoleUpdate: (userId: string, newRoles: RoleName[]) => void
onEmailUpdate: (userId: string, newEmail: string) => void
+ onPasswordReset: (userId: string, userEmail: string) => void
getRoleBadgeColor: (role: string) => string
}
@@ -302,6 +318,7 @@ function UserRow({
onStatusToggle,
onRoleUpdate,
onEmailUpdate,
+ onPasswordReset,
getRoleBadgeColor
}: UserRowProps) {
const [editEmail, setEditEmail] = useState(user.email)
@@ -381,8 +398,8 @@ function UserRow({
onStatusToggle(user.id, user.status)}
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${user.status === 'active'
- ? 'bg-green-100 text-green-800 hover:bg-green-200'
- : 'bg-red-100 text-red-800 hover:bg-red-200'
+ ? 'bg-green-100 text-green-800 hover:bg-green-200'
+ : 'bg-red-100 text-red-800 hover:bg-red-200'
}`}
>
{user.status.charAt(0).toUpperCase() + user.status.slice(1)}
@@ -408,12 +425,26 @@ function UserRow({
) : (
-
- Edit
-
+
+
+
+
+
+
+
onPasswordReset(user.id, user.email)}
+ className="p-1 text-orange-600 hover:text-orange-900 hover:bg-orange-50 rounded"
+ title="Reset password to 'password123'"
+ >
+
+
+
+
+
)}
diff --git a/management-dashboard-web-app/src/components/UserProfile.tsx b/management-dashboard-web-app/src/components/UserProfile.tsx
new file mode 100644
index 0000000..7d4115a
--- /dev/null
+++ b/management-dashboard-web-app/src/components/UserProfile.tsx
@@ -0,0 +1,312 @@
+import { useState } from 'react'
+import { userManagement, type User } from '../lib/supabase'
+
+interface UserProfileProps {
+ user: User
+}
+
+interface ChangePasswordModalProps {
+ isOpen: boolean
+ onClose: () => void
+ onSuccess: () => void
+}
+
+function ChangePasswordModal({ isOpen, onClose, onSuccess }: ChangePasswordModalProps) {
+ const [isLoading, setIsLoading] = useState(false)
+ const [error, setError] = useState(null)
+ const [currentPassword, setCurrentPassword] = useState('')
+ const [newPassword, setNewPassword] = useState('')
+ const [confirmPassword, setConfirmPassword] = useState('')
+
+ const handlePasswordChange = async () => {
+ // Validation
+ if (!currentPassword || !newPassword || !confirmPassword) {
+ setError('All fields are required')
+ return
+ }
+
+ if (newPassword !== confirmPassword) {
+ setError('New passwords do not match')
+ return
+ }
+
+ if (newPassword.length < 6) {
+ setError('New password must be at least 6 characters long')
+ return
+ }
+
+ try {
+ setIsLoading(true)
+ setError(null)
+
+ const result = await userManagement.changeUserPassword(currentPassword, newPassword)
+ alert('Password changed successfully!')
+ onSuccess()
+ onClose()
+ // Clear form
+ setCurrentPassword('')
+ setNewPassword('')
+ setConfirmPassword('')
+ } catch (err: any) {
+ console.error('Password change error:', err)
+ if (err.message?.includes('Current password is incorrect')) {
+ setError('Current password is incorrect')
+ } else {
+ setError('Failed to change password. Please try again.')
+ }
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ if (!isOpen) return null
+
+ return (
+
+
+
+
+
+
+
Change Password
+
+
+
+
+
+
+
+
+
+ {error && (
+
+ )}
+
+
+
+ Cancel
+
+
+ {isLoading ? 'Changing...' : 'Change Password'}
+
+
+
+
+
+ )
+}
+
+export function UserProfile({ user }: UserProfileProps) {
+ const [showChangePasswordModal, setShowChangePasswordModal] = useState(false)
+
+ const getRoleBadgeColor = (role: string) => {
+ switch (role) {
+ case 'admin':
+ return 'bg-red-100 text-red-800'
+ case 'conductor':
+ return 'bg-blue-100 text-blue-800'
+ case 'analyst':
+ return 'bg-green-100 text-green-800'
+ case 'data recorder':
+ return 'bg-purple-100 text-purple-800'
+ default:
+ return 'bg-gray-100 text-gray-800'
+ }
+ }
+
+ const formatDate = (dateString: string) => {
+ return new Date(dateString).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ })
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
User Profile
+
Manage your account information and settings
+
+
+
+ {(user.first_name || user.email).charAt(0).toUpperCase()}
+
+
+
+
+
+ {/* Profile Information */}
+
+
+ {/* Basic Information */}
+
+
Basic Information
+
+
+
+ Full Name
+
+
+ {user.first_name && user.last_name
+ ? `${user.first_name} ${user.last_name}`
+ : 'Name not set'
+ }
+
+
+
+
+
+ Email Address
+
+
+ {user.email}
+
+
+
+
+
+ Account Status
+
+
+
+ {user.status.charAt(0).toUpperCase() + user.status.slice(1)}
+
+
+
+
+
+
+ Member Since
+
+
+ {formatDate(user.created_at)}
+
+
+
+
+ {/* Roles and Permissions */}
+
+
Roles & Permissions
+
+
+
+ Assigned Roles
+
+
+ {user.roles.map((role) => (
+
+ {role.charAt(0).toUpperCase() + role.slice(1)}
+
+ ))}
+
+
+
+
+
+ Last Updated
+
+
+ {formatDate(user.updated_at)}
+
+
+
+
+
+ {/* Actions */}
+
+
Account Actions
+
+
+
setShowChangePasswordModal(true)}
+ className="inline-flex items-center px-4 py-2 border border-blue-300 text-sm font-medium rounded-md text-blue-700 bg-blue-50 hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
+ >
+
+
+
+ Change Password
+
+
+
+
+
+
+ {/* Change Password Modal */}
+
setShowChangePasswordModal(false)}
+ onSuccess={() => {
+ // Could add success notification here
+ }}
+ />
+
+ )
+}
diff --git a/management-dashboard-web-app/src/lib/supabase.ts b/management-dashboard-web-app/src/lib/supabase.ts
index a7555de..e8071fe 100755
--- a/management-dashboard-web-app/src/lib/supabase.ts
+++ b/management-dashboard-web-app/src/lib/supabase.ts
@@ -19,6 +19,8 @@ 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
@@ -257,6 +259,8 @@ export const userManagement = {
.select(`
id,
email,
+ first_name,
+ last_name,
status,
created_at,
updated_at
@@ -373,6 +377,8 @@ export const userManagement = {
.select(`
id,
email,
+ first_name,
+ last_name,
status,
created_at,
updated_at
@@ -397,6 +403,27 @@ export const userManagement = {
...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
}
}
diff --git a/management-dashboard-web-app/supabase/.temp/cli-latest b/management-dashboard-web-app/supabase/.temp/cli-latest
index 322987f..75788de 100755
--- a/management-dashboard-web-app/supabase/.temp/cli-latest
+++ b/management-dashboard-web-app/supabase/.temp/cli-latest
@@ -1 +1 @@
-v2.34.3
\ No newline at end of file
+v2.40.7
\ No newline at end of file
diff --git a/management-dashboard-web-app/supabase/config.toml b/management-dashboard-web-app/supabase/config.toml
index 2848976..812a04a 100755
--- a/management-dashboard-web-app/supabase/config.toml
+++ b/management-dashboard-web-app/supabase/config.toml
@@ -81,7 +81,7 @@ enabled = true
# Port to use for Supabase Studio.
port = 54323
# External URL of the API server that frontend connects to.
-api_url = "http://127.0.0.1"
+api_url = "http://exp-dash"
# OpenAI API Key to use for Supabase AI in the Supabase Studio.
openai_api_key = "env(OPENAI_API_KEY)"
@@ -117,9 +117,9 @@ file_size_limit = "50MiB"
enabled = true
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
# in emails.
-site_url = "http://127.0.0.1:3000"
+site_url = "http://exp-dash:3000"
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
-additional_redirect_urls = ["https://127.0.0.1:3000"]
+additional_redirect_urls = ["https://exp-dash:3000"]
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
jwt_expiry = 3600
# If disabled, the refresh token will never expire.
diff --git a/management-dashboard-web-app/supabase/migrations/20250102000001_add_conductor_availability.sql b/management-dashboard-web-app/supabase/migrations/20250102000001_add_conductor_availability.sql
new file mode 100644
index 0000000..96c6f0d
--- /dev/null
+++ b/management-dashboard-web-app/supabase/migrations/20250102000001_add_conductor_availability.sql
@@ -0,0 +1,341 @@
+-- Add Conductor Availability and Experiment Phase Assignment Tables
+-- This migration adds tables for conductor availability management and future experiment scheduling
+
+-- =============================================
+-- 1. CONDUCTOR AVAILABILITY TABLE
+-- =============================================
+
+-- Create conductor_availability table
+CREATE TABLE IF NOT EXISTS public.conductor_availability (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id UUID NOT NULL REFERENCES public.user_profiles(id) ON DELETE CASCADE,
+ available_from TIMESTAMP WITH TIME ZONE NOT NULL,
+ available_to TIMESTAMP WITH TIME ZONE NOT NULL,
+ notes TEXT, -- Optional notes about the availability
+ status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'cancelled')),
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ created_by UUID NOT NULL REFERENCES public.user_profiles(id),
+
+ -- Ensure available_to is after available_from
+ CONSTRAINT valid_time_range CHECK (available_to > available_from),
+
+ -- Ensure availability is in the future (can be modified if needed for past records)
+ CONSTRAINT future_availability CHECK (available_from >= NOW() - INTERVAL '1 day')
+);
+
+-- =============================================
+-- 2. EXPERIMENT PHASE ASSIGNMENTS TABLE (Future Scheduling)
+-- =============================================
+
+-- Create experiment_phase_assignments table for scheduling conductors to experiment phases
+CREATE TABLE IF NOT EXISTS public.experiment_phase_assignments (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ experiment_id UUID NOT NULL REFERENCES public.experiments(id) ON DELETE CASCADE,
+ repetition_id UUID NOT NULL REFERENCES public.experiment_repetitions(id) ON DELETE CASCADE,
+ conductor_id UUID NOT NULL REFERENCES public.user_profiles(id) ON DELETE CASCADE,
+ phase_name TEXT NOT NULL CHECK (phase_name IN ('pre-soaking', 'air-drying', 'cracking', 'shelling')),
+ scheduled_start_time TIMESTAMP WITH TIME ZONE NOT NULL,
+ scheduled_end_time TIMESTAMP WITH TIME ZONE NOT NULL,
+ status TEXT NOT NULL DEFAULT 'scheduled' CHECK (status IN ('scheduled', 'in-progress', 'completed', 'cancelled')),
+ notes TEXT, -- Optional notes about the assignment
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ created_by UUID NOT NULL REFERENCES public.user_profiles(id),
+
+ -- Ensure scheduled_end_time is after scheduled_start_time
+ CONSTRAINT valid_scheduled_time_range CHECK (scheduled_end_time > scheduled_start_time),
+
+ -- Ensure unique assignment per conductor per phase per repetition
+ CONSTRAINT unique_conductor_phase_assignment UNIQUE (repetition_id, conductor_id, phase_name)
+);
+
+-- =============================================
+-- 3. INDEXES FOR PERFORMANCE
+-- =============================================
+
+-- Conductor availability indexes
+CREATE INDEX IF NOT EXISTS idx_conductor_availability_user_id ON public.conductor_availability(user_id);
+CREATE INDEX IF NOT EXISTS idx_conductor_availability_available_from ON public.conductor_availability(available_from);
+CREATE INDEX IF NOT EXISTS idx_conductor_availability_available_to ON public.conductor_availability(available_to);
+CREATE INDEX IF NOT EXISTS idx_conductor_availability_status ON public.conductor_availability(status);
+CREATE INDEX IF NOT EXISTS idx_conductor_availability_created_by ON public.conductor_availability(created_by);
+CREATE INDEX IF NOT EXISTS idx_conductor_availability_time_range ON public.conductor_availability(available_from, available_to);
+
+-- Experiment phase assignments indexes
+CREATE INDEX IF NOT EXISTS idx_experiment_phase_assignments_experiment_id ON public.experiment_phase_assignments(experiment_id);
+CREATE INDEX IF NOT EXISTS idx_experiment_phase_assignments_repetition_id ON public.experiment_phase_assignments(repetition_id);
+CREATE INDEX IF NOT EXISTS idx_experiment_phase_assignments_conductor_id ON public.experiment_phase_assignments(conductor_id);
+CREATE INDEX IF NOT EXISTS idx_experiment_phase_assignments_phase_name ON public.experiment_phase_assignments(phase_name);
+CREATE INDEX IF NOT EXISTS idx_experiment_phase_assignments_status ON public.experiment_phase_assignments(status);
+CREATE INDEX IF NOT EXISTS idx_experiment_phase_assignments_scheduled_start ON public.experiment_phase_assignments(scheduled_start_time);
+CREATE INDEX IF NOT EXISTS idx_experiment_phase_assignments_created_by ON public.experiment_phase_assignments(created_by);
+
+-- =============================================
+-- 4. FUNCTIONS FOR OVERLAP PREVENTION
+-- =============================================
+
+-- Function to check for overlapping availabilities
+CREATE OR REPLACE FUNCTION public.check_availability_overlap()
+RETURNS TRIGGER AS $$
+DECLARE
+ overlap_count INTEGER;
+BEGIN
+ -- Check for overlapping availabilities for the same user
+ SELECT COUNT(*) INTO overlap_count
+ FROM public.conductor_availability
+ WHERE user_id = NEW.user_id
+ AND id != COALESCE(NEW.id, '00000000-0000-0000-0000-000000000000'::UUID)
+ AND status = 'active'
+ AND (
+ -- New availability starts during an existing one
+ (NEW.available_from >= available_from AND NEW.available_from < available_to) OR
+ -- New availability ends during an existing one
+ (NEW.available_to > available_from AND NEW.available_to <= available_to) OR
+ -- New availability completely contains an existing one
+ (NEW.available_from <= available_from AND NEW.available_to >= available_to) OR
+ -- Existing availability completely contains the new one
+ (available_from <= NEW.available_from AND available_to >= NEW.available_to)
+ );
+
+ IF overlap_count > 0 THEN
+ RAISE EXCEPTION 'Availability overlaps with existing availability for user %. Please adjust the time range or cancel the conflicting availability.', NEW.user_id;
+ END IF;
+
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+-- Function to automatically adjust overlapping availabilities (alternative approach)
+CREATE OR REPLACE FUNCTION public.adjust_overlapping_availability()
+RETURNS TRIGGER AS $$
+DECLARE
+ overlapping_record RECORD;
+BEGIN
+ -- Find overlapping availabilities and adjust them
+ FOR overlapping_record IN
+ SELECT id, available_from, available_to
+ FROM public.conductor_availability
+ WHERE user_id = NEW.user_id
+ AND id != COALESCE(NEW.id, '00000000-0000-0000-0000-000000000000'::UUID)
+ AND status = 'active'
+ AND (
+ (NEW.available_from >= available_from AND NEW.available_from < available_to) OR
+ (NEW.available_to > available_from AND NEW.available_to <= available_to) OR
+ (NEW.available_from <= available_from AND NEW.available_to >= available_to) OR
+ (available_from <= NEW.available_from AND available_to >= NEW.available_to)
+ )
+ LOOP
+ -- Adjust the overlapping record to end where the new one starts
+ IF overlapping_record.available_from < NEW.available_from AND overlapping_record.available_to > NEW.available_from THEN
+ UPDATE public.conductor_availability
+ SET available_to = NEW.available_from,
+ updated_at = NOW()
+ WHERE id = overlapping_record.id;
+ END IF;
+
+ -- If the overlapping record starts after the new one, adjust it to start where the new one ends
+ IF overlapping_record.available_from < NEW.available_to AND overlapping_record.available_to > NEW.available_to THEN
+ UPDATE public.conductor_availability
+ SET available_from = NEW.available_to,
+ updated_at = NOW()
+ WHERE id = overlapping_record.id;
+ END IF;
+
+ -- If the overlapping record is completely contained within the new one, cancel it
+ IF overlapping_record.available_from >= NEW.available_from AND overlapping_record.available_to <= NEW.available_to THEN
+ UPDATE public.conductor_availability
+ SET status = 'cancelled',
+ updated_at = NOW()
+ WHERE id = overlapping_record.id;
+ END IF;
+ END LOOP;
+
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+-- =============================================
+-- 5. TRIGGERS
+-- =============================================
+
+-- Create trigger for updated_at on conductor_availability
+CREATE TRIGGER set_updated_at_conductor_availability
+ BEFORE UPDATE ON public.conductor_availability
+ FOR EACH ROW
+ EXECUTE FUNCTION public.handle_updated_at();
+
+-- Create trigger for updated_at on experiment_phase_assignments
+CREATE TRIGGER set_updated_at_experiment_phase_assignments
+ BEFORE UPDATE ON public.experiment_phase_assignments
+ FOR EACH ROW
+ EXECUTE FUNCTION public.handle_updated_at();
+
+-- Create trigger to prevent overlapping availabilities (strict approach)
+CREATE TRIGGER trigger_check_availability_overlap
+ BEFORE INSERT OR UPDATE ON public.conductor_availability
+ FOR EACH ROW
+ EXECUTE FUNCTION public.check_availability_overlap();
+
+-- Alternative trigger to automatically adjust overlapping availabilities (uncomment if preferred)
+-- CREATE TRIGGER trigger_adjust_overlapping_availability
+-- BEFORE INSERT OR UPDATE ON public.conductor_availability
+-- FOR EACH ROW
+-- EXECUTE FUNCTION public.adjust_overlapping_availability();
+
+-- =============================================
+-- 6. HELPER FUNCTIONS
+-- =============================================
+
+-- Function to get available conductors for a specific time range
+CREATE OR REPLACE FUNCTION public.get_available_conductors(
+ start_time TIMESTAMP WITH TIME ZONE,
+ end_time TIMESTAMP WITH TIME ZONE
+)
+RETURNS TABLE (
+ user_id UUID,
+ email TEXT,
+ available_from TIMESTAMP WITH TIME ZONE,
+ available_to TIMESTAMP WITH TIME ZONE
+) AS $$
+BEGIN
+ RETURN QUERY
+ SELECT
+ ca.user_id,
+ up.email,
+ ca.available_from,
+ ca.available_to
+ FROM public.conductor_availability ca
+ JOIN public.user_profiles up ON ca.user_id = up.id
+ JOIN public.user_roles ur ON up.id = ur.user_id
+ JOIN public.roles r ON ur.role_id = r.id
+ WHERE ca.status = 'active'
+ AND r.name = 'conductor'
+ AND ca.available_from <= start_time
+ AND ca.available_to >= end_time
+ ORDER BY up.email;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+-- Function to check if a conductor is available for a specific time range
+CREATE OR REPLACE FUNCTION public.is_conductor_available(
+ conductor_user_id UUID,
+ start_time TIMESTAMP WITH TIME ZONE,
+ end_time TIMESTAMP WITH TIME ZONE
+)
+RETURNS BOOLEAN AS $$
+DECLARE
+ availability_count INTEGER;
+BEGIN
+ SELECT COUNT(*) INTO availability_count
+ FROM public.conductor_availability
+ WHERE user_id = conductor_user_id
+ AND status = 'active'
+ AND available_from <= start_time
+ AND available_to >= end_time;
+
+ RETURN availability_count > 0;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+-- =============================================
+-- 7. ROW LEVEL SECURITY (RLS)
+-- =============================================
+
+-- Enable RLS on new tables
+ALTER TABLE public.conductor_availability ENABLE ROW LEVEL SECURITY;
+ALTER TABLE public.experiment_phase_assignments ENABLE ROW LEVEL SECURITY;
+
+-- Conductor availability policies
+CREATE POLICY "conductor_availability_select_policy" ON public.conductor_availability
+ FOR SELECT
+ TO authenticated
+ USING (
+ -- Users can view their own availability, admins can view all
+ user_id = auth.uid() OR public.is_admin()
+ );
+
+CREATE POLICY "conductor_availability_insert_policy" ON public.conductor_availability
+ FOR INSERT
+ TO authenticated
+ WITH CHECK (
+ -- Users can create their own availability, admins can create for anyone
+ (user_id = auth.uid() AND created_by = auth.uid()) OR public.is_admin()
+ );
+
+CREATE POLICY "conductor_availability_update_policy" ON public.conductor_availability
+ FOR UPDATE
+ TO authenticated
+ USING (
+ -- Users can update their own availability, admins can update any
+ user_id = auth.uid() OR public.is_admin()
+ )
+ WITH CHECK (
+ -- Users can update their own availability, admins can update any
+ user_id = auth.uid() OR public.is_admin()
+ );
+
+CREATE POLICY "conductor_availability_delete_policy" ON public.conductor_availability
+ FOR DELETE
+ TO authenticated
+ USING (
+ -- Users can delete their own availability, admins can delete any
+ user_id = auth.uid() OR public.is_admin()
+ );
+
+-- Experiment phase assignments policies
+CREATE POLICY "experiment_phase_assignments_select_policy" ON public.experiment_phase_assignments
+ FOR SELECT
+ TO authenticated
+ USING (
+ -- Conductors can view their own assignments, admins can view all
+ conductor_id = auth.uid() OR public.is_admin()
+ );
+
+CREATE POLICY "experiment_phase_assignments_insert_policy" ON public.experiment_phase_assignments
+ FOR INSERT
+ TO authenticated
+ WITH CHECK (
+ -- Only admins and conductors can create assignments
+ public.can_manage_experiments()
+ );
+
+CREATE POLICY "experiment_phase_assignments_update_policy" ON public.experiment_phase_assignments
+ FOR UPDATE
+ TO authenticated
+ USING (
+ -- Conductors can update their own assignments, admins can update any
+ conductor_id = auth.uid() OR public.is_admin()
+ )
+ WITH CHECK (
+ -- Conductors can update their own assignments, admins can update any
+ conductor_id = auth.uid() OR public.is_admin()
+ );
+
+CREATE POLICY "experiment_phase_assignments_delete_policy" ON public.experiment_phase_assignments
+ FOR DELETE
+ TO authenticated
+ USING (
+ -- Only admins can delete assignments
+ public.is_admin()
+ );
+
+-- =============================================
+-- 8. COMMENTS FOR DOCUMENTATION
+-- =============================================
+
+COMMENT ON TABLE public.conductor_availability IS 'Stores conductor availability windows for experiment scheduling';
+COMMENT ON TABLE public.experiment_phase_assignments IS 'Assigns conductors to specific experiment repetition phases with scheduled times';
+
+COMMENT ON COLUMN public.conductor_availability.available_from IS 'Start time of availability window';
+COMMENT ON COLUMN public.conductor_availability.available_to IS 'End time of availability window';
+COMMENT ON COLUMN public.conductor_availability.notes IS 'Optional notes about the availability period';
+COMMENT ON COLUMN public.conductor_availability.status IS 'Status of the availability (active or cancelled)';
+
+COMMENT ON COLUMN public.experiment_phase_assignments.phase_name IS 'Experiment phase being assigned (pre-soaking, air-drying, cracking, shelling)';
+COMMENT ON COLUMN public.experiment_phase_assignments.scheduled_start_time IS 'Planned start time for the phase';
+COMMENT ON COLUMN public.experiment_phase_assignments.scheduled_end_time IS 'Planned end time for the phase';
+COMMENT ON COLUMN public.experiment_phase_assignments.status IS 'Current status of the assignment';
+COMMENT ON COLUMN public.experiment_phase_assignments.notes IS 'Optional notes about the assignment';
+
+
diff --git a/management-dashboard-web-app/supabase/migrations/20250103000001_add_password_reset_function.sql b/management-dashboard-web-app/supabase/migrations/20250103000001_add_password_reset_function.sql
new file mode 100644
index 0000000..c7bb98b
--- /dev/null
+++ b/management-dashboard-web-app/supabase/migrations/20250103000001_add_password_reset_function.sql
@@ -0,0 +1,49 @@
+-- Add password reset function for admin use
+-- This migration adds a function to reset user passwords back to the default "password123"
+
+-- Function to reset user password (admin only)
+CREATE OR REPLACE FUNCTION public.reset_user_password(
+ target_user_id UUID
+)
+RETURNS JSON AS $$
+DECLARE
+ user_email TEXT;
+ result JSON;
+BEGIN
+ -- Only admins can reset passwords
+ IF NOT public.is_admin() THEN
+ RAISE EXCEPTION 'Only administrators can reset user passwords';
+ END IF;
+
+ -- Check if target user exists
+ SELECT email INTO user_email
+ FROM public.user_profiles
+ WHERE id = target_user_id;
+
+ IF user_email IS NULL THEN
+ RAISE EXCEPTION 'User not found';
+ END IF;
+
+ -- Update the password in auth.users table
+ UPDATE auth.users
+ SET
+ encrypted_password = crypt('password123', gen_salt('bf')),
+ updated_at = NOW()
+ WHERE id = target_user_id;
+
+ -- Return result
+ result := json_build_object(
+ 'user_id', target_user_id,
+ 'email', user_email,
+ 'new_password', 'password123',
+ 'reset_at', NOW()
+ );
+
+ RETURN result;
+
+EXCEPTION
+ WHEN OTHERS THEN
+ RAISE;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
diff --git a/management-dashboard-web-app/supabase/migrations/20250103000002_add_user_names.sql b/management-dashboard-web-app/supabase/migrations/20250103000002_add_user_names.sql
new file mode 100644
index 0000000..46d2b71
--- /dev/null
+++ b/management-dashboard-web-app/supabase/migrations/20250103000002_add_user_names.sql
@@ -0,0 +1,12 @@
+-- Add first_name and last_name fields to user_profiles table
+-- This migration adds name fields to store user's first and last names
+
+-- Add first_name and last_name columns to user_profiles table
+ALTER TABLE public.user_profiles
+ADD COLUMN first_name TEXT,
+ADD COLUMN last_name TEXT;
+
+-- Add comments for documentation
+COMMENT ON COLUMN public.user_profiles.first_name IS 'User first name';
+COMMENT ON COLUMN public.user_profiles.last_name IS 'User last name';
+
diff --git a/management-dashboard-web-app/supabase/migrations/20250103000003_add_change_password_function.sql b/management-dashboard-web-app/supabase/migrations/20250103000003_add_change_password_function.sql
new file mode 100644
index 0000000..5942e5c
--- /dev/null
+++ b/management-dashboard-web-app/supabase/migrations/20250103000003_add_change_password_function.sql
@@ -0,0 +1,62 @@
+-- Add change password function for users
+-- This migration adds a function to allow users to change their own password
+
+-- Function to change user password (user can only change their own password)
+CREATE OR REPLACE FUNCTION public.change_user_password(
+ current_password TEXT,
+ new_password TEXT
+)
+RETURNS JSON AS $$
+DECLARE
+ user_id UUID;
+ user_email TEXT;
+ result JSON;
+BEGIN
+ -- Get current user ID
+ user_id := auth.uid();
+
+ IF user_id IS NULL THEN
+ RAISE EXCEPTION 'User not authenticated';
+ END IF;
+
+ -- Get user email
+ SELECT email INTO user_email
+ FROM public.user_profiles
+ WHERE id = user_id;
+
+ IF user_email IS NULL THEN
+ RAISE EXCEPTION 'User profile not found';
+ END IF;
+
+ -- Verify current password
+ IF NOT EXISTS (
+ SELECT 1
+ FROM auth.users
+ WHERE id = user_id
+ AND encrypted_password = crypt(current_password, encrypted_password)
+ ) THEN
+ RAISE EXCEPTION 'Current password is incorrect';
+ END IF;
+
+ -- Update the password in auth.users table
+ UPDATE auth.users
+ SET
+ encrypted_password = crypt(new_password, gen_salt('bf')),
+ updated_at = NOW()
+ WHERE id = user_id;
+
+ -- Return result
+ result := json_build_object(
+ 'user_id', user_id,
+ 'email', user_email,
+ 'password_changed_at', NOW()
+ );
+
+ RETURN result;
+
+EXCEPTION
+ WHEN OTHERS THEN
+ RAISE;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
diff --git a/management-dashboard-web-app/supabase/seed.sql b/management-dashboard-web-app/supabase/seed.sql
index 79b6a2f..8a2134c 100755
--- a/management-dashboard-web-app/supabase/seed.sql
+++ b/management-dashboard-web-app/supabase/seed.sql
@@ -47,8 +47,8 @@ INSERT INTO auth.users (
);
-- Create user profile
-INSERT INTO public.user_profiles (id, email, status)
-SELECT id, email, 'active'
+INSERT INTO public.user_profiles (id, email, first_name, last_name, status)
+SELECT id, email, 'Alireza', 'Vaezi', 'active'
FROM auth.users
WHERE email = 's.alireza.v@gmail.com'
;
@@ -66,7 +66,443 @@ AND r.name = 'admin'
;
-- =============================================
--- 3. CREATE EXPERIMENT PHASES
+-- 3. CREATE ADDITIONAL USERS
+-- =============================================
+
+-- Create Claire Floyd (Conductor & Data Recorder)
+INSERT INTO auth.users (
+ instance_id,
+ id,
+ aud,
+ role,
+ email,
+ encrypted_password,
+ email_confirmed_at,
+ created_at,
+ updated_at,
+ confirmation_token,
+ email_change,
+ email_change_token_new,
+ recovery_token
+) VALUES (
+ '00000000-0000-0000-0000-000000000000',
+ uuid_generate_v4(),
+ 'authenticated',
+ 'authenticated',
+ 'Ashlyn.Floyd@uga.edu',
+ crypt('password123', gen_salt('bf')),
+ NOW(),
+ NOW(),
+ NOW(),
+ '',
+ '',
+ '',
+ ''
+);
+
+INSERT INTO public.user_profiles (id, email, first_name, last_name, status)
+SELECT id, email, 'Claire', 'Floyd', 'active'
+FROM auth.users
+WHERE email = 'Ashlyn.Floyd@uga.edu'
+;
+
+INSERT INTO public.user_roles (user_id, role_id, assigned_by)
+SELECT
+ up.id,
+ r.id,
+ (SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')
+FROM public.user_profiles up
+CROSS JOIN public.roles r
+WHERE up.email = 'Ashlyn.Floyd@uga.edu'
+AND r.name IN ('conductor', 'data recorder')
+;
+
+-- Create Bruna Dos-Santos (Conductor & Data Recorder)
+INSERT INTO auth.users (
+ instance_id,
+ id,
+ aud,
+ role,
+ email,
+ encrypted_password,
+ email_confirmed_at,
+ created_at,
+ updated_at,
+ confirmation_token,
+ email_change,
+ email_change_token_new,
+ recovery_token
+) VALUES (
+ '00000000-0000-0000-0000-000000000000',
+ uuid_generate_v4(),
+ 'authenticated',
+ 'authenticated',
+ 'bkvsantos@uga.edu',
+ crypt('password123', gen_salt('bf')),
+ NOW(),
+ NOW(),
+ NOW(),
+ '',
+ '',
+ '',
+ ''
+);
+
+INSERT INTO public.user_profiles (id, email, first_name, last_name, status)
+SELECT id, email, 'Bruna', 'Dos-Santos', 'active'
+FROM auth.users
+WHERE email = 'bkvsantos@uga.edu'
+;
+
+INSERT INTO public.user_roles (user_id, role_id, assigned_by)
+SELECT
+ up.id,
+ r.id,
+ (SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')
+FROM public.user_profiles up
+CROSS JOIN public.roles r
+WHERE up.email = 'bkvsantos@uga.edu'
+AND r.name IN ('conductor', 'data recorder')
+;
+
+-- Create Beni Rodriguez (Conductor & Data Recorder)
+INSERT INTO auth.users (
+ instance_id,
+ id,
+ aud,
+ role,
+ email,
+ encrypted_password,
+ email_confirmed_at,
+ created_at,
+ updated_at,
+ confirmation_token,
+ email_change,
+ email_change_token_new,
+ recovery_token
+) VALUES (
+ '00000000-0000-0000-0000-000000000000',
+ uuid_generate_v4(),
+ 'authenticated',
+ 'authenticated',
+ 'Beni.Rodriguez@uga.edu',
+ crypt('password123', gen_salt('bf')),
+ NOW(),
+ NOW(),
+ NOW(),
+ '',
+ '',
+ '',
+ ''
+);
+
+INSERT INTO public.user_profiles (id, email, first_name, last_name, status)
+SELECT id, email, 'Beni', 'Rodriguez', 'active'
+FROM auth.users
+WHERE email = 'Beni.Rodriguez@uga.edu'
+;
+
+INSERT INTO public.user_roles (user_id, role_id, assigned_by)
+SELECT
+ up.id,
+ r.id,
+ (SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')
+FROM public.user_profiles up
+CROSS JOIN public.roles r
+WHERE up.email = 'Beni.Rodriguez@uga.edu'
+AND r.name IN ('conductor', 'data recorder')
+;
+
+-- Create Brendan Surio (Data Recorder)
+INSERT INTO auth.users (
+ instance_id,
+ id,
+ aud,
+ role,
+ email,
+ encrypted_password,
+ email_confirmed_at,
+ created_at,
+ updated_at,
+ confirmation_token,
+ email_change,
+ email_change_token_new,
+ recovery_token
+) VALUES (
+ '00000000-0000-0000-0000-000000000000',
+ uuid_generate_v4(),
+ 'authenticated',
+ 'authenticated',
+ 'Brendan.Surio@uga.edu',
+ crypt('password123', gen_salt('bf')),
+ NOW(),
+ NOW(),
+ NOW(),
+ '',
+ '',
+ '',
+ ''
+);
+
+INSERT INTO public.user_profiles (id, email, first_name, last_name, status)
+SELECT id, email, 'Brendan', 'Surio', 'active'
+FROM auth.users
+WHERE email = 'Brendan.Surio@uga.edu'
+;
+
+INSERT INTO public.user_roles (user_id, role_id, assigned_by)
+SELECT
+ up.id,
+ r.id,
+ (SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')
+FROM public.user_profiles up
+CROSS JOIN public.roles r
+WHERE up.email = 'Brendan.Surio@uga.edu'
+AND r.name = 'data recorder'
+;
+
+-- Create William Mcconnell (Data Recorder)
+INSERT INTO auth.users (
+ instance_id,
+ id,
+ aud,
+ role,
+ email,
+ encrypted_password,
+ email_confirmed_at,
+ created_at,
+ updated_at,
+ confirmation_token,
+ email_change,
+ email_change_token_new,
+ recovery_token
+) VALUES (
+ '00000000-0000-0000-0000-000000000000',
+ uuid_generate_v4(),
+ 'authenticated',
+ 'authenticated',
+ 'William.McConnell@uga.edu',
+ crypt('password123', gen_salt('bf')),
+ NOW(),
+ NOW(),
+ NOW(),
+ '',
+ '',
+ '',
+ ''
+);
+
+INSERT INTO public.user_profiles (id, email, first_name, last_name, status)
+SELECT id, email, 'William', 'Mcconnell', 'active'
+FROM auth.users
+WHERE email = 'William.McConnell@uga.edu'
+;
+
+INSERT INTO public.user_roles (user_id, role_id, assigned_by)
+SELECT
+ up.id,
+ r.id,
+ (SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')
+FROM public.user_profiles up
+CROSS JOIN public.roles r
+WHERE up.email = 'William.McConnell@uga.edu'
+AND r.name = 'data recorder'
+;
+
+-- Create Camille Deguzman (Data Recorder)
+INSERT INTO auth.users (
+ instance_id,
+ id,
+ aud,
+ role,
+ email,
+ encrypted_password,
+ email_confirmed_at,
+ created_at,
+ updated_at,
+ confirmation_token,
+ email_change,
+ email_change_token_new,
+ recovery_token
+) VALUES (
+ '00000000-0000-0000-0000-000000000000',
+ uuid_generate_v4(),
+ 'authenticated',
+ 'authenticated',
+ 'cpd08598@uga.edu',
+ crypt('password123', gen_salt('bf')),
+ NOW(),
+ NOW(),
+ NOW(),
+ '',
+ '',
+ '',
+ ''
+);
+
+INSERT INTO public.user_profiles (id, email, first_name, last_name, status)
+SELECT id, email, 'Camille', 'Deguzman', 'active'
+FROM auth.users
+WHERE email = 'cpd08598@uga.edu'
+;
+
+INSERT INTO public.user_roles (user_id, role_id, assigned_by)
+SELECT
+ up.id,
+ r.id,
+ (SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')
+FROM public.user_profiles up
+CROSS JOIN public.roles r
+WHERE up.email = 'cpd08598@uga.edu'
+AND r.name = 'data recorder'
+;
+
+-- Create Justin Hetzler (Data Recorder)
+INSERT INTO auth.users (
+ instance_id,
+ id,
+ aud,
+ role,
+ email,
+ encrypted_password,
+ email_confirmed_at,
+ created_at,
+ updated_at,
+ confirmation_token,
+ email_change,
+ email_change_token_new,
+ recovery_token
+) VALUES (
+ '00000000-0000-0000-0000-000000000000',
+ uuid_generate_v4(),
+ 'authenticated',
+ 'authenticated',
+ 'Justin.Hetzler@uga.edu',
+ crypt('password123', gen_salt('bf')),
+ NOW(),
+ NOW(),
+ NOW(),
+ '',
+ '',
+ '',
+ ''
+);
+
+INSERT INTO public.user_profiles (id, email, first_name, last_name, status)
+SELECT id, email, 'Justin', 'Hetzler', 'active'
+FROM auth.users
+WHERE email = 'Justin.Hetzler@uga.edu'
+;
+
+INSERT INTO public.user_roles (user_id, role_id, assigned_by)
+SELECT
+ up.id,
+ r.id,
+ (SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')
+FROM public.user_profiles up
+CROSS JOIN public.roles r
+WHERE up.email = 'Justin.Hetzler@uga.edu'
+AND r.name = 'data recorder'
+;
+
+-- Create Joshua Wilson (Data Recorder)
+INSERT INTO auth.users (
+ instance_id,
+ id,
+ aud,
+ role,
+ email,
+ encrypted_password,
+ email_confirmed_at,
+ created_at,
+ updated_at,
+ confirmation_token,
+ email_change,
+ email_change_token_new,
+ recovery_token
+) VALUES (
+ '00000000-0000-0000-0000-000000000000',
+ uuid_generate_v4(),
+ 'authenticated',
+ 'authenticated',
+ 'jdw58940@uga.edu',
+ crypt('password123', gen_salt('bf')),
+ NOW(),
+ NOW(),
+ NOW(),
+ '',
+ '',
+ '',
+ ''
+);
+
+INSERT INTO public.user_profiles (id, email, first_name, last_name, status)
+SELECT id, email, 'Joshua', 'Wilson', 'active'
+FROM auth.users
+WHERE email = 'jdw58940@uga.edu'
+;
+
+INSERT INTO public.user_roles (user_id, role_id, assigned_by)
+SELECT
+ up.id,
+ r.id,
+ (SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')
+FROM public.user_profiles up
+CROSS JOIN public.roles r
+WHERE up.email = 'jdw58940@uga.edu'
+AND r.name = 'data recorder'
+;
+
+-- Create Sydney Orlofsky (Data Recorder)
+INSERT INTO auth.users (
+ instance_id,
+ id,
+ aud,
+ role,
+ email,
+ encrypted_password,
+ email_confirmed_at,
+ created_at,
+ updated_at,
+ confirmation_token,
+ email_change,
+ email_change_token_new,
+ recovery_token
+) VALUES (
+ '00000000-0000-0000-0000-000000000000',
+ uuid_generate_v4(),
+ 'authenticated',
+ 'authenticated',
+ 'Sydney.Orlofsky@uga.edu',
+ crypt('password123', gen_salt('bf')),
+ NOW(),
+ NOW(),
+ NOW(),
+ '',
+ '',
+ '',
+ ''
+);
+
+INSERT INTO public.user_profiles (id, email, first_name, last_name, status)
+SELECT id, email, 'Sydney', 'Orlofsky', 'active'
+FROM auth.users
+WHERE email = 'Sydney.Orlofsky@uga.edu'
+;
+
+INSERT INTO public.user_roles (user_id, role_id, assigned_by)
+SELECT
+ up.id,
+ r.id,
+ (SELECT id FROM public.user_profiles WHERE email = 's.alireza.v@gmail.com')
+FROM public.user_profiles up
+CROSS JOIN public.roles r
+WHERE up.email = 'Sydney.Orlofsky@uga.edu'
+AND r.name = 'data recorder'
+;
+
+-- =============================================
+-- 4. CREATE EXPERIMENT PHASES
-- =============================================
-- Create "Phase 2 of JC Experiments" phase
@@ -80,7 +516,7 @@ WHERE up.email = 's.alireza.v@gmail.com'
;
-- =============================================
--- 4. INSERT EXPERIMENTS (First 10 as example)
+-- 5. INSERT EXPERIMENTS (First 10 as example)
-- =============================================
INSERT INTO public.experiments (
@@ -130,7 +566,7 @@ INSERT INTO public.experiments (
;
-- =============================================
--- 5. CREATE SAMPLE EXPERIMENT REPETITIONS
+-- 6. CREATE SAMPLE EXPERIMENT REPETITIONS
-- =============================================
-- Create repetitions for first 5 experiments as examples