Primo commit

This commit is contained in:
2026-01-13 11:20:12 +01:00
parent ce9e2fdf2a
commit 17453f5d13
51 changed files with 3883 additions and 2508 deletions

View File

@@ -2,21 +2,33 @@
Parking Management Routes
Parking assignments, spot management, and pool initialization
Manager-centric model:
- Managers own parking spots (defined by manager_parking_quota)
- Spots are named with manager's letter prefix (A1, A2, B1, B2...)
- Assignments reference manager_id directly
"""
"""
Parking Management Routes
Parking assignments, spot management, and pool initialization
Manager-centric model:
- Managers own parking spots (defined by manager_parking_quota)
- Spots are named with manager's letter prefix (A1, A2, B1, B2...)
- Assignments reference manager_id directly
"""
from typing import List
from datetime import datetime
from datetime import datetime, date
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.orm import Session
from database.connection import get_db
from database.models import DailyParkingAssignment, User
from database.models import DailyParkingAssignment, User, UserRole, Office
from utils.auth_middleware import get_current_user, require_manager_or_admin
from services.parking import initialize_parking_pool, get_spot_display_name
from services.parking import (
initialize_parking_pool, get_spot_display_name, release_user_spot,
run_batch_allocation, clear_assignments_for_office_date
)
from services.notifications import notify_parking_assigned, notify_parking_released, notify_parking_reassigned
from app import config
@@ -25,14 +37,14 @@ router = APIRouter(prefix="/api/parking", tags=["parking"])
# Request/Response Models
class InitPoolRequest(BaseModel):
date: str # YYYY-MM-DD
date: date
class ManualAssignRequest(BaseModel):
manager_id: str
office_id: str
user_id: str
spot_id: str
date: str
date: date
class ReassignSpotRequest(BaseModel):
@@ -42,50 +54,57 @@ class ReassignSpotRequest(BaseModel):
class AssignmentResponse(BaseModel):
id: str
date: str
date: date
spot_id: str
spot_display_name: str | None = None
user_id: str | None
manager_id: str
office_id: str
user_name: str | None = None
user_email: str | None = None
class RunAllocationRequest(BaseModel):
date: date
office_id: str
class ClearAssignmentsRequest(BaseModel):
date: date
office_id: str
# Routes
@router.post("/init-manager-pool")
def init_manager_pool(request: InitPoolRequest, db: Session = Depends(get_db), current_user=Depends(require_manager_or_admin)):
"""Initialize parking pool for a manager on a given date"""
try:
datetime.strptime(request.date, "%Y-%m-%d")
except ValueError:
raise HTTPException(status_code=400, detail="Invalid date format")
@router.post("/init-office-pool")
def init_office_pool(request: InitPoolRequest, db: Session = Depends(get_db), current_user=Depends(require_manager_or_admin)):
"""Initialize parking pool for an office on a given date"""
pool_date = request.date
quota = current_user.manager_parking_quota or 0
if quota == 0:
return {"success": True, "message": "No parking quota configured", "spots": 0}
if not current_user.office_id:
raise HTTPException(status_code=400, detail="User does not belong to an office")
office = db.query(Office).filter(Office.id == current_user.office_id).first()
if not office or not office.parking_quota:
return {"success": True, "message": "No parking quota configured", "spots": 0}
spots = initialize_parking_pool(current_user.id, quota, request.date, db)
spots = initialize_parking_pool(office.id, office.parking_quota, pool_date, db)
return {"success": True, "spots": spots}
@router.get("/assignments/{date}", response_model=List[AssignmentResponse])
def get_assignments(date: str, manager_id: str = None, db: Session = Depends(get_db), current_user=Depends(get_current_user)):
"""Get parking assignments for a date, optionally filtered by manager"""
try:
datetime.strptime(date, "%Y-%m-%d")
except ValueError:
raise HTTPException(status_code=400, detail="Invalid date format")
@router.get("/assignments/{date_val}", response_model=List[AssignmentResponse])
def get_assignments(date_val: date, office_id: str = None, db: Session = Depends(get_db), current_user=Depends(get_current_user)):
"""Get parking assignments for a date, optionally filtered by office"""
query_date = date_val
query = db.query(DailyParkingAssignment).filter(DailyParkingAssignment.date == date)
if manager_id:
query = query.filter(DailyParkingAssignment.manager_id == manager_id)
query = db.query(DailyParkingAssignment).filter(DailyParkingAssignment.date == query_date)
if office_id:
query = query.filter(DailyParkingAssignment.office_id == office_id)
assignments = query.all()
results = []
for assignment in assignments:
# Get display name using manager's spot prefix
spot_display_name = get_spot_display_name(assignment.spot_id, assignment.manager_id, db)
# Get display name using office's spot prefix
spot_display_name = get_spot_display_name(assignment.spot_id, assignment.office_id, db)
result = AssignmentResponse(
id=assignment.id,
@@ -93,7 +112,7 @@ def get_assignments(date: str, manager_id: str = None, db: Session = Depends(get
spot_id=assignment.spot_id,
spot_display_name=spot_display_name,
user_id=assignment.user_id,
manager_id=assignment.manager_id
office_id=assignment.office_id
)
if assignment.user_id:
@@ -108,7 +127,7 @@ def get_assignments(date: str, manager_id: str = None, db: Session = Depends(get
@router.get("/my-assignments", response_model=List[AssignmentResponse])
def get_my_assignments(start_date: str = None, end_date: str = None, db: Session = Depends(get_db), current_user=Depends(get_current_user)):
def get_my_assignments(start_date: date = None, end_date: date = None, db: Session = Depends(get_db), current_user=Depends(get_current_user)):
"""Get current user's parking assignments"""
query = db.query(DailyParkingAssignment).filter(
DailyParkingAssignment.user_id == current_user.id
@@ -123,7 +142,7 @@ def get_my_assignments(start_date: str = None, end_date: str = None, db: Session
results = []
for assignment in assignments:
spot_display_name = get_spot_display_name(assignment.spot_id, assignment.manager_id, db)
spot_display_name = get_spot_display_name(assignment.spot_id, assignment.office_id, db)
results.append(AssignmentResponse(
id=assignment.id,
@@ -131,7 +150,7 @@ def get_my_assignments(start_date: str = None, end_date: str = None, db: Session
spot_id=assignment.spot_id,
spot_display_name=spot_display_name,
user_id=assignment.user_id,
manager_id=assignment.manager_id,
office_id=assignment.office_id,
user_name=current_user.name,
user_email=current_user.email
))
@@ -139,27 +158,55 @@ def get_my_assignments(start_date: str = None, end_date: str = None, db: Session
return results
return results
@router.post("/run-allocation")
def run_fair_allocation(data: RunAllocationRequest, db: Session = Depends(get_db), current_user=Depends(require_manager_or_admin)):
"""Manually trigger fair allocation for a date (Test Tool)"""
# Verify office access
if current_user.role == UserRole.MANAGER and current_user.office_id != data.office_id:
raise HTTPException(status_code=403, detail="Not authorized for this office")
result = run_batch_allocation(data.office_id, data.date, db)
return {"message": "Allocation completed", "result": result}
@router.post("/clear-assignments")
def clear_assignments(data: ClearAssignmentsRequest, db: Session = Depends(get_db), current_user=Depends(require_manager_or_admin)):
"""Clear all assignments for a date (Test Tool)"""
# Verify office access
if current_user.role == UserRole.MANAGER and current_user.office_id != data.office_id:
raise HTTPException(status_code=403, detail="Not authorized for this office")
count = clear_assignments_for_office_date(data.office_id, data.date, db)
return {"message": "Assignments cleared", "count": count}
@router.post("/manual-assign")
def manual_assign(data: ManualAssignRequest, db: Session = Depends(get_db), current_user=Depends(require_manager_or_admin)):
"""Manually assign a spot to a user"""
assign_date = data.date
# Verify user exists
user = db.query(User).filter(User.id == data.user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Verify manager exists and check permission
manager = db.query(User).filter(User.id == data.manager_id, User.role == "manager").first()
if not manager:
raise HTTPException(status_code=404, detail="Manager not found")
# Verify office exists
office = db.query(Office).filter(Office.id == data.office_id).first()
if not office:
raise HTTPException(status_code=404, detail="Office not found")
# Only admin or the manager themselves can assign spots
if current_user.role != "admin" and current_user.id != data.manager_id:
raise HTTPException(status_code=403, detail="Not authorized to assign spots for this manager")
# Only admin or the manager of that office can assign spots
is_manager = (current_user.role == UserRole.MANAGER and current_user.office_id == data.office_id)
if current_user.role != UserRole.ADMIN and not is_manager:
raise HTTPException(status_code=403, detail="Not authorized to assign spots for this office")
# Check if spot exists and is free
spot = db.query(DailyParkingAssignment).filter(
DailyParkingAssignment.manager_id == data.manager_id,
DailyParkingAssignment.date == data.date,
DailyParkingAssignment.office_id == data.office_id,
DailyParkingAssignment.date == assign_date,
DailyParkingAssignment.spot_id == data.spot_id
).first()
@@ -170,7 +217,7 @@ def manual_assign(data: ManualAssignRequest, db: Session = Depends(get_db), curr
# Check if user already has a spot for this date (from any manager)
existing = db.query(DailyParkingAssignment).filter(
DailyParkingAssignment.date == data.date,
DailyParkingAssignment.date == assign_date,
DailyParkingAssignment.user_id == data.user_id
).first()
@@ -180,7 +227,7 @@ def manual_assign(data: ManualAssignRequest, db: Session = Depends(get_db), curr
spot.user_id = data.user_id
db.commit()
spot_display_name = get_spot_display_name(data.spot_id, data.manager_id, db)
spot_display_name = get_spot_display_name(data.spot_id, data.office_id, db)
return {"message": "Spot assigned", "spot_id": data.spot_id, "spot_display_name": spot_display_name}
@@ -198,7 +245,7 @@ def release_my_spot(assignment_id: str, db: Session = Depends(get_db), current_u
raise HTTPException(status_code=403, detail="You can only release your own parking spot")
# Get spot display name for notification
spot_display_name = get_spot_display_name(assignment.spot_id, assignment.manager_id, db)
spot_display_name = get_spot_display_name(assignment.spot_id, assignment.office_id, db)
assignment.user_id = None
db.commit()
@@ -223,9 +270,9 @@ def reassign_spot(data: ReassignSpotRequest, db: Session = Depends(get_db), curr
raise HTTPException(status_code=404, detail="Assignment not found")
# Check permission: admin, manager who owns the spot, or current spot holder
is_admin = current_user.role == 'admin'
is_admin = current_user.role == UserRole.ADMIN
is_spot_owner = assignment.user_id == current_user.id
is_manager = current_user.id == assignment.manager_id
is_manager = (current_user.role == UserRole.MANAGER and current_user.office_id == assignment.office_id)
if not (is_admin or is_manager or is_spot_owner):
raise HTTPException(status_code=403, detail="Not authorized to reassign this spot")
@@ -235,9 +282,17 @@ def reassign_spot(data: ReassignSpotRequest, db: Session = Depends(get_db), curr
old_user = db.query(User).filter(User.id == old_user_id).first() if old_user_id else None
# Get spot display name for notifications
spot_display_name = get_spot_display_name(assignment.spot_id, assignment.manager_id, db)
spot_display_name = get_spot_display_name(assignment.spot_id, assignment.office_id, db)
if data.new_user_id:
if data.new_user_id == "auto":
# "Auto assign" means releasing the spot so the system picks the next person
# release_user_spot returns True if it released it (and potentially reassigned it)
success = release_user_spot(assignment.office_id, assignment.user_id, assignment.date, db)
if not success:
raise HTTPException(status_code=400, detail="Could not auto-reassign spot")
return {"message": "Spot released for auto-assignment"}
elif data.new_user_id:
# Check new user exists
new_user = db.query(User).filter(User.id == data.new_user_id).first()
if not new_user:
@@ -275,7 +330,7 @@ def reassign_spot(data: ReassignSpotRequest, db: Session = Depends(get_db), curr
db.refresh(assignment)
# Build response
spot_display_name = get_spot_display_name(assignment.spot_id, assignment.manager_id, db)
spot_display_name = get_spot_display_name(assignment.spot_id, assignment.office_id, db)
result = AssignmentResponse(
id=assignment.id,
@@ -283,7 +338,7 @@ def reassign_spot(data: ReassignSpotRequest, db: Session = Depends(get_db), curr
spot_id=assignment.spot_id,
spot_display_name=spot_display_name,
user_id=assignment.user_id,
manager_id=assignment.manager_id
office_id=assignment.office_id
)
if assignment.user_id:
@@ -308,16 +363,16 @@ def get_eligible_users(assignment_id: str, db: Session = Depends(get_db), curren
raise HTTPException(status_code=404, detail="Assignment not found")
# Check permission: admin, manager who owns the spot, or current spot holder
is_admin = current_user.role == 'admin'
is_admin = current_user.role == UserRole.ADMIN
is_spot_owner = assignment.user_id == current_user.id
is_manager = current_user.id == assignment.manager_id
is_manager = (current_user.role == UserRole.MANAGER and current_user.office_id == assignment.office_id)
if not (is_admin or is_manager or is_spot_owner):
raise HTTPException(status_code=403, detail="Not authorized")
# Get users in this manager's team (including the manager themselves)
# Get users in this office (including the manager themselves)
users = db.query(User).filter(
(User.manager_id == assignment.manager_id) | (User.id == assignment.manager_id),
User.office_id == assignment.office_id,
User.id != assignment.user_id # Exclude current holder
).all()