"""Routes — Propositions et résolutions (réunions d'affaires).""" from datetime import date, timezone, datetime from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, desc, extract, func from app.core.database import get_db from app.core.security import get_current_membre, require_executif from app.models.proposition import Proposition from app.models.reunion import Reunion from app.models.membre import Membre from app.models.candidature import Candidature from app.models.affectation import Affectation from app.models.poste import Poste from app.schemas.propositions import PropositionCreate, PropositionOut, PropositionVote router = APIRouter(prefix="/propositions", tags=["Propositions et résolutions"]) async def _to_out(p: Proposition, db: AsyncSession) -> PropositionOut: proposeur = await db.get(Membre, p.proposeur_id) secondeur = await db.get(Membre, p.secondeur_id) if p.secondeur_id else None reunion = await db.get(Reunion, p.reunion_id) if p.reunion_id else None return PropositionOut( id=p.id, reunion_id=p.reunion_id, type_proposition=p.type_proposition or "generale", sujet=p.sujet, description=p.description, proposeur_prenom=proposeur.prenom if proposeur else "?", secondeur_prenom=secondeur.prenom if secondeur else None, statut=p.statut, numero_resolution=p.numero_resolution, date_vote=p.date_vote, note=p.note, candidature_id=p.candidature_id, cree_le=p.cree_le, date_reunion=reunion.date_reunion if reunion else None, ) async def _prochain_numero(groupe_id, annee: int, db: AsyncSession) -> str: """Génère le prochain numéro de résolution : RES-2026-001.""" result = await db.execute( select(func.count(Proposition.id)).where( Proposition.groupe_id == groupe_id, Proposition.numero_resolution.isnot(None), Proposition.numero_resolution.like(f"RES-{annee}-%"), ) ) count = result.scalar() or 0 return f"RES-{annee}-{count + 1:03d}" @router.post("/", response_model=PropositionOut) async def creer_proposition( req: PropositionCreate, membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db), ): """Soumettre une proposition — peut être créée en tout temps.""" if req.reunion_id: result = await db.execute( select(Reunion).where( Reunion.id == req.reunion_id, Reunion.groupe_id == membre.groupe_id, ) ) if not result.scalar_one_or_none(): raise HTTPException(status_code=404, detail="Réunion introuvable") prop = Proposition( groupe_id=membre.groupe_id, reunion_id=req.reunion_id, type_proposition=req.type_proposition or "generale", sujet=req.sujet.strip(), description=req.description, proposeur_id=membre.id, statut="proposee", ) db.add(prop) await db.flush() return await _to_out(prop, db) @router.post("/{proposition_id}/seconder", response_model=PropositionOut) async def seconder_proposition( proposition_id: str, membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db), ): """Seconder une proposition pour la rendre recevable.""" result = await db.execute( select(Proposition).where( Proposition.id == proposition_id, Proposition.groupe_id == membre.groupe_id, ) ) prop = result.scalar_one_or_none() if not prop: raise HTTPException(status_code=404, detail="Proposition introuvable") if prop.statut != "proposee": raise HTTPException(status_code=400, detail="Seule une proposition en attente peut être secondée") if prop.proposeur_id == membre.id: raise HTTPException(status_code=400, detail="On ne peut pas seconder sa propre proposition") prop.secondeur_id = membre.id prop.statut = "secondee" await db.flush() return await _to_out(prop, db) @router.patch("/{proposition_id}/voter", response_model=PropositionOut) async def voter_proposition( proposition_id: str, req: PropositionVote, executif=Depends(require_executif), db: AsyncSession = Depends(get_db), ): """Enregistrer le résultat du vote sur une proposition secondée (exécutif).""" if req.statut not in ("adoptee", "rejetee"): raise HTTPException(status_code=400, detail="Le statut doit être 'adoptee' ou 'rejetee'") result = await db.execute( select(Proposition).where( Proposition.id == proposition_id, Proposition.groupe_id == executif.groupe_id, ) ) prop = result.scalar_one_or_none() if not prop: raise HTTPException(status_code=404, detail="Proposition introuvable") if prop.statut != "secondee": raise HTTPException(status_code=400, detail="Seule une proposition secondée peut être mise au vote") prop.statut = req.statut prop.date_vote = date.today() if req.note: prop.note = req.note.strip() # Attribuer un numéro de résolution si adoptée if req.statut == "adoptee": prop.numero_resolution = await _prochain_numero( executif.groupe_id, date.today().year, db ) if req.reunion_id and not prop.reunion_id: prop.reunion_id = req.reunion_id # ── NOMINATION : créer l'affectation automatiquement ── if prop.type_proposition == "nomination" and prop.candidature_id: cand = await db.get(Candidature, prop.candidature_id) if cand and cand.statut == "proposee": cand.statut = "acceptee" cand.proposition_id = prop.id affectation = Affectation( groupe_id=executif.groupe_id, membre_id=cand.membre_id, poste_id=cand.poste_id, date_debut=date.today(), statut="actif", ) db.add(affectation) elif req.statut == "rejetee": # Si nomination rejetée, mettre la candidature en refusée if prop.type_proposition == "nomination" and prop.candidature_id: cand = await db.get(Candidature, prop.candidature_id) if cand: cand.statut = "refusee" await db.flush() return await _to_out(prop, db) @router.get("/", response_model=list[PropositionOut]) async def liste_propositions( statut: str | None = None, reunion_id: str | None = None, type_proposition: str | None = None, limite: int = 50, membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db), ): """Lister les propositions du groupe avec filtres optionnels.""" query = select(Proposition).where(Proposition.groupe_id == membre.groupe_id) if statut: query = query.where(Proposition.statut == statut) if reunion_id: query = query.where(Proposition.reunion_id == reunion_id) if type_proposition: query = query.where(Proposition.type_proposition == type_proposition) query = query.order_by(desc(Proposition.cree_le)).limit(limite) result = await db.execute(query) return [await _to_out(p, db) for p in result.scalars().all()] @router.get("/resolutions", response_model=list[PropositionOut]) async def registre_resolutions( annee: int | None = None, type_proposition: str | None = None, membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db), ): """Registre des résolutions adoptées.""" query = select(Proposition).where( Proposition.groupe_id == membre.groupe_id, Proposition.statut == "adoptee", ) if annee: query = query.where(extract("year", Proposition.cree_le) == annee) if type_proposition: query = query.where(Proposition.type_proposition == type_proposition) query = query.order_by(Proposition.numero_resolution) result = await db.execute(query) return [await _to_out(p, db) for p in result.scalars().all()]