- Added VisionApiClient class to interact with the vision system API. - Defined interfaces for system status, machine status, camera status, recordings, and storage stats. - Implemented methods for health checks, system status retrieval, camera control, and storage management. - Introduced utility functions for formatting bytes, durations, and uptime. test: Create manual verification script for Vision API functionality - Added a test script to verify utility functions and API endpoints. - Included tests for health check, system status, cameras, machines, and storage stats. feat: Create experiment repetitions system migration - Added experiment_repetitions table to manage experiment repetitions with scheduling. - Implemented triggers and functions for validation and timestamp management. - Established row-level security policies for user access control. feat: Introduce phase-specific draft management system migration - Created experiment_phase_drafts and experiment_phase_data tables for managing phase-specific drafts and measurements. - Added pecan_diameter_measurements table for individual diameter measurements. - Implemented row-level security policies for user access control. fix: Adjust draft constraints to allow multiple drafts while preventing multiple submitted drafts - Modified constraints on experiment_phase_drafts to allow multiple drafts in 'draft' or 'withdrawn' status. - Ensured only one 'submitted' draft per user per phase per repetition.
152 lines
6.2 KiB
TypeScript
152 lines
6.2 KiB
TypeScript
import { useState } from 'react'
|
|
import type { User } from '../lib/supabase'
|
|
|
|
interface SidebarProps {
|
|
user: User
|
|
currentView: string
|
|
onViewChange: (view: string) => void
|
|
}
|
|
|
|
interface MenuItem {
|
|
id: string
|
|
name: string
|
|
icon: React.ReactElement
|
|
requiredRoles?: string[]
|
|
}
|
|
|
|
export function Sidebar({ user, currentView, onViewChange }: SidebarProps) {
|
|
const [isCollapsed, setIsCollapsed] = useState(false)
|
|
|
|
const menuItems: MenuItem[] = [
|
|
{
|
|
id: 'dashboard',
|
|
name: 'Dashboard',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 5a2 2 0 012-2h4a2 2 0 012 2v6a2 2 0 01-2 2H10a2 2 0 01-2-2V5z" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
id: 'user-management',
|
|
name: 'User Management',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
|
|
</svg>
|
|
),
|
|
requiredRoles: ['admin']
|
|
},
|
|
{
|
|
id: 'experiments',
|
|
name: 'Experiments',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
|
|
</svg>
|
|
),
|
|
requiredRoles: ['admin', 'conductor']
|
|
},
|
|
{
|
|
id: 'analytics',
|
|
name: 'Analytics',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
</svg>
|
|
),
|
|
requiredRoles: ['admin', 'conductor', 'analyst']
|
|
},
|
|
{
|
|
id: 'data-entry',
|
|
name: 'Data Entry',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
</svg>
|
|
),
|
|
requiredRoles: ['admin', 'conductor', 'data recorder']
|
|
},
|
|
{
|
|
id: 'vision-system',
|
|
name: 'Vision System',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
</svg>
|
|
),
|
|
}
|
|
]
|
|
|
|
const hasAccess = (item: MenuItem): boolean => {
|
|
if (!item.requiredRoles) return true
|
|
return item.requiredRoles.some(role => user.roles.includes(role as any))
|
|
}
|
|
|
|
return (
|
|
<div className={`bg-slate-800 transition-all duration-300 ${isCollapsed ? 'w-16' : 'w-64'} min-h-screen flex flex-col shadow-xl relative z-20`}>
|
|
{/* Header */}
|
|
<div className="p-4 border-b border-slate-700">
|
|
<div className="flex items-center justify-between">
|
|
{!isCollapsed && (
|
|
<div>
|
|
<h1 className="text-xl font-bold text-white">Pecan Experiments</h1>
|
|
<p className="text-sm text-slate-400">Admin Dashboard</p>
|
|
</div>
|
|
)}
|
|
<button
|
|
onClick={() => setIsCollapsed(!isCollapsed)}
|
|
className="p-2 rounded-lg hover:bg-slate-700 transition-colors text-slate-400 hover:text-white"
|
|
title={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
{isCollapsed ? (
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
) : (
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
)}
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Navigation Menu */}
|
|
<nav className="flex-1 p-4">
|
|
<ul className="space-y-2">
|
|
{menuItems.map((item) => {
|
|
if (!hasAccess(item)) return null
|
|
|
|
return (
|
|
<li key={item.id}>
|
|
<button
|
|
onClick={() => onViewChange(item.id)}
|
|
className={`w-full flex items-center px-3 py-3 rounded-lg transition-all duration-200 group ${currentView === item.id
|
|
? 'bg-blue-600 text-white shadow-lg'
|
|
: 'text-slate-300 hover:bg-slate-700 hover:text-white'
|
|
}`}
|
|
title={isCollapsed ? item.name : undefined}
|
|
>
|
|
<span className={`transition-colors ${currentView === item.id ? 'text-white' : 'text-slate-400 group-hover:text-white'}`}>
|
|
{item.icon}
|
|
</span>
|
|
{!isCollapsed && (
|
|
<span className="ml-3 text-sm font-medium">{item.name}</span>
|
|
)}
|
|
{!isCollapsed && currentView === item.id && (
|
|
<div className="ml-auto">
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</div>
|
|
)}
|
|
</button>
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
)
|
|
}
|