fixes, theming, branding
This commit is contained in:
@@ -8,6 +8,17 @@ import { Label } from '@/components/ui/label';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
@@ -37,7 +48,9 @@ export function AdminAnnouncementManagement() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [editingAnnouncement, setEditingAnnouncement] = useState<Announcement | null>(null);
|
||||
const [announcementToDelete, setAnnouncementToDelete] = useState<Announcement | null>(null);
|
||||
const [formData, setFormData] = useState<AnnouncementFormData>({
|
||||
title: '',
|
||||
content: '',
|
||||
@@ -176,12 +189,21 @@ export function AdminAnnouncementManagement() {
|
||||
}
|
||||
};
|
||||
|
||||
const openDeleteDialog = (announcement: Announcement) => {
|
||||
setAnnouncementToDelete(announcement);
|
||||
setIsDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const confirmDeleteAnnouncement = async () => {
|
||||
if (announcementToDelete) {
|
||||
await handleDeleteAnnouncement(announcementToDelete.id);
|
||||
setIsDeleteDialogOpen(false);
|
||||
setAnnouncementToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAnnouncement = async (announcementId: string) => {
|
||||
try {
|
||||
if (!confirm('Are you sure you want to delete this announcement?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/admin/announcements/${announcementId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
@@ -444,7 +466,7 @@ export function AdminAnnouncementManagement() {
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => handleDeleteAnnouncement(announcement.id)}
|
||||
onClick={() => openDeleteDialog(announcement)}
|
||||
className='text-destructive hover:text-destructive/90'
|
||||
>
|
||||
<Trash2 className='h-4 w-4' />
|
||||
@@ -539,6 +561,29 @@ export function AdminAnnouncementManagement() {
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete{' '}
|
||||
{announcementToDelete ? `"${announcementToDelete.title}"` : 'this announcement'}? This
|
||||
action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDeleteAnnouncement}
|
||||
className='bg-destructive hover:bg-destructive/90'
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,17 @@ import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
@@ -32,7 +43,9 @@ export function AdminCourtManagement() {
|
||||
const [editing, setEditing] = useState<string | null>(null);
|
||||
const [deleting, setDeleting] = useState<string | null>(null);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [editingCourt, setEditingCourt] = useState<Court | null>(null);
|
||||
const [courtToDelete, setCourtToDelete] = useState<Court | null>(null);
|
||||
const [formData, setFormData] = useState<CourtFormData>({
|
||||
name: '',
|
||||
isActive: true,
|
||||
@@ -150,11 +163,20 @@ export function AdminCourtManagement() {
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (courtId: string) => {
|
||||
if (!confirm('Are you sure you want to delete this court? This action cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
const openDeleteDialog = (court: Court) => {
|
||||
setCourtToDelete(court);
|
||||
setIsDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const confirmDeleteCourt = async () => {
|
||||
if (courtToDelete) {
|
||||
await handleDelete(courtToDelete.id);
|
||||
setIsDeleteDialogOpen(false);
|
||||
setCourtToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (courtId: string) => {
|
||||
try {
|
||||
setDeleting(courtId);
|
||||
const response = await fetch(`/api/admin/courts/${courtId}`, {
|
||||
@@ -319,7 +341,7 @@ export function AdminCourtManagement() {
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outline'
|
||||
onClick={() => handleDelete(court.id)}
|
||||
onClick={() => openDeleteDialog(court)}
|
||||
disabled={deleting === court.id}
|
||||
className='text-red-600 hover:text-red-700'
|
||||
>
|
||||
@@ -337,6 +359,28 @@ export function AdminCourtManagement() {
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete {courtToDelete ? `"${courtToDelete.name}"` : 'this court'}?
|
||||
This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDeleteCourt}
|
||||
className='bg-destructive hover:bg-destructive/90'
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,12 @@ interface Setting {
|
||||
}
|
||||
|
||||
interface SettingsData {
|
||||
// Club/Brand Settings
|
||||
club_name: string;
|
||||
sport_name: string;
|
||||
app_title: string;
|
||||
app_description: string;
|
||||
// Booking Settings
|
||||
booking_window_days: string;
|
||||
max_booking_duration_hours: string;
|
||||
min_booking_duration_minutes: string;
|
||||
@@ -30,6 +36,12 @@ interface SettingsData {
|
||||
|
||||
export function AdminSettingsManagement() {
|
||||
const [settings, setSettings] = useState<SettingsData>({
|
||||
// Club/Brand Settings
|
||||
club_name: 'TT Club',
|
||||
sport_name: 'Table Tennis',
|
||||
app_title: 'Table Tennis Booking System',
|
||||
app_description: 'Book your table tennis court slots with ease',
|
||||
// Booking Settings
|
||||
booking_window_days: '7',
|
||||
max_booking_duration_hours: '2',
|
||||
min_booking_duration_minutes: '30',
|
||||
@@ -38,7 +50,7 @@ export function AdminSettingsManagement() {
|
||||
allow_weekend_bookings: 'true',
|
||||
max_bookings_per_user_per_hour_per_day: '1',
|
||||
allow_booking_modifications: 'true',
|
||||
booking_modification_hours_before: '2',
|
||||
booking_modification_hours_before: '1',
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -54,6 +66,12 @@ export function AdminSettingsManagement() {
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const settingsMap: SettingsData = {
|
||||
// Club/Brand Settings
|
||||
club_name: 'TT Club',
|
||||
sport_name: 'Table Tennis',
|
||||
app_title: 'Table Tennis Booking System',
|
||||
app_description: 'Book your table tennis court slots with ease',
|
||||
// Booking Settings
|
||||
booking_window_days: '7',
|
||||
max_booking_duration_hours: '2',
|
||||
min_booking_duration_minutes: '30',
|
||||
@@ -62,10 +80,8 @@ export function AdminSettingsManagement() {
|
||||
allow_weekend_bookings: 'true',
|
||||
max_bookings_per_user_per_hour_per_day: '1',
|
||||
allow_booking_modifications: 'true',
|
||||
booking_modification_hours_before: '2',
|
||||
};
|
||||
|
||||
// Map the settings array to our object
|
||||
booking_modification_hours_before: '1',
|
||||
}; // Map the settings array to our object
|
||||
data.settings?.forEach((setting: Setting) => {
|
||||
if (setting.key in settingsMap) {
|
||||
settingsMap[setting.key as keyof SettingsData] = setting.value;
|
||||
@@ -189,139 +205,205 @@ export function AdminSettingsManagement() {
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-6'>
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 gap-6'>
|
||||
{/* Booking Window */}
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='booking_window_days'>Booking Window (days)</Label>
|
||||
<Input
|
||||
id='booking_window_days'
|
||||
type='number'
|
||||
min='1'
|
||||
max='30'
|
||||
value={settings.booking_window_days}
|
||||
onChange={(e) => updateSetting('booking_window_days', e.target.value)}
|
||||
/>
|
||||
<p className='text-sm text-gray-500'>How many days in advance users can book</p>
|
||||
</div>
|
||||
{/* Club/Brand Configuration Section */}
|
||||
<div>
|
||||
<h3 className='text-lg font-medium mb-4'>Club & Branding</h3>
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 gap-6'>
|
||||
{/* Club Name */}
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='club_name'>Club Name</Label>
|
||||
<Input
|
||||
id='club_name'
|
||||
type='text'
|
||||
placeholder='e.g., Downtown TT Club'
|
||||
value={settings.club_name}
|
||||
onChange={(e) => updateSetting('club_name', e.target.value)}
|
||||
/>
|
||||
<p className='text-sm text-gray-500'>The name of your club or organization</p>
|
||||
</div>
|
||||
|
||||
{/* Max Duration */}
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='max_booking_duration_hours'>Max Booking Duration (hours)</Label>
|
||||
<Input
|
||||
id='max_booking_duration_hours'
|
||||
type='number'
|
||||
min='0.5'
|
||||
max='8'
|
||||
step='0.5'
|
||||
value={settings.max_booking_duration_hours}
|
||||
onChange={(e) => updateSetting('max_booking_duration_hours', e.target.value)}
|
||||
/>
|
||||
<p className='text-sm text-gray-500'>Maximum hours per booking session</p>
|
||||
</div>
|
||||
{/* Sport Name */}
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='sport_name'>Sport Name</Label>
|
||||
<Input
|
||||
id='sport_name'
|
||||
type='text'
|
||||
placeholder='e.g., Table Tennis, Ping Pong, Badminton'
|
||||
value={settings.sport_name}
|
||||
onChange={(e) => updateSetting('sport_name', e.target.value)}
|
||||
/>
|
||||
<p className='text-sm text-gray-500'>The sport played at your facility</p>
|
||||
</div>
|
||||
|
||||
{/* Min Duration */}
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='min_booking_duration_minutes'>Min Booking Duration (minutes)</Label>
|
||||
<Input
|
||||
id='min_booking_duration_minutes'
|
||||
type='number'
|
||||
min='15'
|
||||
max='120'
|
||||
step='15'
|
||||
value={settings.min_booking_duration_minutes}
|
||||
onChange={(e) => updateSetting('min_booking_duration_minutes', e.target.value)}
|
||||
/>
|
||||
<p className='text-sm text-gray-500'>Minimum minutes per booking session</p>
|
||||
</div>
|
||||
{/* App Title */}
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='app_title'>Application Title</Label>
|
||||
<Input
|
||||
id='app_title'
|
||||
type='text'
|
||||
placeholder='e.g., Downtown TT Booking'
|
||||
value={settings.app_title}
|
||||
onChange={(e) => updateSetting('app_title', e.target.value)}
|
||||
/>
|
||||
<p className='text-sm text-gray-500'>Main title shown in browser and app header</p>
|
||||
</div>
|
||||
|
||||
{/* Start Time */}
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='booking_start_time'>Daily Start Time</Label>
|
||||
<Input
|
||||
id='booking_start_time'
|
||||
type='time'
|
||||
value={settings.booking_start_time}
|
||||
onChange={(e) => updateSetting('booking_start_time', e.target.value)}
|
||||
/>
|
||||
<p className='text-sm text-gray-500'>When courts open for booking each day</p>
|
||||
{/* App Description */}
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='app_description'>Application Description</Label>
|
||||
<Input
|
||||
id='app_description'
|
||||
type='text'
|
||||
placeholder='e.g., Book your court slots with ease'
|
||||
value={settings.app_description}
|
||||
onChange={(e) => updateSetting('app_description', e.target.value)}
|
||||
/>
|
||||
<p className='text-sm text-gray-500'>Short description for login/register pages</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* End Time */}
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='booking_end_time'>Daily End Time</Label>
|
||||
<Input
|
||||
id='booking_end_time'
|
||||
type='time'
|
||||
value={settings.booking_end_time}
|
||||
onChange={(e) => updateSetting('booking_end_time', e.target.value)}
|
||||
/>
|
||||
<p className='text-sm text-gray-500'>When courts close for booking each day</p>
|
||||
</div>
|
||||
{/* Booking Configuration Section */}
|
||||
<div>
|
||||
<h3 className='text-lg font-medium mb-4'>Booking Configuration</h3>
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 gap-6'>
|
||||
{/* Booking Window */}
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='booking_window_days'>Booking Window (days)</Label>
|
||||
<Input
|
||||
id='booking_window_days'
|
||||
type='number'
|
||||
min='1'
|
||||
max='30'
|
||||
value={settings.booking_window_days}
|
||||
onChange={(e) => updateSetting('booking_window_days', e.target.value)}
|
||||
/>
|
||||
<p className='text-sm text-gray-500'>How many days in advance users can book</p>
|
||||
</div>
|
||||
|
||||
{/* Booking Restrictions */}
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='max_bookings_per_user_per_hour_per_day'>Max Bookings per User per Hour</Label>
|
||||
<Input
|
||||
id='max_bookings_per_user_per_hour_per_day'
|
||||
type='number'
|
||||
min='1'
|
||||
max='5'
|
||||
value={settings.max_bookings_per_user_per_hour_per_day}
|
||||
onChange={(e) => updateSetting('max_bookings_per_user_per_hour_per_day', e.target.value)}
|
||||
/>
|
||||
<p className='text-sm text-gray-500'>Maximum bookings per user per hour on the same day</p>
|
||||
</div>
|
||||
{/* Max Duration */}
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='max_booking_duration_hours'>Max Booking Duration (hours)</Label>
|
||||
<Input
|
||||
id='max_booking_duration_hours'
|
||||
type='number'
|
||||
min='0.5'
|
||||
max='8'
|
||||
step='0.5'
|
||||
value={settings.max_booking_duration_hours}
|
||||
onChange={(e) => updateSetting('max_booking_duration_hours', e.target.value)}
|
||||
/>
|
||||
<p className='text-sm text-gray-500'>Maximum hours per booking session</p>
|
||||
</div>
|
||||
|
||||
{/* Booking Modification Settings */}
|
||||
<div className='space-y-2'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<Switch
|
||||
id='allow_booking_modifications'
|
||||
checked={settings.allow_booking_modifications === 'true'}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
updateSetting('allow_booking_modifications', checked.toString())
|
||||
{/* Min Duration */}
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='min_booking_duration_minutes'>Min Booking Duration (minutes)</Label>
|
||||
<Input
|
||||
id='min_booking_duration_minutes'
|
||||
type='number'
|
||||
min='15'
|
||||
max='120'
|
||||
step='15'
|
||||
value={settings.min_booking_duration_minutes}
|
||||
onChange={(e) => updateSetting('min_booking_duration_minutes', e.target.value)}
|
||||
/>
|
||||
<p className='text-sm text-gray-500'>Minimum minutes per booking session</p>
|
||||
</div>
|
||||
|
||||
{/* Start Time */}
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='booking_start_time'>Daily Start Time</Label>
|
||||
<Input
|
||||
id='booking_start_time'
|
||||
type='time'
|
||||
value={settings.booking_start_time}
|
||||
onChange={(e) => updateSetting('booking_start_time', e.target.value)}
|
||||
/>
|
||||
<p className='text-sm text-gray-500'>When courts open for booking each day</p>
|
||||
</div>
|
||||
|
||||
{/* End Time */}
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='booking_end_time'>Daily End Time</Label>
|
||||
<Input
|
||||
id='booking_end_time'
|
||||
type='time'
|
||||
value={settings.booking_end_time}
|
||||
onChange={(e) => updateSetting('booking_end_time', e.target.value)}
|
||||
/>
|
||||
<p className='text-sm text-gray-500'>When courts close for booking each day</p>
|
||||
</div>
|
||||
|
||||
{/* Booking Restrictions */}
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='max_bookings_per_user_per_hour_per_day'>
|
||||
Max Bookings per User per Hour
|
||||
</Label>
|
||||
<Input
|
||||
id='max_bookings_per_user_per_hour_per_day'
|
||||
type='number'
|
||||
min='1'
|
||||
max='5'
|
||||
value={settings.max_bookings_per_user_per_hour_per_day}
|
||||
onChange={(e) =>
|
||||
updateSetting('max_bookings_per_user_per_hour_per_day', e.target.value)
|
||||
}
|
||||
/>
|
||||
<Label htmlFor='allow_booking_modifications'>Allow Booking Modifications</Label>
|
||||
<p className='text-sm text-gray-500'>Maximum bookings per user per hour on the same day</p>
|
||||
</div>
|
||||
<p className='text-sm text-gray-500'>Whether users can edit or cancel their bookings</p>
|
||||
</div>
|
||||
|
||||
{/* Modification Time Restriction */}
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='booking_modification_hours_before'>
|
||||
Modification Time Limit (hours before session)
|
||||
</Label>
|
||||
<Input
|
||||
id='booking_modification_hours_before'
|
||||
type='number'
|
||||
min='0.5'
|
||||
max='48'
|
||||
step='0.5'
|
||||
value={settings.booking_modification_hours_before}
|
||||
onChange={(e) => updateSetting('booking_modification_hours_before', e.target.value)}
|
||||
disabled={settings.allow_booking_modifications !== 'true'}
|
||||
/>
|
||||
<p className='text-sm text-gray-500'>
|
||||
{settings.allow_booking_modifications === 'true'
|
||||
? 'How many hours before a session users can still modify bookings'
|
||||
: 'Enable booking modifications to configure this setting'}
|
||||
</p>
|
||||
</div>
|
||||
{/* Booking Modification Settings */}
|
||||
<div className='space-y-2'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<Switch
|
||||
id='allow_booking_modifications'
|
||||
checked={settings.allow_booking_modifications === 'true'}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
updateSetting('allow_booking_modifications', checked.toString())
|
||||
}
|
||||
/>
|
||||
<Label htmlFor='allow_booking_modifications'>Allow Booking Modifications</Label>
|
||||
</div>
|
||||
<p className='text-sm text-gray-500'>Whether users can edit or cancel their bookings</p>
|
||||
</div>
|
||||
|
||||
{/* Weekend Bookings */}
|
||||
<div className='space-y-2'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<Switch
|
||||
id='allow_weekend_bookings'
|
||||
checked={settings.allow_weekend_bookings === 'true'}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
updateSetting('allow_weekend_bookings', checked.toString())
|
||||
}
|
||||
{/* Modification Time Restriction */}
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='booking_modification_hours_before'>
|
||||
Modification Time Limit (hours before session)
|
||||
</Label>
|
||||
<Input
|
||||
id='booking_modification_hours_before'
|
||||
type='number'
|
||||
min='0.5'
|
||||
max='48'
|
||||
step='0.5'
|
||||
value={settings.booking_modification_hours_before}
|
||||
onChange={(e) => updateSetting('booking_modification_hours_before', e.target.value)}
|
||||
disabled={settings.allow_booking_modifications !== 'true'}
|
||||
/>
|
||||
<Label htmlFor='allow_weekend_bookings'>Allow Weekend Bookings</Label>
|
||||
<p className='text-sm text-gray-500'>
|
||||
{settings.allow_booking_modifications === 'true'
|
||||
? 'How many hours before a session users can still modify bookings'
|
||||
: 'Enable booking modifications to configure this setting'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Weekend Bookings */}
|
||||
<div className='space-y-2'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<Switch
|
||||
id='allow_weekend_bookings'
|
||||
checked={settings.allow_weekend_bookings === 'true'}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
updateSetting('allow_weekend_bookings', checked.toString())
|
||||
}
|
||||
/>
|
||||
<Label htmlFor='allow_weekend_bookings'>Allow Weekend Bookings</Label>
|
||||
</div>
|
||||
<p className='text-sm text-gray-500'>Whether users can book courts on weekends</p>
|
||||
</div>
|
||||
<p className='text-sm text-gray-500'>Whether users can book courts on weekends</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,6 +7,17 @@ import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Plus, Edit, Trash2, Clock } from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
@@ -30,7 +41,11 @@ export function AdminTimeSlotManagement() {
|
||||
const [timeSlots, setTimeSlots] = useState<TimeSlot[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [showWipeDayDialog, setShowWipeDayDialog] = useState(false);
|
||||
const [editingSlot, setEditingSlot] = useState<TimeSlot | null>(null);
|
||||
const [slotToDelete, setSlotToDelete] = useState<TimeSlot | null>(null);
|
||||
const [dayToWipe, setDayToWipe] = useState<number | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
dayOfWeek: 1, // Default to Monday (Irish standard)
|
||||
startTime: '',
|
||||
@@ -115,11 +130,20 @@ export function AdminTimeSlotManagement() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('Are you sure you want to delete this time slot?')) {
|
||||
return;
|
||||
}
|
||||
const openDeleteDialog = (slot: TimeSlot) => {
|
||||
setSlotToDelete(slot);
|
||||
setShowDeleteDialog(true);
|
||||
};
|
||||
|
||||
const confirmDeleteSlot = async () => {
|
||||
if (slotToDelete) {
|
||||
await handleDelete(slotToDelete.id);
|
||||
setShowDeleteDialog(false);
|
||||
setSlotToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch(`/api/admin/time-slots/${id}`, {
|
||||
@@ -152,14 +176,23 @@ export function AdminTimeSlotManagement() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleWipeDay = async (dayOfWeek: number) => {
|
||||
const dayName = DAYS[dayOfWeek];
|
||||
if (!confirm(`Are you sure you want to delete ALL time slots for ${dayName}? This action cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
const openWipeDayDialog = (dayOfWeek: number) => {
|
||||
setDayToWipe(dayOfWeek);
|
||||
setShowWipeDayDialog(true);
|
||||
};
|
||||
|
||||
const confirmWipeDay = async () => {
|
||||
if (dayToWipe !== null) {
|
||||
await handleWipeDay(dayToWipe);
|
||||
setShowWipeDayDialog(false);
|
||||
setDayToWipe(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWipeDay = async (dayOfWeek: number) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const dayName = DAYS[dayOfWeek];
|
||||
const slotsToDelete = timeSlots.filter((slot) => slot.dayOfWeek === dayOfWeek);
|
||||
|
||||
// Delete all slots for this day
|
||||
@@ -322,7 +355,7 @@ export function AdminTimeSlotManagement() {
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outline'
|
||||
onClick={() => handleWipeDay(jsDayOfWeek)}
|
||||
onClick={() => openWipeDayDialog(jsDayOfWeek)}
|
||||
className='text-destructive hover:text-destructive/80 hover:bg-destructive/10'
|
||||
disabled={loading}
|
||||
>
|
||||
@@ -369,7 +402,7 @@ export function AdminTimeSlotManagement() {
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outline'
|
||||
onClick={() => handleDelete(slot.id)}
|
||||
onClick={() => openDeleteDialog(slot)}
|
||||
className='text-destructive hover:text-destructive/80'
|
||||
>
|
||||
<Trash2 className='h-4 w-4' />
|
||||
@@ -389,6 +422,46 @@ export function AdminTimeSlotManagement() {
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
{/* Delete Time Slot Dialog */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete this time slot? This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDeleteSlot}
|
||||
className='bg-destructive hover:bg-destructive/90'
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* Wipe Day Dialog */}
|
||||
<AlertDialog open={showWipeDayDialog} onOpenChange={setShowWipeDayDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete ALL time slots for{' '}
|
||||
{dayToWipe !== null ? DAYS[dayToWipe] : 'this day'}? This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={confirmWipeDay} className='bg-destructive hover:bg-destructive/90'>
|
||||
Delete All
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,17 @@ import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
@@ -35,7 +46,9 @@ export function AdminUserManagement() {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [editingUser, setEditingUser] = useState<User | null>(null);
|
||||
const [userToDelete, setUserToDelete] = useState<User | null>(null);
|
||||
const [formData, setFormData] = useState<UserFormData>({
|
||||
name: '',
|
||||
surname: '',
|
||||
@@ -184,12 +197,21 @@ export function AdminUserManagement() {
|
||||
}
|
||||
};
|
||||
|
||||
const openDeleteDialog = (user: User) => {
|
||||
setUserToDelete(user);
|
||||
setIsDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const confirmDeleteUser = async () => {
|
||||
if (userToDelete) {
|
||||
await handleDeleteUser(userToDelete.id);
|
||||
setIsDeleteDialogOpen(false);
|
||||
setUserToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteUser = async (userId: string) => {
|
||||
try {
|
||||
if (!confirm('Are you sure you want to delete this user? This action cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/admin/users/${userId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
@@ -406,7 +428,7 @@ export function AdminUserManagement() {
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => handleDeleteUser(user.id)}
|
||||
onClick={() => openDeleteDialog(user)}
|
||||
className='text-red-600 hover:text-red-700'
|
||||
>
|
||||
<Trash2 className='h-4 w-4' />
|
||||
@@ -498,6 +520,29 @@ export function AdminUserManagement() {
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete{' '}
|
||||
{userToDelete ? `${userToDelete.name} ${userToDelete.surname}` : 'this user'}? This action
|
||||
cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDeleteUser}
|
||||
className='bg-destructive hover:bg-destructive/90'
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -473,11 +473,11 @@ export function EnhancedBookingCalendar() {
|
||||
: ''
|
||||
} ${
|
||||
isTodayDate && !isSelectedDate
|
||||
? 'ring-2 ring-blue-400 ring-opacity-50 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800 hover:bg-blue-100 dark:hover:bg-blue-900 text-foreground'
|
||||
? 'ring-2 ring-primary/20 bg-accent border-primary/20 hover:bg-accent/80 text-foreground'
|
||||
: ''
|
||||
} ${
|
||||
isSelectedDate && isTodayDate
|
||||
? 'bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 dark:from-blue-700 dark:to-blue-800 dark:hover:from-blue-800 dark:hover:to-blue-900 text-white'
|
||||
? 'bg-gradient-to-r from-primary to-primary/80 hover:from-primary/90 hover:to-primary/70 text-primary-foreground'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
@@ -501,8 +501,8 @@ export function EnhancedBookingCalendar() {
|
||||
<div
|
||||
className={`text-center p-4 rounded-lg ${
|
||||
isToday(selectedDate)
|
||||
? 'bg-gradient-to-r from-blue-500 to-indigo-600 text-white dark:from-blue-600 dark:to-indigo-700'
|
||||
: 'bg-blue-50 dark:bg-blue-950'
|
||||
? 'bg-gradient-to-r from-primary to-primary/80 text-primary-foreground'
|
||||
: 'bg-accent/50'
|
||||
}`}
|
||||
>
|
||||
<h3
|
||||
@@ -520,7 +520,7 @@ export function EnhancedBookingCalendar() {
|
||||
{isToday(selectedDate) && (
|
||||
<div className='flex items-center justify-center gap-2 mt-2'>
|
||||
<div className='w-2 h-2 bg-orange-300 dark:bg-orange-400 rounded-full animate-pulse' />
|
||||
<span className='text-sm font-medium text-blue-100 dark:text-blue-200'>Today</span>
|
||||
<span className='text-sm font-medium text-primary-foreground/90'>Today</span>
|
||||
<div className='w-2 h-2 bg-orange-300 dark:bg-orange-400 rounded-full animate-pulse' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -61,16 +61,8 @@ export function UserBookingManagement() {
|
||||
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);
|
||||
// API already filters to show only active future bookings (today onwards)
|
||||
setBookings(data.bookings || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching bookings:', error);
|
||||
@@ -282,7 +274,7 @@ export function UserBookingManagement() {
|
||||
<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
|
||||
Your Upcoming Bookings
|
||||
</CardTitle>
|
||||
<Button size='sm' variant='outline' onClick={fetchBookings}>
|
||||
<RefreshCw className='h-4 w-4' />
|
||||
@@ -390,16 +382,16 @@ export function UserBookingManagement() {
|
||||
</DialogHeader>
|
||||
<div className='space-y-4'>
|
||||
{selectedBooking && (
|
||||
<div className='bg-blue-50 p-4 rounded-lg space-y-2 dark:bg-blue-950'>
|
||||
<div className='flex items-center gap-2 text-sm'>
|
||||
<div className='bg-accent/50 p-4 rounded-lg space-y-2'>
|
||||
<div className='flex items-center gap-2 text-sm text-foreground'>
|
||||
<Calendar className='h-4 w-4' />
|
||||
{formatDate(selectedBooking.date)}
|
||||
</div>
|
||||
<div className='flex items-center gap-2 text-sm'>
|
||||
<div className='flex items-center gap-2 text-sm text-foreground'>
|
||||
<Clock className='h-4 w-4' />
|
||||
{selectedBooking.startTime} - {selectedBooking.endTime}
|
||||
</div>
|
||||
<div className='flex items-center gap-2 text-sm'>
|
||||
<div className='flex items-center gap-2 text-sm text-foreground'>
|
||||
<MapPin className='h-4 w-4' />
|
||||
{selectedBooking.court.name}
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { CalendarIcon, Clock, MapPin } from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
const timeSlots = [
|
||||
'09:00',
|
||||
@@ -31,6 +32,7 @@ export function BookingCalendar() {
|
||||
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
|
||||
const [selectedSlot, setSelectedSlot] = useState<string | null>(null);
|
||||
const [selectedCourt, setSelectedCourt] = useState<string | null>(null);
|
||||
const { toast } = useToast();
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString('en-IE', {
|
||||
@@ -66,13 +68,24 @@ export function BookingCalendar() {
|
||||
setSelectedSlot(null);
|
||||
setSelectedCourt(null);
|
||||
// Show success message
|
||||
alert('Booking created successfully!');
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Booking created successfully!',
|
||||
});
|
||||
} else {
|
||||
alert(result.error || 'Booking failed');
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: result.error || 'Booking failed',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Booking error:', error);
|
||||
alert('An error occurred while creating the booking');
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'An error occurred while creating the booking',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useToast } from '@/hooks/use-toast';
|
||||
import { NotificationBell, AnnouncementsModal } from '@/components/notifications/announcements';
|
||||
import { UserProfile } from '@/components/user/user-profile';
|
||||
import { ModeToggle } from '@/components/ui/mode-toggle';
|
||||
import type { AppConfig } from '@/lib/app-config';
|
||||
|
||||
interface DashboardHeaderProps {
|
||||
user: {
|
||||
@@ -28,6 +29,23 @@ export function DashboardHeader({ user }: DashboardHeaderProps) {
|
||||
const [showAnnouncements, setShowAnnouncements] = useState(false);
|
||||
const [showUserProfile, setShowUserProfile] = useState(false);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const [appConfig, setAppConfig] = useState<AppConfig | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAppConfig = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/config');
|
||||
if (response.ok) {
|
||||
const config = await response.json();
|
||||
setAppConfig(config);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching app config:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAppConfig();
|
||||
}, []);
|
||||
|
||||
// Fetch unread announcements count on component mount
|
||||
useEffect(() => {
|
||||
@@ -78,7 +96,7 @@ export function DashboardHeader({ user }: DashboardHeaderProps) {
|
||||
<div className='flex items-center space-x-4'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<Calendar className='h-6 w-6 text-primary' />
|
||||
<h1 className='text-xl font-bold text-foreground'>TT Booking</h1>
|
||||
<h1 className='text-xl font-bold text-foreground'>{appConfig?.clubName || 'TT Booking'}</h1>
|
||||
</div>
|
||||
{user.role === 'admin' && <Badge variant='secondary'>Admin</Badge>}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user