""" Endpoint v2 per gestione assegnazione istruttori (capo istruttore / manager). Solo ROLE_INSTRUCTOR_MANAGER + ROLE_SUPER_ADMIN. """ from datetime import datetime, timezone from typing import Optional from uuid import UUID from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from sqlalchemy import text from ..db import get_db from ..auth import AuthUser, get_current_user from ..models import RemissionPractice from ..schemas import ApiResponse, PracticeReassignBody router = APIRouter( prefix="/api/remission-practices", tags=["assignment-manager"], ) def _require_manager(user: AuthUser = Depends(get_current_user)) -> AuthUser: if user.role not in ("ROLE_INSTRUCTOR_MANAGER", "ROLE_SUPER_ADMIN"): raise HTTPException( status_code=403, detail="Richiesto ruolo manager o superadmin" ) return user @router.get("/instructor-manager/assignments", response_model=ApiResponse) def assignments_overview( db: Session = Depends(get_db), manager: AuthUser = Depends(_require_manager), ): """Vista capo istruttore: pratiche con suggested + assigned + flag 'da assegnare'.""" # Vista manager: tutte le pratiche (incluso DRAFT in compilazione dal benef e # APPROVED/REJECTED chiuse) perche il capo istruttore deve vedere tutto per # riassegnare, monitorare carico, verificare storici. practices = db.query(RemissionPractice).order_by( RemissionPractice.application_id, RemissionPractice.sequence_number ).all() # Enrichment: nome istruttori + company items = [] user_cache: dict = {} def _user_name(uid: Optional[int]) -> Optional[str]: if uid is None: return None if uid in user_cache: return user_cache[uid] row = db.execute(text(""" SELECT first_name || ' ' || last_name as name, email FROM gepafin_schema.gepafin_user WHERE id = :uid """), {"uid": uid}).mappings().first() name = (row["name"] if row else None) or (row["email"] if row else None) user_cache[uid] = name return name for p in practices: company_row = db.execute(text(""" SELECT company_name, vat_number FROM gepafin_schema.company WHERE id = :cid """), {"cid": p.company_id}).mappings().first() call_row = db.execute(text(""" SELECT name FROM gepafin_schema.call WHERE id = :cid """), {"cid": p.call_id}).mappings().first() items.append({ "id": str(p.id), "application_id": p.application_id, "sequence_number": p.sequence_number, "period_label": p.period_label, "call_id": p.call_id, "call_name": call_row["name"] if call_row else None, "company_id": p.company_id, "company_name": company_row["company_name"] if company_row else None, "status": p.status, "submitted_at": p.submitted_at.isoformat() if p.submitted_at else None, "amount_erogato": float(p.amount_erogato or 0), "suggested_instructor_id": p.suggested_instructor_id, "suggested_instructor_name": _user_name(p.suggested_instructor_id), "assigned_instructor_id": p.assigned_instructor_id, "assigned_instructor_name": _user_name(p.assigned_instructor_id), "is_unassigned": p.assigned_instructor_id is None, }) return ApiResponse(data={"assignments": items}) @router.get("/instructor-manager/instructors", response_model=ApiResponse) def list_available_instructors( db: Session = Depends(get_db), manager: AuthUser = Depends(_require_manager), ): """Elenco istruttori disponibili per riassegnazione (pre_instructor + manager ACTIVE).""" rows = db.execute(text(""" SELECT u.id, u.email, u.first_name, u.last_name, r.role_type FROM gepafin_schema.gepafin_user u JOIN gepafin_schema.role r ON r.id = u.role_id WHERE u.is_deleted = false AND r.role_type IN ('ROLE_PRE_INSTRUCTOR', 'ROLE_INSTRUCTOR_MANAGER') ORDER BY u.last_name, u.first_name """)).mappings().all() return ApiResponse(data={"instructors": [ { "user_id": r["id"], "email": r["email"], "first_name": r["first_name"], "last_name": r["last_name"], "role_type": r["role_type"], "display_name": f"{r['first_name'] or ''} {r['last_name'] or ''}".strip() or r["email"], } for r in rows ]}) @router.post("/instructor/{practice_id}/reassign", response_model=ApiResponse) def reassign_instructor( practice_id: UUID, body: PracticeReassignBody, db: Session = Depends(get_db), manager: AuthUser = Depends(_require_manager), ): """Manager assegna/riassegna la pratica a un istruttore diverso (o unassign se new_instructor_id=None). Scrive audit entry in instructor_checklist.reassignment_log. """ p = db.query(RemissionPractice).filter(RemissionPractice.id == practice_id).first() if not p: raise HTTPException(status_code=404, detail="Pratica non trovata") old_instructor_id = p.assigned_instructor_id # Verifica nuovo istruttore se specificato if body.new_instructor_id is not None: row = db.execute(text(""" SELECT u.id, r.role_type FROM gepafin_schema.gepafin_user u JOIN gepafin_schema.role r ON r.id = u.role_id WHERE u.id = :uid AND u.is_deleted = false """), {"uid": body.new_instructor_id}).mappings().first() if not row: raise HTTPException(status_code=404, detail=f"Istruttore {body.new_instructor_id} non trovato") if row["role_type"] not in ("ROLE_PRE_INSTRUCTOR", "ROLE_INSTRUCTOR_MANAGER"): raise HTTPException(status_code=422, detail="Utente non ha ruolo istruttore") # Audit log checklist = dict(p.instructor_checklist or {}) log = list(checklist.get("reassignment_log") or []) log.append({ "at": datetime.now(timezone.utc).isoformat(), "by_user_id": manager.user_id, "by_email": manager.email, "from_instructor_id": old_instructor_id, "to_instructor_id": body.new_instructor_id, "reason": body.reassignment_reason, }) checklist["reassignment_log"] = log p.assigned_instructor_id = body.new_instructor_id p.instructor_checklist = checklist # Se passo da SUBMITTED + assegnato -> UNDER_REVIEW # Altrimenti lascio status invariato (manager puo riassegnare anche durante review) if p.status == "SUBMITTED" and body.new_instructor_id is not None: p.status = "UNDER_REVIEW" db.commit() db.refresh(p) action = "unassigned" if body.new_instructor_id is None else f"assigned to {body.new_instructor_id}" return ApiResponse( message=f"Pratica {action}", data={ "id": str(p.id), "status": p.status, "assigned_instructor_id": p.assigned_instructor_id, "suggested_instructor_id": p.suggested_instructor_id, "reassignment_log": log, } )