Add all calendar

This commit is contained in:
Joshua Schmucker 2026-02-06 15:43:45 +01:00
parent c519f4b11f
commit 37f094168c
3 changed files with 326 additions and 2 deletions

View File

@ -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() {
<Route path="/dashboard" element={<UserDashboard />} />
<Route path="/resources" element={<ResourcesList />} />
<Route path="/resources/:id/calendar" element={<ResourceCalendar />} />
<Route path="/calendar/all" element={<AllResourcesCalendar />} />
<Route path="/reservations/new" element={<CreateReservation />} />
<Route path="/admin" element={<AdminDashboard />} />
</Routes>

View File

@ -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<Resource[]>([]);
const [reservations, setReservations] = useState<Reservation[]>([]);
const [currentWeek, setCurrentWeek] = useState(new Date());
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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 (
<div>
<h2>All Resources Calendar</h2>
<div>Loading calendar...</div>
</div>
);
}
if (error) {
return (
<div>
<h2>All Resources Calendar</h2>
<div style={{ color: '#e74c3c' }}>Error: {error}</div>
</div>
);
}
return (
<div style={{ padding: '0.5rem', maxWidth: '100vw', overflowX: 'auto' }}>
{/* Header */}
<div style={{ marginBottom: '1.5rem' }}>
<h2 style={{ margin: '0 0 0.5rem 0', fontSize: '1.5rem' }}>All Resources Calendar</h2>
<p style={{ color: '#666', margin: '0 0 1rem 0', fontSize: '0.875rem' }}>
View all reservations across all resources
</p>
<Link
to="/resources"
style={{
display: 'inline-block',
padding: '0.75rem',
backgroundColor: '#3498db',
color: 'white',
textDecoration: 'none',
borderRadius: '4px',
fontWeight: '500',
textAlign: 'center',
fontSize: '0.875rem'
}}
>
Back to Resources
</Link>
</div>
{/* Week Navigation */}
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '0.75rem',
marginBottom: '1.5rem',
padding: '1rem',
backgroundColor: '#f8f9fa',
borderRadius: '4px',
textAlign: 'center'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<button
onClick={() => navigateWeek('prev')}
style={{
padding: '0.5rem 1rem',
backgroundColor: '#3498db',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '0.875rem',
flex: 1,
marginRight: '0.5rem'
}}
>
Previous
</button>
<button
onClick={() => navigateWeek('next')}
style={{
padding: '0.5rem 1rem',
backgroundColor: '#3498db',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '0.875rem',
flex: 1,
marginLeft: '0.5rem'
}}
>
Next
</button>
</div>
<h3 style={{ margin: '0.5rem 0 0 0', color: '#2c3e50', fontSize: '1rem' }}>
{format(weekStart, 'MMM d')} - {format(addDays(weekStart, 6), 'MMM d, yyyy')}
</h3>
</div>
{/* Calendar Grid */}
<div style={{
border: '1px solid #ddd',
borderRadius: '4px',
overflow: 'hidden',
backgroundColor: 'white',
fontSize: '0.75rem'
}}>
{/* Header Row */}
<div style={{
display: 'grid',
gridTemplateColumns: isMobile ? '50px repeat(7, 1fr)' : '80px repeat(7, 1fr)',
backgroundColor: '#2c3e50',
color: 'white',
fontSize: isMobile ? '0.6rem' : '0.7rem'
}}>
<div style={{ padding: '0.5rem', textAlign: 'center', fontWeight: 'bold', borderRight: '1px solid #ddd' }}>
Time
</div>
{weekDays.map((day, index) => (
<div
key={index}
style={{
padding: '0.5rem',
textAlign: 'center',
fontWeight: 'bold',
borderRight: index < 6 ? '1px solid #ddd' : 'none',
backgroundColor: isToday(day) ? '#34495e' : 'transparent'
}}
>
<div>{format(day, 'EEE')}</div>
<div style={{ fontSize: '0.7rem', opacity: 0.8 }}>
{format(day, 'M/d')}
</div>
</div>
))}
</div>
{/* Time Slots */}
{timeSlots.map((timeSlot, timeIndex) => (
<div key={timeIndex} style={{ display: 'grid', gridTemplateColumns: isMobile ? '50px repeat(7, 1fr)' : '80px repeat(7, 1fr)' }}>
{/* Time Column */}
<div style={{
padding: '0.5rem',
textAlign: 'center',
borderRight: '1px solid #ddd',
borderBottom: '1px solid #ddd',
backgroundColor: '#f8f9fa',
fontSize: '0.7rem',
fontWeight: '500'
}}>
{format(timeSlot, 'h a')}
</div>
{/* Day Columns */}
{weekDays.map((day, dayIndex) => {
const slotReservations = getReservationsForSlot(day, timeSlot);
return (
<div
key={dayIndex}
style={{
padding: '4px',
borderRight: dayIndex < 6 ? '1px solid #ddd' : 'none',
borderBottom: '1px solid #ddd',
backgroundColor: isToday(day) ? '#f8f9fa' : 'white',
fontSize: '0.7rem',
minHeight: isMobile ? '60px' : '50px',
height: isMobile ? '60px' : '50px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-start',
gap: '2px',
overflow: 'hidden',
boxSizing: 'border-box'
}}
>
{slotReservations.map((reservation, resIndex) => (
<div
key={resIndex}
style={{
backgroundColor: getResourceColor(reservation.resourceId),
color: 'white',
padding: '2px 4px',
borderRadius: '2px',
fontSize: isMobile ? '0.5rem' : '0.55rem',
fontWeight: '500',
textAlign: 'center',
width: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
lineHeight: '1.2'
}}
>
<div style={{ fontWeight: 'bold' }}>
{isMobile && reservation.title.length > 12
? reservation.title.substring(0, 12) + '...'
: reservation.title.length > 15
? reservation.title.substring(0, 15) + '...'
: reservation.title
}
</div>
<div style={{ fontSize: isMobile ? '0.45rem' : '0.5rem', opacity: 0.9 }}>
{reservation.resourceName}
</div>
</div>
))}
</div>
);
})}
</div>
))}
</div>
{/* Resource Legend */}
<div style={{ marginTop: '1.5rem', padding: '1rem', backgroundColor: '#f8f9fa', borderRadius: '4px' }}>
<h4 style={{ margin: '0 0 0.5rem 0' }}>Resources</h4>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
{resources.map((resource) => (
<div key={resource.resourceId} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<div
style={{
width: '16px',
height: '16px',
backgroundColor: getResourceColor(resource.resourceId),
border: '1px solid #ddd',
borderRadius: '2px'
}}
></div>
<span style={{ fontSize: '0.875rem' }}>{resource.name}</span>
</div>
))}
</div>
</div>
</div>
);
};
export default AllResourcesCalendar;

View File

@ -236,7 +236,7 @@ const UserDashboard: React.FC = () => {
<div style={{ marginTop: '1rem', display: 'flex', gap: '0.5rem' }}>
<Link
to={`/resources/${reservation.resourceId}/calendar`}
to="/calendar/all"
style={{
padding: '0.5rem 1rem',
backgroundColor: '#3498db',
@ -349,7 +349,7 @@ const UserDashboard: React.FC = () => {
fontSize: '0.875rem'
}}
>
View Calendar
View All Calendars
</Link>
</div>
</div>