448 lines
14 KiB
TypeScript
448 lines
14 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 { 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 {
|
|
// Club/Brand Settings
|
|
club_name: string;
|
|
sport_name: string;
|
|
app_title: string;
|
|
app_description: string;
|
|
// Booking Settings
|
|
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;
|
|
max_bookings_per_user_per_hour_per_day: string;
|
|
allow_booking_modifications: string;
|
|
booking_modification_hours_before: string;
|
|
}
|
|
|
|
export function AdminSettingsManagement() {
|
|
const [settings, setSettings] = useState<SettingsData>({
|
|
// Club/Brand Settings
|
|
club_name: 'TT Club',
|
|
sport_name: 'Table Tennis',
|
|
app_title: 'Table Tennis Booking System',
|
|
app_description: 'Book your table tennis court slots with ease',
|
|
// Booking Settings
|
|
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',
|
|
max_bookings_per_user_per_hour_per_day: '1',
|
|
allow_booking_modifications: 'true',
|
|
booking_modification_hours_before: '1',
|
|
});
|
|
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 = {
|
|
// Club/Brand Settings
|
|
club_name: 'TT Club',
|
|
sport_name: 'Table Tennis',
|
|
app_title: 'Table Tennis Booking System',
|
|
app_description: 'Book your table tennis court slots with ease',
|
|
// Booking Settings
|
|
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',
|
|
max_bookings_per_user_per_hour_per_day: '1',
|
|
allow_booking_modifications: 'true',
|
|
booking_modification_hours_before: '1',
|
|
}; // 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'>
|
|
{/* Club/Brand Configuration Section */}
|
|
<div>
|
|
<h3 className='text-lg font-medium mb-4'>Club & Branding</h3>
|
|
<div className='grid grid-cols-1 md:grid-cols-2 gap-6'>
|
|
{/* Club Name */}
|
|
<div className='space-y-2'>
|
|
<Label htmlFor='club_name'>Club Name</Label>
|
|
<Input
|
|
id='club_name'
|
|
type='text'
|
|
placeholder='e.g., Downtown TT Club'
|
|
value={settings.club_name}
|
|
onChange={(e) => updateSetting('club_name', e.target.value)}
|
|
/>
|
|
<p className='text-sm text-gray-500'>The name of your club or organization</p>
|
|
</div>
|
|
|
|
{/* Sport Name */}
|
|
<div className='space-y-2'>
|
|
<Label htmlFor='sport_name'>Sport Name</Label>
|
|
<Input
|
|
id='sport_name'
|
|
type='text'
|
|
placeholder='e.g., Table Tennis, Ping Pong, Badminton'
|
|
value={settings.sport_name}
|
|
onChange={(e) => updateSetting('sport_name', e.target.value)}
|
|
/>
|
|
<p className='text-sm text-gray-500'>The sport played at your facility</p>
|
|
</div>
|
|
|
|
{/* App Title */}
|
|
<div className='space-y-2'>
|
|
<Label htmlFor='app_title'>Application Title</Label>
|
|
<Input
|
|
id='app_title'
|
|
type='text'
|
|
placeholder='e.g., Downtown TT Booking'
|
|
value={settings.app_title}
|
|
onChange={(e) => updateSetting('app_title', e.target.value)}
|
|
/>
|
|
<p className='text-sm text-gray-500'>Main title shown in browser and app header</p>
|
|
</div>
|
|
|
|
{/* App Description */}
|
|
<div className='space-y-2'>
|
|
<Label htmlFor='app_description'>Application Description</Label>
|
|
<Input
|
|
id='app_description'
|
|
type='text'
|
|
placeholder='e.g., Book your court slots with ease'
|
|
value={settings.app_description}
|
|
onChange={(e) => updateSetting('app_description', e.target.value)}
|
|
/>
|
|
<p className='text-sm text-gray-500'>Short description for login/register pages</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Booking Configuration Section */}
|
|
<div>
|
|
<h3 className='text-lg font-medium mb-4'>Booking Configuration</h3>
|
|
<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>
|
|
|
|
{/* Booking Restrictions */}
|
|
<div className='space-y-2'>
|
|
<Label htmlFor='max_bookings_per_user_per_hour_per_day'>
|
|
Max Bookings per User per Hour
|
|
</Label>
|
|
<Input
|
|
id='max_bookings_per_user_per_hour_per_day'
|
|
type='number'
|
|
min='1'
|
|
max='5'
|
|
value={settings.max_bookings_per_user_per_hour_per_day}
|
|
onChange={(e) =>
|
|
updateSetting('max_bookings_per_user_per_hour_per_day', e.target.value)
|
|
}
|
|
/>
|
|
<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'>
|
|
<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>
|
|
|
|
<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>
|
|
<p>
|
|
<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>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|