Enhance scheduling features in management dashboard
- Added new scheduling functionality with a dedicated Scheduling component to manage availability and experiment scheduling. - Integrated react-big-calendar for visual calendar representation of availability slots. - Updated Dashboard and DashboardLayout components to handle current route and pass it to child components. - Implemented route handling for scheduling sub-routes to improve user navigation. - Added new dependencies: moment and react-big-calendar for date handling and calendar UI. - Improved user experience with dynamic URL updates based on selected scheduling views.
This commit is contained in:
1382
management-dashboard-web-app/package-lock.json
generated
Executable file → Normal file
1382
management-dashboard-web-app/package-lock.json
generated
Executable file → Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,9 @@
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@supabase/supabase-js": "^2.52.0",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"moment": "^2.30.1",
|
||||
"react": "^19.1.0",
|
||||
"react-big-calendar": "^1.19.4",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^6.28.0",
|
||||
"tailwindcss": "^4.1.11"
|
||||
|
||||
@@ -99,6 +99,20 @@ function App() {
|
||||
return match ? `camera${match[1]}` : null
|
||||
}
|
||||
|
||||
// Check if current route is a scheduling sub-route
|
||||
const isSchedulingRoute = (route: string) => {
|
||||
return route.startsWith('/scheduling')
|
||||
}
|
||||
|
||||
// Extract scheduling sub-route
|
||||
const getSchedulingSubRoute = (route: string) => {
|
||||
if (route === '/scheduling') {
|
||||
return 'main'
|
||||
}
|
||||
const match = route.match(/^\/scheduling\/(.+)$/)
|
||||
return match ? match[1] : 'main'
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
@@ -133,7 +147,7 @@ function App() {
|
||||
return (
|
||||
<>
|
||||
{isAuthenticated ? (
|
||||
<Dashboard onLogout={handleLogout} />
|
||||
<Dashboard onLogout={handleLogout} currentRoute={currentRoute} />
|
||||
) : (
|
||||
<Login onLoginSuccess={handleLoginSuccess} />
|
||||
)}
|
||||
|
||||
204
management-dashboard-web-app/src/components/CalendarStyles.css
Normal file
204
management-dashboard-web-app/src/components/CalendarStyles.css
Normal file
@@ -0,0 +1,204 @@
|
||||
/* Custom styles for React Big Calendar to match dashboard theme */
|
||||
|
||||
.rbc-calendar {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
.dark .rbc-calendar {
|
||||
background: #1f2937;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
/* Header styling */
|
||||
.rbc-header {
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
color: #374151;
|
||||
font-weight: 600;
|
||||
padding: 12px 8px;
|
||||
}
|
||||
|
||||
.dark .rbc-header {
|
||||
background: #374151;
|
||||
border-bottom: 1px solid #4b5563;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
/* Today styling */
|
||||
.rbc-today {
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.dark .rbc-today {
|
||||
background: #1e3a8a;
|
||||
}
|
||||
|
||||
/* Date cells */
|
||||
.rbc-date-cell {
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dark .rbc-date-cell {
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
/* Event styling */
|
||||
.rbc-event {
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.rbc-event-content {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Month view specific */
|
||||
.rbc-month-view {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dark .rbc-month-view {
|
||||
border: 1px solid #4b5563;
|
||||
}
|
||||
|
||||
.rbc-month-row {
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.dark .rbc-month-row {
|
||||
border-bottom: 1px solid #4b5563;
|
||||
}
|
||||
|
||||
.rbc-date-cell {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* Week and day view specific */
|
||||
.rbc-time-view {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dark .rbc-time-view {
|
||||
border: 1px solid #4b5563;
|
||||
}
|
||||
|
||||
.rbc-time-header {
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.dark .rbc-time-header {
|
||||
background: #374151;
|
||||
border-bottom: 1px solid #4b5563;
|
||||
}
|
||||
|
||||
.rbc-time-content {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.dark .rbc-time-content {
|
||||
background: #1f2937;
|
||||
}
|
||||
|
||||
/* Time slots */
|
||||
.rbc-time-slot {
|
||||
border-top: 1px solid #f1f5f9;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.dark .rbc-time-slot {
|
||||
border-top: 1px solid #374151;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* Toolbar */
|
||||
.rbc-toolbar {
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.dark .rbc-toolbar {
|
||||
background: #374151;
|
||||
border-bottom: 1px solid #4b5563;
|
||||
}
|
||||
|
||||
.rbc-toolbar button {
|
||||
background: white;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
padding: 6px 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.rbc-toolbar button:hover {
|
||||
background: #f3f4f6;
|
||||
border-color: #9ca3af;
|
||||
}
|
||||
|
||||
.rbc-toolbar button:active,
|
||||
.rbc-toolbar button.rbc-active {
|
||||
background: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark .rbc-toolbar button {
|
||||
background: #1f2937;
|
||||
border: 1px solid #4b5563;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
.dark .rbc-toolbar button:hover {
|
||||
background: #374151;
|
||||
border-color: #6b7280;
|
||||
}
|
||||
|
||||
.dark .rbc-toolbar button:active,
|
||||
.dark .rbc-toolbar button.rbc-active {
|
||||
background: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Labels */
|
||||
.rbc-toolbar-label {
|
||||
color: #111827;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dark .rbc-toolbar-label {
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.rbc-toolbar {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rbc-toolbar button {
|
||||
font-size: 14px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.rbc-toolbar-label {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,9 @@ import { DashboardLayout } from "./DashboardLayout"
|
||||
|
||||
interface DashboardProps {
|
||||
onLogout: () => void
|
||||
currentRoute: string
|
||||
}
|
||||
|
||||
export function Dashboard({ onLogout }: DashboardProps) {
|
||||
return <DashboardLayout onLogout={onLogout} />
|
||||
export function Dashboard({ onLogout, currentRoute }: DashboardProps) {
|
||||
return <DashboardLayout onLogout={onLogout} currentRoute={currentRoute} />
|
||||
}
|
||||
|
||||
@@ -12,9 +12,10 @@ import { userManagement, type User } from '../lib/supabase'
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
onLogout: () => void
|
||||
currentRoute: string
|
||||
}
|
||||
|
||||
export function DashboardLayout({ onLogout }: DashboardLayoutProps) {
|
||||
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)
|
||||
@@ -59,11 +60,15 @@ export function DashboardLayout({ onLogout }: DashboardLayoutProps) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Handle view change with persistence
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +90,29 @@ export function DashboardLayout({ onLogout }: DashboardLayoutProps) {
|
||||
}
|
||||
}, [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)
|
||||
@@ -163,7 +191,7 @@ export function DashboardLayout({ onLogout }: DashboardLayoutProps) {
|
||||
case 'vision-system':
|
||||
return <VisionSystem />
|
||||
case 'scheduling':
|
||||
return <Scheduling user={user} />
|
||||
return <Scheduling user={user} currentRoute={currentRoute} />
|
||||
case 'video-library':
|
||||
return <VideoStreamingPage />
|
||||
default:
|
||||
|
||||
@@ -1,10 +1,79 @@
|
||||
import { useState } from 'react'
|
||||
// @ts-ignore - react-big-calendar types not available
|
||||
import { Calendar, momentLocalizer, Views } from 'react-big-calendar'
|
||||
import moment from 'moment'
|
||||
import type { User } from '../lib/supabase'
|
||||
import 'react-big-calendar/lib/css/react-big-calendar.css'
|
||||
import './CalendarStyles.css'
|
||||
|
||||
// Type definitions for calendar events
|
||||
interface CalendarEvent {
|
||||
id: number | string
|
||||
title: string
|
||||
start: Date
|
||||
end: Date
|
||||
resource?: string
|
||||
}
|
||||
|
||||
interface SchedulingProps {
|
||||
user: User
|
||||
currentRoute: string
|
||||
}
|
||||
|
||||
export function Scheduling({ user }: SchedulingProps) {
|
||||
type SchedulingView = 'main' | 'view-schedule' | 'indicate-availability' | 'schedule-experiment'
|
||||
|
||||
export function Scheduling({ user, currentRoute }: SchedulingProps) {
|
||||
// Extract current view from route
|
||||
const getCurrentView = (): SchedulingView => {
|
||||
if (currentRoute === '/scheduling') {
|
||||
return 'main'
|
||||
}
|
||||
const match = currentRoute.match(/^\/scheduling\/(.+)$/)
|
||||
if (match) {
|
||||
const subRoute = match[1]
|
||||
switch (subRoute) {
|
||||
case 'view-schedule':
|
||||
return 'view-schedule'
|
||||
case 'indicate-availability':
|
||||
return 'indicate-availability'
|
||||
case 'schedule-experiment':
|
||||
return 'schedule-experiment'
|
||||
default:
|
||||
return 'main'
|
||||
}
|
||||
}
|
||||
return 'main'
|
||||
}
|
||||
|
||||
const currentView = getCurrentView()
|
||||
|
||||
const handleCardClick = (view: SchedulingView) => {
|
||||
const newPath = view === 'main' ? '/scheduling' : `/scheduling/${view}`
|
||||
window.history.pushState({}, '', newPath)
|
||||
// Trigger a popstate event to update the route
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
}
|
||||
|
||||
const handleBackToMain = () => {
|
||||
window.history.pushState({}, '', '/scheduling')
|
||||
// Trigger a popstate event to update the route
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
}
|
||||
|
||||
// Render different views based on currentView
|
||||
if (currentView === 'view-schedule') {
|
||||
return <ViewSchedule user={user} onBack={handleBackToMain} />
|
||||
}
|
||||
|
||||
if (currentView === 'indicate-availability') {
|
||||
return <IndicateAvailability user={user} onBack={handleBackToMain} />
|
||||
}
|
||||
|
||||
if (currentView === 'schedule-experiment') {
|
||||
return <ScheduleExperiment user={user} onBack={handleBackToMain} />
|
||||
}
|
||||
|
||||
// Main view with cards
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
@@ -16,22 +85,520 @@ export function Scheduling({ user }: SchedulingProps) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Scheduling Cards Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{/* View Complete Schedule Card */}
|
||||
<div
|
||||
onClick={() => handleCardClick('view-schedule')}
|
||||
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"
|
||||
>
|
||||
<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 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="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
||||
</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 dark:bg-green-900/20 text-green-800 dark:text-green-400">
|
||||
Available
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
View Complete Schedule
|
||||
</h3>
|
||||
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-2">
|
||||
View the complete schedule of all upcoming experiment runs and their current status.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
|
||||
<span>All experiments</span>
|
||||
<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 Schedule</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" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Indicate Availability Card */}
|
||||
<div
|
||||
onClick={() => handleCardClick('indicate-availability')}
|
||||
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"
|
||||
>
|
||||
<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-green-100 dark:bg-green-900/20 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</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-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-400">
|
||||
Active
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Indicate Your Availability
|
||||
</h3>
|
||||
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-2">
|
||||
Set your availability preferences and time slots for upcoming experiment runs.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
|
||||
<span>Personal settings</span>
|
||||
<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">Set Availability</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" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Schedule Experiment Card */}
|
||||
<div
|
||||
onClick={() => handleCardClick('schedule-experiment')}
|
||||
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"
|
||||
>
|
||||
<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-purple-100 dark:bg-purple-900/20 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</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-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-400">
|
||||
Planning
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Schedule Experiment
|
||||
</h3>
|
||||
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-2">
|
||||
Schedule specific experiment runs and assign team members to upcoming sessions.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
|
||||
<span>Experiment planning</span>
|
||||
<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">Schedule Now</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" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Placeholder components for the three scheduling features
|
||||
function ViewSchedule({ user, onBack }: { user: User; onBack: () => void }) {
|
||||
// User context available for future features
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="flex items-center text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 mb-4"
|
||||
>
|
||||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Back to Scheduling
|
||||
</button>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
Complete Schedule
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
View all scheduled experiment runs and their current status.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div className="text-center py-12">
|
||||
<div className="mx-auto w-16 h-16 bg-blue-100 dark:bg-blue-900/20 rounded-full flex items-center justify-center mb-4">
|
||||
<svg className="w-8 h-8 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
||||
Scheduling Module
|
||||
Complete Schedule View
|
||||
</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400 max-w-md mx-auto">
|
||||
This module will allow you to manage your availability and schedule for upcoming experiment runs.
|
||||
Features will include calendar integration, availability settings, and experiment scheduling.
|
||||
This view will show a comprehensive calendar and list of all scheduled experiment runs,
|
||||
including dates, times, assigned team members, and current status.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function IndicateAvailability({ user, onBack }: { user: User; onBack: () => void }) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="flex items-center text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 mb-4"
|
||||
>
|
||||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Back to Scheduling
|
||||
</button>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
Indicate Availability
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Set your availability preferences and time slots for upcoming experiment runs.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
|
||||
<AvailabilityCalendar user={user} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ScheduleExperiment({ user, onBack }: { user: User; onBack: () => void }) {
|
||||
// User context available for future features
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="flex items-center text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 mb-4"
|
||||
>
|
||||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Back to Scheduling
|
||||
</button>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
Schedule Experiment
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Schedule specific experiment runs and assign team members to upcoming sessions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div className="text-center py-12">
|
||||
<div className="mx-auto w-16 h-16 bg-purple-100 dark:bg-purple-900/20 rounded-full flex items-center justify-center mb-4">
|
||||
<svg className="w-8 h-8 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
||||
Experiment Scheduling
|
||||
</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400 max-w-md mx-auto">
|
||||
This interface will allow you to schedule specific experiment runs, assign team members,
|
||||
and manage the timing of upcoming experimental sessions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Availability Calendar Component
|
||||
function AvailabilityCalendar({ user }: { user: User }) {
|
||||
// User context available for future features like saving preferences
|
||||
const localizer = momentLocalizer(moment)
|
||||
const [events, setEvents] = useState<CalendarEvent[]>([
|
||||
{
|
||||
id: 1,
|
||||
title: 'Available - Morning',
|
||||
start: new Date(2024, 11, 15, 9, 0), // December 15, 2024, 9:00 AM
|
||||
end: new Date(2024, 11, 15, 12, 0), // December 15, 2024, 12:00 PM
|
||||
resource: 'available'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Available - Afternoon',
|
||||
start: new Date(2024, 11, 15, 14, 0), // December 15, 2024, 2:00 PM
|
||||
end: new Date(2024, 11, 15, 17, 0), // December 15, 2024, 5:00 PM
|
||||
resource: 'available'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Available - Full Day',
|
||||
start: new Date(2024, 11, 16, 9, 0), // December 16, 2024, 9:00 AM
|
||||
end: new Date(2024, 11, 16, 17, 0), // December 16, 2024, 5:00 PM
|
||||
resource: 'available'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'Unavailable - Personal',
|
||||
start: new Date(2024, 11, 20, 0, 0), // December 20, 2024
|
||||
end: new Date(2024, 11, 20, 23, 59), // December 20, 2024
|
||||
resource: 'unavailable'
|
||||
}
|
||||
])
|
||||
|
||||
const [selectedDate, setSelectedDate] = useState<Date | null>(null)
|
||||
const [showTimeSlotForm, setShowTimeSlotForm] = useState(false)
|
||||
const [newTimeSlot, setNewTimeSlot] = useState({
|
||||
startTime: '09:00',
|
||||
endTime: '17:00',
|
||||
title: 'Available'
|
||||
})
|
||||
|
||||
const eventStyleGetter = (event: CalendarEvent) => {
|
||||
let backgroundColor = '#3174ad'
|
||||
let borderColor = '#3174ad'
|
||||
|
||||
if (event.resource === 'available') {
|
||||
backgroundColor = '#10b981' // green-500
|
||||
borderColor = '#10b981'
|
||||
} else if (event.resource === 'unavailable') {
|
||||
backgroundColor = '#ef4444' // red-500
|
||||
borderColor = '#ef4444'
|
||||
}
|
||||
|
||||
return {
|
||||
style: {
|
||||
backgroundColor,
|
||||
borderColor,
|
||||
color: 'white',
|
||||
borderRadius: '4px',
|
||||
border: 'none',
|
||||
display: 'block'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectSlot = ({ start, end }: { start: Date; end: Date }) => {
|
||||
// Set the selected date and show the time slot form
|
||||
setSelectedDate(start)
|
||||
setShowTimeSlotForm(true)
|
||||
|
||||
// Pre-fill the form with the selected time range
|
||||
const startTime = moment(start).format('HH:mm')
|
||||
const endTime = moment(end).format('HH:mm')
|
||||
setNewTimeSlot({
|
||||
startTime,
|
||||
endTime,
|
||||
title: 'Available'
|
||||
})
|
||||
}
|
||||
|
||||
const handleSelectEvent = (event: CalendarEvent) => {
|
||||
if (window.confirm('Do you want to remove this availability?')) {
|
||||
setEvents(events.filter(e => e.id !== event.id))
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddTimeSlot = () => {
|
||||
if (!selectedDate) return
|
||||
|
||||
const [startHour, startMinute] = newTimeSlot.startTime.split(':').map(Number)
|
||||
const [endHour, endMinute] = newTimeSlot.endTime.split(':').map(Number)
|
||||
|
||||
const startDateTime = new Date(selectedDate)
|
||||
startDateTime.setHours(startHour, startMinute, 0, 0)
|
||||
|
||||
const endDateTime = new Date(selectedDate)
|
||||
endDateTime.setHours(endHour, endMinute, 0, 0)
|
||||
|
||||
// Check for overlapping events
|
||||
const hasOverlap = events.some(event => {
|
||||
const eventStart = new Date(event.start)
|
||||
const eventEnd = new Date(event.end)
|
||||
return (
|
||||
eventStart.toDateString() === selectedDate.toDateString() &&
|
||||
((startDateTime >= eventStart && startDateTime < eventEnd) ||
|
||||
(endDateTime > eventStart && endDateTime <= eventEnd) ||
|
||||
(startDateTime <= eventStart && endDateTime >= eventEnd))
|
||||
)
|
||||
})
|
||||
|
||||
if (hasOverlap) {
|
||||
alert('This time slot overlaps with an existing availability. Please choose a different time.')
|
||||
return
|
||||
}
|
||||
|
||||
const newEvent = {
|
||||
id: Date.now(),
|
||||
title: newTimeSlot.title,
|
||||
start: startDateTime,
|
||||
end: endDateTime,
|
||||
resource: 'available'
|
||||
}
|
||||
|
||||
setEvents([...events, newEvent])
|
||||
setShowTimeSlotForm(false)
|
||||
setSelectedDate(null)
|
||||
}
|
||||
|
||||
const handleCancelTimeSlot = () => {
|
||||
setShowTimeSlotForm(false)
|
||||
setSelectedDate(null)
|
||||
}
|
||||
|
||||
const getEventsForDate = (date: Date) => {
|
||||
return events.filter(event => {
|
||||
const eventDate = new Date(event.start)
|
||||
return eventDate.toDateString() === date.toDateString()
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Calendar Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Availability Calendar
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
Click and drag to add availability slots, or click on existing events to remove them. You can add multiple time slots per day.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="flex items-center space-x-4 mt-4 sm:mt-0">
|
||||
<div className="flex items-center">
|
||||
<div className="w-3 h-3 bg-green-500 rounded mr-2"></div>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">Available</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="w-3 h-3 bg-red-500 rounded mr-2"></div>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">Unavailable</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Time Slot Form Modal */}
|
||||
{showTimeSlotForm && selectedDate && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-[9999]">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-md mx-4 shadow-2xl">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Add Availability for {moment(selectedDate).format('MMMM D, YYYY')}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Start Time
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
value={newTimeSlot.startTime}
|
||||
onChange={(e) => setNewTimeSlot({ ...newTimeSlot, startTime: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
End Time
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
value={newTimeSlot.endTime}
|
||||
onChange={(e) => setNewTimeSlot({ ...newTimeSlot, endTime: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Description (Optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newTimeSlot.title}
|
||||
onChange={(e) => setNewTimeSlot({ ...newTimeSlot, title: e.target.value })}
|
||||
placeholder="e.g., Available - Morning, Available - Afternoon"
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Show existing time slots for this date */}
|
||||
{getEventsForDate(selectedDate).length > 0 && (
|
||||
<div className="mt-4">
|
||||
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Existing time slots:
|
||||
</h4>
|
||||
<div className="space-y-1">
|
||||
{getEventsForDate(selectedDate).map(event => (
|
||||
<div key={event.id} className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{moment(event.start).format('HH:mm')} - {moment(event.end).format('HH:mm')} ({event.title})
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end space-x-3 mt-6">
|
||||
<button
|
||||
onClick={handleCancelTimeSlot}
|
||||
className="px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAddTimeSlot}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Add Time Slot
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Calendar */}
|
||||
<div className="h-[600px]">
|
||||
<Calendar
|
||||
localizer={localizer}
|
||||
events={events}
|
||||
startAccessor="start"
|
||||
endAccessor="end"
|
||||
style={{ height: '100%' }}
|
||||
defaultView={Views.MONTH}
|
||||
views={[Views.MONTH, Views.WEEK, Views.DAY]}
|
||||
selectable
|
||||
onSelectSlot={handleSelectSlot}
|
||||
onSelectEvent={handleSelectEvent}
|
||||
eventPropGetter={eventStyleGetter}
|
||||
popup
|
||||
showMultiDayTimes
|
||||
step={30}
|
||||
timeslots={2}
|
||||
min={new Date(2024, 0, 1, 6, 0)} // 6:00 AM
|
||||
max={new Date(2024, 0, 1, 22, 0)} // 10:00 PM
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
10
management-dashboard-web-app/start-dev.sh
Executable file
10
management-dashboard-web-app/start-dev.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Set up the correct Node.js environment
|
||||
export PATH="/home/alireza/.nvm/versions/node/v22.18.0/bin:$PATH"
|
||||
|
||||
# Navigate to the project directory
|
||||
cd /home/alireza/Desktop/USDA-VISION/management-dashboard-web-app
|
||||
|
||||
# Start the development server
|
||||
npm run dev
|
||||
Reference in New Issue
Block a user