Spaces:
Sleeping
Sleeping
| 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(""" | |
| <style> | |
| .main-header { | |
| background: linear-gradient(90deg, #2d5016, #4a7c59); | |
| color: white; | |
| padding: 1rem; | |
| border-radius: 10px; | |
| text-align: center; | |
| margin-bottom: 2rem; | |
| } | |
| .main-header h1, .main-header h2, .main-header h3, .main-header p { | |
| color: white !important; | |
| } | |
| .main-header * { | |
| color: white !important; | |
| } | |
| /* Ensure green background elements always have white text regardless of theme */ | |
| .main-header, .main-header * { | |
| color: white !important; | |
| } | |
| .metric-card, .metric-card * { | |
| color: white !important; | |
| } | |
| .metric-card { | |
| background: linear-gradient(135deg, #4a7c59, #6b8e6b) !important; | |
| color: white !important; | |
| padding: 1rem; | |
| border-radius: 10px; | |
| text-align: center; | |
| margin: 0.5rem; | |
| height: 150px; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| .metric-card h1, .metric-card h2, .metric-card h3, .metric-card p { | |
| color: white !important; | |
| background: transparent !important; | |
| } | |
| .metric-card * { | |
| color: white !important; | |
| background: transparent !important; | |
| } | |
| /* Override Streamlit's default styling for elements inside metric cards */ | |
| .metric-card .stMarkdown { | |
| background: transparent !important; | |
| } | |
| .metric-card .stMarkdown * { | |
| color: white !important; | |
| background: transparent !important; | |
| } | |
| /* Additional overrides for Streamlit elements */ | |
| div[data-testid="stMarkdownContainer"] { | |
| background: transparent !important; | |
| } | |
| .metric-card div[data-testid="stMarkdownContainer"] { | |
| background: transparent !important; | |
| } | |
| .metric-card div[data-testid="stMarkdownContainer"] * { | |
| color: white !important; | |
| background: transparent !important; | |
| } | |
| /* Fix text color for dashboard metrics below cards - use dark text for better visibility */ | |
| .main .block-container h1, | |
| .main .block-container h2, | |
| .main .block-container h3, | |
| .main .block-container h4, | |
| .main .block-container h5, | |
| .main .block-container h6 { | |
| color: #262730 !important; | |
| } | |
| .main .block-container p, | |
| .main .block-container div, | |
| .main .block-container span { | |
| color: #262730 !important; | |
| } | |
| /* Specific styling for dashboard content */ | |
| .main .block-container .stMarkdown { | |
| color: #262730 !important; | |
| } | |
| .main .block-container .stMarkdown * { | |
| color: #262730 !important; | |
| } | |
| /* Override for dark mode */ | |
| @media (prefers-color-scheme: dark) { | |
| .main .block-container h1, | |
| .main .block-container h2, | |
| .main .block-container h3, | |
| .main .block-container h4, | |
| .main .block-container h5, | |
| .main .block-container h6 { | |
| color: #fafafa !important; | |
| } | |
| .main .block-container p, | |
| .main .block-container div, | |
| .main .block-container span { | |
| color: #fafafa !important; | |
| } | |
| .main .block-container .stMarkdown { | |
| color: #fafafa !important; | |
| } | |
| .main .block-container .stMarkdown * { | |
| color: #fafafa !important; | |
| } | |
| /* But keep green background elements white even in dark mode */ | |
| .main-header, .main-header * { | |
| color: white !important; | |
| } | |
| .metric-card, .metric-card * { | |
| color: white !important; | |
| } | |
| } | |
| .sidebar .sidebar-content { | |
| background: linear-gradient(180deg, #2d5016, #4a7c59); | |
| } | |
| .stButton > button { | |
| background: linear-gradient(90deg, #4a7c59, #6b8e6b); | |
| color: white; | |
| border: none; | |
| border-radius: 5px; | |
| padding: 0.5rem 1rem; | |
| } | |
| .stButton > button:hover { | |
| background: linear-gradient(90deg, #2d5016, #4a7c59); | |
| color: white; | |
| } | |
| .task-card { | |
| background: #f8f9fa; | |
| border-left: 4px solid #4a7c59; | |
| padding: 1rem; | |
| margin: 0.5rem 0; | |
| border-radius: 5px; | |
| } | |
| .guest-card { | |
| background: #f8f9fa; | |
| border: 1px solid #4a7c59; | |
| padding: 1rem; | |
| margin: 0.5rem 0; | |
| border-radius: 5px; | |
| } | |
| </style> | |
| """, 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(""" | |
| <div style=" | |
| background: linear-gradient(135deg, #2d5016, #4a7c59, #6b8e6b); | |
| color: white; | |
| padding: 4rem 2rem; | |
| text-align: center; | |
| border-radius: 15px; | |
| margin-bottom: 3rem; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.2); | |
| "> | |
| <h1 style="font-size: 3.5rem; margin-bottom: 1rem; font-weight: 700;">๐ Wedding Planner</h1> | |
| <h2 style="font-size: 1.8rem; margin-bottom: 2rem; font-weight: 300; opacity: 0.9;"> | |
| Your Complete Wedding Planning Solution | |
| </h2> | |
| <p style="font-size: 1.2rem; max-width: 600px; margin: 0 auto; line-height: 1.6;"> | |
| Organize your special day with our comprehensive wedding planning tool. | |
| Manage guests, track tasks, coordinate vendors, and create unforgettable memories. | |
| </p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Features Section | |
| st.markdown("## โจ What You Can Do") | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.markdown(""" | |
| <div style=" | |
| background: #f8f9fa; | |
| padding: 2rem; | |
| border-radius: 10px; | |
| border-left: 4px solid #4a7c59; | |
| margin-bottom: 2rem; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1); | |
| "> | |
| <h3 style="color: #2d5016; margin-bottom: 1rem;">๐ฅ Guest Management</h3> | |
| <ul style="color: #666; line-height: 1.8;"> | |
| <li>Organize guest lists by groups</li> | |
| <li>Track RSVP responses</li> | |
| <li>Manage meal preferences</li> | |
| <li>Send invitations</li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with col2: | |
| st.markdown(""" | |
| <div style=" | |
| background: #f8f9fa; | |
| padding: 2rem; | |
| border-radius: 10px; | |
| border-left: 4px solid #4a7c59; | |
| margin-bottom: 2rem; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1); | |
| "> | |
| <h3 style="color: #2d5016; margin-bottom: 1rem;">๐ Task Tracking</h3> | |
| <ul style="color: #666; line-height: 1.8;"> | |
| <li>Create and assign tasks</li> | |
| <li>Set deadlines and priorities</li> | |
| <li>Track progress</li> | |
| <li>Organize by categories</li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with col3: | |
| st.markdown(""" | |
| <div style=" | |
| background: #f8f9fa; | |
| padding: 2rem; | |
| border-radius: 10px; | |
| border-left: 4px solid #4a7c59; | |
| margin-bottom: 2rem; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1); | |
| "> | |
| <h3 style="color: #2d5016; margin-bottom: 1rem;">๐ข Vendor Management</h3> | |
| <ul style="color: #666; line-height: 1.8;"> | |
| <li>Track vendor contacts</li> | |
| <li>Manage payment schedules</li> | |
| <li>Store contracts</li> | |
| <li>Monitor bookings</li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Additional Features | |
| col4, col5, col6 = st.columns(3) | |
| with col4: | |
| st.markdown(""" | |
| <div style=" | |
| background: #f8f9fa; | |
| padding: 2rem; | |
| border-radius: 10px; | |
| border-left: 4px solid #4a7c59; | |
| margin-bottom: 2rem; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1); | |
| "> | |
| <h3 style="color: #2d5016; margin-bottom: 1rem;">๐ฐ Wedding Party</h3> | |
| <ul style="color: #666; line-height: 1.8;"> | |
| <li>Manage bridal party</li> | |
| <li>Track responsibilities</li> | |
| <li>Coordinate schedules</li> | |
| <li>Store contact info</li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with col5: | |
| st.markdown(""" | |
| <div style=" | |
| background: #f8f9fa; | |
| padding: 2rem; | |
| border-radius: 10px; | |
| border-left: 4px solid #4a7c59; | |
| margin-bottom: 2rem; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1); | |
| "> | |
| <h3 style="color: #2d5016; margin-bottom: 1rem;">๐ Dashboard</h3> | |
| <ul style="color: #666; line-height: 1.8;"> | |
| <li>Visual progress tracking</li> | |
| <li>Key metrics overview</li> | |
| <li>Timeline management</li> | |
| <li>Quick insights</li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with col6: | |
| st.markdown(""" | |
| <div style=" | |
| background: #f8f9fa; | |
| padding: 2rem; | |
| border-radius: 10px; | |
| border-left: 4px solid #4a7c59; | |
| margin-bottom: 2rem; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1); | |
| "> | |
| <h3 style="color: #2d5016; margin-bottom: 1rem;">โ๏ธ Cloud Sync</h3> | |
| <ul style="color: #666; line-height: 1.8;"> | |
| <li>Google Drive integration</li> | |
| <li>Automatic backups</li> | |
| <li>Multi-device access</li> | |
| <li>Real-time updates</li> | |
| </ul> | |
| </div> | |
| """, 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'<div class="main-header"><h1>{header_text}</h1></div>', 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() |