/** * CreateAppointmentModal Component * * Modal for creating new appointments with customer, service, and participant selection. * Supports both linked customers and participants with external email addresses. */ import React, { useState, useMemo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { X, Search, User as UserIcon, Calendar, Clock, Users } from 'lucide-react'; import { createPortal } from 'react-dom'; import { Resource, Service, ParticipantInput } from '../types'; import { useCustomers } from '../hooks/useCustomers'; import { ParticipantSelector } from './ParticipantSelector'; interface CreateAppointmentModalProps { resources: Resource[]; services: Service[]; initialDate?: Date; initialResourceId?: string | null; onCreate: (appointmentData: { serviceId: string; customerId: string; startTime: Date; resourceId?: string | null; durationMinutes: number; notes?: string; participantsInput?: ParticipantInput[]; }) => void; onClose: () => void; isCreating?: boolean; } export const CreateAppointmentModal: React.FC = ({ resources, services, initialDate, initialResourceId, onCreate, onClose, isCreating = false, }) => { const { t } = useTranslation(); // Form state const [selectedServiceId, setSelectedServiceId] = useState(''); const [selectedCustomerId, setSelectedCustomerId] = useState(''); const [customerSearch, setCustomerSearch] = useState(''); const [showCustomerDropdown, setShowCustomerDropdown] = useState(false); const [selectedDateTime, setSelectedDateTime] = useState(() => { // Default to initial date or now, rounded to nearest 15 min const date = initialDate || new Date(); const minutes = Math.ceil(date.getMinutes() / 15) * 15; date.setMinutes(minutes, 0, 0); // Convert to datetime-local format const localDateTime = new Date(date.getTime() - date.getTimezoneOffset() * 60000) .toISOString() .slice(0, 16); return localDateTime; }); const [selectedResourceId, setSelectedResourceId] = useState(initialResourceId || ''); const [duration, setDuration] = useState(30); const [notes, setNotes] = useState(''); const [participants, setParticipants] = useState([]); // Fetch customers for search const { data: customers = [] } = useCustomers({ search: customerSearch }); // Get selected customer details const selectedCustomer = useMemo(() => { return customers.find(c => c.id === selectedCustomerId); }, [customers, selectedCustomerId]); // Get selected service details const selectedService = useMemo(() => { return services.find(s => s.id === selectedServiceId); }, [services, selectedServiceId]); // When service changes, update duration to service default const handleServiceChange = useCallback((serviceId: string) => { setSelectedServiceId(serviceId); const service = services.find(s => s.id === serviceId); if (service) { setDuration(service.durationMinutes); } }, [services]); // Handle customer selection from search const handleSelectCustomer = useCallback((customerId: string, customerName: string) => { setSelectedCustomerId(customerId); setCustomerSearch(customerName); setShowCustomerDropdown(false); }, []); // Filter customers based on search const filteredCustomers = useMemo(() => { if (!customerSearch.trim()) return []; return customers.slice(0, 10); }, [customers, customerSearch]); // Validation const canCreate = useMemo(() => { return selectedServiceId && selectedCustomerId && selectedDateTime && duration >= 15; }, [selectedServiceId, selectedCustomerId, selectedDateTime, duration]); // Handle create const handleCreate = useCallback(() => { if (!canCreate) return; const startTime = new Date(selectedDateTime); onCreate({ serviceId: selectedServiceId, customerId: selectedCustomerId, startTime, resourceId: selectedResourceId || null, durationMinutes: duration, notes: notes.trim() || undefined, participantsInput: participants.length > 0 ? participants : undefined, }); }, [canCreate, selectedServiceId, selectedCustomerId, selectedDateTime, selectedResourceId, duration, notes, participants, onCreate]); const modalContent = (
e.stopPropagation()} > {/* Header */}

{t('scheduler.newAppointment', 'New Appointment')}

{/* Scrollable content */}
{/* Service Selection */}
{/* Customer Selection */}
{ setCustomerSearch(e.target.value); setShowCustomerDropdown(true); if (!e.target.value.trim()) { setSelectedCustomerId(''); } }} onFocus={() => setShowCustomerDropdown(true)} placeholder={t('customers.searchPlaceholder', 'Search customers by name or email...')} className="w-full pl-10 pr-4 py-2.5 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-sm text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-brand-500" /> {selectedCustomer && ( )}
{/* Customer search results dropdown */} {showCustomerDropdown && customerSearch.trim() && (
{filteredCustomers.length === 0 ? (
{t('common.noResults', 'No results found')}
) : ( filteredCustomers.map((customer) => ( )) )}
)} {/* Click outside to close dropdown */} {showCustomerDropdown && (
setShowCustomerDropdown(false)} /> )}
{/* Date, Time & Duration */}
setSelectedDateTime(e.target.value)} className="w-full px-3 py-2.5 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-sm text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-brand-500" />
{ const value = parseInt(e.target.value); setDuration(value >= 15 ? value : 15); }} className="w-full px-3 py-2.5 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-sm text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-brand-500" />
{/* Resource Assignment */}
{/* Participants Section */}
{t('participants.additionalParticipants', 'Additional Participants')}
{/* Notes */}