feat: implement admin blocks management feature

- Added AdminBlocksManagement component for managing court blocks.
- Implemented functionality to create, edit, and delete blocks.
- Integrated fetching of courts and blocks from the API.
- Added validation for block creation and editing forms.
- Enhanced UI with responsive design for mobile and desktop views.
- Created database migration for court_blocks table and updated users table with theme_preference.
This commit is contained in:
mikicvi
2025-12-29 17:04:16 +00:00
parent 54240a2cfd
commit 40c56770a2
13 changed files with 2164 additions and 215 deletions
+170 -144
View File
@@ -19,9 +19,10 @@ import {
} from '@/components/ui/alert-dialog';
import { Badge } from '@/components/ui/badge';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { toast } from '@/hooks/use-toast';
import { Plus, Edit, Trash2, MapPin, Settings, RefreshCw } from 'lucide-react';
import { Plus, Edit, Trash2, MapPin, Settings, RefreshCw, Ban } from 'lucide-react';
import { AdminBlocksManagement } from './AdminBlocksManagement';
interface Court {
id: string;
@@ -219,7 +220,7 @@ export function AdminCourtManagement() {
return (
<Card>
<CardHeader>
<CardTitle>Court Management</CardTitle>
<CardTitle>Courts & Closures</CardTitle>
</CardHeader>
<CardContent>
<div className='space-y-4'>
@@ -236,151 +237,176 @@ export function AdminCourtManagement() {
}
return (
<Card>
<CardHeader className='flex flex-row items-center justify-between'>
<CardTitle className='flex items-center gap-2'>
<Settings className='h-5 w-5' />
Court Management
</CardTitle>
<div className='flex gap-2'>
<Button size='sm' variant='outline' onClick={fetchCourts}>
<RefreshCw className='h-4 w-4 mr-2' />
Refresh
</Button>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button size='sm' onClick={() => setEditingCourt(null)}>
<Plus className='h-4 w-4 mr-2' />
Add Court
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{editingCourt ? 'Edit Court' : 'Create New Court'}</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className='space-y-4'>
<div>
<Label htmlFor='name'>Court Name</Label>
<Input
id='name'
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder='e.g., Court 1, Main Court'
required
/>
</div>
<div className='space-y-6'>
<Tabs defaultValue='courts' className='w-full'>
<TabsList className='grid w-full grid-cols-2'>
<TabsTrigger value='courts' className='flex items-center gap-2'>
<MapPin className='h-4 w-4' />
Courts
</TabsTrigger>
<TabsTrigger value='closures' className='flex items-center gap-2'>
<Ban className='h-4 w-4' />
Closures
</TabsTrigger>
</TabsList>
<div className='flex items-center space-x-2'>
<Switch
id='isActive'
checked={formData.isActive}
onCheckedChange={(checked: boolean) =>
setFormData({ ...formData, isActive: checked })
}
/>
<Label htmlFor='isActive'>Active (available for booking)</Label>
</div>
<TabsContent value='courts'>
<Card>
<CardHeader className='flex flex-row items-center justify-between'>
<CardTitle className='flex items-center gap-2'>
<Settings className='h-5 w-5' />
Court Management
</CardTitle>
<div className='flex gap-2'>
<Button size='sm' variant='outline' onClick={fetchCourts}>
<RefreshCw className='h-4 w-4 mr-2' />
Refresh
</Button>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button size='sm' onClick={() => setEditingCourt(null)}>
<Plus className='h-4 w-4 mr-2' />
Add Court
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
{editingCourt ? 'Edit Court' : 'Create New Court'}
</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className='space-y-4'>
<div>
<Label htmlFor='name'>Court Name</Label>
<Input
id='name'
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder='e.g., Court 1, Main Court'
required
/>
</div>
<div className='flex justify-end space-x-2'>
<Button type='button' variant='outline' onClick={resetForm}>
Cancel
</Button>
<Button type='submit' disabled={creating || Boolean(editing)}>
{creating || editing ? (
<>
<RefreshCw className='h-4 w-4 mr-2 animate-spin' />
{editingCourt ? 'Updating...' : 'Creating...'}
</>
) : editingCourt ? (
'Update Court'
) : (
'Create Court'
)}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
{courts.length === 0 ? (
<div className='text-center py-8 text-gray-500'>
<MapPin className='h-12 w-12 mx-auto mb-4 text-gray-300' />
<p>No courts found. Create your first court to get started.</p>
</div>
) : (
<div className='space-y-4'>
{courts.map((court) => (
<div key={court.id} className='border rounded-lg p-4'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-3'>
<MapPin className='h-5 w-5 text-blue-600' />
<div>
<h3 className='font-medium'>{court.name}</h3>
<p className='text-sm text-gray-500'>
Created {new Date(court.createdAt).toLocaleDateString('en-IE')}
</p>
</div>
</div>
<div className='flex items-center space-x-2'>
<Switch
id='isActive'
checked={formData.isActive}
onCheckedChange={(checked: boolean) =>
setFormData({ ...formData, isActive: checked })
}
/>
<Label htmlFor='isActive'>Active (available for booking)</Label>
</div>
<div className='flex items-center gap-3'>
<Badge variant={court.isActive ? 'default' : 'secondary'}>
{court.isActive ? 'Active' : 'Inactive'}
</Badge>
<div className='flex gap-1'>
<Button
size='sm'
variant='outline'
onClick={() => handleEdit(court)}
disabled={editing === court.id}
>
<Edit className='h-4 w-4' />
</Button>
<Button
size='sm'
variant='outline'
onClick={() => openDeleteDialog(court)}
disabled={deleting === court.id}
className='text-red-600 hover:text-red-700'
>
{deleting === court.id ? (
<RefreshCw className='h-4 w-4 animate-spin' />
) : (
<Trash2 className='h-4 w-4' />
)}
</Button>
</div>
</div>
</div>
<div className='flex justify-end space-x-2'>
<Button type='button' variant='outline' onClick={resetForm}>
Cancel
</Button>
<Button type='submit' disabled={creating || Boolean(editing)}>
{creating || editing ? (
<>
<RefreshCw className='h-4 w-4 mr-2 animate-spin' />
{editingCourt ? 'Updating...' : 'Creating...'}
</>
) : editingCourt ? (
'Update Court'
) : (
'Create Court'
)}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</div>
))}
</div>
)}
</CardContent>
</CardHeader>
<CardContent>
{courts.length === 0 ? (
<div className='text-center py-8 text-gray-500'>
<MapPin className='h-12 w-12 mx-auto mb-4 text-gray-300' />
<p>No courts found. Create your first court to get started.</p>
</div>
) : (
<div className='space-y-4'>
{courts.map((court) => (
<div key={court.id} className='border rounded-lg p-4'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-3'>
<MapPin className='h-5 w-5 text-blue-600' />
<div>
<h3 className='font-medium'>{court.name}</h3>
<p className='text-sm text-gray-500'>
Created{' '}
{new Date(court.createdAt).toLocaleDateString('en-IE')}
</p>
</div>
</div>
{/* 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>
<div className='flex items-center gap-3'>
<Badge variant={court.isActive ? 'default' : 'secondary'}>
{court.isActive ? 'Active' : 'Inactive'}
</Badge>
<div className='flex gap-1'>
<Button
size='sm'
variant='outline'
onClick={() => handleEdit(court)}
disabled={editing === court.id}
>
<Edit className='h-4 w-4' />
</Button>
<Button
size='sm'
variant='outline'
onClick={() => openDeleteDialog(court)}
disabled={deleting === court.id}
className='text-red-600 hover:text-red-700'
>
{deleting === court.id ? (
<RefreshCw className='h-4 w-4 animate-spin' />
) : (
<Trash2 className='h-4 w-4' />
)}
</Button>
</div>
</div>
</div>
</div>
))}
</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>
</TabsContent>
<TabsContent value='closures'>
<AdminBlocksManagement />
</TabsContent>
</Tabs>
</div>
);
}