226 lines
8.7 KiB
Python
226 lines
8.7 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
|
|
|
|
from app.core.database import get_db
|
|
from app.core.decisions import prochain_numero_decision
|
|
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 "proposition",
|
|
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,
|
|
date_adoption=p.date_adoption,
|
|
etant_donne_que=p.etant_donne_que,
|
|
groupe_a_decide_de=p.groupe_a_decide_de,
|
|
note=p.note,
|
|
candidature_id=p.candidature_id,
|
|
cree_le=p.cree_le,
|
|
date_reunion=reunion.date_reunion if reunion else None,
|
|
)
|
|
|
|
|
|
@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="proposition",
|
|
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 req.statut == "adoptee" and prop.statut != "secondee":
|
|
raise HTTPException(status_code=400, detail="Seule une proposition secondée peut être transformée en résolution")
|
|
if req.statut == "rejetee" and prop.statut not in ("proposee", "secondee"):
|
|
raise HTTPException(status_code=400, detail="Seule une proposition en cours peut être rejetée")
|
|
|
|
# Attribuer un numéro de résolution si adoptée
|
|
if req.statut == "adoptee":
|
|
etant_donne_que = (req.etant_donne_que or "").strip()
|
|
groupe_a_decide_de = (req.groupe_a_decide_de or "").strip()
|
|
if not etant_donne_que or not groupe_a_decide_de:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail='Les sections "ÉTANT DONNÉ QUE" et "LE GROUPE A DÉCIDÉ DE" sont obligatoires',
|
|
)
|
|
|
|
prop.etant_donne_que = etant_donne_que
|
|
prop.groupe_a_decide_de = groupe_a_decide_de
|
|
prop.note = None
|
|
if prop.type_proposition not in ("nomination", "contribution"):
|
|
prop.type_proposition = "resolution"
|
|
prop.numero_resolution = await prochain_numero_decision(
|
|
executif.groupe_id, prop.type_proposition, 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":
|
|
if req.note:
|
|
prop.note = req.note.strip()
|
|
|
|
# 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"
|
|
|
|
prop.statut = req.statut
|
|
prop.date_vote = date.today()
|
|
if req.statut == "adoptee":
|
|
prop.date_adoption = prop.date_vote
|
|
|
|
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.date_vote, Proposition.cree_le, Proposition.numero_resolution)
|
|
|
|
result = await db.execute(query)
|
|
return [await _to_out(p, db) for p in result.scalars().all()]
|