Ajuster gestion des groupes multi-groupes

This commit is contained in:
Daniel Allaire 2026-05-31 23:30:57 -04:00
parent 1946b8741e
commit ab6ba0c13e
14 changed files with 99 additions and 254 deletions

View file

@ -10,8 +10,8 @@ from app.services.admin_database import (
creer_groupe, creer_groupe,
initialiser_demo, initialiser_demo,
modifier_groupe, modifier_groupe,
reinitialiser_groupe,
reinitialiser_demo, reinitialiser_demo,
remettre_a_zero,
supprimer_groupe, supprimer_groupe,
) )
from app.models.groupe import Groupe from app.models.groupe import Groupe
@ -30,7 +30,6 @@ class SysadminLoginRequest(BaseModel):
class GroupeAdminRequest(BaseModel): class GroupeAdminRequest(BaseModel):
nom: str nom: str
district: str = "87-16" district: str = "87-16"
region: str = "87"
adresse_local: str | None = None adresse_local: str | None = None
jour_reunion: str | None = "mardi" jour_reunion: str | None = "mardi"
heure_reunion: str | None = "19:30" heure_reunion: str | None = "19:30"
@ -55,14 +54,6 @@ async def sysadmin_login(req: SysadminLoginRequest, db: AsyncSession = Depends(g
raise HTTPException(status_code=401, detail="PIN Sysadmin invalide") raise HTTPException(status_code=401, detail="PIN Sysadmin invalide")
@router.post("/reset")
async def reset_database(
_sysadmin=Depends(require_sysadmin),
db: AsyncSession = Depends(get_db),
):
return await remettre_a_zero(db)
@router.post("/demo") @router.post("/demo")
async def demo_database( async def demo_database(
_sysadmin=Depends(require_sysadmin), _sysadmin=Depends(require_sysadmin),
@ -79,13 +70,26 @@ async def reset_demo_database(
return await reinitialiser_demo(db) return await reinitialiser_demo(db)
@router.post("/groupes/{groupe_id}/reset")
async def reset_groupe_admin(
groupe_id: str,
_sysadmin=Depends(require_sysadmin),
db: AsyncSession = Depends(get_db),
):
try:
return await reinitialiser_groupe(db, groupe_id)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc))
@router.get("/groupes") @router.get("/groupes")
async def liste_groupes_admin( async def liste_groupes_admin(
_sysadmin=Depends(require_sysadmin), _sysadmin=Depends(require_sysadmin),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
await initialiser_demo(db) await initialiser_demo(db)
groupes = (await db.execute(select(Groupe).order_by(Groupe.nom))).scalars().all() groupes = (await db.execute(select(Groupe))).scalars().all()
groupes.sort(key=lambda g: (g.nom == "Groupe Démonstration", g.nom.lower()))
out = [] out = []
for groupe in groupes: for groupe in groupes:
nb_membres = (await db.execute( nb_membres = (await db.execute(
@ -101,7 +105,6 @@ async def liste_groupes_admin(
"id": str(groupe.id), "id": str(groupe.id),
"nom": groupe.nom, "nom": groupe.nom,
"district": groupe.district, "district": groupe.district,
"region": groupe.region,
"adresse_local": groupe.adresse_local, "adresse_local": groupe.adresse_local,
"jour_reunion": groupe.jour_reunion, "jour_reunion": groupe.jour_reunion,
"heure_reunion": groupe.heure_reunion.strftime("%H:%M") if groupe.heure_reunion else None, "heure_reunion": groupe.heure_reunion.strftime("%H:%M") if groupe.heure_reunion else None,
@ -112,8 +115,8 @@ async def liste_groupes_admin(
"membres": nb_membres, "membres": nb_membres,
"reunions": nb_reunions, "reunions": nb_reunions,
"traces": nb_traces, "traces": nb_traces,
"demo": groupe.nom == "Groupe Résilience", "demo": groupe.nom == "Groupe Démonstration",
"protege": groupe.nom == "Groupe Résilience", "protege": groupe.nom == "Groupe Démonstration",
}) })
return out return out

View file

@ -29,8 +29,9 @@ async def health(db: AsyncSession = Depends(get_db)):
@router.get("/groupes") @router.get("/groupes")
async def liste_groupes(db: AsyncSession = Depends(get_db)): async def liste_groupes(db: AsyncSession = Depends(get_db)):
await initialiser_demo(db) await initialiser_demo(db)
result = await db.execute(select(Groupe).order_by(Groupe.nom)) result = await db.execute(select(Groupe))
groupes = result.scalars().all() groupes = result.scalars().all()
groupes.sort(key=lambda g: (g.nom == "Groupe Démonstration", g.nom.lower()))
return [{ return [{
"id": str(g.id), "id": str(g.id),
"nom": g.nom, "nom": g.nom,

View file

@ -352,7 +352,6 @@ async def rapport_assemblee_pdf(
item for item in [ item for item in [
groupe.adresse_local if groupe else None, groupe.adresse_local if groupe else None,
f"District {groupe.district}" if groupe and groupe.district 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 ] if item
]) ])
html = f"""<!doctype html><html><head><meta charset="utf-8"><style>{css}</style></head><body> html = f"""<!doctype html><html><head><meta charset="utf-8"><style>{css}</style></head><body>

View file

@ -119,7 +119,7 @@ async def rapport_finances(
html_content = f"""<!DOCTYPE html><html><head><style>{_css()}</style></head><body> html_content = f"""<!DOCTYPE html><html><head><style>{_css()}</style></head><body>
<div class="header"> <div class="header">
<div class="groupe">{groupe.nom if groupe else 'Groupe'}</div> <div class="groupe">{groupe.nom if groupe else 'Groupe'}</div>
<div class="sub">District {groupe.district if groupe else '87-16'} · Région {groupe.region if groupe else '87'}</div> <div class="sub">District {groupe.district if groupe else '87-16'}</div>
</div> </div>
<h1>Rapport financier — {MOIS_FR[mois]} {annee}</h1> <h1>Rapport financier — {MOIS_FR[mois]} {annee}</h1>
@ -190,7 +190,7 @@ async def rapport_reunions(
html_content = f"""<!DOCTYPE html><html><head><style>{_css()}</style></head><body> html_content = f"""<!DOCTYPE html><html><head><style>{_css()}</style></head><body>
<div class="header"> <div class="header">
<div class="groupe">{groupe.nom if groupe else 'Groupe'}</div> <div class="groupe">{groupe.nom if groupe else 'Groupe'}</div>
<div class="sub">District {groupe.district if groupe else '87-16'} · Région {groupe.region if groupe else '87'}</div> <div class="sub">District {groupe.district if groupe else '87-16'}</div>
</div> </div>
<h1>Réunions — {MOIS_FR[mois]} {annee}</h1> <h1>Réunions — {MOIS_FR[mois]} {annee}</h1>
<table><tr><th>Date</th><th>Type</th><th>Animateur</th><th>Thème</th><th>Annonces</th></tr> <table><tr><th>Date</th><th>Type</th><th>Animateur</th><th>Thème</th><th>Annonces</th></tr>
@ -391,7 +391,7 @@ async def rapport_resolutions(
<style>.muted {{ color: #777; font-size: 8pt; }}</style> <style>.muted {{ color: #777; font-size: 8pt; }}</style>
<div class="header"> <div class="header">
<div class="groupe">{groupe.nom if groupe else 'Groupe'}</div> <div class="groupe">{groupe.nom if groupe else 'Groupe'}</div>
<div class="sub">District {groupe.district if groupe else '87-16'} · Région {groupe.region if groupe else '87'}</div> <div class="sub">District {groupe.district if groupe else '87-16'}</div>
</div> </div>
<h1>Registre des résolutions — {annee}</h1> <h1>Registre des résolutions — {annee}</h1>
<p>Décisions prises en Groupe</p> <p>Décisions prises en Groupe</p>
@ -760,12 +760,10 @@ async def _html_etat_resultats(
groupe_nom = groupe.nom if groupe and groupe.nom else "Groupe" groupe_nom = groupe.nom if groupe and groupe.nom else "Groupe"
district = str(groupe.district if groupe and groupe.district else "87-16") district = str(groupe.district if groupe and groupe.district else "87-16")
region = str(groupe.region if groupe and groupe.region else "87")
html_content = f"""<!DOCTYPE html><html><head><style>{css}</style></head><body> html_content = f"""<!DOCTYPE html><html><head><style>{css}</style></head><body>
<div class="header"> <div class="header">
<div class="groupe">{escape(groupe_nom)}</div> <div class="groupe">{escape(groupe_nom)}</div>
<div class="sub">District {escape(district)} · Région {escape(region)}</div> <div class="sub">District {escape(district)}</div>
</div> </div>
<h1>État des résultats — {MOIS_FR[mois]} {annee}</h1> <h1>État des résultats — {MOIS_FR[mois]} {annee}</h1>

View file

@ -394,7 +394,6 @@ async def rapport_reunion_pdf(
item for item in [ item for item in [
groupe.adresse_local if groupe else None, groupe.adresse_local if groupe else None,
f"District {groupe.district}" if groupe and groupe.district 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 ] if item
]) ])
ventes_lit_html = ( ventes_lit_html = (

View file

@ -40,6 +40,9 @@ from app.models.transaction_comptable import TransactionComptable
from app.models.vente_litterature import VenteLitterature from app.models.vente_litterature import VenteLitterature
from app.models.vente_jeton import VenteJeton from app.models.vente_jeton import VenteJeton
DEMO_GROUP_NAME = "Groupe Démonstration"
OLD_DEMO_GROUP_NAMES = ("Groupe Résilience",)
POSTES = [ POSTES = [
("RSG", "executif", "Représente le groupe auprès du district et rapporte les informations de service.", "2 ans de stabilité", 1, ["M3", "C2", "C5"]), ("RSG", "executif", "Représente le groupe auprès du district et rapporte les informations de service.", "2 ans de stabilité", 1, ["M3", "C2", "C5"]),
@ -106,8 +109,21 @@ async def remettre_a_zero(db: AsyncSession) -> dict:
} }
async def _groupe_demo(db: AsyncSession) -> Groupe | None:
result = await db.execute(select(Groupe).where(Groupe.nom.in_([DEMO_GROUP_NAME, *OLD_DEMO_GROUP_NAMES])))
groupe = result.scalars().first()
if groupe and groupe.nom != DEMO_GROUP_NAME:
groupe.nom = DEMO_GROUP_NAME
await db.flush()
return groupe
def _est_groupe_demo(groupe: Groupe | None) -> bool:
return bool(groupe and groupe.nom in {DEMO_GROUP_NAME, *OLD_DEMO_GROUP_NAMES})
async def initialiser_demo(db: AsyncSession) -> dict: async def initialiser_demo(db: AsyncSession) -> dict:
existant = (await db.execute(select(Groupe).where(Groupe.nom == "Groupe Résilience"))).scalar_one_or_none() existant = await _groupe_demo(db)
if existant: if existant:
membres_existants = await _membres_par_courriel(db, existant.id) membres_existants = await _membres_par_courriel(db, existant.id)
return { return {
@ -121,7 +137,7 @@ async def initialiser_demo(db: AsyncSession) -> dict:
groupe, sysadmin, postes, litterature = await _creer_base( groupe, sysadmin, postes, litterature = await _creer_base(
db, db,
nom_groupe="Groupe Résilience", nom_groupe=DEMO_GROUP_NAME,
adresse_local="Salle communautaire Saint-Louis, Montréal", adresse_local="Salle communautaire Saint-Louis, Montréal",
) )
membres = {sysadmin.courriel: sysadmin} membres = {sysadmin.courriel: sysadmin}
@ -141,7 +157,7 @@ async def initialiser_demo(db: AsyncSession) -> dict:
async def reinitialiser_demo(db: AsyncSession) -> dict: async def reinitialiser_demo(db: AsyncSession) -> dict:
groupe = (await db.execute(select(Groupe).where(Groupe.nom == "Groupe Résilience"))).scalar_one_or_none() groupe = await _groupe_demo(db)
if not groupe: if not groupe:
return await initialiser_demo(db) return await initialiser_demo(db)
await _vider_groupe(db, groupe.id) await _vider_groupe(db, groupe.id)
@ -161,16 +177,28 @@ async def reinitialiser_demo(db: AsyncSession) -> dict:
} }
async def reinitialiser_groupe(db: AsyncSession, groupe_id) -> dict:
groupe = await db.get(Groupe, groupe_id)
if not groupe:
raise ValueError("Groupe introuvable")
if _est_groupe_demo(groupe):
return await reinitialiser_demo(db)
await _vider_groupe(db, groupe.id)
await _peupler_groupe_base(db, groupe)
await db.flush()
return {"message": f"Groupe {groupe.nom} réinitialisé", "groupe_id": groupe.id}
async def creer_groupe(db: AsyncSession, data: dict) -> Groupe: async def creer_groupe(db: AsyncSession, data: dict) -> Groupe:
nom = (data.get("nom") or "").strip() nom = (data.get("nom") or "").strip()
if not nom: if not nom:
raise ValueError("Nom requis") raise ValueError("Nom requis")
if nom == "Groupe Résilience": if nom == DEMO_GROUP_NAME or nom in OLD_DEMO_GROUP_NAMES:
raise ValueError("Ce nom est réservé au groupe de démonstration") raise ValueError("Ce nom est réservé au groupe de démonstration")
groupe = Groupe( groupe = Groupe(
nom=nom, nom=nom,
district=(data.get("district") or "87-16").strip(), district=(data.get("district") or "87-16").strip(),
region=(data.get("region") or "87").strip(), region="",
adresse_local=(data.get("adresse_local") or "").strip() or None, adresse_local=(data.get("adresse_local") or "").strip() or None,
jour_reunion=(data.get("jour_reunion") or "mardi").strip() or None, jour_reunion=(data.get("jour_reunion") or "mardi").strip() or None,
heure_reunion=_parse_heure(data.get("heure_reunion")), heure_reunion=_parse_heure(data.get("heure_reunion")),
@ -190,12 +218,12 @@ async def modifier_groupe(db: AsyncSession, groupe_id, data: dict) -> Groupe:
if not groupe: if not groupe:
raise ValueError("Groupe introuvable") raise ValueError("Groupe introuvable")
nom = (data.get("nom") or groupe.nom).strip() nom = (data.get("nom") or groupe.nom).strip()
if groupe.nom == "Groupe Résilience" and nom != "Groupe Résilience": if _est_groupe_demo(groupe) and nom != DEMO_GROUP_NAME:
raise ValueError("Le groupe de démonstration ne peut pas être renommé") raise ValueError("Le groupe de démonstration ne peut pas être renommé")
if nom == "Groupe Résilience" and groupe.nom != "Groupe Résilience": if (nom == DEMO_GROUP_NAME or nom in OLD_DEMO_GROUP_NAMES) and not _est_groupe_demo(groupe):
raise ValueError("Ce nom est réservé au groupe de démonstration") raise ValueError("Ce nom est réservé au groupe de démonstration")
groupe.nom = nom groupe.nom = nom
for attr in ["district", "region", "adresse_local", "jour_reunion", "assemblee_affaires_ordre", "format_reunion", "lien_aa87"]: for attr in ["district", "adresse_local", "jour_reunion", "assemblee_affaires_ordre", "format_reunion", "lien_aa87"]:
if attr in data: if attr in data:
valeur = data.get(attr) valeur = data.get(attr)
setattr(groupe, attr, valeur.strip() if isinstance(valeur, str) and valeur.strip() else None) setattr(groupe, attr, valeur.strip() if isinstance(valeur, str) and valeur.strip() else None)
@ -209,7 +237,7 @@ async def supprimer_groupe(db: AsyncSession, groupe_id) -> dict:
groupe = await db.get(Groupe, groupe_id) groupe = await db.get(Groupe, groupe_id)
if not groupe: if not groupe:
raise ValueError("Groupe introuvable") raise ValueError("Groupe introuvable")
if groupe.nom == "Groupe Résilience": if _est_groupe_demo(groupe):
raise ValueError("Le groupe de démonstration est protégé") raise ValueError("Le groupe de démonstration est protégé")
nb_groupes = (await db.execute(select(Groupe))).scalars().all() nb_groupes = (await db.execute(select(Groupe))).scalars().all()
if len(nb_groupes) <= 1: if len(nb_groupes) <= 1:
@ -222,13 +250,13 @@ async def supprimer_groupe(db: AsyncSession, groupe_id) -> dict:
async def _creer_base( async def _creer_base(
db: AsyncSession, db: AsyncSession,
nom_groupe: str = "Groupe Méditation", nom_groupe: str = DEMO_GROUP_NAME,
adresse_local: str = "Salle communautaire, Montréal", adresse_local: str = "Salle communautaire, Montréal",
): ):
groupe = Groupe( groupe = Groupe(
nom=nom_groupe, nom=nom_groupe,
district="87-16", district="87-16",
region="87", region="",
adresse_local=adresse_local, adresse_local=adresse_local,
jour_reunion="mardi", jour_reunion="mardi",
heure_reunion=time(19, 30), heure_reunion=time(19, 30),

View file

@ -1,22 +1,16 @@
""" """
Script d'initialisation — Groupe Résilience Script d'initialisation.
Crée les postes, le groupe initial et le premier membre. Crée le schéma et le groupe de démonstration initial.
Usage: cd /opt/groupe-meditation/backend && venv/bin/python ../deploy/seed.py Usage: cd /opt/groupe-meditation/backend && venv/bin/python ../deploy/seed.py
""" """
import asyncio import asyncio
import sys import sys
import os import os
import uuid
import secrets
import csv
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
# Ajouter le backend au path # Ajouter le backend au path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "backend")) sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "backend"))
from app.core.database import engine, Base, async_session from app.core.database import engine, Base, async_session
from app.core.security import hash_pin
from app.services.admin_database import initialiser_demo from app.services.admin_database import initialiser_demo
# Importer TOUS les modèles pour que create_all crée toutes les tables # Importer TOUS les modèles pour que create_all crée toutes les tables
from app.models.groupe import Groupe from app.models.groupe import Groupe
@ -48,173 +42,20 @@ from app.models.mouvement_reserve import MouvementReserve
from app.models.destinataire_contribution import DestinataireContribution from app.models.destinataire_contribution import DestinataireContribution
POSTES = [
("RSG", "executif", "Représentant aux Services du Groupe", "730 days", "2 ans sobriété", 1),
("RSG Substitut(e)", "executif", "Remplace le RSG, se prépare à assumer le rôle", "730 days", "1 an sobriété", 2),
("Trésorier(ière)", "executif", "Gestion financière, 7e Tradition", "548 days", "1 an sobriété", 3),
("Secrétaire", "executif", "PV, correspondance, mémoire du groupe", "548 days", "6 mois sobriété", 4),
("Animateur(trice)", "service", "Anime les réunions régulières", None, "6 mois sobriété", 5),
("Responsable accueil", "physique", "Accueil des membres et nouveaux", "365 days", "Sobriété stable", 6),
("Responsable café", "physique", "Café, entretien de la salle", "365 days", "Aucun", 7),
("Responsable littérature", "service", "Inventaire et vente de littérature AA", "365 days", "Connaissance littérature AA", 8),
("Responsable jetons", "service", "Jetons et anniversaires de sobriété", "365 days", "Aucun", 9),
("Responsable clés", "physique", "Ouverture et fermeture du local", None, "Fiabilité", 10),
]
def charger_catalogue_litterature():
chemin = Path(__file__).resolve().parents[1] / "data" / "import" / "Liste-de-prix-rev.-Fevrier-2026.csv"
if not chemin.exists():
return [
{"titre": "Les Alcooliques anonymes (Gros Livre)", "categorie": "livre", "prix_coutant": 12.00, "code_bsg": "B-1"},
{"titre": "Les Douze Étapes et les Douze Traditions", "categorie": "livre", "prix_coutant": 11.00, "code_bsg": "B-2"},
{"titre": "Vivre sans alcool", "categorie": "livre", "prix_coutant": 9.00, "code_bsg": "B-7"},
{"titre": "Les AA sont-ils pour vous?", "categorie": "brochure", "prix_coutant": 0.00, "code_bsg": "P-3"},
{"titre": "Un nouveau arrive", "categorie": "depliant", "prix_coutant": 0.00, "code_bsg": "P-16"},
]
with chemin.open(newline="", encoding="utf-8") as fichier:
return [
{
"code_bsg": row["code_bsg"].strip() or None,
"titre": row["titre"].strip(),
"categorie": row["categorie"].strip(),
"prix_coutant": float(row["prix_coutant"]) if row["prix_coutant"].strip() else None,
}
for row in csv.DictReader(fichier)
if row.get("titre", "").strip()
]
async def seed(): async def seed():
# Créer les tables
async with engine.begin() as conn: async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all) await conn.run_sync(Base.metadata.create_all)
async with async_session() as db: async with async_session() as db:
# ── Créer le groupe ──
print("\n═══ Initialisation du Groupe Résilience ═══\n")
groupe = Groupe(
nom="Groupe Résilience",
district="87-16",
region="87",
adresse_local="Salle communautaire Saint-Louis, Montréal",
jour_reunion="mardi",
heure_reunion=datetime.strptime("19:30", "%H:%M").time(),
format_reunion="Discussion ouverte",
lien_aa87="https://aa87.org",
)
db.add(groupe)
await db.flush()
print(f"✓ Groupe créé: {groupe.nom} (district {groupe.district})")
# ── Créer les postes ──
postes_map = {}
for nom, cat, desc, duree, prereq, ordre in POSTES:
poste = Poste(
groupe_id=groupe.id,
nom=nom,
categorie=cat,
description=desc,
prerequis=prereq,
ordre_affichage=ordre,
)
db.add(poste)
await db.flush()
postes_map[nom] = poste
print(f" ✓ Poste: {nom} ({cat})")
# ── Premier membre ──
print("\n── Premier membre (vous) ──")
prenom = input(" Prénom: ").strip().title()
nom_membre = input(" Nom (optionnel, Entrée pour passer): ").strip()
nom_membre = nom_membre.title() if nom_membre else None
courriel = input(" Courriel: ").strip().lower()
while not courriel or ("@" not in courriel and courriel != "sysadmin"):
courriel = input(" Courriel invalide. Réessayer: ").strip().lower()
telephone = input(" Téléphone: ").strip()
while not telephone:
telephone = input(" Téléphone obligatoire: ").strip()
pin = input(" PIN (4 chiffres): ").strip()
while not pin.isdigit() or len(pin) != 4:
pin = input(" PIN invalide. 4 chiffres: ").strip()
poste_choisi = None
if courriel != "sysadmin":
print("\n Postes disponibles:")
for i, (nom_poste, _, _, _, _, _) in enumerate(POSTES[:4], 1):
print(f" {i}. {nom_poste}")
choix = input(" Votre poste (1-4): ").strip()
poste_idx = int(choix) - 1 if choix.isdigit() and 1 <= int(choix) <= 4 else 0
poste_choisi = POSTES[poste_idx][0]
membre = Membre(
groupe_id=groupe.id,
prenom=prenom,
nom=nom_membre,
courriel=courriel,
telephone=telephone,
pin_hash=hash_pin(pin),
)
db.add(membre)
await db.flush()
if poste_choisi:
affectation = Affectation(
groupe_id=groupe.id,
membre_id=membre.id,
poste_id=postes_map[poste_choisi].id,
date_debut=date.today(),
statut="actif",
)
db.add(affectation)
# ── Première invitation ──
code = secrets.token_hex(6).upper()
invitation = Invitation(
groupe_id=groupe.id,
code=code,
cree_par=membre.id,
expire_le=datetime.now(timezone.utc) + timedelta(days=7),
)
db.add(invitation)
# ── Littérature de base ──
catalogue_litterature = charger_catalogue_litterature()
for item in catalogue_litterature:
db.add(Litterature(
groupe_id=groupe.id,
titre=item["titre"],
categorie=item["categorie"],
prix_coutant=item["prix_coutant"],
quantite_stock=0,
seuil_commande=2,
code_bsg=item["code_bsg"],
))
print(f" ✓ Littérature de base : {len(catalogue_litterature)} titres")
db.add(DestinataireContribution(
groupe_id=groupe.id,
nom="District 87-16",
protege=True,
actif=True,
))
print(" ✓ Destinataire de contribution : District 87-16")
demo = await initialiser_demo(db) demo = await initialiser_demo(db)
print(f" ✓ Groupe de démonstration : {demo['message']}")
await db.commit() await db.commit()
print(f"\n═══ Initialisation terminée ═══") print("\n═══ Initialisation terminée ═══")
print(f" Membre: {prenom} ({courriel})") print(" Groupe: Groupe Démonstration")
print(f" Groupe: Groupe Méditation") print(f" {demo['message']}")
print(f" Poste: {poste_choisi or 'Sysadmin'}") print("\n Connectez-vous avec:")
print(f"\n Code d'invitation pour les prochains membres: {code}") print(" Identifiant: sysadmin")
print(f" (Expire dans 7 jours)") print(" PIN: 0000")
print(f"\n Connectez-vous sur l'app avec:")
print(f" Courriel: {courriel}")
print(f" PIN: {pin}")
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -2,7 +2,7 @@ import React from 'react';
import { useStore } from '../stores/store'; import { useStore } from '../stores/store';
export default function PageHeader({ titre, sousTitre, retour = true, action = null }) { export default function PageHeader({ titre, sousTitre, retour = true, action = null }) {
const { membre, setPage, logout } = useStore(); const { membre, groupe, setPage, logout } = useStore();
return ( return (
<div className="sticky top-0 z-30 bg-aa-blue text-white px-4 pt-4 pb-5 rounded-b-2xl shadow-lg"> <div className="sticky top-0 z-30 bg-aa-blue text-white px-4 pt-4 pb-5 rounded-b-2xl shadow-lg">
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
@ -17,7 +17,9 @@ export default function PageHeader({ titre, sousTitre, retour = true, action = n
</button> </button>
)} )}
<div className="min-w-0"> <div className="min-w-0">
<p className="text-blue-200 text-xs">Bonjour {membre?.prenom}</p> <p className="text-blue-200 text-xs truncate">
{groupe?.nom ? `${groupe.nom} · ` : ''}Bonjour {membre?.prenom}
</p>
<h1 className="text-xl font-bold leading-tight truncate">{titre}</h1> <h1 className="text-xl font-bold leading-tight truncate">{titre}</h1>
{sousTitre && <p className="text-sm text-blue-100 mt-1 truncate">{sousTitre}</p>} {sousTitre && <p className="text-sm text-blue-100 mt-1 truncate">{sousTitre}</p>}
</div> </div>

View file

@ -87,6 +87,7 @@ export default function Accueil() {
modulesGestion, modulesGestion,
navContext, navContext,
setPage, setPage,
groupe,
} = useStore(); } = useStore();
const [recherche, setRecherche] = useState(''); const [recherche, setRecherche] = useState('');
@ -126,7 +127,9 @@ export default function Accueil() {
<div className="bg-aa-blue text-white px-4 pt-4 pb-5 rounded-b-2xl shadow-lg"> <div className="bg-aa-blue text-white px-4 pt-4 pb-5 rounded-b-2xl shadow-lg">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0">
<p className="text-blue-200 text-xs">Bonjour {membre?.prenom}</p> <p className="text-blue-200 text-xs truncate">
{groupe?.nom ? `${groupe.nom} · ` : ''}Bonjour {membre?.prenom}
</p>
<h1 className="text-xl font-bold leading-tight">{contexte.titre}</h1> <h1 className="text-xl font-bold leading-tight">{contexte.titre}</h1>
<p className="text-sm text-blue-100 mt-1">{contexte.description}</p> <p className="text-sm text-blue-100 mt-1">{contexte.description}</p>
</div> </div>

View file

@ -4,7 +4,7 @@ import { useStore } from '../stores/store';
import PageHeader from '../components/PageHeader'; import PageHeader from '../components/PageHeader';
export default function GererMembres() { export default function GererMembres() {
const { membre, estSysadmin, impersoner } = useStore(); const { membre, groupe, estSysadmin, impersoner } = useStore();
const [membres, setMembres] = useState([]); const [membres, setMembres] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [detail, setDetail] = useState(null); const [detail, setDetail] = useState(null);
@ -54,7 +54,7 @@ export default function GererMembres() {
}; };
const partagerSMS = () => { const partagerSMS = () => {
const texte = `Rejoins le Groupe Méditation sur l'app. Ton code d'invitation : ${code}`; const texte = `Rejoins ${groupe?.nom || 'le groupe'} sur l'app. Ton code d'invitation : ${code}`;
window.open(`sms:?body=${encodeURIComponent(texte)}`); window.open(`sms:?body=${encodeURIComponent(texte)}`);
}; };

View file

@ -9,7 +9,7 @@ const jourLabel = (jour) => {
}; };
export default function GestionGroupes() { export default function GestionGroupes() {
const { loadGroupes, logout } = useStore(); const { loadGroupes } = useStore();
const [groupes, setGroupes] = useState([]); const [groupes, setGroupes] = useState([]);
const [ouvertId, setOuvertId] = useState(''); const [ouvertId, setOuvertId] = useState('');
const [editionId, setEditionId] = useState(''); const [editionId, setEditionId] = useState('');
@ -35,30 +35,11 @@ export default function GestionGroupes() {
useEffect(() => { charger(); }, []); useEffect(() => { charger(); }, []);
const resetDemo = async () => {
const confirmation = window.prompt('Cette action réinitialise seulement les données fictives du groupe de démonstration. Tapez CONFIRMER pour continuer.');
if (confirmation !== 'CONFIRMER') return;
setBusy(true);
setError('');
setMsg('');
try {
const res = await api.post('/admin/demo/reset');
setMsg(res.message || 'Groupe de démonstration réinitialisé');
await charger();
await loadGroupes();
} catch (e) {
setError(e.message);
} finally {
setBusy(false);
}
};
const nouveauGroupe = () => { const nouveauGroupe = () => {
setEditionId('nouveau'); setEditionId('nouveau');
setForm({ setForm({
nom: '', nom: '',
district: '87-16', district: '87-16',
region: '87',
adresse_local: '', adresse_local: '',
jour_reunion: 'mardi', jour_reunion: 'mardi',
heure_reunion: '19:30', heure_reunion: '19:30',
@ -73,7 +54,6 @@ export default function GestionGroupes() {
setForm({ setForm({
nom: groupe.nom || '', nom: groupe.nom || '',
district: groupe.district || '87-16', district: groupe.district || '87-16',
region: groupe.region || '87',
adresse_local: groupe.adresse_local || '', adresse_local: groupe.adresse_local || '',
jour_reunion: groupe.jour_reunion || 'mardi', jour_reunion: groupe.jour_reunion || 'mardi',
heure_reunion: groupe.heure_reunion || '19:30', heure_reunion: groupe.heure_reunion || '19:30',
@ -124,17 +104,20 @@ export default function GestionGroupes() {
} }
}; };
const remettreAZero = async () => { const reinitialiser = async (groupe) => {
const confirmation = window.prompt('Cette action efface toute la base et recrée seulement le compte Sysadmin. Tapez EFFACER pour continuer.'); const confirmation = window.prompt(`Réinitialiser les données du groupe "${groupe.nom}"? Tapez REINIT pour continuer.`);
if (confirmation !== 'EFFACER') return; if (confirmation !== 'REINIT') return;
setBusy(true); setBusy(true);
setError(''); setError('');
setMsg('');
try { try {
await api.post('/admin/reset'); const res = await api.post(`/admin/groupes/${groupe.id}/reset`);
alert('Base remise à zéro. Reconnectez-vous avec Sysadmin / 0000.'); setMsg(res.message || 'Groupe réinitialisé');
logout(); await charger();
await loadGroupes();
} catch (e) { } catch (e) {
setError(e.message); setError(e.message);
} finally {
setBusy(false); setBusy(false);
} }
}; };
@ -155,13 +138,6 @@ export default function GestionGroupes() {
> >
Créer un groupe Créer un groupe
</button> </button>
<button
onClick={remettreAZero}
disabled={busy}
className="w-full min-h-[48px] p-3 bg-red-50 border border-red-200 text-red-700 rounded-xl text-sm font-bold disabled:opacity-40"
>
Remettre toute la BD à vide
</button>
</div> </div>
{form && ( {form && (
@ -173,9 +149,8 @@ export default function GestionGroupes() {
<input value={form.jour_reunion} onChange={(e) => setForm({ ...form, jour_reunion: e.target.value })} placeholder="Jour" className="w-full p-3 bg-gray-50 rounded-xl border-0" /> <input value={form.jour_reunion} onChange={(e) => setForm({ ...form, jour_reunion: e.target.value })} placeholder="Jour" className="w-full p-3 bg-gray-50 rounded-xl border-0" />
<input type="time" value={form.heure_reunion} onChange={(e) => setForm({ ...form, heure_reunion: e.target.value })} className="w-full p-3 bg-gray-50 rounded-xl border-0" /> <input type="time" value={form.heure_reunion} onChange={(e) => setForm({ ...form, heure_reunion: e.target.value })} className="w-full p-3 bg-gray-50 rounded-xl border-0" />
</div> </div>
<div className="grid grid-cols-2 gap-2"> <div>
<input value={form.district} onChange={(e) => setForm({ ...form, district: e.target.value })} placeholder="District" className="w-full p-3 bg-gray-50 rounded-xl border-0" /> <input value={form.district} onChange={(e) => setForm({ ...form, district: e.target.value })} placeholder="District" className="w-full p-3 bg-gray-50 rounded-xl border-0" />
<input value={form.region} onChange={(e) => setForm({ ...form, region: e.target.value })} placeholder="Région" className="w-full p-3 bg-gray-50 rounded-xl border-0" />
</div> </div>
<select value={form.assemblee_affaires_ordre} onChange={(e) => setForm({ ...form, assemblee_affaires_ordre: e.target.value })} className="w-full p-3 bg-gray-50 rounded-xl border-0"> <select value={form.assemblee_affaires_ordre} onChange={(e) => setForm({ ...form, assemblee_affaires_ordre: e.target.value })} className="w-full p-3 bg-gray-50 rounded-xl border-0">
<option value="premiere">Assemblée la première réunion du mois</option> <option value="premiere">Assemblée la première réunion du mois</option>
@ -210,14 +185,12 @@ export default function GestionGroupes() {
<span className="block truncate text-lg font-bold text-gray-900">{groupe.nom}</span> <span className="block truncate text-lg font-bold text-gray-900">{groupe.nom}</span>
<span className="mt-1 block text-sm font-semibold text-aa-blue">{horaire}</span> <span className="mt-1 block text-sm font-semibold text-aa-blue">{horaire}</span>
</span> </span>
{groupe.demo && ( <span
<span onClick={(e) => { e.stopPropagation(); reinitialiser(groupe); }}
onClick={(e) => { e.stopPropagation(); resetDemo(); }} className="shrink-0 rounded-xl bg-indigo-50 px-3 py-2 text-xs font-bold text-indigo-700"
className="shrink-0 rounded-xl bg-indigo-50 px-3 py-2 text-xs font-bold text-indigo-700" >
> Réinit
Réinitialiser </span>
</span>
)}
<span className={`shrink-0 text-2xl text-aa-blue transition ${ouvert ? 'rotate-180' : ''}`}>⌄</span> <span className={`shrink-0 text-2xl text-aa-blue transition ${ouvert ? 'rotate-180' : ''}`}>⌄</span>
</button> </button>
@ -232,7 +205,6 @@ export default function GestionGroupes() {
{groupe.adresse_local && <p><span className="font-semibold text-gray-900">Lieu : </span>{groupe.adresse_local}</p>} {groupe.adresse_local && <p><span className="font-semibold text-gray-900">Lieu : </span>{groupe.adresse_local}</p>}
{groupe.format_reunion && <p><span className="font-semibold text-gray-900">Format : </span>{groupe.format_reunion}</p>} {groupe.format_reunion && <p><span className="font-semibold text-gray-900">Format : </span>{groupe.format_reunion}</p>}
<p><span className="font-semibold text-gray-900">District : </span>{groupe.district || '—'}</p> <p><span className="font-semibold text-gray-900">District : </span>{groupe.district || '—'}</p>
<p><span className="font-semibold text-gray-900">Région : </span>{groupe.region || '—'}</p>
</div> </div>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<button onClick={() => modifier(groupe)} className="p-3 bg-gray-100 text-gray-700 rounded-xl text-sm font-bold">Modifier</button> <button onClick={() => modifier(groupe)} className="p-3 bg-gray-100 text-gray-700 rounded-xl text-sm font-bold">Modifier</button>

View file

@ -21,7 +21,7 @@ export default function Inscription() {
<div className="min-h-screen bg-gradient-to-b from-aa-blue to-blue-900 flex flex-col items-center justify-center p-6"> <div className="min-h-screen bg-gradient-to-b from-aa-blue to-blue-900 flex flex-col items-center justify-center p-6">
<div className="text-center mb-6"> <div className="text-center mb-6">
<h1 className="text-xl font-bold text-white">Nouveau membre</h1> <h1 className="text-xl font-bold text-white">Nouveau membre</h1>
<p className="text-blue-200 text-sm">Groupe Méditation</p> <p className="text-blue-200 text-sm">Inscription au groupe</p>
</div> </div>
<form onSubmit={handleSubmit} className="w-full max-w-sm bg-white rounded-2xl shadow-xl p-6 space-y-3"> <form onSubmit={handleSubmit} className="w-full max-w-sm bg-white rounded-2xl shadow-xl p-6 space-y-3">

View file

@ -72,7 +72,7 @@ export default function Login() {
<div className="mx-auto flex min-h-screen w-full max-w-md flex-col py-6"> <div className="mx-auto flex min-h-screen w-full max-w-md flex-col py-6">
<div className="text-center mb-5"> <div className="text-center mb-5">
<h1 className="text-2xl font-bold text-white">Choisir un groupe</h1> <h1 className="text-2xl font-bold text-white">Choisir un groupe</h1>
<p className="text-blue-200 text-sm mt-1">District 87-16 · Région 87</p> <p className="text-blue-200 text-sm mt-1">District 87-16</p>
</div> </div>
<a <a

View file

@ -68,7 +68,6 @@ export default function Parametres() {
<> <>
<Field label="Nom du groupe" value={groupe.nom} /> <Field label="Nom du groupe" value={groupe.nom} />
<Field label="District" value={groupe.district} /> <Field label="District" value={groupe.district} />
<Field label="Région" value={groupe.region} />
<Field label="Adresse du local" value={groupe.adresse_local} /> <Field label="Adresse du local" value={groupe.adresse_local} />
<Field label="Jour de réunion" value={groupe.jour_reunion} /> <Field label="Jour de réunion" value={groupe.jour_reunion} />
<Field label="Heure" value={groupe.heure_reunion} /> <Field label="Heure" value={groupe.heure_reunion} />