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",
|
"@heroicons/react": "^2.2.0",
|
||||||
"@supabase/supabase-js": "^2.52.0",
|
"@supabase/supabase-js": "^2.52.0",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"moment": "^2.30.1",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
|
"react-big-calendar": "^1.19.4",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.28.0",
|
||||||
"tailwindcss": "^4.1.11"
|
"tailwindcss": "^4.1.11"
|
||||||
|
|||||||
@@ -99,6 +99,20 @@ function App() {
|
|||||||
return match ? `camera${match[1]}` : null
|
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) {
|
if (loading) {
|
||||||
return (
|
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">
|
||||||
@@ -133,7 +147,7 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isAuthenticated ? (
|
{isAuthenticated ? (
|
||||||
<Dashboard onLogout={handleLogout} />
|
<Dashboard onLogout={handleLogout} currentRoute={currentRoute} />
|
||||||
) : (
|
) : (
|
||||||
<Login onLoginSuccess={handleLoginSuccess} />
|
<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 {
|
interface DashboardProps {
|
||||||
onLogout: () => void
|
onLogout: () => void
|
||||||
|
currentRoute: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Dashboard({ onLogout }: DashboardProps) {
|
export function Dashboard({ onLogout, currentRoute }: DashboardProps) {
|
||||||
return <DashboardLayout onLogout={onLogout} />
|
return <DashboardLayout onLogout={onLogout} currentRoute={currentRoute} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ import { userManagement, type User } from '../lib/supabase'
|
|||||||
|
|
||||||
interface DashboardLayoutProps {
|
interface DashboardLayoutProps {
|
||||||
onLogout: () => void
|
onLogout: () => void
|
||||||
|
currentRoute: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DashboardLayout({ onLogout }: DashboardLayoutProps) {
|
export function DashboardLayout({ onLogout, currentRoute }: DashboardLayoutProps) {
|
||||||
const [user, setUser] = useState<User | null>(null)
|
const [user, setUser] = useState<User | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
@@ -59,11 +60,15 @@ export function DashboardLayout({ onLogout }: DashboardLayoutProps) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle view change with persistence
|
// Handle view change with persistence and URL updates
|
||||||
const handleViewChange = (view: string) => {
|
const handleViewChange = (view: string) => {
|
||||||
if (validViews.includes(view) && hasAccessToView(view)) {
|
if (validViews.includes(view) && hasAccessToView(view)) {
|
||||||
setCurrentView(view)
|
setCurrentView(view)
|
||||||
saveCurrentView(view)
|
saveCurrentView(view)
|
||||||
|
|
||||||
|
// Update URL
|
||||||
|
const newPath = view === 'dashboard' ? '/' : `/${view}`
|
||||||
|
window.history.pushState({}, '', newPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +90,29 @@ export function DashboardLayout({ onLogout }: DashboardLayoutProps) {
|
|||||||
}
|
}
|
||||||
}, [user])
|
}, [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 () => {
|
const fetchUserProfile = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -163,7 +191,7 @@ export function DashboardLayout({ onLogout }: DashboardLayoutProps) {
|
|||||||
case 'vision-system':
|
case 'vision-system':
|
||||||
return <VisionSystem />
|
return <VisionSystem />
|
||||||
case 'scheduling':
|
case 'scheduling':
|
||||||
return <Scheduling user={user} />
|
return <Scheduling user={user} currentRoute={currentRoute} />
|
||||||
case 'video-library':
|
case 'video-library':
|
||||||
return <VideoStreamingPage />
|
return <VideoStreamingPage />
|
||||||
default:
|
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 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 {
|
interface SchedulingProps {
|
||||||
user: User
|
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 (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
@@ -16,22 +85,520 @@ export function Scheduling({ user }: SchedulingProps) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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="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="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">
|
<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">
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
||||||
Scheduling Module
|
Complete Schedule View
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-500 dark:text-gray-400 max-w-md mx-auto">
|
<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.
|
This view will show a comprehensive calendar and list of all scheduled experiment runs,
|
||||||
Features will include calendar integration, availability settings, and experiment scheduling.
|
including dates, times, assigned team members, and current status.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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