Files
tt-booking/components/admin/AdminCourtManagement.tsx
T

343 lines
9.0 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 { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Badge } from '@/components/ui/badge';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { toast } from '@/hooks/use-toast';
import { Plus, Edit, Trash2, MapPin, Settings, RefreshCw } from 'lucide-react';
interface Court {
id: string;
name: string;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
}
interface CourtFormData {
name: string;
isActive: boolean;
}
export function AdminCourtManagement() {
const [courts, setCourts] = useState<Court[]>([]);
const [loading, setLoading] = useState(true);
const [creating, setCreating] = useState(false);
const [editing, setEditing] = useState<string | null>(null);
const [deleting, setDeleting] = useState<string | null>(null);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingCourt, setEditingCourt] = useState<Court | null>(null);
const [formData, setFormData] = useState<CourtFormData>({
name: '',
isActive: true,
});
useEffect(() => {
fetchCourts();
}, []);
const fetchCourts = async () => {
try {
setLoading(true);
const response = await fetch('/api/admin/courts');
if (response.ok) {
const data = await response.json();
setCourts(data.courts || []);
} else {
toast({
title: 'Error',
description: 'Failed to fetch courts',
variant: 'destructive',
});
}
} catch (error) {
console.error('Error fetching courts:', error);
toast({
title: 'Error',
description: 'Failed to fetch courts',
variant: 'destructive',
});
} finally {
setLoading(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name.trim()) {
toast({
title: 'Error',
description: 'Court name is required',
variant: 'destructive',
});
return;
}
try {
if (editingCourt) {
setEditing(editingCourt.id);
const response = await fetch(`/api/admin/courts/${editingCourt.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
if (response.ok) {
toast({
title: 'Success',
description: 'Court updated successfully',
});
await fetchCourts();
resetForm();
} else {
const error = await response.json();
toast({
title: 'Error',
description: error.error || 'Failed to update court',
variant: 'destructive',
});
}
} else {
setCreating(true);
const response = await fetch('/api/admin/courts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
if (response.ok) {
toast({
title: 'Success',
description: 'Court created successfully',
});
await fetchCourts();
resetForm();
} else {
const error = await response.json();
toast({
title: 'Error',
description: error.error || 'Failed to create court',
variant: 'destructive',
});
}
}
} catch (error) {
console.error('Error saving court:', error);
toast({
title: 'Error',
description: 'Failed to save court',
variant: 'destructive',
});
} finally {
setCreating(false);
setEditing(null);
}
};
const handleEdit = (court: Court) => {
setEditingCourt(court);
setFormData({
name: court.name,
isActive: court.isActive,
});
setIsDialogOpen(true);
};
const handleDelete = async (courtId: string) => {
if (!confirm('Are you sure you want to delete this court? This action cannot be undone.')) {
return;
}
try {
setDeleting(courtId);
const response = await fetch(`/api/admin/courts/${courtId}`, {
method: 'DELETE',
});
if (response.ok) {
toast({
title: 'Success',
description: 'Court deleted successfully',
});
await fetchCourts();
} else {
const error = await response.json();
toast({
title: 'Error',
description: error.error || 'Failed to delete court',
variant: 'destructive',
});
}
} catch (error) {
console.error('Error deleting court:', error);
toast({
title: 'Error',
description: 'Failed to delete court',
variant: 'destructive',
});
} finally {
setDeleting(null);
}
};
const resetForm = () => {
setFormData({ name: '', isActive: true });
setEditingCourt(null);
setIsDialogOpen(false);
};
if (loading) {
return (
<Card>
<CardHeader>
<CardTitle>Court Management</CardTitle>
</CardHeader>
<CardContent>
<div className='space-y-4'>
{[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='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 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 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 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={() => handleDelete(court.id)}
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>
</Card>
);
}