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.")