2026-05-15 16:41:17 -04:00
|
|
|
"""Routes postes, affectations, candidatures et accès aux modules."""
|
2026-03-04 14:01:00 -05:00
|
|
|
from datetime import date
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
2026-05-15 16:41:17 -04:00
|
|
|
from sqlalchemy import select, func
|
2026-03-04 14:01:00 -05:00
|
|
|
|
|
|
|
|
from app.core.database import get_db
|
|
|
|
|
from app.core.security import get_current_membre, require_executif
|
|
|
|
|
from app.models.poste import Poste
|
|
|
|
|
from app.models.affectation import Affectation
|
2026-04-17 04:04:04 -04:00
|
|
|
from app.models.membre import Membre
|
|
|
|
|
from app.models.candidature import Candidature
|
2026-05-15 16:41:17 -04:00
|
|
|
from app.models.poste_module import PosteModule
|
|
|
|
|
from app.schemas.schemas import PosteOut, PosteCreate, AffectationCreate, TerminerMandatRequest
|
2026-03-04 14:01:00 -05:00
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/postes", tags=["Postes et affectations"])
|
|
|
|
|
|
|
|
|
|
|
2026-05-15 16:41:17 -04:00
|
|
|
async def _prochain_numero_resolution(groupe_id, annee: int, db: AsyncSession) -> str:
|
|
|
|
|
from app.models.proposition import Proposition
|
|
|
|
|
|
|
|
|
|
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}"
|
|
|
|
|
|
|
|
|
|
|
2026-04-17 04:04:04 -04:00
|
|
|
# ── Postes ──
|
|
|
|
|
|
2026-03-04 14:01:00 -05:00
|
|
|
@router.get("/", response_model=list[PosteOut])
|
2026-04-17 04:04:04 -04:00
|
|
|
async def liste_postes(
|
|
|
|
|
inclure_abolis: bool = False,
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
_=Depends(get_current_membre),
|
|
|
|
|
):
|
|
|
|
|
query = select(Poste)
|
|
|
|
|
if not inclure_abolis:
|
|
|
|
|
query = query.where(Poste.actif == True)
|
|
|
|
|
result = await db.execute(query.order_by(Poste.ordre_affichage))
|
|
|
|
|
postes = result.scalars().all()
|
|
|
|
|
|
2026-05-15 16:41:17 -04:00
|
|
|
return [PosteOut.model_validate(p) for p in postes]
|
2026-04-17 04:04:04 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/", response_model=PosteOut)
|
|
|
|
|
async def creer_poste(
|
|
|
|
|
req: PosteCreate,
|
|
|
|
|
executif=Depends(require_executif),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
"""Créer un nouveau poste (exécutif)."""
|
|
|
|
|
if req.categorie not in ("executif", "service", "physique"):
|
|
|
|
|
raise HTTPException(status_code=400, detail="Catégorie invalide")
|
|
|
|
|
poste = Poste(
|
|
|
|
|
nom=req.nom.strip(),
|
|
|
|
|
categorie=req.categorie,
|
|
|
|
|
description=req.description,
|
|
|
|
|
prerequis=req.prerequis,
|
|
|
|
|
informations=req.informations,
|
|
|
|
|
ordre_affichage=req.ordre_affichage or 99,
|
|
|
|
|
)
|
|
|
|
|
db.add(poste)
|
|
|
|
|
await db.flush()
|
|
|
|
|
return PosteOut.model_validate(poste)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/{poste_id}")
|
|
|
|
|
async def abolir_poste(
|
|
|
|
|
poste_id: str,
|
|
|
|
|
executif=Depends(require_executif),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
"""Abolir un poste — termine tous les mandats actifs associés."""
|
|
|
|
|
poste = await db.get(Poste, poste_id)
|
|
|
|
|
if not poste:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Poste introuvable")
|
|
|
|
|
|
|
|
|
|
# Terminer les mandats actifs
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(Affectation).where(
|
|
|
|
|
Affectation.poste_id == poste.id,
|
|
|
|
|
Affectation.statut == "actif",
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
mandats_termines = 0
|
|
|
|
|
for a in result.scalars().all():
|
|
|
|
|
a.statut = "termine"
|
|
|
|
|
a.date_fin = date.today()
|
|
|
|
|
a.note_transition = f"Poste aboli: {poste.nom}"
|
|
|
|
|
a.termine_par = executif.id
|
|
|
|
|
mandats_termines += 1
|
|
|
|
|
|
|
|
|
|
# Refuser les candidatures en attente
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(Candidature).where(
|
|
|
|
|
Candidature.poste_id == poste.id,
|
|
|
|
|
Candidature.statut == "proposee",
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
for c in result.scalars().all():
|
|
|
|
|
c.statut = "refusee"
|
|
|
|
|
|
|
|
|
|
poste.actif = False
|
|
|
|
|
await db.flush()
|
|
|
|
|
return {
|
|
|
|
|
"message": f"Poste «{poste.nom}» aboli",
|
|
|
|
|
"mandats_termines": mandats_termines,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Postes disponibles (vacants) ──
|
|
|
|
|
|
|
|
|
|
@router.get("/disponibles")
|
|
|
|
|
async def postes_disponibles(
|
|
|
|
|
membre=Depends(get_current_membre),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
"""Postes actifs sans titulaire — opportunités d'implication."""
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(Poste).where(Poste.actif == True).order_by(Poste.ordre_affichage)
|
|
|
|
|
)
|
|
|
|
|
postes = result.scalars().all()
|
|
|
|
|
|
|
|
|
|
disponibles = []
|
|
|
|
|
for p in postes:
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(Affectation).where(
|
|
|
|
|
Affectation.poste_id == p.id,
|
|
|
|
|
Affectation.statut == "actif",
|
|
|
|
|
Affectation.groupe_id == membre.groupe_id,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
if not result.scalars().first():
|
|
|
|
|
# Vérifier les candidatures existantes du membre
|
|
|
|
|
result_cand = await db.execute(
|
|
|
|
|
select(Candidature).where(
|
|
|
|
|
Candidature.poste_id == p.id,
|
|
|
|
|
Candidature.membre_id == membre.id,
|
|
|
|
|
Candidature.statut == "proposee",
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
deja_postule = result_cand.scalars().first() is not None
|
|
|
|
|
|
|
|
|
|
disponibles.append({
|
|
|
|
|
"id": str(p.id),
|
|
|
|
|
"nom": p.nom,
|
|
|
|
|
"categorie": p.categorie,
|
2026-05-15 16:41:17 -04:00
|
|
|
"description": p.description,
|
2026-04-17 04:04:04 -04:00
|
|
|
"prerequis": p.prerequis,
|
|
|
|
|
"deja_postule": deja_postule,
|
|
|
|
|
})
|
|
|
|
|
return disponibles
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Candidatures ──
|
|
|
|
|
|
|
|
|
|
@router.post("/candidatures")
|
|
|
|
|
async def postuler(
|
|
|
|
|
poste_id: str,
|
|
|
|
|
motivation: str = "",
|
|
|
|
|
membre=Depends(get_current_membre),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
"""Proposer sa candidature — crée automatiquement une proposition de nomination."""
|
|
|
|
|
from app.models.proposition import Proposition
|
|
|
|
|
|
|
|
|
|
poste = await db.get(Poste, poste_id)
|
|
|
|
|
if not poste or not poste.actif:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Poste introuvable ou aboli")
|
|
|
|
|
|
|
|
|
|
# Vérifier qu'il n'a pas déjà postulé
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(Candidature).where(
|
|
|
|
|
Candidature.poste_id == poste_id,
|
|
|
|
|
Candidature.membre_id == membre.id,
|
|
|
|
|
Candidature.statut == "proposee",
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
if result.scalars().first():
|
|
|
|
|
raise HTTPException(status_code=400, detail="Vous avez déjà postulé pour ce poste")
|
|
|
|
|
|
|
|
|
|
# Créer la candidature
|
|
|
|
|
cand = Candidature(
|
|
|
|
|
groupe_id=membre.groupe_id,
|
|
|
|
|
poste_id=poste_id,
|
|
|
|
|
membre_id=membre.id,
|
|
|
|
|
type="candidature",
|
|
|
|
|
motivation=motivation.strip() or None,
|
|
|
|
|
)
|
|
|
|
|
db.add(cand)
|
|
|
|
|
await db.flush()
|
|
|
|
|
|
|
|
|
|
# Créer automatiquement la proposition de nomination
|
|
|
|
|
prop = Proposition(
|
|
|
|
|
groupe_id=membre.groupe_id,
|
|
|
|
|
type_proposition="nomination",
|
|
|
|
|
sujet=f"Nomination de {membre.prenom} au poste de {poste.nom}",
|
|
|
|
|
description=motivation.strip() or None,
|
|
|
|
|
proposeur_id=membre.id,
|
|
|
|
|
candidature_id=cand.id,
|
|
|
|
|
statut="proposee",
|
|
|
|
|
)
|
|
|
|
|
db.add(prop)
|
|
|
|
|
cand.proposition_id = prop.id
|
|
|
|
|
await db.flush()
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"message": f"Candidature soumise pour {poste.nom} — proposition créée",
|
|
|
|
|
"id": str(cand.id),
|
|
|
|
|
"proposition_id": str(prop.id),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/candidatures/nominer")
|
|
|
|
|
async def nominer(
|
|
|
|
|
poste_id: str,
|
|
|
|
|
membre_id: str,
|
|
|
|
|
motivation: str = "",
|
|
|
|
|
executif=Depends(require_executif),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
"""Nominer quelqu'un (exécutif). Le nominé devra accepter avant que la proposition soit créée."""
|
|
|
|
|
poste = await db.get(Poste, poste_id)
|
|
|
|
|
if not poste or not poste.actif:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Poste introuvable ou aboli")
|
|
|
|
|
nominee = await db.get(Membre, membre_id)
|
|
|
|
|
if not nominee or nominee.groupe_id != executif.groupe_id:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Membre introuvable")
|
|
|
|
|
|
|
|
|
|
cand = Candidature(
|
|
|
|
|
groupe_id=executif.groupe_id,
|
|
|
|
|
poste_id=poste_id,
|
|
|
|
|
membre_id=membre_id,
|
|
|
|
|
type="nomination",
|
|
|
|
|
nomme_par=executif.id,
|
|
|
|
|
motivation=motivation.strip() or None,
|
|
|
|
|
)
|
|
|
|
|
db.add(cand)
|
|
|
|
|
await db.flush()
|
|
|
|
|
return {
|
|
|
|
|
"message": f"{nominee.prenom} nominé(e) pour {poste.nom} — en attente de son acceptation",
|
|
|
|
|
"id": str(cand.id),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/candidatures/{candidature_id}/retirer")
|
|
|
|
|
async def retirer_candidature(
|
|
|
|
|
candidature_id: str,
|
|
|
|
|
membre=Depends(get_current_membre),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
"""Retirer sa propre candidature. Retire aussi la proposition associée."""
|
|
|
|
|
from app.models.proposition import Proposition
|
|
|
|
|
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(Candidature).where(
|
|
|
|
|
Candidature.id == candidature_id,
|
|
|
|
|
Candidature.membre_id == membre.id,
|
|
|
|
|
Candidature.statut == "proposee",
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
cand = result.scalar_one_or_none()
|
|
|
|
|
if not cand:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Candidature introuvable ou déjà traitée")
|
|
|
|
|
cand.statut = "retiree"
|
|
|
|
|
|
|
|
|
|
# Retirer la proposition associée si elle n'est pas encore votée
|
|
|
|
|
if cand.proposition_id:
|
|
|
|
|
prop = await db.get(Proposition, cand.proposition_id)
|
|
|
|
|
if prop and prop.statut in ("proposee", "secondee"):
|
|
|
|
|
prop.statut = "rejetee"
|
|
|
|
|
prop.note = "Candidature retirée par le membre"
|
|
|
|
|
|
|
|
|
|
await db.flush()
|
|
|
|
|
return {"message": "Candidature retirée"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/candidatures/{candidature_id}/accepter-nomination")
|
|
|
|
|
async def accepter_nomination(
|
|
|
|
|
candidature_id: str,
|
|
|
|
|
accepter: bool = True,
|
|
|
|
|
membre=Depends(get_current_membre),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
"""Le nominé accepte/refuse. Si accepté, la proposition est créée automatiquement."""
|
|
|
|
|
from app.models.proposition import Proposition
|
|
|
|
|
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(Candidature).where(
|
|
|
|
|
Candidature.id == candidature_id,
|
|
|
|
|
Candidature.membre_id == membre.id,
|
|
|
|
|
Candidature.type == "nomination",
|
|
|
|
|
Candidature.statut == "proposee",
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
cand = result.scalar_one_or_none()
|
|
|
|
|
if not cand:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Nomination introuvable")
|
|
|
|
|
|
|
|
|
|
cand.acceptee_par_membre = accepter
|
|
|
|
|
|
|
|
|
|
if not accepter:
|
|
|
|
|
cand.statut = "refusee"
|
|
|
|
|
await db.flush()
|
|
|
|
|
return {"message": "Nomination refusée"}
|
|
|
|
|
|
|
|
|
|
# Acceptée → créer la proposition de nomination
|
|
|
|
|
poste = await db.get(Poste, cand.poste_id)
|
|
|
|
|
nomme_par = await db.get(Membre, cand.nomme_par) if cand.nomme_par else None
|
|
|
|
|
|
|
|
|
|
prop = Proposition(
|
|
|
|
|
groupe_id=cand.groupe_id,
|
|
|
|
|
type_proposition="nomination",
|
|
|
|
|
sujet=f"Nomination de {membre.prenom} au poste de {poste.nom if poste else '?'}",
|
|
|
|
|
description=f"Nominé par {nomme_par.prenom if nomme_par else '?'}. {cand.motivation or ''}".strip(),
|
|
|
|
|
proposeur_id=cand.nomme_par or membre.id,
|
|
|
|
|
candidature_id=cand.id,
|
|
|
|
|
statut="proposee",
|
|
|
|
|
)
|
|
|
|
|
db.add(prop)
|
|
|
|
|
cand.proposition_id = prop.id
|
|
|
|
|
await db.flush()
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"message": "Nomination acceptée — proposition créée, en attente d'être secondée",
|
|
|
|
|
"proposition_id": str(prop.id),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/candidatures")
|
|
|
|
|
async def liste_candidatures(
|
|
|
|
|
executif=Depends(require_executif),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
"""Liste des candidatures du groupe (exécutif)."""
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(Candidature).where(
|
|
|
|
|
Candidature.groupe_id == executif.groupe_id,
|
|
|
|
|
Candidature.statut == "proposee",
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
candidatures = result.scalars().all()
|
|
|
|
|
out = []
|
|
|
|
|
for c in candidatures:
|
|
|
|
|
mbr = await db.get(Membre, c.membre_id)
|
|
|
|
|
poste = await db.get(Poste, c.poste_id)
|
|
|
|
|
nomme = await db.get(Membre, c.nomme_par) if c.nomme_par else None
|
|
|
|
|
out.append({
|
|
|
|
|
"id": str(c.id),
|
|
|
|
|
"membre_prenom": mbr.prenom if mbr else "?",
|
|
|
|
|
"poste_nom": poste.nom if poste else "?",
|
|
|
|
|
"type": c.type,
|
|
|
|
|
"motivation": c.motivation,
|
|
|
|
|
"nomme_par_prenom": nomme.prenom if nomme else None,
|
|
|
|
|
"acceptee_par_membre": c.acceptee_par_membre,
|
|
|
|
|
"cree_le": str(c.cree_le),
|
|
|
|
|
})
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/candidatures/mes")
|
|
|
|
|
async def mes_candidatures(
|
|
|
|
|
membre=Depends(get_current_membre),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
"""Mes candidatures et nominations en attente."""
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(Candidature).where(
|
|
|
|
|
Candidature.membre_id == membre.id,
|
|
|
|
|
Candidature.statut == "proposee",
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
out = []
|
|
|
|
|
for c in result.scalars().all():
|
|
|
|
|
poste = await db.get(Poste, c.poste_id)
|
|
|
|
|
nomme = await db.get(Membre, c.nomme_par) if c.nomme_par else None
|
|
|
|
|
out.append({
|
|
|
|
|
"id": str(c.id),
|
|
|
|
|
"poste_nom": poste.nom if poste else "?",
|
|
|
|
|
"type": c.type,
|
|
|
|
|
"nomme_par_prenom": nomme.prenom if nomme else None,
|
|
|
|
|
"acceptee_par_membre": c.acceptee_par_membre,
|
|
|
|
|
})
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch("/candidatures/{candidature_id}")
|
|
|
|
|
async def traiter_candidature(
|
|
|
|
|
candidature_id: str,
|
|
|
|
|
statut: str,
|
|
|
|
|
executif=Depends(require_executif),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
"""Accepter ou refuser une candidature (exécutif)."""
|
|
|
|
|
if statut not in ("acceptee", "refusee"):
|
|
|
|
|
raise HTTPException(status_code=400, detail="Statut: acceptee ou refusee")
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(Candidature).where(
|
|
|
|
|
Candidature.id == candidature_id,
|
|
|
|
|
Candidature.groupe_id == executif.groupe_id,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
cand = result.scalar_one_or_none()
|
|
|
|
|
if not cand:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Candidature introuvable")
|
|
|
|
|
|
|
|
|
|
cand.statut = statut
|
|
|
|
|
|
|
|
|
|
# Si acceptée, créer l'affectation automatiquement
|
|
|
|
|
if statut == "acceptee":
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
await db.flush()
|
|
|
|
|
return {"message": "Candidature " + ("acceptée" if statut == "acceptee" else "refusée")}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Affectations ──
|
2026-03-04 14:01:00 -05:00
|
|
|
|
|
|
|
|
@router.get("/affectations")
|
|
|
|
|
async def mes_affectations(membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db)):
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(Affectation).join(Poste).where(
|
|
|
|
|
Affectation.membre_id == membre.id,
|
|
|
|
|
Affectation.statut == "actif",
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
out = []
|
2026-04-17 04:04:04 -04:00
|
|
|
for a in result.scalars().all():
|
2026-03-04 14:01:00 -05:00
|
|
|
poste = await db.get(Poste, a.poste_id)
|
|
|
|
|
out.append({
|
|
|
|
|
"id": str(a.id),
|
|
|
|
|
"poste_nom": poste.nom if poste else "?",
|
|
|
|
|
"poste_categorie": poste.categorie if poste else "?",
|
|
|
|
|
"date_debut": str(a.date_debut),
|
|
|
|
|
"statut": a.statut,
|
|
|
|
|
})
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/affectations/groupe")
|
2026-04-17 04:04:04 -04:00
|
|
|
async def affectations_groupe(executif=Depends(require_executif), db: AsyncSession = Depends(get_db)):
|
2026-03-04 14:01:00 -05:00
|
|
|
result = await db.execute(
|
|
|
|
|
select(Affectation).where(
|
|
|
|
|
Affectation.groupe_id == executif.groupe_id,
|
|
|
|
|
Affectation.statut == "actif",
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
out = []
|
2026-04-17 04:04:04 -04:00
|
|
|
for a in result.scalars().all():
|
2026-03-04 14:01:00 -05:00
|
|
|
poste = await db.get(Poste, a.poste_id)
|
|
|
|
|
mbr = await db.get(Membre, a.membre_id)
|
|
|
|
|
out.append({
|
|
|
|
|
"id": str(a.id),
|
|
|
|
|
"membre_prenom": mbr.prenom if mbr else "?",
|
|
|
|
|
"poste_nom": poste.nom if poste else "?",
|
|
|
|
|
"poste_categorie": poste.categorie if poste else "?",
|
|
|
|
|
"date_debut": str(a.date_debut),
|
|
|
|
|
})
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/affectations")
|
2026-04-17 04:04:04 -04:00
|
|
|
async def creer_affectation(req: AffectationCreate, executif=Depends(require_executif), db: AsyncSession = Depends(get_db)):
|
2026-05-15 16:41:17 -04:00
|
|
|
from app.models.proposition import Proposition
|
|
|
|
|
|
|
|
|
|
if req.proposeur_id == req.secondeur_id:
|
|
|
|
|
raise HTTPException(status_code=400, detail="Le proposeur et le secondeur doivent être différents")
|
|
|
|
|
|
|
|
|
|
cible = await db.get(Membre, req.membre_id)
|
|
|
|
|
proposeur = await db.get(Membre, req.proposeur_id)
|
|
|
|
|
secondeur = await db.get(Membre, req.secondeur_id)
|
|
|
|
|
poste = await db.get(Poste, req.poste_id)
|
|
|
|
|
if not cible or cible.groupe_id != executif.groupe_id or not cible.actif:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Membre à affecter introuvable ou inactif")
|
|
|
|
|
if not proposeur or proposeur.groupe_id != executif.groupe_id or not proposeur.actif:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Proposeur introuvable ou inactif")
|
|
|
|
|
if not secondeur or secondeur.groupe_id != executif.groupe_id or not secondeur.actif:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Secondeur introuvable ou inactif")
|
|
|
|
|
if not poste or not poste.actif:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Poste introuvable ou aboli")
|
|
|
|
|
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(Affectation).where(
|
|
|
|
|
Affectation.groupe_id == executif.groupe_id,
|
|
|
|
|
Affectation.poste_id == req.poste_id,
|
|
|
|
|
Affectation.statut == "actif",
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
if result.scalar_one_or_none():
|
|
|
|
|
raise HTTPException(status_code=400, detail="Ce poste est déjà affecté")
|
|
|
|
|
|
|
|
|
|
numero_resolution = await _prochain_numero_resolution(executif.groupe_id, req.date_debut.year, db)
|
|
|
|
|
resolution = Proposition(
|
|
|
|
|
groupe_id=executif.groupe_id,
|
|
|
|
|
type_proposition="nomination",
|
|
|
|
|
sujet=f"Nomination de {cible.prenom} au poste de {poste.nom}",
|
|
|
|
|
description=f"Affectation directe au poste de {poste.nom} à partir du {req.date_debut}.",
|
|
|
|
|
proposeur_id=req.proposeur_id,
|
|
|
|
|
secondeur_id=req.secondeur_id,
|
|
|
|
|
statut="adoptee",
|
|
|
|
|
numero_resolution=numero_resolution,
|
|
|
|
|
date_vote=req.date_debut,
|
|
|
|
|
)
|
|
|
|
|
db.add(resolution)
|
|
|
|
|
await db.flush()
|
|
|
|
|
|
2026-03-04 14:01:00 -05:00
|
|
|
affectation = Affectation(
|
|
|
|
|
groupe_id=executif.groupe_id,
|
|
|
|
|
membre_id=req.membre_id,
|
|
|
|
|
poste_id=req.poste_id,
|
|
|
|
|
date_debut=req.date_debut,
|
|
|
|
|
statut="actif",
|
|
|
|
|
)
|
|
|
|
|
db.add(affectation)
|
|
|
|
|
await db.flush()
|
2026-05-15 16:41:17 -04:00
|
|
|
return {
|
|
|
|
|
"message": "Affectation créée",
|
|
|
|
|
"id": str(affectation.id),
|
|
|
|
|
"resolution_id": str(resolution.id),
|
|
|
|
|
"numero_resolution": resolution.numero_resolution,
|
|
|
|
|
}
|
2026-03-04 14:01:00 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/affectations/{affectation_id}/terminer")
|
|
|
|
|
async def terminer_mandat(
|
2026-04-17 04:04:04 -04:00
|
|
|
affectation_id: str, req: TerminerMandatRequest,
|
|
|
|
|
executif=Depends(require_executif), db: AsyncSession = Depends(get_db),
|
2026-03-04 14:01:00 -05:00
|
|
|
):
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(Affectation).where(
|
|
|
|
|
Affectation.id == affectation_id,
|
|
|
|
|
Affectation.groupe_id == executif.groupe_id,
|
|
|
|
|
Affectation.statut == "actif",
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
affectation = result.scalar_one_or_none()
|
|
|
|
|
if not affectation:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Affectation introuvable")
|
|
|
|
|
affectation.statut = "termine"
|
|
|
|
|
affectation.date_fin = date.today()
|
|
|
|
|
affectation.note_transition = req.note_transition
|
|
|
|
|
affectation.termine_par = executif.id
|
|
|
|
|
await db.flush()
|
|
|
|
|
return {"message": "Mandat terminé"}
|
2026-04-17 04:04:04 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Réactivation / suppression définitive de postes ──
|
|
|
|
|
|
|
|
|
|
@router.post("/{poste_id}/reactiver")
|
|
|
|
|
async def reactiver_poste(
|
|
|
|
|
poste_id: str,
|
|
|
|
|
executif=Depends(require_executif),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
"""Réactiver un poste aboli."""
|
|
|
|
|
poste = await db.get(Poste, poste_id)
|
|
|
|
|
if not poste:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Poste introuvable")
|
|
|
|
|
if poste.actif:
|
|
|
|
|
raise HTTPException(status_code=400, detail="Ce poste est déjà actif")
|
|
|
|
|
poste.actif = True
|
|
|
|
|
await db.flush()
|
|
|
|
|
return {"message": f"Poste «{poste.nom}» réactivé"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/{poste_id}/permanent")
|
|
|
|
|
async def supprimer_poste_permanent(
|
|
|
|
|
poste_id: str,
|
|
|
|
|
executif=Depends(require_executif),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
"""Supprimer définitivement un poste aboli (irréversible)."""
|
|
|
|
|
poste = await db.get(Poste, poste_id)
|
|
|
|
|
if not poste:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Poste introuvable")
|
|
|
|
|
if poste.actif:
|
|
|
|
|
raise HTTPException(status_code=400, detail="Abolissez d'abord le poste avant de le supprimer")
|
|
|
|
|
|
|
|
|
|
await db.delete(poste)
|
|
|
|
|
await db.flush()
|
|
|
|
|
return {"message": f"Poste «{poste.nom}» supprimé définitivement"}
|
|
|
|
|
|
|
|
|
|
|
2026-05-15 16:41:17 -04:00
|
|
|
# ── Acces aux modules ──
|
2026-04-17 04:04:04 -04:00
|
|
|
|
2026-05-15 16:41:17 -04:00
|
|
|
@router.get("/modules")
|
|
|
|
|
async def modules_par_poste(
|
2026-04-17 04:04:04 -04:00
|
|
|
executif=Depends(require_executif),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
):
|
2026-05-15 16:41:17 -04:00
|
|
|
result = await db.execute(select(PosteModule))
|
|
|
|
|
modules = result.scalars().all()
|
|
|
|
|
par_poste: dict[str, list[str]] = {}
|
|
|
|
|
for m in modules:
|
|
|
|
|
par_poste.setdefault(str(m.poste_id), []).append(m.module_code)
|
|
|
|
|
return par_poste
|
2026-04-17 04:04:04 -04:00
|
|
|
|
|
|
|
|
|
2026-05-15 16:41:17 -04:00
|
|
|
@router.put("/{poste_id}/modules")
|
|
|
|
|
async def definir_modules_poste(
|
2026-04-17 04:04:04 -04:00
|
|
|
poste_id: str,
|
2026-05-15 16:41:17 -04:00
|
|
|
codes: list[str],
|
2026-04-17 04:04:04 -04:00
|
|
|
executif=Depends(require_executif),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
poste = await db.get(Poste, poste_id)
|
|
|
|
|
if not poste or not poste.actif:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Poste introuvable ou aboli")
|
|
|
|
|
|
2026-05-15 16:41:17 -04:00
|
|
|
result = await db.execute(select(PosteModule).where(PosteModule.poste_id == poste.id))
|
|
|
|
|
for item in result.scalars().all():
|
|
|
|
|
await db.delete(item)
|
|
|
|
|
for code in sorted(set(codes)):
|
|
|
|
|
db.add(PosteModule(poste_id=poste.id, module_code=code))
|
2026-04-17 04:04:04 -04:00
|
|
|
await db.flush()
|
2026-05-15 16:41:17 -04:00
|
|
|
return {"message": "Accès aux modules mis à jour"}
|