/** * TimeBlockCreatorModal - A visual, intuitive time block creation experience * * Features: * - Quick presets for common blocks (holidays, weekends, lunch) * - Visual calendar for date range selection * - Step-by-step flow * - Large, clear UI elements */ import React, { useState, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { X, Calendar, Clock, Sun, Moon, Coffee, Palmtree, PartyPopper, Building2, User, Ban, AlertCircle, ChevronLeft, ChevronRight, Check, Sparkles, Repeat, CalendarDays, CalendarRange, Loader2, } from 'lucide-react'; import Portal from '../Portal'; import { BlockType, RecurrenceType, RecurrencePattern, Holiday, Resource, TimeBlockListItem, } from '../../types'; // Preset block types const PRESETS = [ { id: 'weekend', name: 'Block Weekends', description: 'Every Saturday & Sunday', icon: Sun, color: 'bg-orange-100 text-orange-600 dark:bg-orange-900/30 dark:text-orange-400', config: { title: 'Weekend', recurrence_type: 'WEEKLY' as RecurrenceType, recurrence_pattern: { days_of_week: [5, 6] }, all_day: true, block_type: 'HARD' as BlockType, }, }, { id: 'lunch', name: 'Daily Lunch Break', description: '12:00 PM - 1:00 PM', icon: Coffee, color: 'bg-amber-100 text-amber-600 dark:bg-amber-900/30 dark:text-amber-400', config: { title: 'Lunch Break', recurrence_type: 'WEEKLY' as RecurrenceType, recurrence_pattern: { days_of_week: [0, 1, 2, 3, 4] }, all_day: false, start_time: '12:00', end_time: '13:00', block_type: 'SOFT' as BlockType, }, }, { id: 'vacation', name: 'Vacation / Time Off', description: 'Block a date range', icon: Palmtree, color: 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400', config: { title: 'Vacation', recurrence_type: 'NONE' as RecurrenceType, all_day: true, block_type: 'HARD' as BlockType, }, }, { id: 'holiday', name: 'Holiday', description: 'Annual recurring holiday', icon: PartyPopper, color: 'bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400', config: { title: '', recurrence_type: 'HOLIDAY' as RecurrenceType, all_day: true, block_type: 'HARD' as BlockType, }, }, { id: 'monthly', name: 'Monthly Closure', description: 'Same day(s) each month', icon: CalendarDays, color: 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400', config: { title: 'Monthly Block', recurrence_type: 'MONTHLY' as RecurrenceType, all_day: true, block_type: 'HARD' as BlockType, }, }, { id: 'custom', name: 'Custom Block', description: 'Full control over settings', icon: Sparkles, color: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400', config: { title: '', recurrence_type: 'NONE' as RecurrenceType, all_day: true, block_type: 'HARD' as BlockType, }, }, ]; // Popular US Holidays quick picks const POPULAR_HOLIDAYS = [ { code: 'new_years_day', name: "New Year's Day", date: 'Jan 1' }, { code: 'mlk_day', name: 'MLK Day', date: '3rd Mon Jan' }, { code: 'presidents_day', name: "Presidents' Day", date: '3rd Mon Feb' }, { code: 'memorial_day', name: 'Memorial Day', date: 'Last Mon May' }, { code: 'independence_day', name: 'Independence Day', date: 'Jul 4' }, { code: 'labor_day', name: 'Labor Day', date: '1st Mon Sep' }, { code: 'thanksgiving', name: 'Thanksgiving', date: '4th Thu Nov' }, { code: 'christmas', name: 'Christmas Day', date: 'Dec 25' }, ]; const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; const MONTHS = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ]; interface TimeBlockCreatorModalProps { isOpen: boolean; onClose: () => void; onSubmit: (data: any) => void; isSubmitting: boolean; editingBlock?: TimeBlockListItem | null; holidays: Holiday[]; resources: Resource[]; isResourceLevel?: boolean; } type Step = 'preset' | 'details' | 'schedule' | 'review'; const TimeBlockCreatorModal: React.FC = ({ isOpen, onClose, onSubmit, isSubmitting, editingBlock, holidays, resources, isResourceLevel: initialIsResourceLevel = false, }) => { const { t } = useTranslation(); const [step, setStep] = useState(editingBlock ? 'details' : 'preset'); const [selectedPreset, setSelectedPreset] = useState(null); const [isResourceLevel, setIsResourceLevel] = useState(initialIsResourceLevel); // Form state const [title, setTitle] = useState(editingBlock?.title || ''); const [description, setDescription] = useState(editingBlock?.description || ''); const [blockType, setBlockType] = useState(editingBlock?.block_type || 'HARD'); const [recurrenceType, setRecurrenceType] = useState(editingBlock?.recurrence_type || 'NONE'); const [allDay, setAllDay] = useState(editingBlock?.all_day ?? true); const [startTime, setStartTime] = useState(editingBlock?.start_time || '09:00'); const [endTime, setEndTime] = useState(editingBlock?.end_time || '17:00'); const [resourceId, setResourceId] = useState(editingBlock?.resource || null); // Date selection const [selectedDates, setSelectedDates] = useState([]); const [calendarMonth, setCalendarMonth] = useState(new Date()); const [dragStart, setDragStart] = useState(null); // Recurrence patterns const [daysOfWeek, setDaysOfWeek] = useState( editingBlock?.recurrence_pattern?.days_of_week || [] ); const [daysOfMonth, setDaysOfMonth] = useState( editingBlock?.recurrence_pattern?.days_of_month || [] ); const [yearlyMonth, setYearlyMonth] = useState( editingBlock?.recurrence_pattern?.month || 1 ); const [yearlyDay, setYearlyDay] = useState( editingBlock?.recurrence_pattern?.day || 1 ); const [holidayCodes, setHolidayCodes] = useState( editingBlock?.recurrence_pattern?.holiday_code ? [editingBlock.recurrence_pattern.holiday_code] : [] ); // Active period for recurring const [recurrenceStart, setRecurrenceStart] = useState(editingBlock?.recurrence_start || ''); const [recurrenceEnd, setRecurrenceEnd] = useState(editingBlock?.recurrence_end || ''); // Reset or populate form when modal opens React.useEffect(() => { if (isOpen) { if (editingBlock) { // Populate form with existing block data setStep('details'); // Skip preset step when editing setSelectedPreset(null); setTitle(editingBlock.title || ''); setDescription(editingBlock.description || ''); setBlockType(editingBlock.block_type || 'HARD'); setRecurrenceType(editingBlock.recurrence_type || 'NONE'); setAllDay(editingBlock.all_day ?? true); setStartTime(editingBlock.start_time || '09:00'); setEndTime(editingBlock.end_time || '17:00'); setResourceId(editingBlock.resource || null); setIsResourceLevel(!!editingBlock.resource); // Set level based on whether block has a resource // Parse dates if available if (editingBlock.start_date) { const startDate = new Date(editingBlock.start_date); const endDate = editingBlock.end_date ? new Date(editingBlock.end_date) : startDate; const dates: Date[] = []; const current = new Date(startDate); while (current <= endDate) { dates.push(new Date(current)); current.setDate(current.getDate() + 1); } setSelectedDates(dates); setCalendarMonth(startDate); } else { setSelectedDates([]); } // Parse recurrence_pattern - handle both string and object formats let pattern = editingBlock.recurrence_pattern; if (typeof pattern === 'string') { try { pattern = JSON.parse(pattern); } catch { pattern = {}; } } pattern = pattern || {}; // Recurrence patterns setDaysOfWeek(pattern.days_of_week || []); setDaysOfMonth(pattern.days_of_month || []); setYearlyMonth(pattern.month || 1); setYearlyDay(pattern.day || 1); setHolidayCodes(pattern.holiday_code ? [pattern.holiday_code] : []); setRecurrenceStart(editingBlock.recurrence_start || ''); setRecurrenceEnd(editingBlock.recurrence_end || ''); } else { // Reset form for new block setStep('preset'); setSelectedPreset(null); setTitle(''); setDescription(''); setBlockType('HARD'); setRecurrenceType('NONE'); setAllDay(true); setStartTime('09:00'); setEndTime('17:00'); setResourceId(null); setSelectedDates([]); setDaysOfWeek([]); setDaysOfMonth([]); setYearlyMonth(1); setYearlyDay(1); setHolidayCodes([]); setRecurrenceStart(''); setRecurrenceEnd(''); setIsResourceLevel(initialIsResourceLevel); } } }, [isOpen, editingBlock, initialIsResourceLevel]); // Apply preset configuration const applyPreset = (presetId: string) => { const preset = PRESETS.find(p => p.id === presetId); if (!preset) return; setSelectedPreset(presetId); setTitle(preset.config.title); setRecurrenceType(preset.config.recurrence_type); setAllDay(preset.config.all_day); setBlockType(preset.config.block_type); if (preset.config.start_time) setStartTime(preset.config.start_time); if (preset.config.end_time) setEndTime(preset.config.end_time); if (preset.config.recurrence_pattern?.days_of_week) { setDaysOfWeek(preset.config.recurrence_pattern.days_of_week); } setStep('details'); }; // Calendar helpers const getDaysInMonth = (date: Date): (Date | null)[] => { const year = date.getFullYear(); const month = date.getMonth(); const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const days: (Date | null)[] = []; // Padding for days before first of month for (let i = 0; i < firstDay.getDay(); i++) { days.push(null); } // Days of the month for (let d = 1; d <= lastDay.getDate(); d++) { days.push(new Date(year, month, d)); } return days; }; const isDateSelected = (date: Date): boolean => { return selectedDates.some(d => d.getFullYear() === date.getFullYear() && d.getMonth() === date.getMonth() && d.getDate() === date.getDate() ); }; const isDateInRange = (date: Date): boolean => { if (!dragStart || selectedDates.length === 0) return false; const start = dragStart < date ? dragStart : date; const end = dragStart < date ? date : dragStart; return date >= start && date <= end; }; const handleDateClick = (date: Date) => { if (recurrenceType === 'NONE') { // Single date or range selection if (selectedDates.length === 0) { setSelectedDates([date]); setDragStart(date); } else if (selectedDates.length === 1) { // Create range const start = selectedDates[0] < date ? selectedDates[0] : date; const end = selectedDates[0] < date ? date : selectedDates[0]; const range: Date[] = []; const current = new Date(start); while (current <= end) { range.push(new Date(current)); current.setDate(current.getDate() + 1); } setSelectedDates(range); setDragStart(null); } else { // Start new selection setSelectedDates([date]); setDragStart(date); } } }; const handleSubmit = () => { const baseData: any = { description: description || undefined, block_type: blockType, recurrence_type: recurrenceType, all_day: allDay, resource: isResourceLevel ? resourceId : null, }; if (!allDay) { baseData.start_time = startTime; baseData.end_time = endTime; } if (recurrenceType !== 'NONE') { if (recurrenceStart) baseData.recurrence_start = recurrenceStart; if (recurrenceEnd) baseData.recurrence_end = recurrenceEnd; } // For holidays with multiple selections, create separate blocks if (recurrenceType === 'HOLIDAY' && holidayCodes.length > 0) { const blocks = holidayCodes.map(code => { const holiday = holidays.find(h => h.code === code); return { ...baseData, title: holiday?.name || code, recurrence_pattern: { holiday_code: code }, }; }); onSubmit(blocks); return; } // Single block for other recurrence types const data = { ...baseData, title }; if (recurrenceType === 'NONE') { if (selectedDates.length > 0) { const sorted = [...selectedDates].sort((a, b) => a.getTime() - b.getTime()); data.start_date = sorted[0].toISOString().split('T')[0]; data.end_date = sorted[sorted.length - 1].toISOString().split('T')[0]; } } else if (recurrenceType === 'WEEKLY') { data.recurrence_pattern = { days_of_week: daysOfWeek }; } else if (recurrenceType === 'MONTHLY') { data.recurrence_pattern = { days_of_month: daysOfMonth }; } else if (recurrenceType === 'YEARLY') { data.recurrence_pattern = { month: yearlyMonth, day: yearlyDay }; } onSubmit(data); }; const canProceed = (): boolean => { switch (step) { case 'preset': return true; case 'details': if (!title.trim()) return false; if (isResourceLevel && !resourceId) return false; return true; case 'schedule': if (recurrenceType === 'NONE' && selectedDates.length === 0) return false; if (recurrenceType === 'WEEKLY' && daysOfWeek.length === 0) return false; if (recurrenceType === 'MONTHLY' && daysOfMonth.length === 0) return false; if (recurrenceType === 'HOLIDAY' && holidayCodes.length === 0) return false; return true; case 'review': return true; default: return false; } }; const nextStep = () => { if (step === 'preset') setStep('details'); else if (step === 'details') setStep('schedule'); else if (step === 'schedule') setStep('review'); }; const prevStep = () => { if (step === 'details' && !editingBlock) setStep('preset'); else if (step === 'schedule') setStep('details'); else if (step === 'review') setStep('schedule'); }; if (!isOpen) return null; return (
{/* Header */}

{editingBlock ? 'Edit Time Block' : 'Create Time Block'}

{step === 'preset' && 'Choose a quick preset or start custom'} {step === 'details' && 'Configure block details'} {step === 'schedule' && 'Set the schedule'} {step === 'review' && 'Review and create'}

{/* Progress Steps */}
{(['preset', 'details', 'schedule', 'review'] as Step[]).map((s, i) => (
{i < ['preset', 'details', 'schedule', 'review'].indexOf(step) ? ( ) : ( {i + 1} )} {s}
{i < 3 && (
)} ))}
{/* Content */}
{/* Step 1: Preset Selection */} {step === 'preset' && (
{PRESETS.map((preset) => { const Icon = preset.icon; return ( ); })}
)} {/* Step 2: Details */} {step === 'details' && (
{/* Block Level Selector */}
{/* Title */}
setTitle(e.target.value)} className="w-full px-4 py-3 text-lg border border-gray-300 dark:border-gray-600 rounded-xl bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-brand-500 transition-colors" placeholder="e.g., Christmas Day, Team Meeting, Vacation" />
{/* Description */}