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:
salirezav
2025-09-19 12:33:25 -04:00
parent d1fe478478
commit ed6c242faa
8 changed files with 1656 additions and 574 deletions

1382
management-dashboard-web-app/package-lock.json generated Executable file → Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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"

View File

@@ -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} />
)}

View 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;
}
}

View File

@@ -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} />
}

View File

@@ -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:

View File

@@ -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>
)
}

View 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