groupe-meditation/backend/app/routers/accueil.py
Daniel Allaire 69309acd13 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

189 lines
8.7 KiB
Python

"""Route accueil — écran dynamique par rôle."""
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.core.database import get_db
from app.core.security import get_current_membre, is_sysadmin
from app.models.affectation import Affectation
from app.models.poste import Poste
from app.models.groupe import Groupe
from app.models.poste_module import PosteModule
from app.schemas.schemas import AccueilResponse, ModuleTuile, MembrePublic
router = APIRouter(prefix="/accueil", tags=["Accueil"])
# ── Définition des modules ──
MODULES_CONSULTATION = [
ModuleTuile(code="C1", nom="Rapports PDF", description="Finances, PV, résolutions", icone="📄", couleur="blue"),
ModuleTuile(code="C2", nom="Événements", description="Anniversaires et événements", icone="📅", couleur="green"),
ModuleTuile(code="C4", nom="Littérature", description="Inventaire et prix", icone="📚", couleur="purple"),
ModuleTuile(code="C5", nom="Gouvernance", description="Décisions du groupe", icone="📌", couleur="red"),
ModuleTuile(code="C7", nom="Mon profil", description="Modifier, PIN, notifications", icone="👤", couleur="gray"),
ModuleTuile(code="C9", nom="Postes à pourvoir", description="Postes disponibles, candidature", icone="🙋", couleur="yellow"),
ModuleTuile(code="S1T", nom="Trésorerie", description="État des résultats, soldes, registre", icone="🏦", couleur="green"),
]
MODULES_PAR_CODE = {m.code: m for m in MODULES_CONSULTATION}
MODULES_PAR_CODE["S0"] = ModuleTuile(code="S0", nom="Rencontres", description="Saisies, ventes, jetons et rapports", icone="🗓️", couleur="green")
MODULES_PAR_CODE["M0"] = ModuleTuile(code="M0", nom="Assemblées", description="Présences, procès-verbal et rapports", icone="🏛️", couleur="indigo")
MODULES_PAR_CODE["S1"] = ModuleTuile(code="S1", nom="Collecte 7e", description="Saisir le panier", icone="💰", couleur="green")
MODULES_SAISIE_TOUS = [
ModuleTuile(code="S5", nom="Dépense", description="Soumettre une dépense", icone="💸", couleur="red"),
]
# Modules de saisie par poste
MODULES_SAISIE = {
"Trésorier(ière)": [
ModuleTuile(code="M4", nom="Donner au suivant", description="Destinataires et contributions", icone="📮", couleur="blue"),
],
"Secrétaire": [
ModuleTuile(code="M1", nom="Rédiger PV", description="Procès-verbal", icone="📝", couleur="blue"),
ModuleTuile(code="M2", nom="Présences", description="Réunion d'affaires", icone="✅", couleur="green"),
],
"RSG": [
ModuleTuile(code="M3", nom="Rapport RSG", description="Compte-rendu district", icone="📋", couleur="blue"),
],
"RSG Substitut(e)": [
ModuleTuile(code="M3", nom="Rapport RSG", description="Compte-rendu district", icone="📋", couleur="blue"),
],
"Animateur(trice)": [],
"Responsable littérature": [
ModuleTuile(code="S3", nom="Vente littérature", description="Enregistrer une vente", icone="📖", couleur="purple"),
ModuleTuile(code="M5", nom="Ajuster inventaire", description="Mettre à jour le stock", icone="📦", couleur="orange"),
],
"Responsable jetons": [
ModuleTuile(code="S2", nom="Remise jeton", description="Enregistrer un jeton", icone="🏅", couleur="yellow"),
ModuleTuile(code="M6", nom="Commander jetons", description="Commander à l'Intergroupe", icone="📦", couleur="orange"),
],
}
MODULES_GESTION = [
ModuleTuile(code="G2", nom="Gestion des membres", description="Membres, invitations, désactivation", icone="👥", couleur="blue"),
ModuleTuile(code="G9", nom="Paramètres groupe", description="Nom, horaire, adresse", icone="🔧", couleur="gray"),
ModuleTuile(code="G11", nom="Gestion des mandats", description="Affecter, terminer, rotations", icone="🎯", couleur="purple"),
ModuleTuile(code="G12", nom="Gestion des postes", description="Postes, candidatures, accès", icone="📋", couleur="purple"),
ModuleTuile(code="G13", nom="Journal", description="Changements enregistrés", icone="🧾", couleur="gray"),
]
for _modules in [MODULES_SAISIE_TOUS, MODULES_GESTION, *MODULES_SAISIE.values()]:
for _m in _modules:
MODULES_PAR_CODE[_m.code] = _m
@router.get("/", response_model=AccueilResponse)
async def accueil(
membre=Depends(get_current_membre),
db: AsyncSession = Depends(get_db),
):
"""Écran d'accueil dynamique — retourne les modules selon les postes du membre."""
if is_sysadmin(membre):
modules_saisie_sysadmin = []
codes_vus = set()
for module in [MODULES_PAR_CODE["S0"], MODULES_PAR_CODE["M0"], *MODULES_SAISIE_TOUS, *[m for mods in MODULES_SAISIE.values() for m in mods]]:
if module.code not in codes_vus:
modules_saisie_sysadmin.append(module)
codes_vus.add(module.code)
groupe = await db.get(Groupe, membre.groupe_id)
groupe_info = None
if groupe:
groupe_info = {
"id": str(groupe.id),
"nom": groupe.nom,
"jour_reunion": groupe.jour_reunion,
"heure_reunion": groupe.heure_reunion.strftime("%H:%M") if groupe.heure_reunion else None,
"assemblee_affaires_ordre": groupe.assemblee_affaires_ordre,
"adresse_local": groupe.adresse_local,
}
return AccueilResponse(
membre=MembrePublic.model_validate(membre),
postes=["Sysadmin"],
est_executif=True,
modules_consultation=MODULES_CONSULTATION,
modules_saisie=modules_saisie_sysadmin,
modules_gestion=MODULES_GESTION,
groupe=groupe_info,
)
# Récupérer les affectations actives
result = await db.execute(
select(Affectation).join(Poste).where(
Affectation.membre_id == membre.id,
Affectation.statut == "actif",
)
)
affectations = result.scalars().all()
postes_noms = []
postes_categories = []
poste_ids = []
for a in affectations:
poste = await db.get(Poste, a.poste_id)
if poste:
postes_noms.append(poste.nom)
postes_categories.append(poste.categorie)
poste_ids.append(poste.id)
# Modules de consultation : toujours visibles
modules_consultation = MODULES_CONSULTATION
# Modules de saisie : collecte pour tous + modules par poste
modules_saisie = list(MODULES_SAISIE_TOUS)
codes_vus = {"S5"}
result = await db.execute(select(PosteModule).where(PosteModule.poste_id.in_(poste_ids))) if poste_ids else None
modules_configures = result.scalars().all() if result else []
if modules_configures:
codes = sorted({pm.module_code for pm in modules_configures})
for code in codes:
if code == "S1" and "executif" not in postes_categories:
continue
m = MODULES_PAR_CODE.get(code)
if m and not code.startswith("G") and m.code not in codes_vus:
modules_saisie.append(m)
codes_vus.add(m.code)
else:
for nom_poste in postes_noms:
for m in MODULES_SAISIE.get(nom_poste, []):
if m.code not in codes_vus:
modules_saisie.append(m)
codes_vus.add(m.code)
if "executif" in postes_categories and "S1" not in codes_vus:
modules_saisie.insert(0, MODULES_PAR_CODE["S0"])
modules_saisie.insert(1, MODULES_PAR_CODE["M0"])
codes_vus.add("S0")
codes_vus.add("M0")
# Modules de gestion : seulement si exécutif
if modules_configures:
modules_gestion = [
MODULES_PAR_CODE[code]
for code in sorted({pm.module_code for pm in modules_configures})
if code.startswith("G") and code in MODULES_PAR_CODE
]
else:
modules_gestion = MODULES_GESTION if "executif" in postes_categories else []
# Info groupe pour le frontend
groupe = await db.get(Groupe, membre.groupe_id)
groupe_info = None
if groupe:
groupe_info = {
"id": str(groupe.id),
"nom": groupe.nom,
"jour_reunion": groupe.jour_reunion,
"heure_reunion": groupe.heure_reunion.strftime("%H:%M") if groupe.heure_reunion else None,
"assemblee_affaires_ordre": groupe.assemblee_affaires_ordre,
"adresse_local": groupe.adresse_local,
}
return AccueilResponse(
membre=MembrePublic.model_validate(membre),
postes=postes_noms,
est_executif="executif" in postes_categories,
modules_consultation=modules_consultation,
modules_saisie=modules_saisie,
modules_gestion=modules_gestion,
groupe=groupe_info,
)