├── requirements.txt └── app.py /requirements.txt: -------------------------------------------------------------------------------- 1 | streamlit>=1.28.0 2 | supabase>=1.0.0 3 | pandas>=1.5.0 4 | python-dotenv>=0.19.0 -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | import pandas as pd 3 | from datetime import datetime, date, timedelta 4 | import hashlib 5 | from supabase import create_client, Client 6 | import os 7 | from typing import Dict, List, Optional, Tuple 8 | import calendar 9 | 10 | # Supabase configuration 11 | SUPABASE_URL = st.secrets.get("SUPABASE_URL", "your-supabase-url") 12 | SUPABASE_KEY = st.secrets.get("SUPABASE_KEY", "your-supabase-key") 13 | 14 | # Initialize Supabase client 15 | @st.cache_resource 16 | def init_supabase(): 17 | try: 18 | client = create_client(SUPABASE_URL, SUPABASE_KEY) 19 | # Test the connection 20 | client.table('families').select('id').limit(1).execute() 21 | return client 22 | except Exception as e: 23 | st.error(f"Failed to connect to Supabase: {str(e)}") 24 | st.error("Please check your SUPABASE_URL and SUPABASE_KEY in secrets") 25 | st.stop() 26 | 27 | supabase: Client = init_supabase() 28 | 29 | # Database helper functions 30 | def hash_password(password: str) -> str: 31 | return hashlib.sha256(password.encode()).hexdigest() 32 | 33 | # Authentication functions 34 | def register_family(email: str, password: str, family_name: str, phone: str = None) -> bool: 35 | try: 36 | password_hash = hash_password(password) 37 | result = supabase.table('families').insert({ 38 | 'email': email, 39 | 'password_hash': password_hash, 40 | 'family_name': family_name, 41 | 'phone': phone 42 | }).execute() 43 | return True 44 | except Exception as e: 45 | st.error(f"Registration failed: {str(e)}") 46 | return False 47 | 48 | def login_family(email: str, password: str) -> Optional[Dict]: 49 | try: 50 | password_hash = hash_password(password) 51 | result = supabase.table('families').select('*').eq('email', email).eq('password_hash', password_hash).execute() 52 | if result.data: 53 | return result.data[0] 54 | return None 55 | except Exception as e: 56 | st.error(f"Login failed: {str(e)}") 57 | return None 58 | 59 | # Student management functions 60 | def add_student(family_id: int, first_name: str, last_name: str, grade: str, student_type: str, is_prepaid: bool): 61 | try: 62 | result = supabase.table('students').insert({ 63 | 'family_id': family_id, 64 | 'first_name': first_name, 65 | 'last_name': last_name, 66 | 'grade': grade, 67 | 'student_type': student_type, 68 | 'is_prepaid': is_prepaid 69 | }).execute() 70 | return True 71 | except Exception as e: 72 | st.error(f"Failed to add student: {str(e)}") 73 | return False 74 | 75 | def get_family_students(family_id: int) -> List[Dict]: 76 | try: 77 | result = supabase.table('students').select('*').eq('family_id', family_id).execute() 78 | return result.data 79 | except Exception as e: 80 | st.error(f"Failed to get students: {str(e)}") 81 | return [] 82 | 83 | def update_student(student_id: int, **updates): 84 | try: 85 | result = supabase.table('students').update(updates).eq('id', student_id).execute() 86 | return True 87 | except Exception as e: 88 | st.error(f"Failed to update student: {str(e)}") 89 | return False 90 | 91 | def delete_student(student_id: int): 92 | try: 93 | # Delete student selections first 94 | supabase.table('student_selections').delete().eq('student_id', student_id).execute() 95 | # Delete the student 96 | result = supabase.table('students').delete().eq('id', student_id).execute() 97 | return True 98 | except Exception as e: 99 | st.error(f"Failed to delete student: {str(e)}") 100 | return False 101 | 102 | # Menu management functions 103 | def create_menu(month: int, year: int, title: str, due_date: date) -> Optional[int]: 104 | try: 105 | result = supabase.table('menus').insert({ 106 | 'month': month, 107 | 'year': year, 108 | 'title': title, 109 | 'due_date': due_date.isoformat(), 110 | 'is_active': True 111 | }).execute() 112 | return result.data[0]['id'] if result.data else None 113 | except Exception as e: 114 | st.error(f"Failed to create menu: {str(e)}") 115 | return None 116 | 117 | def add_menu_day(menu_id: int, day_date: date, day_name: str, main_title: str, option1: str, option2: str, general_description: str, is_no_lunch: bool = False, no_lunch_reason: str = None): 118 | try: 119 | result = supabase.table('menu_days').insert({ 120 | 'menu_id': menu_id, 121 | 'day_date': day_date.isoformat(), 122 | 'day_name': day_name, 123 | 'main_title': main_title, 124 | 'option1_description': option1, 125 | 'option2_description': option2, 126 | 'general_description': general_description, 127 | 'is_no_lunch': is_no_lunch, 128 | 'no_lunch_reason': no_lunch_reason 129 | }).execute() 130 | return True 131 | except Exception as e: 132 | st.error(f"Failed to add menu day: {str(e)}") 133 | return False 134 | 135 | def update_menu_day(day_id: int, **updates): 136 | try: 137 | result = supabase.table('menu_days').update(updates).eq('id', day_id).execute() 138 | return True 139 | except Exception as e: 140 | st.error(f"Failed to update menu day: {str(e)}") 141 | return False 142 | 143 | def delete_menu(menu_id: int): 144 | try: 145 | # Delete menu days first (cascade should handle this, but let's be explicit) 146 | supabase.table('menu_days').delete().eq('menu_id', menu_id).execute() 147 | # Delete the menu 148 | result = supabase.table('menus').delete().eq('id', menu_id).execute() 149 | return True 150 | except Exception as e: 151 | st.error(f"Failed to delete menu: {str(e)}") 152 | return False 153 | 154 | def delete_menu_day(day_id: int): 155 | try: 156 | result = supabase.table('menu_days').delete().eq('id', day_id).execute() 157 | return True 158 | except Exception as e: 159 | st.error(f"Failed to delete menu day: {str(e)}") 160 | return False 161 | 162 | def get_active_menus() -> List[Dict]: 163 | try: 164 | result = supabase.table('menus').select('*').eq('is_active', True).order('year', desc=True).order('month', desc=True).execute() 165 | return result.data 166 | except Exception as e: 167 | st.error(f"Failed to get menus: {str(e)}") 168 | return [] 169 | 170 | def get_menu_days(menu_id: int) -> List[Dict]: 171 | try: 172 | result = supabase.table('menu_days').select('*').eq('menu_id', menu_id).order('day_date').execute() 173 | return result.data 174 | except Exception as e: 175 | st.error(f"Failed to get menu days: {str(e)}") 176 | return [] 177 | 178 | # Selection functions 179 | def save_student_selection(student_id: int, menu_day_id: int, selected_option: Optional[int], extra_portions: int = 0): 180 | try: 181 | # First, check if a selection already exists 182 | existing_result = supabase.table('student_selections').select('id').eq('student_id', student_id).eq('menu_day_id', menu_day_id).execute() 183 | 184 | if existing_result.data: 185 | # Update existing record 186 | result = supabase.table('student_selections').update({ 187 | 'selected_option': selected_option, 188 | 'extra_portions': extra_portions, 189 | 'updated_at': datetime.now().isoformat() 190 | }).eq('student_id', student_id).eq('menu_day_id', menu_day_id).execute() 191 | else: 192 | # Insert new record 193 | result = supabase.table('student_selections').insert({ 194 | 'student_id': student_id, 195 | 'menu_day_id': menu_day_id, 196 | 'selected_option': selected_option, 197 | 'extra_portions': extra_portions, 198 | 'updated_at': datetime.now().isoformat() 199 | }).execute() 200 | 201 | return True 202 | except Exception as e: 203 | st.error(f"Failed to save selection: {str(e)}") 204 | return False 205 | 206 | def get_menu_submission_status(student_id: int, menu_id: int) -> Dict: 207 | """Get submission status for a student's menu""" 208 | try: 209 | result = supabase.table('menu_submissions').select('*').eq('student_id', student_id).eq('menu_id', menu_id).execute() 210 | if result.data: 211 | return result.data[0] 212 | else: 213 | return {'is_submitted': False, 'submitted_at': None} 214 | except Exception as e: 215 | # Table might not exist yet, return default 216 | return {'is_submitted': False, 'submitted_at': None} 217 | 218 | def submit_menu_selections(student_id: int, menu_id: int): 219 | """Mark a student's menu selections as submitted""" 220 | try: 221 | # Upsert submission status 222 | result = supabase.table('menu_submissions').upsert({ 223 | 'student_id': student_id, 224 | 'menu_id': menu_id, 225 | 'is_submitted': True, 226 | 'submitted_at': datetime.now().isoformat() 227 | }).execute() 228 | return True 229 | except Exception as e: 230 | st.error(f"Failed to submit menu: {str(e)}") 231 | return False 232 | 233 | def unsubmit_menu_selections(student_id: int, menu_id: int): 234 | """Mark a student's menu selections as draft (unsubmitted)""" 235 | try: 236 | result = supabase.table('menu_submissions').upsert({ 237 | 'student_id': student_id, 238 | 'menu_id': menu_id, 239 | 'is_submitted': False, 240 | 'submitted_at': None 241 | }).execute() 242 | return True 243 | except Exception as e: 244 | st.error(f"Failed to unsubmit menu: {str(e)}") 245 | return False 246 | 247 | def can_edit_menu(menu_due_date: str, is_submitted: bool) -> Tuple[bool, str]: 248 | """Check if a menu can be edited based on submission status and deadline""" 249 | due_date = datetime.fromisoformat(menu_due_date).date() 250 | is_past_due = date.today() > due_date 251 | 252 | if is_past_due: 253 | return False, "Past deadline - no changes allowed" 254 | elif is_submitted: 255 | return True, "Submitted - can unsubmit to make changes" 256 | else: 257 | return True, "Draft - can edit freely" 258 | 259 | def get_student_selections(student_id: int, menu_id: int) -> Dict: 260 | try: 261 | # Join query to get selections for a specific menu 262 | result = supabase.table('student_selections').select(''' 263 | *, 264 | menu_days!inner ( 265 | id, 266 | day_date, 267 | menu_id 268 | ) 269 | ''').eq('student_id', student_id).eq('menu_days.menu_id', menu_id).execute() 270 | 271 | # Convert to dict keyed by menu_day_id 272 | selections = {} 273 | for selection in result.data: 274 | menu_day_id = selection['menu_day_id'] 275 | selections[menu_day_id] = { 276 | 'selected_option': selection['selected_option'], 277 | 'extra_portions': selection['extra_portions'] 278 | } 279 | return selections 280 | except Exception as e: 281 | st.error(f"Failed to get selections: {str(e)}") 282 | return {} 283 | 284 | # Calculation functions 285 | def calculate_monthly_cost(student_selections: List[Dict], is_prepaid: bool, base_meal_price: float = 5.00, extra_meal_price: float = 1.50) -> Tuple[int, int, float]: 286 | """Calculate total meals, extra meals, and cost for a student""" 287 | total_meals = 0 288 | extra_meals = 0 289 | 290 | for selection in student_selections: 291 | if selection['selected_option'] is not None: # Has lunch 292 | total_meals += 1 293 | extra_meals += selection['extra_portions'] 294 | 295 | if is_prepaid: 296 | # Only pay for extra meals 297 | cost = extra_meals * extra_meal_price 298 | else: 299 | # Pay for all meals + extra meals 300 | cost = (total_meals * base_meal_price) + (extra_meals * extra_meal_price) 301 | 302 | return total_meals, extra_meals, cost 303 | 304 | # UI Components 305 | def login_page(): 306 | st.title("🍽️ Margo's Lunch Menu System") 307 | 308 | tab1, tab2 = st.tabs(["Login", "Register"]) 309 | 310 | with tab1: 311 | st.subheader("Family Login") 312 | email = st.text_input("Email", key="login_email") 313 | password = st.text_input("Password", type="password", key="login_password") 314 | 315 | if st.button("Login"): 316 | family = login_family(email, password) 317 | if family: 318 | st.session_state.family = family 319 | st.success("Login successful!") 320 | st.rerun() 321 | else: 322 | st.error("Invalid email or password") 323 | 324 | with tab2: 325 | st.subheader("Register New Family") 326 | reg_email = st.text_input("Email", key="reg_email") 327 | reg_password = st.text_input("Password", type="password", key="reg_password") 328 | family_name = st.text_input("Family Last Name") 329 | phone = st.text_input("Phone Number (optional)") 330 | 331 | if st.button("Register"): 332 | if reg_email and reg_password and family_name: 333 | if register_family(reg_email, reg_password, family_name, phone): 334 | st.success("Registration successful! Please login.") 335 | else: 336 | st.error("Registration failed") 337 | else: 338 | st.error("Please fill in all required fields") 339 | 340 | def student_management_page(): 341 | st.title("👨‍👩‍👧‍👦 Manage Students") 342 | 343 | family = st.session_state.family 344 | students = get_family_students(family['id']) 345 | 346 | st.subheader("Your Children") 347 | 348 | if students: 349 | for student in students: 350 | with st.expander(f"{student['first_name']} {student['last_name']} - Grade {student['grade']}", expanded=False): 351 | col1, col2 = st.columns(2) 352 | 353 | with col1: 354 | st.markdown("**Edit Student Information**") 355 | new_first_name = st.text_input( 356 | "First Name", 357 | value=student['first_name'], 358 | key=f"first_{student['id']}" 359 | ) 360 | new_last_name = st.text_input( 361 | "Last Name", 362 | value=student['last_name'], 363 | key=f"last_{student['id']}" 364 | ) 365 | new_grade = st.text_input( 366 | "Grade", 367 | value=student['grade'], 368 | key=f"grade_{student['id']}" 369 | ) 370 | 371 | with col2: 372 | st.markdown("**Settings**") 373 | new_type = st.selectbox( 374 | "School Type", 375 | ["preschool", "day_school"], 376 | index=0 if student['student_type'] == 'preschool' else 1, 377 | key=f"type_{student['id']}" 378 | ) 379 | new_prepaid = st.checkbox( 380 | "Prepaid", 381 | value=student['is_prepaid'], 382 | key=f"prepaid_{student['id']}" 383 | ) 384 | 385 | # Action buttons 386 | col_btn1, col_btn2, col_btn3 = st.columns([2, 2, 1]) 387 | 388 | with col_btn1: 389 | if st.button(f"Update {student['first_name']}", key=f"update_{student['id']}", type="primary"): 390 | if update_student( 391 | student['id'], 392 | first_name=new_first_name, 393 | last_name=new_last_name, 394 | grade=new_grade, 395 | student_type=new_type, 396 | is_prepaid=new_prepaid 397 | ): 398 | st.success(f"Updated {new_first_name}!") 399 | st.rerun() 400 | 401 | with col_btn2: 402 | # Delete confirmation 403 | delete_key = f"delete_confirm_{student['id']}" 404 | if delete_key not in st.session_state: 405 | st.session_state[delete_key] = False 406 | 407 | if not st.session_state[delete_key]: 408 | if st.button(f"Delete {student['first_name']}", key=f"delete_{student['id']}", type="secondary"): 409 | st.session_state[delete_key] = True 410 | st.rerun() 411 | else: 412 | st.warning(f"⚠️ Really delete {student['first_name']}? This will remove all their lunch selections!") 413 | col_yes, col_no = st.columns(2) 414 | with col_yes: 415 | if st.button("Yes, Delete", key=f"delete_yes_{student['id']}", type="primary"): 416 | if delete_student(student['id']): 417 | st.success(f"Deleted {student['first_name']}") 418 | del st.session_state[delete_key] 419 | st.rerun() 420 | with col_no: 421 | if st.button("Cancel", key=f"delete_no_{student['id']}"): 422 | st.session_state[delete_key] = False 423 | st.rerun() 424 | else: 425 | st.info("No students added yet.") 426 | 427 | st.markdown("---") 428 | st.subheader("Add New Student") 429 | col1, col2 = st.columns(2) 430 | 431 | with col1: 432 | first_name = st.text_input("First Name") 433 | grade = st.text_input("Grade") 434 | student_type = st.selectbox("School Type", ["preschool", "day_school"]) 435 | 436 | with col2: 437 | last_name = st.text_input("Last Name") 438 | is_prepaid = st.checkbox("Prepaid") 439 | 440 | if st.button("Add Student", type="primary"): 441 | if first_name and last_name and grade: 442 | if add_student(family['id'], first_name, last_name, grade, student_type, is_prepaid): 443 | st.success(f"Added {first_name} {last_name}!") 444 | st.rerun() 445 | else: 446 | st.error("Please fill in all fields") 447 | 448 | def menu_selection_page(): 449 | st.title("🍽️ Menu Selection") 450 | 451 | family = st.session_state.family 452 | students = get_family_students(family['id']) 453 | active_menus = get_active_menus() 454 | 455 | if not students: 456 | st.warning("Please add students first in the 'Manage Students' section.") 457 | return 458 | 459 | if not active_menus: 460 | st.warning("No active menus available. Please contact Margo's.") 461 | return 462 | 463 | # Select student 464 | student_options = [f"{s['first_name']} {s['last_name']}" for s in students] 465 | selected_student_idx = st.selectbox("Select Student", range(len(student_options)), format_func=lambda x: student_options[x]) 466 | selected_student = students[selected_student_idx] 467 | 468 | # Select menu 469 | menu_options = [f"{m['title']} (Due: {m['due_date']})" for m in active_menus] 470 | selected_menu_idx = st.selectbox("Select Menu", range(len(menu_options)), format_func=lambda x: menu_options[x]) 471 | selected_menu = active_menus[selected_menu_idx] 472 | 473 | # Get submission status 474 | submission_status = get_menu_submission_status(selected_student['id'], selected_menu['id']) 475 | is_submitted = submission_status.get('is_submitted', False) 476 | submitted_at = submission_status.get('submitted_at', None) 477 | 478 | # Check if menu can be edited 479 | can_edit, edit_status = can_edit_menu(selected_menu['due_date'], is_submitted) 480 | due_date = datetime.fromisoformat(selected_menu['due_date']).date() 481 | is_past_due = date.today() > due_date 482 | 483 | # Get menu days and existing selections 484 | menu_days = get_menu_days(selected_menu['id']) 485 | existing_selections = get_student_selections(selected_student['id'], selected_menu['id']) 486 | 487 | st.subheader(f"Menu for {selected_student['first_name']} {selected_student['last_name']}") 488 | 489 | # Status indicators 490 | col1, col2, col3 = st.columns(3) 491 | with col1: 492 | st.info(f"Student Type: {selected_student['student_type'].title()} | Prepaid: {'Yes' if selected_student['is_prepaid'] else 'No'}") 493 | with col2: 494 | if is_submitted: 495 | st.success(f"✅ SUBMITTED on {submitted_at[:10] if submitted_at else 'Unknown'}") 496 | else: 497 | st.warning("📝 DRAFT - Not submitted yet") 498 | with col3: 499 | if is_past_due: 500 | st.error("🔴 PAST DUE - No changes allowed") 501 | else: 502 | st.info(f"⏰ Due: {due_date}") 503 | 504 | # Show edit status 505 | if not can_edit and is_past_due: 506 | st.error("⚠️ This menu is past due. No changes can be made.") 507 | # Still show the selections in read-only mode 508 | show_readonly = True 509 | else: 510 | show_readonly = False 511 | 512 | # Create calendar view for student selections 513 | if menu_days: 514 | # Organize menu days by date 515 | days_dict = {} 516 | for day in menu_days: 517 | day_date = datetime.fromisoformat(day['day_date']).date() 518 | days_dict[day_date] = day 519 | 520 | # Create calendar view 521 | if days_dict: 522 | first_date = min(days_dict.keys()) 523 | month_name = first_date.strftime("%B %Y") 524 | 525 | st.markdown(f"### 🗓️ {month_name} Lunch Selection") 526 | 527 | if is_submitted and not show_readonly: 528 | st.info("💡 Menu is submitted. Click 'Switch to Draft Mode' below to make changes.") 529 | 530 | # Calendar header 531 | col_headers = st.columns(7) 532 | days_of_week = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] 533 | for i, day_name in enumerate(days_of_week): 534 | with col_headers[i]: 535 | st.markdown(f"**{day_name}**") 536 | 537 | # Get calendar boundaries 538 | year = first_date.year 539 | month = first_date.month 540 | first_day_of_month = date(year, month, 1) 541 | days_to_subtract = first_day_of_month.weekday() 542 | calendar_start = first_day_of_month - timedelta(days=days_to_subtract) 543 | 544 | # Store selections 545 | selections = {} 546 | current_date = calendar_start 547 | week_num = 0 548 | 549 | while current_date.month <= month or week_num < 6: 550 | cols = st.columns(7) 551 | 552 | for day_idx in range(7): 553 | with cols[day_idx]: 554 | if current_date in days_dict: 555 | day_data = days_dict[current_date] 556 | day_id = day_data['id'] 557 | 558 | if day_data['is_no_lunch']: 559 | # No lunch day 560 | st.markdown(f""" 561 |
562 |
{current_date.day}
563 |
🚫 No Lunch
564 |
{day_data['no_lunch_reason']}
565 |
566 | """, unsafe_allow_html=True) 567 | selections[day_id] = {'selected_option': None, 'extra_portions': 0} 568 | else: 569 | # Regular lunch day with selection options 570 | with st.container(): 571 | st.markdown(f"
{current_date.day}
", unsafe_allow_html=True) 572 | 573 | # Show main title 574 | if day_data.get('main_title'): 575 | st.markdown(f"
{day_data['main_title']}
", unsafe_allow_html=True) 576 | 577 | # Get existing selection 578 | existing = existing_selections.get(day_id, {'selected_option': None, 'extra_portions': 0}) 579 | 580 | # Lunch options as radio buttons 581 | option_labels = ["No lunch"] 582 | if day_data['option1_description']: 583 | option_labels.append(day_data['option1_description']) 584 | if day_data['option2_description']: 585 | option_labels.append(day_data['option2_description']) 586 | 587 | if show_readonly or (is_submitted and can_edit): 588 | # Read-only or submitted mode - just show current selection 589 | current_selection = existing['selected_option'] or 0 590 | selected_label = option_labels[current_selection] 591 | st.markdown(f"
{selected_label}
", unsafe_allow_html=True) 592 | 593 | if existing['extra_portions'] > 0: 594 | st.markdown(f"
+{existing['extra_portions']} extra
", unsafe_allow_html=True) 595 | 596 | # Store existing selection 597 | selections[day_id] = existing 598 | else: 599 | # Editable mode 600 | lunch_option = st.radio( 601 | "Choice", 602 | range(len(option_labels)), 603 | format_func=lambda x: option_labels[x], 604 | index=existing['selected_option'] or 0, 605 | key=f"cal_option_{day_id}", 606 | label_visibility="collapsed" 607 | ) 608 | 609 | # Show general description if exists 610 | if day_data.get('general_description') and lunch_option > 0: 611 | desc_lines = day_data['general_description'].split('\n') 612 | short_desc = desc_lines[0][:20] + "..." if len(desc_lines[0]) > 20 else desc_lines[0] 613 | st.markdown(f"
{short_desc}
", unsafe_allow_html=True) 614 | 615 | # Extra portions checkbox for day school 616 | extra_portions = 0 617 | if lunch_option > 0 and selected_student['student_type'] == 'day_school': 618 | extra_portions = st.checkbox( 619 | "Extra (+$1.50)", 620 | value=bool(existing['extra_portions']), 621 | key=f"cal_extra_{day_id}" 622 | ) 623 | extra_portions = 1 if extra_portions else 0 624 | 625 | selections[day_id] = { 626 | 'selected_option': lunch_option if lunch_option > 0 else None, 627 | 'extra_portions': extra_portions 628 | } 629 | 630 | elif current_date.month == month: 631 | # Day in current month but no lunch scheduled 632 | st.markdown(f""" 633 |
634 |
{current_date.day}
635 |
636 | """, unsafe_allow_html=True) 637 | else: 638 | # Day outside current month 639 | st.markdown(f""" 640 |
641 |
{current_date.day}
642 |
643 | """, unsafe_allow_html=True) 644 | 645 | current_date += timedelta(days=1) 646 | 647 | week_num += 1 648 | if current_date.month > month and week_num >= 6: 649 | break 650 | 651 | # Calculate totals 652 | selection_list = [{'selected_option': s['selected_option'], 'extra_portions': s['extra_portions']} 653 | for s in selections.values()] 654 | total_meals, extra_meals, total_cost = calculate_monthly_cost( 655 | selection_list, 656 | selected_student['is_prepaid'] 657 | ) 658 | 659 | # Display summary 660 | st.subheader("Summary") 661 | col1, col2, col3 = st.columns(3) 662 | with col1: 663 | st.metric("Total Meals", total_meals) 664 | with col2: 665 | st.metric("Extra Portions", extra_meals) 666 | with col3: 667 | st.metric("Total Cost", f"${total_cost:.2f}") 668 | 669 | # Action buttons 670 | if not show_readonly: # Not past due 671 | col1, col2, col3 = st.columns(3) 672 | 673 | with col1: 674 | if not is_submitted: 675 | # Draft mode - can save draft or submit 676 | if st.button("💾 Save Draft", type="secondary"): 677 | success = True 678 | for day_id, selection in selections.items(): 679 | if not save_student_selection( 680 | selected_student['id'], 681 | day_id, 682 | selection['selected_option'], 683 | selection['extra_portions'] 684 | ): 685 | success = False 686 | break 687 | 688 | if success: 689 | st.success("Draft saved successfully!") 690 | st.rerun() 691 | 692 | with col2: 693 | if not is_submitted: 694 | # Can submit 695 | if st.button("✅ Submit Menu", type="primary"): 696 | # First save all selections 697 | success = True 698 | for day_id, selection in selections.items(): 699 | if not save_student_selection( 700 | selected_student['id'], 701 | day_id, 702 | selection['selected_option'], 703 | selection['extra_portions'] 704 | ): 705 | success = False 706 | break 707 | 708 | # Then mark as submitted 709 | if success and submit_menu_selections(selected_student['id'], selected_menu['id']): 710 | st.success("Menu submitted successfully! 🎉") 711 | st.rerun() 712 | else: 713 | # Can unsubmit 714 | if st.button("📝 Switch to Draft Mode", type="secondary"): 715 | if unsubmit_menu_selections(selected_student['id'], selected_menu['id']): 716 | st.success("Menu switched to draft mode - you can now make changes!") 717 | st.rerun() 718 | 719 | with col3: 720 | if is_submitted: 721 | st.info("Menu is locked after submission to prevent accidental changes.") 722 | else: 723 | st.error("⚠️ Menu is past due date. No changes allowed.") 724 | 725 | def family_summary_page(): 726 | st.title("📊 Family Summary") 727 | 728 | family = st.session_state.family 729 | students = get_family_students(family['id']) 730 | 731 | if not students: 732 | st.warning("Please add students first in the 'Manage Students' section.") 733 | return 734 | 735 | st.subheader(f"Summary for {family['family_name']} Family") 736 | 737 | # Get all active menus 738 | active_menus = get_active_menus() 739 | 740 | if not active_menus: 741 | st.info("No active menus available yet.") 742 | return 743 | 744 | # Create summary table 745 | summary_data = [] 746 | total_family_cost = 0 747 | 748 | for menu in active_menus: 749 | menu_month_year = f"{calendar.month_name[menu['month']]} {menu['year']}" 750 | menu_due_date = menu['due_date'] 751 | 752 | for student in students: 753 | # Get selections for this student and menu 754 | try: 755 | # Get all menu days for this menu 756 | menu_days = get_menu_days(menu['id']) 757 | total_lunch_days = len([d for d in menu_days if not d.get('is_no_lunch', False)]) 758 | 759 | # Get student's selections and submission status 760 | selections_result = supabase.table('student_selections').select(''' 761 | selected_option, 762 | extra_portions, 763 | menu_days!inner (menu_id) 764 | ''').eq('student_id', student['id']).eq('menu_days.menu_id', menu['id']).execute() 765 | 766 | selections = selections_result.data 767 | 768 | # Get submission status 769 | submission_status = get_menu_submission_status(student['id'], menu['id']) 770 | is_submitted = submission_status.get('is_submitted', False) 771 | 772 | # Calculate completion and costs 773 | meals_selected = len([s for s in selections if s['selected_option'] is not None]) 774 | extra_portions = sum(s['extra_portions'] for s in selections if s['selected_option'] is not None) 775 | 776 | # Calculate cost 777 | _, _, cost = calculate_monthly_cost(selections, student['is_prepaid']) 778 | total_family_cost += cost 779 | 780 | # Determine completion status 781 | if meals_selected == 0: 782 | completion_status = "❌ Not Started" 783 | completion_color = "#ffebee" 784 | elif meals_selected < total_lunch_days: 785 | if is_submitted: 786 | completion_status = f"✅ Submitted (Partial: {meals_selected}/{total_lunch_days})" 787 | completion_color = "#fff3e0" 788 | else: 789 | completion_status = f"⚠️ Draft (Partial: {meals_selected}/{total_lunch_days})" 790 | completion_color = "#fff3e0" 791 | else: 792 | if is_submitted: 793 | completion_status = "✅ Submitted (Complete)" 794 | completion_color = "#e8f5e8" 795 | else: 796 | completion_status = "📝 Draft (Complete - Ready to Submit)" 797 | completion_color = "#e3f2fd" 798 | 799 | # Check if past due 800 | due_date_obj = datetime.fromisoformat(menu_due_date).date() 801 | past_due = date.today() > due_date_obj 802 | due_status = "🔴 PAST DUE" if past_due else "✅ On Time" 803 | 804 | summary_data.append({ 805 | 'Month': menu_month_year, 806 | 'Student': f"{student['first_name']} {student['last_name']}", 807 | 'Type': student['student_type'].title(), 808 | 'Prepaid': 'Yes' if student['is_prepaid'] else 'No', 809 | 'Completion': completion_status, 810 | 'Meals': meals_selected, 811 | 'Extra Portions': extra_portions, 812 | 'Cost': f"${cost:.2f}", 813 | 'Due Date': menu_due_date, 814 | 'Status': due_status, 815 | 'completion_color': completion_color 816 | }) 817 | 818 | except Exception as e: 819 | st.error(f"Error getting data for {student['first_name']} - {menu_month_year}: {str(e)}") 820 | 821 | # Display summary 822 | if summary_data: 823 | # Overall family metrics 824 | col1, col2, col3, col4 = st.columns(4) 825 | 826 | completed_forms = len([s for s in summary_data if "Complete" in s['Completion']]) 827 | total_forms = len(summary_data) 828 | 829 | with col1: 830 | st.metric("Total Students", len(students)) 831 | with col2: 832 | st.metric("Forms Completed", f"{completed_forms}/{total_forms}") 833 | with col3: 834 | st.metric("Completion Rate", f"{(completed_forms/max(total_forms,1)*100):.1f}%") 835 | with col4: 836 | st.metric("Total Amount Due", f"${total_family_cost:.2f}") 837 | 838 | # Detailed breakdown by month 839 | st.subheader("📋 Monthly Breakdown") 840 | 841 | # Group by month 842 | months = list(set([s['Month'] for s in summary_data])) 843 | months.sort() 844 | 845 | for month in months: 846 | month_data = [s for s in summary_data if s['Month'] == month] 847 | month_cost = sum(float(s['Cost'].replace('$', '')) for s in month_data) 848 | 849 | with st.expander(f"📅 {month} - Total: ${month_cost:.2f}", expanded=True): 850 | 851 | # Show each student for this month 852 | for student_data in month_data: 853 | col1, col2, col3 = st.columns([2, 2, 1]) 854 | 855 | with col1: 856 | st.markdown(f""" 857 |
858 | {student_data['Student']} ({student_data['Type']}) 859 |
Prepaid: {student_data['Prepaid']} 860 |
861 | """, unsafe_allow_html=True) 862 | 863 | with col2: 864 | st.write(f"**Status:** {student_data['Completion']}") 865 | st.write(f"**Meals:** {student_data['Meals']} | **Extra:** {student_data['Extra Portions']}") 866 | st.write(f"**Due:** {student_data['Due Date']} {student_data['Status']}") 867 | 868 | with col3: 869 | st.metric("Cost", student_data['Cost']) 870 | 871 | # Action button for incomplete forms 872 | if "Complete" not in student_data['Completion']: 873 | if st.button(f"Complete Form", key=f"complete_{student_data['Student']}_{month}"): 874 | st.session_state.goto_menu_selection = True 875 | st.rerun() 876 | 877 | # Summary table view 878 | st.subheader("📊 Full Summary Table") 879 | 880 | # Create dataframe for display 881 | display_df = pd.DataFrame([{ 882 | 'Month': s['Month'], 883 | 'Student': s['Student'], 884 | 'Type': s['Type'], 885 | 'Status': s['Completion'], 886 | 'Meals': s['Meals'], 887 | 'Extra': s['Extra Portions'], 888 | 'Cost': s['Cost'], 889 | 'Due Date': s['Due Date'] 890 | } for s in summary_data]) 891 | 892 | st.dataframe(display_df, use_container_width=True) 893 | 894 | # Export option 895 | if st.button("📊 Export Family Summary to CSV"): 896 | csv = display_df.to_csv(index=False) 897 | st.download_button( 898 | label="Download CSV", 899 | data=csv, 900 | file_name=f"family_summary_{family['family_name']}_{datetime.now().strftime('%Y%m%d')}.csv", 901 | mime="text/csv" 902 | ) 903 | 904 | else: 905 | st.info("No menu data available yet.") 906 | 907 | def admin_page(): 908 | st.title("👩‍💼 Admin Panel - Margo's Kitchen") 909 | 910 | # Simple admin authentication 911 | if 'admin_logged_in' not in st.session_state: 912 | st.session_state.admin_logged_in = False 913 | 914 | if not st.session_state.admin_logged_in: 915 | admin_password = st.text_input("Admin Password", type="password") 916 | if st.button("Login as Admin"): 917 | # Replace with your actual admin password 918 | if admin_password == "margos2025": # Change this! 919 | st.session_state.admin_logged_in = True 920 | st.rerun() 921 | else: 922 | st.error("Invalid admin password") 923 | return 924 | 925 | tab1, tab2, tab3, tab4 = st.tabs(["🛒 Meal Planning", "📋 Manage Menus", "View Submissions", "Export Data"]) 926 | 927 | with tab1: 928 | st.subheader("🗓️ Monthly Calendar - Meal Planning & Grocery Shopping") 929 | 930 | # Get active menus for selection 931 | active_menus = get_active_menus() 932 | if active_menus: 933 | menu_options = [f"{m['title']}" for m in active_menus] 934 | planning_menu_idx = st.selectbox("Select Menu for Planning", range(len(menu_options)), format_func=lambda x: menu_options[x], key="planning_menu_select") 935 | planning_menu = active_menus[planning_menu_idx] 936 | 937 | # Get menu days 938 | menu_days = get_menu_days(planning_menu['id']) 939 | 940 | if menu_days: 941 | st.info(f"📅 Planning for: **{planning_menu['title']}** (Due: {planning_menu['due_date']})") 942 | 943 | # Organize menu days by date 944 | days_dict = {} 945 | total_meals_month = 0 946 | total_extra_portions_month = 0 947 | 948 | for day in menu_days: 949 | day_date = datetime.fromisoformat(day['day_date']).date() 950 | days_dict[day_date] = day 951 | 952 | # Create calendar view 953 | if days_dict: 954 | # Get month/year info 955 | first_date = min(days_dict.keys()) 956 | month_name = first_date.strftime("%B %Y") 957 | 958 | st.markdown(f"### 📊 {month_name} Lunch Calendar") 959 | 960 | # Calendar header 961 | col_headers = st.columns(7) 962 | days_of_week = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] 963 | for i, day_name in enumerate(days_of_week): 964 | with col_headers[i]: 965 | st.markdown(f"**{day_name}**") 966 | 967 | # Get all dates in the month to create proper calendar 968 | year = first_date.year 969 | month = first_date.month 970 | 971 | # Find first Monday of the calendar view 972 | first_day_of_month = date(year, month, 1) 973 | days_to_subtract = first_day_of_month.weekday() 974 | calendar_start = first_day_of_month - timedelta(days=days_to_subtract) 975 | 976 | # Create calendar grid 977 | current_date = calendar_start 978 | week_num = 0 979 | 980 | while current_date.month <= month or week_num < 6: 981 | cols = st.columns(7) 982 | 983 | for day_idx in range(7): 984 | with cols[day_idx]: 985 | if current_date in days_dict: 986 | # This is a lunch day 987 | day_data = days_dict[current_date] 988 | 989 | if day_data['is_no_lunch']: 990 | # No lunch day 991 | st.markdown(f""" 992 |
993 |
{current_date.day}
994 |
🚫 No Lunch
995 |
{day_data['no_lunch_reason']}
996 |
997 | """, unsafe_allow_html=True) 998 | else: 999 | # Regular lunch day - get meal counts 1000 | try: 1001 | result = supabase.table('student_selections').select(''' 1002 | selected_option, 1003 | extra_portions, 1004 | students!inner (student_type) 1005 | ''').eq('menu_day_id', day_data['id']).execute() 1006 | 1007 | selections = result.data 1008 | 1009 | # Count selections and extras BY OPTION 1010 | option1_count = sum(1 for s in selections if s['selected_option'] == 1) 1011 | option2_count = sum(1 for s in selections if s['selected_option'] == 2) 1012 | option1_extra = sum(s['extra_portions'] for s in selections if s['selected_option'] == 1) 1013 | option2_extra = sum(s['extra_portions'] for s in selections if s['selected_option'] == 2) 1014 | 1015 | # Calculate TOTAL portions needed for each option 1016 | option1_total = option1_count + option1_extra 1017 | option2_total = option2_count + option2_extra 1018 | total_day = option1_count + option2_count 1019 | total_portions_day = option1_total + option2_total 1020 | 1021 | # Add to monthly totals 1022 | total_meals_month += total_day 1023 | total_extra_portions_month += (option1_extra + option2_extra) 1024 | 1025 | # Display day card with actual meal names and PROPER TOTALS 1026 | color = "#e8f5e8" if total_day > 0 else "#fff5f5" 1027 | option1_name = (day_data.get('option1_description', 'Opt1') or 'Opt1')[:8] 1028 | option2_name = (day_data.get('option2_description', 'Opt2') or 'Opt2')[:8] 1029 | 1030 | st.markdown(f""" 1031 |
1032 |
{current_date.day}
1033 |
{option1_name}: {option1_total} {f"({option1_count}+{option1_extra})" if option1_extra > 0 else ""}
1034 |
{option2_name}: {option2_total} {f"({option2_count}+{option2_extra})" if option2_extra > 0 else ""}
1035 |
1036 | Total: {total_portions_day} 1037 |
1038 |
1039 | """, unsafe_allow_html=True) 1040 | 1041 | except Exception as e: 1042 | st.markdown(f""" 1043 |
1044 |
{current_date.day}
1045 |
Loading...
1046 |
1047 | """, unsafe_allow_html=True) 1048 | 1049 | elif current_date.month == month: 1050 | # Day in current month but no lunch scheduled 1051 | st.markdown(f""" 1052 |
1053 |
{current_date.day}
1054 |
1055 | """, unsafe_allow_html=True) 1056 | else: 1057 | # Day outside current month 1058 | st.markdown(f""" 1059 |
1060 |
{current_date.day}
1061 |
1062 | """, unsafe_allow_html=True) 1063 | 1064 | current_date += timedelta(days=1) 1065 | 1066 | week_num += 1 1067 | st.markdown("---") 1068 | 1069 | # Stop if we've gone past the current month significantly 1070 | if current_date.month > month and week_num >= 6: 1071 | break 1072 | 1073 | # Monthly Summary 1074 | st.markdown("---") 1075 | st.subheader("📈 Monthly Summary") 1076 | 1077 | col1, col2, col3 = st.columns(3) 1078 | with col1: 1079 | st.metric("Total Meals This Month", total_meals_month, help="All base meals selected") 1080 | with col2: 1081 | st.metric("Total Extra Portions", total_extra_portions_month, help="Additional portions requested") 1082 | with col3: 1083 | st.metric("Total Food Portions to Prepare", total_meals_month + total_extra_portions_month) 1084 | 1085 | # Detailed daily breakdown 1086 | with st.expander("📋 Detailed Daily Breakdown", expanded=False): 1087 | for day_date in sorted(days_dict.keys()): 1088 | day_data = days_dict[day_date] 1089 | day_name = day_date.strftime("%A") 1090 | 1091 | st.markdown(f"**{day_name} - {day_date.strftime('%B %d')}**") 1092 | 1093 | # Show main title if exists 1094 | if day_data.get('main_title'): 1095 | st.markdown(f"*{day_data['main_title']}*") 1096 | 1097 | if day_data['is_no_lunch']: 1098 | st.info(f"🚫 No lunch - {day_data['no_lunch_reason']}") 1099 | else: 1100 | try: 1101 | result = supabase.table('student_selections').select(''' 1102 | selected_option, 1103 | extra_portions 1104 | ''').eq('menu_day_id', day_data['id']).execute() 1105 | 1106 | selections = result.data 1107 | option1_count = sum(1 for s in selections if s['selected_option'] == 1) 1108 | option2_count = sum(1 for s in selections if s['selected_option'] == 2) 1109 | option1_extra = sum(s['extra_portions'] for s in selections if s['selected_option'] == 1) 1110 | option2_extra = sum(s['extra_portions'] for s in selections if s['selected_option'] == 2) 1111 | 1112 | st.write(f"🛒 **Grocery List:**") 1113 | if option1_count > 0 or option1_extra > 0: 1114 | total_option1 = option1_count + option1_extra 1115 | extra_text = f" (includes {option1_extra} extra)" if option1_extra > 0 else "" 1116 | st.write(f"• **{total_option1}** portions of: {day_data['option1_description']}{extra_text}") 1117 | if option2_count > 0 or option2_extra > 0: 1118 | total_option2 = option2_count + option2_extra 1119 | extra_text = f" (includes {option2_extra} extra)" if option2_extra > 0 else "" 1120 | st.write(f"• **{total_option2}** portions of: {day_data['option2_description']}{extra_text}") 1121 | 1122 | # Show general description/sides 1123 | if day_data.get('general_description'): 1124 | st.write(f"📝 **Sides/Details:** {day_data['general_description']}") 1125 | 1126 | except Exception as e: 1127 | st.error(f"Error loading details for {day_name}") 1128 | 1129 | st.markdown("---") 1130 | 1131 | else: 1132 | st.warning("No menu days found for this menu.") 1133 | else: 1134 | st.warning("No active menus available. Create a menu first!") 1135 | 1136 | with tab2: 1137 | st.subheader("📋 Manage Menus") 1138 | 1139 | # Show existing menus first 1140 | active_menus = get_active_menus() 1141 | if active_menus: 1142 | st.markdown("### Existing Menus") 1143 | 1144 | for menu in active_menus: 1145 | with st.expander(f"📅 {menu['title']} (Due: {menu['due_date']})", expanded=False): 1146 | menu_id = menu['id'] 1147 | menu_days = get_menu_days(menu_id) 1148 | 1149 | # Menu info and delete option 1150 | col_info, col_delete = st.columns([3, 1]) 1151 | with col_info: 1152 | st.write(f"**Month/Year:** {menu['month']}/{menu['year']}") 1153 | st.write(f"**Days:** {len(menu_days)} lunch days configured") 1154 | 1155 | with col_delete: 1156 | # Delete menu confirmation 1157 | delete_menu_key = f"delete_menu_{menu_id}" 1158 | if delete_menu_key not in st.session_state: 1159 | st.session_state[delete_menu_key] = False 1160 | 1161 | if not st.session_state[delete_menu_key]: 1162 | if st.button("Delete Menu", key=f"del_menu_{menu_id}", type="secondary"): 1163 | st.session_state[delete_menu_key] = True 1164 | st.rerun() 1165 | else: 1166 | st.error("⚠️ Delete entire menu?") 1167 | col_yes, col_no = st.columns(2) 1168 | with col_yes: 1169 | if st.button("Yes", key=f"del_menu_yes_{menu_id}", type="primary"): 1170 | if delete_menu(menu_id): 1171 | st.success("Menu deleted!") 1172 | del st.session_state[delete_menu_key] 1173 | st.rerun() 1174 | with col_no: 1175 | if st.button("No", key=f"del_menu_no_{menu_id}"): 1176 | st.session_state[delete_menu_key] = False 1177 | st.rerun() 1178 | 1179 | # Show and edit menu days 1180 | if menu_days: 1181 | st.markdown("#### Menu Days") 1182 | for day in menu_days: 1183 | day_date = datetime.fromisoformat(day['day_date']).date() 1184 | 1185 | with st.container(): 1186 | st.markdown(f"**{day['day_name']} - {day_date.strftime('%B %d')}**") 1187 | 1188 | # Edit form for this day 1189 | col1, col2 = st.columns(2) 1190 | 1191 | with col1: 1192 | new_main_title = st.text_input( 1193 | "Main Title", 1194 | value=day.get('main_title', ''), 1195 | key=f"edit_title_{day['id']}" 1196 | ) 1197 | new_option1 = st.text_input( 1198 | "Option 1", 1199 | value=day.get('option1_description', ''), 1200 | key=f"edit_opt1_{day['id']}" 1201 | ) 1202 | new_option2 = st.text_input( 1203 | "Option 2", 1204 | value=day.get('option2_description', ''), 1205 | key=f"edit_opt2_{day['id']}" 1206 | ) 1207 | 1208 | with col2: 1209 | new_general_desc = st.text_area( 1210 | "General Description", 1211 | value=day.get('general_description', ''), 1212 | key=f"edit_desc_{day['id']}", 1213 | height=100 1214 | ) 1215 | 1216 | new_is_no_lunch = st.checkbox( 1217 | "No lunch this day", 1218 | value=day.get('is_no_lunch', False), 1219 | key=f"edit_nolunch_{day['id']}" 1220 | ) 1221 | 1222 | new_no_lunch_reason = "" 1223 | if new_is_no_lunch: 1224 | new_no_lunch_reason = st.text_input( 1225 | "No lunch reason", 1226 | value=day.get('no_lunch_reason', ''), 1227 | key=f"edit_reason_{day['id']}" 1228 | ) 1229 | 1230 | # Action buttons 1231 | col_update, col_delete_day = st.columns([2, 1]) 1232 | 1233 | with col_update: 1234 | if st.button(f"Update {day['day_name']}", key=f"update_day_{day['id']}", type="primary"): 1235 | updates = { 1236 | 'day_name': day_date.strftime("%A"), # Auto-update day name based on date 1237 | 'main_title': new_main_title, 1238 | 'option1_description': new_option1, 1239 | 'option2_description': new_option2, 1240 | 'general_description': new_general_desc, 1241 | 'is_no_lunch': new_is_no_lunch, 1242 | 'no_lunch_reason': new_no_lunch_reason if new_is_no_lunch else None 1243 | } 1244 | if update_menu_day(day['id'], **updates): 1245 | st.success(f"Updated {day['day_name']}!") 1246 | st.rerun() 1247 | 1248 | with col_delete_day: 1249 | # Delete day confirmation 1250 | delete_day_key = f"delete_day_{day['id']}" 1251 | if delete_day_key not in st.session_state: 1252 | st.session_state[delete_day_key] = False 1253 | 1254 | if not st.session_state[delete_day_key]: 1255 | if st.button("Delete Day", key=f"del_day_{day['id']}", type="secondary"): 1256 | st.session_state[delete_day_key] = True 1257 | st.rerun() 1258 | else: 1259 | if st.button("Confirm Delete", key=f"del_day_yes_{day['id']}", type="primary"): 1260 | if delete_menu_day(day['id']): 1261 | st.success("Day deleted!") 1262 | del st.session_state[delete_day_key] 1263 | st.rerun() 1264 | if st.button("Cancel", key=f"del_day_no_{day['id']}"): 1265 | st.session_state[delete_day_key] = False 1266 | st.rerun() 1267 | 1268 | st.markdown("---") 1269 | 1270 | # Add new day to existing menu 1271 | st.markdown("#### Add Day to This Menu") 1272 | with st.form(f"add_day_form_{menu_id}"): 1273 | col1, col2 = st.columns(2) 1274 | 1275 | with col1: 1276 | add_day_date = st.date_input("Day", key=f"add_date_{menu_id}") 1277 | add_main_title = st.text_input("Main Title", key=f"add_title_{menu_id}") 1278 | add_option1 = st.text_input("Option 1", key=f"add_opt1_{menu_id}") 1279 | add_option2 = st.text_input("Option 2", key=f"add_opt2_{menu_id}") 1280 | 1281 | with col2: 1282 | add_general_desc = st.text_area("General Description", height=100, key=f"add_desc_{menu_id}") 1283 | add_is_no_lunch = st.checkbox("No lunch available", key=f"add_nolunch_{menu_id}") 1284 | add_no_lunch_reason = "" 1285 | if add_is_no_lunch: 1286 | add_no_lunch_reason = st.text_input("Reason", key=f"add_reason_{menu_id}") 1287 | 1288 | if st.form_submit_button("Add Day to Menu"): 1289 | add_day_name = add_day_date.strftime("%A") # Auto-generate day name 1290 | if add_menu_day(menu_id, add_day_date, add_day_name, add_main_title, add_option1, add_option2, add_general_desc, add_is_no_lunch, add_no_lunch_reason): 1291 | st.success("Day added to menu!") 1292 | st.rerun() 1293 | 1294 | # Create new menu section 1295 | st.markdown("---") 1296 | st.markdown("### Create New Monthly Menu") 1297 | 1298 | col1, col2 = st.columns(2) 1299 | with col1: 1300 | menu_month = st.selectbox("Month", list(range(1, 13)), format_func=lambda x: calendar.month_name[x]) 1301 | menu_year = st.number_input("Year", min_value=2024, max_value=2030, value=datetime.now().year) 1302 | with col2: 1303 | menu_title = st.text_input("Menu Title", value=f"{calendar.month_name[menu_month]} {menu_year} Lunch Menu") 1304 | due_date = st.date_input("Due Date") 1305 | 1306 | if st.button("Create New Menu", type="primary"): 1307 | menu_id = create_menu(menu_month, menu_year, menu_title, due_date) 1308 | if menu_id: 1309 | st.success(f"Menu created! ID: {menu_id}") 1310 | st.rerun() 1311 | 1312 | with tab3: 1313 | st.subheader("View Submissions") 1314 | 1315 | # Get active menus for selection 1316 | active_menus = get_active_menus() 1317 | if active_menus: 1318 | menu_options = [f"{m['title']}" for m in active_menus] 1319 | selected_menu_idx = st.selectbox("Select Menu to View", range(len(menu_options)), format_func=lambda x: menu_options[x], key="admin_menu_select") 1320 | selected_menu = active_menus[selected_menu_idx] 1321 | 1322 | # Show summary statistics 1323 | try: 1324 | # Get all students and their selections for this menu 1325 | result = supabase.table('students').select(''' 1326 | id, first_name, last_name, student_type, is_prepaid, 1327 | families (family_name, email) 1328 | ''').execute() 1329 | 1330 | students_data = result.data 1331 | total_students = len(students_data) 1332 | 1333 | # Count submissions - students who have submitted their menu 1334 | submissions_result = supabase.table('menu_submissions').select(''' 1335 | student_id, 1336 | is_submitted 1337 | ''').eq('menu_id', selected_menu['id']).eq('is_submitted', True).execute() 1338 | 1339 | submitted_student_ids = list(set([s['student_id'] for s in submissions_result.data])) 1340 | submissions_count = len(submitted_student_ids) 1341 | 1342 | # Get draft submissions (has selections but not submitted) 1343 | draft_result = supabase.table('student_selections').select(''' 1344 | student_id, 1345 | menu_days!inner (menu_id) 1346 | ''').eq('menu_days.menu_id', selected_menu['id']).execute() 1347 | 1348 | students_with_selections = list(set([s['student_id'] for s in draft_result.data])) 1349 | 1350 | # Students with drafts but not submitted 1351 | draft_student_ids = [s_id for s_id in students_with_selections if s_id not in submitted_student_ids] 1352 | 1353 | col1, col2, col3, col4 = st.columns(4) 1354 | with col1: 1355 | st.metric("Total Students", total_students) 1356 | with col2: 1357 | st.metric("Submitted", submissions_count) 1358 | with col3: 1359 | st.metric("Drafts", len(draft_student_ids)) 1360 | with col4: 1361 | completion_rate = (submissions_count/max(total_students,1)*100) if total_students > 0 else 0 1362 | st.metric("Completion Rate", f"{completion_rate:.1f}%") 1363 | 1364 | # Show who hasn't submitted yet 1365 | not_submitted_students = [s for s in students_data if s['id'] not in submitted_student_ids] 1366 | 1367 | if not_submitted_students: 1368 | st.subheader("⚠️ Missing Submissions") 1369 | 1370 | # Separate into drafts vs not started 1371 | draft_students = [s for s in not_submitted_students if s['id'] in draft_student_ids] 1372 | not_started_students = [s for s in not_submitted_students if s['id'] not in draft_student_ids] 1373 | 1374 | if draft_students: 1375 | st.markdown("**📝 Has Draft (Not Yet Submitted):**") 1376 | draft_df = pd.DataFrame([{ 1377 | 'Family': s['families']['family_name'], 1378 | 'Student': f"{s['first_name']} {s['last_name']}", 1379 | 'Type': s['student_type'].title(), 1380 | 'Email': s['families']['email'], 1381 | 'Status': '📝 Draft - Needs to Submit' 1382 | } for s in draft_students]) 1383 | st.dataframe(draft_df, use_container_width=True) 1384 | 1385 | if not_started_students: 1386 | st.markdown("**❌ Not Started:**") 1387 | not_started_df = pd.DataFrame([{ 1388 | 'Family': s['families']['family_name'], 1389 | 'Student': f"{s['first_name']} {s['last_name']}", 1390 | 'Type': s['student_type'].title(), 1391 | 'Email': s['families']['email'], 1392 | 'Status': '❌ Not Started' 1393 | } for s in not_started_students]) 1394 | st.dataframe(not_started_df, use_container_width=True) 1395 | 1396 | st.info(f"💌 Contact these {len(not_submitted_students)} families to complete their lunch selections!") 1397 | else: 1398 | st.success("🎉 All families have submitted their lunch selections!") 1399 | 1400 | # Display all students with status 1401 | st.subheader("All Registered Students") 1402 | if students_data: 1403 | all_students_df = pd.DataFrame([{ 1404 | 'Family': s['families']['family_name'], 1405 | 'Student': f"{s['first_name']} {s['last_name']}", 1406 | 'Type': s['student_type'].title(), 1407 | 'Prepaid': 'Yes' if s['is_prepaid'] else 'No', 1408 | 'Email': s['families']['email'], 1409 | 'Status': ('✅ Submitted' if s['id'] in submitted_student_ids 1410 | else '📝 Draft' if s['id'] in draft_student_ids 1411 | else '❌ Not Started') 1412 | } for s in students_data]) 1413 | 1414 | st.dataframe(all_students_df, use_container_width=True) 1415 | 1416 | except Exception as e: 1417 | st.error(f"Error loading submissions: {str(e)}") 1418 | else: 1419 | st.info("No active menus found.") 1420 | 1421 | with tab4: 1422 | st.subheader("Export Data") 1423 | 1424 | active_menus = get_active_menus() 1425 | if active_menus: 1426 | menu_options = [f"{m['title']}" for m in active_menus] 1427 | export_menu_idx = st.selectbox("Select Menu to Export", range(len(menu_options)), format_func=lambda x: menu_options[x], key="export_menu_select") 1428 | export_menu = active_menus[export_menu_idx] 1429 | 1430 | if st.button("Generate CSV Export"): 1431 | try: 1432 | # This would generate a comprehensive CSV with all selections 1433 | # For now, showing placeholder structure 1434 | export_data = { 1435 | 'Family': ['Smith', 'Johnson'], 1436 | 'Student': ['John Smith', 'Emma Johnson'], 1437 | 'Type': ['Day School', 'Preschool'], 1438 | 'Prepaid': ['Yes', 'No'], 1439 | 'Total_Meals': [15, 12], 1440 | 'Extra_Portions': [3, 0], 1441 | 'Total_Cost': [4.50, 60.00] 1442 | } 1443 | 1444 | df = pd.DataFrame(export_data) 1445 | csv = df.to_csv(index=False) 1446 | 1447 | st.download_button( 1448 | label="Download CSV", 1449 | data=csv, 1450 | file_name=f"lunch_selections_{export_menu['title'].replace(' ', '_')}.csv", 1451 | mime="text/csv" 1452 | ) 1453 | 1454 | except Exception as e: 1455 | st.error(f"Export failed: {str(e)}") 1456 | else: 1457 | st.info("No active menus to export.") 1458 | 1459 | if st.button("Logout Admin"): 1460 | st.session_state.admin_logged_in = False 1461 | st.rerun() 1462 | 1463 | def main(): 1464 | st.set_page_config( 1465 | page_title="Margo's Lunch Menu", 1466 | page_icon="🍽️", 1467 | layout="wide" 1468 | ) 1469 | 1470 | # Check if family is logged in 1471 | if 'family' not in st.session_state: 1472 | login_page() 1473 | return 1474 | 1475 | # Navigation 1476 | family = st.session_state.family 1477 | 1478 | st.sidebar.title(f"Welcome, {family['family_name']}!") 1479 | st.sidebar.write(f"Email: {family['email']}") 1480 | 1481 | page = st.sidebar.selectbox( 1482 | "Navigate", 1483 | ["Menu Selection", "Family Summary", "Manage Students", "Admin Panel"] 1484 | ) 1485 | 1486 | if st.sidebar.button("Logout"): 1487 | del st.session_state.family 1488 | st.rerun() 1489 | 1490 | # Route to appropriate page 1491 | if page == "Menu Selection": 1492 | menu_selection_page() 1493 | elif page == "Family Summary": 1494 | family_summary_page() 1495 | elif page == "Manage Students": 1496 | student_management_page() 1497 | elif page == "Admin Panel": 1498 | admin_page() 1499 | 1500 | if __name__ == "__main__": 1501 | main() --------------------------------------------------------------------------------