2026-03-04 14:01:00 -05:00
|
|
|
"""Routes S4 — Rapport de réunion."""
|
Intégrer rencontres et assemblées pilotées par tuiles
Refond les pages Rencontres et Assemblées autour de tuiles ouvrables avec actions locales, PDF serveur, ventes, collectes, présences, décisions, postes et contributions.
Ajoute les invitations publiques d'assemblée en lecture seule avec lien expirant, l'adoption formelle du rapport du trésorier avec proposeur/secondeur, et bloque les contributions tant que ce rapport n'est pas adopté.
Uniformise la navigation et les entêtes, retire les modules redondants des contextes, ajoute la création de comptes membres par exécutif et les données démo reliées aux nouveaux processus.
Ce commit capture un lot fonctionnel important; une refactorisation ciblée du module Assemblées, puis Trésorerie/Gouvernance, est maintenant pertinente pour isoler les règles métier et faciliter les tests.
2026-05-31 09:02:02 -04:00
|
|
|
import io
|
|
|
|
|
from html import escape
|
2026-04-17 04:04:04 -04:00
|
|
|
from datetime import datetime, timezone
|
Intégrer rencontres et assemblées pilotées par tuiles
Refond les pages Rencontres et Assemblées autour de tuiles ouvrables avec actions locales, PDF serveur, ventes, collectes, présences, décisions, postes et contributions.
Ajoute les invitations publiques d'assemblée en lecture seule avec lien expirant, l'adoption formelle du rapport du trésorier avec proposeur/secondeur, et bloque les contributions tant que ce rapport n'est pas adopté.
Uniformise la navigation et les entêtes, retire les modules redondants des contextes, ajoute la création de comptes membres par exécutif et les données démo reliées aux nouveaux processus.
Ce commit capture un lot fonctionnel important; une refactorisation ciblée du module Assemblées, puis Trésorerie/Gouvernance, est maintenant pertinente pour isoler les règles métier et faciliter les tests.
2026-05-31 09:02:02 -04:00
|
|
|
from zoneinfo import ZoneInfo
|
2026-05-31 10:59:41 -04:00
|
|
|
from fastapi import APIRouter, Depends, Header, HTTPException
|
Intégrer rencontres et assemblées pilotées par tuiles
Refond les pages Rencontres et Assemblées autour de tuiles ouvrables avec actions locales, PDF serveur, ventes, collectes, présences, décisions, postes et contributions.
Ajoute les invitations publiques d'assemblée en lecture seule avec lien expirant, l'adoption formelle du rapport du trésorier avec proposeur/secondeur, et bloque les contributions tant que ce rapport n'est pas adopté.
Uniformise la navigation et les entêtes, retire les modules redondants des contextes, ajoute la création de comptes membres par exécutif et les données démo reliées aux nouveaux processus.
Ce commit capture un lot fonctionnel important; une refactorisation ciblée du module Assemblées, puis Trésorerie/Gouvernance, est maintenant pertinente pour isoler les règles métier et faciliter les tests.
2026-05-31 09:02:02 -04:00
|
|
|
from fastapi.responses import StreamingResponse
|
2026-03-04 14:01:00 -05:00
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
2026-04-17 04:04:04 -04:00
|
|
|
from sqlalchemy import select, desc, extract
|
2026-03-04 14:01:00 -05:00
|
|
|
|
|
|
|
|
from app.core.database import get_db
|
2026-05-25 17:39:38 -04:00
|
|
|
from app.core.reunions import jour_reunion_attendu
|
|
|
|
|
from app.core.security import get_current_membre, require_executif
|
2026-03-04 14:01:00 -05:00
|
|
|
from app.models.reunion import Reunion
|
Intégrer rencontres et assemblées pilotées par tuiles
Refond les pages Rencontres et Assemblées autour de tuiles ouvrables avec actions locales, PDF serveur, ventes, collectes, présences, décisions, postes et contributions.
Ajoute les invitations publiques d'assemblée en lecture seule avec lien expirant, l'adoption formelle du rapport du trésorier avec proposeur/secondeur, et bloque les contributions tant que ce rapport n'est pas adopté.
Uniformise la navigation et les entêtes, retire les modules redondants des contextes, ajoute la création de comptes membres par exécutif et les données démo reliées aux nouveaux processus.
Ce commit capture un lot fonctionnel important; une refactorisation ciblée du module Assemblées, puis Trésorerie/Gouvernance, est maintenant pertinente pour isoler les règles métier et faciliter les tests.
2026-05-31 09:02:02 -04:00
|
|
|
from app.models.collecte import Collecte
|
2026-04-17 04:04:04 -04:00
|
|
|
from app.models.groupe import Groupe
|
2026-03-04 14:01:00 -05:00
|
|
|
from app.models.membre import Membre
|
Intégrer rencontres et assemblées pilotées par tuiles
Refond les pages Rencontres et Assemblées autour de tuiles ouvrables avec actions locales, PDF serveur, ventes, collectes, présences, décisions, postes et contributions.
Ajoute les invitations publiques d'assemblée en lecture seule avec lien expirant, l'adoption formelle du rapport du trésorier avec proposeur/secondeur, et bloque les contributions tant que ce rapport n'est pas adopté.
Uniformise la navigation et les entêtes, retire les modules redondants des contextes, ajoute la création de comptes membres par exécutif et les données démo reliées aux nouveaux processus.
Ce commit capture un lot fonctionnel important; une refactorisation ciblée du module Assemblées, puis Trésorerie/Gouvernance, est maintenant pertinente pour isoler les règles métier et faciliter les tests.
2026-05-31 09:02:02 -04:00
|
|
|
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
|
2026-03-04 14:01:00 -05:00
|
|
|
from app.schemas.phase1 import ReunionCreate, ReunionOut
|
2026-05-31 10:59:41 -04:00
|
|
|
from app.services.historique import enregistrer_historique, verifier_version_historisee
|
2026-03-04 14:01:00 -05:00
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/reunions", tags=["S4 — Rapport de réunion"])
|
|
|
|
|
|
|
|
|
|
|
Intégrer rencontres et assemblées pilotées par tuiles
Refond les pages Rencontres et Assemblées autour de tuiles ouvrables avec actions locales, PDF serveur, ventes, collectes, présences, décisions, postes et contributions.
Ajoute les invitations publiques d'assemblée en lecture seule avec lien expirant, l'adoption formelle du rapport du trésorier avec proposeur/secondeur, et bloque les contributions tant que ce rapport n'est pas adopté.
Uniformise la navigation et les entêtes, retire les modules redondants des contextes, ajoute la création de comptes membres par exécutif et les données démo reliées aux nouveaux processus.
Ce commit capture un lot fonctionnel important; une refactorisation ciblée du module Assemblées, puis Trésorerie/Gouvernance, est maintenant pertinente pour isoler les règles métier et faciliter les tests.
2026-05-31 09:02:02 -04:00
|
|
|
def _date_civile_reunion(valeur: datetime):
|
|
|
|
|
if valeur.tzinfo:
|
|
|
|
|
return valeur.astimezone(ZoneInfo("America/Toronto")).date()
|
|
|
|
|
return valeur.date()
|
|
|
|
|
|
|
|
|
|
|
2026-03-04 14:01:00 -05:00
|
|
|
@router.post("/", response_model=ReunionOut)
|
|
|
|
|
async def creer_rapport_reunion(
|
|
|
|
|
req: ReunionCreate,
|
2026-05-25 17:39:38 -04:00
|
|
|
membre=Depends(require_executif),
|
2026-03-04 14:01:00 -05:00
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
"""S4 — L'animateur note le thème et les annonces."""
|
2026-04-17 04:04:04 -04:00
|
|
|
groupe = await db.get(Groupe, membre.groupe_id)
|
|
|
|
|
|
|
|
|
|
# ── Validation : réunion régulière ──
|
|
|
|
|
if req.type == "reguliere" and groupe.jour_reunion:
|
Intégrer rencontres et assemblées pilotées par tuiles
Refond les pages Rencontres et Assemblées autour de tuiles ouvrables avec actions locales, PDF serveur, ventes, collectes, présences, décisions, postes et contributions.
Ajoute les invitations publiques d'assemblée en lecture seule avec lien expirant, l'adoption formelle du rapport du trésorier avec proposeur/secondeur, et bloque les contributions tant que ce rapport n'est pas adopté.
Uniformise la navigation et les entêtes, retire les modules redondants des contextes, ajoute la création de comptes membres par exécutif et les données démo reliées aux nouveaux processus.
Ce commit capture un lot fonctionnel important; une refactorisation ciblée du module Assemblées, puis Trésorerie/Gouvernance, est maintenant pertinente pour isoler les règles métier et faciliter les tests.
2026-05-31 09:02:02 -04:00
|
|
|
date_civile = _date_civile_reunion(req.date_reunion)
|
2026-05-25 17:39:38 -04:00
|
|
|
jour_attendu = jour_reunion_attendu(groupe)
|
Intégrer rencontres et assemblées pilotées par tuiles
Refond les pages Rencontres et Assemblées autour de tuiles ouvrables avec actions locales, PDF serveur, ventes, collectes, présences, décisions, postes et contributions.
Ajoute les invitations publiques d'assemblée en lecture seule avec lien expirant, l'adoption formelle du rapport du trésorier avec proposeur/secondeur, et bloque les contributions tant que ce rapport n'est pas adopté.
Uniformise la navigation et les entêtes, retire les modules redondants des contextes, ajoute la création de comptes membres par exécutif et les données démo reliées aux nouveaux processus.
Ce commit capture un lot fonctionnel important; une refactorisation ciblée du module Assemblées, puis Trésorerie/Gouvernance, est maintenant pertinente pour isoler les règles métier et faciliter les tests.
2026-05-31 09:02:02 -04:00
|
|
|
jour_soumis = date_civile.weekday()
|
2026-04-17 04:04:04 -04:00
|
|
|
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
|
Intégrer rencontres et assemblées pilotées par tuiles
Refond les pages Rencontres et Assemblées autour de tuiles ouvrables avec actions locales, PDF serveur, ventes, collectes, présences, décisions, postes et contributions.
Ajoute les invitations publiques d'assemblée en lecture seule avec lien expirant, l'adoption formelle du rapport du trésorier avec proposeur/secondeur, et bloque les contributions tant que ce rapport n'est pas adopté.
Uniformise la navigation et les entêtes, retire les modules redondants des contextes, ajoute la création de comptes membres par exécutif et les données démo reliées aux nouveaux processus.
Ce commit capture un lot fonctionnel important; une refactorisation ciblée du module Assemblées, puis Trésorerie/Gouvernance, est maintenant pertinente pour isoler les règles métier et faciliter les tests.
2026-05-31 09:02:02 -04:00
|
|
|
iso_year, iso_week, _ = date_civile.isocalendar()
|
2026-04-17 04:04:04 -04:00
|
|
|
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}",
|
|
|
|
|
)
|
|
|
|
|
|
Intégrer rencontres et assemblées pilotées par tuiles
Refond les pages Rencontres et Assemblées autour de tuiles ouvrables avec actions locales, PDF serveur, ventes, collectes, présences, décisions, postes et contributions.
Ajoute les invitations publiques d'assemblée en lecture seule avec lien expirant, l'adoption formelle du rapport du trésorier avec proposeur/secondeur, et bloque les contributions tant que ce rapport n'est pas adopté.
Uniformise la navigation et les entêtes, retire les modules redondants des contextes, ajoute la création de comptes membres par exécutif et les données démo reliées aux nouveaux processus.
Ce commit capture un lot fonctionnel important; une refactorisation ciblée du module Assemblées, puis Trésorerie/Gouvernance, est maintenant pertinente pour isoler les règles métier et faciliter les tests.
2026-05-31 09:02:02 -04:00
|
|
|
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")
|
|
|
|
|
|
2026-03-04 14:01:00 -05:00
|
|
|
reunion = Reunion(
|
|
|
|
|
groupe_id=membre.groupe_id,
|
|
|
|
|
type=req.type,
|
|
|
|
|
date_reunion=req.date_reunion,
|
Intégrer rencontres et assemblées pilotées par tuiles
Refond les pages Rencontres et Assemblées autour de tuiles ouvrables avec actions locales, PDF serveur, ventes, collectes, présences, décisions, postes et contributions.
Ajoute les invitations publiques d'assemblée en lecture seule avec lien expirant, l'adoption formelle du rapport du trésorier avec proposeur/secondeur, et bloque les contributions tant que ce rapport n'est pas adopté.
Uniformise la navigation et les entêtes, retire les modules redondants des contextes, ajoute la création de comptes membres par exécutif et les données démo reliées aux nouveaux processus.
Ce commit capture un lot fonctionnel important; une refactorisation ciblée du module Assemblées, puis Trésorerie/Gouvernance, est maintenant pertinente pour isoler les règles métier et faciliter les tests.
2026-05-31 09:02:02 -04:00
|
|
|
animateur_id=animateur.id,
|
2026-03-04 14:01:00 -05:00
|
|
|
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,
|
Intégrer rencontres et assemblées pilotées par tuiles
Refond les pages Rencontres et Assemblées autour de tuiles ouvrables avec actions locales, PDF serveur, ventes, collectes, présences, décisions, postes et contributions.
Ajoute les invitations publiques d'assemblée en lecture seule avec lien expirant, l'adoption formelle du rapport du trésorier avec proposeur/secondeur, et bloque les contributions tant que ce rapport n'est pas adopté.
Uniformise la navigation et les entêtes, retire les modules redondants des contextes, ajoute la création de comptes membres par exécutif et les données démo reliées aux nouveaux processus.
Ce commit capture un lot fonctionnel important; une refactorisation ciblée du module Assemblées, puis Trésorerie/Gouvernance, est maintenant pertinente pour isoler les règles métier et faciliter les tests.
2026-05-31 09:02:02 -04:00
|
|
|
animateur_prenom=animateur.prenom,
|
2026-03-04 14:01:00 -05:00
|
|
|
theme=reunion.theme,
|
|
|
|
|
annonces=reunion.annonces,
|
|
|
|
|
note=reunion.note,
|
|
|
|
|
cree_le=reunion.cree_le,
|
2026-05-31 10:59:41 -04:00
|
|
|
modifie_le=reunion.modifie_le,
|
2026-03-04 14:01:00 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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,
|
2026-05-31 10:59:41 -04:00
|
|
|
modifie_le=r.modifie_le,
|
2026-03-04 14:01:00 -05:00
|
|
|
))
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
Intégrer rencontres et assemblées pilotées par tuiles
Refond les pages Rencontres et Assemblées autour de tuiles ouvrables avec actions locales, PDF serveur, ventes, collectes, présences, décisions, postes et contributions.
Ajoute les invitations publiques d'assemblée en lecture seule avec lien expirant, l'adoption formelle du rapport du trésorier avec proposeur/secondeur, et bloque les contributions tant que ce rapport n'est pas adopté.
Uniformise la navigation et les entêtes, retire les modules redondants des contextes, ajoute la création de comptes membres par exécutif et les données démo reliées aux nouveaux processus.
Ce commit capture un lot fonctionnel important; une refactorisation ciblée du module Assemblées, puis Trésorerie/Gouvernance, est maintenant pertinente pour isoler les règles métier et faciliter les tests.
2026-05-31 09:02:02 -04:00
|
|
|
@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,
|
2026-05-31 10:59:41 -04:00
|
|
|
"modifie_le": collecte.modifie_le,
|
Intégrer rencontres et assemblées pilotées par tuiles
Refond les pages Rencontres et Assemblées autour de tuiles ouvrables avec actions locales, PDF serveur, ventes, collectes, présences, décisions, postes et contributions.
Ajoute les invitations publiques d'assemblée en lecture seule avec lien expirant, l'adoption formelle du rapport du trésorier avec proposeur/secondeur, et bloque les contributions tant que ce rapport n'est pas adopté.
Uniformise la navigation et les entêtes, retire les modules redondants des contextes, ajoute la création de comptes membres par exécutif et les données démo reliées aux nouveaux processus.
Ce commit capture un lot fonctionnel important; une refactorisation ciblée du module Assemblées, puis Trésorerie/Gouvernance, est maintenant pertinente pour isoler les règles métier et faciliter les tests.
2026-05-31 09:02:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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,
|
2026-05-31 10:59:41 -04:00
|
|
|
"modifie_le": reunion.modifie_le,
|
Intégrer rencontres et assemblées pilotées par tuiles
Refond les pages Rencontres et Assemblées autour de tuiles ouvrables avec actions locales, PDF serveur, ventes, collectes, présences, décisions, postes et contributions.
Ajoute les invitations publiques d'assemblée en lecture seule avec lien expirant, l'adoption formelle du rapport du trésorier avec proposeur/secondeur, et bloque les contributions tant que ce rapport n'est pas adopté.
Uniformise la navigation et les entêtes, retire les modules redondants des contextes, ajoute la création de comptes membres par exécutif et les données démo reliées aux nouveaux processus.
Ce commit capture un lot fonctionnel important; une refactorisation ciblée du module Assemblées, puis Trésorerie/Gouvernance, est maintenant pertinente pour isoler les règles métier et faciliter les tests.
2026-05-31 09:02:02 -04:00
|
|
|
"collecte": collecte_out,
|
|
|
|
|
"ventes_litterature": ventes_litterature,
|
|
|
|
|
"ventes_jetons": ventes_jetons,
|
|
|
|
|
"jetons": jetons,
|
|
|
|
|
})
|
|
|
|
|
return rencontres
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch("/{reunion_id}", response_model=ReunionOut)
|
|
|
|
|
async def modifier_reunion(
|
|
|
|
|
reunion_id: str,
|
|
|
|
|
req: ReunionCreate,
|
2026-05-31 10:59:41 -04:00
|
|
|
version_attendue: str | None = Header(default=None, alias="X-Resource-Version"),
|
Intégrer rencontres et assemblées pilotées par tuiles
Refond les pages Rencontres et Assemblées autour de tuiles ouvrables avec actions locales, PDF serveur, ventes, collectes, présences, décisions, postes et contributions.
Ajoute les invitations publiques d'assemblée en lecture seule avec lien expirant, l'adoption formelle du rapport du trésorier avec proposeur/secondeur, et bloque les contributions tant que ce rapport n'est pas adopté.
Uniformise la navigation et les entêtes, retire les modules redondants des contextes, ajoute la création de comptes membres par exécutif et les données démo reliées aux nouveaux processus.
Ce commit capture un lot fonctionnel important; une refactorisation ciblée du module Assemblées, puis Trésorerie/Gouvernance, est maintenant pertinente pour isoler les règles métier et faciliter les tests.
2026-05-31 09:02:02 -04:00
|
|
|
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")
|
2026-05-31 10:59:41 -04:00
|
|
|
await verifier_version_historisee(db, reunion, version_attendue, table_cible="reunions")
|
Intégrer rencontres et assemblées pilotées par tuiles
Refond les pages Rencontres et Assemblées autour de tuiles ouvrables avec actions locales, PDF serveur, ventes, collectes, présences, décisions, postes et contributions.
Ajoute les invitations publiques d'assemblée en lecture seule avec lien expirant, l'adoption formelle du rapport du trésorier avec proposeur/secondeur, et bloque les contributions tant que ce rapport n'est pas adopté.
Uniformise la navigation et les entêtes, retire les modules redondants des contextes, ajoute la création de comptes membres par exécutif et les données démo reliées aux nouveaux processus.
Ce commit capture un lot fonctionnel important; une refactorisation ciblée du module Assemblées, puis Trésorerie/Gouvernance, est maintenant pertinente pour isoler les règles métier et faciliter les tests.
2026-05-31 09:02:02 -04:00
|
|
|
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)
|
2026-05-31 10:59:41 -04:00
|
|
|
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,
|
|
|
|
|
)
|
Intégrer rencontres et assemblées pilotées par tuiles
Refond les pages Rencontres et Assemblées autour de tuiles ouvrables avec actions locales, PDF serveur, ventes, collectes, présences, décisions, postes et contributions.
Ajoute les invitations publiques d'assemblée en lecture seule avec lien expirant, l'adoption formelle du rapport du trésorier avec proposeur/secondeur, et bloque les contributions tant que ce rapport n'est pas adopté.
Uniformise la navigation et les entêtes, retire les modules redondants des contextes, ajoute la création de comptes membres par exécutif et les données démo reliées aux nouveaux processus.
Ce commit capture un lot fonctionnel important; une refactorisation ciblée du module Assemblées, puis Trésorerie/Gouvernance, est maintenant pertinente pour isoler les règles métier et faciliter les tests.
2026-05-31 09:02:02 -04:00
|
|
|
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,
|
2026-05-31 10:59:41 -04:00
|
|
|
modifie_le=reunion.modifie_le,
|
Intégrer rencontres et assemblées pilotées par tuiles
Refond les pages Rencontres et Assemblées autour de tuiles ouvrables avec actions locales, PDF serveur, ventes, collectes, présences, décisions, postes et contributions.
Ajoute les invitations publiques d'assemblée en lecture seule avec lien expirant, l'adoption formelle du rapport du trésorier avec proposeur/secondeur, et bloque les contributions tant que ce rapport n'est pas adopté.
Uniformise la navigation et les entêtes, retire les modules redondants des contextes, ajoute la création de comptes membres par exécutif et les données démo reliées aux nouveaux processus.
Ce commit capture un lot fonctionnel important; une refactorisation ciblée du module Assemblées, puis Trésorerie/Gouvernance, est maintenant pertinente pour isoler les règles métier et faciliter les tests.
2026-05-31 09:02:02 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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")
|
|
|
|
|
|
|
|
|
|
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_html = "".join(f"<li>{escape(a)}</li>" for a in annonces) or "<li>Aucune annonce.</li>"
|
|
|
|
|
css = """
|
|
|
|
|
body { font-family: 'DejaVu Sans', sans-serif; font-size: 10.5pt; color: #27323a; margin: 1.6cm; }
|
|
|
|
|
.header { border-bottom: 3px solid #1a5276; padding-bottom: 12pt; margin-bottom: 18pt; }
|
|
|
|
|
.groupe { color: #1a5276; font-size: 20pt; font-weight: 800; }
|
|
|
|
|
.coords { color: #667; font-size: 9pt; margin-top: 3pt; }
|
|
|
|
|
h1 { font-size: 16pt; color: #1a5276; margin: 0 0 12pt; }
|
|
|
|
|
h2 { font-size: 12pt; color: #1a5276; margin: 16pt 0 6pt; border-bottom: 1px solid #d8e2ea; padding-bottom: 3pt; }
|
|
|
|
|
.meta { display: grid; grid-template-columns: 1fr 1fr; gap: 8pt; }
|
|
|
|
|
.box { border: 1px solid #d8e2ea; border-radius: 6pt; padding: 8pt; background: #f8fafc; }
|
|
|
|
|
.label { font-size: 8pt; color: #667; text-transform: uppercase; font-weight: 700; }
|
|
|
|
|
.value { font-size: 11pt; font-weight: 700; margin-top: 2pt; }
|
|
|
|
|
table { width: 100%; border-collapse: collapse; margin-top: 5pt; }
|
|
|
|
|
th { text-align: left; background: #1a5276; color: white; padding: 5pt; font-size: 8.5pt; }
|
|
|
|
|
td { border-bottom: 1px solid #e5e9ef; padding: 5pt; vertical-align: top; }
|
|
|
|
|
.montant { text-align: right; white-space: nowrap; }
|
|
|
|
|
.note { white-space: pre-wrap; }
|
|
|
|
|
.page-break { break-before: page; page-break-before: always; }
|
|
|
|
|
.footer { margin-top: 20pt; color: #889; font-size: 8pt; text-align: center; }
|
|
|
|
|
"""
|
|
|
|
|
groupe_nom = groupe.nom if groupe and groupe.nom else "Groupe"
|
|
|
|
|
coords = " · ".join([
|
|
|
|
|
item for item in [
|
|
|
|
|
groupe.adresse_local if groupe else None,
|
|
|
|
|
f"District {groupe.district}" if groupe and groupe.district else None,
|
|
|
|
|
f"Région {groupe.region}" if groupe and groupe.region else None,
|
|
|
|
|
] if item
|
|
|
|
|
])
|
|
|
|
|
ventes_lit_html = (
|
|
|
|
|
"<table><tr><th>Titre</th><th>Qté</th><th>Montant</th><th>Vendu par</th><th>Statut</th></tr>"
|
|
|
|
|
+ "".join(ventes_lit_rows) + "</table>"
|
|
|
|
|
if ventes_lit_rows else "<p>Aucune vente de littérature.</p>"
|
|
|
|
|
)
|
|
|
|
|
ventes_jetons_html = (
|
|
|
|
|
"<table><tr><th>Type</th><th>Qté</th><th>Montant</th><th>Vendu par</th><th>Statut</th></tr>"
|
|
|
|
|
+ "".join(ventes_jetons_rows) + "</table>"
|
|
|
|
|
if ventes_jetons_rows else "<p>Aucune vente de jetons.</p>"
|
|
|
|
|
)
|
|
|
|
|
jetons_html = (
|
|
|
|
|
"<table><tr><th>Type</th><th>Membre</th><th>Détail</th></tr>" + "".join(jetons_rows) + "</table>"
|
|
|
|
|
if jetons_rows else "<p>Aucun jeton/gâteau comptabilisé.</p>"
|
|
|
|
|
)
|
|
|
|
|
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="coords">{escape(coords)}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<h1>Rapport de rencontre</h1>
|
|
|
|
|
<div class="meta">
|
|
|
|
|
<div class="box"><div class="label">Date</div><div class="value">{reunion.date_reunion.date().isoformat()}</div></div>
|
|
|
|
|
<div class="box"><div class="label">Heure</div><div class="value">{reunion.date_reunion.strftime('%H:%M')}</div></div>
|
|
|
|
|
<div class="box"><div class="label">Sujet</div><div class="value">{escape(reunion.theme or 'Rencontre hebdomadaire')}</div></div>
|
|
|
|
|
<div class="box"><div class="label">Animé par</div><div class="value">{escape(animateur.prenom if animateur else '—')}</div></div>
|
|
|
|
|
</div>
|
|
|
|
|
<h2>Annonces</h2><ul>{annonces_html}</ul>
|
|
|
|
|
<h2>Notes</h2><div class="note">{escape(reunion.note or 'Aucune note.')}</div>
|
|
|
|
|
<h2>Collecte</h2>{collecte_html}
|
|
|
|
|
<div class="page-break"></div>
|
|
|
|
|
<h2>Ventes de littérature</h2>{ventes_lit_html}
|
|
|
|
|
<h2>Ventes de jetons</h2>{ventes_jetons_html}
|
|
|
|
|
<h2>Jetons/Gâteaux</h2>{jetons_html}
|
|
|
|
|
<div class="footer">Rapport généré automatiquement · {datetime.now().strftime('%Y-%m-%d %H:%M')}</div>
|
|
|
|
|
</body></html>"""
|
|
|
|
|
|
|
|
|
|
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}"'},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-03-04 14:01:00 -05:00
|
|
|
@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,
|
2026-05-31 10:59:41 -04:00
|
|
|
modifie_le=reunion.modifie_le,
|
2026-03-04 14:01:00 -05:00
|
|
|
)
|