theming, date, time localisation, additional features, seeding initial cleanup

This commit is contained in:
mikicvi
2025-09-26 21:12:59 +01:00
parent b89d91ade2
commit 22c462c61c
43 changed files with 2647 additions and 550 deletions
@@ -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>
)}
+1 -1
View File
@@ -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>
+81 -73
View File
@@ -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>
+1 -1
View File
@@ -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>
+7 -7
View File
@@ -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