""" 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 from database.connection import get_db from database.models import DailyParkingAssignment, User 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 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: 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 # 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 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() # 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 == '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 # 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.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 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 == '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 users in this manager's team (including the manager themselves) users = db.query(User).filter( (User.manager_id == assignment.manager_id) | (User.id == assignment.manager_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