Skip to content

Commit 2234736

Browse files
committed
implement
1 parent 90ed25e commit 2234736

File tree

6 files changed

+652
-61
lines changed

6 files changed

+652
-61
lines changed

blueprints/admin/routes.py

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,49 @@ def admin():
175175
flash('Role assigned successfully', 'success')
176176
return redirect(url_for('admin_panel.admin'))
177177

178+
if action == 'bulk_reassign':
179+
try:
180+
from_user_id = int(request.form.get('from_user_id') or '')
181+
to_user_id = int(request.form.get('to_user_id') or '')
182+
reassign_types = request.form.getlist('reassign_types')
183+
184+
if from_user_id == to_user_id:
185+
flash('Source and target user cannot be the same.', 'error')
186+
return redirect(url_for('admin_panel.admin'))
187+
188+
from_user = tenant_filter(User.query).filter_by(id=from_user_id).first()
189+
to_user = tenant_filter(User.query).filter_by(id=to_user_id).first()
190+
191+
if not from_user or not to_user:
192+
flash('One or both users not found.', 'error')
193+
return redirect(url_for('admin_panel.admin'))
194+
195+
updates = 0
196+
if 'menu' in reassign_types:
197+
count = tenant_filter(Menu.query).filter_by(assigned_to_id=from_user_id).update(
198+
{Menu.assigned_to_id: to_user_id}, synchronize_session=False
199+
)
200+
updates += count
201+
202+
if 'tea' in reassign_types:
203+
count = tenant_filter(TeaTask.query).filter_by(assigned_to_id=from_user_id).update(
204+
{TeaTask.assigned_to_id: to_user_id}, synchronize_session=False
205+
)
206+
updates += count
207+
208+
if 'procurement' in reassign_types:
209+
count = tenant_filter(ProcurementItem.query).filter_by(assigned_to_id=from_user_id).update(
210+
{ProcurementItem.assigned_to_id: to_user_id}, synchronize_session=False
211+
)
212+
updates += count
213+
214+
db.session.commit()
215+
flash(f'Successfully reassigned {updates} items from {from_user.full_name or from_user.email} to {to_user.full_name or to_user.email}.', 'success')
216+
except Exception as e:
217+
db.session.rollback()
218+
flash(f'Error during reassignment: {str(e)}', 'error')
219+
return redirect(url_for('admin_panel.admin'))
220+
178221
if action == 'delete_user':
179222
try:
180223
target_user_id = int(request.form.get('user_id') or '')
@@ -286,16 +329,22 @@ def admin_floor_members():
286329
if floor < FLOOR_MIN or floor > FLOOR_MAX:
287330
return jsonify({"error": "invalid_floor"}), 400
288331

289-
members = (
290-
tenant_filter(User.query).filter_by(floor=floor, role='member')
291-
.order_by(User.tr_number.asc(), User.full_name.asc(), User.email.asc())
332+
role_type = (request.args.get('role') or 'member').strip()
333+
334+
query = tenant_filter(User.query).filter_by(floor=floor)
335+
if role_type != 'all':
336+
query = query.filter_by(role=role_type)
337+
338+
users = (
339+
query.order_by(User.tr_number.asc(), User.full_name.asc(), User.email.asc())
292340
.all()
293341
)
294342

295343
def _label(u):
296344
name = (u.full_name or u.username or u.email or '').strip()
297345
tr = (u.tr_number or '-').strip()
298-
return f"{tr} - {name}".strip(' -')
346+
role_label = f" ({u.role})" if role_type == 'all' else ""
347+
return f"{tr} - {name}{role_label}".strip(' -')
299348

300349
return jsonify(
301350
{
@@ -307,7 +356,7 @@ def _label(u):
307356
"name": (u.full_name or u.username or u.email),
308357
"label": _label(u),
309358
}
310-
for u in members
359+
for u in users
311360
],
312361
}
313362
)

blueprints/ops/routes.py

Lines changed: 168 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def tea():
2626
floor = _get_active_floor(user)
2727
floor_users = tenant_filter(User.query).filter_by(floor=floor).all()
2828

29-
if request.method == 'POST' and user.role in ['admin', 'teaManager']:
29+
if request.method == 'POST' and user.role in ['admin', 'teaManager', 'pantryHead']:
3030
try:
3131
task_date = datetime.strptime(request.form.get('date'), '%Y-%m-%d').date()
3232
except Exception:
@@ -66,6 +66,14 @@ def tea():
6666
flash('tea task added successfully', 'success')
6767
return redirect(url_for('ops.tea'))
6868

69+
# Auto-complete past pending tasks for this floor
70+
tenant_filter(TeaTask.query).filter(
71+
TeaTask.floor == floor,
72+
TeaTask.date < date.today(),
73+
TeaTask.status == 'pending'
74+
).update({TeaTask.status: 'completed'}, synchronize_session=False)
75+
db.session.commit()
76+
6977
month_param = (request.args.get('month') or '').strip()
7078
today = date.today()
7179
if month_param:
@@ -117,6 +125,7 @@ def tea():
117125
selected_month=month_param,
118126
month_start=month_start,
119127
current_user=user,
128+
today=today
120129
)
121130

122131
@ops_bp.route('/tea/complete/<int:task_id>', methods=['POST'])
@@ -125,7 +134,7 @@ def complete_tea_task(task_id):
125134
if not user:
126135
return ('', 401)
127136

128-
if user.role not in ['admin', 'teaManager']:
137+
if user.role not in ['admin', 'teaManager', 'pantryHead']:
129138
return ('', 403)
130139

131140
task = tenant_filter(TeaTask.query).filter_by(id=task_id).first()
@@ -138,6 +147,163 @@ def complete_tea_task(task_id):
138147
db.session.commit()
139148
return ('', 204)
140149

150+
@ops_bp.route('/tea/bulk-assign', methods=['POST'])
151+
def bulk_assign_tea():
152+
user = _require_user()
153+
if not user or user.role not in ['admin', 'teaManager', 'pantryHead']:
154+
abort(403)
155+
156+
floor = _get_active_floor(user)
157+
try:
158+
start_date = datetime.strptime(request.form.get('start_date'), '%Y-%m-%d').date()
159+
end_date = datetime.strptime(request.form.get('end_date'), '%Y-%m-%d').date()
160+
user_ids = request.form.getlist('user_ids')
161+
except Exception:
162+
flash('Invalid rotation parameters', 'error')
163+
return redirect(url_for('ops.tea'))
164+
165+
if not user_ids:
166+
flash('Please select at least one user for rotation.', 'error')
167+
return redirect(url_for('ops.tea'))
168+
169+
if start_date > end_date:
170+
flash('Start date cannot be after end_date.', 'error')
171+
return redirect(url_for('ops.tea'))
172+
173+
current_date = start_date
174+
user_index = 0
175+
tasks_created = 0
176+
from datetime import timedelta
177+
178+
while current_date <= end_date:
179+
# Check if task already exists for this floor and date
180+
existing = tenant_filter(TeaTask.query).filter_by(floor=floor, date=current_date).first()
181+
if not existing:
182+
# Try to find the next available user in the rotation
183+
found_user = False
184+
for _ in range(len(user_ids)):
185+
target_user_id = int(user_ids[user_index % len(user_ids)])
186+
187+
# Check for approved absence request
188+
absence = tenant_filter(Request.query).filter(
189+
Request.user_id == target_user_id,
190+
Request.status == 'approved',
191+
Request.request_type == 'absence',
192+
Request.start_date <= current_date,
193+
Request.end_date >= current_date
194+
).first()
195+
196+
if not absence:
197+
# Found an available user
198+
new_task = TeaTask(
199+
date=current_date,
200+
assigned_to_id=target_user_id,
201+
floor=floor,
202+
created_by_id=user.id,
203+
tenant_id=getattr(g, 'tenant_id', None)
204+
)
205+
db.session.add(new_task)
206+
tasks_created += 1
207+
user_index += 1 # Move to next user for next date
208+
found_user = True
209+
break
210+
else:
211+
# User is on leave, try the next one for THIS same date
212+
user_index += 1
213+
214+
# if no user was found available for this date, the date is skipped
215+
216+
current_date += timedelta(days=1)
217+
218+
db.session.commit()
219+
flash(f'Successfully generated {tasks_created} tea tasks.', 'success')
220+
return redirect(url_for('ops.tea'))
221+
222+
@ops_bp.route('/tea/bulk-assign-preview', methods=['POST'])
223+
def bulk_assign_preview():
224+
user = _require_user()
225+
if not user or user.role not in ['admin', 'teaManager', 'pantryHead']:
226+
return jsonify({"error": "unauthorized"}), 403
227+
228+
floor = _get_active_floor(user)
229+
try:
230+
data = request.get_json()
231+
start_date = datetime.strptime(data.get('start_date'), '%Y-%m-%d').date()
232+
end_date = datetime.strptime(data.get('end_date'), '%Y-%m-%d').date()
233+
user_ids = [int(uid) for uid in data.get('user_ids', [])]
234+
except Exception:
235+
return jsonify({"error": "invalid_parameters"}), 400
236+
237+
if not user_ids:
238+
return jsonify({"error": "no_users_selected"}), 400
239+
240+
current_date = start_date
241+
user_index = 0
242+
preview_tasks = []
243+
from datetime import timedelta
244+
245+
# Get floor user names for labeling
246+
users_map = {u.id: (u.full_name or u.username or u.email) for u in tenant_filter(User.query).filter_by(floor=floor).all()}
247+
248+
while current_date <= end_date:
249+
existing = tenant_filter(TeaTask.query).filter_by(floor=floor, date=current_date).first()
250+
if not existing:
251+
found_user = False
252+
for _ in range(len(user_ids)):
253+
target_user_id = user_ids[user_index % len(user_ids)]
254+
absence = tenant_filter(Request.query).filter(
255+
Request.user_id == target_user_id,
256+
Request.status == 'approved',
257+
Request.request_type == 'absence',
258+
Request.start_date <= current_date,
259+
Request.end_date >= current_date
260+
).first()
261+
262+
if not absence:
263+
preview_tasks.append({
264+
"date": current_date.strftime('%Y-%m-%d'),
265+
"day": current_date.strftime('%A'),
266+
"user_id": target_user_id,
267+
"user_name": users_map.get(target_user_id, "Unknown")
268+
})
269+
user_index += 1
270+
found_user = True
271+
break
272+
else:
273+
user_index += 1
274+
275+
current_date += timedelta(days=1)
276+
277+
return jsonify({"tasks": preview_tasks})
278+
279+
@ops_bp.route('/tea/fail/<int:task_id>', methods=['POST'])
280+
def fail_tea_task(task_id):
281+
user = _require_user()
282+
if not user or user.role not in ['admin', 'teaManager', 'pantryHead']:
283+
return ('', 403)
284+
285+
task = tenant_filter(TeaTask.query).filter_by(id=task_id).first()
286+
if not task: return ('', 404)
287+
if user.role != 'admin' and task.floor != user.floor: return ('', 403)
288+
289+
task.status = 'failed'
290+
db.session.commit()
291+
return ('', 204)
292+
293+
@ops_bp.route('/tea/pending/<int:task_id>', methods=['POST'])
294+
def reset_tea_task(task_id):
295+
user = _require_user()
296+
if not user or user.role not in ['admin', 'teaManager', 'pantryHead']:
297+
return ('', 403)
298+
299+
task = tenant_filter(TeaTask.query).filter_by(id=task_id).first()
300+
if not task: return ('', 404)
301+
if user.role != 'admin' and task.floor != user.floor: return ('', 403)
302+
303+
task.status = 'pending'
304+
db.session.commit()
305+
return ('', 204)
306+
141307
@ops_bp.route('/requests', methods=['GET', 'POST'])
142308
def requests():
143309
user = _require_user()

blueprints/pantry/routes.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -387,9 +387,49 @@ def calendar():
387387
return redirect(url_for('auth.login'))
388388

389389
floor = _get_active_floor(user)
390-
floor_menus = tenant_filter(Menu.query).options(joinedload(Menu.assigned_to), joinedload(Menu.assigned_team)).filter_by(floor=floor).all()
391-
floor_tea_tasks = tenant_filter(TeaTask.query).options(joinedload(TeaTask.assigned_to)).filter_by(floor=floor).all()
392-
floor_special_events = tenant_filter(SpecialEvent.query).options(joinedload(SpecialEvent.created_by)).filter_by(floor=floor).all()
390+
391+
# Get year and month from query params or default to current date
392+
now = datetime.now()
393+
try:
394+
current_year = int(request.args.get('year', now.year))
395+
current_month = int(request.args.get('month', now.month))
396+
except (ValueError, TypeError):
397+
current_year = now.year
398+
current_month = now.month
399+
400+
# Handle month rollover safely (e.g. month=0 or month=13)
401+
if current_month < 1:
402+
current_month = 12
403+
current_year -= 1
404+
elif current_month > 12:
405+
current_month = 1
406+
current_year += 1
407+
408+
# Calculate range for calendar grid (roughly 6 weeks)
409+
start_bound = date(current_year, current_month, 1) - timedelta(days=7)
410+
if current_month == 12:
411+
next_month_start = date(current_year + 1, 1, 1)
412+
else:
413+
next_month_start = date(current_year, current_month + 1, 1)
414+
end_bound = next_month_start + timedelta(days=14)
415+
416+
floor_menus = tenant_filter(Menu.query).options(joinedload(Menu.assigned_to), joinedload(Menu.assigned_team)).filter(
417+
Menu.floor == floor,
418+
Menu.date >= start_bound,
419+
Menu.date <= end_bound
420+
).all()
421+
422+
floor_tea_tasks = tenant_filter(TeaTask.query).options(joinedload(TeaTask.assigned_to)).filter(
423+
TeaTask.floor == floor,
424+
TeaTask.date >= start_bound,
425+
TeaTask.date <= end_bound
426+
).all()
427+
428+
floor_special_events = tenant_filter(SpecialEvent.query).options(joinedload(SpecialEvent.created_by)).filter(
429+
SpecialEvent.floor == floor,
430+
SpecialEvent.date >= start_bound,
431+
SpecialEvent.date <= end_bound
432+
).all()
393433

394434
menus = [
395435
{
@@ -440,7 +480,14 @@ def calendar():
440480
for s in floor_special_events
441481
]
442482

443-
return render_template('calendar.html', menus=menus, tea_tasks=tea_tasks, special_events=special_events, current_user=user, active_floor=floor)
483+
return render_template('calendar.html',
484+
menus=menus,
485+
tea_tasks=tea_tasks,
486+
special_events=special_events,
487+
current_user=user,
488+
active_floor=floor,
489+
current_year=current_year,
490+
current_month=current_month)
444491

445492
@pantry_bp.route('/special-events', methods=['POST'])
446493
def create_special_event():

0 commit comments

Comments
 (0)