Primo commit
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user