11from flask import render_template , request , redirect , url_for , session , flash , abort , jsonify , g
22from app import db
3- from models import User , Dish , Menu , Feedback , Request , ProcurementItem , Team , TeamMember , TeaTask , FloorLendBorrow , SpecialEvent , Announcement , Suggestion , SuggestionVote , Expense , Budget , FacultyBudgetCycle , FacultyReportSubmission , FacultyMessage , FacultyMessageFloor
3+ from models import User , Dish , DishAuditLog , Menu , Feedback , Request , ProcurementItem , Team , TeamMember , TeaTask , FloorLendBorrow , SpecialEvent , Announcement , Suggestion , SuggestionVote , Expense , Budget , FacultyBudgetCycle , FacultyReportSubmission , FacultyMessage , FacultyMessageFloor , normalize_dish_name
44from datetime import datetime , date , timedelta
55from sqlalchemy import or_ , func
66from sqlalchemy .orm import joinedload
@@ -24,6 +24,69 @@ def _clear_dashboard_cache(tenant_id, floor):
2424 """Helper to clear cached dashboard stats for a specific tenant and floor."""
2525 cache .delete_memoized (_get_dashboard_stats , tenant_id , floor )
2626
27+ def _active_dish_query ():
28+ return Dish .query .filter (Dish .is_archived == False )
29+
30+ def _find_global_dish_by_name (name , category = None ):
31+ normalized = normalize_dish_name (name )
32+ if not normalized :
33+ return None
34+ query = _active_dish_query ().filter (Dish .normalized_name == normalized )
35+ if category == 'main' :
36+ query = query .filter (Dish .category .in_ (['main' , 'both' ]))
37+ elif category == 'side' :
38+ query = query .filter (Dish .category .in_ (['side' , 'both' ]))
39+ return query .order_by (Dish .id .asc ()).first ()
40+
41+ def _log_dish_action (action , dish , user , description , details = None , target_dish_id = None ):
42+ db .session .add (DishAuditLog (
43+ action = action ,
44+ dish_id = dish .id if dish else None ,
45+ target_dish_id = target_dish_id ,
46+ description = description ,
47+ details_json = details or {},
48+ performed_by_id = user .id if user else None ,
49+ actor_tenant_id = getattr (g , 'tenant_id' , None ),
50+ ))
51+
52+ def _create_global_dish (name , category , user ):
53+ dish = Dish (
54+ name = (name or '' ).strip (),
55+ category = category ,
56+ created_by_id = user .id if user else None ,
57+ origin_tenant_id = getattr (g , 'tenant_id' , None ),
58+ )
59+ db .session .add (dish )
60+ db .session .flush ()
61+ _log_dish_action (
62+ 'create' ,
63+ dish ,
64+ user ,
65+ f'Created global dish "{ dish .name } " from menu scheduling.' ,
66+ {'category' : category },
67+ )
68+ return dish
69+
70+ def _estimate_payload_for (dish ):
71+ estimate = getattr (dish , 'estimate' , None )
72+ if not estimate :
73+ return {
74+ 'available' : False ,
75+ 'serving_count' : 30 ,
76+ 'summary' : '' ,
77+ 'ingredients' : [],
78+ 'tips' : [],
79+ 'updated_at' : None ,
80+ }
81+ return {
82+ 'available' : True ,
83+ 'serving_count' : estimate .serving_count or 30 ,
84+ 'summary' : estimate .summary or '' ,
85+ 'ingredients' : estimate .ingredients_json or [],
86+ 'tips' : estimate .tips_json or [],
87+ 'updated_at' : estimate .updated_at .isoformat () if estimate .updated_at else None ,
88+ }
89+
2790@cache .memoize (timeout = 300 ) # 5-minute cache
2891def _get_dashboard_stats (tenant_id , floor ):
2992 """Heavy aggregate queries moved to a memoized function."""
@@ -575,17 +638,18 @@ def bulk_schedule():
575638
576639 new_dish_name = item .get ('new_dish_name' )
577640
641+ if dish_id and not _active_dish_query ().filter_by (id = dish_id ).first ():
642+ dish_id = None
643+
578644 if not dish_id and not new_dish_name : continue
579645
580646 # Handle Main Dish creation
581647 if not dish_id and new_dish_name :
582- existing = tenant_filter ( Dish . query ). filter ( func . lower ( Dish . name ) == new_dish_name . lower ()). first ( )
648+ existing = _find_global_dish_by_name ( new_dish_name , category = 'main' )
583649 if existing :
584650 dish_id = existing .id
585651 else :
586- new_dish = Dish (name = new_dish_name , category = 'main' , created_by_id = user .id , tenant_id = getattr (g , 'tenant_id' , None ))
587- db .session .add (new_dish )
588- db .session .flush ()
652+ new_dish = _create_global_dish (new_dish_name , 'main' , user )
589653 dish_id = new_dish .id
590654
591655 # Handle Side Dish creation
@@ -596,14 +660,14 @@ def bulk_schedule():
596660 else : side_dish_id = None if not side_dish_id else side_dish_id
597661
598662 new_side_dish_name = item .get ('new_side_dish_name' )
663+ if side_dish_id and not _active_dish_query ().filter_by (id = side_dish_id ).first ():
664+ side_dish_id = None
599665 if not side_dish_id and new_side_dish_name :
600- existing_side = tenant_filter ( Dish . query ). filter ( func . lower ( Dish . name ) == new_side_dish_name . lower ()). first ( )
666+ existing_side = _find_global_dish_by_name ( new_side_dish_name , category = 'side' )
601667 if existing_side :
602668 side_dish_id = existing_side .id
603669 else :
604- new_side = Dish (name = new_side_dish_name , category = 'side' , created_by_id = user .id , tenant_id = getattr (g , 'tenant_id' , None ))
605- db .session .add (new_side )
606- db .session .flush ()
670+ new_side = _create_global_dish (new_side_dish_name , 'side' , user )
607671 side_dish_id = new_side .id
608672
609673 assigned_team_id = item .get ('assigned_team_id' )
@@ -642,8 +706,8 @@ def bulk_schedule():
642706 members = tenant_filter (TeamMember .query ).filter_by (team_id = assigned_team_id ).all ()
643707 recipients .extend ([m .user_id for m in members ])
644708
645- dish_obj = tenant_filter ( Dish .query ) .filter_by (id = dish_id ).first () if dish_id else None
646- side_obj = tenant_filter ( Dish .query ) .filter_by (id = side_dish_id ).first () if side_dish_id else None
709+ dish_obj = Dish .query .filter_by (id = dish_id ).first () if dish_id else None
710+ side_obj = Dish .query .filter_by (id = side_dish_id ).first () if side_dish_id else None
647711 dish_label = f"{ dish_obj .name if dish_obj else menu .title } { ' + ' + side_obj .name if side_obj else '' } "
648712
649713 print (f"DEBUG: Found { len (recipients )} recipients for date { menu_date } " )
@@ -1105,10 +1169,10 @@ def menus():
11051169 floor_users = tenant_filter (User .query ).filter_by (floor = floor ).all ()
11061170 floor_teams = tenant_filter (Team .query ).filter_by (floor = floor ).order_by (Team .name .asc ()).all ()
11071171
1108- # Standardize on 'Dish' (singular) as per models.py
1109- main_dishes = tenant_filter ( Dish . query ).filter (Dish .category .in_ (['main' , 'both' ])).order_by (func .lower (Dish .name ).asc ()).all ()
1110- side_dishes = tenant_filter ( Dish . query ).filter (Dish .category .in_ (['side' , 'both' ])).order_by (func .lower (Dish .name ).asc ()).all ()
1111- all_dishes = tenant_filter ( Dish . query ).order_by (func .lower (Dish .name ).asc ()).all ()
1172+ # Dishes are global platform catalog entries; floor data remains tenant scoped.
1173+ main_dishes = _active_dish_query ( ).filter (Dish .category .in_ (['main' , 'both' ])).order_by (func .lower (Dish .name ).asc ()).all ()
1174+ side_dishes = _active_dish_query ( ).filter (Dish .category .in_ (['side' , 'both' ])).order_by (func .lower (Dish .name ).asc ()).all ()
1175+ all_dishes = _active_dish_query ( ).order_by (func .lower (Dish .name ).asc ()).all ()
11121176
11131177 if request .method == 'POST' and user .role in ['admin' , 'pantryHead' ]:
11141178 try :
@@ -1136,15 +1200,16 @@ def menus():
11361200 if not dish_id_val :
11371201 flash ('Invalid dish selected' , 'error' )
11381202 return redirect (url_for ('pantry.menus' ))
1139- dish = tenant_filter (Dish .query ).filter_by (id = dish_id_val ).first ()
1203+ dish = _active_dish_query ().filter_by (id = dish_id_val ).first ()
1204+ if not dish :
1205+ flash ('Selected dish is no longer available' , 'error' )
1206+ return redirect (url_for ('pantry.menus' ))
11401207 elif new_dish_name :
1141- existing = tenant_filter ( Dish . query ). filter ( func . lower ( Dish . name ) == new_dish_name . lower ()). first ( )
1208+ existing = _find_global_dish_by_name ( new_dish_name , category = 'main' )
11421209 if existing :
11431210 dish = existing
11441211 else :
1145- dish = Dish (name = new_dish_name , category = 'main' , created_by_id = user .id , tenant_id = getattr (g , 'tenant_id' , None ))
1146- db .session .add (dish )
1147- db .session .flush ()
1212+ dish = _create_global_dish (new_dish_name , 'main' , user )
11481213 else :
11491214 flash ('Please select a main dish' , 'error' )
11501215 return redirect (url_for ('pantry.menus' ))
@@ -1154,17 +1219,17 @@ def menus():
11541219 new_side_dish_name = (request .form .get ('new_side_dish_name' ) or '' ).strip ()
11551220
11561221 if new_side_dish_name :
1157- existing_side = tenant_filter ( Dish . query ). filter ( func . lower ( Dish . name ) == new_side_dish_name . lower ()). first ( )
1222+ existing_side = _find_global_dish_by_name ( new_side_dish_name , category = 'side' )
11581223 if existing_side :
11591224 side_dish_id = existing_side .id
11601225 else :
1161- side_dish = Dish (name = new_side_dish_name , category = 'side' , created_by_id = user .id , tenant_id = getattr (g , 'tenant_id' , None ))
1162- db .session .add (side_dish )
1163- db .session .flush ()
1226+ side_dish = _create_global_dish (new_side_dish_name , 'side' , user )
11641227 side_dish_id = side_dish .id
11651228 elif side_dish_id :
11661229 try :
11671230 side_dish_id = int (side_dish_id )
1231+ if not _active_dish_query ().filter_by (id = side_dish_id ).first ():
1232+ side_dish_id = None
11681233 except ValueError :
11691234 side_dish_id = None
11701235
@@ -1322,10 +1387,18 @@ def suggestions():
13221387 return redirect (url_for ('pantry.feedbacks' ) + '#suggestions' )
13231388
13241389 floor = _get_active_floor (user )
1390+ dish_id = request .form .get ('dish_id' ) or None
1391+ if dish_id :
1392+ try :
1393+ dish_id_val = int (dish_id )
1394+ except (TypeError , ValueError ):
1395+ dish_id_val = None
1396+ dish_id = dish_id_val if dish_id_val and _active_dish_query ().filter_by (id = dish_id_val ).first () else None
1397+
13251398 suggestion = Suggestion (
13261399 title = (request .form .get ('title' ) or '' ).strip (),
13271400 description = (request .form .get ('description' ) or '' ).strip (),
1328- dish_id = request . form . get ( ' dish_id' ) or None ,
1401+ dish_id = dish_id ,
13291402 user_id = user .id ,
13301403 floor = floor ,
13311404 tenant_id = getattr (g , 'tenant_id' , None )
@@ -1350,6 +1423,9 @@ def get_dish_insights(dish_id):
13501423 return jsonify ({'error' : 'Unauthorized' }), 401
13511424
13521425 floor = _get_active_floor (user )
1426+ dish = Dish .query .filter_by (id = dish_id ).first ()
1427+ if not dish :
1428+ return jsonify ({'error' : 'Dish not found' }), 404
13531429
13541430 # 1. Overall Avg Rating for this dish on this floor
13551431 avg_rating = db .session .query (func .avg (Feedback .rating )).join (Menu ).filter (
@@ -1385,7 +1461,8 @@ def get_dish_insights(dish_id):
13851461 return jsonify ({
13861462 'avg_rating' : round (float (avg_rating ), 1 ),
13871463 'champion' : champion_name ,
1388- 'suggestions' : [s [0 ] for s in suggestions ]
1464+ 'suggestions' : [s [0 ] for s in suggestions ],
1465+ 'estimate' : _estimate_payload_for (dish ),
13891466 })
13901467
13911468@pantry_bp .route ('/suggestions/<int:suggestion_id>/vote' , methods = ['POST' ])
@@ -1452,6 +1529,12 @@ def feedbacks():
14521529 title = (request .form .get ('title' ) or '' ).strip ()
14531530 description = (request .form .get ('description' ) or '' ).strip ()
14541531 dish_id = request .form .get ('dish_id' ) or None
1532+ if dish_id :
1533+ try :
1534+ dish_id_val = int (dish_id )
1535+ except (TypeError , ValueError ):
1536+ dish_id_val = None
1537+ dish_id = dish_id_val if dish_id_val and _active_dish_query ().filter_by (id = dish_id_val ).first () else None
14551538
14561539 if not title or not description :
14571540 flash ('Please provide both a title and description for your suggestion.' , 'error' )
@@ -1562,7 +1645,7 @@ def feedbacks():
15621645 f .menu_id for f in tenant_filter (Feedback .query ).filter_by (user_id = user .id ).filter (Feedback .menu_id .isnot (None )).all ()
15631646 }
15641647
1565- dishes = tenant_filter ( Dish . query ).order_by (func .lower (Dish .name ).asc ()).all ()
1648+ dishes = _active_dish_query ( ).order_by (func .lower (Dish .name ).asc ()).all ()
15661649 vote_count_subquery = (
15671650 db .session .query (func .count (SuggestionVote .id ))
15681651 .filter (SuggestionVote .suggestion_id == Suggestion .id )
0 commit comments