diff --git a/src/App.tsx b/src/App.tsx index dbc31bc..dd5270f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import Layout from './components/Layout'; import ErrorBoundary from './components/ErrorBoundary'; import ResourcesList from './pages/ResourcesList'; import ResourceCalendar from './pages/ResourceCalendar'; +import AllResourcesCalendar from './pages/AllResourcesCalendar'; import CreateReservation from './pages/CreateReservation'; import UserDashboard from './pages/UserDashboard'; import AdminDashboard from './pages/AdminDashboard'; @@ -19,6 +20,7 @@ function App() { } /> } /> } /> + } /> } /> } /> diff --git a/src/pages/AllResourcesCalendar.tsx b/src/pages/AllResourcesCalendar.tsx new file mode 100644 index 0000000..55e9e16 --- /dev/null +++ b/src/pages/AllResourcesCalendar.tsx @@ -0,0 +1,322 @@ +import React, { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { Resource, Reservation } from '../types'; +import { SimpleLibreBookingClient } from '../services/librebooking-api'; +import { format, startOfWeek, addDays, isSameDay, isToday, addHours, setMinutes, setHours } from 'date-fns'; + +const AllResourcesCalendar: React.FC = () => { + const [resources, setResources] = useState([]); + const [reservations, setReservations] = useState([]); + const [currentWeek, setCurrentWeek] = useState(new Date()); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Mobile detection + const [isMobile, setIsMobile] = useState(window.innerWidth < 768); + + useEffect(() => { + const handleResize = () => { + setIsMobile(window.innerWidth < 768); + }; + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + useEffect(() => { + const api = new SimpleLibreBookingClient(); + + const loadData = async () => { + try { + setLoading(true); + const [resourcesData, reservationsData] = await Promise.all([ + api.getResources(), + api.getReservations() + ]); + setResources(resourcesData); + setReservations(reservationsData); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load data'); + } finally { + setLoading(false); + } + }; + + loadData(); + }, []); + + const weekStart = startOfWeek(currentWeek, { weekStartsOn: 1 }); + const weekDays = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i)); + + // Generate time slots from 8 AM to 8 PM + const timeSlots = Array.from({ length: 13 }, (_, i) => { + const hour = i + 8; // Start at 8 AM + return setMinutes(setHours(new Date(), hour), 0); + }); + + const getReservationsForSlot = (date: Date, timeSlot: Date): Reservation[] => { + const slotStart = setHours(setMinutes(new Date(date), timeSlot.getHours()), timeSlot.getMinutes()); + const slotEnd = addHours(slotStart, 1); + + return reservations.filter(reservation => { + const reservationStart = new Date(reservation.startDate); + const reservationEnd = new Date(reservation.endDate); + + // Check if reservation occupies this time slot + return ( + (reservationStart < slotEnd && reservationEnd > slotStart) || + (isSameDay(reservationStart, date) && + reservationStart.getHours() === timeSlot.getHours()) + ); + }); + }; + + const navigateWeek = (direction: 'prev' | 'next') => { + const days = direction === 'next' ? 7 : -7; + setCurrentWeek(addDays(currentWeek, days)); + }; + + const getResourceColor = (resourceId: string): string => { + const colors = [ + '#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6', + '#1abc9c', '#34495e', '#e67e22', '#95a5a6', '#d35400' + ]; + const index = parseInt(resourceId) % colors.length; + return colors[index]; + }; + + if (loading) { + return ( +
+

All Resources Calendar

+
Loading calendar...
+
+ ); + } + + if (error) { + return ( +
+

All Resources Calendar

+
Error: {error}
+
+ ); + } + + return ( +
+ {/* Header */} +
+

All Resources Calendar

+

+ View all reservations across all resources +

+ + ← Back to Resources + +
+ + {/* Week Navigation */} +
+
+ + +
+

+ {format(weekStart, 'MMM d')} - {format(addDays(weekStart, 6), 'MMM d, yyyy')} +

+
+ + {/* Calendar Grid */} +
+ {/* Header Row */} +
+
+ Time +
+ {weekDays.map((day, index) => ( +
+
{format(day, 'EEE')}
+
+ {format(day, 'M/d')} +
+
+ ))} +
+ + {/* Time Slots */} + {timeSlots.map((timeSlot, timeIndex) => ( +
+ {/* Time Column */} +
+ {format(timeSlot, 'h a')} +
+ + {/* Day Columns */} + {weekDays.map((day, dayIndex) => { + const slotReservations = getReservationsForSlot(day, timeSlot); + + return ( +
+ {slotReservations.map((reservation, resIndex) => ( +
+
+ {isMobile && reservation.title.length > 12 + ? reservation.title.substring(0, 12) + '...' + : reservation.title.length > 15 + ? reservation.title.substring(0, 15) + '...' + : reservation.title + } +
+
+ {reservation.resourceName} +
+
+ ))} +
+ ); + })} +
+ ))} +
+ + {/* Resource Legend */} +
+

Resources

+
+ {resources.map((resource) => ( +
+
+ {resource.name} +
+ ))} +
+
+
+ ); +}; + +export default AllResourcesCalendar; \ No newline at end of file diff --git a/src/pages/UserDashboard.tsx b/src/pages/UserDashboard.tsx index cf2e82a..c1c24c5 100644 --- a/src/pages/UserDashboard.tsx +++ b/src/pages/UserDashboard.tsx @@ -236,7 +236,7 @@ const UserDashboard: React.FC = () => {
{ fontSize: '0.875rem' }} > - View Calendar +View All Calendars