/** * Business Hours Settings * * Configure weekly operating hours that automatically block customer bookings * outside those times while allowing staff manual override. */ import React, { useState, useEffect } from 'react'; import { useTimeBlocks, useCreateTimeBlock, useUpdateTimeBlock, useDeleteTimeBlock } from '../../hooks/useTimeBlocks'; import { Button, FormInput, Alert, LoadingSpinner, Card } from '../../components/ui'; import { BlockPurpose, TimeBlock } from '../../types'; interface DayHours { enabled: boolean; open: string; // "09:00" close: string; // "17:00" } interface BusinessHours { monday: DayHours; tuesday: DayHours; wednesday: DayHours; thursday: DayHours; friday: DayHours; saturday: DayHours; sunday: DayHours; } const DAYS = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] as const; const DAY_LABELS: Record = { monday: 'Monday', tuesday: 'Tuesday', wednesday: 'Wednesday', thursday: 'Thursday', friday: 'Friday', saturday: 'Saturday', sunday: 'Sunday', }; const DAY_INDICES: Record = { monday: 0, tuesday: 1, wednesday: 2, thursday: 3, friday: 4, saturday: 5, sunday: 6, }; const DEFAULT_HOURS: BusinessHours = { monday: { enabled: true, open: '09:00', close: '17:00' }, tuesday: { enabled: true, open: '09:00', close: '17:00' }, wednesday: { enabled: true, open: '09:00', close: '17:00' }, thursday: { enabled: true, open: '09:00', close: '17:00' }, friday: { enabled: true, open: '09:00', close: '17:00' }, saturday: { enabled: false, open: '09:00', close: '17:00' }, sunday: { enabled: false, open: '09:00', close: '17:00' }, }; const BusinessHoursSettings: React.FC = () => { const [hours, setHours] = useState(DEFAULT_HOURS); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const [isSaving, setIsSaving] = useState(false); // Fetch existing business hours time blocks const { data: timeBlocks, isLoading } = useTimeBlocks({ purpose: 'BUSINESS_HOURS' as BlockPurpose, is_active: true, }); const createTimeBlock = useCreateTimeBlock(); const updateTimeBlock = useUpdateTimeBlock(); const deleteTimeBlock = useDeleteTimeBlock(); // Parse existing time blocks into UI state useEffect(() => { if (!timeBlocks || timeBlocks.length === 0) return; const parsed: BusinessHours = { ...DEFAULT_HOURS }; // Group blocks by day timeBlocks.forEach((block) => { if (block.recurrence_type === 'WEEKLY' && block.recurrence_pattern?.days_of_week) { const daysOfWeek = block.recurrence_pattern.days_of_week; daysOfWeek.forEach((dayIndex) => { const dayName = Object.keys(DAY_INDICES).find( (key) => DAY_INDICES[key as typeof DAYS[number]] === dayIndex ) as typeof DAYS[number] | undefined; if (dayName) { // Check if this is a "before hours" or "after hours" block if (block.start_time === '00:00:00') { // Before hours block: 00:00 to open time parsed[dayName].enabled = true; parsed[dayName].open = block.end_time?.substring(0, 5) || '09:00'; } else if (block.end_time === '23:59:59' || block.end_time === '00:00:00') { // After hours block: close time to 24:00 parsed[dayName].enabled = true; parsed[dayName].close = block.start_time?.substring(0, 5) || '17:00'; } } }); } }); setHours(parsed); }, [timeBlocks]); const handleDayToggle = (day: typeof DAYS[number]) => { setHours({ ...hours, [day]: { ...hours[day], enabled: !hours[day].enabled, }, }); }; const handleTimeChange = (day: typeof DAYS[number], field: 'open' | 'close', value: string) => { setHours({ ...hours, [day]: { ...hours[day], [field]: value, }, }); }; const validateHours = (): boolean => { setError(''); // Check that enabled days have valid times for (const day of DAYS) { if (hours[day].enabled) { const open = hours[day].open; const close = hours[day].close; if (!open || !close) { setError(`Please set both open and close times for ${DAY_LABELS[day]}`); return false; } if (open >= close) { setError(`${DAY_LABELS[day]}: Close time must be after open time`); return false; } } } return true; }; const handleSave = async () => { if (!validateHours()) return; setIsSaving(true); setError(''); setSuccess(''); try { console.log('Starting save, existing blocks:', timeBlocks); // Delete all existing business hours blocks if (timeBlocks && timeBlocks.length > 0) { console.log('Deleting', timeBlocks.length, 'existing blocks'); for (const block of timeBlocks) { try { await deleteTimeBlock.mutateAsync(block.id); console.log('Deleted block:', block.id); } catch (delErr: any) { console.error('Error deleting block:', block.id, delErr); throw new Error(`Failed to delete existing block: ${delErr.response?.data?.message || delErr.message}`); } } } // Group days by hours for efficient block creation const hourGroups: Map = new Map(); DAYS.forEach((day) => { if (hours[day].enabled) { const key = `${hours[day].open}-${hours[day].close}`; const dayIndex = DAY_INDICES[day]; if (!hourGroups.has(key)) { hourGroups.set(key, []); } hourGroups.get(key)!.push(dayIndex); } }); console.log('Hour groups:', Array.from(hourGroups.entries())); // Create new time blocks for each group for (const [hoursKey, daysOfWeek] of hourGroups.entries()) { const [open, close] = hoursKey.split('-'); // Before hours block: 00:00 to open time try { const beforeBlock = await createTimeBlock.mutateAsync({ title: 'Before Business Hours', purpose: 'BUSINESS_HOURS' as BlockPurpose, block_type: 'SOFT', resource: null, recurrence_type: 'WEEKLY', recurrence_pattern: { days_of_week: daysOfWeek }, all_day: false, start_time: '00:00:00', end_time: `${open}:00`, is_active: true, }); console.log('Created before-hours block:', beforeBlock); } catch (createErr: any) { console.error('Error creating before-hours block:', createErr); throw new Error(`Failed to create before-hours block: ${createErr.response?.data?.message || createErr.message}`); } // After hours block: close time to 23:59:59 try { const afterBlock = await createTimeBlock.mutateAsync({ title: 'After Business Hours', purpose: 'BUSINESS_HOURS' as BlockPurpose, block_type: 'SOFT', resource: null, recurrence_type: 'WEEKLY', recurrence_pattern: { days_of_week: daysOfWeek }, all_day: false, start_time: `${close}:00`, end_time: '23:59:59', is_active: true, }); console.log('Created after-hours block:', afterBlock); } catch (createErr: any) { console.error('Error creating after-hours block:', createErr); throw new Error(`Failed to create after-hours block: ${createErr.response?.data?.message || createErr.message}`); } } console.log('Save completed successfully'); setSuccess('Business hours saved successfully! Customer bookings will be blocked outside these hours.'); } catch (err: any) { console.error('Save error:', err); setError(err.message || err.response?.data?.message || 'Failed to save business hours. Please try again.'); } finally { setIsSaving(false); } }; if (isLoading) { return (
); } return (

Business Hours

Set your regular operating hours. Customer bookings will be blocked outside these times, but staff can still manually schedule appointments if needed.

{error && ( {error} )} {success && ( {success} )}
{DAYS.map((day) => (
handleDayToggle(day)} className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" />
{hours[day].enabled ? (
handleTimeChange(day, 'open', e.target.value)} className="px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" />
handleTimeChange(day, 'close', e.target.value)} className="px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" />
({calculateHours(hours[day].open, hours[day].close)} hours)
) : (
Closed
)}
))}
Note: These hours apply to customer bookings only. Staff can override.
{/* Preview */}

Preview

{DAYS.map((day) => (
{DAY_LABELS[day]}: {hours[day].enabled ? `${formatTime(hours[day].open)} - ${formatTime(hours[day].close)}` : 'Closed'}
))}
); }; // Helper functions const calculateHours = (open: string, close: string): string => { try { if (!open || !close || !open.includes(':') || !close.includes(':')) { return '0'; } const [openHour, openMin] = open.split(':').map(Number); const [closeHour, closeMin] = close.split(':').map(Number); if (isNaN(openHour) || isNaN(openMin) || isNaN(closeHour) || isNaN(closeMin)) { return '0'; } const totalMinutes = (closeHour * 60 + closeMin) - (openHour * 60 + openMin); const hours = Math.floor(totalMinutes / 60); const minutes = totalMinutes % 60; if (minutes === 0) return `${hours}`; return `${hours}.${minutes < 10 ? '0' : ''}${minutes}`; } catch (e) { return '0'; } }; const formatTime = (time: string): string => { try { if (!time || !time.includes(':')) { return time; } const [hour, min] = time.split(':').map(Number); if (isNaN(hour) || isNaN(min)) { return time; } const period = hour >= 12 ? 'PM' : 'AM'; const displayHour = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour; return `${displayHour}:${min.toString().padStart(2, '0')} ${period}`; } catch (e) { return time; } }; export default BusinessHoursSettings;