545 lines
22 KiB
Python
545 lines
22 KiB
Python
"""Routes S4 — Rapport de réunion."""
|
|
import io
|
|
from html import escape
|
|
from datetime import datetime, timezone
|
|
from zoneinfo import ZoneInfo
|
|
from fastapi import APIRouter, Depends, Header, HTTPException
|
|
from fastapi.responses import StreamingResponse
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, desc, extract
|
|
|
|
from app.core.database import get_db
|
|
from app.core.reunions import jour_reunion_attendu
|
|
from app.core.security import get_current_membre, require_executif
|
|
from app.models.reunion import Reunion
|
|
from app.models.collecte import Collecte
|
|
from app.models.groupe import Groupe
|
|
from app.models.membre import Membre
|
|
from app.models.jeton import Jeton
|
|
from app.models.litterature import Litterature
|
|
from app.models.vente_jeton import VenteJeton
|
|
from app.models.vente_litterature import VenteLitterature
|
|
from app.models.rapport_adopte import RapportAdopte
|
|
from app.schemas.phase1 import ReunionCreate, ReunionOut
|
|
from app.services.historique import enregistrer_historique, snapshot, verifier_version_historisee
|
|
from app.services.rapports_adoptes import rapport_adopte_pour_reunion, verifier_reunion_non_cristallisee
|
|
|
|
router = APIRouter(prefix="/reunions", tags=["S4 — Rapport de réunion"])
|
|
|
|
|
|
def _date_civile_reunion(valeur: datetime):
|
|
if valeur.tzinfo:
|
|
return valeur.astimezone(ZoneInfo("America/Toronto")).date()
|
|
return valeur.date()
|
|
|
|
|
|
async def _html_rapport_rencontre(reunion: Reunion, membre, db: AsyncSession) -> tuple[str, datetime.date]:
|
|
groupe = await db.get(Groupe, membre.groupe_id)
|
|
animateur = await db.get(Membre, reunion.animateur_id) if reunion.animateur_id else None
|
|
jour = reunion.date_reunion.date()
|
|
|
|
collecte = (await db.execute(
|
|
select(Collecte).where(
|
|
Collecte.groupe_id == membre.groupe_id,
|
|
(Collecte.reunion_id == reunion.id) | (Collecte.date_collecte == jour),
|
|
).limit(1)
|
|
)).scalar_one_or_none()
|
|
collecte_html = "<p>Aucune collecte saisie.</p>"
|
|
if collecte:
|
|
detenteur = await db.get(Membre, collecte.detenteur_id) if collecte.detenteur_id else None
|
|
collecte_html = (
|
|
"<table>"
|
|
f"<tr><td>Montant</td><td class=\"montant\">{float(collecte.montant):.2f} $</td></tr>"
|
|
f"<tr><td>Détenteur</td><td>{escape(detenteur.prenom if detenteur else '—')}</td></tr>"
|
|
f"<tr><td>Statut</td><td>{escape(collecte.statut)}</td></tr>"
|
|
"</table>"
|
|
)
|
|
|
|
ventes_lit_rows = []
|
|
ventes_lit_result = await db.execute(
|
|
select(VenteLitterature).where(
|
|
VenteLitterature.groupe_id == membre.groupe_id,
|
|
(VenteLitterature.reunion_id == reunion.id) | (VenteLitterature.date_vente == jour),
|
|
)
|
|
)
|
|
for vente in ventes_lit_result.scalars().all():
|
|
item = await db.get(Litterature, vente.litterature_id)
|
|
vendeur = await db.get(Membre, vente.vendu_par) if vente.vendu_par else None
|
|
ventes_lit_rows.append(
|
|
f"<tr><td>{escape(item.titre if item else '?')}</td><td>{vente.quantite}</td>"
|
|
f"<td class=\"montant\">{float(vente.montant):.2f} $</td><td>{escape(vendeur.prenom if vendeur else '—')}</td>"
|
|
f"<td>{'Reçue' if vente.remis_tresorier else 'À recevoir'}</td></tr>"
|
|
)
|
|
|
|
ventes_jetons_rows = []
|
|
ventes_jetons_result = await db.execute(
|
|
select(VenteJeton).where(
|
|
VenteJeton.groupe_id == membre.groupe_id,
|
|
(VenteJeton.reunion_id == reunion.id) | (VenteJeton.date_vente == jour),
|
|
)
|
|
)
|
|
for vente in ventes_jetons_result.scalars().all():
|
|
vendeur = await db.get(Membre, vente.vendu_par) if vente.vendu_par else None
|
|
ventes_jetons_rows.append(
|
|
f"<tr><td>{escape(vente.type_jeton)}</td><td>{vente.quantite}</td>"
|
|
f"<td class=\"montant\">{float(vente.montant):.2f} $</td><td>{escape(vendeur.prenom if vendeur else '—')}</td>"
|
|
f"<td>{'Reçue' if vente.remis_tresorier else 'À recevoir'}</td></tr>"
|
|
)
|
|
|
|
jetons_rows = []
|
|
jetons_result = await db.execute(
|
|
select(Jeton).where(
|
|
Jeton.groupe_id == membre.groupe_id,
|
|
(Jeton.reunion_id == reunion.id) | (Jeton.date_remise == jour),
|
|
)
|
|
)
|
|
for jeton in jetons_result.scalars().all():
|
|
cible = await db.get(Membre, jeton.membre_id) if jeton.membre_id else None
|
|
detail = f"{jeton.annees} ans" if jeton.annees else ""
|
|
jetons_rows.append(
|
|
f"<tr><td>{escape(jeton.type_jeton)}</td><td>{escape(cible.prenom if cible else 'Anonyme')}</td><td>{escape(detail)}</td></tr>"
|
|
)
|
|
|
|
annonces = reunion.annonces or []
|
|
annonces_texte = "\n".join(annonces)
|
|
collecte_texte = "Aucune collecte saisie."
|
|
if collecte:
|
|
detenteur = await db.get(Membre, collecte.detenteur_id) if collecte.detenteur_id else None
|
|
collecte_texte = (
|
|
f"{float(collecte.montant):.2f} $"
|
|
f"\nDétenteur: {detenteur.prenom if detenteur else '—'}"
|
|
f"\nStatut: {collecte.statut}"
|
|
)
|
|
ventes_lit_texte = "\n".join(
|
|
row.replace("<tr><td>", "")
|
|
.replace("</td><td>", " · ")
|
|
.replace("</td><td class=\"montant\">", " · ")
|
|
.replace("</td></tr>", "")
|
|
for row in ventes_lit_rows
|
|
)
|
|
jetons_texte = "\n".join(
|
|
row.replace("<tr><td>", "")
|
|
.replace("</td><td>", " · ")
|
|
.replace("</td></tr>", "")
|
|
for row in jetons_rows
|
|
)
|
|
ventes_jetons_texte = "\n".join(
|
|
row.replace("<tr><td>", "")
|
|
.replace("</td><td>", " · ")
|
|
.replace("</td><td class=\"montant\">", " · ")
|
|
.replace("</td></tr>", "")
|
|
for row in ventes_jetons_rows
|
|
)
|
|
notes_texte = "\n\n".join([texte for texte in [reunion.note, annonces_texte] if texte])
|
|
|
|
def bloc(label: str, contenu: str = "", cls: str = "") -> str:
|
|
return (
|
|
f"<div class=\"field {cls}\"><div class=\"box\">"
|
|
f"{escape(contenu or '')}</div><div class=\"label\">{escape(label)}</div></div>"
|
|
)
|
|
|
|
css = """
|
|
@page { size: letter; margin: 0.55in 0.65in; }
|
|
body { font-family: Arial, 'DejaVu Sans', sans-serif; color: #111; font-size: 10pt; }
|
|
.header { text-align: center; margin: 12pt 0 40pt; }
|
|
.groupe { font-size: 21pt; font-weight: 400; margin-bottom: 16pt; }
|
|
.district { font-size: 16pt; margin-bottom: 18pt; }
|
|
h1 { font-size: 14pt; font-weight: 500; letter-spacing: 0; margin: 0; }
|
|
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18pt 18pt; }
|
|
.field { break-inside: avoid; }
|
|
.field.full { grid-column: 1 / -1; }
|
|
.box {
|
|
border: 1pt solid #222;
|
|
min-height: 27pt;
|
|
padding: 6pt 8pt;
|
|
white-space: pre-wrap;
|
|
line-height: 1.25;
|
|
overflow: hidden;
|
|
}
|
|
.label { font-style: italic; font-size: 8.5pt; margin: 4pt 0 0 1pt; }
|
|
.short .box { min-height: 24pt; }
|
|
.theme .box { min-height: 58pt; }
|
|
.medium .box { min-height: 58pt; }
|
|
.deep .box { min-height: 95pt; }
|
|
.notes .box { min-height: 150pt; }
|
|
.footer { margin-top: 10pt; color: #777; font-size: 7.5pt; text-align: center; }
|
|
"""
|
|
groupe_nom = groupe.nom if groupe and groupe.nom else "Groupe"
|
|
district = f"District {groupe.district}" if groupe and groupe.district else "District"
|
|
html = f"""<!doctype html><html><head><meta charset="utf-8"><style>{css}</style></head><body>
|
|
<div class="header">
|
|
<div class="groupe">{escape(groupe_nom)}</div>
|
|
<div class="district">{escape(district)}</div>
|
|
<h1>RÉUNION HEBDOMADAIRE</h1>
|
|
</div>
|
|
<div class="grid">
|
|
{bloc("DATE ( AAAA / MM / JJ)", reunion.date_reunion.date().isoformat(), "short")}
|
|
{bloc("PRÉSENCES", "", "short")}
|
|
{bloc("SALLE & CAFÉ", groupe.adresse_local if groupe else "", "short")}
|
|
{bloc("ANIMATION", animateur.prenom if animateur else "", "short")}
|
|
{bloc("THÈME DE LA DISCUSSION", reunion.theme or "", "full theme")}
|
|
{bloc("NOUVEAU(X)", "", "medium")}
|
|
{bloc("ANNIVERSAIRES / JETONS", jetons_texte, "medium")}
|
|
{bloc("COLLECTE", collecte_texte, "short")}
|
|
{bloc("DÉPENSES", "", "deep")}
|
|
{bloc("VENTES (Littérature)", ventes_lit_texte, "short")}
|
|
{bloc("VENTES (Jetons)", ventes_jetons_texte, "short")}
|
|
{bloc("NOTES", notes_texte, "full notes")}
|
|
</div>
|
|
<div class="footer">Rapport généré automatiquement · {datetime.now().strftime('%Y-%m-%d %H:%M')}</div>
|
|
</body></html>"""
|
|
return html, jour
|
|
|
|
|
|
@router.post("/", response_model=ReunionOut)
|
|
async def creer_rapport_reunion(
|
|
req: ReunionCreate,
|
|
membre=Depends(require_executif),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""S4 — L'animateur note le thème et les annonces."""
|
|
groupe = await db.get(Groupe, membre.groupe_id)
|
|
|
|
# ── Validation : réunion régulière ──
|
|
if req.type == "reguliere" and groupe.jour_reunion:
|
|
date_civile = _date_civile_reunion(req.date_reunion)
|
|
jour_attendu = jour_reunion_attendu(groupe)
|
|
jour_soumis = date_civile.weekday()
|
|
if jour_attendu is not None and jour_soumis != jour_attendu:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"La réunion régulière doit avoir lieu le {groupe.jour_reunion}",
|
|
)
|
|
# Une seule régulière par semaine ISO
|
|
iso_year, iso_week, _ = date_civile.isocalendar()
|
|
result = await db.execute(
|
|
select(Reunion).where(
|
|
Reunion.groupe_id == membre.groupe_id,
|
|
Reunion.type == "reguliere",
|
|
extract("isoyear", Reunion.date_reunion) == iso_year,
|
|
extract("week", Reunion.date_reunion) == iso_week,
|
|
)
|
|
)
|
|
if result.scalars().first():
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Il y a déjà une réunion régulière pour la semaine {iso_week}",
|
|
)
|
|
|
|
animateur = membre
|
|
if req.animateur_id:
|
|
animateur = await db.get(Membre, req.animateur_id)
|
|
if not animateur or animateur.groupe_id != membre.groupe_id or not animateur.actif:
|
|
raise HTTPException(status_code=404, detail="Animateur introuvable ou inactif")
|
|
|
|
reunion = Reunion(
|
|
groupe_id=membre.groupe_id,
|
|
type=req.type,
|
|
date_reunion=req.date_reunion,
|
|
animateur_id=animateur.id,
|
|
theme=req.theme,
|
|
annonces=req.annonces,
|
|
note=req.note,
|
|
)
|
|
db.add(reunion)
|
|
await db.flush()
|
|
return ReunionOut(
|
|
id=reunion.id,
|
|
type=reunion.type,
|
|
date_reunion=reunion.date_reunion,
|
|
animateur_prenom=animateur.prenom,
|
|
theme=reunion.theme,
|
|
annonces=reunion.annonces,
|
|
note=reunion.note,
|
|
cree_le=reunion.cree_le,
|
|
modifie_le=reunion.modifie_le,
|
|
)
|
|
|
|
|
|
@router.get("/", response_model=list[ReunionOut])
|
|
async def liste_reunions(
|
|
limite: int = 20,
|
|
membre=Depends(get_current_membre),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Lister les réunions récentes du groupe."""
|
|
result = await db.execute(
|
|
select(Reunion)
|
|
.where(Reunion.groupe_id == membre.groupe_id)
|
|
.order_by(desc(Reunion.date_reunion))
|
|
.limit(limite)
|
|
)
|
|
reunions = result.scalars().all()
|
|
out = []
|
|
for r in reunions:
|
|
anim = await db.get(Membre, r.animateur_id) if r.animateur_id else None
|
|
out.append(ReunionOut(
|
|
id=r.id,
|
|
type=r.type,
|
|
date_reunion=r.date_reunion,
|
|
animateur_prenom=anim.prenom if anim else None,
|
|
theme=r.theme,
|
|
annonces=r.annonces,
|
|
note=r.note,
|
|
cree_le=r.cree_le,
|
|
modifie_le=r.modifie_le,
|
|
))
|
|
return out
|
|
|
|
|
|
@router.get("/rencontres/details")
|
|
async def liste_rencontres_details(
|
|
limite: int = 80,
|
|
membre=Depends(get_current_membre),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Lister les rencontres régulières avec leurs saisies associées."""
|
|
result = await db.execute(
|
|
select(Reunion)
|
|
.where(Reunion.groupe_id == membre.groupe_id, Reunion.type == "reguliere")
|
|
.order_by(desc(Reunion.date_reunion))
|
|
.limit(limite)
|
|
)
|
|
rencontres = []
|
|
for reunion in result.scalars().all():
|
|
jour = reunion.date_reunion.date()
|
|
animateur = await db.get(Membre, reunion.animateur_id) if reunion.animateur_id else None
|
|
|
|
collecte_result = await db.execute(
|
|
select(Collecte).where(
|
|
Collecte.groupe_id == membre.groupe_id,
|
|
(Collecte.reunion_id == reunion.id) | (Collecte.date_collecte == jour),
|
|
).limit(1)
|
|
)
|
|
collecte = collecte_result.scalar_one_or_none()
|
|
collecte_out = None
|
|
if collecte:
|
|
detenteur = await db.get(Membre, collecte.detenteur_id) if collecte.detenteur_id else None
|
|
collecte_out = {
|
|
"id": str(collecte.id),
|
|
"montant": float(collecte.montant),
|
|
"statut": collecte.statut,
|
|
"detenteur_prenom": detenteur.prenom if detenteur else None,
|
|
"modifie_le": collecte.modifie_le,
|
|
}
|
|
|
|
ventes_lit_result = await db.execute(
|
|
select(VenteLitterature).where(
|
|
VenteLitterature.groupe_id == membre.groupe_id,
|
|
(VenteLitterature.reunion_id == reunion.id) | (VenteLitterature.date_vente == jour),
|
|
)
|
|
)
|
|
ventes_litterature = []
|
|
for vente in ventes_lit_result.scalars().all():
|
|
item = await db.get(Litterature, vente.litterature_id)
|
|
vendeur = await db.get(Membre, vente.vendu_par) if vente.vendu_par else None
|
|
ventes_litterature.append({
|
|
"id": str(vente.id),
|
|
"titre": item.titre if item else "?",
|
|
"quantite": vente.quantite,
|
|
"montant": float(vente.montant),
|
|
"vendu_par_prenom": vendeur.prenom if vendeur else None,
|
|
"remis_tresorier": vente.remis_tresorier,
|
|
})
|
|
|
|
ventes_jetons_result = await db.execute(
|
|
select(VenteJeton).where(
|
|
VenteJeton.groupe_id == membre.groupe_id,
|
|
(VenteJeton.reunion_id == reunion.id) | (VenteJeton.date_vente == jour),
|
|
)
|
|
)
|
|
ventes_jetons = []
|
|
for vente in ventes_jetons_result.scalars().all():
|
|
vendeur = await db.get(Membre, vente.vendu_par) if vente.vendu_par else None
|
|
ventes_jetons.append({
|
|
"id": str(vente.id),
|
|
"type_jeton": vente.type_jeton,
|
|
"quantite": vente.quantite,
|
|
"montant": float(vente.montant),
|
|
"vendu_par_prenom": vendeur.prenom if vendeur else None,
|
|
"remis_tresorier": vente.remis_tresorier,
|
|
})
|
|
|
|
jetons_result = await db.execute(
|
|
select(Jeton).where(
|
|
Jeton.groupe_id == membre.groupe_id,
|
|
(Jeton.reunion_id == reunion.id) | (Jeton.date_remise == jour),
|
|
)
|
|
)
|
|
jetons = []
|
|
for jeton in jetons_result.scalars().all():
|
|
cible = await db.get(Membre, jeton.membre_id) if jeton.membre_id else None
|
|
jetons.append({
|
|
"id": str(jeton.id),
|
|
"type_jeton": jeton.type_jeton,
|
|
"membre_prenom": cible.prenom if cible else None,
|
|
"annees": jeton.annees,
|
|
})
|
|
|
|
rapport_adopte = await rapport_adopte_pour_reunion(db, membre.groupe_id, reunion.id, "rencontre")
|
|
rencontres.append({
|
|
"id": str(reunion.id),
|
|
"type": reunion.type,
|
|
"date_reunion": reunion.date_reunion,
|
|
"animateur_prenom": animateur.prenom if animateur else None,
|
|
"theme": reunion.theme,
|
|
"annonces": reunion.annonces,
|
|
"note": reunion.note,
|
|
"modifie_le": reunion.modifie_le,
|
|
"collecte": collecte_out,
|
|
"ventes_litterature": ventes_litterature,
|
|
"ventes_jetons": ventes_jetons,
|
|
"jetons": jetons,
|
|
"rapport_adopte_id": str(rapport_adopte.id) if rapport_adopte else None,
|
|
})
|
|
return rencontres
|
|
|
|
|
|
@router.patch("/{reunion_id}", response_model=ReunionOut)
|
|
async def modifier_reunion(
|
|
reunion_id: str,
|
|
req: ReunionCreate,
|
|
version_attendue: str | None = Header(default=None, alias="X-Resource-Version"),
|
|
membre=Depends(require_executif),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
result = await db.execute(
|
|
select(Reunion).where(Reunion.id == 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")
|
|
await verifier_reunion_non_cristallisee(
|
|
db, membre.groupe_id, reunion.id, "rencontre", "Cette rencontre est clôturée"
|
|
)
|
|
await verifier_version_historisee(db, reunion, version_attendue, table_cible="reunions")
|
|
avant = snapshot(reunion, "animateur_id", "theme", "annonces", "note")
|
|
animateur = None
|
|
if req.animateur_id:
|
|
animateur = await db.get(Membre, req.animateur_id)
|
|
if not animateur or animateur.groupe_id != membre.groupe_id or not animateur.actif:
|
|
raise HTTPException(status_code=404, detail="Animateur introuvable ou inactif")
|
|
reunion.animateur_id = animateur.id
|
|
reunion.theme = req.theme
|
|
reunion.annonces = req.annonces
|
|
reunion.note = req.note
|
|
anim = animateur or (await db.get(Membre, reunion.animateur_id) if reunion.animateur_id else None)
|
|
await enregistrer_historique(
|
|
db,
|
|
groupe_id=membre.groupe_id,
|
|
table_cible="reunions",
|
|
objet_id=reunion.id,
|
|
action="reunion.modifiee",
|
|
raison="Rencontre modifiée.",
|
|
membre_id=membre.id,
|
|
avant=avant,
|
|
apres=snapshot(reunion, "animateur_id", "theme", "annonces", "note"),
|
|
)
|
|
await db.flush()
|
|
return ReunionOut(
|
|
id=reunion.id,
|
|
type=reunion.type,
|
|
date_reunion=reunion.date_reunion,
|
|
animateur_prenom=anim.prenom if anim else None,
|
|
theme=reunion.theme,
|
|
annonces=reunion.annonces,
|
|
note=reunion.note,
|
|
cree_le=reunion.cree_le,
|
|
modifie_le=reunion.modifie_le,
|
|
)
|
|
|
|
|
|
@router.get("/{reunion_id}/rapport.pdf")
|
|
async def rapport_reunion_pdf(
|
|
reunion_id: str,
|
|
membre=Depends(get_current_membre),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
try:
|
|
from weasyprint import HTML
|
|
except ImportError:
|
|
raise HTTPException(status_code=500, detail="WeasyPrint non installé")
|
|
|
|
result = await db.execute(
|
|
select(Reunion).where(Reunion.id == 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")
|
|
adopte = await rapport_adopte_pour_reunion(db, membre.groupe_id, reunion.id, "rencontre")
|
|
if adopte:
|
|
pdf_bytes = HTML(string=adopte.contenu_html).write_pdf()
|
|
filename = f"rapport_rencontre_{reunion.date_reunion.date().isoformat()}.pdf"
|
|
return StreamingResponse(
|
|
io.BytesIO(pdf_bytes),
|
|
media_type="application/pdf",
|
|
headers={"Content-Disposition": f'inline; filename="{filename}"'},
|
|
)
|
|
html, jour = await _html_rapport_rencontre(reunion, membre, db)
|
|
pdf_bytes = HTML(string=html).write_pdf()
|
|
filename = f"rapport_rencontre_{jour.isoformat()}.pdf"
|
|
return StreamingResponse(
|
|
io.BytesIO(pdf_bytes),
|
|
media_type="application/pdf",
|
|
headers={"Content-Disposition": f'inline; filename="{filename}"'},
|
|
)
|
|
|
|
|
|
@router.post("/{reunion_id}/cloturer")
|
|
async def cloturer_rencontre(
|
|
reunion_id: str,
|
|
membre=Depends(require_executif),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Cristalliser le rapport d'une rencontre régulière."""
|
|
reunion = (await db.execute(
|
|
select(Reunion).where(
|
|
Reunion.id == reunion_id,
|
|
Reunion.groupe_id == membre.groupe_id,
|
|
Reunion.type == "reguliere",
|
|
)
|
|
)).scalar_one_or_none()
|
|
if not reunion:
|
|
raise HTTPException(status_code=404, detail="Rencontre introuvable")
|
|
if await rapport_adopte_pour_reunion(db, membre.groupe_id, reunion.id, "rencontre"):
|
|
raise HTTPException(status_code=400, detail="Cette rencontre est déjà clôturée")
|
|
html, jour = await _html_rapport_rencontre(reunion, membre, db)
|
|
rapport = RapportAdopte(
|
|
groupe_id=membre.groupe_id,
|
|
adopte_par_id=membre.id,
|
|
type_rapport="rencontre",
|
|
titre=f"Rapport de rencontre — {jour.isoformat()}",
|
|
annee=jour.year,
|
|
mois=jour.month,
|
|
contenu_html=html,
|
|
donnees={"reunion_id": str(reunion.id), "date": jour.isoformat()},
|
|
)
|
|
db.add(rapport)
|
|
await db.flush()
|
|
return {"id": str(rapport.id), "titre": rapport.titre, "adopte_le": rapport.adopte_le}
|
|
|
|
|
|
@router.get("/{reunion_id}", response_model=ReunionOut)
|
|
async def detail_reunion(
|
|
reunion_id: str,
|
|
membre=Depends(get_current_membre),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
result = await db.execute(
|
|
select(Reunion).where(Reunion.id == 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")
|
|
anim = await db.get(Membre, reunion.animateur_id) if reunion.animateur_id else None
|
|
return ReunionOut(
|
|
id=reunion.id,
|
|
type=reunion.type,
|
|
date_reunion=reunion.date_reunion,
|
|
animateur_prenom=anim.prenom if anim else None,
|
|
theme=reunion.theme,
|
|
annonces=reunion.annonces,
|
|
note=reunion.note,
|
|
cree_le=reunion.cree_le,
|
|
modifie_le=reunion.modifie_le,
|
|
)
|