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 axios from 'axios'; interface RSVP { id: number; name: string; attending: string; bringing_guests: string; guest_count: number; guest_names: string; items_bringing: string[] | 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; } 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: [] as string[], }); 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 [eventResponse, rsvpsResponse] = await Promise.all([ axios.get(`/api/events/${slug}`), axios.get(`/api/events/${slug}/rsvps`) ]); 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) { 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, bringing_guests: rsvp.bringing_guests, guest_count: rsvp.guest_count, guest_names: rsvp.guest_names, items_bringing: Array.isArray(rsvp.items_bringing) ? rsvp.items_bringing : [], }); setEditDialogOpen(true); }; const handleTextInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setEditForm((prev: typeof editForm) => ({ ...prev, [name]: 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 = Array.isArray(value) ? value : []; setEditForm(prev => ({ ...prev, items_bringing: newItems })); }; const handleEditSubmit = async () => { if (!rsvpToEdit) return; try { const submissionData = { ...editForm, items_bringing: JSON.stringify(editForm.items_bringing) }; await axios.put(`/api/events/${slug}/rsvps/${rsvpToEdit.id}`, submissionData); // Update the local state const updatedRsvps = rsvps.map((r: RSVP) => { if (r.id === rsvpToEdit.id) { return { ...r, ...editForm, items_bringing: editForm.items_bringing // Keep as array in local state }; } return r; }); // Recalculate claimed items const claimed = new Set(); 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)); setRsvps(updatedRsvps); setNeededItems(availableItems); setClaimedItems(claimedArray); setEditDialogOpen(false); setRsvpToEdit(null); } catch (error) { setError('Failed to update RSVP'); } }; 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}) Name Attending Guests Items Bringing Actions {rsvps.map((rsvp: RSVP) => ( {rsvp.name} {rsvp.attending.charAt(0).toUpperCase() + rsvp.attending.slice(1)} {rsvp.bringing_guests === 'yes' ? `${rsvp.guest_count} (${rsvp.guest_names.replace(/\s+/g, ', ')})` : '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? 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;