""" Manager Rules Routes Manager settings, closing days, guarantees, and exclusions Key concept: Managers own parking spots and set rules for their managed users. Rules are set at manager level (users have manager_id pointing to their manager). """ from datetime import datetime from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from sqlalchemy.orm import Session from sqlalchemy import func from database.connection import get_db from database.models import ( User, ManagerClosingDay, ManagerWeeklyClosingDay, ParkingGuarantee, ParkingExclusion ) from utils.auth_middleware import require_admin, require_manager_or_admin, get_current_user from utils.helpers import generate_uuid from app import config router = APIRouter(prefix="/api/managers", tags=["managers"]) # Request/Response Models class ClosingDayCreate(BaseModel): date: str # YYYY-MM-DD reason: str | None = None class WeeklyClosingDayCreate(BaseModel): weekday: int # 0=Sunday, 1=Monday, ..., 6=Saturday class GuaranteeCreate(BaseModel): user_id: str start_date: str | None = None end_date: str | None = None class ExclusionCreate(BaseModel): user_id: str start_date: str | None = None end_date: str | None = None class ManagerSettingsUpdate(BaseModel): parking_quota: int | None = None spot_prefix: str | None = None # Manager listing and details @router.get("") def list_managers(db: Session = Depends(get_db), user=Depends(require_manager_or_admin)): """Get all managers with their managed user count and parking quota""" managers = db.query(User).filter(User.role == "manager").all() # Batch query to get managed user counts for all managers at once manager_ids = [m.id for m in managers] if manager_ids: counts = db.query(User.manager_id, func.count(User.id)).filter( User.manager_id.in_(manager_ids) ).group_by(User.manager_id).all() managed_counts = {manager_id: count for manager_id, count in counts} else: managed_counts = {} return [ { "id": manager.id, "name": manager.name, "email": manager.email, "parking_quota": manager.manager_parking_quota or 0, "spot_prefix": manager.manager_spot_prefix, "managed_user_count": managed_counts.get(manager.id, 0) } for manager in managers ] @router.get("/{manager_id}") def get_manager_details(manager_id: str, db: Session = Depends(get_db), user=Depends(require_manager_or_admin)): """Get manager details including managed users and parking settings""" manager = db.query(User).filter(User.id == manager_id, User.role == "manager").first() if not manager: raise HTTPException(status_code=404, detail="Manager not found") managed_user_count = db.query(User).filter(User.manager_id == manager_id).count() return { "id": manager.id, "name": manager.name, "email": manager.email, "parking_quota": manager.manager_parking_quota or 0, "spot_prefix": manager.manager_spot_prefix, "managed_user_count": managed_user_count } @router.put("/{manager_id}/settings") def update_manager_settings(manager_id: str, data: ManagerSettingsUpdate, db: Session = Depends(get_db), user=Depends(require_admin)): """Update manager parking settings (admin only)""" manager = db.query(User).filter(User.id == manager_id, User.role == "manager").first() if not manager: raise HTTPException(status_code=404, detail="Manager not found") if data.parking_quota is not None: if data.parking_quota < 0: raise HTTPException(status_code=400, detail="Parking quota must be non-negative") manager.manager_parking_quota = data.parking_quota if data.spot_prefix is not None: if data.spot_prefix and not data.spot_prefix.isalpha(): raise HTTPException(status_code=400, detail="Spot prefix must be a letter") if data.spot_prefix: data.spot_prefix = data.spot_prefix.upper() existing = db.query(User).filter( User.manager_spot_prefix == data.spot_prefix, User.id != manager_id ).first() if existing: raise HTTPException(status_code=400, detail=f"Spot prefix '{data.spot_prefix}' is already used") manager.manager_spot_prefix = data.spot_prefix manager.updated_at = datetime.utcnow().isoformat() db.commit() return { "id": manager.id, "parking_quota": manager.manager_parking_quota, "spot_prefix": manager.manager_spot_prefix } @router.get("/{manager_id}/users") def get_manager_users(manager_id: str, db: Session = Depends(get_db), user=Depends(require_manager_or_admin)): """Get all users in a manager's team (including the manager themselves)""" manager = db.query(User).filter(User.id == manager_id, User.role == "manager").first() if not manager: raise HTTPException(status_code=404, detail="Manager not found") # Include users managed by this manager + the manager themselves users = db.query(User).filter( (User.manager_id == manager_id) | (User.id == manager_id) ).all() return [{"id": u.id, "name": u.name, "email": u.email, "role": u.role} for u in users] # Closing days @router.get("/{manager_id}/closing-days") def get_manager_closing_days(manager_id: str, db: Session = Depends(get_db), user=Depends(get_current_user)): """Get closing days for a manager""" days = db.query(ManagerClosingDay).filter( ManagerClosingDay.manager_id == manager_id ).order_by(ManagerClosingDay.date).all() return [{"id": d.id, "date": d.date, "reason": d.reason} for d in days] @router.post("/{manager_id}/closing-days") def add_manager_closing_day(manager_id: str, data: ClosingDayCreate, db: Session = Depends(get_db), user=Depends(require_manager_or_admin)): """Add a closing day for a manager""" manager = db.query(User).filter(User.id == manager_id, User.role == "manager").first() if not manager: raise HTTPException(status_code=404, detail="Manager not found") existing = db.query(ManagerClosingDay).filter( ManagerClosingDay.manager_id == manager_id, ManagerClosingDay.date == data.date ).first() if existing: raise HTTPException(status_code=400, detail="Closing day already exists for this date") closing_day = ManagerClosingDay( id=generate_uuid(), manager_id=manager_id, date=data.date, reason=data.reason ) db.add(closing_day) db.commit() return {"id": closing_day.id, "message": "Closing day added"} @router.delete("/{manager_id}/closing-days/{closing_day_id}") def remove_manager_closing_day(manager_id: str, closing_day_id: str, db: Session = Depends(get_db), user=Depends(require_manager_or_admin)): """Remove a closing day for a manager""" closing_day = db.query(ManagerClosingDay).filter( ManagerClosingDay.id == closing_day_id, ManagerClosingDay.manager_id == manager_id ).first() if not closing_day: raise HTTPException(status_code=404, detail="Closing day not found") db.delete(closing_day) db.commit() return {"message": "Closing day removed"} # Weekly closing days @router.get("/{manager_id}/weekly-closing-days") def get_manager_weekly_closing_days(manager_id: str, db: Session = Depends(get_db), user=Depends(get_current_user)): """Get weekly closing days for a manager""" days = db.query(ManagerWeeklyClosingDay).filter( ManagerWeeklyClosingDay.manager_id == manager_id ).all() return [{"id": d.id, "weekday": d.weekday} for d in days] @router.post("/{manager_id}/weekly-closing-days") def add_manager_weekly_closing_day(manager_id: str, data: WeeklyClosingDayCreate, db: Session = Depends(get_db), user=Depends(require_manager_or_admin)): """Add a weekly closing day for a manager""" manager = db.query(User).filter(User.id == manager_id, User.role == "manager").first() if not manager: raise HTTPException(status_code=404, detail="Manager not found") if data.weekday < 0 or data.weekday > 6: raise HTTPException(status_code=400, detail="Weekday must be 0-6 (0=Sunday, 6=Saturday)") existing = db.query(ManagerWeeklyClosingDay).filter( ManagerWeeklyClosingDay.manager_id == manager_id, ManagerWeeklyClosingDay.weekday == data.weekday ).first() if existing: raise HTTPException(status_code=400, detail="Weekly closing day already exists for this weekday") weekly_closing = ManagerWeeklyClosingDay( id=generate_uuid(), manager_id=manager_id, weekday=data.weekday ) db.add(weekly_closing) db.commit() return {"id": weekly_closing.id, "message": "Weekly closing day added"} @router.delete("/{manager_id}/weekly-closing-days/{weekly_id}") def remove_manager_weekly_closing_day(manager_id: str, weekly_id: str, db: Session = Depends(get_db), user=Depends(require_manager_or_admin)): """Remove a weekly closing day for a manager""" weekly_closing = db.query(ManagerWeeklyClosingDay).filter( ManagerWeeklyClosingDay.id == weekly_id, ManagerWeeklyClosingDay.manager_id == manager_id ).first() if not weekly_closing: raise HTTPException(status_code=404, detail="Weekly closing day not found") db.delete(weekly_closing) db.commit() return {"message": "Weekly closing day removed"} # Guarantees @router.get("/{manager_id}/guarantees") def get_manager_guarantees(manager_id: str, db: Session = Depends(get_db), user=Depends(require_manager_or_admin)): """Get parking guarantees for a manager""" guarantees = db.query(ParkingGuarantee).filter(ParkingGuarantee.manager_id == manager_id).all() # Batch query to get all user names at once user_ids = [g.user_id for g in guarantees] if user_ids: users = db.query(User).filter(User.id.in_(user_ids)).all() user_lookup = {u.id: u.name for u in users} else: user_lookup = {} return [ { "id": g.id, "user_id": g.user_id, "user_name": user_lookup.get(g.user_id), "start_date": g.start_date, "end_date": g.end_date } for g in guarantees ] @router.post("/{manager_id}/guarantees") def add_manager_guarantee(manager_id: str, data: GuaranteeCreate, db: Session = Depends(get_db), user=Depends(require_manager_or_admin)): """Add parking guarantee for a manager""" manager = db.query(User).filter(User.id == manager_id, User.role == "manager").first() if not manager: raise HTTPException(status_code=404, detail="Manager not found") if not db.query(User).filter(User.id == data.user_id).first(): raise HTTPException(status_code=404, detail="User not found") existing = db.query(ParkingGuarantee).filter( ParkingGuarantee.manager_id == manager_id, ParkingGuarantee.user_id == data.user_id ).first() if existing: raise HTTPException(status_code=400, detail="User already has a parking guarantee") guarantee = ParkingGuarantee( id=generate_uuid(), manager_id=manager_id, user_id=data.user_id, start_date=data.start_date, end_date=data.end_date, created_at=datetime.utcnow().isoformat() ) db.add(guarantee) db.commit() return {"id": guarantee.id, "message": "Guarantee added"} @router.delete("/{manager_id}/guarantees/{guarantee_id}") def remove_manager_guarantee(manager_id: str, guarantee_id: str, db: Session = Depends(get_db), user=Depends(require_manager_or_admin)): """Remove parking guarantee for a manager""" guarantee = db.query(ParkingGuarantee).filter( ParkingGuarantee.id == guarantee_id, ParkingGuarantee.manager_id == manager_id ).first() if not guarantee: raise HTTPException(status_code=404, detail="Guarantee not found") db.delete(guarantee) db.commit() return {"message": "Guarantee removed"} # Exclusions @router.get("/{manager_id}/exclusions") def get_manager_exclusions(manager_id: str, db: Session = Depends(get_db), user=Depends(require_manager_or_admin)): """Get parking exclusions for a manager""" exclusions = db.query(ParkingExclusion).filter(ParkingExclusion.manager_id == manager_id).all() # Batch query to get all user names at once user_ids = [e.user_id for e in exclusions] if user_ids: users = db.query(User).filter(User.id.in_(user_ids)).all() user_lookup = {u.id: u.name for u in users} else: user_lookup = {} return [ { "id": e.id, "user_id": e.user_id, "user_name": user_lookup.get(e.user_id), "start_date": e.start_date, "end_date": e.end_date } for e in exclusions ] @router.post("/{manager_id}/exclusions") def add_manager_exclusion(manager_id: str, data: ExclusionCreate, db: Session = Depends(get_db), user=Depends(require_manager_or_admin)): """Add parking exclusion for a manager""" manager = db.query(User).filter(User.id == manager_id, User.role == "manager").first() if not manager: raise HTTPException(status_code=404, detail="Manager not found") if not db.query(User).filter(User.id == data.user_id).first(): raise HTTPException(status_code=404, detail="User not found") existing = db.query(ParkingExclusion).filter( ParkingExclusion.manager_id == manager_id, ParkingExclusion.user_id == data.user_id ).first() if existing: raise HTTPException(status_code=400, detail="User already has a parking exclusion") exclusion = ParkingExclusion( id=generate_uuid(), manager_id=manager_id, user_id=data.user_id, start_date=data.start_date, end_date=data.end_date, created_at=datetime.utcnow().isoformat() ) db.add(exclusion) db.commit() return {"id": exclusion.id, "message": "Exclusion added"} @router.delete("/{manager_id}/exclusions/{exclusion_id}") def remove_manager_exclusion(manager_id: str, exclusion_id: str, db: Session = Depends(get_db), user=Depends(require_manager_or_admin)): """Remove parking exclusion for a manager""" exclusion = db.query(ParkingExclusion).filter( ParkingExclusion.id == exclusion_id, ParkingExclusion.manager_id == manager_id ).first() if not exclusion: raise HTTPException(status_code=404, detail="Exclusion not found") db.delete(exclusion) db.commit() return {"message": "Exclusion removed"}