umangchaudhry commited on
Commit
b03a18c
·
verified ·
1 Parent(s): 5f9023d

Upload 11 files

Browse files
Files changed (4) hide show
  1. app.py +827 -229
  2. config_manager.py +0 -64
  3. google_drive_manager.py +24 -118
  4. requirements.txt +2 -2
app.py CHANGED
@@ -16,7 +16,7 @@ from vendors import VendorManager
16
  # Page configuration
17
  st.set_page_config(
18
  page_title="Wedding Planner",
19
- page_icon="👑",
20
  layout="wide",
21
  initial_sidebar_state="expanded"
22
  )
@@ -212,108 +212,12 @@ def load_auth_config():
212
  config = yaml.load(file, Loader=SafeLoader)
213
  return config
214
 
215
- st.error(" No auth config found")
216
  return None
217
  except Exception as e:
218
  st.error(f"Error loading authentication config: {e}")
219
  return None
220
 
221
- def save_auth_config(auth_config):
222
- """Save authentication configuration to Google Drive and local storage"""
223
- try:
224
- config_manager = st.session_state.config_manager
225
-
226
- # Convert to YAML string
227
- config_yaml = yaml.dump(auth_config, default_flow_style=False, sort_keys=False)
228
-
229
- # Save to Google Drive if enabled
230
- if config_manager.google_drive_enabled:
231
- success = config_manager.drive_manager.upload_file('config.yaml', config_yaml)
232
- if not success:
233
- st.error("Failed to save authentication config to Google Drive")
234
- return False
235
-
236
- # Also save locally as backup
237
- try:
238
- with open('config.yaml', 'w') as file:
239
- yaml.dump(auth_config, file, default_flow_style=False, sort_keys=False)
240
- except Exception as e:
241
- print(f"Warning: Could not save local config backup: {e}")
242
-
243
- return True
244
- except Exception as e:
245
- st.error(f"Error saving authentication config: {e}")
246
- return False
247
-
248
- def create_wedding_folder_structure(username, wedding_name, partner1, partner2):
249
- """Create the folder structure and initial files for a new wedding"""
250
- try:
251
- config_manager = st.session_state.config_manager
252
-
253
- if not config_manager.google_drive_enabled:
254
- st.error("Google Drive is required for creating new weddings")
255
- return False
256
-
257
- # Create wedding folder name (sanitize it)
258
- folder_name = username.lower().replace(' ', '').replace('@', '').replace('.', '')
259
-
260
- # Create default wedding configuration
261
- default_wedding_config = {
262
- 'wedding_info': {
263
- 'partner1_name': partner1,
264
- 'partner2_name': partner2,
265
- 'wedding_start_date': '',
266
- 'wedding_end_date': '',
267
- 'venue_city': ''
268
- },
269
- 'custom_settings': {
270
- 'custom_tags': [
271
- "Wedding Party", "Urgent", "Rehearsal", "Timeline",
272
- "Maid of Honor", "Best Man", "Bridesmaid", "Groomsman",
273
- "Flower Girl", "Ring Bearer", "Usher", "Reader",
274
- "Venue", "Catering", "Photography", "Videography", "Music/DJ",
275
- "Flowers", "Decor", "Attire", "Hair & Makeup", "Transportation",
276
- "Invitations", "Cake", "Officiant", "Other",
277
- "Decorations", "Centerpieces", "Favors", "Signage", "Linens",
278
- "Tableware", "Lighting", "Accessories", "Stationery", "Gifts",
279
- "Vendor", "Item", "Deposit Required", "Final Payment", "Installment",
280
- "Food & Beverage", "Music", "Entertainment", "Lodging"
281
- ],
282
- 'task_assignees': []
283
- },
284
- 'wedding_events': [
285
- {"name": "Welcome Dinner", "date_offset": -1, "description": "Welcome dinner for out-of-town guests", "requires_meal_choice": True, "meal_options": ["Duck", "Surf & Turf", "Risotto (vegetarian)", "Stuffed Squash (vegetarian)"], "location": "", "address": ""},
286
- {"name": "Church Ceremony", "date_offset": 0, "description": "Main wedding ceremony", "requires_meal_choice": False, "meal_options": [], "location": "", "address": ""},
287
- {"name": "Reception", "date_offset": 0, "description": "Wedding reception with dinner", "requires_meal_choice": True, "meal_options": ["Duck", "Surf & Turf", "Risotto (vegetarian)", "Stuffed Squash (vegetarian)"], "location": "", "address": ""},
288
- {"name": "Mehndi Afterparty", "date_offset": 1, "description": "Mehndi celebration and afterparty", "requires_meal_choice": False, "meal_options": [], "location": "", "address": ""},
289
- {"name": "Indian Ceremony", "date_offset": 1, "description": "Traditional Indian wedding ceremony", "requires_meal_choice": False, "meal_options": [], "location": "", "address": ""},
290
- {"name": "Indian Reception", "date_offset": 1, "description": "Indian reception celebration", "requires_meal_choice": True, "meal_options": ["Duck", "Surf & Turf", "Risotto (vegetarian)", "Stuffed Squash (vegetarian)"], "location": "", "address": ""}
291
- ]
292
- }
293
-
294
- # Create empty data files
295
- empty_data_files = {
296
- 'guest_list_data.json': [],
297
- 'rsvp_data.json': {},
298
- 'tasks.json': [],
299
- 'vendors.json': [],
300
- 'wedding_party.json': []
301
- }
302
-
303
- # Upload wedding config to user's folder
304
- if not config_manager.drive_manager.upload_file(f'{folder_name}/wedding_config.json', default_wedding_config):
305
- return False
306
-
307
- # Upload empty data files to user's folder
308
- for filename, data in empty_data_files.items():
309
- if not config_manager.drive_manager.upload_file(f'{folder_name}/{filename}', data):
310
- return False
311
-
312
- return folder_name
313
- except Exception as e:
314
- st.error(f"Error creating wedding folder structure: {e}")
315
- return False
316
-
317
  def get_user_folder_from_username(username):
318
  """Get the user folder based on username using wedding mappings"""
319
  try:
@@ -381,7 +285,7 @@ def main():
381
  st.error("Failed to load authentication configuration. Please check your Google Drive setup.")
382
  st.stop()
383
 
384
- # Create authenticator with updated API for 0.4.2
385
  authenticator = stauth.Authenticate(
386
  st.session_state.auth_config['credentials'],
387
  st.session_state.auth_config['cookie']['name'],
@@ -389,16 +293,20 @@ def main():
389
  st.session_state.auth_config['cookie']['expiry_days']
390
  )
391
 
 
 
 
 
392
  # Check if user is already authenticated
393
  if st.session_state.get('authentication_status'):
394
  # User is authenticated, show main app
395
  show_main_app(authenticator)
396
  else:
397
- # Show login/signup page and handle authentication
398
  show_login_page(authenticator)
399
 
400
  def show_login_page(authenticator):
401
- """Show the login page with signup option"""
402
 
403
  # Hero Section
404
  st.markdown("""
@@ -411,7 +319,7 @@ def show_login_page(authenticator):
411
  margin-bottom: 3rem;
412
  box-shadow: 0 10px 30px rgba(0,0,0,0.2);
413
  ">
414
- <h1 style="font-size: 3.5rem; margin-bottom: 1rem; font-weight: 700;">👑 Wedding Planner</h1>
415
  <h2 style="font-size: 1.8rem; margin-bottom: 2rem; font-weight: 300; opacity: 0.9;">
416
  Your Complete Wedding Planning Solution
417
  </h2>
@@ -422,7 +330,7 @@ def show_login_page(authenticator):
422
  </div>
423
  """, unsafe_allow_html=True)
424
 
425
- # Features Section (existing code remains the same)
426
  st.markdown("## ✨ What You Can Do")
427
 
428
  col1, col2, col3 = st.columns(3)
@@ -500,7 +408,7 @@ def show_login_page(authenticator):
500
  margin-bottom: 2rem;
501
  box-shadow: 0 4px 6px rgba(0,0,0,0.1);
502
  ">
503
- <h3 style="color: #2d5016; margin-bottom: 1rem;">💐 Wedding Party</h3>
504
  <ul style="color: #666; line-height: 1.8;">
505
  <li>Manage bridal party</li>
506
  <li>Track responsibilities</li>
@@ -552,113 +460,29 @@ def show_login_page(authenticator):
552
 
553
  st.markdown("---")
554
 
555
- # Login/Signup Section
556
- tab1, tab2 = st.tabs(["🔐 Login", "📝 Create New Wedding"])
557
 
558
- with tab1:
559
- st.markdown("## 🔐 Login to Your Wedding Planner")
560
-
561
- # Login form - Updated for 0.4.2 API
562
- try:
563
- authenticator.login(location='main')
564
- except Exception as e:
565
- st.error(f"Login error: {e}")
566
-
567
- # Check authentication status and show appropriate message
568
- if st.session_state.get('authentication_status') is False:
569
- st.error("⚠ Invalid username or password")
570
- elif st.session_state.get('authentication_status') is None:
571
- st.info("🔐 Please enter your username and password")
572
- elif st.session_state.get('authentication_status'):
573
- st.success(f"✅ Welcome, {st.session_state.get('name', 'User')}!")
574
- st.rerun() # Refresh to show main app
575
 
576
- with tab2:
577
- st.markdown("## 📝 Create Your Wedding Planner")
578
- st.markdown("Sign up to create your personalized wedding planning workspace!")
579
-
580
- # Signup form using register_user widget - Updated for 0.4.2 API
581
- try:
582
- email, username, name = authenticator.register_user(
583
- location='main',
584
- pre_authorized=None, # Allow anyone to register
585
- fields={'Form name': 'Create Your Wedding Account',
586
- 'Email': 'Email Address',
587
- 'Username': 'Username',
588
- 'Password': 'Password',
589
- 'Repeat password': 'Confirm Password',
590
- 'First name': 'First Partner Name',
591
- 'Last name': 'Second Partner Name',
592
- 'Register': 'Create Wedding Account'},
593
- captcha=False, # Disable captcha for simplicity
594
- merge_username_email=False, # Keep username and email separate
595
- password_hint=False, # Disable password hint
596
- key='register_user'
597
- )
598
-
599
- if email:
600
- st.success(f"🎉 Welcome to Wedding Planner! Account created for {name}")
601
-
602
- # Extract partner names from the registration
603
- # For the updated API, 'name' might contain "FirstName LastName"
604
- name_parts = name.split(' ', 1) if name else ['', '']
605
- partner1 = name_parts[0] if len(name_parts) > 0 else 'Partner 1'
606
- partner2 = name_parts[1] if len(name_parts) > 1 else 'Partner 2'
607
-
608
- # Create wedding folder structure
609
- with st.spinner("Setting up your wedding workspace..."):
610
- folder_name = create_wedding_folder_structure(username, f"{partner1} & {partner2}", partner1, partner2)
611
-
612
- if folder_name:
613
- # Update the wedding mappings in auth config
614
- auth_config = st.session_state.auth_config
615
-
616
- # Initialize wedding_mappings if it doesn't exist
617
- if 'wedding_mappings' not in auth_config:
618
- auth_config['wedding_mappings'] = {}
619
-
620
- # Add new wedding mapping
621
- wedding_display_name = f"{partner1} & {partner2}"
622
- auth_config['wedding_mappings'][wedding_display_name] = {
623
- 'folder': folder_name,
624
- 'users': [username],
625
- 'wedding_name': wedding_display_name
626
- }
627
-
628
- # Save updated auth config
629
- if save_auth_config(auth_config):
630
- st.session_state.auth_config = auth_config
631
- st.success("✅ Wedding workspace created successfully!")
632
- st.info("🔄 Please refresh the page and log in with your new account to complete the setup.")
633
-
634
- # Show next steps
635
- st.markdown("### 🎯 Next Steps:")
636
- st.markdown(f"""
637
- 1. **Log in** using your credentials:
638
- - Username: `{username}`
639
- - Password: The password you just created
640
-
641
- 2. **Complete your wedding setup** by adding:
642
- - Wedding dates and venue
643
- - Wedding events and timeline
644
- - Guest lists and vendors
645
-
646
- 3. **Start planning** your perfect day!
647
- """)
648
- else:
649
- st.error("❌ Failed to save wedding configuration. Please try again.")
650
- else:
651
- st.error("❌ Failed to create wedding workspace. Please try again.")
652
-
653
- except Exception as e:
654
- st.error(f"Registration error: {e}")
655
- # Additional helpful information
656
- st.info("💡 Registration requirements: unique username, valid email, and matching passwords")
657
 
658
  def show_wedding_setup_form():
659
- """Show the wedding setup form for creating a new wedding (updated for new users)"""
660
- st.markdown("### 🔧 Complete Your Wedding Setup")
661
- st.info("Welcome! Let's finish setting up your wedding planner with your specific details.")
662
 
663
  # Initialize session state for events and form data if not exists
664
  if 'setup_events' not in st.session_state:
@@ -853,6 +677,7 @@ def show_wedding_setup_form():
853
  else:
854
  st.error("Please fill in at least the partner names and wedding date range in the form above.")
855
 
 
856
  def show_main_app(authenticator):
857
  # Get current user from session state (set by authenticator.login)
858
  username = st.session_state.get('username')
@@ -891,20 +716,8 @@ def show_main_app(authenticator):
891
  st.session_state.app_initialized = True
892
  st.session_state.last_user_folder = user_folder
893
  else:
894
- # For new users, check if we need to show setup form
895
- config = config_manager.load_config()
896
- wedding_info_data = config.get('wedding_info', {})
897
-
898
- # If basic wedding info is missing, show setup form
899
- if (not wedding_info_data.get('partner1_name') or
900
- not wedding_info_data.get('partner2_name') or
901
- not wedding_info_data.get('wedding_start_date')):
902
-
903
- show_wedding_setup_form()
904
- return
905
-
906
- st.session_state.app_initialized = True
907
- st.session_state.last_user_folder = user_folder
908
 
909
  # Load config
910
  config = st.session_state.config_manager.load_config()
@@ -997,14 +810,14 @@ def show_main_app(authenticator):
997
 
998
  # Always show push button
999
  if modified_files:
1000
- st.warning(f"🔄 {len(modified_files)} unsaved changes")
1001
  if st.button("📤 Push Changes", type="primary", help="Save changes to Google Drive", key="sidebar_push"):
1002
  with st.spinner("Pushing changes..."):
1003
  if config_manager.manual_sync_to_drive():
1004
  st.success("✅ Changes saved!")
1005
  st.rerun()
1006
  else:
1007
- st.error(" Failed to save changes")
1008
  else:
1009
  st.success("✅ All changes saved")
1010
  if st.button("📤 Push to Drive", help="Save current data to Google Drive", key="sidebar_push_all"):
@@ -1013,16 +826,16 @@ def show_main_app(authenticator):
1013
  st.success("✅ Data saved!")
1014
  st.rerun()
1015
  else:
1016
- st.error(" Failed to save")
1017
 
1018
  # Always show pull button
1019
- if st.button("📄 Pull Latest", help="Get latest from Google Drive", key="sidebar_pull"):
1020
  with st.spinner("Pulling latest..."):
1021
  if config_manager.manual_sync_from_drive():
1022
  st.success("✅ Latest loaded!")
1023
  st.rerun()
1024
  else:
1025
- st.error(" Failed to load changes")
1026
 
1027
  # Route to appropriate page
1028
  if page == "Dashboard":
@@ -1040,9 +853,794 @@ def show_main_app(authenticator):
1040
  elif page == "Settings":
1041
  show_settings_page(config)
1042
 
1043
- # [Rest of the functions remain the same - show_wedding_timeline_page, get_vendors_for_event,
1044
- # get_meal_choices_for_event, show_wedding_events_section, show_settings_page, etc.]
1045
- # ... (keeping all existing functions for brevity)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1046
 
1047
  if __name__ == "__main__":
1048
  main()
 
16
  # Page configuration
17
  st.set_page_config(
18
  page_title="Wedding Planner",
19
+ page_icon="💒",
20
  layout="wide",
21
  initial_sidebar_state="expanded"
22
  )
 
212
  config = yaml.load(file, Loader=SafeLoader)
213
  return config
214
 
215
+ st.error(" No auth config found")
216
  return None
217
  except Exception as e:
218
  st.error(f"Error loading authentication config: {e}")
219
  return None
220
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  def get_user_folder_from_username(username):
222
  """Get the user folder based on username using wedding mappings"""
223
  try:
 
285
  st.error("Failed to load authentication configuration. Please check your Google Drive setup.")
286
  st.stop()
287
 
288
+ # Create authenticator (auto_hash=True by default, so passwords will be hashed automatically)
289
  authenticator = stauth.Authenticate(
290
  st.session_state.auth_config['credentials'],
291
  st.session_state.auth_config['cookie']['name'],
 
293
  st.session_state.auth_config['cookie']['expiry_days']
294
  )
295
 
296
+ # Check authentication status
297
+ if 'authentication_status' not in st.session_state:
298
+ st.session_state.authentication_status = None
299
+
300
  # Check if user is already authenticated
301
  if st.session_state.get('authentication_status'):
302
  # User is authenticated, show main app
303
  show_main_app(authenticator)
304
  else:
305
+ # Show login page and handle authentication
306
  show_login_page(authenticator)
307
 
308
  def show_login_page(authenticator):
309
+ """Show the login page"""
310
 
311
  # Hero Section
312
  st.markdown("""
 
319
  margin-bottom: 3rem;
320
  box-shadow: 0 10px 30px rgba(0,0,0,0.2);
321
  ">
322
+ <h1 style="font-size: 3.5rem; margin-bottom: 1rem; font-weight: 700;">💒 Wedding Planner</h1>
323
  <h2 style="font-size: 1.8rem; margin-bottom: 2rem; font-weight: 300; opacity: 0.9;">
324
  Your Complete Wedding Planning Solution
325
  </h2>
 
330
  </div>
331
  """, unsafe_allow_html=True)
332
 
333
+ # Features Section
334
  st.markdown("## ✨ What You Can Do")
335
 
336
  col1, col2, col3 = st.columns(3)
 
408
  margin-bottom: 2rem;
409
  box-shadow: 0 4px 6px rgba(0,0,0,0.1);
410
  ">
411
+ <h3 style="color: #2d5016; margin-bottom: 1rem;">👰 Wedding Party</h3>
412
  <ul style="color: #666; line-height: 1.8;">
413
  <li>Manage bridal party</li>
414
  <li>Track responsibilities</li>
 
460
 
461
  st.markdown("---")
462
 
463
+ # Login Section
464
+ st.markdown("## 🔐 Login to Your Wedding Planner")
465
 
466
+ # Login form
467
+ try:
468
+ authenticator.login(location='main')
469
+ except Exception as e:
470
+ st.error(f"Login error: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
471
 
472
+ # Check authentication status and show appropriate message
473
+ if st.session_state.get('authentication_status') is False:
474
+ st.error(" Invalid username or password")
475
+ elif st.session_state.get('authentication_status') is None:
476
+ st.info("🔐 Please enter your username and password")
477
+ elif st.session_state.get('authentication_status'):
478
+ st.success(f"✅ Welcome, {st.session_state.get('name', 'User')}!")
479
+ st.rerun() # Refresh to show main app
480
+
481
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482
 
483
  def show_wedding_setup_form():
484
+ """Show the wedding setup form for creating a new wedding"""
485
+ st.markdown("### 📝 Create Your Wedding Configuration")
 
486
 
487
  # Initialize session state for events and form data if not exists
488
  if 'setup_events' not in st.session_state:
 
677
  else:
678
  st.error("Please fill in at least the partner names and wedding date range in the form above.")
679
 
680
+
681
  def show_main_app(authenticator):
682
  # Get current user from session state (set by authenticator.login)
683
  username = st.session_state.get('username')
 
716
  st.session_state.app_initialized = True
717
  st.session_state.last_user_folder = user_folder
718
  else:
719
+ st.error("Failed to load wedding data.")
720
+ return
 
 
 
 
 
 
 
 
 
 
 
 
721
 
722
  # Load config
723
  config = st.session_state.config_manager.load_config()
 
810
 
811
  # Always show push button
812
  if modified_files:
813
+ st.warning(f"📝 {len(modified_files)} unsaved changes")
814
  if st.button("📤 Push Changes", type="primary", help="Save changes to Google Drive", key="sidebar_push"):
815
  with st.spinner("Pushing changes..."):
816
  if config_manager.manual_sync_to_drive():
817
  st.success("✅ Changes saved!")
818
  st.rerun()
819
  else:
820
+ st.error(" Failed to save changes")
821
  else:
822
  st.success("✅ All changes saved")
823
  if st.button("📤 Push to Drive", help="Save current data to Google Drive", key="sidebar_push_all"):
 
826
  st.success("✅ Data saved!")
827
  st.rerun()
828
  else:
829
+ st.error(" Failed to save")
830
 
831
  # Always show pull button
832
+ if st.button("🔄 Pull Latest", help="Get latest from Google Drive", key="sidebar_pull"):
833
  with st.spinner("Pulling latest..."):
834
  if config_manager.manual_sync_from_drive():
835
  st.success("✅ Latest loaded!")
836
  st.rerun()
837
  else:
838
+ st.error(" Failed to load changes")
839
 
840
  # Route to appropriate page
841
  if page == "Dashboard":
 
853
  elif page == "Settings":
854
  show_settings_page(config)
855
 
856
+ def show_wedding_timeline_page(config):
857
+ st.markdown("## 📅 Wedding Overview")
858
+
859
+ # Show wedding events directly without tabs
860
+ show_wedding_events_section(config)
861
+
862
+ def get_vendors_for_event(event_name, vendors_data):
863
+ """Get vendors associated with a specific event, handling multiple categories per vendor"""
864
+ event_vendors = []
865
+ for vendor in vendors_data:
866
+ vendor_events = vendor.get('events', [])
867
+ if event_name in vendor_events:
868
+ # Get all categories for this vendor
869
+ categories = vendor.get('categories', [])
870
+ primary_category = vendor.get('category', '')
871
+
872
+ # If no categories array, use the primary category
873
+ if not categories and primary_category:
874
+ categories = [primary_category]
875
+
876
+ # If still no categories, use a default
877
+ if not categories:
878
+ categories = ['Vendor/Service']
879
+
880
+ # Create an entry for each category this vendor serves
881
+ for category in categories:
882
+ event_vendors.append({
883
+ 'name': vendor.get('name', ''),
884
+ 'category': category,
885
+ 'status': vendor.get('status', ''),
886
+ 'vendor_id': vendor.get('id', '') # Add ID to help identify duplicates
887
+ })
888
+
889
+ return event_vendors
890
+
891
+ def get_meal_choices_for_event(event_name, rsvp_data, meal_options):
892
+ """Get meal choice counts for a specific event from RSVP data"""
893
+ meal_counts = {}
894
+
895
+ # Initialize counts for all meal options
896
+ for option in meal_options:
897
+ meal_counts[option] = 0
898
+
899
+ # Count meal choices from RSVP data
900
+ for group_code, group_data in rsvp_data.items():
901
+ event_responses = group_data.get('event_responses', {})
902
+ if event_name in event_responses:
903
+ event_data = event_responses[event_name]
904
+ meal_choices = event_data.get('meal_choice', {})
905
+
906
+ for attendee, choice in meal_choices.items():
907
+ if choice in meal_counts:
908
+ meal_counts[choice] += 1
909
+
910
+ return meal_counts
911
+
912
+ def show_wedding_events_section(config):
913
+ st.markdown("## 📅 Wedding Events")
914
+
915
+ wedding_events = config.get('wedding_events', [])
916
+ wedding_info = config.get('wedding_info', {})
917
+
918
+ if not wedding_events:
919
+ st.info("No events configured yet. Please complete the setup to define your wedding events.")
920
+ return
921
+
922
+ # Get wedding dates
923
+ wedding_start_str = wedding_info.get('wedding_start_date', '')
924
+ wedding_end_str = wedding_info.get('wedding_end_date', '')
925
+
926
+ if wedding_start_str and wedding_end_str:
927
+ try:
928
+ wedding_start = datetime.fromisoformat(wedding_start_str).date()
929
+ wedding_end = datetime.fromisoformat(wedding_end_str).date()
930
+ except:
931
+ st.error("Invalid wedding date format. Please check your settings.")
932
+ return
933
+ else:
934
+ st.warning("Wedding dates not set. Please complete the setup.")
935
+ return
936
+
937
+ # Load vendors and RSVP data
938
+ config_manager = ConfigManager()
939
+ vendors_data = config_manager.load_json_data('vendors.json')
940
+ rsvp_data = config_manager.load_json_data('rsvp_data.json')
941
+
942
+ # Display events
943
+ st.markdown(f"### Your Wedding Events ({len(wedding_events)} total)")
944
+
945
+ # Group events by day
946
+ events_by_day = {}
947
+ for event in wedding_events:
948
+ event_date = wedding_start + timedelta(days=event.get('date_offset', 0))
949
+ day_key = event_date.strftime('%Y-%m-%d')
950
+ if day_key not in events_by_day:
951
+ events_by_day[day_key] = []
952
+ events_by_day[day_key].append(event)
953
+
954
+ # Sort days
955
+ sorted_days = sorted(events_by_day.keys())
956
+
957
+ for day in sorted_days:
958
+ day_date = datetime.fromisoformat(day).date()
959
+ day_events = events_by_day[day]
960
+
961
+ # Determine day label
962
+ if day_date == wedding_start:
963
+ day_label = "Day 1 - Wedding Start"
964
+ elif day_date == wedding_end:
965
+ day_label = "Final Day - Wedding End"
966
+ else:
967
+ days_from_start = (day_date - wedding_start).days
968
+ day_label = f"Day {days_from_start + 1}"
969
+
970
+ st.markdown(f"#### {day_label} - {day_date.strftime('%B %d, %Y')}")
971
+
972
+ for event in day_events:
973
+ # Simple event display
974
+ time_info = event.get('description', '') or 'Time TBD'
975
+ location = event.get('location', '') or 'Location TBD'
976
+ address = event.get('address', '')
977
+ meal_required = event.get('requires_meal_choice', False)
978
+ event_name = event.get('name', 'Untitled Event')
979
+
980
+ st.markdown(f"**{event_name}**")
981
+ st.markdown(f"🕐 **Time:** {time_info}")
982
+ st.markdown(f"📍 **Location:** {location}")
983
+ if address:
984
+ st.markdown(f"🏠 **Address:** {address}")
985
+ st.markdown(f"🍽️ **Meal Choice:** {'Required' if meal_required else 'Not Required'}")
986
+
987
+ # Display meal choices and counts if meal choice is required
988
+ if meal_required:
989
+ meal_options = event.get('meal_options', [])
990
+ if meal_options:
991
+ meal_counts = get_meal_choices_for_event(event_name, rsvp_data, meal_options)
992
+ st.markdown("🍽️ **Meal Choices:**")
993
+ for option in meal_options:
994
+ count = meal_counts.get(option, 0)
995
+ st.markdown(f" • **{option}:** {count} orders")
996
+ else:
997
+ st.markdown("🍽️ **Meal Choices:** No options configured")
998
+
999
+ # Display vendors for this event
1000
+ event_vendors = get_vendors_for_event(event_name, vendors_data)
1001
+ if event_vendors:
1002
+ st.markdown("🏢 **Vendors:**")
1003
+
1004
+ # Group vendors by name to handle multiple categories
1005
+ vendors_by_name = {}
1006
+ for vendor in event_vendors:
1007
+ vendor_name = vendor['name']
1008
+ if vendor_name not in vendors_by_name:
1009
+ vendors_by_name[vendor_name] = {
1010
+ 'categories': [],
1011
+ 'status': vendor['status']
1012
+ }
1013
+ vendors_by_name[vendor_name]['categories'].append(vendor['category'])
1014
+
1015
+ # Display grouped vendors
1016
+ for vendor_name, vendor_info in vendors_by_name.items():
1017
+ status_emoji = "✅" if vendor_info['status'] == "Booked" else "⏳" if vendor_info['status'] == "Researching" else "📋"
1018
+ categories_text = ", ".join(vendor_info['categories'])
1019
+ st.markdown(f" • {status_emoji} **{categories_text}:** {vendor_name}")
1020
+ else:
1021
+ st.markdown("🏢 **Vendors:** None assigned")
1022
+
1023
+ st.markdown("---")
1024
+
1025
+ # Event summary
1026
+ st.markdown("### Event Summary")
1027
+
1028
+ col1, col2, col3 = st.columns(3)
1029
+
1030
+ with col1:
1031
+ total_events = len(wedding_events)
1032
+ st.metric("Total Events", total_events)
1033
+
1034
+ with col2:
1035
+ meal_events = len([e for e in wedding_events if e.get('requires_meal_choice', False)])
1036
+ st.metric("Events with Meals", meal_events)
1037
+
1038
+ with col3:
1039
+ days_span = (wedding_end - wedding_start).days + 1
1040
+ st.metric("Celebration Days", days_span)
1041
+
1042
+ def show_settings_page(config):
1043
+ st.markdown("### Settings")
1044
+
1045
+ # Check demo mode status
1046
+ is_demo_mode = st.session_state.config_manager.is_demo_mode()
1047
+
1048
+ # Demo mode toggle at the top
1049
+ st.markdown("#### Demo Mode")
1050
+ col1, col2 = st.columns([3, 1])
1051
+ with col1:
1052
+ if is_demo_mode:
1053
+ 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.")
1054
+ else:
1055
+ st.info("📝 **Demo Mode is OFF** - You are viewing your actual wedding data.")
1056
+
1057
+ with col2:
1058
+ if st.button("Toggle Demo Mode", type="secondary"):
1059
+ if st.session_state.config_manager.toggle_demo_mode():
1060
+ st.success("Demo mode toggled! Please refresh the page.")
1061
+ st.rerun()
1062
+ else:
1063
+ st.error("Failed to toggle demo mode.")
1064
+
1065
+ # Create tabs for different settings
1066
+ tab1, tab2, tab3, tab4, tab5, tab6 = st.tabs(["Edit Configuration", "Manage Events", "Google Drive", "Cache Management", "Current Configuration", "Reset"])
1067
+
1068
+ with tab1:
1069
+ st.markdown("#### Edit Wedding Configuration")
1070
+
1071
+ # Initialize session state for editing if not exists
1072
+ if 'edit_config' not in st.session_state:
1073
+ st.session_state.edit_config = config.copy()
1074
+
1075
+ with st.form("edit_wedding_config"):
1076
+ st.markdown("##### Basic Wedding Information")
1077
+
1078
+ col1, col2 = st.columns(2)
1079
+ with col1:
1080
+ partner1_name = st.text_input("Partner 1 Name", value=st.session_state.edit_config.get('wedding_info', {}).get('partner1_name', ''))
1081
+ partner2_name = st.text_input("Partner 2 Name", value=st.session_state.edit_config.get('wedding_info', {}).get('partner2_name', ''))
1082
+ venue_city = st.text_input("City", value=st.session_state.edit_config.get('wedding_info', {}).get('venue_city', ''))
1083
+
1084
+ with col2:
1085
+ # Get current dates
1086
+ wedding_start_str = st.session_state.edit_config.get('wedding_info', {}).get('wedding_start_date', '')
1087
+ wedding_end_str = st.session_state.edit_config.get('wedding_info', {}).get('wedding_end_date', '')
1088
+
1089
+ try:
1090
+ if wedding_start_str:
1091
+ wedding_start = datetime.fromisoformat(wedding_start_str).date()
1092
+ else:
1093
+ wedding_start = date.today()
1094
+
1095
+ if wedding_end_str:
1096
+ wedding_end = datetime.fromisoformat(wedding_end_str).date()
1097
+ else:
1098
+ wedding_end = date.today()
1099
+ except:
1100
+ wedding_start = date.today()
1101
+ wedding_end = date.today()
1102
+
1103
+ wedding_start_date = st.date_input("Wedding Start Date", value=wedding_start)
1104
+ wedding_end_date = st.date_input("Wedding End Date", value=wedding_end)
1105
+
1106
+ if wedding_end_date < wedding_start_date:
1107
+ st.error("End date must be after start date")
1108
+ wedding_end_date = wedding_start_date
1109
+
1110
+ st.markdown("##### Custom Tags")
1111
+ current_tags = st.session_state.edit_config.get('custom_settings', {}).get('custom_tags', [])
1112
+ custom_tags_text = '\n'.join(current_tags) if current_tags else ''
1113
+ custom_tags = st.text_area("Custom Tags (one per line)", value=custom_tags_text, placeholder="e.g.,\nUrgent\nHigh Priority\nDeposit Required")
1114
+
1115
+ st.markdown("##### Task Assignees")
1116
+ current_assignees = st.session_state.edit_config.get('custom_settings', {}).get('task_assignees', [])
1117
+ task_assignees_text = '\n'.join(current_assignees) if current_assignees else ''
1118
+ task_assignees = st.text_area("Task Assignees (one per line)", value=task_assignees_text, placeholder="e.g.,\nMom\nDad\nWedding Planner\nBest Friend\nCoordinator")
1119
+
1120
+ submitted = st.form_submit_button("Save Changes", type="primary")
1121
+
1122
+ if submitted:
1123
+ # Update the configuration
1124
+ updated_config = st.session_state.edit_config.copy()
1125
+ updated_config['wedding_info'] = {
1126
+ 'partner1_name': partner1_name,
1127
+ 'partner2_name': partner2_name,
1128
+ 'venue_city': venue_city,
1129
+ 'wedding_start_date': wedding_start_date.isoformat(),
1130
+ 'wedding_end_date': wedding_end_date.isoformat()
1131
+ }
1132
+
1133
+ # Parse custom tags and task assignees
1134
+ custom_tags_list = [tag.strip() for tag in custom_tags.split('\n') if tag.strip()]
1135
+ task_assignees_list = [assignee.strip() for assignee in task_assignees.split('\n') if assignee.strip()]
1136
+ updated_config['custom_settings'] = {
1137
+ 'custom_tags': custom_tags_list,
1138
+ 'task_assignees': task_assignees_list
1139
+ }
1140
+
1141
+ # Save the updated configuration
1142
+ st.session_state.config_manager.save_config(updated_config)
1143
+ st.success("Configuration updated successfully!")
1144
+ st.rerun()
1145
+
1146
+ with tab2:
1147
+ show_event_management_section(config)
1148
+
1149
+ with tab3:
1150
+ show_google_drive_section()
1151
+
1152
+ with tab4:
1153
+ show_cache_management_section()
1154
+
1155
+ with tab5:
1156
+ st.markdown("#### Current Configuration")
1157
+ st.json(config)
1158
+
1159
+ with tab6:
1160
+ st.markdown("#### Reset Configuration")
1161
+ st.warning("⚠️ This will permanently delete all your wedding configuration data. This action cannot be undone.")
1162
+
1163
+ if st.button("Reset Configuration", type="secondary"):
1164
+ if st.session_state.config_manager.reset_config():
1165
+ st.success("Configuration reset! Please refresh the page.")
1166
+ st.rerun()
1167
+
1168
+ def show_cache_management_section():
1169
+ """Show cache management section in settings"""
1170
+ st.markdown("#### Cache Management")
1171
+
1172
+ config_manager = st.session_state.config_manager
1173
+
1174
+ # Get cache status
1175
+ cache_status = config_manager.get_cache_status()
1176
+
1177
+ st.markdown("**Current Cache Status:**")
1178
+
1179
+ col1, col2 = st.columns(2)
1180
+
1181
+ with col1:
1182
+ st.metric("Total Cached Items", cache_status['total_cached'])
1183
+ if cache_status['config_cached']:
1184
+ st.success("✅ Wedding configuration cached")
1185
+ else:
1186
+ st.info("ℹ️ Wedding configuration not cached")
1187
+
1188
+ with col2:
1189
+ if cache_status['cached_data_files']:
1190
+ st.success(f"✅ {len(cache_status['cached_data_files'])} data files cached")
1191
+ with st.expander("View cached files"):
1192
+ for filename in cache_status['cached_data_files']:
1193
+ st.markdown(f"• {filename}")
1194
+ else:
1195
+ st.info("ℹ️ No data files cached")
1196
+
1197
+ st.markdown("---")
1198
+
1199
+ # Cache management buttons
1200
+ st.markdown("**Cache Actions:**")
1201
+
1202
+ col1, col2, col3 = st.columns(3)
1203
+
1204
+ with col1:
1205
+ if st.button("🗑️ Clear All Cache", help="Clear all cached data to force reload from source"):
1206
+ config_manager.clear_cache()
1207
+ st.success("Cache cleared! Data will be reloaded from source on next access.")
1208
+ st.rerun()
1209
+
1210
+ with col2:
1211
+ if st.button("🔄 Refresh Cache", help="Reload all data from Google Drive and update cache"):
1212
+ if config_manager.google_drive_enabled:
1213
+ with st.spinner("Refreshing cache from Google Drive..."):
1214
+ if config_manager.manual_sync_from_drive():
1215
+ st.success("Cache refreshed from Google Drive!")
1216
+ st.rerun()
1217
+ else:
1218
+ st.error("Failed to refresh cache from Google Drive")
1219
+ else:
1220
+ st.warning("Google Drive not enabled. Cannot refresh from Drive.")
1221
+
1222
+ with col3:
1223
+ if st.button("ℹ️ Cache Info", help="Show detailed cache information"):
1224
+ st.info("Cache helps improve performance by storing data in memory. Data is automatically cached when first loaded and updated when modified.")
1225
+
1226
+ def show_google_drive_status_setup():
1227
+ """Show Google Drive status on the setup page"""
1228
+ config_manager = st.session_state.config_manager
1229
+ drive_status = config_manager.get_google_drive_status()
1230
+
1231
+ # Create an expander for Google Drive status
1232
+ with st.expander("🔗 Google Drive Connection Status", expanded=False):
1233
+ # Status display
1234
+ if drive_status['enabled']:
1235
+ if drive_status['status'] == 'Online':
1236
+ st.success(f"✅ {drive_status['message']}")
1237
+ elif drive_status['status'] == 'Offline':
1238
+ st.warning(f"⚠️ {drive_status['message']}")
1239
+ else:
1240
+ st.error(f"❌ {drive_status['message']}")
1241
+ else:
1242
+ st.info(f"ℹ️ {drive_status['message']}")
1243
+
1244
+ # Show files if available
1245
+ if drive_status['enabled'] and 'files' in drive_status:
1246
+ st.markdown("**Files found in Google Drive:**")
1247
+ for file_name in drive_status['files']:
1248
+ friendly_name = get_friendly_file_name(file_name)
1249
+ st.markdown(f"• {friendly_name}")
1250
+
1251
+ # Manual sync buttons (if enabled) - only show pull and load options
1252
+ if drive_status['enabled']:
1253
+ st.markdown("**Manual Sync:**")
1254
+ col1, col2 = st.columns(2)
1255
+
1256
+ with col1:
1257
+ if st.button("📥 Sync from Google Drive", help="Download latest data from Google Drive", key="setup_sync_from"):
1258
+ with st.spinner("Syncing from Google Drive..."):
1259
+ if config_manager.manual_sync_from_drive():
1260
+ st.success("Successfully synced from Google Drive!")
1261
+ st.rerun()
1262
+ else:
1263
+ st.error("Failed to sync from Google Drive")
1264
+
1265
+ with col2:
1266
+ if st.button("🔄 Load Existing Data", help="Load existing wedding data from Google Drive", key="setup_load_existing"):
1267
+ with st.spinner("Loading existing data from Google Drive..."):
1268
+ if config_manager.load_existing_data_from_drive():
1269
+ st.success("Successfully loaded existing data from Google Drive!")
1270
+ st.info("Your wedding data has been loaded. The page will refresh to show your wedding planner.")
1271
+ st.rerun()
1272
+ else:
1273
+ st.error("Failed to load existing data from Google Drive")
1274
+
1275
+ # Configuration help
1276
+ if not drive_status['enabled']:
1277
+ st.markdown("**To enable Google Drive integration:**")
1278
+ st.markdown("Set the following **secrets** in your Hugging Face Space settings:")
1279
+ st.code("""
1280
+ GOOGLE_DRIVE_FOLDER_ID=your_folder_id
1281
+ GOOGLE_PROJECT_ID=your_project_id
1282
+ GOOGLE_PRIVATE_KEY_ID=your_private_key_id
1283
+ GOOGLE_PRIVATE_KEY=your_private_key
1284
+ GOOGLE_CLIENT_EMAIL=your_client_email
1285
+ GOOGLE_CLIENT_ID=your_client_id
1286
+ """)
1287
+
1288
+ # Debug information
1289
+ st.markdown("**Debug Information:**")
1290
+ folder_id = os.getenv('GOOGLE_DRIVE_FOLDER_ID')
1291
+ if folder_id:
1292
+ st.success(f"✅ GOOGLE_DRIVE_FOLDER_ID found: {folder_id[:10]}...")
1293
+ else:
1294
+ st.error("❌ GOOGLE_DRIVE_FOLDER_ID not found")
1295
+
1296
+ # Check other variables
1297
+ project_id = os.getenv('GOOGLE_PROJECT_ID')
1298
+ client_email = os.getenv('GOOGLE_CLIENT_EMAIL')
1299
+ private_key_id = os.getenv('GOOGLE_PRIVATE_KEY_ID')
1300
+ private_key = os.getenv('GOOGLE_PRIVATE_KEY')
1301
+ client_id = os.getenv('GOOGLE_CLIENT_ID')
1302
+
1303
+ if project_id:
1304
+ st.success(f"✅ GOOGLE_PROJECT_ID found: {project_id[:10]}...")
1305
+ else:
1306
+ st.error("❌ GOOGLE_PROJECT_ID not found")
1307
+
1308
+ if client_email:
1309
+ st.success(f"✅ GOOGLE_CLIENT_EMAIL found: {client_email}")
1310
+ else:
1311
+ st.error("❌ GOOGLE_CLIENT_EMAIL not found")
1312
+
1313
+ if private_key_id:
1314
+ st.success(f"✅ GOOGLE_PRIVATE_KEY_ID found: {private_key_id[:10]}...")
1315
+ else:
1316
+ st.error("❌ GOOGLE_PRIVATE_KEY_ID not found")
1317
+
1318
+ if private_key:
1319
+ st.success(f"✅ GOOGLE_PRIVATE_KEY found: {len(private_key)} characters")
1320
+ else:
1321
+ st.error("❌ GOOGLE_PRIVATE_KEY not found")
1322
+
1323
+ if client_id:
1324
+ st.success(f"✅ GOOGLE_CLIENT_ID found: {client_id[:10]}...")
1325
+ else:
1326
+ st.error("❌ GOOGLE_CLIENT_ID not found")
1327
+
1328
+ # Check if all required variables are present
1329
+ all_required = all([folder_id, project_id, private_key_id, private_key, client_email, client_id])
1330
+ if all_required:
1331
+ st.success("🎉 All required secrets are present!")
1332
+ st.info("If Google Drive is still not working, there might be an authentication issue. Try restarting your Space.")
1333
+
1334
+ # Test file writing permissions
1335
+ st.markdown("**Testing file permissions:**")
1336
+ try:
1337
+ test_file = "/tmp/test_write.txt"
1338
+ with open(test_file, 'w') as f:
1339
+ f.write("test")
1340
+ st.success("✅ Can write to /tmp directory")
1341
+ os.remove(test_file)
1342
+ except Exception as e:
1343
+ st.error(f"❌ Cannot write to /tmp directory: {e}")
1344
+
1345
+ try:
1346
+ test_dir = "wedding_data"
1347
+ os.makedirs(test_dir, exist_ok=True)
1348
+ test_file = os.path.join(test_dir, "test.txt")
1349
+ with open(test_file, 'w') as f:
1350
+ f.write("test")
1351
+ st.success("✅ Can write to wedding_data directory")
1352
+ os.remove(test_file)
1353
+ except Exception as e:
1354
+ st.error(f"❌ Cannot write to wedding_data directory: {e}")
1355
+ else:
1356
+ st.warning("⚠️ Some required secrets are missing. Please add all secrets and restart your Space.")
1357
+
1358
+ def get_friendly_file_name(filename):
1359
+ """Convert technical file names to user-friendly names"""
1360
+ friendly_names = {
1361
+ 'wedding_config.json': 'Wedding Configuration',
1362
+ 'guest_list_data.json': 'Guest List',
1363
+ 'rsvp_data.json': 'RSVP Responses',
1364
+ 'tasks.json': 'Tasks',
1365
+ 'vendors.json': 'Vendors',
1366
+ 'wedding_party.json': 'Wedding Party'
1367
+ }
1368
+ return friendly_names.get(filename, filename)
1369
+
1370
+ def show_google_drive_sync_status():
1371
+ """Show Google Drive sync status and push button prominently in main app"""
1372
+ config_manager = st.session_state.config_manager
1373
+ drive_status = config_manager.get_google_drive_status()
1374
+
1375
+ # Only show if Google Drive is enabled
1376
+ if not drive_status['enabled']:
1377
+ return
1378
+
1379
+ # Get modified files
1380
+ modified_files = config_manager.get_modified_files()
1381
+
1382
+ # Create a prominent status bar with always-visible push button
1383
+ col1, col2, col3 = st.columns([2, 1, 1])
1384
+
1385
+ with col1:
1386
+ if modified_files:
1387
+ friendly_names = [get_friendly_file_name(f) for f in modified_files]
1388
+ st.warning(f"📝 **{len(modified_files)} unsaved changes** - {', '.join(friendly_names)}")
1389
+ else:
1390
+ st.success("✅ **All changes saved** - Your data is up to date with Google Drive")
1391
+
1392
+ with col2:
1393
+ # Always show push button - it will sync all current data
1394
+ if modified_files:
1395
+ button_text = f"📤 Push {len(modified_files)} Changes"
1396
+ button_help = f"Save {len(modified_files)} modified files to Google Drive"
1397
+ else:
1398
+ button_text = "📤 Push to Drive"
1399
+ button_help = "Save current data to Google Drive"
1400
+
1401
+ if st.button(button_text, type="primary", help=button_help):
1402
+ with st.spinner("Pushing changes to Google Drive..."):
1403
+ if config_manager.manual_sync_to_drive():
1404
+ st.success("✅ Changes saved to Google Drive!")
1405
+ st.rerun()
1406
+ else:
1407
+ st.error("❌ Failed to save changes to Google Drive")
1408
+
1409
+ with col3:
1410
+ if st.button("🔄 Pull Latest", help="Get latest changes from Google Drive"):
1411
+ with st.spinner("Pulling latest changes from Google Drive..."):
1412
+ if config_manager.manual_sync_from_drive():
1413
+ st.success("✅ Latest changes loaded from Google Drive!")
1414
+ st.rerun()
1415
+ else:
1416
+ st.error("❌ Failed to load changes from Google Drive")
1417
+
1418
+ # Add a small separator
1419
+ st.markdown("---")
1420
+
1421
+ def show_google_drive_section():
1422
+ st.markdown("#### Google Drive Integration")
1423
+
1424
+ config_manager = st.session_state.config_manager
1425
+ drive_status = config_manager.get_google_drive_status()
1426
+
1427
+ # Status display
1428
+ if drive_status['enabled']:
1429
+ if drive_status['status'] == 'Online':
1430
+ st.success(f"✅ {drive_status['message']}")
1431
+ elif drive_status['status'] == 'Offline':
1432
+ st.warning(f"⚠️ {drive_status['message']}")
1433
+ else:
1434
+ st.error(f"❌ {drive_status['message']}")
1435
+ else:
1436
+ st.info(f"ℹ️ {drive_status['message']}")
1437
+
1438
+ # Show files if available
1439
+ if drive_status['enabled'] and 'files' in drive_status:
1440
+ st.markdown("**Files in Google Drive:**")
1441
+ for file_name in drive_status['files']:
1442
+ friendly_name = get_friendly_file_name(file_name)
1443
+ st.markdown(f"• {friendly_name}")
1444
+
1445
+ # Show modified files status
1446
+ if drive_status['enabled']:
1447
+ modified_files = config_manager.get_modified_files()
1448
+ if modified_files:
1449
+ st.markdown("#### 📝 Modified Files (Not Synced)")
1450
+ st.warning(f"The following files have been modified and need to be synced to Google Drive:")
1451
+ for file_name in modified_files:
1452
+ friendly_name = get_friendly_file_name(file_name)
1453
+ st.markdown(f"• {friendly_name}")
1454
+ else:
1455
+ st.markdown("#### ✅ All Files Synced")
1456
+ st.success("All your data is up to date with Google Drive.")
1457
+
1458
+ # Manual sync buttons
1459
+ if drive_status['enabled']:
1460
+ st.markdown("#### Manual Sync")
1461
+ col1, col2 = st.columns(2)
1462
+
1463
+ with col1:
1464
+ if st.button("📥 Sync from Google Drive", help="Download latest data from Google Drive"):
1465
+ with st.spinner("Syncing from Google Drive..."):
1466
+ if config_manager.manual_sync_from_drive():
1467
+ st.success("Successfully synced from Google Drive!")
1468
+ st.rerun()
1469
+ else:
1470
+ st.error("Failed to sync from Google Drive")
1471
+
1472
+ with col2:
1473
+ # Show different button text based on whether there are modified files
1474
+ modified_files = config_manager.get_modified_files()
1475
+ if modified_files:
1476
+ button_text = f"📤 Sync {len(modified_files)} Modified Files to Drive"
1477
+ button_help = f"Upload {len(modified_files)} modified files to Google Drive"
1478
+ else:
1479
+ button_text = "📤 Sync to Google Drive"
1480
+ button_help = "Upload current data to Google Drive (no changes to sync)"
1481
+
1482
+ if st.button(button_text, help=button_help, disabled=not modified_files):
1483
+ with st.spinner("Syncing to Google Drive..."):
1484
+ if config_manager.manual_sync_to_drive():
1485
+ st.success("Successfully synced to Google Drive!")
1486
+ st.rerun()
1487
+ else:
1488
+ st.error("Failed to sync to Google Drive")
1489
+
1490
+ # Configuration info
1491
+ st.markdown("#### Configuration")
1492
+ st.markdown("To enable Google Drive integration, set the following environment variables:")
1493
+ st.code("""
1494
+ GOOGLE_DRIVE_FOLDER_ID=your_folder_id
1495
+ GOOGLE_PROJECT_ID=your_project_id
1496
+ GOOGLE_PRIVATE_KEY_ID=your_private_key_id
1497
+ GOOGLE_PRIVATE_KEY=your_private_key
1498
+ GOOGLE_CLIENT_EMAIL=your_client_email
1499
+ GOOGLE_CLIENT_ID=your_client_id
1500
+ """)
1501
+
1502
+ st.markdown("**Note:** Google Drive integration is automatically enabled when running on Hugging Face Spaces with proper credentials configured.")
1503
+
1504
+ def show_event_management_section(config):
1505
+ st.markdown("#### Manage Wedding Events")
1506
+
1507
+ # Initialize session state for event editing if not exists
1508
+ if 'edit_events' not in st.session_state:
1509
+ st.session_state.edit_events = config.get('wedding_events', []).copy()
1510
+
1511
+ wedding_events = st.session_state.edit_events
1512
+ wedding_info = config.get('wedding_info', {})
1513
+
1514
+ # Get wedding dates for date calculations
1515
+ wedding_start_str = wedding_info.get('wedding_start_date', '')
1516
+ wedding_end_str = wedding_info.get('wedding_end_date', '')
1517
+
1518
+ if not wedding_start_str or not wedding_end_str:
1519
+ st.warning("Please set your wedding date range in the 'Edit Configuration' tab first.")
1520
+ return
1521
+
1522
+ try:
1523
+ wedding_start = datetime.fromisoformat(wedding_start_str).date()
1524
+ wedding_end = datetime.fromisoformat(wedding_end_str).date()
1525
+ except:
1526
+ st.error("Invalid wedding date format. Please check your configuration.")
1527
+ return
1528
+
1529
+ # Add event button
1530
+ if st.button("➕ Add New Event"):
1531
+ st.session_state.edit_events.append({
1532
+ "name": "New Event",
1533
+ "description": "",
1534
+ "date_offset": 0,
1535
+ "requires_meal_choice": False,
1536
+ "meal_options": [],
1537
+ "location": "",
1538
+ "address": ""
1539
+ })
1540
+ st.rerun()
1541
+
1542
+ # Display events for editing
1543
+ if st.session_state.edit_events:
1544
+ st.markdown("##### Edit Events")
1545
+
1546
+ for i, event in enumerate(st.session_state.edit_events):
1547
+ with st.expander(f"Event {i+1}: {event['name']}", expanded=True):
1548
+ # Add delete button at the top right of each event
1549
+ col_header1, col_header2 = st.columns([4, 1])
1550
+ with col_header2:
1551
+ if st.button("🗑️ Delete", key=f"delete_event_{i}", help="Delete this event"):
1552
+ st.session_state.edit_events.pop(i)
1553
+ st.rerun()
1554
+
1555
+ col1, col2 = st.columns(2)
1556
+
1557
+ with col1:
1558
+ event_name = st.text_input("Event Name", value=event['name'], key=f"settings_event_name_{i}")
1559
+ event_description = st.text_input("Time", value=event['description'], placeholder="e.g., 2:00 PM, 6:30 PM", key=f"settings_event_desc_{i}")
1560
+ event_location = st.text_input("Location Name", value=event.get('location', ''), placeholder="e.g., Central Park, Grand Ballroom", key=f"settings_event_location_{i}")
1561
+
1562
+ with col2:
1563
+ # Calculate current event date from date_offset
1564
+ current_event_date = wedding_start + timedelta(days=event['date_offset'])
1565
+
1566
+ # Use date input without constraints - allow any date
1567
+ event_date = st.date_input(
1568
+ "Event Date",
1569
+ value=current_event_date,
1570
+ key=f"settings_event_date_{i}",
1571
+ help="Select any date for this event"
1572
+ )
1573
+
1574
+ # Show warning if date is outside wedding range
1575
+ if event_date < wedding_start or event_date > wedding_end:
1576
+ st.warning(f"⚠️ Selected date is outside your wedding date range ({wedding_start.strftime('%B %d, %Y')} - {wedding_end.strftime('%B %d, %Y')})")
1577
+
1578
+ requires_meal_choice = st.checkbox("Requires Meal Choice", value=event['requires_meal_choice'], key=f"settings_event_meal_{i}")
1579
+
1580
+ # Meal options section (only show if meal choice is required)
1581
+ if requires_meal_choice:
1582
+ st.markdown("**Meal Options**")
1583
+ st.markdown("Enter meal options (one per line):")
1584
+ current_meal_options = event.get('meal_options', [])
1585
+ meal_options_text = '\n'.join(current_meal_options) if current_meal_options else ''
1586
+ 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)
1587
+ else:
1588
+ meal_options = ""
1589
+
1590
+ 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)
1591
+
1592
+ # Calculate date_offset from the selected date
1593
+ date_offset = (event_date - wedding_start).days
1594
+
1595
+ # Parse meal options
1596
+ meal_options_list = []
1597
+ if requires_meal_choice and meal_options:
1598
+ meal_options_list = [option.strip() for option in meal_options.split('\n') if option.strip()]
1599
+
1600
+ # Update session state
1601
+ st.session_state.edit_events[i] = {
1602
+ "name": event_name,
1603
+ "description": event_description,
1604
+ "date_offset": date_offset,
1605
+ "requires_meal_choice": requires_meal_choice,
1606
+ "meal_options": meal_options_list,
1607
+ "location": event_location,
1608
+ "address": event_address
1609
+ }
1610
+
1611
+ # Save events button
1612
+ st.markdown("---")
1613
+ col1, col2, col3 = st.columns([1, 1, 1])
1614
+ with col2:
1615
+ if st.button("💾 Save Event Changes", type="primary"):
1616
+ # Update the configuration with edited events
1617
+ updated_config = config.copy()
1618
+ updated_config['wedding_events'] = st.session_state.edit_events
1619
+
1620
+ # Save the updated configuration
1621
+ st.session_state.config_manager.save_config(updated_config)
1622
+ st.success("Event changes saved successfully!")
1623
+ st.rerun()
1624
+ else:
1625
+ st.info("No events added yet. Click 'Add New Event' to get started!")
1626
+
1627
+ # Event summary
1628
+ if st.session_state.edit_events:
1629
+ st.markdown("##### Event Summary")
1630
+
1631
+ col1, col2, col3 = st.columns(3)
1632
+
1633
+ with col1:
1634
+ total_events = len(st.session_state.edit_events)
1635
+ st.metric("Total Events", total_events)
1636
+
1637
+ with col2:
1638
+ meal_events = len([e for e in st.session_state.edit_events if e.get('requires_meal_choice', False)])
1639
+ st.metric("Events with Meals", meal_events)
1640
+
1641
+ with col3:
1642
+ days_span = (wedding_end - wedding_start).days + 1
1643
+ st.metric("Celebration Days", days_span)
1644
 
1645
  if __name__ == "__main__":
1646
  main()
config_manager.py CHANGED
@@ -1,6 +1,5 @@
1
  import json
2
  import os
3
- import yaml
4
  from datetime import datetime
5
  from google_drive_manager import GoogleDriveManager
6
  import streamlit as st
@@ -788,66 +787,3 @@ class ConfigManager:
788
  except Exception as e:
789
  print(f"Error resetting app state: {e}")
790
  return False
791
-
792
- def create_wedding_folder_structure(self, folder_name: str, wedding_config: dict) -> bool:
793
- """Create a complete wedding folder structure in Google Drive with initial files"""
794
- if not self.google_drive_enabled:
795
- return False
796
-
797
- try:
798
- # Check if folder already exists
799
- if self.drive_manager.check_folder_exists(folder_name):
800
- print(f"Folder {folder_name} already exists")
801
- return True
802
-
803
- # Create the folder
804
- folder_id = self.drive_manager.create_folder(folder_name)
805
- if not folder_id:
806
- return False
807
-
808
- # Create initial data files for the new wedding
809
- initial_files = {
810
- 'wedding_config.json': wedding_config,
811
- 'guest_list_data.json': [],
812
- 'rsvp_data.json': {},
813
- 'tasks.json': [],
814
- 'vendors.json': [],
815
- 'wedding_party.json': []
816
- }
817
-
818
- # Upload each file to the new folder
819
- for filename, content in initial_files.items():
820
- full_path = f"{folder_name}/{filename}"
821
- if not self.drive_manager.upload_file(full_path, content):
822
- print(f"Failed to create {filename} in {folder_name}")
823
- return False
824
-
825
- print(f"Successfully created wedding folder structure: {folder_name}")
826
- return True
827
-
828
- except Exception as e:
829
- print(f"Error creating wedding folder structure: {e}")
830
- return False
831
-
832
- def load_auth_config(self):
833
- """Load authentication configuration from Google Drive"""
834
- if not self.google_drive_enabled:
835
- return None
836
-
837
- try:
838
- auth_config = self.drive_manager.download_file('config.yaml')
839
- return auth_config
840
- except Exception as e:
841
- print(f"Error loading auth config: {e}")
842
- return None
843
-
844
- def save_auth_config(self, config):
845
- """Save authentication configuration to Google Drive"""
846
- if not self.google_drive_enabled:
847
- return False
848
-
849
- try:
850
- return self.drive_manager.upload_file('config.yaml', config)
851
- except Exception as e:
852
- print(f"Error saving auth config: {e}")
853
- return False
 
1
  import json
2
  import os
 
3
  from datetime import datetime
4
  from google_drive_manager import GoogleDriveManager
5
  import streamlit as st
 
787
  except Exception as e:
788
  print(f"Error resetting app state: {e}")
789
  return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
google_drive_manager.py CHANGED
@@ -2,8 +2,7 @@ import os
2
  import json
3
  import tempfile
4
  import time
5
- import yaml
6
- from typing import Dict, List, Optional, Any, Union
7
  from google.oauth2 import service_account
8
  from google.oauth2.credentials import Credentials
9
  from googleapiclient.discovery import build
@@ -130,7 +129,7 @@ class GoogleDriveManager:
130
  return []
131
 
132
  @retry_on_ssl_error(max_retries=3, delay=1)
133
- def download_file(self, file_name: str) -> Optional[Union[Dict[str, Any], str]]:
134
  """Download a file from Google Drive and return its content"""
135
  if not self.service or not self.folder_id:
136
  return None
@@ -146,7 +145,7 @@ class GoogleDriveManager:
146
  folders = folder_results.get('files', [])
147
 
148
  if not folders:
149
- print(f"Folder '{folder_name}' not found in Google Drive")
150
  return None
151
 
152
  folder_id = folders[0]['id']
@@ -157,7 +156,7 @@ class GoogleDriveManager:
157
  files = results.get('files', [])
158
 
159
  if not files:
160
- print(f"File '{file_name}' not found in Google Drive")
161
  return None
162
  else:
163
  # Direct file search in root folder
@@ -166,7 +165,7 @@ class GoogleDriveManager:
166
  files = results.get('files', [])
167
 
168
  if not files:
169
- print(f"File '{file_name}' not found in Google Drive")
170
  return None
171
 
172
  file_id = files[0]['id']
@@ -175,27 +174,15 @@ class GoogleDriveManager:
175
  request = self.service.files().get_media(fileId=file_id)
176
  content = request.execute()
177
 
178
- # Determine file type and parse accordingly
179
- content_str = content.decode('utf-8')
180
-
181
- if file_name.endswith('.yaml') or file_name.endswith('.yml'):
182
- # Parse as YAML
183
- try:
184
- return yaml.safe_load(content_str)
185
- except yaml.YAMLError:
186
- return content_str
187
- elif file_name.endswith('.json'):
188
- # Parse as JSON
189
- try:
190
- return json.loads(content_str)
191
- except json.JSONDecodeError:
192
- return content_str
193
- else:
194
- # Return as string
195
- return content_str
196
 
197
  except HttpError as e:
198
- print(f"Error downloading file '{file_name}': {str(e)}")
199
  return None
200
 
201
  @retry_on_ssl_error(max_retries=3, delay=1)
@@ -209,20 +196,13 @@ class GoogleDriveManager:
209
  if isinstance(content, (dict, list)):
210
  if file_name.endswith('.yaml') or file_name.endswith('.yml'):
211
  # For YAML files, convert to YAML string
 
212
  content_str = yaml.dump(content, default_flow_style=False, sort_keys=False)
213
- mimetype = 'text/yaml'
214
  else:
215
  # For JSON files, convert to JSON string
216
  content_str = json.dumps(content, indent=2)
217
- mimetype = 'application/json'
218
  else:
219
  content_str = str(content)
220
- if file_name.endswith('.yaml') or file_name.endswith('.yml'):
221
- mimetype = 'text/yaml'
222
- elif file_name.endswith('.json'):
223
- mimetype = 'application/json'
224
- else:
225
- mimetype = 'text/plain'
226
 
227
  # Convert string to bytes
228
  content_bytes = content_str.encode('utf-8')
@@ -231,6 +211,12 @@ class GoogleDriveManager:
231
  from io import BytesIO
232
  media_body = BytesIO(content_bytes)
233
 
 
 
 
 
 
 
234
  # Handle subfolder paths like 'laraandumang/wedding_config.json'
235
  target_folder_id = self.folder_id
236
  actual_file_name = file_name
@@ -255,7 +241,6 @@ class GoogleDriveManager:
255
  fields='id'
256
  ).execute()
257
  target_folder_id = created_folder.get('id')
258
- print(f"Created new folder: {folder_name}")
259
  else:
260
  target_folder_id = folders[0]['id']
261
 
@@ -273,7 +258,6 @@ class GoogleDriveManager:
273
  fileId=file_id,
274
  media_body=media
275
  ).execute()
276
- print(f"Updated existing file: {file_name}")
277
  else:
278
  # Create new file
279
  file_metadata = {
@@ -286,15 +270,14 @@ class GoogleDriveManager:
286
  body=file_metadata,
287
  media_body=media
288
  ).execute()
289
- print(f"Created new file: {file_name}")
290
 
291
  return True
292
 
293
  except HttpError as e:
294
- print(f"Error uploading file '{file_name}': {str(e)}")
295
  return False
296
  except Exception as e:
297
- print(f"Unexpected error uploading file '{file_name}': {str(e)}")
298
  return False
299
 
300
  def sync_from_drive(self, file_names: List[str]) -> Dict[str, Any]:
@@ -307,14 +290,9 @@ class GoogleDriveManager:
307
  synced_files[file_name] = content
308
  # Save to local temp directory
309
  local_path = os.path.join(self.temp_dir, file_name)
310
- os.makedirs(os.path.dirname(local_path), exist_ok=True)
311
-
312
  with open(local_path, 'w') as f:
313
  if isinstance(content, (dict, list)):
314
- if file_name.endswith('.yaml') or file_name.endswith('.yml'):
315
- yaml.dump(content, f, default_flow_style=False, sort_keys=False)
316
- else:
317
- json.dump(content, f, indent=2)
318
  else:
319
  f.write(str(content))
320
 
@@ -337,23 +315,7 @@ class GoogleDriveManager:
337
  return None
338
 
339
  try:
340
- # Handle subfolder paths
341
- if '/' in file_name:
342
- folder_name, actual_file_name = file_name.split('/', 1)
343
-
344
- # Find the subfolder first
345
- folder_query = f"name='{folder_name}' and '{self.folder_id}' in parents and trashed=false and mimeType='application/vnd.google-apps.folder'"
346
- folder_results = self.service.files().list(q=folder_query).execute()
347
- folders = folder_results.get('files', [])
348
-
349
- if not folders:
350
- return None
351
-
352
- folder_id = folders[0]['id']
353
- query = f"name='{actual_file_name}' and '{folder_id}' in parents and trashed=false"
354
- else:
355
- query = f"name='{file_name}' and '{self.folder_id}' in parents and trashed=false"
356
-
357
  results = self.service.files().list(q=query).execute()
358
  files = results.get('files', [])
359
 
@@ -362,65 +324,9 @@ class GoogleDriveManager:
362
  return None
363
 
364
  except HttpError as e:
365
- print(f"Error getting file info for '{file_name}': {str(e)}")
366
  return None
367
 
368
  def is_online(self) -> bool:
369
  """Check if Google Drive service is available"""
370
  return self.service is not None and self.folder_id is not None
371
-
372
- def create_folder(self, folder_name: str) -> Optional[str]:
373
- """Create a new folder in Google Drive and return its ID"""
374
- if not self.service or not self.folder_id:
375
- return None
376
-
377
- try:
378
- folder_metadata = {
379
- 'name': folder_name,
380
- 'mimeType': 'application/vnd.google-apps.folder',
381
- 'parents': [self.folder_id]
382
- }
383
-
384
- created_folder = self.service.files().create(
385
- body=folder_metadata,
386
- fields='id'
387
- ).execute()
388
-
389
- folder_id = created_folder.get('id')
390
- print(f"Created folder '{folder_name}' with ID: {folder_id}")
391
- return folder_id
392
-
393
- except HttpError as e:
394
- print(f"Error creating folder '{folder_name}': {str(e)}")
395
- return None
396
-
397
- def list_folders(self) -> List[Dict[str, Any]]:
398
- """List all folders in the Google Drive directory"""
399
- if not self.service or not self.folder_id:
400
- return []
401
-
402
- try:
403
- query = f"'{self.folder_id}' in parents and trashed=false and mimeType='application/vnd.google-apps.folder'"
404
- results = self.service.files().list(
405
- q=query,
406
- fields="files(id, name, modifiedTime)"
407
- ).execute()
408
-
409
- return results.get('files', [])
410
- except HttpError as e:
411
- print(f"Error listing folders: {str(e)}")
412
- return []
413
-
414
- def check_folder_exists(self, folder_name: str) -> bool:
415
- """Check if a folder exists in Google Drive"""
416
- if not self.service or not self.folder_id:
417
- return False
418
-
419
- try:
420
- query = f"name='{folder_name}' and '{self.folder_id}' in parents and trashed=false and mimeType='application/vnd.google-apps.folder'"
421
- results = self.service.files().list(q=query).execute()
422
- files = results.get('files', [])
423
- return len(files) > 0
424
- except HttpError as e:
425
- print(f"Error checking folder existence: {str(e)}")
426
- return False
 
2
  import json
3
  import tempfile
4
  import time
5
+ from typing import Dict, List, Optional, Any
 
6
  from google.oauth2 import service_account
7
  from google.oauth2.credentials import Credentials
8
  from googleapiclient.discovery import build
 
129
  return []
130
 
131
  @retry_on_ssl_error(max_retries=3, delay=1)
132
+ def download_file(self, file_name: str) -> Optional[Dict[str, Any]]:
133
  """Download a file from Google Drive and return its content"""
134
  if not self.service or not self.folder_id:
135
  return None
 
145
  folders = folder_results.get('files', [])
146
 
147
  if not folders:
148
+ st.warning(f"File '{file_name}' not found in Google Drive")
149
  return None
150
 
151
  folder_id = folders[0]['id']
 
156
  files = results.get('files', [])
157
 
158
  if not files:
159
+ st.warning(f"File '{file_name}' not found in Google Drive")
160
  return None
161
  else:
162
  # Direct file search in root folder
 
165
  files = results.get('files', [])
166
 
167
  if not files:
168
+ st.warning(f"File '{file_name}' not found in Google Drive")
169
  return None
170
 
171
  file_id = files[0]['id']
 
174
  request = self.service.files().get_media(fileId=file_id)
175
  content = request.execute()
176
 
177
+ # Try to parse as JSON
178
+ try:
179
+ return json.loads(content.decode('utf-8'))
180
+ except json.JSONDecodeError:
181
+ # If not JSON, return as string
182
+ return content.decode('utf-8')
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
  except HttpError as e:
185
+ st.error(f"Error downloading file '{file_name}': {str(e)}")
186
  return None
187
 
188
  @retry_on_ssl_error(max_retries=3, delay=1)
 
196
  if isinstance(content, (dict, list)):
197
  if file_name.endswith('.yaml') or file_name.endswith('.yml'):
198
  # For YAML files, convert to YAML string
199
+ import yaml
200
  content_str = yaml.dump(content, default_flow_style=False, sort_keys=False)
 
201
  else:
202
  # For JSON files, convert to JSON string
203
  content_str = json.dumps(content, indent=2)
 
204
  else:
205
  content_str = str(content)
 
 
 
 
 
 
206
 
207
  # Convert string to bytes
208
  content_bytes = content_str.encode('utf-8')
 
211
  from io import BytesIO
212
  media_body = BytesIO(content_bytes)
213
 
214
+ # Determine MIME type based on file extension
215
+ if file_name.endswith('.yaml') or file_name.endswith('.yml'):
216
+ mimetype = 'text/yaml'
217
+ else:
218
+ mimetype = 'application/json'
219
+
220
  # Handle subfolder paths like 'laraandumang/wedding_config.json'
221
  target_folder_id = self.folder_id
222
  actual_file_name = file_name
 
241
  fields='id'
242
  ).execute()
243
  target_folder_id = created_folder.get('id')
 
244
  else:
245
  target_folder_id = folders[0]['id']
246
 
 
258
  fileId=file_id,
259
  media_body=media
260
  ).execute()
 
261
  else:
262
  # Create new file
263
  file_metadata = {
 
270
  body=file_metadata,
271
  media_body=media
272
  ).execute()
 
273
 
274
  return True
275
 
276
  except HttpError as e:
277
+ st.error(f"Error uploading file '{file_name}': {str(e)}")
278
  return False
279
  except Exception as e:
280
+ st.error(f"Unexpected error uploading file '{file_name}': {str(e)}")
281
  return False
282
 
283
  def sync_from_drive(self, file_names: List[str]) -> Dict[str, Any]:
 
290
  synced_files[file_name] = content
291
  # Save to local temp directory
292
  local_path = os.path.join(self.temp_dir, file_name)
 
 
293
  with open(local_path, 'w') as f:
294
  if isinstance(content, (dict, list)):
295
+ json.dump(content, f, indent=2)
 
 
 
296
  else:
297
  f.write(str(content))
298
 
 
315
  return None
316
 
317
  try:
318
+ query = f"name='{file_name}' and '{self.folder_id}' in parents and trashed=false"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  results = self.service.files().list(q=query).execute()
320
  files = results.get('files', [])
321
 
 
324
  return None
325
 
326
  except HttpError as e:
327
+ st.error(f"Error getting file info for '{file_name}': {str(e)}")
328
  return None
329
 
330
  def is_online(self) -> bool:
331
  """Check if Google Drive service is available"""
332
  return self.service is not None and self.folder_id is not None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,4 +1,4 @@
1
- streamlit>=1.28.1
2
  pandas==2.1.3
3
  plotly==5.17.0
4
  datetime
@@ -8,5 +8,5 @@ google-api-python-client>=2.0.0
8
  google-auth-httplib2>=0.1.0
9
  google-auth-oauthlib>=0.5.0
10
  google-auth>=2.0.0
11
- streamlit-authenticator==0.4.2
12
  PyYAML>=6.0
 
1
+ streamlit==1.28.1
2
  pandas==2.1.3
3
  plotly==5.17.0
4
  datetime
 
8
  google-auth-httplib2>=0.1.0
9
  google-auth-oauthlib>=0.5.0
10
  google-auth>=2.0.0
11
+ streamlit-authenticator>=0.2.3
12
  PyYAML>=6.0