Files
gepafin-rendicontazione-api/app/routers/assignment.py
BFLOWS 345856f55c fix(assignment): vista manager mostra TUTTE le pratiche non solo quelle attive
Il capo istruttore deve vedere anche pratiche in DRAFT (beneficiario le sta
compilando) e APPROVED/REJECTED (chiuse) per: monitorare carico istruttori,
riassegnare tranches successive prima del submit, verificare storici.

Prima il filtro era RemissionPractice.status.in_(SUBMITTED,UNDER_REVIEW,AWAITING_AMENDMENT)
ed escludeva drafts e closed. Ora nessun filtro su status — tutte le pratiche.

Segnalazione Carlo: capo istruttore vedeva 'Nessuna pratica in coda' anche
con 2 tranches NAPOLI SAS in DB.
2026-04-18 19:13:01 +02:00

185 lines
7.1 KiB
Python

"""
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,
}
)