├── 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()
--------------------------------------------------------------------------------