District {groupe.district if groupe else '87-16'} · Région {groupe.region if groupe else '87'}
+
District {groupe.district if groupe else '87-16'}
Registre des résolutions — {annee}
Décisions prises en Groupe
@@ -760,12 +760,10 @@ async def _html_etat_resultats(
groupe_nom = groupe.nom if groupe and groupe.nom else "Groupe"
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"""
{escape(groupe_nom)}
-
District {escape(district)} · Région {escape(region)}
+
District {escape(district)}
État des résultats — {MOIS_FR[mois]} {annee}
diff --git a/backend/app/routers/reunions.py b/backend/app/routers/reunions.py
index 0900b0e..b616e7b 100644
--- a/backend/app/routers/reunions.py
+++ b/backend/app/routers/reunions.py
@@ -394,7 +394,6 @@ async def rapport_reunion_pdf(
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 = (
diff --git a/backend/app/services/admin_database.py b/backend/app/services/admin_database.py
index 39156fe..9b36ff2 100644
--- a/backend/app/services/admin_database.py
+++ b/backend/app/services/admin_database.py
@@ -40,6 +40,9 @@ from app.models.transaction_comptable import TransactionComptable
from app.models.vente_litterature import VenteLitterature
from app.models.vente_jeton import VenteJeton
+DEMO_GROUP_NAME = "Groupe Démonstration"
+OLD_DEMO_GROUP_NAMES = ("Groupe Résilience",)
+
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"]),
@@ -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:
- existant = (await db.execute(select(Groupe).where(Groupe.nom == "Groupe Résilience"))).scalar_one_or_none()
+ existant = await _groupe_demo(db)
if existant:
membres_existants = await _membres_par_courriel(db, existant.id)
return {
@@ -121,7 +137,7 @@ async def initialiser_demo(db: AsyncSession) -> dict:
groupe, sysadmin, postes, litterature = await _creer_base(
db,
- nom_groupe="Groupe Résilience",
+ nom_groupe=DEMO_GROUP_NAME,
adresse_local="Salle communautaire Saint-Louis, Montréal",
)
membres = {sysadmin.courriel: sysadmin}
@@ -141,7 +157,7 @@ async def initialiser_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:
return await initialiser_demo(db)
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:
nom = (data.get("nom") or "").strip()
if not nom:
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")
groupe = Groupe(
nom=nom,
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,
jour_reunion=(data.get("jour_reunion") or "mardi").strip() or None,
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:
raise ValueError("Groupe introuvable")
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é")
- 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")
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:
valeur = data.get(attr)
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)
if not groupe:
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é")
nb_groupes = (await db.execute(select(Groupe))).scalars().all()
if len(nb_groupes) <= 1:
@@ -222,13 +250,13 @@ async def supprimer_groupe(db: AsyncSession, groupe_id) -> dict:
async def _creer_base(
db: AsyncSession,
- nom_groupe: str = "Groupe Méditation",
+ nom_groupe: str = DEMO_GROUP_NAME,
adresse_local: str = "Salle communautaire, Montréal",
):
groupe = Groupe(
nom=nom_groupe,
district="87-16",
- region="87",
+ region="",
adresse_local=adresse_local,
jour_reunion="mardi",
heure_reunion=time(19, 30),
diff --git a/deploy/seed.py b/deploy/seed.py
index ca50417..a4644a5 100644
--- a/deploy/seed.py
+++ b/deploy/seed.py
@@ -1,22 +1,16 @@
"""
-Script d'initialisation — Groupe Résilience
-Crée les postes, le groupe initial et le premier membre.
+Script d'initialisation.
+Crée le schéma et le groupe de démonstration initial.
Usage: cd /opt/groupe-meditation/backend && venv/bin/python ../deploy/seed.py
"""
import asyncio
import sys
import os
-import uuid
-import secrets
-import csv
-from datetime import date, datetime, timedelta, timezone
-from pathlib import Path
# Ajouter le backend au path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "backend"))
from app.core.database import engine, Base, async_session
-from app.core.security import hash_pin
from app.services.admin_database import initialiser_demo
# Importer TOUS les modèles pour que create_all crée toutes les tables
from app.models.groupe import Groupe
@@ -48,173 +42,20 @@ from app.models.mouvement_reserve import MouvementReserve
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():
- # Créer les tables
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
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)
- print(f" ✓ Groupe de démonstration : {demo['message']}")
-
await db.commit()
- print(f"\n═══ Initialisation terminée ═══")
- print(f" Membre: {prenom} ({courriel})")
- print(f" Groupe: Groupe Méditation")
- print(f" Poste: {poste_choisi or 'Sysadmin'}")
- print(f"\n Code d'invitation pour les prochains membres: {code}")
- print(f" (Expire dans 7 jours)")
- print(f"\n Connectez-vous sur l'app avec:")
- print(f" Courriel: {courriel}")
- print(f" PIN: {pin}")
+ print("\n═══ Initialisation terminée ═══")
+ print(" Groupe: Groupe Démonstration")
+ print(f" {demo['message']}")
+ print("\n Connectez-vous avec:")
+ print(" Identifiant: sysadmin")
+ print(" PIN: 0000")
if __name__ == "__main__":
diff --git a/frontend/src/components/PageHeader.jsx b/frontend/src/components/PageHeader.jsx
index 8412039..578a296 100644
--- a/frontend/src/components/PageHeader.jsx
+++ b/frontend/src/components/PageHeader.jsx
@@ -2,7 +2,7 @@ import React from 'react';
import { useStore } from '../stores/store';
export default function PageHeader({ titre, sousTitre, retour = true, action = null }) {
- const { membre, setPage, logout } = useStore();
+ const { membre, groupe, setPage, logout } = useStore();
return (
@@ -17,7 +17,9 @@ export default function PageHeader({ titre, sousTitre, retour = true, action = n
)}