Files
tt-booking/components/booking/user-booking-management.tsx
T

433 lines
12 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Calendar, Clock, MapPin, Edit, Trash2, User, RefreshCw } from 'lucide-react';
import { format } from 'date-fns';
import { useToast } from '@/hooks/use-toast';
interface Booking {
id: string;
date: string;
startTime: string;
endTime: string;
status: string;
notes?: string;
createdAt: Date;
court: {
id: string;
name: string;
};
}
export function UserBookingManagement() {
const [bookings, setBookings] = useState<Booking[]>([]);
const [loading, setLoading] = useState(true);
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [selectedBooking, setSelectedBooking] = useState<Booking | null>(null);
const [editNotes, setEditNotes] = useState('');
const [editPartner, setEditPartner] = useState('');
const { toast } = useToast();
useEffect(() => {
fetchBookings();
}, []);
const fetchBookings = async () => {
try {
setLoading(true);
const response = await fetch('/api/bookings');
if (response.ok) {
const data = await response.json();
// Filter to show only future and today's bookings
const now = new Date();
const today = now.toISOString().split('T')[0];
const relevantBookings = data.bookings.filter((booking: Booking) => {
if (booking.status !== 'active') return false;
return booking.date >= today;
});
setBookings(relevantBookings);
}
} catch (error) {
console.error('Error fetching bookings:', error);
toast({
title: 'Error',
description: 'Failed to fetch your bookings',
variant: 'destructive',
});
} finally {
setLoading(false);
}
};
const parseBookingNotes = (notes?: string) => {
if (!notes) return { partner: '', additionalNotes: '' };
const parts = notes.split(' | ');
let partner = '';
let additionalNotes = '';
parts.forEach((part) => {
if (part.startsWith('Partner: ')) {
partner = part.replace('Partner: ', '');
} else {
additionalNotes = additionalNotes ? `${additionalNotes} | ${part}` : part;
}
});
return { partner, additionalNotes };
};
const handleEditClick = (booking: Booking) => {
setSelectedBooking(booking);
const { partner, additionalNotes } = parseBookingNotes(booking.notes);
setEditPartner(partner);
setEditNotes(additionalNotes);
setEditDialogOpen(true);
};
const handleDeleteClick = (booking: Booking) => {
setSelectedBooking(booking);
setDeleteDialogOpen(true);
};
const handleEditSave = async () => {
if (!selectedBooking) return;
try {
const bookingNotes = [];
if (editPartner.trim()) {
bookingNotes.push(`Partner: ${editPartner.trim()}`);
}
if (editNotes.trim()) {
bookingNotes.push(editNotes.trim());
}
const response = await fetch(`/api/bookings/${selectedBooking.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
notes: bookingNotes.join(' | '),
}),
});
if (response.ok) {
toast({
title: 'Success',
description: 'Booking updated successfully',
});
setEditDialogOpen(false);
fetchBookings();
} else {
const data = await response.json();
toast({
title: 'Error',
description: data.error || 'Failed to update booking',
variant: 'destructive',
});
}
} catch (error) {
console.error('Error updating booking:', error);
toast({
title: 'Error',
description: 'Failed to update booking',
variant: 'destructive',
});
}
};
const handleDeleteConfirm = async () => {
if (!selectedBooking) return;
try {
const response = await fetch(`/api/bookings/${selectedBooking.id}`, {
method: 'DELETE',
});
if (response.ok) {
toast({
title: 'Success',
description: 'Booking cancelled successfully',
});
setDeleteDialogOpen(false);
fetchBookings();
} else {
const data = await response.json();
toast({
title: 'Error',
description: data.error || 'Failed to cancel booking',
variant: 'destructive',
});
}
} catch (error) {
console.error('Error cancelling booking:', error);
toast({
title: 'Error',
description: 'Failed to cancel booking',
variant: 'destructive',
});
}
};
const formatDate = (dateStr: string) => {
try {
const date = new Date(dateStr);
return format(date, 'EEE, MMM dd');
} catch {
return dateStr;
}
};
const isToday = (dateStr: string) => {
const today = new Date().toISOString().split('T')[0];
return dateStr === today;
};
const canModifyBooking = (booking: Booking) => {
const bookingDateTime = new Date(`${booking.date}T${booking.startTime}`);
const now = new Date();
const timeDiff = bookingDateTime.getTime() - now.getTime();
const hoursDiff = timeDiff / (1000 * 60 * 60);
// Allow modifications if booking is more than 2 hours away
return hoursDiff > 2;
};
if (loading) {
return (
<Card>
<CardHeader className='pb-3'>
<CardTitle className='text-base flex items-center gap-2'>
<Calendar className='h-4 w-4' />
Your Bookings
</CardTitle>
</CardHeader>
<CardContent>
<div className='space-y-3'>
{[1, 2, 3].map((i) => (
<div key={i} className='animate-pulse border rounded-lg p-4'>
<div className='h-4 bg-gray-200 rounded w-3/4 mb-2'></div>
<div className='h-3 bg-gray-200 rounded w-1/2'></div>
</div>
))}
</div>
</CardContent>
</Card>
);
}
return (
<>
<Card>
<CardHeader className='pb-3 flex flex-row items-center justify-between'>
<CardTitle className='text-base flex items-center gap-2'>
<Calendar className='h-4 w-4' />
Your Bookings
</CardTitle>
<Button size='sm' variant='outline' onClick={fetchBookings}>
<RefreshCw className='h-4 w-4' />
</Button>
</CardHeader>
<CardContent>
{bookings.length === 0 ? (
<div className='text-sm text-gray-500 text-center py-6'>
No upcoming bookings. Make your first booking!
</div>
) : (
<div className='space-y-3'>
{bookings.map((booking) => {
const { partner, additionalNotes } = parseBookingNotes(booking.notes);
const canModify = canModifyBooking(booking);
return (
<div key={booking.id} className='border rounded-lg p-4 space-y-3'>
<div className='flex items-start justify-between'>
<div className='space-y-2 flex-1'>
<div className='flex items-center gap-2'>
<MapPin className='h-4 w-4 text-blue-600' />
<span className='font-medium text-sm'>{booking.court.name}</span>
{isToday(booking.date) && (
<Badge
variant='secondary'
className='text-xs bg-gradient-to-r from-orange-100 to-orange-200 text-orange-700 border-orange-300'
>
🎯 Today
</Badge>
)}
</div>
<div className='flex items-center gap-4 text-xs text-gray-500'>
<div className='flex items-center gap-1'>
<Calendar className='h-3 w-3' />
<span>{formatDate(booking.date)}</span>
</div>
<div className='flex items-center gap-1'>
<Clock className='h-3 w-3' />
<span>
{booking.startTime} - {booking.endTime}
</span>
</div>
</div>
{partner && (
<div className='flex items-center gap-1 text-xs text-gray-600'>
<User className='h-3 w-3' />
<span>Playing with: {partner}</span>
</div>
)}
{additionalNotes && (
<p className='text-xs text-gray-600 italic bg-gray-50 p-2 rounded'>
{additionalNotes}
</p>
)}
</div>
<div className='flex gap-1 ml-2'>
<Button
size='sm'
variant='outline'
onClick={() => handleEditClick(booking)}
disabled={!canModify}
className='h-8 w-8 p-0'
>
<Edit className='h-3 w-3' />
</Button>
<Button
size='sm'
variant='outline'
onClick={() => handleDeleteClick(booking)}
disabled={!canModify}
className='h-8 w-8 p-0 text-red-600 hover:text-red-700'
>
<Trash2 className='h-3 w-3' />
</Button>
</div>
</div>
{!canModify && (
<p className='text-xs text-amber-600 bg-amber-50 p-2 rounded'>
Booking can only be modified more than 2 hours before the session
</p>
)}
</div>
);
})}
</div>
)}
</CardContent>
</Card>
{/* Edit Dialog */}
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
<DialogContent className='sm:max-w-md'>
<DialogHeader>
<DialogTitle>Edit Booking</DialogTitle>
</DialogHeader>
<div className='space-y-4'>
{selectedBooking && (
<div className='bg-blue-50 p-4 rounded-lg space-y-2'>
<div className='flex items-center gap-2 text-sm'>
<Calendar className='h-4 w-4' />
{formatDate(selectedBooking.date)}
</div>
<div className='flex items-center gap-2 text-sm'>
<Clock className='h-4 w-4' />
{selectedBooking.startTime} - {selectedBooking.endTime}
</div>
<div className='flex items-center gap-2 text-sm'>
<MapPin className='h-4 w-4' />
{selectedBooking.court.name}
</div>
</div>
)}
<div className='space-y-2'>
<Label htmlFor='edit-partner'>Playing Partner</Label>
<div className='relative'>
<User className='absolute left-3 top-3 h-4 w-4 text-gray-400' />
<Input
id='edit-partner'
placeholder='Who will you be playing with?'
value={editPartner}
onChange={(e) => setEditPartner(e.target.value)}
className='pl-10'
/>
</div>
</div>
<div className='space-y-2'>
<Label htmlFor='edit-notes'>Additional Notes</Label>
<Textarea
id='edit-notes'
placeholder='Any additional information...'
value={editNotes}
onChange={(e) => setEditNotes(e.target.value)}
className='min-h-[80px]'
/>
</div>
<div className='flex gap-2 pt-4'>
<Button variant='outline' className='flex-1' onClick={() => setEditDialogOpen(false)}>
Cancel
</Button>
<Button className='flex-1' onClick={handleEditSave}>
Save Changes
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Delete Confirmation Dialog */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Cancel Booking</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to cancel this booking? This action cannot be undone.
{selectedBooking && (
<div className='mt-3 p-3 bg-gray-50 rounded'>
<p className='text-sm font-medium'>
{selectedBooking.court.name} - {formatDate(selectedBooking.date)}
</p>
<p className='text-sm text-gray-600'>
{selectedBooking.startTime} - {selectedBooking.endTime}
</p>
</div>
)}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Keep Booking</AlertDialogCancel>
<AlertDialogAction onClick={handleDeleteConfirm} className='bg-red-600 hover:bg-red-700'>
Cancel Booking
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}