172 lines
6 KiB
Python
172 lines
6 KiB
Python
"""Routes M2 — Présences en réunion d'affaires."""
|
|
from datetime import date as dt_date
|
|
from calendar import monthrange
|
|
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.security import get_current_membre
|
|
from app.models.reunion import Reunion
|
|
from app.models.groupe import Groupe
|
|
from app.models.presence import Presence
|
|
from app.models.membre import Membre
|
|
from app.models.affectation import Affectation
|
|
from app.models.poste import Poste
|
|
from app.schemas.phase4 import (
|
|
PresencesBatch, PresenceOut, ReunionAffairesCreate, ReunionAffairesOut,
|
|
)
|
|
|
|
router = APIRouter(prefix="/presences", tags=["M2 — Présences affaires"])
|
|
|
|
JOURS_SEMAINE = {
|
|
"lundi": 0, "mardi": 1, "mercredi": 2, "jeudi": 3,
|
|
"vendredi": 4, "samedi": 5, "dimanche": 6,
|
|
}
|
|
|
|
|
|
def _premier_jour_du_mois(annee: int, mois: int, jour_cible: int) -> dt_date:
|
|
"""Retourne la date du premier 'jour_cible' (0=lundi) du mois."""
|
|
premier = dt_date(annee, mois, 1)
|
|
diff = (jour_cible - premier.weekday()) % 7
|
|
return dt_date(annee, mois, 1 + diff)
|
|
|
|
|
|
async def require_secretaire(membre=Depends(get_current_membre), db=Depends(get_db)):
|
|
"""Vérifie que le membre est secrétaire ou exécutif."""
|
|
result = await db.execute(
|
|
select(Affectation).join(Poste).where(
|
|
Affectation.membre_id == membre.id,
|
|
Affectation.statut == "actif",
|
|
(Poste.nom.ilike("%secrétaire%") | (Poste.categorie == "executif")),
|
|
)
|
|
)
|
|
if not result.scalars().first():
|
|
raise HTTPException(status_code=403, detail="Réservé au secrétaire ou à l'exécutif")
|
|
return membre
|
|
|
|
|
|
@router.post("/reunion", response_model=ReunionAffairesOut)
|
|
async def creer_reunion_affaires(
|
|
req: ReunionAffairesCreate,
|
|
membre=Depends(require_secretaire),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Créer une réunion d'affaires (point de départ pour présences et PV)."""
|
|
groupe = await db.get(Groupe, membre.groupe_id)
|
|
|
|
# ── Validation : premier jour de réunion du mois, une seule par mois ──
|
|
dt = req.date_reunion
|
|
if groupe.jour_reunion:
|
|
jour_attendu = JOURS_SEMAINE.get(groupe.jour_reunion.lower())
|
|
if jour_attendu is not None:
|
|
premier = _premier_jour_du_mois(dt.year, dt.month, jour_attendu)
|
|
if dt.date() != premier:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"La réunion d'affaires doit avoir lieu le premier {groupe.jour_reunion} du mois ({premier.isoformat()})",
|
|
)
|
|
|
|
# Une seule réunion d'affaires par mois
|
|
result = await db.execute(
|
|
select(Reunion).where(
|
|
Reunion.groupe_id == membre.groupe_id,
|
|
Reunion.type == "affaires",
|
|
extract("year", Reunion.date_reunion) == dt.year,
|
|
extract("month", Reunion.date_reunion) == dt.month,
|
|
)
|
|
)
|
|
if result.scalars().first():
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Il y a déjà une réunion d'affaires pour {dt.strftime('%B %Y')}",
|
|
)
|
|
|
|
reunion = Reunion(
|
|
groupe_id=membre.groupe_id,
|
|
type="affaires",
|
|
date_reunion=req.date_reunion,
|
|
note=req.note,
|
|
)
|
|
db.add(reunion)
|
|
await db.flush()
|
|
return ReunionAffairesOut(id=reunion.id, date_reunion=reunion.date_reunion, note=reunion.note)
|
|
|
|
|
|
@router.get("/reunions", response_model=list[ReunionAffairesOut])
|
|
async def liste_reunions_affaires(
|
|
limite: int = 12,
|
|
membre=Depends(get_current_membre),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Lister les réunions d'affaires récentes avec leurs présences."""
|
|
result = await db.execute(
|
|
select(Reunion).where(
|
|
Reunion.groupe_id == membre.groupe_id,
|
|
Reunion.type == "affaires",
|
|
).order_by(desc(Reunion.date_reunion)).limit(limite)
|
|
)
|
|
reunions = result.scalars().all()
|
|
|
|
out = []
|
|
for r in reunions:
|
|
pres_result = await db.execute(
|
|
select(Presence).where(Presence.reunion_id == r.id)
|
|
)
|
|
presences = pres_result.scalars().all()
|
|
pres_out = []
|
|
for p in presences:
|
|
mbr = await db.get(Membre, p.membre_id)
|
|
pres_out.append(PresenceOut(
|
|
membre_id=p.membre_id,
|
|
membre_prenom=mbr.prenom if mbr else "?",
|
|
present=p.present,
|
|
))
|
|
out.append(ReunionAffairesOut(
|
|
id=r.id, date_reunion=r.date_reunion, note=r.note, presences=pres_out,
|
|
))
|
|
return out
|
|
|
|
|
|
@router.post("/", response_model=list[PresenceOut])
|
|
async def enregistrer_presences(
|
|
req: PresencesBatch,
|
|
membre=Depends(require_secretaire),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""M2 — Enregistrer les présences pour une réunion d'affaires."""
|
|
# Vérifier que la réunion existe et est de type affaires
|
|
result = await db.execute(
|
|
select(Reunion).where(
|
|
Reunion.id == req.reunion_id,
|
|
Reunion.groupe_id == membre.groupe_id,
|
|
)
|
|
)
|
|
reunion = result.scalar_one_or_none()
|
|
if not reunion:
|
|
raise HTTPException(status_code=404, detail="Réunion introuvable")
|
|
|
|
# Supprimer les présences existantes pour cette réunion (pour pouvoir re-saisir)
|
|
existing = await db.execute(
|
|
select(Presence).where(Presence.reunion_id == req.reunion_id)
|
|
)
|
|
for p in existing.scalars().all():
|
|
await db.delete(p)
|
|
|
|
# Insérer les nouvelles
|
|
out = []
|
|
for item in req.presences:
|
|
pres = Presence(
|
|
reunion_id=req.reunion_id,
|
|
membre_id=item.membre_id,
|
|
present=item.present,
|
|
)
|
|
db.add(pres)
|
|
mbr = await db.get(Membre, item.membre_id)
|
|
out.append(PresenceOut(
|
|
membre_id=item.membre_id,
|
|
membre_prenom=mbr.prenom if mbr else "?",
|
|
present=item.present,
|
|
))
|
|
await db.flush()
|
|
return out
|