Files
org-parking/app/routes/managers.py
Stefano Manfredi c74a0ed350 Initial commit: Parking Manager
Features:
- Manager-centric parking spot management
- Fair assignment algorithm (parking/presence ratio)
- Presence tracking calendar
- Closing days (specific & weekly recurring)
- Guarantees and exclusions
- Authelia/LLDAP integration for SSO

Stack:
- FastAPI backend
- SQLite database
- Vanilla JS frontend
- Docker deployment
2025-11-26 23:37:50 +00:00

373 lines
14 KiB
Python

"""
Manager Rules Routes
Manager settings, closing days, guarantees, and exclusions
Key concept: Managers own parking spots and set rules for all their managed offices.
Rules are set at manager level, not office level.
"""
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.orm import Session
import uuid
from database.connection import get_db
from database.models import (
Office, User, OfficeMembership,
ManagerClosingDay, ManagerWeeklyClosingDay,
ParkingGuarantee, ParkingExclusion
)
from utils.auth_middleware import require_admin, require_manager_or_admin, get_current_user
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 offices and parking quota"""
managers = db.query(User).filter(User.role == "manager").all()
result = []
for manager in managers:
memberships = db.query(OfficeMembership).filter(OfficeMembership.user_id == manager.id).all()
office_ids = [m.office_id for m in memberships]
offices = db.query(Office).filter(Office.id.in_(office_ids)).all() if office_ids else []
result.append({
"id": manager.id,
"name": manager.name,
"email": manager.email,
"parking_quota": manager.manager_parking_quota or 0,
"spot_prefix": manager.manager_spot_prefix,
"offices": [{"id": o.id, "name": o.name} for o in offices]
})
return result
@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 offices 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")
memberships = db.query(OfficeMembership).filter(OfficeMembership.user_id == manager_id).all()
office_ids = [m.office_id for m in memberships]
offices = db.query(Office).filter(Office.id.in_(office_ids)).all() if office_ids else []
return {
"id": manager.id,
"name": manager.name,
"email": manager.email,
"parking_quota": manager.manager_parking_quota or 0,
"spot_prefix": manager.manager_spot_prefix,
"offices": [{"id": o.id, "name": o.name} for o in offices]
}
@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 from offices managed by this 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")
memberships = db.query(OfficeMembership).filter(OfficeMembership.user_id == manager_id).all()
managed_office_ids = [m.office_id for m in memberships]
if not managed_office_ids:
return []
users = db.query(User).filter(User.office_id.in_(managed_office_ids)).all()
return [{"id": u.id, "name": u.name, "email": u.email, "role": u.role, "office_id": u.office_id} 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=str(uuid.uuid4()),
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=str(uuid.uuid4()),
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()
result = []
for g in guarantees:
target_user = db.query(User).filter(User.id == g.user_id).first()
result.append({
"id": g.id,
"user_id": g.user_id,
"user_name": target_user.name if target_user else None,
"start_date": g.start_date,
"end_date": g.end_date
})
return result
@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=str(uuid.uuid4()),
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()
result = []
for e in exclusions:
target_user = db.query(User).filter(User.id == e.user_id).first()
result.append({
"id": e.id,
"user_id": e.user_id,
"user_name": target_user.name if target_user else None,
"start_date": e.start_date,
"end_date": e.end_date
})
return result
@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=str(uuid.uuid4()),
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"}