import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Box, Paper, Typography, Button, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, Container, Dialog, DialogTitle, DialogContent, DialogActions, TextField, FormControl, InputLabel, Select, MenuItem, FormControlLabel, Switch, SelectChangeEvent, OutlinedInput, ListItemText, Checkbox, Chip, } from '@mui/material'; import DeleteIcon from '@mui/icons-material/Delete'; import EditIcon from '@mui/icons-material/Edit'; import AddIcon from '@mui/icons-material/Add'; import WallpaperIcon from '@mui/icons-material/Wallpaper'; import axios from 'axios'; interface RSVP { id: number; name: string; attending: 'yes' | 'no' | 'maybe'; bringing_guests: 'yes' | 'no'; guest_count: number; guest_names: string; items_bringing: string[] | string; event_id?: number; created_at?: string; updated_at?: string; } interface Event { id: number; title: string; description: string; date: string; location: string; slug: string; needed_items?: string[] | string; wallpaper?: string; rsvp_cutoff_date?: string; } interface EditFormData { name: string; attending: 'yes' | 'no' | 'maybe'; bringing_guests: 'yes' | 'no'; guest_count: number; guest_names: string; items_bringing: string[]; } const EventAdmin: React.FC = () => { const { slug } = useParams<{ slug: string }>(); const navigate = useNavigate(); const [event, setEvent] = useState(null); const [rsvps, setRsvps] = useState([]); const [neededItems, setNeededItems] = useState([]); const [claimedItems, setClaimedItems] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [rsvpToDelete, setRsvpToDelete] = useState(null); const [editDialogOpen, setEditDialogOpen] = useState(false); const [rsvpToEdit, setRsvpToEdit] = useState(null); const [editForm, setEditForm] = useState({ name: '', attending: 'yes', bringing_guests: 'no', guest_count: 0, guest_names: '', items_bringing: [] }); const [deleteEventDialogOpen, setDeleteEventDialogOpen] = useState(false); const [manageItemsDialogOpen, setManageItemsDialogOpen] = useState(false); const [newItem, setNewItem] = useState(''); const [itemToDelete, setItemToDelete] = useState(null); const [updateInfoDialogOpen, setUpdateInfoDialogOpen] = useState(false); const [updateForm, setUpdateForm] = useState({ description: '', location: '', date: '', rsvp_cutoff_date: '', wallpaper: null as File | null }); useEffect(() => { fetchEventAndRsvps(); }, [slug]); const fetchEventAndRsvps = async () => { try { const config = { headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }; const [eventResponse, rsvpsResponse] = await Promise.all([ axios.get(`/api/events/${slug}`, config), axios.get(`/api/events/${slug}/rsvps`, config) ]); if (!eventResponse.data || !rsvpsResponse.data) { throw new Error('Failed to fetch data from server'); } setEvent(eventResponse.data); // Process needed items let items: string[] = []; if (eventResponse.data.needed_items) { try { if (typeof eventResponse.data.needed_items === 'string') { const parsed = JSON.parse(eventResponse.data.needed_items); items = Array.isArray(parsed) ? parsed : []; } else if (Array.isArray(eventResponse.data.needed_items)) { items = eventResponse.data.needed_items; } } catch (e) { console.error('Error parsing needed_items:', e); items = []; } } // Get all claimed items from existing RSVPs const claimed = new Set(); const processedRsvps = rsvpsResponse.data.map((rsvp: RSVP) => { let itemsBringing: string[] = []; try { if (typeof rsvp.items_bringing === 'string') { try { const parsed = JSON.parse(rsvp.items_bringing); itemsBringing = Array.isArray(parsed) ? parsed : []; } catch (e) { console.error('Error parsing items_bringing JSON:', e); } } else if (Array.isArray(rsvp.items_bringing)) { itemsBringing = rsvp.items_bringing; } // Add items to claimed set itemsBringing.forEach(item => claimed.add(item)); } catch (e) { console.error('Error processing items for RSVP:', e); } return { ...rsvp, items_bringing: itemsBringing }; }); // Update state with processed data setRsvps(processedRsvps); setClaimedItems(Array.from(claimed)); // Filter needed items to only show unclaimed ones setNeededItems(items.filter(item => !claimed.has(item))); setLoading(false); } catch (error) { console.error('Failed to load event data:', error); if (axios.isAxiosError(error)) { console.error('Server error details:', { status: error.response?.status, statusText: error.response?.statusText, data: error.response?.data }); } setError('Failed to load event data'); setLoading(false); } }; const handleDeleteRsvp = async (rsvp: RSVP) => { setRsvpToDelete(rsvp); setDeleteDialogOpen(true); }; const confirmDelete = async () => { if (!rsvpToDelete) return; try { await axios.delete(`/api/events/${slug}/rsvps/${rsvpToDelete.id}`); setRsvps(rsvps.filter((r: RSVP) => r.id !== rsvpToDelete.id)); setDeleteDialogOpen(false); setRsvpToDelete(null); } catch (error) { setError('Failed to delete RSVP'); } }; const handleEditRsvp = (rsvp: RSVP) => { setRsvpToEdit(rsvp); setEditForm({ name: rsvp.name, attending: rsvp.attending || 'yes', bringing_guests: rsvp.bringing_guests || 'no', guest_count: typeof rsvp.guest_count === 'number' ? rsvp.guest_count : 0, guest_names: rsvp.guest_names || '', items_bringing: Array.isArray(rsvp.items_bringing) ? rsvp.items_bringing : typeof rsvp.items_bringing === 'string' ? (rsvp.items_bringing ? JSON.parse(rsvp.items_bringing) : []) : [] }); setEditDialogOpen(true); }; const handleTextInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setEditForm(prev => ({ ...prev, [name]: name === 'guest_count' ? Math.max(0, parseInt(value) || 0) : value })); }; const handleSelectChange = (e: SelectChangeEvent) => { const { name, value } = e.target; setEditForm((prev: typeof editForm) => ({ ...prev, [name as string]: value, })); }; const handleItemsChange = (e: SelectChangeEvent) => { const { value } = e.target; const newItems = typeof value === 'string' ? value.split(',') : value; setEditForm(prev => ({ ...prev, items_bringing: newItems })); }; const handleEditSubmit = async () => { if (!rsvpToEdit || !event) return; try { // Prepare submission data in the exact format the backend expects const submissionData = { name: editForm.name, attending: editForm.attending, bringing_guests: editForm.bringing_guests, guest_count: parseInt(editForm.guest_count.toString(), 10), guest_names: editForm.guest_names, items_bringing: JSON.stringify(editForm.items_bringing), event_id: event.id // Ensure event_id is included }; console.log('Submitting RSVP update:', { endpoint: `/api/events/${slug}/rsvps/${rsvpToEdit.id}`, data: submissionData }); // Update the RSVP const response = await axios({ method: 'put', url: `/api/events/${slug}/rsvps/${rsvpToEdit.id}`, data: submissionData, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }); console.log('Server response:', { status: response.status, statusText: response.statusText, data: response.data, headers: response.headers }); // Handle successful update (both 200 and 204 responses) if (response.status === 204 || response.status === 200) { // Create updated RSVP object using our submitted data since 204 returns no content const updatedRsvp: RSVP = { ...rsvpToEdit, ...submissionData, items_bringing: editForm.items_bringing // Keep as array in local state }; // Update the local state setRsvps(prevRsvps => prevRsvps.map((r: RSVP) => r.id === rsvpToEdit.id ? updatedRsvp : r )); // Recalculate claimed items const claimed = new Set(); const updatedRsvps = rsvps.map((r: RSVP) => r.id === rsvpToEdit.id ? updatedRsvp : r ); updatedRsvps.forEach((rsvp: RSVP) => { let rsvpItems: string[] = []; try { if (typeof rsvp.items_bringing === 'string') { const parsed = JSON.parse(rsvp.items_bringing); rsvpItems = Array.isArray(parsed) ? parsed : []; } else if (Array.isArray(rsvp.items_bringing)) { rsvpItems = rsvp.items_bringing; } rsvpItems.forEach(item => claimed.add(item)); } catch (e) { console.error('Error processing items for RSVP:', e); } }); // Get all items from the event let allItems: string[] = []; if (event?.needed_items) { try { if (typeof event.needed_items === 'string') { const parsed = JSON.parse(event.needed_items); allItems = Array.isArray(parsed) ? parsed : []; } else if (Array.isArray(event.needed_items)) { allItems = event.needed_items; } } catch (e) { console.error('Error parsing event needed_items:', e); allItems = []; } } // Update needed and claimed items const claimedArray = Array.from(claimed); const availableItems = allItems.filter(item => !claimed.has(item)); setNeededItems(availableItems); setClaimedItems(claimedArray); setEditDialogOpen(false); setRsvpToEdit(null); // Verify the update was successful but don't throw error if verification response is empty try { const verifyResponse = await axios.get(`/api/events/${slug}/rsvps`, { headers: { 'Accept': 'application/json' } }); console.log('Verification response:', verifyResponse.data); } catch (verifyError) { console.warn('Verification request failed, but update may still be successful:', verifyError); } } else { throw new Error(`Unexpected response status: ${response.status}`); } } catch (error) { console.error('Error in handleEditSubmit:', error); setError('Failed to update RSVP. Please try again.'); // Only refresh data if we get a specific error indicating the RSVP doesn't exist if (axios.isAxiosError(error) && error.response?.status === 404) { await fetchEventAndRsvps(); } } }; const handleDeleteEvent = async () => { try { await axios.delete(`/api/events/${slug}`); navigate('/'); } catch (error) { setError('Failed to delete event'); setDeleteEventDialogOpen(false); } }; const handleAddItem = async () => { if (!event || !newItem.trim()) return; try { const updatedItems = [...(Array.isArray(event.needed_items) ? event.needed_items : []), newItem.trim()]; await axios.put(`/api/events/${slug}`, { ...event, needed_items: updatedItems }); setEvent(prev => prev ? { ...prev, needed_items: updatedItems } : null); setNeededItems(prev => [...prev, newItem.trim()]); setNewItem(''); } catch (error) { setError('Failed to add item'); } }; const handleRemoveItem = async (itemToRemove: string) => { if (!event) return; try { // Update event's needed_items const updatedItems = Array.isArray(event.needed_items) ? event.needed_items.filter(item => item !== itemToRemove) : []; await axios.put(`/api/events/${slug}`, { ...event, needed_items: updatedItems }); // Update RSVPs to remove the item from any that had claimed it const updatedRsvps = rsvps.map(rsvp => { let currentItems: string[] = Array.isArray(rsvp.items_bringing) ? rsvp.items_bringing : typeof rsvp.items_bringing === 'string' ? JSON.parse(rsvp.items_bringing) : []; // Remove the item if it exists in this RSVP if (currentItems.includes(itemToRemove)) { const updatedRsvpItems = currentItems.filter((item: string) => item !== itemToRemove); // Update the RSVP in the database axios.put(`/api/events/${slug}/rsvps/${rsvp.id}`, { ...rsvp, items_bringing: JSON.stringify(updatedRsvpItems) }); return { ...rsvp, items_bringing: updatedRsvpItems }; } return rsvp; }); // Recalculate claimed items const claimed = new Set(); updatedRsvps.forEach(rsvp => { let rsvpItems: string[] = Array.isArray(rsvp.items_bringing) ? rsvp.items_bringing : typeof rsvp.items_bringing === 'string' ? JSON.parse(rsvp.items_bringing) : []; rsvpItems.forEach((item: string) => claimed.add(item)); }); // Update all state setEvent(prev => prev ? { ...prev, needed_items: updatedItems } : null); setNeededItems(prev => prev.filter((item: string) => item !== itemToRemove)); setRsvps(updatedRsvps); setClaimedItems(Array.from(claimed)); } catch (error) { setError('Failed to remove item'); } }; const handleUpdateInfoClick = () => { if (!event) return; setUpdateForm({ description: event.description, location: event.location, date: event.date.slice(0, 16), // Format date for datetime-local input rsvp_cutoff_date: event.rsvp_cutoff_date ? event.rsvp_cutoff_date.slice(0, 16) : '', wallpaper: null }); setUpdateInfoDialogOpen(true); }; const handleUpdateInfoSubmit = async () => { if (!event) return; try { // Create FormData and append all fields const formData = new FormData(); formData.append('description', updateForm.description); formData.append('location', updateForm.location); formData.append('date', updateForm.date); formData.append('rsvp_cutoff_date', updateForm.rsvp_cutoff_date); formData.append('title', event.title); // Keep existing title formData.append('needed_items', JSON.stringify(event.needed_items)); // Keep existing needed items // Append wallpaper if a new one was selected if (updateForm.wallpaper) { formData.append('wallpaper', updateForm.wallpaper); } const response = await axios.put(`/api/events/${slug}`, formData, { headers: { 'Content-Type': 'multipart/form-data', }, }); setEvent(prev => prev ? { ...prev, description: updateForm.description, location: updateForm.location, date: updateForm.date, rsvp_cutoff_date: updateForm.rsvp_cutoff_date, wallpaper: response.data.wallpaper || prev.wallpaper } : null); setUpdateInfoDialogOpen(false); } catch (error) { console.error('Error updating event:', error); setError('Failed to update event information'); } }; const handleWallpaperChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { setUpdateForm(prev => ({ ...prev, wallpaper: e.target.files![0] })); } }; if (loading) { return ( Loading... ); } if (error || !event) { return ( {error || 'Event not found'} ); } return ( {event.title} - Admin Info: {event.description || 'None'} Location: {event.location} Date: {new Date(event.date).toLocaleString()} {event.rsvp_cutoff_date && ( RSVP cut-off date: {new Date(event.rsvp_cutoff_date).toLocaleString()} )} {/* Add items status section */} Items Status Still Needed: {neededItems.map((item: string, index: number) => ( ))} {neededItems.length === 0 && ( All items have been claimed )} Claimed Items: {claimedItems.map((item: string, index: number) => ( ))} {claimedItems.length === 0 && ( No items have been claimed yet )} RSVPs ({rsvps.length}) | Total Guests: {rsvps.reduce((total, rsvp) => { // Count the RSVP person as 1 if they're attending const rsvpCount = rsvp.attending === 'yes' ? 1 : 0; // Add their guests if they're bringing any const guestCount = (rsvp.attending === 'yes' && rsvp.bringing_guests === 'yes') ? rsvp.guest_count : 0; return total + rsvpCount + guestCount; }, 0)} Name Attending Guests Items Bringing Actions {rsvps.map((rsvp: RSVP) => ( {rsvp.name || 'No name provided'} {rsvp.attending ? rsvp.attending.charAt(0).toUpperCase() + rsvp.attending.slice(1) : 'Unknown' } {rsvp.bringing_guests === 'yes' ? `${rsvp.guest_count || 0} (${rsvp.guest_names ? rsvp.guest_names.replace(/\s+/g, ', ') : 'No names provided'})` : 'No' } {(() => { let items: string[] = []; try { if (typeof rsvp.items_bringing === 'string') { try { const parsed = JSON.parse(rsvp.items_bringing); items = Array.isArray(parsed) ? parsed : []; } catch (e) { console.error('Error parsing items_bringing JSON in table:', e); } } else if (Array.isArray(rsvp.items_bringing)) { items = rsvp.items_bringing; } } catch (e) { console.error('Error processing items in table:', e); } return items.length > 0 ? items.map((item: string, index: number) => ( )) : ( No items ); })()} handleEditRsvp(rsvp)} sx={{ mr: 1 }} > handleDeleteRsvp(rsvp)} > ))}
setDeleteDialogOpen(false)} > Delete RSVP Are you sure you want to delete {rsvpToDelete?.name}'s RSVP? setEditDialogOpen(false)} maxWidth="sm" fullWidth > Edit RSVP Attending Bringing Guests {editForm.bringing_guests === 'yes' && ( <> )} What items are you bringing? multiple name="items_bringing" value={editForm.items_bringing} onChange={handleItemsChange} input={} renderValue={(selected) => ( {selected.map((value: string) => ( ))} )} > {Array.from(new Set([...neededItems, ...editForm.items_bringing])).map((item: string) => ( ))} setDeleteEventDialogOpen(false)} > Delete Event Are you sure you want to delete "{event.title}"? This action cannot be undone. All RSVPs associated with this event will also be deleted. setManageItemsDialogOpen(false)} maxWidth="sm" fullWidth > Manage Needed Items setNewItem(e.target.value)} fullWidth /> Current Items: {event?.needed_items && Array.isArray(event.needed_items) && event.needed_items.map((item, index) => ( handleRemoveItem(item)} color={claimedItems.includes(item) ? "success" : "primary"} /> ))} setUpdateInfoDialogOpen(false)} maxWidth="sm" fullWidth > Update Event Information setUpdateForm(prev => ({ ...prev, description: e.target.value }))} fullWidth multiline rows={3} /> setUpdateForm(prev => ({ ...prev, location: e.target.value }))} fullWidth /> setUpdateForm(prev => ({ ...prev, date: e.target.value }))} fullWidth InputLabelProps={{ shrink: true, }} /> setUpdateForm(prev => ({ ...prev, rsvp_cutoff_date: e.target.value }))} fullWidth InputLabelProps={{ shrink: true, }} /> Wallpaper {event.wallpaper && ( Current wallpaper: )} {updateForm.wallpaper && ( Selected file: {updateForm.wallpaper.name} )}
); }; export default EventAdmin;