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
- Organize guest lists by groups
- Track RSVP responses
- Manage meal preferences
- Send invitations
""", unsafe_allow_html=True)
with col2:
st.markdown("""
đ Task Tracking
- Create and assign tasks
- Set deadlines and priorities
- Track progress
- Organize by categories
""", unsafe_allow_html=True)
with col3:
st.markdown("""
đĸ Vendor Management
- Track vendor contacts
- Manage payment schedules
- Store contracts
- Monitor bookings
""", unsafe_allow_html=True)
# Additional Features
col4, col5, col6 = st.columns(3)
with col4:
st.markdown("""
đ° Wedding Party
- Manage bridal party
- Track responsibilities
- Coordinate schedules
- Store contact info
""", unsafe_allow_html=True)
with col5:
st.markdown("""
đ Dashboard
- Visual progress tracking
- Key metrics overview
- Timeline management
- Quick insights
""", unsafe_allow_html=True)
with col6:
st.markdown("""
âī¸ Cloud Sync
- Google Drive integration
- Automatic backups
- Multi-device access
- Real-time updates
""", 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()