365 lines
13 KiB
TypeScript
365 lines
13 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { Reservation } from '../types';
|
|
import { SimpleLibreBookingClient } from '../services/librebooking-api';
|
|
import { format, isToday, isTomorrow } from 'date-fns';
|
|
|
|
const UserDashboard: React.FC = () => {
|
|
const [reservations, setReservations] = useState<Reservation[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [filter, setFilter] = useState<'all' | 'upcoming' | 'past'>('upcoming');
|
|
|
|
useEffect(() => {
|
|
const api = new SimpleLibreBookingClient();
|
|
|
|
const loadReservations = async () => {
|
|
try {
|
|
const reservationsData = await api.getReservations();
|
|
setReservations(reservationsData);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to load reservations');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
loadReservations();
|
|
}, []);
|
|
|
|
// Filter reservations based on selected filter
|
|
const filteredReservations = filter === 'all'
|
|
? reservations
|
|
: filter === 'upcoming'
|
|
? reservations.filter(r => {
|
|
if (r.status === 'cancelled') return false;
|
|
// Use endDate field from API response
|
|
const endTime = new Date(r.endDate);
|
|
const now = new Date();
|
|
return endTime > now;
|
|
})
|
|
: reservations.filter(r => {
|
|
if (r.status === 'cancelled') return false;
|
|
// Use endDate field from API response
|
|
const endTime = new Date(r.endDate);
|
|
const now = new Date();
|
|
return endTime <= now;
|
|
});
|
|
|
|
// Debug: Check what status field exists
|
|
// Debug: Check the first reservation structure
|
|
if (reservations.length > 0) {
|
|
const firstReservation = reservations[0];
|
|
console.log('=== RESERVATION DEBUG ===');
|
|
console.log('Sample reservation:', firstReservation);
|
|
console.log('Available fields:', Object.keys(firstReservation));
|
|
console.log('Resource ID field:', firstReservation.resourceId);
|
|
console.log('Resource ID type:', typeof firstReservation.resourceId);
|
|
console.log('========================');
|
|
}
|
|
|
|
// Stats should always use all reservations (not filtered)
|
|
const upcomingReservations = reservations.filter(r => {
|
|
const endTime = new Date(r.endDate);
|
|
return endTime > new Date();
|
|
});
|
|
|
|
// Filtered versions for display
|
|
const filteredTodayReservations = filteredReservations.filter(r => {
|
|
const startTime = new Date(r.startDate);
|
|
const endTime = new Date(r.endDate);
|
|
return isToday(startTime) || isToday(endTime);
|
|
});
|
|
|
|
const getReservationLabel = (reservation: Reservation): string => {
|
|
// Use startDate and endDate from API response
|
|
const startDate = new Date(reservation.startDate);
|
|
const endDate = new Date(reservation.endDate);
|
|
|
|
// Validate dates before formatting
|
|
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
|
|
return 'Invalid date';
|
|
}
|
|
|
|
if (isToday(startDate)) {
|
|
return `Today, ${format(startDate, 'h:mm a')} - ${format(endDate, 'h:mm a')}`;
|
|
} else if (isTomorrow(startDate)) {
|
|
return `Tomorrow, ${format(startDate, 'h:mm a')} - ${format(endDate, 'h:mm a')}`;
|
|
} else {
|
|
return format(startDate, 'MMM d, h:mm a') + ' - ' + format(endDate, 'h:mm a');
|
|
}
|
|
};
|
|
|
|
const getStatusColor = (status: string): string => {
|
|
switch (status) {
|
|
case 'confirmed':
|
|
return '#27ae60';
|
|
case 'pending':
|
|
return '#f39c12';
|
|
case 'cancelled':
|
|
return '#e74c3c';
|
|
default:
|
|
return '#95a5a6';
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div>
|
|
<h2>My Reservations</h2>
|
|
<div>Loading your reservations...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div>
|
|
<h2>My Reservations</h2>
|
|
<div style={{ color: '#e74c3c' }}>Error: {error}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
|
|
<h2>My Reservations</h2>
|
|
<Link
|
|
to="/reservations/new"
|
|
style={{
|
|
display: 'inline-block',
|
|
padding: '0.75rem 1.5rem',
|
|
backgroundColor: '#27ae60',
|
|
color: 'white',
|
|
textDecoration: 'none',
|
|
borderRadius: '4px',
|
|
fontWeight: '500'
|
|
}}
|
|
>
|
|
New Booking
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Quick Stats */}
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1rem', marginBottom: '2rem' }}>
|
|
<div style={{ backgroundColor: 'white', padding: '1.5rem', borderRadius: '8px', border: '1px solid #ddd', textAlign: 'center' }}>
|
|
<div style={{ fontSize: '2rem', fontWeight: 'bold', color: '#3498db', marginBottom: '0.5rem' }}>
|
|
{upcomingReservations.length}
|
|
</div>
|
|
<div style={{ color: '#666' }}>Upcoming Bookings</div>
|
|
</div>
|
|
|
|
<div style={{ backgroundColor: 'white', padding: '1.5rem', borderRadius: '8px', border: '1px solid #ddd', textAlign: 'center' }}>
|
|
<div style={{ fontSize: '2rem', fontWeight: 'bold', color: '#f39c12', marginBottom: '0.5rem' }}>
|
|
{reservations.filter(r => r.requiresApproval).length}
|
|
</div>
|
|
<div style={{ color: '#666' }}>Pending Confirmation</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filter Buttons */}
|
|
<div style={{ marginBottom: '2rem' }}>
|
|
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem' }}>
|
|
{(['all', 'upcoming', 'past'] as const).map(filterOption => (
|
|
<button
|
|
key={filterOption}
|
|
onClick={() => setFilter(filterOption)}
|
|
style={{
|
|
padding: '0.5rem 1rem',
|
|
backgroundColor: filter === filterOption ? '#3498db' : '#ecf0f1',
|
|
color: filter === filterOption ? 'white' : '#2c3e50',
|
|
border: 'none',
|
|
borderRadius: '4px',
|
|
cursor: 'pointer',
|
|
fontWeight: filter === filterOption ? '500' : 'normal',
|
|
textTransform: 'capitalize'
|
|
}}
|
|
>
|
|
{filterOption}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div style={{ fontSize: '0.875rem', color: '#666' }}>
|
|
Showing {filteredReservations.length} {filter === 'all' ? 'reservations' : filter}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Today's Reservations */}
|
|
{filteredTodayReservations.length > 0 && (
|
|
<div style={{ marginBottom: '2rem' }}>
|
|
<h3 style={{ color: '#2c3e50', marginBottom: '1rem' }}>Today's Reservations</h3>
|
|
<div style={{ display: 'grid', gap: '1rem' }}>
|
|
{filteredTodayReservations.map(reservation => (
|
|
<div
|
|
key={reservation.referenceNumber}
|
|
style={{
|
|
backgroundColor: 'white',
|
|
padding: '1.5rem',
|
|
borderRadius: '8px',
|
|
border: '1px solid #ddd',
|
|
borderLeft: `4px solid ${getStatusColor(reservation.status)}`
|
|
}}
|
|
>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', marginBottom: '0.5rem' }}>
|
|
<div>
|
|
<h4 style={{ margin: '0 0 0.25rem 0', color: '#2c3e50' }}>
|
|
{reservation.title}
|
|
</h4>
|
|
<div style={{ color: '#666', fontSize: '0.875rem' }}>
|
|
{reservation.resourceName}
|
|
</div>
|
|
</div>
|
|
<span
|
|
style={{
|
|
padding: '0.25rem 0.75rem',
|
|
borderRadius: '20px',
|
|
fontSize: '0.75rem',
|
|
fontWeight: '500',
|
|
backgroundColor: getStatusColor(reservation.status) + '20',
|
|
color: getStatusColor(reservation.status)
|
|
}}
|
|
>
|
|
{reservation.status}
|
|
</span>
|
|
</div>
|
|
|
|
<div style={{ color: '#555', fontSize: '0.875rem', marginBottom: '0.5rem' }}>
|
|
{getReservationLabel(reservation)}
|
|
</div>
|
|
|
|
{reservation.description && (
|
|
<div style={{ color: '#666', fontSize: '0.875rem', fontStyle: 'italic' }}>
|
|
{reservation.description}
|
|
</div>
|
|
)}
|
|
|
|
<div style={{ marginTop: '1rem', display: 'flex', gap: '0.5rem' }}>
|
|
<Link
|
|
to="/calendar/all"
|
|
style={{
|
|
padding: '0.5rem 1rem',
|
|
backgroundColor: '#3498db',
|
|
color: 'white',
|
|
textDecoration: 'none',
|
|
borderRadius: '4px',
|
|
fontSize: '0.875rem'
|
|
}}
|
|
>
|
|
View Calendar
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* All Reservations */}
|
|
<div>
|
|
<h3 style={{ color: '#2c3e50', marginBottom: '1rem' }}>All Reservations</h3>
|
|
|
|
{filteredReservations.length === 0 ? (
|
|
<div style={{
|
|
textAlign: 'center',
|
|
padding: '2rem',
|
|
backgroundColor: '#f8f9fa',
|
|
borderRadius: '8px',
|
|
border: '1px solid #ddd'
|
|
}}>
|
|
<p style={{ color: '#666', marginBottom: '1rem' }}>
|
|
You don't have any reservations yet.
|
|
</p>
|
|
<Link
|
|
to="/resources"
|
|
style={{
|
|
display: 'inline-block',
|
|
padding: '0.75rem 1.5rem',
|
|
backgroundColor: '#3498db',
|
|
color: 'white',
|
|
textDecoration: 'none',
|
|
borderRadius: '4px'
|
|
}}
|
|
>
|
|
Browse Resources
|
|
</Link>
|
|
</div>
|
|
) : (
|
|
<div style={{ display: 'grid', gap: '1rem' }}>
|
|
{filteredReservations
|
|
.sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime())
|
|
.map(reservation => (
|
|
<div
|
|
key={reservation.referenceNumber}
|
|
style={{
|
|
backgroundColor: 'white',
|
|
padding: '1.5rem',
|
|
borderRadius: '8px',
|
|
border: '1px solid #ddd',
|
|
borderLeft: `4px solid ${getStatusColor(reservation.status)}`
|
|
}}
|
|
>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', marginBottom: '0.5rem' }}>
|
|
<div>
|
|
<h4 style={{ margin: '0 0 0.25rem 0', color: '#2c3e50' }}>
|
|
{reservation.title}
|
|
</h4>
|
|
<div style={{ color: '#666', fontSize: '0.875rem' }}>
|
|
{reservation.resourceName}
|
|
</div>
|
|
</div>
|
|
<span
|
|
style={{
|
|
padding: '0.25rem 0.75rem',
|
|
borderRadius: '20px',
|
|
fontSize: '0.75rem',
|
|
fontWeight: '500',
|
|
backgroundColor: getStatusColor(reservation.status) + '20',
|
|
color: getStatusColor(reservation.status)
|
|
}}
|
|
>
|
|
{reservation.status}
|
|
</span>
|
|
</div>
|
|
|
|
<div style={{ color: '#555', fontSize: '0.875rem', marginBottom: '0.5rem' }}>
|
|
{getReservationLabel(reservation)}
|
|
</div>
|
|
|
|
{reservation.description && (
|
|
<div style={{ color: '#666', fontSize: '0.875rem', fontStyle: 'italic', marginBottom: '0.5rem' }}>
|
|
{reservation.description}
|
|
</div>
|
|
)}
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<div style={{ color: '#888', fontSize: '0.75rem' }}>
|
|
Booked on {reservation.startDate ? format(new Date(reservation.startDate), 'MMM d, yyyy') : 'Unknown date'}
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
|
<Link
|
|
to={`/resources/${reservation.resourceId}/calendar`}
|
|
style={{
|
|
padding: '0.5rem 1rem',
|
|
backgroundColor: '#3498db',
|
|
color: 'white',
|
|
textDecoration: 'none',
|
|
borderRadius: '4px',
|
|
fontSize: '0.875rem'
|
|
}}
|
|
>
|
|
View All Calendars
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default UserDashboard; |