Skip to content

Commit 013959f

Browse files
committed
menu revamp v1
1 parent f7f3aae commit 013959f

10 files changed

Lines changed: 2166 additions & 33 deletions

File tree

ROUTE_MAP.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,25 @@ This file serves as a master index for all routes within the modularized AJS Pan
130130

131131
---
132132

133+
## 7. Super Admin Blueprint (`blueprints/super_admin/routes.py`)
134+
*Platform-wide tenant and global catalog governance.*
135+
136+
| Route Path | Function Name | Methods | Description |
137+
|:---|:---|:---|:---|
138+
| `/platform-admin/login` | `login` | GET, POST | Super Admin login |
139+
| `/platform-admin/dashboard` | `dashboard` | GET | Platform analytics dashboard |
140+
| `/platform-admin/tenants` | `tenants_list` | GET | Tenant list and infrastructure status |
141+
| `/platform-admin/tenants/<tenant_id>` | `tenant_detail` | GET | Tenant configuration and Faculty account management |
142+
| `/platform-admin/dishes` | `global_dishes` | GET | Global dish catalog, duplicate candidates, trends, and audit log |
143+
| `/platform-admin/dishes/add` | `add_global_dish` | POST | Create a global dish |
144+
| `/platform-admin/dishes/<id>/edit` | `edit_global_dish` | POST | Edit global dish name/category |
145+
| `/platform-admin/dishes/<id>/archive` | `archive_global_dish` | POST | Archive or restore a global dish |
146+
| `/platform-admin/dishes/<id>/estimate` | `update_dish_estimate` | POST | Create or update the 30-person dish estimate |
147+
| `/platform-admin/dishes/merge/preview` | `preview_dish_merge` | POST | Preview a manual duplicate merge |
148+
| `/platform-admin/dishes/merge/confirm` | `confirm_dish_merge` | POST | Confirm manual merge and archive duplicate dishes |
149+
150+
---
151+
133152
## Helper Functions Reference (`blueprints/utils.py`)
134153
*Critical internal logic used across routes:*
135154
* `_get_current_user()`: Retrieves User object from session.

blueprints/pantry/routes.py

Lines changed: 110 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from flask import render_template, request, redirect, url_for, session, flash, abort, jsonify, g
22
from 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
44
from datetime import datetime, date, timedelta
55
from sqlalchemy import or_, func
66
from 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
2891
def _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

Comments
 (0)