theming, date, time localisation, additional features, seeding initial cleanup
This commit is contained in:
@@ -236,13 +236,13 @@ export function AdminAnnouncementManagement() {
|
||||
const getPriorityColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'high':
|
||||
return 'text-red-600 bg-red-50';
|
||||
return 'text-destructive bg-destructive/10 dark:bg-destructive/20';
|
||||
case 'medium':
|
||||
return 'text-yellow-600 bg-yellow-50';
|
||||
return 'text-amber-600 bg-amber-50 dark:text-amber-400 dark:bg-amber-950/50';
|
||||
case 'low':
|
||||
return 'text-green-600 bg-green-50';
|
||||
return 'text-green-600 bg-green-50 dark:text-green-400 dark:bg-green-950/50';
|
||||
default:
|
||||
return 'text-gray-600 bg-gray-50';
|
||||
return 'text-muted-foreground bg-muted';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -387,8 +387,8 @@ export function AdminAnnouncementManagement() {
|
||||
<TableRow key={announcement.id}>
|
||||
<TableCell>
|
||||
<div>
|
||||
<div className='font-medium'>{announcement.title}</div>
|
||||
<div className='text-sm text-gray-500 truncate max-w-xs'>
|
||||
<div className='font-medium text-foreground'>{announcement.title}</div>
|
||||
<div className='text-sm text-muted-foreground truncate max-w-xs'>
|
||||
{announcement.content}
|
||||
</div>
|
||||
</div>
|
||||
@@ -420,16 +420,16 @@ export function AdminAnnouncementManagement() {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Calendar className='h-4 w-4 text-gray-500' />
|
||||
<Calendar className='h-4 w-4 text-muted-foreground' />
|
||||
{announcement.expiresAt
|
||||
? new Date(announcement.expiresAt).toLocaleDateString()
|
||||
? new Date(announcement.expiresAt).toLocaleDateString('en-IE')
|
||||
: 'Never'}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Calendar className='h-4 w-4 text-gray-500' />
|
||||
{new Date(announcement.createdAt).toLocaleDateString()}
|
||||
<Calendar className='h-4 w-4 text-muted-foreground' />
|
||||
{new Date(announcement.createdAt).toLocaleDateString('en-IE')}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
@@ -445,7 +445,7 @@ export function AdminAnnouncementManagement() {
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => handleDeleteAnnouncement(announcement.id)}
|
||||
className='text-red-600 hover:text-red-700'
|
||||
className='text-destructive hover:text-destructive/90'
|
||||
>
|
||||
<Trash2 className='h-4 w-4' />
|
||||
</Button>
|
||||
@@ -456,7 +456,7 @@ export function AdminAnnouncementManagement() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
{announcements.length === 0 && (
|
||||
<div className='text-center py-8 text-gray-500'>
|
||||
<div className='text-center py-8 text-muted-foreground'>
|
||||
No announcements found. Create your first announcement!
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -297,7 +297,7 @@ export function AdminCourtManagement() {
|
||||
<div>
|
||||
<h3 className='font-medium'>{court.name}</h3>
|
||||
<p className='text-sm text-gray-500'>
|
||||
Created {new Date(court.createdAt).toLocaleDateString()}
|
||||
Created {new Date(court.createdAt).toLocaleDateString('en-IE')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,8 @@ interface SettingsData {
|
||||
booking_end_time: string;
|
||||
allow_weekend_bookings: string;
|
||||
max_bookings_per_user_per_hour_per_day: string;
|
||||
allow_booking_modifications: string;
|
||||
booking_modification_hours_before: string;
|
||||
}
|
||||
|
||||
export function AdminSettingsManagement() {
|
||||
@@ -35,6 +37,8 @@ export function AdminSettingsManagement() {
|
||||
booking_end_time: '22:00',
|
||||
allow_weekend_bookings: 'true',
|
||||
max_bookings_per_user_per_hour_per_day: '1',
|
||||
allow_booking_modifications: 'true',
|
||||
booking_modification_hours_before: '2',
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -57,6 +61,8 @@ export function AdminSettingsManagement() {
|
||||
booking_end_time: '22:00',
|
||||
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
|
||||
@@ -266,6 +272,43 @@ export function AdminSettingsManagement() {
|
||||
<p className='text-sm text-gray-500'>Maximum bookings per user per hour on the same day</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>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* Weekend Bookings */}
|
||||
<div className='space-y-2'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
@@ -307,6 +350,12 @@ export function AdminSettingsManagement() {
|
||||
<strong>Booking Limit:</strong> {settings.max_bookings_per_user_per_hour_per_day} per
|
||||
hour
|
||||
</p>
|
||||
<p>
|
||||
<strong>Booking Modifications:</strong>{' '}
|
||||
{settings.allow_booking_modifications === 'true' ? 'Enabled' : 'Disabled'}
|
||||
{settings.allow_booking_modifications === 'true' &&
|
||||
` (${settings.booking_modification_hours_before}h before)`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Plus, Edit, Trash2, Clock } from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { getWeekDays } from '@/lib/utils';
|
||||
|
||||
interface TimeSlot {
|
||||
id: string;
|
||||
@@ -21,7 +22,9 @@ interface TimeSlot {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
const DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
// Use Irish week order (Monday first)
|
||||
const DAYS = getWeekDays().map((day) => day.label);
|
||||
const IRISH_DAY_ORDER = [1, 2, 3, 4, 5, 6, 0]; // Monday-Sunday in JS getDay() values
|
||||
|
||||
export function AdminTimeSlotManagement() {
|
||||
const [timeSlots, setTimeSlots] = useState<TimeSlot[]>([]);
|
||||
@@ -29,7 +32,7 @@ export function AdminTimeSlotManagement() {
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const [editingSlot, setEditingSlot] = useState<TimeSlot | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
dayOfWeek: 0,
|
||||
dayOfWeek: 1, // Default to Monday (Irish standard)
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
isActive: true,
|
||||
@@ -208,7 +211,7 @@ export function AdminTimeSlotManagement() {
|
||||
const resetForm = () => {
|
||||
setEditingSlot(null);
|
||||
setFormData({
|
||||
dayOfWeek: 0,
|
||||
dayOfWeek: 1, // Default to Monday (Irish standard)
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
isActive: true,
|
||||
@@ -255,9 +258,9 @@ export function AdminTimeSlotManagement() {
|
||||
<SelectValue placeholder='Select day' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DAYS.map((day, index) => (
|
||||
<SelectItem key={index} value={index.toString()}>
|
||||
{day}
|
||||
{IRISH_DAY_ORDER.map((jsDayOfWeek, displayIndex) => (
|
||||
<SelectItem key={jsDayOfWeek} value={jsDayOfWeek.toString()}>
|
||||
{DAYS[displayIndex]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -309,75 +312,80 @@ export function AdminTimeSlotManagement() {
|
||||
<div className='text-center py-4'>Loading time slots...</div>
|
||||
) : (
|
||||
<div className='space-y-6'>
|
||||
{DAYS.map((day, dayIndex) => (
|
||||
<div key={dayIndex} className='space-y-2'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<h3 className='font-semibold text-lg'>{day}</h3>
|
||||
{groupedTimeSlots[dayIndex]?.length > 0 && (
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outline'
|
||||
onClick={() => handleWipeDay(dayIndex)}
|
||||
className='text-red-600 hover:text-red-700 hover:bg-red-50'
|
||||
disabled={loading}
|
||||
>
|
||||
<Trash2 className='h-4 w-4 mr-1' />
|
||||
Wipe All
|
||||
</Button>
|
||||
{IRISH_DAY_ORDER.map((jsDayOfWeek, displayIndex) => {
|
||||
const dayName = DAYS[displayIndex];
|
||||
return (
|
||||
<div key={jsDayOfWeek} className='space-y-2'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<h3 className='font-semibold text-lg'>{dayName}</h3>
|
||||
{groupedTimeSlots[jsDayOfWeek]?.length > 0 && (
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outline'
|
||||
onClick={() => handleWipeDay(jsDayOfWeek)}
|
||||
className='text-destructive hover:text-destructive/80 hover:bg-destructive/10'
|
||||
disabled={loading}
|
||||
>
|
||||
<Trash2 className='h-4 w-4 mr-1' />
|
||||
Wipe All
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{groupedTimeSlots[jsDayOfWeek]?.length > 0 ? (
|
||||
<div className='grid gap-2'>
|
||||
{groupedTimeSlots[jsDayOfWeek]
|
||||
.sort((a, b) => a.startTime.localeCompare(b.startTime))
|
||||
.map((slot) => (
|
||||
<div
|
||||
key={slot.id}
|
||||
className={`flex items-center justify-between p-3 border rounded-lg ${
|
||||
slot.isActive
|
||||
? 'bg-green-50 border-green-200 dark:bg-green-950 dark:border-green-800'
|
||||
: 'bg-muted border-border'
|
||||
}`}
|
||||
>
|
||||
<div className='flex items-center space-x-3'>
|
||||
<div className='font-medium'>
|
||||
{slot.startTime} - {slot.endTime}
|
||||
</div>
|
||||
<div
|
||||
className={`px-2 py-1 rounded-full text-xs ${
|
||||
slot.isActive
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{slot.isActive ? 'Active' : 'Inactive'}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex space-x-2'>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outline'
|
||||
onClick={() => handleEdit(slot)}
|
||||
>
|
||||
<Edit className='h-4 w-4' />
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outline'
|
||||
onClick={() => handleDelete(slot.id)}
|
||||
className='text-destructive hover:text-destructive/80'
|
||||
>
|
||||
<Trash2 className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className='text-muted-foreground italic'>
|
||||
No time slots configured for {dayName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{groupedTimeSlots[dayIndex]?.length > 0 ? (
|
||||
<div className='grid gap-2'>
|
||||
{groupedTimeSlots[dayIndex]
|
||||
.sort((a, b) => a.startTime.localeCompare(b.startTime))
|
||||
.map((slot) => (
|
||||
<div
|
||||
key={slot.id}
|
||||
className={`flex items-center justify-between p-3 border rounded-lg ${
|
||||
slot.isActive
|
||||
? 'bg-green-50 border-green-200'
|
||||
: 'bg-gray-50 border-gray-200'
|
||||
}`}
|
||||
>
|
||||
<div className='flex items-center space-x-3'>
|
||||
<div className='font-medium'>
|
||||
{slot.startTime} - {slot.endTime}
|
||||
</div>
|
||||
<div
|
||||
className={`px-2 py-1 rounded-full text-xs ${
|
||||
slot.isActive
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-gray-100 text-gray-800'
|
||||
}`}
|
||||
>
|
||||
{slot.isActive ? 'Active' : 'Inactive'}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex space-x-2'>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outline'
|
||||
onClick={() => handleEdit(slot)}
|
||||
>
|
||||
<Edit className='h-4 w-4' />
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outline'
|
||||
onClick={() => handleDelete(slot.id)}
|
||||
className='text-red-600 hover:text-red-700'
|
||||
>
|
||||
<Trash2 className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className='text-gray-500 italic'>No time slots configured for {day}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -395,7 +395,7 @@ export function AdminUserManagement() {
|
||||
<TableCell>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Calendar className='h-4 w-4 text-gray-500' />
|
||||
{new Date(user.createdAt).toLocaleDateString()}
|
||||
{new Date(user.createdAt).toLocaleDateString('en-IE')}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { AdminRecentBookings } from './AdminRecentBookings';
|
||||
import { AdminCourtManagement } from './AdminCourtManagement';
|
||||
import { AdminSettingsManagement } from './AdminSettingsManagement';
|
||||
import { AdminTimeSlotManagement } from './AdminTimeSlotManagement';
|
||||
import { ModeToggle } from '@/components/ui/mode-toggle';
|
||||
|
||||
interface AdminStats {
|
||||
totalUsers: number;
|
||||
@@ -74,20 +75,19 @@ export function AdminDashboard() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='min-h-screen bg-gray-50'>
|
||||
<div className='min-h-screen bg-background'>
|
||||
{/* Header */}
|
||||
<header className='bg-white border-b border-gray-200'>
|
||||
<header className='bg-card border-b border-border'>
|
||||
<div className='container mx-auto px-4'>
|
||||
<div className='flex items-center justify-between h-16'>
|
||||
<div className='flex items-center space-x-4'>
|
||||
<Shield className='h-6 w-6 text-blue-600' />
|
||||
<h1 className='text-xl font-semibold text-gray-900'>Admin Dashboard</h1>
|
||||
<Shield className='h-6 w-6 text-blue-600 dark:text-blue-400' />
|
||||
<h1 className='text-xl font-semibold text-foreground'>Admin Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center space-x-4'>
|
||||
<Badge variant='secondary' className='bg-blue-100 text-blue-800'>
|
||||
Administrator
|
||||
</Badge>
|
||||
<Badge variant='secondary'>Administrator</Badge>
|
||||
<ModeToggle />
|
||||
<Button variant='ghost' size='sm' onClick={handleLogout}>
|
||||
<LogOut className='h-4 w-4' />
|
||||
Logout
|
||||
|
||||
Reference in New Issue
Block a user