""" 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