Add scheduling-remote service to docker-compose and enhance camera error handling

- Introduced a new service for scheduling-remote in docker-compose.yml, allowing for better management of scheduling functionalities.
- Enhanced error handling in CameraMonitor and CameraStreamer classes to improve robustness during camera initialization and streaming processes.
- Updated various components in the management dashboard to support dark mode and improve user experience with consistent styling.
- Implemented feature flags for enabling/disabling modules, including the new scheduling module.
This commit is contained in:
salirezav
2025-11-02 19:33:13 -05:00
parent f6a37ca1ba
commit 868aa3f036
33 changed files with 7471 additions and 136 deletions

View File

@@ -115,7 +115,7 @@ function App() {
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading...</p>
@@ -127,7 +127,7 @@ function App() {
// Handle signout route
if (currentRoute === '/signout') {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Signing out...</p>

View File

@@ -7,7 +7,8 @@ import { ExperimentManagement } from './ExperimentManagement'
import { DataEntry } from './DataEntry'
// VisionSystem is now loaded as a microfrontend - see RemoteVisionSystem below
// import { VisionSystem } from './VisionSystem'
import { Scheduling } from './Scheduling'
// Scheduling is now loaded as a microfrontend - see RemoteScheduling below
// import { Scheduling } from './Scheduling'
import React, { Suspense } from 'react'
import { loadRemoteComponent } from '../lib/loadRemote'
import { ErrorBoundary } from './ErrorBoundary'
@@ -172,6 +173,13 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps
LocalVisionSystemPlaceholder as any
) as unknown as React.ComponentType
const LocalSchedulingPlaceholder = () => (<div className="p-6">Scheduling module not enabled.</div>)
const RemoteScheduling = loadRemoteComponent(
isFeatureEnabled('enableSchedulingModule'),
() => import('schedulingRemote/App'),
LocalSchedulingPlaceholder as any
) as unknown as React.ComponentType<{ user: User; currentRoute: string }>
const renderCurrentView = () => {
if (!user) return null
@@ -216,7 +224,13 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps
</ErrorBoundary>
)
case 'scheduling':
return <Scheduling user={user} currentRoute={currentRoute} />
return (
<ErrorBoundary fallback={<div className="p-6">Failed to load scheduling module. Please try again.</div>}>
<Suspense fallback={<div className="p-6">Loading scheduling module...</div>}>
<RemoteScheduling user={user} currentRoute={currentRoute} />
</Suspense>
</ErrorBoundary>
)
case 'video-library':
return (
<ErrorBoundary fallback={<div className="p-6">Failed to load video module. Please try again.</div>}>
@@ -234,7 +248,7 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
<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>
@@ -245,7 +259,7 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps
if (error) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
<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>
@@ -263,7 +277,7 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps
if (!user) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
<div className="text-center">
<div className="text-gray-600 dark:text-gray-400">No user data available</div>
<button
@@ -298,7 +312,7 @@ export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps
)}
</div>
<div
className={`flex-1 transition-all duration-300 ease-in-out ${isExpanded || isHovered ? "lg:ml-[290px]" : "lg:ml-[90px]"
className={`flex-1 transition-all duration-300 ease-in-out bg-gray-50 dark:bg-gray-900 ${isExpanded || isHovered ? "lg:ml-[290px]" : "lg:ml-[90px]"
} ${isMobileOpen ? "ml-0" : ""}`}
>
<TopNavbar

View File

@@ -98,7 +98,7 @@ export function DataEntry() {
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading experiments...</p>
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading experiments...</p>
</div>
</div>
</div>
@@ -108,8 +108,8 @@ export function DataEntry() {
if (error) {
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">{error}</div>
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md p-4">
<div className="text-sm text-red-700 dark:text-red-400">{error}</div>
</div>
</div>
)
@@ -129,8 +129,8 @@ export function DataEntry() {
return (
<div className="p-6">
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900">Data Entry</h1>
<p className="mt-1 text-sm text-gray-500">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Data Entry</h1>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Select a repetition to enter measurement data
</p>
</div>
@@ -142,13 +142,13 @@ export function DataEntry() {
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Past/Completed Repetitions */}
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
<div className="px-4 py-5 sm:px-6 border-b border-gray-200">
<h2 className="text-xl font-bold text-gray-900 flex items-center">
<div className="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-lg border border-gray-200 dark:border-gray-700">
<div className="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-xl font-bold text-gray-900 dark:text-white flex items-center">
<span className="w-4 h-4 bg-green-500 rounded-full mr-3"></span>
Past/Completed ({pastRepetitions.length})
</h2>
<p className="mt-1 text-sm text-gray-500">
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Completed or past scheduled repetitions
</p>
</div>
@@ -164,7 +164,7 @@ export function DataEntry() {
/>
))}
{pastRepetitions.length === 0 && (
<p className="text-sm text-gray-500 italic text-center py-8">
<p className="text-sm text-gray-500 dark:text-gray-400 italic text-center py-8">
No completed repetitions
</p>
)}
@@ -173,13 +173,13 @@ export function DataEntry() {
</div>
{/* In Progress Repetitions */}
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
<div className="px-4 py-5 sm:px-6 border-b border-gray-200">
<h2 className="text-xl font-bold text-gray-900 flex items-center">
<div className="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-lg border border-gray-200 dark:border-gray-700">
<div className="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-xl font-bold text-gray-900 dark:text-white flex items-center">
<span className="w-4 h-4 bg-blue-500 rounded-full mr-3"></span>
In Progress ({inProgressRepetitions.length})
</h2>
<p className="mt-1 text-sm text-gray-500">
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Currently scheduled or active repetitions
</p>
</div>
@@ -195,7 +195,7 @@ export function DataEntry() {
/>
))}
{inProgressRepetitions.length === 0 && (
<p className="text-sm text-gray-500 italic text-center py-8">
<p className="text-sm text-gray-500 dark:text-gray-400 italic text-center py-8">
No repetitions in progress
</p>
)}
@@ -204,13 +204,13 @@ export function DataEntry() {
</div>
{/* Upcoming Repetitions */}
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
<div className="px-4 py-5 sm:px-6 border-b border-gray-200">
<h2 className="text-xl font-bold text-gray-900 flex items-center">
<div className="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-lg border border-gray-200 dark:border-gray-700">
<div className="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-xl font-bold text-gray-900 dark:text-white flex items-center">
<span className="w-4 h-4 bg-yellow-500 rounded-full mr-3"></span>
Upcoming ({upcomingRepetitions.length})
</h2>
<p className="mt-1 text-sm text-gray-500">
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Future scheduled repetitions
</p>
</div>
@@ -226,7 +226,7 @@ export function DataEntry() {
/>
))}
{upcomingRepetitions.length === 0 && (
<p className="text-sm text-gray-500 italic text-center py-8">
<p className="text-sm text-gray-500 dark:text-gray-400 italic text-center py-8">
No upcoming repetitions
</p>
)}
@@ -239,7 +239,7 @@ export function DataEntry() {
{experiments.length === 0 && (
<div className="text-center py-12">
<div className="text-gray-500">
<div className="text-gray-500 dark:text-gray-400">
No experiments available for data entry
</div>
</div>
@@ -291,35 +291,35 @@ function RepetitionCard({ experiment, repetition, onSelect, status }: Repetition
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-3">
{/* Large, bold experiment number */}
<span className="text-2xl font-bold text-gray-900">
<span className="text-2xl font-bold text-gray-900 dark:text-white">
#{experiment.experiment_number}
</span>
{/* Smaller repetition number */}
<span className="text-lg font-semibold text-gray-700">
<span className="text-lg font-semibold text-gray-700 dark:text-gray-300">
Rep #{repetition.repetition_number}
</span>
<span className="text-lg">{getStatusIcon()}</span>
<span className="text-lg dark:text-white">{getStatusIcon()}</span>
</div>
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${repetition.scheduled_date
? 'bg-blue-100 text-blue-800'
: 'bg-yellow-100 text-yellow-800'
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300'
: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300'
}`}>
{repetition.scheduled_date ? 'scheduled' : 'pending'}
</span>
</div>
{/* Experiment details */}
<div className="text-sm text-gray-600 mb-2">
<div className="text-sm text-gray-600 dark:text-gray-400 mb-2">
{experiment.soaking_duration_hr}h soaking {experiment.air_drying_time_min}min drying
</div>
{repetition.scheduled_date && (
<div className="text-sm text-gray-600 mb-2">
<strong>Scheduled:</strong> {new Date(repetition.scheduled_date).toLocaleString()}
<div className="text-sm text-gray-600 dark:text-gray-400 mb-2">
<strong className="dark:text-white">Scheduled:</strong> {new Date(repetition.scheduled_date).toLocaleString()}
</div>
)}
<div className="text-xs text-gray-500">
<div className="text-xs text-gray-500 dark:text-gray-400">
Click to enter data for this repetition
</div>
</button>

View File

@@ -61,9 +61,9 @@ export function ExperimentPhases({ onPhaseSelect }: ExperimentPhasesProps) {
<div className="mb-8">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-gray-900">Experiment Phases</h1>
<p className="mt-2 text-gray-600">Select an experiment phase to view and manage its experiments</p>
<p className="mt-2 text-gray-600">Experiment phases help organize experiments into logical groups for easier navigation and management.</p>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Experiment Phases</h1>
<p className="mt-2 text-gray-600 dark:text-gray-400">Select an experiment phase to view and manage its experiments</p>
<p className="mt-2 text-gray-600 dark:text-gray-400">Experiment phases help organize experiments into logical groups for easier navigation and management.</p>
</div>
{canManagePhases && (
<button
@@ -78,8 +78,8 @@ export function ExperimentPhases({ onPhaseSelect }: ExperimentPhasesProps) {
{/* Error Message */}
{error && (
<div className="mb-6 rounded-md bg-red-50 p-4">
<div className="text-sm text-red-700">{error}</div>
<div className="mb-6 rounded-md bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-4">
<div className="text-sm text-red-700 dark:text-red-400">{error}</div>
</div>
)}
@@ -89,30 +89,30 @@ export function ExperimentPhases({ onPhaseSelect }: ExperimentPhasesProps) {
<div
key={phase.id}
onClick={() => onPhaseSelect(phase)}
className="bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200 cursor-pointer border border-gray-200 hover:border-blue-300"
className="bg-white dark:bg-gray-800 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200 cursor-pointer border border-gray-200 dark:border-gray-700 hover:border-blue-300 dark:hover:border-blue-600"
>
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center">
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/20 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-blue-600 dark:text-blue-400" 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>
</div>
</div>
<div className="text-right">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300">
Active
</span>
</div>
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
{phase.name}
</h3>
{phase.description && (
<p className="text-sm text-gray-600 mb-4 line-clamp-2">
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-2">
{phase.description}
</p>
)}
@@ -121,31 +121,31 @@ export function ExperimentPhases({ onPhaseSelect }: ExperimentPhasesProps) {
<div className="mb-4">
<div className="flex flex-wrap gap-1">
{phase.has_soaking && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300">
🌰 Soaking
</span>
)}
{phase.has_airdrying && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300">
💨 Air-Drying
</span>
)}
{phase.has_cracking && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300">
🔨 Cracking
</span>
)}
{phase.has_shelling && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-300">
📊 Shelling
</span>
)}
</div>
</div>
<div className="flex items-center justify-between text-sm text-gray-500">
<div className="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
<span>Created {new Date(phase.created_at).toLocaleDateString()}</span>
<div className="flex items-center text-blue-600 hover:text-blue-800">
<div className="flex items-center text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300">
<span className="mr-1">View Experiments</span>
<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" />
@@ -159,11 +159,11 @@ export function ExperimentPhases({ onPhaseSelect }: ExperimentPhasesProps) {
{phases.length === 0 && (
<div className="text-center py-12">
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<svg className="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
<h3 className="mt-2 text-sm font-medium text-gray-900">No experiment phases found</h3>
<p className="mt-1 text-sm text-gray-500">
<h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-white">No experiment phases found</h3>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Get started by creating your first experiment phase.
</p>
{canManagePhases && (

View File

@@ -36,7 +36,7 @@ export function Login({ onLoginSuccess }: LoginProps) {
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">

View File

@@ -1,5 +1,6 @@
import { useState } from 'react'
import type { User } from '../lib/supabase'
import { useTheme } from '../hooks/useTheme'
interface TopNavbarProps {
user: User
@@ -19,6 +20,7 @@ export function TopNavbar({
onNavigateToProfile
}: TopNavbarProps) {
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false)
const { theme, toggleTheme } = useTheme()
const getPageTitle = (view: string) => {
switch (view) {
@@ -139,6 +141,48 @@ export function TopNavbar({
</div>
<div className="flex items-center justify-between w-full gap-4 px-5 py-4 lg:flex shadow-theme-md lg:justify-end lg:px-0 lg:shadow-none">
{/* Theme Toggle Button */}
<button
onClick={toggleTheme}
className="flex items-center justify-center w-10 h-10 text-gray-500 border border-gray-200 rounded-lg hover:bg-gray-100 dark:border-gray-800 dark:text-gray-400 dark:hover:bg-white/5 transition-colors"
aria-label="Toggle theme"
title={theme === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}
>
{theme === 'light' ? (
// Moon icon for dark mode
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
/>
</svg>
) : (
// Sun icon for light mode
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
)}
</button>
{/* User Area */}
<div className="relative">
<button

View File

@@ -103,15 +103,15 @@ export function UserManagement() {
const getRoleBadgeColor = (role: string) => {
switch (role) {
case 'admin':
return 'bg-red-100 text-red-800'
return 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300'
case 'conductor':
return 'bg-blue-100 text-blue-800'
return 'bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300'
case 'analyst':
return 'bg-green-100 text-green-800'
return 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300'
case 'data recorder':
return 'bg-purple-100 text-purple-800'
return 'bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-300'
default:
return 'bg-gray-100 text-gray-800'
return 'bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-300'
}
}
@@ -120,7 +120,7 @@ export function UserManagement() {
<div className="p-6">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading users...</p>
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading users...</p>
</div>
</div>
)
@@ -129,8 +129,8 @@ export function UserManagement() {
if (error) {
return (
<div className="p-6">
<div className="rounded-md bg-red-50 p-4">
<div className="text-sm text-red-700">{error}</div>
<div className="rounded-md bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-4">
<div className="text-sm text-red-700 dark:text-red-400">{error}</div>
</div>
<button
onClick={loadData}
@@ -148,8 +148,8 @@ export function UserManagement() {
<div className="mb-8">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-gray-900">User Management</h1>
<p className="mt-2 text-gray-600">Manage user accounts, roles, and permissions</p>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">User Management</h1>
<p className="mt-2 text-gray-600 dark:text-gray-400">Manage user accounts, roles, and permissions</p>
</div>
<button
onClick={() => setShowCreateModal(true)}
@@ -162,7 +162,7 @@ export function UserManagement() {
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg border border-gray-200 dark:border-gray-700">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
@@ -170,15 +170,15 @@ export function UserManagement() {
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">Total Users</dt>
<dd className="text-lg font-medium text-gray-900">{users.length}</dd>
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">Total Users</dt>
<dd className="text-lg font-medium text-gray-900 dark:text-white">{users.length}</dd>
</dl>
</div>
</div>
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg border border-gray-200 dark:border-gray-700">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
@@ -186,8 +186,8 @@ export function UserManagement() {
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">Active Users</dt>
<dd className="text-lg font-medium text-gray-900">
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">Active Users</dt>
<dd className="text-lg font-medium text-gray-900 dark:text-white">
{users.filter(u => u.status === 'active').length}
</dd>
</dl>
@@ -196,7 +196,7 @@ export function UserManagement() {
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg border border-gray-200 dark:border-gray-700">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
@@ -204,8 +204,8 @@ export function UserManagement() {
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">Disabled Users</dt>
<dd className="text-lg font-medium text-gray-900">
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">Disabled Users</dt>
<dd className="text-lg font-medium text-gray-900 dark:text-white">
{users.filter(u => u.status === 'disabled').length}
</dd>
</dl>
@@ -214,7 +214,7 @@ export function UserManagement() {
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg border border-gray-200 dark:border-gray-700">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
@@ -222,8 +222,8 @@ export function UserManagement() {
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">Admins</dt>
<dd className="text-lg font-medium text-gray-900">
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">Admins</dt>
<dd className="text-lg font-medium text-gray-900 dark:text-white">
{users.filter(u => u.roles.includes('admin')).length}
</dd>
</dl>
@@ -234,35 +234,35 @@ export function UserManagement() {
</div>
{/* Users Table */}
<div className="bg-white shadow overflow-hidden sm:rounded-md">
<div className="px-4 py-5 sm:px-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">Users</h3>
<p className="mt-1 max-w-2xl text-sm text-gray-500">
<div className="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-md border border-gray-200 dark:border-gray-700">
<div className="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Users</h3>
<p className="mt-1 max-w-2xl text-sm text-gray-500 dark:text-gray-400">
Click on any field to edit user details
</p>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-900">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Email
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Roles
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Created
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{users.map((user) => (
<UserRow
key={user.id}
@@ -352,11 +352,11 @@ function UserRow({
type="email"
value={editEmail}
onChange={(e) => setEditEmail(e.target.value)}
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
className="block w-full border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
) : (
<div
className="text-sm text-gray-900 cursor-pointer hover:text-blue-600"
className="text-sm text-gray-900 dark:text-white cursor-pointer hover:text-blue-600 dark:hover:text-blue-400"
onClick={onEdit}
>
{user.email}
@@ -374,7 +374,7 @@ function UserRow({
onChange={() => handleRoleToggle(role.name)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<span className="ml-2 text-sm text-gray-700">{role.name}</span>
<span className="ml-2 text-sm text-gray-700 dark:text-gray-300">{role.name}</span>
</label>
))}
</div>
@@ -398,14 +398,14 @@ function UserRow({
<button
onClick={() => 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 dark:bg-green-900/30 text-green-800 dark:text-green-300 hover:bg-green-200 dark:hover:bg-green-900/40'
: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300 hover:bg-red-200 dark:hover:bg-red-900/40'
}`}
>
{user.status.charAt(0).toUpperCase() + user.status.slice(1)}
</button>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{new Date(user.created_at).toLocaleDateString()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
@@ -413,13 +413,13 @@ function UserRow({
<div className="flex space-x-2">
<button
onClick={handleSave}
className="text-blue-600 hover:text-blue-900"
className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300"
>
Save
</button>
<button
onClick={onCancel}
className="text-gray-600 hover:text-gray-900"
className="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
>
Cancel
</button>
@@ -428,7 +428,7 @@ function UserRow({
<div className="flex space-x-2">
<button
onClick={onEdit}
className="p-1 text-blue-600 hover:text-blue-900 hover:bg-blue-50 rounded"
className="p-1 text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded"
title="Edit user"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -437,7 +437,7 @@ function UserRow({
</button>
<button
onClick={() => onPasswordReset(user.id, user.email)}
className="p-1 text-orange-600 hover:text-orange-900 hover:bg-orange-50 rounded"
className="p-1 text-orange-600 dark:text-orange-400 hover:text-orange-900 dark:hover:text-orange-300 hover:bg-orange-50 dark:hover:bg-orange-900/20 rounded"
title="Reset password to 'password123'"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">

View File

@@ -0,0 +1,44 @@
import { useState, useEffect } from 'react'
type Theme = 'light' | 'dark'
// Initialize theme on script load (before React renders)
function initializeTheme(): Theme {
const stored = localStorage.getItem('theme') as Theme | null
if (stored) {
return stored
}
// Check system preference
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark'
}
return 'light'
}
// Apply theme class to document root
function applyTheme(theme: Theme) {
const root = window.document.documentElement
root.classList.remove('light', 'dark')
root.classList.add(theme)
}
// Initialize theme immediately on module load
const initialTheme = initializeTheme()
applyTheme(initialTheme)
export function useTheme() {
const [theme, setTheme] = useState<Theme>(initialTheme)
useEffect(() => {
// Apply theme whenever it changes
applyTheme(theme)
localStorage.setItem('theme', theme)
}, [theme])
const toggleTheme = () => {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'))
}
return { theme, toggleTheme }
}

View File

@@ -188,7 +188,7 @@
}
body {
@apply relative font-normal font-outfit z-1 bg-gray-50;
@apply relative font-normal font-outfit z-1 bg-gray-50 dark:bg-gray-900;
}
}

View File

@@ -4,6 +4,7 @@ export type FeatureFlags = {
enableExperimentModule: boolean
enableCameraModule: boolean
enableVisionSystemModule: boolean
enableSchedulingModule: boolean
}
const toBool = (v: unknown, fallback = false): boolean => {
@@ -21,6 +22,7 @@ export const featureFlags: FeatureFlags = {
enableExperimentModule: toBool(import.meta.env.VITE_ENABLE_EXPERIMENT_MODULE ?? false),
enableCameraModule: toBool(import.meta.env.VITE_ENABLE_CAMERA_MODULE ?? false),
enableVisionSystemModule: toBool(import.meta.env.VITE_ENABLE_VISION_SYSTEM_MODULE ?? false),
enableSchedulingModule: toBool(import.meta.env.VITE_ENABLE_SCHEDULING_MODULE ?? false),
}
export const isFeatureEnabled = (flag: keyof FeatureFlags): boolean => featureFlags[flag]