import streamlit as st import json import os import yaml from datetime import datetime, date, timedelta import pandas as pd from yaml.loader import SafeLoader import streamlit_authenticator as stauth from config_manager import ConfigManager from dashboard import Dashboard from tasks import TasksManager from guests import GuestManager from wedding_party import WeddingPartyManager from vendors import VendorManager # Page configuration st.set_page_config( page_title="Wedding Planner", page_icon="💒", layout="wide", initial_sidebar_state="expanded" ) # Custom CSS for green/Adirondack theme st.markdown(""" """, unsafe_allow_html=True) def load_auth_config(): """Load authentication configuration from Google Drive""" try: config_manager = st.session_state.config_manager # Try to load config.yaml from Google Drive root if config_manager.google_drive_enabled: config_content = config_manager.drive_manager.download_file('config.yaml') if config_content: # Parse YAML content config = yaml.load(config_content, Loader=SafeLoader) return config # Fallback to local config.yaml if Google Drive fails if os.path.exists('config.yaml'): with open('config.yaml') as file: config = yaml.load(file, Loader=SafeLoader) return config st.error("❌ No auth config found") return None except Exception as e: st.error(f"Error loading authentication config: {e}") return None def get_user_folder_from_username(username): """Get the user folder based on username using wedding mappings""" try: # Load auth config to get wedding mappings auth_config = st.session_state.get('auth_config') if not auth_config: # Fallback to loading config directly auth_config = load_auth_config() if auth_config and 'wedding_mappings' in auth_config: wedding_mappings = auth_config['wedding_mappings'] # Search through all wedding mappings to find the user for wedding_name, wedding_info in wedding_mappings.items(): if 'users' in wedding_info and username in wedding_info['users']: return wedding_info['folder'] # Fallback to old hardcoded logic for backward compatibility if username == 'demo': return 'demo_data' elif username == 'laraandumang': return 'laraandumang' else: return 'demo_data' # Default fallback except Exception as e: st.error(f"Error getting user folder for {username}: {e}") # Fallback to demo_data on error return 'demo_data' def get_wedding_info_for_user(username): """Get wedding information for a specific user""" try: auth_config = st.session_state.get('auth_config') if not auth_config: auth_config = load_auth_config() if auth_config and 'wedding_mappings' in auth_config: wedding_mappings = auth_config['wedding_mappings'] for wedding_name, wedding_info in wedding_mappings.items(): if 'users' in wedding_info and username in wedding_info['users']: return { 'wedding_name': wedding_name, 'folder': wedding_info['folder'], 'users': wedding_info['users'], 'display_name': wedding_info.get('wedding_name', wedding_name) } return None except Exception as e: st.error(f"Error getting wedding info for {username}: {e}") return None def main(): # Initialize session state if 'config_manager' not in st.session_state: st.session_state.config_manager = ConfigManager() # Load authentication configuration if 'auth_config' not in st.session_state: st.session_state.auth_config = load_auth_config() if st.session_state.auth_config is None: st.error("Failed to load authentication configuration. Please check your Google Drive setup.") st.stop() # Create authenticator (auto_hash=True by default, so passwords will be hashed automatically) authenticator = stauth.Authenticate( st.session_state.auth_config['credentials'], st.session_state.auth_config['cookie']['name'], st.session_state.auth_config['cookie']['key'], st.session_state.auth_config['cookie']['expiry_days'] ) # Check authentication status if 'authentication_status' not in st.session_state: st.session_state.authentication_status = None # Check if user is already authenticated if st.session_state.get('authentication_status'): # User is authenticated, show main app show_main_app(authenticator) else: # Show login page and handle authentication show_login_page(authenticator) def show_login_page(authenticator): """Show the login page""" # Hero Section st.markdown("""

💒 Wedding Planner

Your Complete Wedding Planning Solution

Organize your special day with our comprehensive wedding planning tool. Manage guests, track tasks, coordinate vendors, and create unforgettable memories.

""", unsafe_allow_html=True) # Features Section st.markdown("## ✨ What You Can Do") col1, col2, col3 = st.columns(3) with col1: st.markdown("""

đŸ‘Ĩ Guest Management

""", unsafe_allow_html=True) with col2: st.markdown("""

📋 Task Tracking

""", unsafe_allow_html=True) with col3: st.markdown("""

đŸĸ Vendor Management

""", unsafe_allow_html=True) # Additional Features col4, col5, col6 = st.columns(3) with col4: st.markdown("""

👰 Wedding Party

""", unsafe_allow_html=True) with col5: st.markdown("""

📊 Dashboard

""", unsafe_allow_html=True) with col6: st.markdown("""

â˜ī¸ Cloud Sync

""", unsafe_allow_html=True) st.markdown("---") # Login Section st.markdown("## 🔐 Login to Your Wedding Planner") # Login form try: authenticator.login(location='main') except Exception as e: st.error(f"Login error: {e}") # Check authentication status and show appropriate message if st.session_state.get('authentication_status') is False: st.error("❌ Invalid username or password") elif st.session_state.get('authentication_status') is None: st.info("🔐 Please enter your username and password") elif st.session_state.get('authentication_status'): st.success(f"✅ Welcome, {st.session_state.get('name', 'User')}!") st.rerun() # Refresh to show main app def show_wedding_setup_form(): """Show the wedding setup form for creating a new wedding""" st.markdown("### 📝 Create Your Wedding Configuration") # Initialize session state for events and form data if not exists if 'setup_events' not in st.session_state: st.session_state.setup_events = [] if 'setup_form_data' not in st.session_state: st.session_state.setup_form_data = { 'partner1_name': '', 'partner2_name': '', 'venue_city': '', 'wedding_start_date': date.today(), 'wedding_end_date': date.today(), 'custom_tags': '', 'task_assignees': '' } # Basic wedding information form with st.form("wedding_setup"): st.markdown("#### Basic Wedding Information") col1, col2 = st.columns(2) with col1: partner1_name = st.text_input("Partner 1 Name", value=st.session_state.setup_form_data['partner1_name'], placeholder="Enter first partner's name") partner2_name = st.text_input("Partner 2 Name", value=st.session_state.setup_form_data['partner2_name'], placeholder="Enter second partner's name") venue_city = st.text_input("City", value=st.session_state.setup_form_data['venue_city'], placeholder="Enter city") with col2: st.markdown("**Wedding Date Range**") wedding_start_date = st.date_input("Start Date", value=st.session_state.setup_form_data['wedding_start_date']) wedding_end_date = st.date_input("End Date", value=st.session_state.setup_form_data['wedding_end_date']) if wedding_end_date < wedding_start_date: st.error("End date must be after start date") wedding_end_date = wedding_start_date st.markdown("#### Task Organization") st.info("Tasks will be automatically grouped by your wedding events.") st.markdown("#### Custom Tags") st.markdown("Enter custom tags (one per line):") custom_tags = st.text_area("Custom Tags", value=st.session_state.setup_form_data['custom_tags'], placeholder="e.g.,\nUrgent\nHigh Priority\nDeposit Required\nResearch Needed") st.markdown("#### Task Assignees") st.markdown("Enter people who will regularly be assigned tasks (one per line):") task_assignees = st.text_area("Task Assignees", value=st.session_state.setup_form_data['task_assignees'], placeholder="e.g.,\nMom\nDad\nWedding Planner\nBest Friend\nCoordinator") form_submitted = st.form_submit_button("Update Wedding Information") if form_submitted: # Update session state with form data st.session_state.setup_form_data = { 'partner1_name': partner1_name, 'partner2_name': partner2_name, 'venue_city': venue_city, 'wedding_start_date': wedding_start_date, 'wedding_end_date': wedding_end_date, 'custom_tags': custom_tags, 'task_assignees': task_assignees } st.success("Wedding information updated!") st.rerun() # Event management section (outside form) st.markdown("#### Wedding Events") st.markdown("Define all your wedding events with their details:") # Add/Remove event buttons col1, col2 = st.columns(2) with col1: if st.button("➕ Add Event"): # Set default date to wedding start date wedding_start = st.session_state.setup_form_data['wedding_start_date'] st.session_state.setup_events.append({ "name": "New Event", "description": "", "date_offset": 0, "requires_meal_choice": False, "meal_options": [], "location": "", "address": "" }) st.rerun() with col2: if len(st.session_state.setup_events) > 0 and st.button("➖ Remove Last Event"): st.session_state.setup_events.pop() st.rerun() # Display events if st.session_state.setup_events: for i, event in enumerate(st.session_state.setup_events): with st.expander(f"Event {i+1}: {event['name']}", expanded=True): col1, col2 = st.columns(2) with col1: event_name = st.text_input("Event Name", value=event['name'], key=f"event_name_{i}") event_description = st.text_input("Time", value=event['description'], placeholder="e.g., 2:00 PM, 6:30 PM", key=f"event_desc_{i}") event_location = st.text_input("Location Name", value=event.get('location', ''), placeholder="e.g., Central Park, Grand Ballroom", key=f"event_location_{i}") with col2: # Get wedding date range wedding_start = st.session_state.setup_form_data['wedding_start_date'] wedding_end = st.session_state.setup_form_data['wedding_end_date'] # Calculate current event date from date_offset current_event_date = wedding_start + timedelta(days=event['date_offset']) # Use date input without constraints - allow any date event_date = st.date_input( "Event Date", value=current_event_date, key=f"event_date_{i}", help="Select any date for this event" ) # Show warning if date is outside wedding range if event_date < wedding_start or event_date > wedding_end: st.warning(f"âš ī¸ Selected date is outside your wedding date range ({wedding_start.strftime('%B %d, %Y')} - {wedding_end.strftime('%B %d, %Y')})") requires_meal_choice = st.checkbox("Requires Meal Choice", value=event['requires_meal_choice'], key=f"event_meal_{i}") # Meal options section (only show if meal choice is required) if requires_meal_choice: st.markdown("**Meal Options**") st.markdown("Enter meal options (one per line):") current_meal_options = event.get('meal_options', []) meal_options_text = '\n'.join(current_meal_options) if current_meal_options else '' meal_options = st.text_area("Meal Options", value=meal_options_text, placeholder="e.g.,\nDuck\nSurf & Turf\nRisotto (vegetarian)\nStuffed Squash (vegetarian)", key=f"event_meal_options_{i}", height=100) else: meal_options = "" event_address = st.text_area("Address", value=event.get('address', ''), placeholder="Enter full address (street, city, state, zip code)", key=f"event_address_{i}", height=80) # Calculate date_offset from the selected date date_offset = (event_date - wedding_start).days # Parse meal options meal_options_list = [] if requires_meal_choice and meal_options: meal_options_list = [option.strip() for option in meal_options.split('\n') if option.strip()] # Update session state st.session_state.setup_events[i] = { "name": event_name, "description": event_description, "date_offset": date_offset, "requires_meal_choice": requires_meal_choice, "meal_options": meal_options_list, "location": event_location, "address": event_address } else: st.info("No events added yet. Click 'Add Event' to get started!") # Save configuration button (after event management) st.markdown("---") if st.button("Save Configuration", type="primary"): # Get form values from session state form_data = st.session_state.setup_form_data if form_data['partner1_name'] and form_data['partner2_name'] and form_data['wedding_start_date'] and form_data['wedding_end_date']: # Parse tags (task groups will be auto-generated from events) custom_tags_list = [tag.strip() for tag in form_data['custom_tags'].split('\n') if tag.strip()] task_assignees_list = [assignee.strip() for assignee in form_data['task_assignees'].split('\n') if assignee.strip()] # Create configuration config = { 'wedding_info': { 'partner1_name': form_data['partner1_name'], 'partner2_name': form_data['partner2_name'], 'wedding_start_date': form_data['wedding_start_date'].isoformat(), 'wedding_end_date': form_data['wedding_end_date'].isoformat(), 'venue_city': form_data['venue_city'] }, 'custom_settings': { 'custom_tags': custom_tags_list, 'task_assignees': task_assignees_list }, 'wedding_events': st.session_state.setup_events } # Save configuration st.session_state.config_manager.save_config(config) # Clear setup session state if 'setup_events' in st.session_state: del st.session_state.setup_events if 'setup_form_data' in st.session_state: del st.session_state.setup_form_data if 'show_setup_form' in st.session_state: del st.session_state.show_setup_form st.success("Configuration saved successfully!") st.rerun() else: st.error("Please fill in at least the partner names and wedding date range in the form above.") def show_main_app(authenticator): # Get current user from session state (set by authenticator.login) username = st.session_state.get('username') name = st.session_state.get('name') if not username or not name: st.error("Authentication error: Missing user information") return # Set user folder based on username user_folder = get_user_folder_from_username(username) # Get wedding info for the user (for future use) wedding_info = get_wedding_info_for_user(username) # Update config manager to use the correct user folder config_manager = st.session_state.config_manager config_manager.set_user_folder(user_folder) # Check if we need to load data (either not initialized or user changed) current_user_folder = config_manager.get_current_user_folder() user_changed = st.session_state.get('last_user_folder') != user_folder if not st.session_state.get('app_initialized', False) or user_changed: with st.spinner(f"Loading {name}'s wedding data..."): if user_folder == 'demo_data': if config_manager.load_demo_data_from_drive(): st.session_state.app_initialized = True st.session_state.last_user_folder = user_folder st.success("✅ Demo data loaded successfully!") else: st.error("Failed to load demo data.") return else: if config_manager.load_existing_data_from_drive(): st.session_state.app_initialized = True st.session_state.last_user_folder = user_folder else: st.error("Failed to load wedding data.") return # Load config config = st.session_state.config_manager.load_config() wedding_info = config.get('wedding_info', {}) # Check demo mode status is_demo_mode = st.session_state.config_manager.is_demo_mode() # Header partner1 = wedding_info.get('partner1_name', 'Partner 1') partner2 = wedding_info.get('partner2_name', 'Partner 2') venue_city = wedding_info.get('venue_city', '') wedding_start_str = wedding_info.get('wedding_start_date', '') wedding_end_str = wedding_info.get('wedding_end_date', '') # Build base header text with location if venue_city: header_text = f"{partner1} & {partner2}'s Wedding Planner - {venue_city} \n" else: header_text = f"{partner1} & {partner2}'s Wedding Planner \n" # Add wedding mapping info if available user_wedding_info = get_wedding_info_for_user(username) # Add demo mode indicator if is_demo_mode: header_text += "🎭 DEMO MODE - Sample Data" if wedding_start_str and wedding_end_str: try: wedding_start = datetime.fromisoformat(wedding_start_str).date() wedding_end = datetime.fromisoformat(wedding_end_str).date() today = date.today() if today < wedding_start: days_until = (wedding_start - today).days header_text += f"\n{days_until} days until wedding festivities begin!" elif wedding_start <= today <= wedding_end: header_text += " - Wedding festivities are happening now! 🎉" else: days_since = (today - wedding_end).days header_text += f" - {days_since} days since the wedding celebration ended!" except: pass # Keep the base header text if date parsing fails st.markdown(f'

{header_text}

', unsafe_allow_html=True) # Add Google Drive sync status and push button show_google_drive_sync_status() # Sidebar navigation with st.sidebar: # User info and logout # Extract first name for a more friendly greeting first_name = name.split()[0] if name else "User" st.markdown(f"**Welcome, {first_name}!**") if authenticator.logout(location='sidebar', key='logout_button'): # Clear session state on logout for key in list(st.session_state.keys()): if key not in ['config_manager', 'auth_config']: del st.session_state[key] # Reset app initialization state st.session_state.app_initialized = False st.session_state.last_user_folder = None # Reset config manager state if 'config_manager' in st.session_state: st.session_state.config_manager.reset_app_state() st.session_state.config_manager.user_folder = None st.rerun() st.markdown("---") st.markdown("### Navigation") page = st.radio( "Choose a page:", ["Dashboard", "Tasks", "Guest Management", "Wedding Party", "Wedding Overview", "Vendors & Purchases", "Settings"] ) # Add Google Drive sync button in sidebar if enabled config_manager = st.session_state.config_manager drive_status = config_manager.get_google_drive_status() if drive_status['enabled']: st.markdown("---") st.markdown("### â˜ī¸ Google Drive") modified_files = config_manager.get_modified_files() # Always show push button if modified_files: st.warning(f"📝 {len(modified_files)} unsaved changes") if st.button("📤 Push Changes", type="primary", help="Save changes to Google Drive", key="sidebar_push"): with st.spinner("Pushing changes..."): if config_manager.manual_sync_to_drive(): st.success("✅ Changes saved!") st.rerun() else: st.error("❌ Failed to save changes") else: st.success("✅ All changes saved") if st.button("📤 Push to Drive", help="Save current data to Google Drive", key="sidebar_push_all"): with st.spinner("Pushing to Drive..."): if config_manager.manual_sync_to_drive(): st.success("✅ Data saved!") st.rerun() else: st.error("❌ Failed to save") # Always show pull button if st.button("🔄 Pull Latest", help="Get latest from Google Drive", key="sidebar_pull"): with st.spinner("Pulling latest..."): if config_manager.manual_sync_from_drive(): st.success("✅ Latest loaded!") st.rerun() else: st.error("❌ Failed to load changes") # Route to appropriate page if page == "Dashboard": Dashboard().render(config) elif page == "Tasks": TasksManager().render(config) elif page == "Guest Management": GuestManager().render(config) elif page == "Wedding Party": WeddingPartyManager().render(config) elif page == "Wedding Overview": show_wedding_timeline_page(config) elif page == "Vendors & Purchases": VendorManager().render(config) elif page == "Settings": show_settings_page(config) def show_wedding_timeline_page(config): st.markdown("## 📅 Wedding Overview") # Show wedding events directly without tabs show_wedding_events_section(config) def get_vendors_for_event(event_name, vendors_data): """Get vendors associated with a specific event, handling multiple categories per vendor""" event_vendors = [] for vendor in vendors_data: vendor_events = vendor.get('events', []) if event_name in vendor_events: # Get all categories for this vendor categories = vendor.get('categories', []) primary_category = vendor.get('category', '') # If no categories array, use the primary category if not categories and primary_category: categories = [primary_category] # If still no categories, use a default if not categories: categories = ['Vendor/Service'] # Create an entry for each category this vendor serves for category in categories: event_vendors.append({ 'name': vendor.get('name', ''), 'category': category, 'status': vendor.get('status', ''), 'vendor_id': vendor.get('id', '') # Add ID to help identify duplicates }) return event_vendors def get_meal_choices_for_event(event_name, rsvp_data, meal_options): """Get meal choice counts for a specific event from RSVP data""" meal_counts = {} # Initialize counts for all meal options for option in meal_options: meal_counts[option] = 0 # Count meal choices from RSVP data for group_code, group_data in rsvp_data.items(): event_responses = group_data.get('event_responses', {}) if event_name in event_responses: event_data = event_responses[event_name] meal_choices = event_data.get('meal_choice', {}) for attendee, choice in meal_choices.items(): if choice in meal_counts: meal_counts[choice] += 1 return meal_counts def show_wedding_events_section(config): st.markdown("## 📅 Wedding Events") wedding_events = config.get('wedding_events', []) wedding_info = config.get('wedding_info', {}) if not wedding_events: st.info("No events configured yet. Please complete the setup to define your wedding events.") return # Get wedding dates wedding_start_str = wedding_info.get('wedding_start_date', '') wedding_end_str = wedding_info.get('wedding_end_date', '') if wedding_start_str and wedding_end_str: try: wedding_start = datetime.fromisoformat(wedding_start_str).date() wedding_end = datetime.fromisoformat(wedding_end_str).date() except: st.error("Invalid wedding date format. Please check your settings.") return else: st.warning("Wedding dates not set. Please complete the setup.") return # Load vendors and RSVP data config_manager = ConfigManager() vendors_data = config_manager.load_json_data('vendors.json') rsvp_data = config_manager.load_json_data('rsvp_data.json') # Display events st.markdown(f"### Your Wedding Events ({len(wedding_events)} total)") # Group events by day events_by_day = {} for event in wedding_events: event_date = wedding_start + timedelta(days=event.get('date_offset', 0)) day_key = event_date.strftime('%Y-%m-%d') if day_key not in events_by_day: events_by_day[day_key] = [] events_by_day[day_key].append(event) # Sort days sorted_days = sorted(events_by_day.keys()) for day in sorted_days: day_date = datetime.fromisoformat(day).date() day_events = events_by_day[day] # Determine day label if day_date == wedding_start: day_label = "Day 1 - Wedding Start" elif day_date == wedding_end: day_label = "Final Day - Wedding End" else: days_from_start = (day_date - wedding_start).days day_label = f"Day {days_from_start + 1}" st.markdown(f"#### {day_label} - {day_date.strftime('%B %d, %Y')}") for event in day_events: # Simple event display time_info = event.get('description', '') or 'Time TBD' location = event.get('location', '') or 'Location TBD' address = event.get('address', '') meal_required = event.get('requires_meal_choice', False) event_name = event.get('name', 'Untitled Event') st.markdown(f"**{event_name}**") st.markdown(f"🕐 **Time:** {time_info}") st.markdown(f"📍 **Location:** {location}") if address: st.markdown(f"🏠 **Address:** {address}") st.markdown(f"đŸŊī¸ **Meal Choice:** {'Required' if meal_required else 'Not Required'}") # Display meal choices and counts if meal choice is required if meal_required: meal_options = event.get('meal_options', []) if meal_options: meal_counts = get_meal_choices_for_event(event_name, rsvp_data, meal_options) st.markdown("đŸŊī¸ **Meal Choices:**") for option in meal_options: count = meal_counts.get(option, 0) st.markdown(f" â€ĸ **{option}:** {count} orders") else: st.markdown("đŸŊī¸ **Meal Choices:** No options configured") # Display vendors for this event event_vendors = get_vendors_for_event(event_name, vendors_data) if event_vendors: st.markdown("đŸĸ **Vendors:**") # Group vendors by name to handle multiple categories vendors_by_name = {} for vendor in event_vendors: vendor_name = vendor['name'] if vendor_name not in vendors_by_name: vendors_by_name[vendor_name] = { 'categories': [], 'status': vendor['status'] } vendors_by_name[vendor_name]['categories'].append(vendor['category']) # Display grouped vendors for vendor_name, vendor_info in vendors_by_name.items(): status_emoji = "✅" if vendor_info['status'] == "Booked" else "âŗ" if vendor_info['status'] == "Researching" else "📋" categories_text = ", ".join(vendor_info['categories']) st.markdown(f" â€ĸ {status_emoji} **{categories_text}:** {vendor_name}") else: st.markdown("đŸĸ **Vendors:** None assigned") st.markdown("---") # Event summary st.markdown("### Event Summary") col1, col2, col3 = st.columns(3) with col1: total_events = len(wedding_events) st.metric("Total Events", total_events) with col2: meal_events = len([e for e in wedding_events if e.get('requires_meal_choice', False)]) st.metric("Events with Meals", meal_events) with col3: days_span = (wedding_end - wedding_start).days + 1 st.metric("Celebration Days", days_span) def show_settings_page(config): st.markdown("### Settings") # Check demo mode status is_demo_mode = st.session_state.config_manager.is_demo_mode() # Demo mode toggle at the top st.markdown("#### Demo Mode") col1, col2 = st.columns([3, 1]) with col1: if is_demo_mode: st.info("🎭 **Demo Mode is ON** - You are currently viewing sample data. This includes demo guests, vendors with complex payment schedules, tasks, and wedding party information.") else: st.info("📝 **Demo Mode is OFF** - You are viewing your actual wedding data.") with col2: if st.button("Toggle Demo Mode", type="secondary"): if st.session_state.config_manager.toggle_demo_mode(): st.success("Demo mode toggled! Please refresh the page.") st.rerun() else: st.error("Failed to toggle demo mode.") # Create tabs for different settings tab1, tab2, tab3, tab4, tab5, tab6 = st.tabs(["Edit Configuration", "Manage Events", "Google Drive", "Cache Management", "Current Configuration", "Reset"]) with tab1: st.markdown("#### Edit Wedding Configuration") # Initialize session state for editing if not exists if 'edit_config' not in st.session_state: st.session_state.edit_config = config.copy() with st.form("edit_wedding_config"): st.markdown("##### Basic Wedding Information") col1, col2 = st.columns(2) with col1: partner1_name = st.text_input("Partner 1 Name", value=st.session_state.edit_config.get('wedding_info', {}).get('partner1_name', '')) partner2_name = st.text_input("Partner 2 Name", value=st.session_state.edit_config.get('wedding_info', {}).get('partner2_name', '')) venue_city = st.text_input("City", value=st.session_state.edit_config.get('wedding_info', {}).get('venue_city', '')) with col2: # Get current dates wedding_start_str = st.session_state.edit_config.get('wedding_info', {}).get('wedding_start_date', '') wedding_end_str = st.session_state.edit_config.get('wedding_info', {}).get('wedding_end_date', '') try: if wedding_start_str: wedding_start = datetime.fromisoformat(wedding_start_str).date() else: wedding_start = date.today() if wedding_end_str: wedding_end = datetime.fromisoformat(wedding_end_str).date() else: wedding_end = date.today() except: wedding_start = date.today() wedding_end = date.today() wedding_start_date = st.date_input("Wedding Start Date", value=wedding_start) wedding_end_date = st.date_input("Wedding End Date", value=wedding_end) if wedding_end_date < wedding_start_date: st.error("End date must be after start date") wedding_end_date = wedding_start_date st.markdown("##### Custom Tags") current_tags = st.session_state.edit_config.get('custom_settings', {}).get('custom_tags', []) custom_tags_text = '\n'.join(current_tags) if current_tags else '' custom_tags = st.text_area("Custom Tags (one per line)", value=custom_tags_text, placeholder="e.g.,\nUrgent\nHigh Priority\nDeposit Required") st.markdown("##### Task Assignees") current_assignees = st.session_state.edit_config.get('custom_settings', {}).get('task_assignees', []) task_assignees_text = '\n'.join(current_assignees) if current_assignees else '' task_assignees = st.text_area("Task Assignees (one per line)", value=task_assignees_text, placeholder="e.g.,\nMom\nDad\nWedding Planner\nBest Friend\nCoordinator") submitted = st.form_submit_button("Save Changes", type="primary") if submitted: # Update the configuration updated_config = st.session_state.edit_config.copy() updated_config['wedding_info'] = { 'partner1_name': partner1_name, 'partner2_name': partner2_name, 'venue_city': venue_city, 'wedding_start_date': wedding_start_date.isoformat(), 'wedding_end_date': wedding_end_date.isoformat() } # Parse custom tags and task assignees custom_tags_list = [tag.strip() for tag in custom_tags.split('\n') if tag.strip()] task_assignees_list = [assignee.strip() for assignee in task_assignees.split('\n') if assignee.strip()] updated_config['custom_settings'] = { 'custom_tags': custom_tags_list, 'task_assignees': task_assignees_list } # Save the updated configuration st.session_state.config_manager.save_config(updated_config) st.success("Configuration updated successfully!") st.rerun() with tab2: show_event_management_section(config) with tab3: show_google_drive_section() with tab4: show_cache_management_section() with tab5: st.markdown("#### Current Configuration") st.json(config) with tab6: st.markdown("#### Reset Configuration") st.warning("âš ī¸ This will permanently delete all your wedding configuration data. This action cannot be undone.") if st.button("Reset Configuration", type="secondary"): if st.session_state.config_manager.reset_config(): st.success("Configuration reset! Please refresh the page.") st.rerun() def show_cache_management_section(): """Show cache management section in settings""" st.markdown("#### Cache Management") config_manager = st.session_state.config_manager # Get cache status cache_status = config_manager.get_cache_status() st.markdown("**Current Cache Status:**") col1, col2 = st.columns(2) with col1: st.metric("Total Cached Items", cache_status['total_cached']) if cache_status['config_cached']: st.success("✅ Wedding configuration cached") else: st.info("â„šī¸ Wedding configuration not cached") with col2: if cache_status['cached_data_files']: st.success(f"✅ {len(cache_status['cached_data_files'])} data files cached") with st.expander("View cached files"): for filename in cache_status['cached_data_files']: st.markdown(f"â€ĸ {filename}") else: st.info("â„šī¸ No data files cached") st.markdown("---") # Cache management buttons st.markdown("**Cache Actions:**") col1, col2, col3 = st.columns(3) with col1: if st.button("đŸ—‘ī¸ Clear All Cache", help="Clear all cached data to force reload from source"): config_manager.clear_cache() st.success("Cache cleared! Data will be reloaded from source on next access.") st.rerun() with col2: if st.button("🔄 Refresh Cache", help="Reload all data from Google Drive and update cache"): if config_manager.google_drive_enabled: with st.spinner("Refreshing cache from Google Drive..."): if config_manager.manual_sync_from_drive(): st.success("Cache refreshed from Google Drive!") st.rerun() else: st.error("Failed to refresh cache from Google Drive") else: st.warning("Google Drive not enabled. Cannot refresh from Drive.") with col3: if st.button("â„šī¸ Cache Info", help="Show detailed cache information"): st.info("Cache helps improve performance by storing data in memory. Data is automatically cached when first loaded and updated when modified.") def show_google_drive_status_setup(): """Show Google Drive status on the setup page""" config_manager = st.session_state.config_manager drive_status = config_manager.get_google_drive_status() # Create an expander for Google Drive status with st.expander("🔗 Google Drive Connection Status", expanded=False): # Status display if drive_status['enabled']: if drive_status['status'] == 'Online': st.success(f"✅ {drive_status['message']}") elif drive_status['status'] == 'Offline': st.warning(f"âš ī¸ {drive_status['message']}") else: st.error(f"❌ {drive_status['message']}") else: st.info(f"â„šī¸ {drive_status['message']}") # Show files if available if drive_status['enabled'] and 'files' in drive_status: st.markdown("**Files found in Google Drive:**") for file_name in drive_status['files']: friendly_name = get_friendly_file_name(file_name) st.markdown(f"â€ĸ {friendly_name}") # Manual sync buttons (if enabled) - only show pull and load options if drive_status['enabled']: st.markdown("**Manual Sync:**") col1, col2 = st.columns(2) with col1: if st.button("đŸ“Ĩ Sync from Google Drive", help="Download latest data from Google Drive", key="setup_sync_from"): with st.spinner("Syncing from Google Drive..."): if config_manager.manual_sync_from_drive(): st.success("Successfully synced from Google Drive!") st.rerun() else: st.error("Failed to sync from Google Drive") with col2: if st.button("🔄 Load Existing Data", help="Load existing wedding data from Google Drive", key="setup_load_existing"): with st.spinner("Loading existing data from Google Drive..."): if config_manager.load_existing_data_from_drive(): st.success("Successfully loaded existing data from Google Drive!") st.info("Your wedding data has been loaded. The page will refresh to show your wedding planner.") st.rerun() else: st.error("Failed to load existing data from Google Drive") # Configuration help if not drive_status['enabled']: st.markdown("**To enable Google Drive integration:**") st.markdown("Set the following **secrets** in your Hugging Face Space settings:") st.code(""" GOOGLE_DRIVE_FOLDER_ID=your_folder_id GOOGLE_PROJECT_ID=your_project_id GOOGLE_PRIVATE_KEY_ID=your_private_key_id GOOGLE_PRIVATE_KEY=your_private_key GOOGLE_CLIENT_EMAIL=your_client_email GOOGLE_CLIENT_ID=your_client_id """) # Debug information st.markdown("**Debug Information:**") folder_id = os.getenv('GOOGLE_DRIVE_FOLDER_ID') if folder_id: st.success(f"✅ GOOGLE_DRIVE_FOLDER_ID found: {folder_id[:10]}...") else: st.error("❌ GOOGLE_DRIVE_FOLDER_ID not found") # Check other variables project_id = os.getenv('GOOGLE_PROJECT_ID') client_email = os.getenv('GOOGLE_CLIENT_EMAIL') private_key_id = os.getenv('GOOGLE_PRIVATE_KEY_ID') private_key = os.getenv('GOOGLE_PRIVATE_KEY') client_id = os.getenv('GOOGLE_CLIENT_ID') if project_id: st.success(f"✅ GOOGLE_PROJECT_ID found: {project_id[:10]}...") else: st.error("❌ GOOGLE_PROJECT_ID not found") if client_email: st.success(f"✅ GOOGLE_CLIENT_EMAIL found: {client_email}") else: st.error("❌ GOOGLE_CLIENT_EMAIL not found") if private_key_id: st.success(f"✅ GOOGLE_PRIVATE_KEY_ID found: {private_key_id[:10]}...") else: st.error("❌ GOOGLE_PRIVATE_KEY_ID not found") if private_key: st.success(f"✅ GOOGLE_PRIVATE_KEY found: {len(private_key)} characters") else: st.error("❌ GOOGLE_PRIVATE_KEY not found") if client_id: st.success(f"✅ GOOGLE_CLIENT_ID found: {client_id[:10]}...") else: st.error("❌ GOOGLE_CLIENT_ID not found") # Check if all required variables are present all_required = all([folder_id, project_id, private_key_id, private_key, client_email, client_id]) if all_required: st.success("🎉 All required secrets are present!") st.info("If Google Drive is still not working, there might be an authentication issue. Try restarting your Space.") # Test file writing permissions st.markdown("**Testing file permissions:**") try: test_file = "/tmp/test_write.txt" with open(test_file, 'w') as f: f.write("test") st.success("✅ Can write to /tmp directory") os.remove(test_file) except Exception as e: st.error(f"❌ Cannot write to /tmp directory: {e}") try: test_dir = "wedding_data" os.makedirs(test_dir, exist_ok=True) test_file = os.path.join(test_dir, "test.txt") with open(test_file, 'w') as f: f.write("test") st.success("✅ Can write to wedding_data directory") os.remove(test_file) except Exception as e: st.error(f"❌ Cannot write to wedding_data directory: {e}") else: st.warning("âš ī¸ Some required secrets are missing. Please add all secrets and restart your Space.") def get_friendly_file_name(filename): """Convert technical file names to user-friendly names""" friendly_names = { 'wedding_config.json': 'Wedding Configuration', 'guest_list_data.json': 'Guest List', 'rsvp_data.json': 'RSVP Responses', 'tasks.json': 'Tasks', 'vendors.json': 'Vendors', 'wedding_party.json': 'Wedding Party' } return friendly_names.get(filename, filename) def show_google_drive_sync_status(): """Show Google Drive sync status and push button prominently in main app""" config_manager = st.session_state.config_manager drive_status = config_manager.get_google_drive_status() # Only show if Google Drive is enabled if not drive_status['enabled']: return # Get modified files modified_files = config_manager.get_modified_files() # Create a prominent status bar with always-visible push button col1, col2, col3 = st.columns([2, 1, 1]) with col1: if modified_files: friendly_names = [get_friendly_file_name(f) for f in modified_files] st.warning(f"📝 **{len(modified_files)} unsaved changes** - {', '.join(friendly_names)}") else: st.success("✅ **All changes saved** - Your data is up to date with Google Drive") with col2: # Always show push button - it will sync all current data if modified_files: button_text = f"📤 Push {len(modified_files)} Changes" button_help = f"Save {len(modified_files)} modified files to Google Drive" else: button_text = "📤 Push to Drive" button_help = "Save current data to Google Drive" if st.button(button_text, type="primary", help=button_help): with st.spinner("Pushing changes to Google Drive..."): if config_manager.manual_sync_to_drive(): st.success("✅ Changes saved to Google Drive!") st.rerun() else: st.error("❌ Failed to save changes to Google Drive") with col3: if st.button("🔄 Pull Latest", help="Get latest changes from Google Drive"): with st.spinner("Pulling latest changes from Google Drive..."): if config_manager.manual_sync_from_drive(): st.success("✅ Latest changes loaded from Google Drive!") st.rerun() else: st.error("❌ Failed to load changes from Google Drive") # Add a small separator st.markdown("---") def show_google_drive_section(): st.markdown("#### Google Drive Integration") config_manager = st.session_state.config_manager drive_status = config_manager.get_google_drive_status() # Status display if drive_status['enabled']: if drive_status['status'] == 'Online': st.success(f"✅ {drive_status['message']}") elif drive_status['status'] == 'Offline': st.warning(f"âš ī¸ {drive_status['message']}") else: st.error(f"❌ {drive_status['message']}") else: st.info(f"â„šī¸ {drive_status['message']}") # Show files if available if drive_status['enabled'] and 'files' in drive_status: st.markdown("**Files in Google Drive:**") for file_name in drive_status['files']: friendly_name = get_friendly_file_name(file_name) st.markdown(f"â€ĸ {friendly_name}") # Show modified files status if drive_status['enabled']: modified_files = config_manager.get_modified_files() if modified_files: st.markdown("#### 📝 Modified Files (Not Synced)") st.warning(f"The following files have been modified and need to be synced to Google Drive:") for file_name in modified_files: friendly_name = get_friendly_file_name(file_name) st.markdown(f"â€ĸ {friendly_name}") else: st.markdown("#### ✅ All Files Synced") st.success("All your data is up to date with Google Drive.") # Manual sync buttons if drive_status['enabled']: st.markdown("#### Manual Sync") col1, col2 = st.columns(2) with col1: if st.button("đŸ“Ĩ Sync from Google Drive", help="Download latest data from Google Drive"): with st.spinner("Syncing from Google Drive..."): if config_manager.manual_sync_from_drive(): st.success("Successfully synced from Google Drive!") st.rerun() else: st.error("Failed to sync from Google Drive") with col2: # Show different button text based on whether there are modified files modified_files = config_manager.get_modified_files() if modified_files: button_text = f"📤 Sync {len(modified_files)} Modified Files to Drive" button_help = f"Upload {len(modified_files)} modified files to Google Drive" else: button_text = "📤 Sync to Google Drive" button_help = "Upload current data to Google Drive (no changes to sync)" if st.button(button_text, help=button_help, disabled=not modified_files): with st.spinner("Syncing to Google Drive..."): if config_manager.manual_sync_to_drive(): st.success("Successfully synced to Google Drive!") st.rerun() else: st.error("Failed to sync to Google Drive") # Configuration info st.markdown("#### Configuration") st.markdown("To enable Google Drive integration, set the following environment variables:") st.code(""" GOOGLE_DRIVE_FOLDER_ID=your_folder_id GOOGLE_PROJECT_ID=your_project_id GOOGLE_PRIVATE_KEY_ID=your_private_key_id GOOGLE_PRIVATE_KEY=your_private_key GOOGLE_CLIENT_EMAIL=your_client_email GOOGLE_CLIENT_ID=your_client_id """) st.markdown("**Note:** Google Drive integration is automatically enabled when running on Hugging Face Spaces with proper credentials configured.") def show_event_management_section(config): st.markdown("#### Manage Wedding Events") # Initialize session state for event editing if not exists if 'edit_events' not in st.session_state: st.session_state.edit_events = config.get('wedding_events', []).copy() wedding_events = st.session_state.edit_events wedding_info = config.get('wedding_info', {}) # Get wedding dates for date calculations wedding_start_str = wedding_info.get('wedding_start_date', '') wedding_end_str = wedding_info.get('wedding_end_date', '') if not wedding_start_str or not wedding_end_str: st.warning("Please set your wedding date range in the 'Edit Configuration' tab first.") return try: wedding_start = datetime.fromisoformat(wedding_start_str).date() wedding_end = datetime.fromisoformat(wedding_end_str).date() except: st.error("Invalid wedding date format. Please check your configuration.") return # Add event button if st.button("➕ Add New Event"): st.session_state.edit_events.append({ "name": "New Event", "description": "", "date_offset": 0, "requires_meal_choice": False, "meal_options": [], "location": "", "address": "" }) st.rerun() # Display events for editing if st.session_state.edit_events: st.markdown("##### Edit Events") for i, event in enumerate(st.session_state.edit_events): with st.expander(f"Event {i+1}: {event['name']}", expanded=True): # Add delete button at the top right of each event col_header1, col_header2 = st.columns([4, 1]) with col_header2: if st.button("đŸ—‘ī¸ Delete", key=f"delete_event_{i}", help="Delete this event"): st.session_state.edit_events.pop(i) st.rerun() col1, col2 = st.columns(2) with col1: event_name = st.text_input("Event Name", value=event['name'], key=f"settings_event_name_{i}") event_description = st.text_input("Time", value=event['description'], placeholder="e.g., 2:00 PM, 6:30 PM", key=f"settings_event_desc_{i}") event_location = st.text_input("Location Name", value=event.get('location', ''), placeholder="e.g., Central Park, Grand Ballroom", key=f"settings_event_location_{i}") with col2: # Calculate current event date from date_offset current_event_date = wedding_start + timedelta(days=event['date_offset']) # Use date input without constraints - allow any date event_date = st.date_input( "Event Date", value=current_event_date, key=f"settings_event_date_{i}", help="Select any date for this event" ) # Show warning if date is outside wedding range if event_date < wedding_start or event_date > wedding_end: st.warning(f"âš ī¸ Selected date is outside your wedding date range ({wedding_start.strftime('%B %d, %Y')} - {wedding_end.strftime('%B %d, %Y')})") requires_meal_choice = st.checkbox("Requires Meal Choice", value=event['requires_meal_choice'], key=f"settings_event_meal_{i}") # Meal options section (only show if meal choice is required) if requires_meal_choice: st.markdown("**Meal Options**") st.markdown("Enter meal options (one per line):") current_meal_options = event.get('meal_options', []) meal_options_text = '\n'.join(current_meal_options) if current_meal_options else '' meal_options = st.text_area("Meal Options", value=meal_options_text, placeholder="e.g.,\nDuck\nSurf & Turf\nRisotto (vegetarian)\nStuffed Squash (vegetarian)", key=f"settings_event_meal_options_{i}", height=100) else: meal_options = "" event_address = st.text_area("Address", value=event.get('address', ''), placeholder="Enter full address (street, city, state, zip code)", key=f"settings_event_address_{i}", height=80) # Calculate date_offset from the selected date date_offset = (event_date - wedding_start).days # Parse meal options meal_options_list = [] if requires_meal_choice and meal_options: meal_options_list = [option.strip() for option in meal_options.split('\n') if option.strip()] # Update session state st.session_state.edit_events[i] = { "name": event_name, "description": event_description, "date_offset": date_offset, "requires_meal_choice": requires_meal_choice, "meal_options": meal_options_list, "location": event_location, "address": event_address } # Save events button st.markdown("---") col1, col2, col3 = st.columns([1, 1, 1]) with col2: if st.button("💾 Save Event Changes", type="primary"): # Update the configuration with edited events updated_config = config.copy() updated_config['wedding_events'] = st.session_state.edit_events # Save the updated configuration st.session_state.config_manager.save_config(updated_config) st.success("Event changes saved successfully!") st.rerun() else: st.info("No events added yet. Click 'Add New Event' to get started!") # Event summary if st.session_state.edit_events: st.markdown("##### Event Summary") col1, col2, col3 = st.columns(3) with col1: total_events = len(st.session_state.edit_events) st.metric("Total Events", total_events) with col2: meal_events = len([e for e in st.session_state.edit_events if e.get('requires_meal_choice', False)]) st.metric("Events with Meals", meal_events) with col3: days_span = (wedding_end - wedding_start).days + 1 st.metric("Celebration Days", days_span) if __name__ == "__main__": main()