217 lines
8 KiB
Python
217 lines
8 KiB
Python
"""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()]
|