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
This commit is contained in:
372
app/routes/managers.py
Normal file
372
app/routes/managers.py
Normal file
@@ -0,0 +1,372 @@
|
||||
"""
|
||||
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"}
|
||||
Reference in New Issue
Block a user