""" 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 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 DailyParkingAssignment, User, OfficeMembership 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.notifications import queue_parking_change_notification router = APIRouter(prefix="/api/parking", tags=["parking"]) # Request/Response Models class InitPoolRequest(BaseModel): date: str # YYYY-MM-DD class ManualAssignRequest(BaseModel): manager_id: str user_id: str spot_id: str date: str class ReassignSpotRequest(BaseModel): assignment_id: str new_user_id: str | None # None = release spot class AssignmentResponse(BaseModel): id: str date: str spot_id: str spot_display_name: str | None = None user_id: str | None manager_id: str user_name: str | None = None user_email: str | None = None user_office_id: str | None = None # 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") quota = current_user.manager_parking_quota or 0 if quota == 0: return {"success": True, "message": "No parking quota configured", "spots": 0} spots = initialize_parking_pool(current_user.id, quota, request.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") query = db.query(DailyParkingAssignment).filter(DailyParkingAssignment.date == date) if manager_id: query = query.filter(DailyParkingAssignment.manager_id == manager_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) result = AssignmentResponse( id=assignment.id, date=assignment.date, spot_id=assignment.spot_id, spot_display_name=spot_display_name, user_id=assignment.user_id, manager_id=assignment.manager_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 result.user_office_id = user.office_id results.append(result) return results @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)): """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.manager_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, manager_id=assignment.manager_id, user_name=current_user.name, user_email=current_user.email )) return results @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""" # 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") # 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") # Check if spot exists and is free spot = db.query(DailyParkingAssignment).filter( DailyParkingAssignment.manager_id == data.manager_id, DailyParkingAssignment.date == data.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 == data.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.manager_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.manager_id, db) assignment.user_id = None db.commit() # Queue notification (self-release, so just confirmation) queue_parking_change_notification( current_user, assignment.date, "released", spot_display_name, db=db ) 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 == 'admin' is_spot_owner = assignment.user_id == current_user.id is_manager = current_user.id == assignment.manager_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.manager_id, db) if 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 # Queue notifications # Notify old user that spot was reassigned if old_user and old_user.id != new_user.id: queue_parking_change_notification( old_user, assignment.date, "reassigned", spot_display_name, new_user.name, db ) # Notify new user that spot was assigned queue_parking_change_notification( new_user, assignment.date, "assigned", spot_display_name, db=db ) else: assignment.user_id = None # Notify old user that spot was released if old_user: queue_parking_change_notification( old_user, assignment.date, "released", spot_display_name, db=db ) db.commit() db.refresh(assignment) # Build response spot_display_name = get_spot_display_name(assignment.spot_id, assignment.manager_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, manager_id=assignment.manager_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 in the same manager's offices. """ 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 == 'admin' is_spot_owner = assignment.user_id == current_user.id is_manager = current_user.id == assignment.manager_id if not (is_admin or is_manager or is_spot_owner): raise HTTPException(status_code=403, detail="Not authorized") # Get all users belonging to offices managed by this manager # Get offices managed by this manager managed_office_ids = db.query(OfficeMembership.office_id).filter( OfficeMembership.user_id == assignment.manager_id ).all() managed_office_ids = [o[0] for o in managed_office_ids] # Get users in those offices users = db.query(User).filter( User.office_id.in_(managed_office_ids), 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, "office_id": user.office_id }) return result