tracker-test / guests.py
umangchaudhry's picture
Upload 11 files
64b6fa7 verified
import streamlit as st
import json
import pandas as pd
import io
import hashlib
import plotly.graph_objects as go
from datetime import datetime
from config_manager import ConfigManager
class GuestManager:
def __init__(self):
self.config_manager = ConfigManager()
self.guest_list_data = None
self.rsvp_data = None
def render(self, config):
st.markdown("## 👥 Guest Management")
# Load current data
self.load_data()
# Import section
st.markdown("### Import Data")
import_type = st.radio("Import Type", ["Guest List (CSV)", "RSVP Responses (CSV)"], horizontal=True)
if import_type == "Guest List (CSV)":
uploaded_file = st.file_uploader("Upload Guest List (CSV)", type=['csv'], help="Upload a CSV file with guest data")
else:
uploaded_file = st.file_uploader("Upload RSVP Responses (CSV)", type=['csv'], help="Upload a CSV file with RSVP responses")
if uploaded_file is not None:
try:
df = pd.read_csv(uploaded_file)
if import_type == "Guest List (CSV)":
self.process_guest_list_csv(df)
st.success("Guest list processed successfully!")
st.rerun()
else:
self.process_rsvp_csv(df, config)
st.success("RSVP data processed successfully!")
st.rerun()
except Exception as e:
st.error(f"Error processing CSV: {e}")
# Display current data
if self.guest_list_data is not None:
self.render_guest_table(config)
else:
st.info("Please upload a guest list CSV file to get started.")
def get_confirmed_attendees(self, guests):
"""Get list of guests who have RSVPed 'Yes' to at least one event"""
confirmed = []
for guest in guests:
rsvp_by_event = guest.get('rsvp_by_event', {})
if any(status == 'Yes' for status in rsvp_by_event.values()):
confirmed.append(guest)
return confirmed
def render_confirmed_attendees_view(self, confirmed_guests, config):
"""Render detailed view for confirmed attendees with stats"""
# Create tabs for different views
tab1, tab2, tab3, tab4 = st.tabs(["Guest List", "Attendance by Event", "Meal Choices", "Summary Stats"])
with tab1:
self.render_guests_table(confirmed_guests, config)
with tab2:
st.markdown("### Attendance by Event for Confirmed Guests")
self.render_attendance_chart(confirmed_guests, config)
with tab3:
st.markdown("### Meal Choices for Confirmed Guests")
self.render_meal_choices_chart(confirmed_guests, config)
with tab4:
st.markdown("### Summary Statistics")
self.render_confirmed_attendees_stats(confirmed_guests, config)
def render_attendance_chart(self, confirmed_guests, config):
"""Render attendance chart for confirmed guests with guest list functionality"""
wedding_events = config.get('wedding_events', [])
# Count attendance for each event
event_attendance = {}
for event in wedding_events:
event_name = event.get('name', '')
if event_name:
event_attendance[event_name] = {'Yes': 0, 'No': 0, 'Pending': 0}
for guest in confirmed_guests:
rsvp_by_event = guest.get('rsvp_by_event', {})
for event_name in event_attendance.keys():
rsvp_status = rsvp_by_event.get(event_name, 'Pending')
if rsvp_status in event_attendance[event_name]:
event_attendance[event_name][rsvp_status] += 1
else:
event_attendance[event_name]['Pending'] += 1
# Create bar chart
event_names = list(event_attendance.keys())
yes_counts = [event_attendance[event]['Yes'] for event in event_names]
no_counts = [event_attendance[event]['No'] for event in event_names]
pending_counts = [event_attendance[event]['Pending'] for event in event_names]
fig = go.Figure(data=[
go.Bar(name='Yes', x=event_names, y=yes_counts, marker_color='#4a7c59'),
go.Bar(name='No', x=event_names, y=no_counts, marker_color='#d32f2f'),
go.Bar(name='Pending', x=event_names, y=pending_counts, marker_color='#ffa726')
])
fig.update_layout(
title="Event Attendance for Confirmed Guests",
barmode='stack',
xaxis_tickangle=-45,
height=400
)
st.plotly_chart(fig, use_container_width=True)
# Add guest list functionality below the chart
st.markdown("---")
st.markdown("### 📋 View Guest Lists by Event")
if event_names:
# Create columns for event selection and guest list display
col1, col2 = st.columns([1, 2])
with col1:
selected_event = st.selectbox(
"Select Event to View Guest List:",
event_names,
help="Choose an event to see the list of guests who confirmed attendance"
)
# Show attendance summary for selected event
if selected_event in event_attendance:
st.markdown("#### Attendance Summary")
st.metric("Confirmed", event_attendance[selected_event]['Yes'])
st.metric("Declined", event_attendance[selected_event]['No'])
st.metric("Pending", event_attendance[selected_event]['Pending'])
with col2:
if selected_event:
self.render_event_guest_list(confirmed_guests, selected_event, config)
else:
st.info("No events configured yet.")
def render_event_guest_list(self, confirmed_guests, selected_event, config):
"""Render the list of guests who confirmed for a specific event"""
st.markdown(f"#### Guest List for {selected_event}")
# Get guests who confirmed for this event
confirmed_for_event = []
declined_for_event = []
pending_for_event = []
for guest in confirmed_guests:
rsvp_by_event = guest.get('rsvp_by_event', {})
rsvp_status = rsvp_by_event.get(selected_event, 'Pending')
if rsvp_status == 'Yes':
confirmed_for_event.append(guest)
elif rsvp_status == 'No':
declined_for_event.append(guest)
else:
pending_for_event.append(guest)
# Create tabs for different RSVP statuses
tab1, tab2, tab3 = st.tabs([
f"✅ Confirmed ({len(confirmed_for_event)})",
f"❌ Declined ({len(declined_for_event)})",
f"⏳ Pending ({len(pending_for_event)})"
])
with tab1:
if confirmed_for_event:
self.render_guest_list_table(confirmed_for_event, selected_event, config, "confirmed")
else:
st.info("No guests have confirmed for this event yet.")
with tab2:
if declined_for_event:
self.render_guest_list_table(declined_for_event, selected_event, config, "declined")
else:
st.info("No guests have declined this event.")
with tab3:
if pending_for_event:
self.render_guest_list_table(pending_for_event, selected_event, config, "pending")
else:
st.info("All guests have responded to this event.")
def render_guest_list_table(self, guests, event_name, config, status="confirmed"):
"""Render a table of guests for a specific event"""
if not guests:
return
# Create data for the table
table_data = []
for guest in guests:
first_name = guest.get('first_name', '')
last_name = guest.get('last_name', '')
# Handle guests with "nan" names
if first_name in ['nan', ''] and last_name in ['nan', '']:
first_name = '[Unassigned Plus One]'
last_name = ''
# Get meal selection for this event
meal_selections = guest.get('meal_selections', {})
meal_choice = meal_selections.get(event_name, 'Not Selected')
# Check if this event requires meal choice
wedding_events = config.get('wedding_events', [])
event_requires_meal = False
for event in wedding_events:
if event.get('name') == event_name and event.get('requires_meal_choice', False):
event_requires_meal = True
break
row = {
'Name': f"{first_name} {last_name}".strip(),
'Group': guest.get('group', ''),
'Party': guest.get('party', ''),
'Phone': guest.get('phone', ''),
'Plus One Name': guest.get('plus_one_name', '')
}
# Add meal choice column only if event requires meal choice
if event_requires_meal:
row['Meal Choice'] = meal_choice
table_data.append(row)
if table_data:
df = pd.DataFrame(table_data)
# Display the table
st.dataframe(
df,
use_container_width=True,
hide_index=True,
height=min(400, len(table_data) * 35 + 50) # Dynamic height based on number of guests
)
# Add export functionality
csv_data = df.to_csv(index=False)
st.download_button(
label=f"📥 Download {event_name} Guest List (CSV)",
data=csv_data,
file_name=f"{event_name.replace(' ', '_')}_{status}_guest_list.csv",
mime="text/csv",
key=f"download_{event_name.replace(' ', '_')}_{status}_{len(guests)}"
)
def render_meal_choices_chart(self, confirmed_guests, config):
"""Render meal choices chart for confirmed guests"""
wedding_events = config.get('wedding_events', [])
# Get events that require meal choices
meal_events = [event for event in wedding_events if event.get('requires_meal_choice', False)]
if not meal_events:
st.info("No events require meal choices.")
return
for event in meal_events:
event_name = event.get('name', '')
meal_options = event.get('meal_options', [])
if not meal_options:
continue
st.markdown(f"#### {event_name}")
# Count meal choices for this event
meal_counts = {option: 0 for option in meal_options}
meal_counts['Not Selected'] = 0
for guest in confirmed_guests:
meal_selections = guest.get('meal_selections', {})
selected_meal = meal_selections.get(event_name, 'Not Selected')
if selected_meal in meal_counts:
meal_counts[selected_meal] += 1
else:
meal_counts['Not Selected'] += 1
# Create pie chart
fig = go.Figure(data=[go.Pie(
labels=list(meal_counts.keys()),
values=list(meal_counts.values()),
marker_colors=['#4a7c59', '#ff9800', '#2196f3', '#9c27b0', '#f44336', '#ffa726']
)])
fig.update_layout(title=f"Meal Choices for {event_name}")
st.plotly_chart(fig, use_container_width=True)
def render_confirmed_attendees_stats(self, confirmed_guests, config):
"""Render summary statistics for confirmed attendees"""
wedding_events = config.get('wedding_events', [])
# Basic stats
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("Total Confirmed", len(confirmed_guests))
with col2:
plus_ones = sum(1 for guest in confirmed_guests if guest.get('plus_one', False))
st.metric("With Plus Ones", plus_ones)
with col3:
total_attending = len(confirmed_guests) + plus_ones
st.metric("Total Attending", total_attending)
with col4:
# Calculate average attendance across all events
total_yes = 0
total_possible = len(confirmed_guests) * len(wedding_events)
for guest in confirmed_guests:
rsvp_by_event = guest.get('rsvp_by_event', {})
total_yes += sum(1 for status in rsvp_by_event.values() if status == 'Yes')
avg_attendance = (total_yes / total_possible * 100) if total_possible > 0 else 0
st.metric("Avg Event Attendance", f"{avg_attendance:.1f}%")
# Event-specific stats
st.markdown("#### Event Attendance Summary")
event_stats = []
for event in wedding_events:
event_name = event.get('name', '')
yes_count = 0
for guest in confirmed_guests:
rsvp_by_event = guest.get('rsvp_by_event', {})
if rsvp_by_event.get(event_name) == 'Yes':
yes_count += 1
attendance_rate = (yes_count / len(confirmed_guests) * 100) if confirmed_guests else 0
event_stats.append({
'Event': event_name,
'Attending': yes_count,
'Attendance Rate': f"{attendance_rate:.1f}%"
})
if event_stats:
stats_df = pd.DataFrame(event_stats)
st.dataframe(stats_df, use_container_width=True, hide_index=True)
def convert_csv_to_guests(self, df):
"""Convert CSV data to guest format"""
guest_data = []
for _, row in df.iterrows():
first_name = str(row.get('First Name (Empty Rows are +1s)', '')).strip()
last_name = str(row.get('Last Name', '')).strip()
group = str(row.get('Group', '')).strip()
group_size = str(row.get('Group Size', '')).strip()
party = str(row.get('Party', '')).strip()
street_address = str(row.get(' Street address', '')).strip()
apt_suite = str(row.get(' Apt/Suite', '')).strip()
city = str(row.get(' City', '')).strip()
state = str(row.get(' State', '')).strip()
zip_code = str(row.get(' ZIP', '')).strip()
country = str(row.get('Country', '')).strip()
# Skip completely empty rows
if not first_name and not last_name and not group:
continue
# Build full address
address_parts = []
if street_address and street_address != 'nan':
address_parts.append(street_address)
if apt_suite and apt_suite != 'nan':
address_parts.append(f"Apt {apt_suite}")
if city and city != 'nan':
address_parts.append(city)
if state and state != 'nan':
address_parts.append(state)
if zip_code and zip_code != 'nan':
address_parts.append(zip_code)
if country and country != 'nan':
address_parts.append(country)
full_address = ', '.join(address_parts) if address_parts else ''
# Determine if this is a plus one (empty first name but has party/group)
is_plus_one = not first_name and (group or party)
if is_plus_one:
# This is a plus one entry - create a guest entry with "nan" names
# The plus_one_name will be filled in from RSVP data
guest = {
'id': datetime.now().strftime("%Y%m%d_%H%M%S_%f"),
'first_name': 'nan',
'last_name': 'nan',
'party': party,
'group': group,
'group_size': group_size,
'phone': '',
'address': full_address, # Use full address as address info
'lodging': '', # Leave lodging empty for manual entry
'allergies': '',
'plus_one': False, # This will be updated from RSVP data
'plus_one_name': '', # This will be filled from RSVP data
'plus_one_phone': '',
'plus_one_allergies': '',
'rsvp_by_event': {},
'meal_selections': {},
'created_date': datetime.now().isoformat()
}
else:
# Create guest object for main guests
guest = {
'id': datetime.now().strftime("%Y%m%d_%H%M%S_%f"),
'first_name': first_name,
'last_name': last_name,
'party': party,
'group': group,
'group_size': group_size,
'phone': '',
'address': full_address, # Use full address as address info
'lodging': '', # Leave lodging empty for manual entry
'allergies': '',
'plus_one': False, # Will be updated from RSVP data
'plus_one_name': '',
'plus_one_phone': '',
'plus_one_allergies': '',
'rsvp_by_event': {},
'meal_selections': {},
'created_date': datetime.now().isoformat()
}
guest_data.append(guest)
return guest_data
def update_guests_with_rsvp(self, df, config):
"""Update existing guests with RSVP responses from CSV"""
import json
existing_guests = self.config_manager.load_json_data('guests.json')
updated_count = 0
# Track new meal options found in RSVP data
new_meal_options_by_event = {}
# Track which plus one names have been assigned to avoid duplicates across all parties
global_assigned_plus_ones = set()
for _, row in df.iterrows():
party_code = str(row.get('party_code', '')).strip()
name = str(row.get('name', '')).strip()
overall_rsvp = str(row.get('overall_rsvp', '')).strip()
bringing_plus_one = str(row.get('bringing_plus_one', '')).strip().lower() == 'yes'
party_attendees = str(row.get('party_attendees', '')).strip()
event_responses = str(row.get('event_responses', '')).strip()
dietary_restrictions = str(row.get('dietary_restrictions', '')).strip()
phone_number = str(row.get('phone_number', '')).strip()
# Skip empty rows
if not party_code and not name:
continue
# Parse party attendees to get all names in the party
party_attendee_names = []
if party_attendees and party_attendees != 'nan':
party_attendee_names = [name.strip() for name in party_attendees.split(';') if name.strip()]
# Parse dietary restrictions
dietary_dict = {}
if dietary_restrictions and dietary_restrictions != 'nan' and dietary_restrictions != '{}':
try:
dietary_dict = json.loads(dietary_restrictions)
except:
pass
# Parse event responses
event_dict = {}
if event_responses and event_responses != 'nan' and event_responses != '{}':
try:
event_dict = json.loads(event_responses)
except Exception as e:
st.warning(f"Could not parse event responses for {name}: {e}")
# First, ensure all party attendees exist as separate guest entries
# This handles cases where RSVP data includes people not in the original guest list
for attendee_name in party_attendee_names:
# Check if this attendee already exists as a guest
attendee_exists = False
for guest in existing_guests:
guest_name = f"{guest.get('first_name', '')} {guest.get('last_name', '')}".strip()
if attendee_name.lower() == guest_name.lower():
attendee_exists = True
break
# If attendee doesn't exist, create a new guest entry
if not attendee_exists:
# Parse the attendee name into first and last name
name_parts = attendee_name.strip().split()
if len(name_parts) >= 2:
first_name = name_parts[0]
last_name = ' '.join(name_parts[1:])
else:
first_name = attendee_name
last_name = ''
# Find the group/party info from existing guests with the same party code
group_name = party_code
party_name = 'Lara' # Default party
address = ''
group_size = '1'
for guest in existing_guests:
guest_group = guest.get('group', '').strip()
if party_code.lower() == guest_group.lower():
group_name = guest_group
party_name = guest.get('party', 'Lara')
address = guest.get('address', '')
group_size = guest.get('group_size', '1')
break
# Create new guest entry
new_guest = {
'id': datetime.now().strftime("%Y%m%d_%H%M%S_%f"),
'first_name': first_name,
'last_name': last_name,
'party': party_name,
'group': group_name,
'group_size': group_size,
'phone': phone_number if phone_number != 'nan' else '',
'address': address,
'lodging': '',
'allergies': dietary_dict.get(attendee_name, ''),
'plus_one': False, # This is a main guest, not a plus one
'plus_one_name': '',
'plus_one_phone': '',
'plus_one_allergies': '',
'rsvp_by_event': {},
'meal_selections': {},
'created_date': datetime.now().isoformat()
}
# Apply RSVP data to the new guest
if event_dict:
rsvp_by_event = {}
meal_selections = {}
for event_name, event_data in event_dict.items():
if isinstance(event_data, dict):
attendees = event_data.get('attendees', [])
party_rsvp = event_data.get('rsvp', 'Pending')
# Determine individual RSVP status
if attendee_name in attendees:
rsvp_status = party_rsvp
else:
rsvp_status = 'No'
rsvp_by_event[event_name] = rsvp_status
# Update meal choices if provided and guest is attending
if rsvp_status == 'Yes':
meal_choice = event_data.get('meal_choice', {})
if meal_choice and isinstance(meal_choice, dict) and attendee_name in meal_choice:
selected_meal = meal_choice[attendee_name]
meal_selections[event_name] = selected_meal
# Track new meal options for this event
if event_name not in new_meal_options_by_event:
new_meal_options_by_event[event_name] = set()
new_meal_options_by_event[event_name].add(selected_meal)
new_guest['rsvp_by_event'] = rsvp_by_event
new_guest['meal_selections'] = meal_selections
existing_guests.append(new_guest)
updated_count += 1
# Now update existing guests with RSVP data
# Find matching guest(s) by name or party
matching_guests = []
for guest in existing_guests:
guest_name = f"{guest.get('first_name', '')} {guest.get('last_name', '')}".strip()
guest_party = guest.get('party', '').strip()
guest_group = guest.get('group', '').strip()
# Match by exact name, party code, or exact group name (including guests with "nan" names)
# Use exact group matching to prevent partial matches (e.g., "Perrin" matching "Ben Perrin")
if (name and name.lower() in guest_name.lower()) or \
(party_code and (party_code.lower() == guest_party.lower() or party_code.lower() == guest_group.lower())):
matching_guests.append(guest)
# Track which plus one names have been assigned for this specific party
party_assigned_plus_ones = set()
# Update matching guests
for guest in matching_guests:
guest_name = f"{guest.get('first_name', '')} {guest.get('last_name', '')}".strip()
is_nan_guest = guest.get('first_name') == 'nan' and guest.get('last_name') == 'nan'
# Update phone if provided
if phone_number and phone_number != 'nan':
guest['phone'] = self._format_phone_number(phone_number)
# Handle guests with "nan" names (these are plus ones that need names)
if is_nan_guest and party_attendee_names:
# Find the plus one name that matches this guest's party/group
# Look for names that aren't the main guest name
main_guest_name = name # The main guest from RSVP data
plus_one_candidates = [name for name in party_attendee_names if name.lower() != main_guest_name.lower()]
# Find the first plus one candidate that hasn't been assigned yet for this party
plus_one_name = None
for candidate in plus_one_candidates:
if candidate not in party_assigned_plus_ones:
plus_one_name = candidate
party_assigned_plus_ones.add(candidate)
break
if plus_one_name:
# Parse the plus one name into first and last name
name_parts = plus_one_name.strip().split()
if len(name_parts) >= 2:
guest['first_name'] = name_parts[0]
guest['last_name'] = ' '.join(name_parts[1:]) # Join remaining parts as last name
else:
guest['first_name'] = plus_one_name
guest['last_name'] = ''
guest['plus_one'] = False # This is now a named guest
guest['plus_one_name'] = '' # Clear the plus one name field
# Update allergies for the plus one
if plus_one_name in dietary_dict:
guest['allergies'] = dietary_dict[plus_one_name]
# Apply party RSVP to this plus one
if event_dict:
rsvp_by_event = {}
meal_selections = {}
for event_name, event_data in event_dict.items():
if isinstance(event_data, dict):
# Check if this plus one is specifically listed as an attendee
attendees = event_data.get('attendees', [])
party_rsvp = event_data.get('rsvp', 'Pending')
# Determine individual RSVP status
if plus_one_name in attendees:
# Plus one is listed as attending, use party RSVP
rsvp_status = party_rsvp
else:
# Plus one is not listed as attending, mark as "No"
rsvp_status = 'No'
rsvp_by_event[event_name] = rsvp_status
# Update meal choices if provided and plus one is attending
if rsvp_status == 'Yes':
meal_choice = event_data.get('meal_choice', {})
if meal_choice and isinstance(meal_choice, dict) and plus_one_name in meal_choice:
selected_meal = meal_choice[plus_one_name]
meal_selections[event_name] = selected_meal
# Track new meal options for this event
if event_name not in new_meal_options_by_event:
new_meal_options_by_event[event_name] = set()
new_meal_options_by_event[event_name].add(selected_meal)
guest['rsvp_by_event'] = rsvp_by_event
guest['meal_selections'] = meal_selections
# Handle named guests (main guests) - but don't assign plus ones if they're separate guests
elif not is_nan_guest:
# Check if this guest should have a plus one
# Only assign plus one if there are attendees not already represented as separate guests
other_attendees = [name for name in party_attendee_names if name.lower() != guest_name.lower()]
# Check if any of these other attendees are already separate guests
separate_guests = []
for other_attendee in other_attendees:
for existing_guest in existing_guests:
existing_guest_name = f"{existing_guest.get('first_name', '')} {existing_guest.get('last_name', '')}".strip()
if other_attendee.lower() == existing_guest_name.lower():
separate_guests.append(other_attendee)
# Only assign plus one if there are attendees that aren't separate guests
plus_one_candidates = [name for name in other_attendees if name not in separate_guests]
if plus_one_candidates:
guest['plus_one'] = True
guest['plus_one_name'] = plus_one_candidates[0] # Take the first plus one
# Update plus one allergies
if plus_one_candidates[0] in dietary_dict:
guest['plus_one_allergies'] = dietary_dict[plus_one_candidates[0]]
else:
guest['plus_one'] = False
guest['plus_one_name'] = ''
# Update allergies for the main guest
if guest_name in dietary_dict:
guest['allergies'] = dietary_dict[guest_name]
# Apply party RSVP to main guest
if event_dict:
rsvp_by_event = guest.get('rsvp_by_event', {})
meal_selections = guest.get('meal_selections', {})
for event_name, event_data in event_dict.items():
if isinstance(event_data, dict):
# Check if this guest is specifically listed as an attendee
attendees = event_data.get('attendees', [])
party_rsvp = event_data.get('rsvp', 'Pending')
# Determine individual RSVP status
if guest_name in attendees:
# Guest is listed as attending, use party RSVP
rsvp_status = party_rsvp
else:
# Guest is not listed as attending, mark as "No"
rsvp_status = 'No'
rsvp_by_event[event_name] = rsvp_status
# Update meal choices if provided and guest is attending
if rsvp_status == 'Yes':
meal_choice = event_data.get('meal_choice', {})
if meal_choice and isinstance(meal_choice, dict) and guest_name in meal_choice:
selected_meal = meal_choice[guest_name]
meal_selections[event_name] = selected_meal
# Track new meal options for this event
if event_name not in new_meal_options_by_event:
new_meal_options_by_event[event_name] = set()
new_meal_options_by_event[event_name].add(selected_meal)
guest['rsvp_by_event'] = rsvp_by_event
guest['meal_selections'] = meal_selections
updated_count += 1
# Update event configuration with new meal options
config_updated = False
if new_meal_options_by_event:
wedding_events = config.get('wedding_events', [])
for event in wedding_events:
event_name = event.get('name', '')
if event_name in new_meal_options_by_event:
# Get existing meal options for this event
existing_meal_options = set(event.get('meal_options', []))
# Get new meal options from RSVP data
new_meal_options = new_meal_options_by_event[event_name]
# Merge existing and new meal options
all_meal_options = existing_meal_options.union(new_meal_options)
# Update the event configuration
event['meal_options'] = sorted(list(all_meal_options))
config_updated = True
# Save updated configuration if changes were made
if config_updated:
self.config_manager.save_config(config)
st.info(f"Updated event meal options with new choices from RSVP data")
# Clean up any remaining "nan" guests that weren't updated
# These are likely plus ones that don't have corresponding RSVP data
for guest in existing_guests:
if guest.get('first_name') == 'nan' and guest.get('last_name') == 'nan':
# If this nan guest still has no name, it means no RSVP data was provided
# We can either keep it as a placeholder or remove it
# For now, we'll keep it but mark it as inactive
guest['plus_one'] = False
guest['plus_one_name'] = ''
# Clean up duplicate guests
existing_guests = self.remove_duplicate_guests(existing_guests)
# Save updated guests
if updated_count > 0:
self.config_manager.save_json_data('guests.json', existing_guests)
return updated_count
def remove_duplicate_guests(self, guests):
"""Remove duplicate guests based on name and group"""
seen_guests = set()
unique_guests = []
for guest in guests:
first_name = guest.get('first_name', '').strip()
last_name = guest.get('last_name', '').strip()
group = guest.get('group', '').strip()
# Create a unique identifier for this guest
guest_key = f"{first_name.lower()}_{last_name.lower()}_{group.lower()}"
if guest_key not in seen_guests:
seen_guests.add(guest_key)
unique_guests.append(guest)
else:
# This is a duplicate - we can optionally log it or merge data
st.warning(f"Removed duplicate guest: {first_name} {last_name} from {group}")
return unique_guests
def render_guest_form(self, config):
"""Render form to add new guest"""
with st.form("guest_form"):
st.markdown("#### Basic Information")
col1, col2 = st.columns(2)
with col1:
first_name = st.text_input("First Name *", placeholder="Enter first name")
last_name = st.text_input("Last Name *", placeholder="Enter last name")
group = st.text_input("Group", placeholder="Enter group name")
party = st.text_input("Party/Family", placeholder="Enter party or family name")
with col2:
phone = st.text_input("Phone Number", placeholder="Enter phone number")
address = st.text_input("Address", placeholder="Home address")
lodging = st.text_input("Wedding Lodging", placeholder="Hotel, Airbnb, etc. for wedding")
allergies = st.text_area("Allergies/Dietary Restrictions", placeholder="List any allergies or dietary restrictions")
st.markdown("#### Plus One Information")
plus_one = st.checkbox("Bringing Plus One")
if plus_one:
col1, col2 = st.columns(2)
with col1:
plus_one_name = st.text_input("Plus One Name", placeholder="Enter plus one's name")
plus_one_phone = st.text_input("Plus One Phone", placeholder="Enter plus one's phone")
with col2:
plus_one_allergies = st.text_area("Plus One Allergies", placeholder="Plus one's allergies/dietary restrictions")
else:
plus_one_name = ""
plus_one_phone = ""
plus_one_allergies = ""
# RSVP and meal selections for each event
wedding_events = config.get('wedding_events', [])
if wedding_events:
st.markdown("#### RSVP & Meal Selections")
rsvp_by_event = {}
meal_selections = {}
for event in wedding_events:
event_name = event.get('name', '')
if event_name:
col1, col2 = st.columns(2)
with col1:
# RSVP selection
rsvp_options = ['Pending', 'Yes', 'No']
rsvp = st.selectbox(f"RSVP for {event_name}", rsvp_options, key=f"add_rsvp_{event_name}")
rsvp_by_event[event_name] = rsvp
with col2:
# Meal selection (only if event requires meal choice)
if event.get('requires_meal_choice', False):
meal_options = event.get('meal_options', [])
if meal_options:
meal_options_with_default = ['Not Selected'] + meal_options
meal = st.selectbox(f"Meal for {event_name}", meal_options_with_default, key=f"add_meal_{event_name}")
meal_selections[event_name] = meal
submitted = st.form_submit_button("Add Guest", type="primary")
if submitted:
if first_name and last_name:
new_guest = {
'id': datetime.now().strftime("%Y%m%d_%H%M%S_%f"),
'first_name': first_name,
'last_name': last_name,
'group': group,
'party': party,
'phone': phone,
'address': address,
'lodging': lodging,
'allergies': allergies,
'plus_one': plus_one,
'plus_one_name': plus_one_name,
'plus_one_phone': plus_one_phone,
'plus_one_allergies': plus_one_allergies,
'rsvp_by_event': rsvp_by_event if wedding_events else {},
'meal_selections': meal_selections if wedding_events else {},
'created_date': datetime.now().isoformat()
}
# Load existing guests and add new one
guests = self.config_manager.load_json_data('guests.json')
guests.append(new_guest)
if self.config_manager.save_json_data('guests.json', guests):
st.success("Guest added successfully!")
st.rerun()
else:
st.error("Error saving guest")
else:
st.error("Please enter at least first and last name")
def render_guests_table(self, guests, config):
"""Render guests in an editable table format"""
# Get wedding events for RSVP and meal options
wedding_events = config.get('wedding_events', [])
# Create DataFrame for display
df_data = []
for guest in guests:
# Handle guests with "nan" names (these are plus ones)
first_name = guest.get('first_name', '')
last_name = guest.get('last_name', '')
# If first/last name is "nan" or empty, this is likely an unassigned plus one
if first_name in ['nan', ''] and last_name in ['nan', '']:
# This is an unassigned plus one placeholder
first_name = '[Unassigned Plus One]'
last_name = ''
row = {
'First Name': first_name,
'Last Name': last_name,
'Group': guest.get('group', ''),
'Party': guest.get('party', ''),
'Phone': guest.get('phone', ''),
'Address': guest.get('address', ''),
'Wedding Lodging': guest.get('lodging', ''),
'Allergies': guest.get('allergies', ''),
'Plus One Allergies': guest.get('plus_one_allergies', '')
}
# Add RSVP columns for each event
rsvp_by_event = guest.get('rsvp_by_event', {})
for event in wedding_events:
event_name = event.get('name', '')
if event_name:
row[f'RSVP - {event_name}'] = rsvp_by_event.get(event_name, 'Pending')
# Add meal selection columns for events that require meal choice
meal_selections = guest.get('meal_selections', {})
for event in wedding_events:
if event.get('requires_meal_choice', False):
event_name = event.get('name', '')
if event_name:
row[f'Meal - {event_name}'] = meal_selections.get(event_name, 'Not Selected')
df_data.append(row)
if df_data:
df = pd.DataFrame(df_data)
# Display the table
st.dataframe(
df,
use_container_width=True,
hide_index=True,
height=400
)
# Edit/Delete guest section
st.markdown("### Manage Guest")
guest_ids = [f"{guest.get('first_name', '')} {guest.get('last_name', '')}" for guest in guests]
selected_guest_name = st.selectbox("Select Guest to Manage", guest_ids)
if selected_guest_name:
# Find the selected guest
selected_guest = None
for guest in guests:
if f"{guest.get('first_name', '')} {guest.get('last_name', '')}" == selected_guest_name:
selected_guest = guest
break
if selected_guest:
# Create tabs for edit and delete
tab1, tab2 = st.tabs(["Edit Guest", "Delete Guest"])
with tab1:
self.render_guest_edit_form(selected_guest, guests, config)
with tab2:
self.render_guest_delete_form(selected_guest, guests)
def render_guest_edit_form(self, guest, all_guests, config):
"""Render form to edit guest information"""
with st.form(f"edit_guest_{guest.get('id', '')}"):
col1, col2 = st.columns(2)
with col1:
first_name = st.text_input("First Name", value=guest.get('first_name', ''), key=f"edit_first_{guest.get('id', '')}")
last_name = st.text_input("Last Name", value=guest.get('last_name', ''), key=f"edit_last_{guest.get('id', '')}")
group = st.text_input("Group", value=guest.get('group', ''), key=f"edit_group_{guest.get('id', '')}")
party = st.text_input("Party/Family", value=guest.get('party', ''), key=f"edit_party_{guest.get('id', '')}")
phone = st.text_input("Phone Number", value=guest.get('phone', ''), key=f"edit_phone_{guest.get('id', '')}")
address = st.text_input("Address", value=guest.get('address', ''), key=f"edit_address_{guest.get('id', '')}")
lodging = st.text_input("Wedding Lodging", value=guest.get('lodging', ''), key=f"edit_lodging_{guest.get('id', '')}")
with col2:
allergies = st.text_area("Allergies/Dietary Restrictions", value=guest.get('allergies', ''), key=f"edit_allergies_{guest.get('id', '')}")
plus_one = st.checkbox("Bringing Plus One", value=guest.get('plus_one', False), key=f"edit_plus_one_{guest.get('id', '')}")
if plus_one:
plus_one_name = st.text_input("Plus One Name", value=guest.get('plus_one_name', ''), key=f"edit_plus_one_name_{guest.get('id', '')}")
plus_one_phone = st.text_input("Plus One Phone", value=guest.get('plus_one_phone', ''), key=f"edit_plus_one_phone_{guest.get('id', '')}")
plus_one_allergies = st.text_area("Plus One Allergies", value=guest.get('plus_one_allergies', ''), key=f"edit_plus_one_allergies_{guest.get('id', '')}")
else:
plus_one_name = ""
plus_one_phone = ""
plus_one_allergies = ""
# RSVP and meal selections for each event
st.markdown("#### RSVP & Meal Selections")
wedding_events = config.get('wedding_events', [])
rsvp_by_event = guest.get('rsvp_by_event', {})
meal_selections = guest.get('meal_selections', {})
for event in wedding_events:
event_name = event.get('name', '')
if event_name:
col1, col2 = st.columns(2)
with col1:
# RSVP selection
current_rsvp = rsvp_by_event.get(event_name, 'Pending')
rsvp_options = ['Pending', 'Yes', 'No']
rsvp_index = rsvp_options.index(current_rsvp) if current_rsvp in rsvp_options else 0
new_rsvp = st.selectbox(f"RSVP for {event_name}", rsvp_options, index=rsvp_index, key=f"edit_rsvp_{event_name}_{guest.get('id', '')}")
rsvp_by_event[event_name] = new_rsvp
with col2:
# Meal selection (only if event requires meal choice)
if event.get('requires_meal_choice', False):
meal_options = event.get('meal_options', [])
if meal_options:
current_meal = meal_selections.get(event_name, 'Not Selected')
meal_options_with_default = ['Not Selected'] + meal_options
meal_index = meal_options_with_default.index(current_meal) if current_meal in meal_options_with_default else 0
new_meal = st.selectbox(f"Meal for {event_name}", meal_options_with_default, index=meal_index, key=f"edit_meal_{event_name}_{guest.get('id', '')}")
meal_selections[event_name] = new_meal
submitted = st.form_submit_button("Save Changes", type="primary")
if submitted:
# Update guest data
guest['first_name'] = first_name
guest['last_name'] = last_name
guest['group'] = group
guest['party'] = party
guest['phone'] = phone
guest['address'] = address
guest['lodging'] = lodging
guest['allergies'] = allergies
guest['plus_one'] = plus_one
guest['plus_one_name'] = plus_one_name
guest['plus_one_phone'] = plus_one_phone
guest['plus_one_allergies'] = plus_one_allergies
guest['rsvp_by_event'] = rsvp_by_event
guest['meal_selections'] = meal_selections
# Save updated guests
if self.config_manager.save_json_data('guests.json', all_guests):
st.success("Guest updated successfully!")
st.rerun()
else:
st.error("Error saving guest changes")
def render_guest_delete_form(self, guest, all_guests):
"""Render form to delete guest"""
st.markdown("### ⚠️ Delete Guest")
# Show guest information for confirmation
st.markdown(f"**Guest to Delete:** {guest.get('first_name', '')} {guest.get('last_name', '')}")
st.markdown(f"**Party:** {guest.get('party', 'N/A')}")
st.markdown(f"**Phone:** {guest.get('phone', 'N/A')}")
if guest.get('plus_one', False):
st.markdown(f"**Plus One:** {guest.get('plus_one_name', 'N/A')}")
st.warning("⚠️ **Warning:** This action cannot be undone. All guest information, RSVP responses, and meal selections will be permanently deleted.")
# Confirmation checkbox
confirm_delete = st.checkbox("I understand this action cannot be undone", key=f"confirm_delete_{guest.get('id', '')}")
# Delete button
if st.button("🗑️ Delete Guest", type="secondary", disabled=not confirm_delete, key=f"delete_guest_{guest.get('id', '')}"):
# Remove guest from list
all_guests = [g for g in all_guests if g.get('id') != guest.get('id')]
# Save updated guests
if self.config_manager.save_json_data('guests.json', all_guests):
st.success(f"Guest {guest.get('first_name', '')} {guest.get('last_name', '')} has been deleted successfully!")
st.rerun()
else:
st.error("Error deleting guest")
def get_guest_summary(self, guests):
"""Get summary statistics for guests"""
total_guests = len(guests)
plus_ones = sum(1 for guest in guests if guest.get('plus_one', False))
total_attendees = total_guests + plus_ones
return {
'total_guests': total_guests,
'plus_ones': plus_ones,
'total_attendees': total_attendees
}
def load_data(self):
"""Load guest list and RSVP data from storage"""
self.guest_list_data = self.config_manager.load_json_data('guest_list_data.json')
self.rsvp_data = self.config_manager.load_json_data('rsvp_data.json')
def save_data(self):
"""Save guest list and RSVP data to storage"""
if self.guest_list_data is not None:
self.config_manager.save_json_data('guest_list_data.json', self.guest_list_data)
if self.rsvp_data is not None:
self.config_manager.save_json_data('rsvp_data.json', self.rsvp_data)
def process_guest_list_csv(self, df):
"""Process guest list CSV and create structured data"""
groups = {}
for _, row in df.iterrows():
first_name = str(row.get('First Name (Empty Rows are +1s)', '')).strip()
last_name = str(row.get('Last Name', '')).strip()
group_name = str(row.get('Group', '')).strip()
group_size = str(row.get('Group Size', '')).strip()
party = str(row.get('Party', '')).strip()
# Skip empty rows
if not group_name:
continue
# Initialize group if not exists
if group_name not in groups:
groups[group_name] = {
'group_name': group_name,
'group_size': int(group_size) if group_size.isdigit() else 0,
'party': party,
'address': self._build_address(row),
'named_guests': [],
'plus_one_spots': 0
}
# Check if this is a named guest or plus one spot
# A named guest has at least a first name (last name can be empty)
# A plus one spot has both first and last names empty
if first_name and first_name != 'nan':
# Named guest (even if last name is empty)
full_name = f"{first_name} {last_name}".strip() if last_name and last_name != 'nan' else first_name
groups[group_name]['named_guests'].append({
'first_name': first_name,
'last_name': last_name if last_name and last_name != 'nan' else '',
'full_name': full_name
})
else:
# Plus one spot (both first and last names are empty)
groups[group_name]['plus_one_spots'] += 1
self.guest_list_data = groups
self.save_data()
def process_rsvp_csv(self, df, config):
"""Process RSVP CSV and update guest data"""
if self.guest_list_data is None:
st.error("Please upload guest list first")
return
rsvp_responses = {}
for _, row in df.iterrows():
group_code = str(row.get('group_code', '')).strip()
name = str(row.get('name', '')).strip()
overall_rsvp = str(row.get('overall_rsvp', '')).strip()
bringing_plus_one = str(row.get('bringing_plus_one', '')).strip().lower() == 'yes'
group_attendees = str(row.get('group_attendees', '')).strip()
event_responses = str(row.get('event_responses', '')).strip()
dietary_restrictions = str(row.get('dietary_restrictions', '')).strip()
phone_number = str(row.get('phone_number', '')).strip()
if not group_code:
continue
# Parse event responses
event_dict = {}
if event_responses and event_responses != 'nan' and event_responses != '{}':
try:
event_dict = json.loads(event_responses)
except:
pass
# Parse dietary restrictions
dietary_dict = {}
if dietary_restrictions and dietary_restrictions != 'nan' and dietary_restrictions != '{}':
try:
dietary_dict = json.loads(dietary_restrictions)
except:
pass
# Parse group attendees
attendee_names = []
if group_attendees and group_attendees != 'nan':
attendee_names = [name.strip() for name in group_attendees.split(';') if name.strip()]
rsvp_responses[group_code] = {
'group_code': group_code,
'name': name,
'overall_rsvp': overall_rsvp,
'bringing_plus_one': bringing_plus_one,
'group_attendees': attendee_names,
'event_responses': event_dict,
'dietary_restrictions': dietary_dict,
'phone_number': self._format_phone_number(phone_number)
}
self.rsvp_data = rsvp_responses
self.save_data()
def _format_phone_number(self, phone):
"""Format phone number for display"""
if not phone or phone.strip() == '' or phone == 'nan':
return 'No phone provided'
# Remove all non-digit characters
digits_only = ''.join(filter(str.isdigit, phone))
# Handle empty result
if not digits_only:
return 'No phone provided'
# US phone numbers (10 digits)
if len(digits_only) == 10:
return f"({digits_only[:3]}) {digits_only[3:6]}-{digits_only[6:]}"
# US phone numbers with country code (11 digits starting with 1)
elif len(digits_only) == 11 and digits_only.startswith('1'):
return f"+1 ({digits_only[1:4]}) {digits_only[4:7]}-{digits_only[7:]}"
# International numbers (more than 11 digits or doesn't start with 1)
elif len(digits_only) > 11 or (len(digits_only) == 11 and not digits_only.startswith('1')):
# Format as international with + prefix
return f"+{digits_only}"
# Other cases - return as is with some formatting
else:
return phone.strip()
def _build_address(self, row):
"""Build full address from row data"""
address_parts = []
street = str(row.get(' Street address', '')).strip()
apt = str(row.get(' Apt/Suite', '')).strip()
city = str(row.get(' City', '')).strip()
state = str(row.get(' State', '')).strip()
zip_code = str(row.get(' ZIP', '')).strip()
country = str(row.get('Country', '')).strip()
if street and street != 'nan':
address_parts.append(street)
if apt and apt != 'nan':
address_parts.append(f"Apt {apt}")
if city and city != 'nan':
address_parts.append(city)
if state and state != 'nan':
address_parts.append(state)
if zip_code and zip_code != 'nan':
address_parts.append(zip_code)
if country and country != 'nan':
address_parts.append(country)
return ', '.join(address_parts)
def render_guest_table(self, config):
"""Render the comprehensive guest table"""
if not self.guest_list_data:
return
st.markdown("### Guest List Overview")
# Create comprehensive guest list
all_guests = self._create_comprehensive_guest_list(config)
if not all_guests:
st.info("No guests to display")
return
# Add filter options
col1, col2 = st.columns([1, 3])
with col1:
filter_option = st.selectbox(
"Filter Guests:",
["All Guests", "Confirmed Guests Only"],
help="Filter to show all guests or only those who have confirmed attendance to at least one event"
)
# Filter guests based on selection
if filter_option == "Confirmed Guests Only":
confirmed_guests = [guest for guest in all_guests if any(status == 'Yes' for status in guest.get('rsvp_by_event', {}).values())]
display_guests = confirmed_guests
st.info(f"Showing {len(confirmed_guests)} confirmed guests out of {len(all_guests)} total guests")
else:
display_guests = all_guests
st.info(f"Showing all {len(all_guests)} guests")
# Create DataFrame for display
df_data = []
for guest in display_guests:
row = {
'Name': guest['display_name'],
'Group': guest['group_name'],
'Party': guest['party'],
'Phone': guest.get('phone', ''),
'Address': guest.get('address', '')
}
# Add RSVP columns for each event
wedding_events = config.get('wedding_events', [])
for event in wedding_events:
event_name = event.get('name', '')
if event_name:
row[f'RSVP - {event_name}'] = guest.get('rsvp_by_event', {}).get(event_name, 'Pending')
# Add meal selection columns for events that require meal choice
for event in wedding_events:
if event.get('requires_meal_choice', False):
event_name = event.get('name', '')
if event_name:
row[f'Meal - {event_name}'] = guest.get('meal_selections', {}).get(event_name, 'Not Selected')
df_data.append(row)
if df_data:
df = pd.DataFrame(df_data)
# Display the table
st.dataframe(
df,
use_container_width=True,
hide_index=True,
height=min(600, len(df_data) * 35 + 50)
)
# Summary statistics
self._render_summary_stats(display_guests, config)
def _create_comprehensive_guest_list(self, config):
"""Create a comprehensive list of all guests with RSVP data"""
all_guests = []
for group_name, group_data in self.guest_list_data.items():
# Get RSVP data for this group
rsvp_data = self.rsvp_data.get(group_name, {}) if self.rsvp_data else {}
# Add named guests
for named_guest in group_data['named_guests']:
guest = {
'display_name': named_guest['full_name'],
'first_name': named_guest['first_name'],
'last_name': named_guest['last_name'],
'group_name': group_name,
'party': group_data['party'],
'address': group_data['address'],
'type': 'Named Guest',
'phone': rsvp_data.get('phone_number', ''),
'rsvp_by_event': {},
'meal_selections': {}
}
# Apply RSVP data
self._apply_rsvp_to_guest(guest, rsvp_data, config)
all_guests.append(guest)
# Add plus one spots
plus_one_spots = group_data['plus_one_spots']
if plus_one_spots > 0:
# Get plus one names from RSVP data
group_attendees = rsvp_data.get('group_attendees', [])
named_guest_names = [g['full_name'] for g in group_data['named_guests']]
# Find plus one names (attendees not in named guests)
plus_one_names = [name for name in group_attendees if name not in named_guest_names]
# Create plus one entries
for i in range(plus_one_spots):
if i < len(plus_one_names):
# Named plus one
plus_one_name = plus_one_names[i]
guest = {
'display_name': plus_one_name,
'first_name': plus_one_name.split()[0] if plus_one_name.split() else plus_one_name,
'last_name': ' '.join(plus_one_name.split()[1:]) if len(plus_one_name.split()) > 1 else '',
'group_name': group_name,
'party': group_data['party'],
'address': group_data['address'],
'type': 'Plus One (Named)',
'phone': '',
'rsvp_by_event': {},
'meal_selections': {}
}
else:
# Unnamed plus one
guest = {
'display_name': f'Unnamed Plus One {i+1}',
'first_name': '',
'last_name': '',
'group_name': group_name,
'party': group_data['party'],
'address': group_data['address'],
'type': 'Plus One (Unnamed)',
'phone': '',
'rsvp_by_event': {},
'meal_selections': {}
}
# Apply RSVP data
self._apply_rsvp_to_guest(guest, rsvp_data, config)
all_guests.append(guest)
return all_guests
def _apply_rsvp_to_guest(self, guest, rsvp_data, config):
"""Apply RSVP data to a guest"""
wedding_events = config.get('wedding_events', [])
# Initialize all events with "Pending" status
for event in wedding_events:
event_name = event.get('name', '')
if event_name:
guest['rsvp_by_event'][event_name] = 'Pending'
if not rsvp_data:
return
event_responses = rsvp_data.get('event_responses', {})
group_attendees = rsvp_data.get('group_attendees', [])
dietary_restrictions = rsvp_data.get('dietary_restrictions', {})
guest_name = guest['display_name']
# Apply dietary restrictions
if guest_name in dietary_restrictions:
guest['allergies'] = dietary_restrictions[guest_name]
# Apply event responses
for event in wedding_events:
event_name = event.get('name', '')
if event_name in event_responses:
event_data = event_responses[event_name]
attendees = event_data.get('attendees', [])
party_rsvp = event_data.get('rsvp', 'Pending')
# Determine individual RSVP status
if guest_name in attendees:
guest['rsvp_by_event'][event_name] = party_rsvp
# Apply meal choice if attending and event requires meal choice
if party_rsvp == 'Yes' and event.get('requires_meal_choice', False):
meal_choice = event_data.get('meal_choice', {})
if guest_name in meal_choice:
guest['meal_selections'][event_name] = meal_choice[guest_name]
else:
guest['rsvp_by_event'][event_name] = 'No'
def _render_summary_stats(self, all_guests, config):
"""Render summary statistics"""
st.markdown("### Summary Statistics")
col1, col2, col3, col4, col5 = st.columns(5)
with col1:
total_guests = len(all_guests)
st.metric("Total Guests", total_guests)
with col2:
named_guests = len([g for g in all_guests if g['type'] == 'Named Guest'])
st.metric("Named Guests", named_guests)
with col3:
plus_ones = len([g for g in all_guests if 'Plus One' in g['type']])
st.metric("Plus Ones", plus_ones)
with col4:
confirmed = len([g for g in all_guests if any(status == 'Yes' for status in g.get('rsvp_by_event', {}).values())])
st.metric("Confirmed Attendees", confirmed)
with col5:
# Count confirmed plus ones (plus ones who have confirmed to at least one event)
confirmed_plus_ones = len([g for g in all_guests if 'Plus One' in g['type'] and any(status == 'Yes' for status in g.get('rsvp_by_event', {}).values())])
st.metric("Confirmed Plus Ones", confirmed_plus_ones)
# Event attendance breakdown
st.markdown("#### Event Attendance")
wedding_events = config.get('wedding_events', [])
event_stats = []
for event in wedding_events:
event_name = event.get('name', '')
yes_count = len([g for g in all_guests if g.get('rsvp_by_event', {}).get(event_name) == 'Yes'])
no_count = len([g for g in all_guests if g.get('rsvp_by_event', {}).get(event_name) == 'No'])
pending_count = len([g for g in all_guests if g.get('rsvp_by_event', {}).get(event_name) == 'Pending'])
event_stats.append({
'Event': event_name,
'Yes': yes_count,
'No': no_count,
'Pending': pending_count
})
if event_stats:
stats_df = pd.DataFrame(event_stats)
st.dataframe(stats_df, use_container_width=True, hide_index=True)
# Meal choice breakdown
st.markdown("#### Meal Choices")
wedding_events = config.get('wedding_events', [])
# Get events that require meal choices
meal_events = [event for event in wedding_events if event.get('requires_meal_choice', False)]
if meal_events:
for event in meal_events:
event_name = event.get('name', '')
meal_options = event.get('meal_options', [])
if meal_options:
st.markdown(f"**{event_name}**")
# Count meal choices for this event
meal_counts = {option: 0 for option in meal_options}
meal_counts['Not Selected'] = 0
outdated_choices = {}
for guest in all_guests:
meal_selections = guest.get('meal_selections', {})
selected_meal = meal_selections.get(event_name, 'Not Selected')
if selected_meal in meal_counts:
meal_counts[selected_meal] += 1
elif selected_meal != 'Not Selected':
# This is an outdated meal choice
if selected_meal not in outdated_choices:
outdated_choices[selected_meal] = []
outdated_choices[selected_meal].append(guest['display_name'])
else:
meal_counts['Not Selected'] += 1
# Display meal choice counts
col1, col2, col3, col4 = st.columns(4)
cols = [col1, col2, col3, col4]
for i, (meal, count) in enumerate(meal_counts.items()):
if i < len(cols):
with cols[i]:
st.metric(meal, count)
# Show outdated meal choices warning
if outdated_choices:
st.warning("⚠️ **Outdated Meal Choices Detected!**")
st.markdown("The following guests selected meal options that are no longer available on the current menu:")
for outdated_meal, guests in outdated_choices.items():
with st.expander(f"🔍 {outdated_meal} ({len(guests)} guests)", expanded=False):
st.markdown("**Guests who selected this outdated option:**")
# Create a table for better formatting
guest_data = []
for guest_name in guests:
# Find the guest to get their phone number
guest_info = next((g for g in all_guests if g['display_name'] == guest_name), None)
# Get phone number - prefer individual phone, fall back to group contact
phone = 'No phone provided'
if guest_info:
# Check if guest has individual phone number
individual_phone = guest_info.get('phone', '')
if individual_phone and individual_phone.strip() and individual_phone != 'No phone provided':
phone = individual_phone
else:
# Use group contact phone number
group_name = guest_info.get('group_name', '')
if group_name and self.rsvp_data and group_name in self.rsvp_data:
group_phone = self.rsvp_data[group_name].get('phone_number', '')
if group_phone and group_phone.strip() and group_phone != 'No phone provided':
phone = f"{group_phone} (Group Contact)"
guest_data.append({
'Name': guest_name,
'Phone': phone
})
if guest_data:
guest_df = pd.DataFrame(guest_data)
st.dataframe(guest_df, use_container_width=True, hide_index=True)
st.markdown("**Action Required:**")
st.markdown(f"Please contact these {len(guests)} guests to update their meal choice from '{outdated_meal}' to one of the current options: {', '.join(meal_options)}")
else:
st.info("No events require meal choices.")