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:
197
app/routes/offices.py
Normal file
197
app/routes/offices.py
Normal file
@@ -0,0 +1,197 @@
|
||||
"""
|
||||
Office Management Routes
|
||||
Admin CRUD for offices and manager-office memberships
|
||||
"""
|
||||
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
|
||||
from utils.auth_middleware import require_admin, require_manager_or_admin, get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/offices", tags=["offices"])
|
||||
|
||||
|
||||
# Request/Response Models
|
||||
class OfficeCreate(BaseModel):
|
||||
name: str
|
||||
location: str | None = None
|
||||
|
||||
|
||||
class OfficeUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
location: str | None = None
|
||||
|
||||
|
||||
class OfficeResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
location: str | None = None
|
||||
created_at: str | None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class AddManagerRequest(BaseModel):
|
||||
user_id: str
|
||||
|
||||
|
||||
# Office CRUD Routes
|
||||
@router.get("")
|
||||
def list_offices(db: Session = Depends(get_db), user=Depends(get_current_user)):
|
||||
"""List all offices with counts"""
|
||||
offices = db.query(Office).all()
|
||||
result = []
|
||||
for office in offices:
|
||||
manager_count = db.query(OfficeMembership).filter(OfficeMembership.office_id == office.id).count()
|
||||
employee_count = db.query(User).filter(User.office_id == office.id).count()
|
||||
result.append({
|
||||
"id": office.id,
|
||||
"name": office.name,
|
||||
"location": office.location,
|
||||
"created_at": office.created_at,
|
||||
"manager_count": manager_count,
|
||||
"employee_count": employee_count
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/{office_id}", response_model=OfficeResponse)
|
||||
def get_office(office_id: str, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
||||
"""Get office by ID"""
|
||||
office = db.query(Office).filter(Office.id == office_id).first()
|
||||
if not office:
|
||||
raise HTTPException(status_code=404, detail="Office not found")
|
||||
return office
|
||||
|
||||
|
||||
@router.post("", response_model=OfficeResponse)
|
||||
def create_office(data: OfficeCreate, db: Session = Depends(get_db), user=Depends(require_admin)):
|
||||
"""Create new office (admin only)"""
|
||||
office = Office(
|
||||
id=str(uuid.uuid4()),
|
||||
name=data.name,
|
||||
location=data.location,
|
||||
created_at=datetime.utcnow().isoformat()
|
||||
)
|
||||
|
||||
db.add(office)
|
||||
db.commit()
|
||||
db.refresh(office)
|
||||
return office
|
||||
|
||||
|
||||
@router.put("/{office_id}", response_model=OfficeResponse)
|
||||
def update_office(office_id: str, data: OfficeUpdate, db: Session = Depends(get_db), user=Depends(require_admin)):
|
||||
"""Update office (admin only)"""
|
||||
office = db.query(Office).filter(Office.id == office_id).first()
|
||||
if not office:
|
||||
raise HTTPException(status_code=404, detail="Office not found")
|
||||
|
||||
if data.name is not None:
|
||||
office.name = data.name
|
||||
if data.location is not None:
|
||||
office.location = data.location
|
||||
|
||||
office.updated_at = datetime.utcnow().isoformat()
|
||||
db.commit()
|
||||
db.refresh(office)
|
||||
return office
|
||||
|
||||
|
||||
@router.delete("/{office_id}")
|
||||
def delete_office(office_id: str, db: Session = Depends(get_db), user=Depends(require_admin)):
|
||||
"""Delete office (admin only)"""
|
||||
office = db.query(Office).filter(Office.id == office_id).first()
|
||||
if not office:
|
||||
raise HTTPException(status_code=404, detail="Office not found")
|
||||
|
||||
if db.query(User).filter(User.office_id == office_id).count() > 0:
|
||||
raise HTTPException(status_code=400, detail="Cannot delete office with assigned users")
|
||||
|
||||
db.delete(office)
|
||||
db.commit()
|
||||
return {"message": "Office deleted"}
|
||||
|
||||
|
||||
# Office membership routes (linking managers to offices)
|
||||
@router.get("/{office_id}/managers")
|
||||
def get_office_managers(office_id: str, db: Session = Depends(get_db), user=Depends(require_manager_or_admin)):
|
||||
"""Get managers for an office"""
|
||||
memberships = db.query(OfficeMembership).filter(OfficeMembership.office_id == office_id).all()
|
||||
manager_ids = [m.user_id for m in memberships]
|
||||
managers = db.query(User).filter(User.id.in_(manager_ids)).all()
|
||||
return [{"id": m.id, "name": m.name, "email": m.email} for m in managers]
|
||||
|
||||
|
||||
@router.post("/{office_id}/managers")
|
||||
def add_office_manager(office_id: str, data: AddManagerRequest, db: Session = Depends(get_db), user=Depends(require_admin)):
|
||||
"""Add manager to office (admin only)"""
|
||||
if not db.query(Office).filter(Office.id == office_id).first():
|
||||
raise HTTPException(status_code=404, detail="Office not found")
|
||||
|
||||
manager = db.query(User).filter(User.id == data.user_id).first()
|
||||
if not manager:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
if manager.role != "manager":
|
||||
raise HTTPException(status_code=400, detail="User must have manager role")
|
||||
|
||||
existing = db.query(OfficeMembership).filter(
|
||||
OfficeMembership.office_id == office_id,
|
||||
OfficeMembership.user_id == data.user_id
|
||||
).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="Manager already assigned to office")
|
||||
|
||||
membership = OfficeMembership(
|
||||
id=str(uuid.uuid4()),
|
||||
office_id=office_id,
|
||||
user_id=data.user_id,
|
||||
created_at=datetime.utcnow().isoformat()
|
||||
)
|
||||
db.add(membership)
|
||||
db.commit()
|
||||
return {"message": "Manager added to office"}
|
||||
|
||||
|
||||
@router.delete("/{office_id}/managers/{manager_id}")
|
||||
def remove_office_manager(office_id: str, manager_id: str, db: Session = Depends(get_db), user=Depends(require_admin)):
|
||||
"""Remove manager from office (admin only)"""
|
||||
membership = db.query(OfficeMembership).filter(
|
||||
OfficeMembership.office_id == office_id,
|
||||
OfficeMembership.user_id == manager_id
|
||||
).first()
|
||||
if not membership:
|
||||
raise HTTPException(status_code=404, detail="Manager not assigned to office")
|
||||
|
||||
db.delete(membership)
|
||||
db.commit()
|
||||
return {"message": "Manager removed from office"}
|
||||
|
||||
|
||||
# Legacy redirect for /api/offices/managers/list -> /api/managers
|
||||
@router.get("/managers/list")
|
||||
def list_managers_legacy(db: Session = Depends(get_db), user=Depends(require_manager_or_admin)):
|
||||
"""Get all managers with their managed offices and parking quota (legacy endpoint)"""
|
||||
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
|
||||
Reference in New Issue
Block a user