initial version of the app
This commit is contained in:
@@ -0,0 +1,544 @@
|
||||
'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 { 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 { 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';
|
||||
import { Megaphone, Plus, Edit, Trash2, Calendar, AlertCircle, CheckCircle, Clock } from 'lucide-react';
|
||||
|
||||
interface Announcement {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
priority: 'low' | 'medium' | 'high';
|
||||
isActive: boolean;
|
||||
expiresAt?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface AnnouncementFormData {
|
||||
title: string;
|
||||
content: string;
|
||||
priority: 'low' | 'medium' | 'high';
|
||||
isActive: boolean;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
export function AdminAnnouncementManagement() {
|
||||
const [announcements, setAnnouncements] = useState<Announcement[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||||
const [editingAnnouncement, setEditingAnnouncement] = useState<Announcement | null>(null);
|
||||
const [formData, setFormData] = useState<AnnouncementFormData>({
|
||||
title: '',
|
||||
content: '',
|
||||
priority: 'medium',
|
||||
isActive: true,
|
||||
expiresAt: '',
|
||||
});
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
fetchAnnouncements();
|
||||
}, []);
|
||||
|
||||
const fetchAnnouncements = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch('/api/admin/announcements');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setAnnouncements(data.announcements);
|
||||
} else {
|
||||
throw new Error('Failed to fetch announcements');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching announcements:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to fetch announcements',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateAnnouncement = async () => {
|
||||
try {
|
||||
if (!formData.title || !formData.content) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Please fill in title and content',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const submitData = {
|
||||
...formData,
|
||||
expiresAt: formData.expiresAt || null,
|
||||
};
|
||||
|
||||
const response = await fetch('/api/admin/announcements', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(submitData),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Announcement created successfully',
|
||||
});
|
||||
setIsCreateDialogOpen(false);
|
||||
resetForm();
|
||||
fetchAnnouncements();
|
||||
} else {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: data.error || 'Failed to create announcement',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating announcement:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to create announcement',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditAnnouncement = async () => {
|
||||
try {
|
||||
if (!editingAnnouncement || !formData.title || !formData.content) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Please fill in title and content',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const submitData = {
|
||||
...formData,
|
||||
expiresAt: formData.expiresAt || null,
|
||||
};
|
||||
|
||||
const response = await fetch(`/api/admin/announcements/${editingAnnouncement.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(submitData),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Announcement updated successfully',
|
||||
});
|
||||
setIsEditDialogOpen(false);
|
||||
setEditingAnnouncement(null);
|
||||
resetForm();
|
||||
fetchAnnouncements();
|
||||
} else {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: data.error || 'Failed to update announcement',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating announcement:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to update announcement',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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',
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Announcement deleted successfully',
|
||||
});
|
||||
fetchAnnouncements();
|
||||
} else {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: data.error || 'Failed to delete announcement',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting announcement:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to delete announcement',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const openEditDialog = (announcement: Announcement) => {
|
||||
setEditingAnnouncement(announcement);
|
||||
setFormData({
|
||||
title: announcement.title,
|
||||
content: announcement.content,
|
||||
priority: announcement.priority,
|
||||
isActive: announcement.isActive,
|
||||
expiresAt: announcement.expiresAt ? announcement.expiresAt.split('T')[0] : '',
|
||||
});
|
||||
setIsEditDialogOpen(true);
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
title: '',
|
||||
content: '',
|
||||
priority: 'medium',
|
||||
isActive: true,
|
||||
expiresAt: '',
|
||||
});
|
||||
};
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'high':
|
||||
return 'text-red-600 bg-red-50';
|
||||
case 'medium':
|
||||
return 'text-yellow-600 bg-yellow-50';
|
||||
case 'low':
|
||||
return 'text-green-600 bg-green-50';
|
||||
default:
|
||||
return 'text-gray-600 bg-gray-50';
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityIcon = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'high':
|
||||
return <AlertCircle className='h-4 w-4' />;
|
||||
case 'medium':
|
||||
return <Clock className='h-4 w-4' />;
|
||||
case 'low':
|
||||
return <CheckCircle className='h-4 w-4' />;
|
||||
default:
|
||||
return <CheckCircle className='h-4 w-4' />;
|
||||
}
|
||||
};
|
||||
|
||||
const isExpired = (expiresAt?: string) => {
|
||||
if (!expiresAt) return false;
|
||||
return new Date(expiresAt) < new Date();
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className='flex justify-center items-center py-8'>
|
||||
<div className='animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900'></div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='space-y-6'>
|
||||
{/* Header */}
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Megaphone className='h-6 w-6' />
|
||||
<h2 className='text-2xl font-bold'>Announcement Management</h2>
|
||||
</div>
|
||||
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button onClick={() => resetForm()}>
|
||||
<Plus className='h-4 w-4 mr-2' />
|
||||
Create Announcement
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className='max-w-2xl'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Announcement</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className='space-y-4'>
|
||||
<div>
|
||||
<Label htmlFor='title'>Title</Label>
|
||||
<Input
|
||||
id='title'
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||
placeholder='Announcement title'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor='content'>Content</Label>
|
||||
<Textarea
|
||||
id='content'
|
||||
value={formData.content}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setFormData({ ...formData, content: e.target.value })
|
||||
}
|
||||
placeholder='Announcement content'
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<div>
|
||||
<Label htmlFor='priority'>Priority</Label>
|
||||
<Select
|
||||
value={formData.priority}
|
||||
onValueChange={(value: 'low' | 'medium' | 'high') =>
|
||||
setFormData({ ...formData, priority: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='Select priority' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='low'>Low</SelectItem>
|
||||
<SelectItem value='medium'>Medium</SelectItem>
|
||||
<SelectItem value='high'>High</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor='expiresAt'>Expires On (Optional)</Label>
|
||||
<Input
|
||||
id='expiresAt'
|
||||
type='date'
|
||||
value={formData.expiresAt}
|
||||
onChange={(e) => setFormData({ ...formData, expiresAt: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<input
|
||||
type='checkbox'
|
||||
id='isActive'
|
||||
checked={formData.isActive}
|
||||
onChange={(e) => setFormData({ ...formData, isActive: e.target.checked })}
|
||||
className='rounded'
|
||||
/>
|
||||
<Label htmlFor='isActive'>Active (visible to users)</Label>
|
||||
</div>
|
||||
<div className='flex gap-2 justify-end'>
|
||||
<Button variant='outline' onClick={() => setIsCreateDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleCreateAnnouncement}>Create Announcement</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Announcements Table */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>All Announcements ({announcements.length})</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Title</TableHead>
|
||||
<TableHead>Priority</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Expires</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{announcements.map((announcement) => (
|
||||
<TableRow key={announcement.id}>
|
||||
<TableCell>
|
||||
<div>
|
||||
<div className='font-medium'>{announcement.title}</div>
|
||||
<div className='text-sm text-gray-500 truncate max-w-xs'>
|
||||
{announcement.content}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={getPriorityColor(announcement.priority)}>
|
||||
<div className='flex items-center gap-1'>
|
||||
{getPriorityIcon(announcement.priority)}
|
||||
{announcement.priority}
|
||||
</div>
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={
|
||||
isExpired(announcement.expiresAt)
|
||||
? 'destructive'
|
||||
: announcement.isActive
|
||||
? 'default'
|
||||
: 'secondary'
|
||||
}
|
||||
>
|
||||
{isExpired(announcement.expiresAt)
|
||||
? 'Expired'
|
||||
: announcement.isActive
|
||||
? 'Active'
|
||||
: 'Inactive'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Calendar className='h-4 w-4 text-gray-500' />
|
||||
{announcement.expiresAt
|
||||
? new Date(announcement.expiresAt).toLocaleDateString()
|
||||
: '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()}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className='flex gap-2'>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => openEditDialog(announcement)}
|
||||
>
|
||||
<Edit className='h-4 w-4' />
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => handleDeleteAnnouncement(announcement.id)}
|
||||
className='text-red-600 hover:text-red-700'
|
||||
>
|
||||
<Trash2 className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{announcements.length === 0 && (
|
||||
<div className='text-center py-8 text-gray-500'>
|
||||
No announcements found. Create your first announcement!
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Edit Announcement Dialog */}
|
||||
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
|
||||
<DialogContent className='max-w-2xl'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Announcement</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className='space-y-4'>
|
||||
<div>
|
||||
<Label htmlFor='edit-title'>Title</Label>
|
||||
<Input
|
||||
id='edit-title'
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||
placeholder='Announcement title'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor='edit-content'>Content</Label>
|
||||
<Textarea
|
||||
id='edit-content'
|
||||
value={formData.content}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setFormData({ ...formData, content: e.target.value })
|
||||
}
|
||||
placeholder='Announcement content'
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<div>
|
||||
<Label htmlFor='edit-priority'>Priority</Label>
|
||||
<Select
|
||||
value={formData.priority}
|
||||
onValueChange={(value: 'low' | 'medium' | 'high') =>
|
||||
setFormData({ ...formData, priority: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='Select priority' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='low'>Low</SelectItem>
|
||||
<SelectItem value='medium'>Medium</SelectItem>
|
||||
<SelectItem value='high'>High</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor='edit-expiresAt'>Expires On (Optional)</Label>
|
||||
<Input
|
||||
id='edit-expiresAt'
|
||||
type='date'
|
||||
value={formData.expiresAt}
|
||||
onChange={(e) => setFormData({ ...formData, expiresAt: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<input
|
||||
type='checkbox'
|
||||
id='edit-isActive'
|
||||
checked={formData.isActive}
|
||||
onChange={(e) => setFormData({ ...formData, isActive: e.target.checked })}
|
||||
className='rounded'
|
||||
/>
|
||||
<Label htmlFor='edit-isActive'>Active (visible to users)</Label>
|
||||
</div>
|
||||
<div className='flex gap-2 justify-end'>
|
||||
<Button variant='outline' onClick={() => setIsEditDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleEditAnnouncement}>Update Announcement</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
'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()}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { RefreshCw, User, Clock, Globe } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
interface ActivityLog {
|
||||
id: string;
|
||||
action: string;
|
||||
entityType: string;
|
||||
entityId?: string;
|
||||
details?: string;
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
createdAt: Date;
|
||||
user?: {
|
||||
id: string;
|
||||
name: string;
|
||||
surname: string;
|
||||
email: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export function AdminLogs() {
|
||||
const [logs, setLogs] = useState<ActivityLog[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchLogs();
|
||||
}, []);
|
||||
|
||||
const fetchLogs = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch('/api/admin/logs?limit=50');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setLogs(data.logs || []);
|
||||
} else {
|
||||
console.error('Failed to fetch logs');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching logs:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getActionBadgeColor = (action: string) => {
|
||||
switch (action.toLowerCase()) {
|
||||
case 'create':
|
||||
case 'created':
|
||||
return 'bg-green-100 text-green-800';
|
||||
case 'update':
|
||||
case 'updated':
|
||||
return 'bg-blue-100 text-blue-800';
|
||||
case 'delete':
|
||||
case 'deleted':
|
||||
return 'bg-red-100 text-red-800';
|
||||
case 'login':
|
||||
return 'bg-purple-100 text-purple-800';
|
||||
case 'logout':
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
const formatUserAgent = (userAgent?: string) => {
|
||||
if (!userAgent) return 'Unknown';
|
||||
if (userAgent.includes('Chrome')) return 'Chrome';
|
||||
if (userAgent.includes('Firefox')) return 'Firefox';
|
||||
if (userAgent.includes('Safari')) return 'Safari';
|
||||
if (userAgent.includes('Edge')) return 'Edge';
|
||||
return 'Unknown Browser';
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className='flex flex-row items-center justify-between'>
|
||||
<CardTitle>Activity Logs</CardTitle>
|
||||
<Button size='sm' disabled>
|
||||
<RefreshCw className='h-4 w-4 animate-spin mr-2' />
|
||||
Loading...
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='space-y-4'>
|
||||
{[1, 2, 3, 4, 5].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>Activity Logs</CardTitle>
|
||||
<Button size='sm' onClick={fetchLogs}>
|
||||
<RefreshCw className='h-4 w-4 mr-2' />
|
||||
Refresh
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{logs.length === 0 ? (
|
||||
<div className='text-center py-8 text-gray-500'>No activity logs found.</div>
|
||||
) : (
|
||||
<div className='space-y-4 max-h-96 overflow-y-auto'>
|
||||
{logs.map((log) => (
|
||||
<div key={log.id} className='border rounded-lg p-4 space-y-3'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<Badge className={getActionBadgeColor(log.action)}>{log.action}</Badge>
|
||||
<span className='text-sm font-medium'>
|
||||
{log.entityType}
|
||||
{log.entityId && (
|
||||
<span className='text-gray-500 ml-1'>
|
||||
({log.entityId.substring(0, 8)}...)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-1 text-xs text-gray-500'>
|
||||
<Clock className='h-3 w-3' />
|
||||
{format(new Date(log.createdAt), 'MMM dd, HH:mm')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between text-sm'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<User className='h-4 w-4 text-gray-400' />
|
||||
{log.user ? (
|
||||
<span>
|
||||
{log.user.name} {log.user.surname} ({log.user.email})
|
||||
</span>
|
||||
) : (
|
||||
<span className='text-gray-500'>System/Anonymous</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{log.ipAddress && (
|
||||
<div className='flex items-center gap-2 text-xs text-gray-500'>
|
||||
<Globe className='h-3 w-3' />
|
||||
<span>{log.ipAddress}</span>
|
||||
<span>•</span>
|
||||
<span>{formatUserAgent(log.userAgent)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{log.details && (
|
||||
<div className='text-xs text-gray-600 bg-gray-50 rounded p-2'>
|
||||
<pre className='whitespace-pre-wrap break-words'>
|
||||
{JSON.stringify(JSON.parse(log.details), null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { RefreshCw, Calendar, Clock, MapPin } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
interface Booking {
|
||||
id: string;
|
||||
date: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
status: string;
|
||||
notes?: string;
|
||||
createdAt: Date;
|
||||
court: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
user: {
|
||||
id: string;
|
||||
name: string;
|
||||
surname: string;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function AdminRecentBookings() {
|
||||
const [bookings, setBookings] = useState<Booking[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchBookings();
|
||||
}, []);
|
||||
|
||||
const fetchBookings = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch('/api/admin/recent-bookings?limit=5');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setBookings(data.bookings || []);
|
||||
} else {
|
||||
console.error('Failed to fetch bookings');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching bookings:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
return format(date, 'MMM dd');
|
||||
} catch {
|
||||
return dateStr;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadgeColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return 'bg-green-100 text-green-800';
|
||||
case 'cancelled':
|
||||
return 'bg-red-100 text-red-800';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className='flex flex-row items-center justify-between'>
|
||||
<CardTitle>Recent Bookings</CardTitle>
|
||||
<Button size='sm' disabled>
|
||||
<RefreshCw className='h-4 w-4 animate-spin mr-2' />
|
||||
Loading...
|
||||
</Button>
|
||||
</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>Recent Bookings</CardTitle>
|
||||
<Button size='sm' onClick={fetchBookings}>
|
||||
<RefreshCw className='h-4 w-4 mr-2' />
|
||||
Refresh
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{bookings.length === 0 ? (
|
||||
<div className='text-center py-6 text-gray-500'>No recent bookings found.</div>
|
||||
) : (
|
||||
<div className='space-y-4'>
|
||||
{bookings.map((booking) => (
|
||||
<div key={booking.id} className='border rounded-lg p-4 space-y-3'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='space-y-1'>
|
||||
<p className='font-medium'>
|
||||
{booking.user.name} {booking.user.surname}
|
||||
</p>
|
||||
<p className='text-sm text-gray-500'>{booking.user.email}</p>
|
||||
</div>
|
||||
<Badge className={getStatusBadgeColor(booking.status)}>{booking.status}</Badge>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-4 text-sm text-gray-600'>
|
||||
<div className='flex items-center gap-1'>
|
||||
<MapPin className='h-4 w-4' />
|
||||
<span>{booking.court.name}</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
<Calendar className='h-4 w-4' />
|
||||
<span>{formatDate(booking.date)}</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
<Clock className='h-4 w-4' />
|
||||
<span>
|
||||
{booking.startTime} - {booking.endTime}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{booking.notes && (
|
||||
<p className='text-sm text-gray-600 bg-gray-50 rounded p-2'>{booking.notes}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
'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 { Label } from '@/components/ui/label';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { Settings, Save, RefreshCw } from 'lucide-react';
|
||||
|
||||
interface Setting {
|
||||
id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
interface SettingsData {
|
||||
booking_window_days: string;
|
||||
max_booking_duration_hours: string;
|
||||
min_booking_duration_minutes: string;
|
||||
booking_start_time: string;
|
||||
booking_end_time: string;
|
||||
allow_weekend_bookings: string;
|
||||
}
|
||||
|
||||
export function AdminSettingsManagement() {
|
||||
const [settings, setSettings] = useState<SettingsData>({
|
||||
booking_window_days: '7',
|
||||
max_booking_duration_hours: '2',
|
||||
min_booking_duration_minutes: '30',
|
||||
booking_start_time: '08:00',
|
||||
booking_end_time: '22:00',
|
||||
allow_weekend_bookings: 'true',
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
|
||||
const fetchSettings = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch('/api/admin/settings');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const settingsMap: SettingsData = {
|
||||
booking_window_days: '7',
|
||||
max_booking_duration_hours: '2',
|
||||
min_booking_duration_minutes: '30',
|
||||
booking_start_time: '08:00',
|
||||
booking_end_time: '22:00',
|
||||
allow_weekend_bookings: 'true',
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
|
||||
setSettings(settingsMap);
|
||||
} else {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to fetch settings',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching settings:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to fetch settings',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setSaving(true);
|
||||
|
||||
// Convert settings object to array format
|
||||
const settingsArray = Object.entries(settings).map(([key, value]) => ({
|
||||
key,
|
||||
value,
|
||||
}));
|
||||
|
||||
const response = await fetch('/api/admin/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ settings: settingsArray }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Settings updated successfully',
|
||||
});
|
||||
await fetchSettings();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error.error || 'Failed to update settings',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating settings:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to update settings',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateSetting = (key: keyof SettingsData, value: string) => {
|
||||
setSettings((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className='flex items-center gap-2'>
|
||||
<Settings className='h-5 w-5' />
|
||||
System Settings
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='space-y-4'>
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div key={i} className='animate-pulse'>
|
||||
<div className='h-4 bg-gray-200 rounded w-1/4 mb-2'></div>
|
||||
<div className='h-10 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' />
|
||||
System Settings
|
||||
</CardTitle>
|
||||
<div className='flex gap-2'>
|
||||
<Button size='sm' variant='outline' onClick={fetchSettings}>
|
||||
<RefreshCw className='h-4 w-4 mr-2' />
|
||||
Refresh
|
||||
</Button>
|
||||
<Button size='sm' onClick={handleSave} disabled={saving}>
|
||||
{saving ? (
|
||||
<>
|
||||
<RefreshCw className='h-4 w-4 mr-2 animate-spin' />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className='h-4 w-4 mr-2' />
|
||||
Save Changes
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</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>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* 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>
|
||||
</div>
|
||||
|
||||
<div className='border-t pt-6'>
|
||||
<h3 className='text-lg font-medium mb-4'>Current Configuration Summary</h3>
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 gap-4 text-sm'>
|
||||
<div className='space-y-2'>
|
||||
<p>
|
||||
<strong>Booking Window:</strong> {settings.booking_window_days} days
|
||||
</p>
|
||||
<p>
|
||||
<strong>Session Duration:</strong> {settings.min_booking_duration_minutes}min -{' '}
|
||||
{settings.max_booking_duration_hours}hrs
|
||||
</p>
|
||||
<p>
|
||||
<strong>Operating Hours:</strong> {settings.booking_start_time} -{' '}
|
||||
{settings.booking_end_time}
|
||||
</p>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<p>
|
||||
<strong>Weekend Bookings:</strong>{' '}
|
||||
{settings.allow_weekend_bookings === 'true' ? 'Enabled' : 'Disabled'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
'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 { Label } from '@/components/ui/label';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/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';
|
||||
import { UserPlus, Edit, Trash2, Search, Users, Mail, Calendar } from 'lucide-react';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
surname: string;
|
||||
email: string;
|
||||
role: 'user' | 'admin';
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface UserFormData {
|
||||
name: string;
|
||||
surname: string;
|
||||
email: string;
|
||||
role: 'user' | 'admin';
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export function AdminUserManagement() {
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||||
const [editingUser, setEditingUser] = useState<User | null>(null);
|
||||
const [formData, setFormData] = useState<UserFormData>({
|
||||
name: '',
|
||||
surname: '',
|
||||
email: '',
|
||||
role: 'user',
|
||||
password: '',
|
||||
});
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, []);
|
||||
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch('/api/admin/users');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setUsers(data.users);
|
||||
} else {
|
||||
throw new Error('Failed to fetch users');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching users:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to fetch users',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateUser = async () => {
|
||||
try {
|
||||
if (!formData.name || !formData.surname || !formData.email || !formData.password) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Please fill in all required fields',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/admin/users', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'User created successfully',
|
||||
});
|
||||
setIsCreateDialogOpen(false);
|
||||
resetForm();
|
||||
fetchUsers();
|
||||
} else {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: data.error || 'Failed to create user',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating user:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to create user',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditUser = async () => {
|
||||
try {
|
||||
if (!editingUser || !formData.name || !formData.surname || !formData.email) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Please fill in all required fields',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData = { ...formData };
|
||||
if (!updateData.password) {
|
||||
delete updateData.password; // Don't update password if not provided
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/admin/users/${editingUser.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(updateData),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'User updated successfully',
|
||||
});
|
||||
setIsEditDialogOpen(false);
|
||||
setEditingUser(null);
|
||||
resetForm();
|
||||
fetchUsers();
|
||||
} else {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: data.error || 'Failed to update user',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating user:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to update user',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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',
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'User deleted successfully',
|
||||
});
|
||||
fetchUsers();
|
||||
} else {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: data.error || 'Failed to delete user',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting user:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to delete user',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const openEditDialog = (user: User) => {
|
||||
setEditingUser(user);
|
||||
setFormData({
|
||||
name: user.name,
|
||||
surname: user.surname,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
password: '', // Don't pre-fill password
|
||||
});
|
||||
setIsEditDialogOpen(true);
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
name: '',
|
||||
surname: '',
|
||||
email: '',
|
||||
role: 'user',
|
||||
password: '',
|
||||
});
|
||||
};
|
||||
|
||||
const filteredUsers = users.filter(
|
||||
(user) =>
|
||||
user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
user.surname.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
user.email.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className='flex justify-center items-center py-8'>
|
||||
<div className='animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900'></div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='space-y-6'>
|
||||
{/* Header */}
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Users className='h-6 w-6' />
|
||||
<h2 className='text-2xl font-bold'>User Management</h2>
|
||||
</div>
|
||||
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button onClick={() => resetForm()}>
|
||||
<UserPlus className='h-4 w-4 mr-2' />
|
||||
Add User
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New User</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className='space-y-4'>
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<div>
|
||||
<Label htmlFor='name'>First Name</Label>
|
||||
<Input
|
||||
id='name'
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder='John'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor='surname'>Last Name</Label>
|
||||
<Input
|
||||
id='surname'
|
||||
value={formData.surname}
|
||||
onChange={(e) => setFormData({ ...formData, surname: e.target.value })}
|
||||
placeholder='Doe'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor='email'>Email</Label>
|
||||
<Input
|
||||
id='email'
|
||||
type='email'
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
placeholder='john.doe@example.com'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor='password'>Password</Label>
|
||||
<Input
|
||||
id='password'
|
||||
type='password'
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
placeholder='Enter password'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor='role'>Role</Label>
|
||||
<Select
|
||||
value={formData.role}
|
||||
onValueChange={(value: 'user' | 'admin') =>
|
||||
setFormData({ ...formData, role: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='Select role' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='user'>User</SelectItem>
|
||||
<SelectItem value='admin'>Admin</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className='flex gap-2 justify-end'>
|
||||
<Button variant='outline' onClick={() => setIsCreateDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleCreateUser}>Create User</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className='flex items-center gap-2'>
|
||||
<Search className='h-4 w-4 text-gray-500' />
|
||||
<Input
|
||||
placeholder='Search users by name or email...'
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className='max-w-sm'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Users Table */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>All Users ({filteredUsers.length})</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Role</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredUsers.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell className='font-medium'>
|
||||
{user.name} {user.surname}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Mail className='h-4 w-4 text-gray-500' />
|
||||
{user.email}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={user.role === 'admin' ? 'default' : 'secondary'}>
|
||||
{user.role}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Calendar className='h-4 w-4 text-gray-500' />
|
||||
{new Date(user.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className='flex gap-2'>
|
||||
<Button variant='outline' size='sm' onClick={() => openEditDialog(user)}>
|
||||
<Edit className='h-4 w-4' />
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => handleDeleteUser(user.id)}
|
||||
className='text-red-600 hover:text-red-700'
|
||||
>
|
||||
<Trash2 className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{filteredUsers.length === 0 && (
|
||||
<div className='text-center py-8 text-gray-500'>
|
||||
No users found matching your search criteria
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Edit User Dialog */}
|
||||
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit User</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className='space-y-4'>
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<div>
|
||||
<Label htmlFor='edit-name'>First Name</Label>
|
||||
<Input
|
||||
id='edit-name'
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder='John'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor='edit-surname'>Last Name</Label>
|
||||
<Input
|
||||
id='edit-surname'
|
||||
value={formData.surname}
|
||||
onChange={(e) => setFormData({ ...formData, surname: e.target.value })}
|
||||
placeholder='Doe'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor='edit-email'>Email</Label>
|
||||
<Input
|
||||
id='edit-email'
|
||||
type='email'
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
placeholder='john.doe@example.com'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor='edit-password'>New Password (leave blank to keep current)</Label>
|
||||
<Input
|
||||
id='edit-password'
|
||||
type='password'
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
placeholder='Enter new password'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor='edit-role'>Role</Label>
|
||||
<Select
|
||||
value={formData.role}
|
||||
onValueChange={(value: 'user' | 'admin') => setFormData({ ...formData, role: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='Select role' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='user'>User</SelectItem>
|
||||
<SelectItem value='admin'>Admin</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className='flex gap-2 justify-end'>
|
||||
<Button variant='outline' onClick={() => setIsEditDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleEditUser}>Update User</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Users, Calendar, Settings, BarChart3, Bell, Shield, Clock, MapPin, Activity, LogOut } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { AdminUserManagement } from './AdminUserManagement';
|
||||
import { AdminAnnouncementManagement } from './AdminAnnouncementManagement';
|
||||
import { AdminLogs } from './AdminLogs';
|
||||
import { AdminRecentBookings } from './AdminRecentBookings';
|
||||
import { AdminCourtManagement } from './AdminCourtManagement';
|
||||
import { AdminSettingsManagement } from './AdminSettingsManagement';
|
||||
|
||||
export function AdminDashboard() {
|
||||
const router = useRouter();
|
||||
const [stats] = useState({
|
||||
totalUsers: 125,
|
||||
todayBookings: 18,
|
||||
totalCourts: 2,
|
||||
weeklyRevenue: 850,
|
||||
});
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
});
|
||||
router.push('/');
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='min-h-screen bg-gray-50'>
|
||||
{/* Header */}
|
||||
<header className='bg-white border-b border-gray-200'>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center space-x-4'>
|
||||
<Badge variant='secondary' className='bg-blue-100 text-blue-800'>
|
||||
Administrator
|
||||
</Badge>
|
||||
<Button variant='ghost' size='sm' onClick={handleLogout}>
|
||||
<LogOut className='h-4 w-4' />
|
||||
Logout
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className='container mx-auto px-4 py-8'>
|
||||
{/* Stats Cards */}
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8'>
|
||||
<Card>
|
||||
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
|
||||
<CardTitle className='text-sm font-medium'>Total Users</CardTitle>
|
||||
<Users className='h-4 w-4 text-muted-foreground' />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='text-2xl font-bold'>{stats.totalUsers}</div>
|
||||
<p className='text-xs text-muted-foreground'>+12% from last month</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
|
||||
<CardTitle className='text-sm font-medium'>Today's Bookings</CardTitle>
|
||||
<Calendar className='h-4 w-4 text-muted-foreground' />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='text-2xl font-bold'>{stats.todayBookings}</div>
|
||||
<p className='text-xs text-muted-foreground'>+5% from yesterday</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
|
||||
<CardTitle className='text-sm font-medium'>Active Courts</CardTitle>
|
||||
<MapPin className='h-4 w-4 text-muted-foreground' />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='text-2xl font-bold'>{stats.totalCourts}</div>
|
||||
<p className='text-xs text-muted-foreground'>All courts operational</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
|
||||
<CardTitle className='text-sm font-medium'>Weekly Revenue</CardTitle>
|
||||
<BarChart3 className='h-4 w-4 text-muted-foreground' />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='text-2xl font-bold'>${stats.weeklyRevenue}</div>
|
||||
<p className='text-xs text-muted-foreground'>+8% from last week</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Admin Tabs */}
|
||||
<Tabs defaultValue='bookings' className='space-y-6'>
|
||||
<TabsList className='grid w-full grid-cols-6'>
|
||||
<TabsTrigger value='bookings'>Bookings</TabsTrigger>
|
||||
<TabsTrigger value='users'>Users</TabsTrigger>
|
||||
<TabsTrigger value='courts'>Courts</TabsTrigger>
|
||||
<TabsTrigger value='settings'>Settings</TabsTrigger>
|
||||
<TabsTrigger value='announcements'>Announcements</TabsTrigger>
|
||||
<TabsTrigger value='logs'>Logs</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value='bookings'>
|
||||
<AdminRecentBookings />
|
||||
</TabsContent>
|
||||
<TabsContent value='users'>
|
||||
<AdminUserManagement />
|
||||
</TabsContent>
|
||||
<TabsContent value='courts'>
|
||||
<AdminCourtManagement />
|
||||
</TabsContent>{' '}
|
||||
<TabsContent value='settings'>
|
||||
<AdminSettingsManagement />
|
||||
</TabsContent>
|
||||
<TabsContent value='announcements'>
|
||||
<AdminAnnouncementManagement />
|
||||
</TabsContent>{' '}
|
||||
<TabsContent value='logs'>
|
||||
<AdminLogs />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user