""" Projects Router Provides API endpoints for the Projects system: - Project CRUD operations - Project dashboards - Project statistics - Type-aware features """ from fastapi import APIRouter, Request, Depends, HTTPException, Query from fastapi.templating import Jinja2Templates from fastapi.responses import HTMLResponse, JSONResponse from sqlalchemy.orm import Session from sqlalchemy import func, and_ from datetime import datetime, timedelta from typing import Optional import uuid import json from backend.database import get_db from backend.models import ( Project, ProjectType, MonitoringLocation, UnitAssignment, RecordingSession, ScheduledAction, RosterUnit, ) router = APIRouter(prefix="/api/projects", tags=["projects"]) templates = Jinja2Templates(directory="templates") # ============================================================================ # Project List & Overview # ============================================================================ @router.get("/list", response_class=HTMLResponse) async def get_projects_list( request: Request, db: Session = Depends(get_db), status: Optional[str] = Query(None), project_type_id: Optional[str] = Query(None), view: Optional[str] = Query(None), ): """ Get list of all projects. Returns HTML partial with project cards. """ query = db.query(Project) # Filter by status if provided if status: query = query.filter(Project.status == status) # Filter by project type if provided if project_type_id: query = query.filter(Project.project_type_id == project_type_id) projects = query.order_by(Project.created_at.desc()).all() # Enrich each project with stats projects_data = [] for project in projects: # Get project type project_type = db.query(ProjectType).filter_by(id=project.project_type_id).first() # Count locations location_count = db.query(func.count(MonitoringLocation.id)).filter_by( project_id=project.id ).scalar() # Count assigned units unit_count = db.query(func.count(UnitAssignment.id)).filter( and_( UnitAssignment.project_id == project.id, UnitAssignment.status == "active", ) ).scalar() # Count active sessions active_session_count = db.query(func.count(RecordingSession.id)).filter( and_( RecordingSession.project_id == project.id, RecordingSession.status == "recording", ) ).scalar() projects_data.append({ "project": project, "project_type": project_type, "location_count": location_count, "unit_count": unit_count, "active_session_count": active_session_count, }) template_name = "partials/projects/project_list.html" if view == "compact": template_name = "partials/projects/project_list_compact.html" return templates.TemplateResponse(template_name, { "request": request, "projects": projects_data, }) @router.get("/stats", response_class=HTMLResponse) async def get_projects_stats(request: Request, db: Session = Depends(get_db)): """ Get summary statistics for projects overview. Returns HTML partial with stat cards. """ # Count projects by status total_projects = db.query(func.count(Project.id)).scalar() active_projects = db.query(func.count(Project.id)).filter_by(status="active").scalar() completed_projects = db.query(func.count(Project.id)).filter_by(status="completed").scalar() # Count total locations across all projects total_locations = db.query(func.count(MonitoringLocation.id)).scalar() # Count assigned units assigned_units = db.query(func.count(UnitAssignment.id)).filter_by( status="active" ).scalar() # Count active recording sessions active_sessions = db.query(func.count(RecordingSession.id)).filter_by( status="recording" ).scalar() return templates.TemplateResponse("partials/projects/project_stats.html", { "request": request, "total_projects": total_projects, "active_projects": active_projects, "completed_projects": completed_projects, "total_locations": total_locations, "assigned_units": assigned_units, "active_sessions": active_sessions, }) # ============================================================================ # Project CRUD # ============================================================================ @router.post("/create") async def create_project(request: Request, db: Session = Depends(get_db)): """ Create a new project. Expects form data with project details. """ form_data = await request.form() project = Project( id=str(uuid.uuid4()), name=form_data.get("name"), description=form_data.get("description"), project_type_id=form_data.get("project_type_id"), status="active", client_name=form_data.get("client_name"), site_address=form_data.get("site_address"), site_coordinates=form_data.get("site_coordinates"), start_date=datetime.fromisoformat(form_data.get("start_date")) if form_data.get("start_date") else None, end_date=datetime.fromisoformat(form_data.get("end_date")) if form_data.get("end_date") else None, ) db.add(project) db.commit() db.refresh(project) return JSONResponse({ "success": True, "project_id": project.id, "message": f"Project '{project.name}' created successfully", }) @router.get("/{project_id}") async def get_project(project_id: str, db: Session = Depends(get_db)): """ Get project details by ID. Returns JSON with full project data. """ project = db.query(Project).filter_by(id=project_id).first() if not project: raise HTTPException(status_code=404, detail="Project not found") project_type = db.query(ProjectType).filter_by(id=project.project_type_id).first() return { "id": project.id, "name": project.name, "description": project.description, "project_type_id": project.project_type_id, "project_type_name": project_type.name if project_type else None, "status": project.status, "client_name": project.client_name, "site_address": project.site_address, "site_coordinates": project.site_coordinates, "start_date": project.start_date.isoformat() if project.start_date else None, "end_date": project.end_date.isoformat() if project.end_date else None, "created_at": project.created_at.isoformat(), "updated_at": project.updated_at.isoformat(), } @router.put("/{project_id}") async def update_project( project_id: str, request: Request, db: Session = Depends(get_db), ): """ Update project details. Expects JSON body with fields to update. """ project = db.query(Project).filter_by(id=project_id).first() if not project: raise HTTPException(status_code=404, detail="Project not found") data = await request.json() # Update fields if provided if "name" in data: project.name = data["name"] if "description" in data: project.description = data["description"] if "status" in data: project.status = data["status"] if "client_name" in data: project.client_name = data["client_name"] if "site_address" in data: project.site_address = data["site_address"] if "site_coordinates" in data: project.site_coordinates = data["site_coordinates"] if "start_date" in data: project.start_date = datetime.fromisoformat(data["start_date"]) if data["start_date"] else None if "end_date" in data: project.end_date = datetime.fromisoformat(data["end_date"]) if data["end_date"] else None project.updated_at = datetime.utcnow() db.commit() return {"success": True, "message": "Project updated successfully"} @router.delete("/{project_id}") async def delete_project(project_id: str, db: Session = Depends(get_db)): """ Delete a project (soft delete by archiving). """ project = db.query(Project).filter_by(id=project_id).first() if not project: raise HTTPException(status_code=404, detail="Project not found") project.status = "archived" project.updated_at = datetime.utcnow() db.commit() return {"success": True, "message": "Project archived successfully"} # ============================================================================ # Project Dashboard Data # ============================================================================ @router.get("/{project_id}/dashboard", response_class=HTMLResponse) async def get_project_dashboard( project_id: str, request: Request, db: Session = Depends(get_db), ): """ Get project dashboard data. Returns HTML partial with project summary. """ project = db.query(Project).filter_by(id=project_id).first() if not project: raise HTTPException(status_code=404, detail="Project not found") project_type = db.query(ProjectType).filter_by(id=project.project_type_id).first() # Get locations locations = db.query(MonitoringLocation).filter_by(project_id=project_id).all() # Get assigned units with details assignments = db.query(UnitAssignment).filter( and_( UnitAssignment.project_id == project_id, UnitAssignment.status == "active", ) ).all() assigned_units = [] for assignment in assignments: unit = db.query(RosterUnit).filter_by(id=assignment.unit_id).first() if unit: assigned_units.append({ "assignment": assignment, "unit": unit, }) # Get active recording sessions active_sessions = db.query(RecordingSession).filter( and_( RecordingSession.project_id == project_id, RecordingSession.status == "recording", ) ).all() # Get completed sessions count completed_sessions_count = db.query(func.count(RecordingSession.id)).filter( and_( RecordingSession.project_id == project_id, RecordingSession.status == "completed", ) ).scalar() # Get upcoming scheduled actions upcoming_actions = db.query(ScheduledAction).filter( and_( ScheduledAction.project_id == project_id, ScheduledAction.execution_status == "pending", ScheduledAction.scheduled_time > datetime.utcnow(), ) ).order_by(ScheduledAction.scheduled_time).limit(5).all() return templates.TemplateResponse("partials/projects/project_dashboard.html", { "request": request, "project": project, "project_type": project_type, "locations": locations, "assigned_units": assigned_units, "active_sessions": active_sessions, "completed_sessions_count": completed_sessions_count, "upcoming_actions": upcoming_actions, }) # ============================================================================ # Project Types # ============================================================================ @router.get("/types/list", response_class=HTMLResponse) async def get_project_types(request: Request, db: Session = Depends(get_db)): """ Get all available project types. Returns HTML partial with project type cards. """ project_types = db.query(ProjectType).all() return templates.TemplateResponse("partials/projects/project_type_cards.html", { "request": request, "project_types": project_types, })