Files
Org-Parking/app/routes/parking.py
2026-01-13 11:20:12 +01:00

397 lines
15 KiB
Python

"""
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, 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, 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, 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
router = APIRouter(prefix="/api/parking", tags=["parking"])
# Request/Response Models
class InitPoolRequest(BaseModel):
date: date
class ManualAssignRequest(BaseModel):
office_id: str
user_id: str
spot_id: str
date: date
class ReassignSpotRequest(BaseModel):
assignment_id: str
new_user_id: str | None # None = release spot
class AssignmentResponse(BaseModel):
id: str
date: date
spot_id: str
spot_display_name: str | None = None
user_id: str | None
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-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
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(office.id, office.parking_quota, pool_date, db)
return {"success": True, "spots": spots}
@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 == 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 office's spot prefix
spot_display_name = get_spot_display_name(assignment.spot_id, assignment.office_id, db)
result = AssignmentResponse(
id=assignment.id,
date=assignment.date,
spot_id=assignment.spot_id,
spot_display_name=spot_display_name,
user_id=assignment.user_id,
office_id=assignment.office_id
)
if assignment.user_id:
user = db.query(User).filter(User.id == assignment.user_id).first()
if user:
result.user_name = user.name
result.user_email = user.email
results.append(result)
return results
@router.get("/my-assignments", response_model=List[AssignmentResponse])
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
)
if start_date:
query = query.filter(DailyParkingAssignment.date >= start_date)
if end_date:
query = query.filter(DailyParkingAssignment.date <= end_date)
assignments = query.order_by(DailyParkingAssignment.date.desc()).all()
results = []
for assignment in assignments:
spot_display_name = get_spot_display_name(assignment.spot_id, assignment.office_id, db)
results.append(AssignmentResponse(
id=assignment.id,
date=assignment.date,
spot_id=assignment.spot_id,
spot_display_name=spot_display_name,
user_id=assignment.user_id,
office_id=assignment.office_id,
user_name=current_user.name,
user_email=current_user.email
))
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 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 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.office_id == data.office_id,
DailyParkingAssignment.date == assign_date,
DailyParkingAssignment.spot_id == data.spot_id
).first()
if not spot:
raise HTTPException(status_code=404, detail="Spot not found")
if spot.user_id:
raise HTTPException(status_code=400, detail="Spot already assigned")
# Check if user already has a spot for this date (from any manager)
existing = db.query(DailyParkingAssignment).filter(
DailyParkingAssignment.date == assign_date,
DailyParkingAssignment.user_id == data.user_id
).first()
if existing:
raise HTTPException(status_code=400, detail="User already has a spot for this date")
spot.user_id = data.user_id
db.commit()
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}
@router.post("/release-my-spot/{assignment_id}")
def release_my_spot(assignment_id: str, db: Session = Depends(get_db), current_user=Depends(get_current_user)):
"""Release a parking spot assigned to the current user"""
assignment = db.query(DailyParkingAssignment).filter(
DailyParkingAssignment.id == assignment_id
).first()
if not assignment:
raise HTTPException(status_code=404, detail="Assignment not found")
if assignment.user_id != current_user.id:
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.office_id, db)
assignment.user_id = None
db.commit()
# Send notification (self-release, so just confirmation)
notify_parking_released(current_user, assignment.date, spot_display_name)
config.logger.info(f"User {current_user.email} released parking spot {spot_display_name} on {assignment.date}")
return {"message": "Parking spot released"}
@router.post("/reassign-spot")
def reassign_spot(data: ReassignSpotRequest, db: Session = Depends(get_db), current_user=Depends(get_current_user)):
"""Reassign a spot to another user or release it.
Allowed by: spot owner, their manager, or admin.
"""
assignment = db.query(DailyParkingAssignment).filter(
DailyParkingAssignment.id == data.assignment_id
).first()
if not assignment:
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 == UserRole.ADMIN
is_spot_owner = assignment.user_id == current_user.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")
# Store old user for notification
old_user_id = assignment.user_id
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.office_id, db)
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:
raise HTTPException(status_code=404, detail="User not found")
# Check user doesn't already have a spot for this date
existing = db.query(DailyParkingAssignment).filter(
DailyParkingAssignment.user_id == data.new_user_id,
DailyParkingAssignment.date == assignment.date,
DailyParkingAssignment.id != assignment.id
).first()
if existing:
raise HTTPException(status_code=400, detail="User already has a spot for this date")
assignment.user_id = data.new_user_id
# Send notifications
# Notify old user that spot was reassigned
if old_user and old_user.id != new_user.id:
notify_parking_reassigned(old_user, assignment.date, spot_display_name, new_user.name)
# Notify new user that spot was assigned
notify_parking_assigned(new_user, assignment.date, spot_display_name)
config.logger.info(f"Parking spot {spot_display_name} on {assignment.date} reassigned from {old_user.email if old_user else 'unassigned'} to {new_user.email}")
else:
assignment.user_id = None
# Notify old user that spot was released
if old_user:
notify_parking_released(old_user, assignment.date, spot_display_name)
config.logger.info(f"Parking spot {spot_display_name} on {assignment.date} released by {old_user.email if old_user else 'unknown'}")
db.commit()
db.refresh(assignment)
# Build response
spot_display_name = get_spot_display_name(assignment.spot_id, assignment.office_id, db)
result = AssignmentResponse(
id=assignment.id,
date=assignment.date,
spot_id=assignment.spot_id,
spot_display_name=spot_display_name,
user_id=assignment.user_id,
office_id=assignment.office_id
)
if assignment.user_id:
user = db.query(User).filter(User.id == assignment.user_id).first()
if user:
result.user_name = user.name
result.user_email = user.email
return result
@router.get("/eligible-users/{assignment_id}")
def get_eligible_users(assignment_id: str, db: Session = Depends(get_db), current_user=Depends(get_current_user)):
"""Get users eligible for reassignment of a parking spot.
Returns users managed by the same manager.
"""
assignment = db.query(DailyParkingAssignment).filter(
DailyParkingAssignment.id == assignment_id
).first()
if not assignment:
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 == UserRole.ADMIN
is_spot_owner = assignment.user_id == current_user.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 office (including the manager themselves)
users = db.query(User).filter(
User.office_id == assignment.office_id,
User.id != assignment.user_id # Exclude current holder
).all()
# Filter out users who already have a spot for this date
existing_assignments = db.query(DailyParkingAssignment.user_id).filter(
DailyParkingAssignment.date == assignment.date,
DailyParkingAssignment.user_id.isnot(None),
DailyParkingAssignment.id != assignment.id
).all()
users_with_spots = {a[0] for a in existing_assignments}
result = []
for user in users:
if user.id not in users_with_spots:
result.append({
"id": user.id,
"name": user.name,
"email": user.email
})
return result