/** * ParticipantSelector Component * * A reusable component for selecting participants (staff, customers, observers) * for appointments. Supports both linked users and external email participants. */ import React, { useState, useMemo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { X, Plus, Search, User, UserPlus, Mail, Users } from 'lucide-react'; import { Participant, ParticipantInput, ParticipantRole } from '../types'; import { useStaff, StaffMember } from '../hooks/useStaff'; import { useCustomers } from '../hooks/useCustomers'; import { Customer } from '../types'; interface ParticipantSelectorProps { value: ParticipantInput[]; onChange: (participants: ParticipantInput[]) => void; allowedRoles?: ParticipantRole[]; allowExternalEmail?: boolean; existingParticipants?: Participant[]; } // Role badge colors const roleBadgeColors: Record = { STAFF: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300', CUSTOMER: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300', OBSERVER: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300', RESOURCE: 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-300', }; export const ParticipantSelector: React.FC = ({ value, onChange, allowedRoles = ['STAFF', 'CUSTOMER', 'OBSERVER'], allowExternalEmail = true, existingParticipants = [], }) => { const { t } = useTranslation(); const [searchQuery, setSearchQuery] = useState(''); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [showExternalInput, setShowExternalInput] = useState(false); const [externalEmail, setExternalEmail] = useState(''); const [externalName, setExternalName] = useState(''); const [externalRole, setExternalRole] = useState('OBSERVER'); const [externalEmailError, setExternalEmailError] = useState(''); // Fetch staff and customers for search const { data: staff = [] } = useStaff({ search: searchQuery }); const { data: customers = [] } = useCustomers({ search: searchQuery }); // Get display name for a participant const getDisplayName = useCallback((p: ParticipantInput): string => { if (p.externalName) return p.externalName; if (p.externalEmail) return p.externalEmail; // Find from staff or customers if (p.userId) { const staffMember = staff.find(s => s.id === String(p.userId)); if (staffMember) return staffMember.name; const customer = customers.find(c => c.id === String(p.userId)); if (customer) return customer.name; } return t('participants.unknownParticipant', 'Unknown'); }, [staff, customers, t]); // Filter search results const searchResults = useMemo(() => { if (!searchQuery.trim()) return []; const results: Array<{ id: string; name: string; email: string; type: 'staff' | 'customer'; role: ParticipantRole; }> = []; // Add staff members if STAFF role is allowed if (allowedRoles.includes('STAFF')) { staff.forEach(s => { // Skip if already added const alreadyAdded = value.some(p => p.userId === parseInt(s.id)); if (!alreadyAdded) { results.push({ id: s.id, name: s.name, email: s.email, type: 'staff', role: 'STAFF', }); } }); } // Add customers if CUSTOMER role is allowed if (allowedRoles.includes('CUSTOMER')) { customers.forEach(c => { // Skip if already added const alreadyAdded = value.some(p => p.userId === parseInt(c.id)); if (!alreadyAdded) { results.push({ id: c.id, name: c.name, email: c.email, type: 'customer', role: 'CUSTOMER', }); } }); } return results.slice(0, 10); // Limit results }, [searchQuery, staff, customers, value, allowedRoles]); // Add a user as participant const handleAddUser = useCallback((userId: string, role: ParticipantRole) => { const newParticipant: ParticipantInput = { role, userId: parseInt(userId), }; onChange([...value, newParticipant]); setSearchQuery(''); setIsDropdownOpen(false); }, [value, onChange]); // Add external email participant const handleAddExternal = useCallback(() => { // Validate email const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!externalEmail.trim()) { setExternalEmailError(t('participants.emailRequired')); return; } if (!emailRegex.test(externalEmail)) { setExternalEmailError(t('participants.invalidEmail')); return; } // Check if already added const alreadyAdded = value.some(p => p.externalEmail === externalEmail); if (alreadyAdded) { setExternalEmailError(t('participants.alreadyAdded', 'This email is already added')); return; } const newParticipant: ParticipantInput = { role: externalRole, externalEmail: externalEmail.trim(), externalName: externalName.trim(), }; onChange([...value, newParticipant]); setExternalEmail(''); setExternalName(''); setExternalEmailError(''); setShowExternalInput(false); }, [externalEmail, externalName, externalRole, value, onChange, t]); // Remove a participant const handleRemove = useCallback((index: number) => { const newValue = [...value]; newValue.splice(index, 1); onChange(newValue); }, [value, onChange]); return (
{/* Header */}
{allowExternalEmail && ( )}
{/* Selected participants */}
{value.length === 0 ? (

{t('participants.noParticipants')}

) : ( value.map((participant, index) => (
{participant.externalEmail ? ( ) : ( )} {getDisplayName(participant)} {participant.externalEmail && ( ({t(`participants.roles.${participant.role}`)}) )}
)) )}
{/* External email input */} {showExternalInput && (
{ setExternalEmail(e.target.value); setExternalEmailError(''); }} placeholder="guest@example.com" className="w-full px-3 py-2 text-sm border rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white" /> {externalEmailError && (

{externalEmailError}

)}
setExternalName(e.target.value)} placeholder="Guest Name" className="w-full px-3 py-2 text-sm border rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white" />
)} {/* Search input */}
{ setSearchQuery(e.target.value); setIsDropdownOpen(true); }} onFocus={() => setIsDropdownOpen(true)} placeholder={t('participants.searchPlaceholder')} className="w-full pl-10 pr-4 py-2 text-sm border rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white" />
{/* Search results dropdown */} {isDropdownOpen && searchQuery.trim() && (
{searchResults.length === 0 ? (
{t('common.noResults', 'No results found')}
) : ( searchResults.map((result) => ( )) )}
)}
{/* Click outside to close dropdown */} {isDropdownOpen && (
setIsDropdownOpen(false)} /> )}
); }; export default ParticipantSelector;