- Changed VITE_SUPABASE_URL in .env.example for deployment consistency. - Added new user management functionality to reset user passwords in UserManagement component. - Updated supabase.ts to include first and last name fields in user profiles and added password reset functionality. - Enhanced DashboardLayout to include a user profile view and improved user display in TopNavbar. - Updated seed.sql to create additional users with roles for testing purposes.
289 lines
8.9 KiB
TypeScript
Executable File
289 lines
8.9 KiB
TypeScript
Executable File
import { useState, useEffect } from 'react'
|
|
import { Sidebar } from './Sidebar'
|
|
import { TopNavbar } from './TopNavbar'
|
|
import { DashboardHome } from './DashboardHome'
|
|
import { UserManagement } from './UserManagement'
|
|
import { ExperimentManagement } from './ExperimentManagement'
|
|
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 {
|
|
onLogout: () => void
|
|
currentRoute: string
|
|
}
|
|
|
|
export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps) {
|
|
const [user, setUser] = useState<User | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [currentView, setCurrentView] = useState('dashboard')
|
|
const [isExpanded, setIsExpanded] = useState(true)
|
|
const [isMobileOpen, setIsMobileOpen] = useState(false)
|
|
const [isHovered, setIsHovered] = useState(false)
|
|
|
|
// Valid dashboard views
|
|
const validViews = ['dashboard', 'user-management', 'experiments', 'analytics', 'data-entry', 'vision-system', 'scheduling', 'video-library', 'profile']
|
|
|
|
// Save current view to localStorage
|
|
const saveCurrentView = (view: string) => {
|
|
try {
|
|
localStorage.setItem('dashboard-current-view', view)
|
|
} catch (error) {
|
|
console.warn('Failed to save current view to localStorage:', error)
|
|
}
|
|
}
|
|
|
|
// Get saved view from localStorage
|
|
const getSavedView = (): string => {
|
|
try {
|
|
const savedView = localStorage.getItem('dashboard-current-view')
|
|
return savedView && validViews.includes(savedView) ? savedView : 'dashboard'
|
|
} catch (error) {
|
|
console.warn('Failed to get saved view from localStorage:', error)
|
|
return 'dashboard'
|
|
}
|
|
}
|
|
|
|
// Check if user has access to a specific view
|
|
const hasAccessToView = (view: string): boolean => {
|
|
if (!user) return false
|
|
|
|
// Admin-only views
|
|
if (view === 'user-management') {
|
|
return user.roles.includes('admin')
|
|
}
|
|
|
|
// All other views are accessible to authenticated users
|
|
return true
|
|
}
|
|
|
|
// Handle view change with persistence and URL updates
|
|
const handleViewChange = (view: string) => {
|
|
if (validViews.includes(view) && hasAccessToView(view)) {
|
|
setCurrentView(view)
|
|
saveCurrentView(view)
|
|
|
|
// Update URL
|
|
const newPath = view === 'dashboard' ? '/' : `/${view}`
|
|
window.history.pushState({}, '', newPath)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchUserProfile()
|
|
}, [])
|
|
|
|
// Restore saved view when user is loaded
|
|
useEffect(() => {
|
|
if (user) {
|
|
const savedView = getSavedView()
|
|
if (hasAccessToView(savedView)) {
|
|
setCurrentView(savedView)
|
|
} else {
|
|
// If user doesn't have access to saved view, default to dashboard
|
|
setCurrentView('dashboard')
|
|
saveCurrentView('dashboard')
|
|
}
|
|
}
|
|
}, [user])
|
|
|
|
// Handle route changes for scheduling sub-routes
|
|
useEffect(() => {
|
|
if (currentRoute.startsWith('/scheduling')) {
|
|
setCurrentView('scheduling')
|
|
} else if (currentRoute === '/') {
|
|
// Handle root route - use saved view or default to dashboard
|
|
if (user) {
|
|
const savedView = getSavedView()
|
|
if (hasAccessToView(savedView)) {
|
|
setCurrentView(savedView)
|
|
} else {
|
|
setCurrentView('dashboard')
|
|
}
|
|
}
|
|
} else {
|
|
// Handle other routes by extracting the view name
|
|
const routeView = currentRoute.substring(1) // Remove leading slash
|
|
if (validViews.includes(routeView) && hasAccessToView(routeView)) {
|
|
setCurrentView(routeView)
|
|
}
|
|
}
|
|
}, [currentRoute, user])
|
|
|
|
const fetchUserProfile = async () => {
|
|
try {
|
|
setLoading(true)
|
|
setError(null)
|
|
|
|
const currentUser = await userManagement.getCurrentUser()
|
|
if (currentUser) {
|
|
setUser(currentUser)
|
|
} else {
|
|
setError('No authenticated user found')
|
|
}
|
|
} catch (err) {
|
|
setError('Failed to fetch user profile')
|
|
console.error('Profile fetch error:', err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleLogout = async () => {
|
|
// Call the logout function passed from parent
|
|
onLogout()
|
|
}
|
|
|
|
const toggleSidebar = () => {
|
|
setIsExpanded(!isExpanded)
|
|
}
|
|
|
|
const toggleMobileSidebar = () => {
|
|
setIsMobileOpen(!isMobileOpen)
|
|
}
|
|
|
|
const handleToggleSidebar = () => {
|
|
if (window.innerWidth >= 1024) {
|
|
toggleSidebar()
|
|
} else {
|
|
toggleMobileSidebar()
|
|
}
|
|
}
|
|
|
|
const renderCurrentView = () => {
|
|
if (!user) return null
|
|
|
|
switch (currentView) {
|
|
case 'dashboard':
|
|
return <DashboardHome user={user} />
|
|
case 'user-management':
|
|
if (user.roles.includes('admin')) {
|
|
return <UserManagement />
|
|
} else {
|
|
return (
|
|
<div className="p-6">
|
|
<div className="bg-red-50 border border-red-200 rounded-md p-4">
|
|
<div className="text-sm text-red-700">
|
|
Access denied. You need admin privileges to access user management.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
case 'experiments':
|
|
return <ExperimentManagement />
|
|
case 'analytics':
|
|
return (
|
|
<div className="p-6">
|
|
<h1 className="text-2xl font-bold text-gray-900 mb-4">Analytics</h1>
|
|
<div className="bg-green-50 border border-green-200 rounded-md p-4">
|
|
<div className="text-sm text-green-700">
|
|
Analytics module coming soon...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
case 'data-entry':
|
|
return <DataEntry />
|
|
case 'vision-system':
|
|
return <VisionSystem />
|
|
case 'scheduling':
|
|
return <Scheduling user={user} currentRoute={currentRoute} />
|
|
case 'video-library':
|
|
return <VideoStreamingPage />
|
|
case 'profile':
|
|
return <UserProfile user={user} />
|
|
default:
|
|
return <DashboardHome user={user} />
|
|
}
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-brand-500 mx-auto"></div>
|
|
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading dashboard...</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
|
<div className="max-w-md w-full">
|
|
<div className="rounded-2xl bg-error-50 border border-error-200 p-4 dark:bg-error-500/15 dark:border-error-500/20">
|
|
<div className="text-sm text-error-700 dark:text-error-500">{error}</div>
|
|
</div>
|
|
<button
|
|
onClick={handleLogout}
|
|
className="mt-4 w-full flex justify-center py-2.5 px-4 border border-transparent text-sm font-medium rounded-lg text-white bg-gray-600 hover:bg-gray-700 dark:bg-gray-700 dark:hover:bg-gray-600 transition-colors"
|
|
>
|
|
Back to Login
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!user) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
|
<div className="text-center">
|
|
<div className="text-gray-600 dark:text-gray-400">No user data available</div>
|
|
<button
|
|
onClick={handleLogout}
|
|
className="mt-4 px-4 py-2.5 bg-gray-600 text-white rounded-lg hover:bg-gray-700 dark:bg-gray-700 dark:hover:bg-gray-600 transition-colors"
|
|
>
|
|
Back to Login
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen xl:flex">
|
|
<div>
|
|
<Sidebar
|
|
user={user}
|
|
currentView={currentView}
|
|
onViewChange={handleViewChange}
|
|
isExpanded={isExpanded}
|
|
isMobileOpen={isMobileOpen}
|
|
isHovered={isHovered}
|
|
setIsHovered={setIsHovered}
|
|
/>
|
|
{/* Backdrop for mobile */}
|
|
{isMobileOpen && (
|
|
<div
|
|
className="fixed inset-0 z-40 bg-gray-900/50 lg:hidden"
|
|
onClick={() => setIsMobileOpen(false)}
|
|
/>
|
|
)}
|
|
</div>
|
|
<div
|
|
className={`flex-1 transition-all duration-300 ease-in-out ${isExpanded || isHovered ? "lg:ml-[290px]" : "lg:ml-[90px]"
|
|
} ${isMobileOpen ? "ml-0" : ""}`}
|
|
>
|
|
<TopNavbar
|
|
user={user}
|
|
onLogout={handleLogout}
|
|
currentView={currentView}
|
|
onToggleSidebar={handleToggleSidebar}
|
|
isSidebarOpen={isMobileOpen}
|
|
onNavigateToProfile={() => handleViewChange('profile')}
|
|
/>
|
|
<div className="p-4 mx-auto max-w-(--breakpoint-2xl) md:p-6">
|
|
{renderCurrentView()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|