import { useState, useEffect } from 'react' import { getGuests, getGuestOwners, createGuest, updateGuest, deleteGuest, sendWhatsAppInvitationToGuests, getEvent } from '../api/api' import GuestForm from './GuestForm' import GoogleImport from './GoogleImport' import ImportContacts from './ImportContacts' import SearchFilter from './SearchFilter' import DuplicateManager from './DuplicateManager' import WhatsAppInviteModal from './WhatsAppInviteModal' import * as XLSX from 'xlsx' import './GuestList.css' // Hebrew translations const he = { backToEvents: '← חזרה לאירועים', guestManagement: 'ניהול אורחים', manageMembers: '👥 ניהול חברים', exportExcel: '📥 ייצוא לאקסל', addGuest: '+ הוסף אורח', totalGuests: 'סה"כ אורחים', confirmed: 'אישרו הגעה', declined: 'דחו הגעה', inviteSent: 'הזמנות שנשלחו', filterByStatus: 'סנן לפי סטטוס:', filterByOwner: 'האורחים של:', allGuests: 'כל האורחים', selfService: 'רישום עצמי', noGuestsFound: 'לא נמצאו אורחים. התחל בהוספת אורח ראשון!', addFirstGuest: 'הוסף אורח ראשון', name: 'שם', phone: 'טלפון', email: 'אימייל', rsvpStatus: 'סטטוס RSVP', mealPref: 'העדפת מזון', plusOne: 'חברה נוספת', actions: 'פעולות', edit: 'עריכה', delete: 'מחיקה', selectAll: 'בחר הכל', selectedCount: 'נבחרו {count} אורחים', confirm: 'אישור', decline: 'דחייה', invited: 'הזמנה', sure: 'האם אתה בטוח שברצונך למחוק אורח זה?', failedToLoadOwners: 'נכשל בטעינת בעלים', failedToLoadGuests: 'נכשל בטעינת אורחים', failedToDelete: 'נכשל במחיקת אורח', sendWhatsApp: '💬 שלח בוואטסאפ', noGuestsSelected: 'בחר לפחות אורח אחד', selectGuestsFirst: 'בחר אורחים לשליחת הזמנה' } function GuestList({ eventId, onBack, onShowMembers }) { const [guests, setGuests] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState('') const [eventNotFound, setEventNotFound] = useState(false) const [showGuestForm, setShowGuestForm] = useState(false) const [editingGuest, setEditingGuest] = useState(null) const [owners, setOwners] = useState([]) const [ownerList, setOwnerList] = useState([]) const [selectedGuestIds, setSelectedGuestIds] = useState(new Set()) const [searchFilters, setSearchFilters] = useState({ query: '', rsvpStatus: '', mealPreference: '', owner: '' }) const [showDuplicateManager, setShowDuplicateManager] = useState(false) const [itemsPerPage, setItemsPerPage] = useState(25) const [showWhatsAppModal, setShowWhatsAppModal] = useState(false) const [eventData, setEventData] = useState({}) useEffect(() => { loadGuests() loadOwners() loadEventData() }, [eventId]) const loadOwners = async () => { try { const data = await getGuestOwners(eventId) if (data.owners) { setOwnerList(data.owners) setOwners(data) } } catch (err) { if (err?.response?.status === 404) { setEventNotFound(true) } else { console.error('Failed to load guest owners:', err) setError(he.failedToLoadOwners) } } } const loadEventData = async () => { try { const data = await getEvent(eventId) setEventData(data) } catch (err) { if (err?.response?.status === 404) { setEventNotFound(true) setLoading(false) } console.error('Failed to load event data:', err) } } const loadGuests = async () => { try { setLoading(true) const data = await getGuests(eventId) setGuests(data) setSelectedGuestIds(new Set()) setError('') } catch (err) { if (err?.response?.status === 404) { setEventNotFound(true) } else { setError(he.failedToLoadGuests) console.error(err) } } finally { setLoading(false) } } const handleGuestCreated = async (guestData) => { try { const newGuest = await createGuest(eventId, guestData) setGuests([...guests, newGuest]) setShowGuestForm(false) setEditingGuest(null) } catch (err) { console.error('Failed to create guest:', err) throw err } } const handleGuestUpdated = async (guestId, guestData) => { try { const updatedGuest = await updateGuest(eventId, guestId, guestData) setGuests(guests.map(g => g.id === guestId ? updatedGuest : g)) setShowGuestForm(false) setEditingGuest(null) } catch (err) { console.error('Failed to update guest:', err) throw err } } const handleDelete = async (guestId) => { if (!window.confirm(he.sure)) { return } try { await deleteGuest(eventId, guestId) setGuests(guests.filter(g => g.id !== guestId)) setSelectedGuestIds(prev => { const newSet = new Set(prev) newSet.delete(guestId) return newSet }) } catch (err) { setError(he.failedToDelete) console.error(err) } } const handleEdit = (guest) => { setEditingGuest(guest) setShowGuestForm(true) } const toggleGuestSelection = (guestId) => { const newSet = new Set(selectedGuestIds) if (newSet.has(guestId)) { newSet.delete(guestId) } else { newSet.add(guestId) } setSelectedGuestIds(newSet) } const toggleSelectAll = () => { if (selectedGuestIds.size === filteredGuests.length) { setSelectedGuestIds(new Set()) } else { setSelectedGuestIds(new Set(filteredGuests.map(g => g.id))) } } // Apply search and filter logic const filteredGuests = guests.filter(guest => { // Text search — normalize whitespace first, then match token-by-token so that: // • trailing/leading spaces don't break results ("דור " == "דור") // • multiple spaces collapse to one ("דור נחמני" == "דור נחמני") // • full-name search works ("דור נחמני" matches first="דור" last="נחמני") if (searchFilters.query) { const normalized = searchFilters.query.trim().replace(/\s+/g, ' ') if (normalized === '') { // After normalization the query is blank → treat as "no filter" } else { const tokens = normalized.toLowerCase().split(' ').filter(Boolean) const haystack = [ guest.first_name || '', guest.last_name || '', guest.phone_number|| '', guest.email || '', ].join(' ').toLowerCase() const matchesQuery = tokens.every(token => haystack.includes(token)) if (!matchesQuery) return false } } // RSVP Status filter if (searchFilters.rsvpStatus && guest.rsvp_status !== searchFilters.rsvpStatus) { return false } // Meal preference filter if (searchFilters.mealPreference && guest.meal_preference !== searchFilters.mealPreference) { return false } // Owner filter if (searchFilters.owner) { if (searchFilters.owner === 'self-service' && guest.owner_email !== 'self-service') { return false } else if (searchFilters.owner !== 'self-service' && guest.owner_email !== searchFilters.owner) { return false } } return true }) const stats = { total: guests.length, confirmed: guests.filter(g => g.rsvp_status === 'confirmed').length, declined: guests.filter(g => g.rsvp_status === 'declined').length, invited: guests.filter(g => g.rsvp_status === 'invited').length, } const exportToExcel = () => { const exportData = guests.map(guest => ({ 'First Name': guest.first_name, 'Last Name': guest.last_name, 'Email': guest.email || '', 'Phone': guest.phone_number || '', 'RSVP Status': guest.rsvp_status, 'Meal Preference': guest.meal_preference || '', 'Plus One': guest.has_plus_one ? 'Yes' : 'No', 'Plus One Name': guest.plus_one_name || '', 'Table Number': guest.table_number || '', 'Notes': guest.notes || '' })) const ws = XLSX.utils.json_to_sheet(exportData) ws['!cols'] = [ { wch: 15 }, // First Name { wch: 15 }, // Last Name { wch: 25 }, // Email { wch: 15 }, // Phone { wch: 15 }, // RSVP Status { wch: 15 }, // Meal Preference { wch: 10 }, // Plus One { wch: 15 }, // Plus One Name { wch: 12 }, // Table Number { wch: 20 } // Notes ] const wb = XLSX.utils.book_new() XLSX.utils.book_append_sheet(wb, ws, 'Guests') const date = new Date().toISOString().split('T')[0] const fileName = `guest-list-${date}.xlsx` XLSX.writeFile(wb, fileName) } const handleSendWhatsApp = async (data) => { if (selectedGuestIds.size === 0) { alert(he.noGuestsSelected) return null } try { const selectedGuests = filteredGuests.filter(g => selectedGuestIds.has(g.id)) const result = await sendWhatsAppInvitationToGuests( eventId, Array.from(selectedGuestIds), data.formData, data.templateKey || 'wedding_invitation', data.extraParams || null ) // Clear selection after successful send setSelectedGuestIds(new Set()) return result } catch (err) { console.error('Failed to send WhatsApp invitations:', err) throw err } } if (eventNotFound) { return (
האירוע שביקשת אינו קיים או שאין לך הרשאה לצפות בו.
{he.noGuestsFound}
| 0} onChange={toggleSelectAll} title={he.selectAll} /> | {he.name} | {he.phone} | {he.email} | {he.rsvpStatus} | {he.mealPref} | {he.plusOne} | {he.actions} |
|---|---|---|---|---|---|---|---|
| toggleGuestSelection(guest.id)} /> | {guest.first_name} {guest.last_name} | {guest.phone_number || '-'} | {guest.email || '-'} | {he[guest.rsvp_status] || guest.rsvp_status} | {guest.meal_preference || '-'} | {guest.plus_one_name || (guest.has_plus_one ? 'Yes (not named)' : 'No')} |