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
198 lines
6.8 KiB
Python
198 lines
6.8 KiB
Python
"""
|
|
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
|