Ajuster gestion des groupes multi-groupes
This commit is contained in:
parent
1946b8741e
commit
ab6ba0c13e
14 changed files with 99 additions and 254 deletions
|
|
@ -10,8 +10,8 @@ from app.services.admin_database import (
|
|||
creer_groupe,
|
||||
initialiser_demo,
|
||||
modifier_groupe,
|
||||
reinitialiser_groupe,
|
||||
reinitialiser_demo,
|
||||
remettre_a_zero,
|
||||
supprimer_groupe,
|
||||
)
|
||||
from app.models.groupe import Groupe
|
||||
|
|
@ -30,7 +30,6 @@ class SysadminLoginRequest(BaseModel):
|
|||
class GroupeAdminRequest(BaseModel):
|
||||
nom: str
|
||||
district: str = "87-16"
|
||||
region: str = "87"
|
||||
adresse_local: str | None = None
|
||||
jour_reunion: str | None = "mardi"
|
||||
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")
|
||||
|
||||
|
||||
@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")
|
||||
async def demo_database(
|
||||
_sysadmin=Depends(require_sysadmin),
|
||||
|
|
@ -79,13 +70,26 @@ async def reset_demo_database(
|
|||
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")
|
||||
async def liste_groupes_admin(
|
||||
_sysadmin=Depends(require_sysadmin),
|
||||
db: AsyncSession = Depends(get_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 = []
|
||||
for groupe in groupes:
|
||||
nb_membres = (await db.execute(
|
||||
|
|
@ -101,7 +105,6 @@ async def liste_groupes_admin(
|
|||
"id": str(groupe.id),
|
||||
"nom": groupe.nom,
|
||||
"district": groupe.district,
|
||||
"region": groupe.region,
|
||||
"adresse_local": groupe.adresse_local,
|
||||
"jour_reunion": groupe.jour_reunion,
|
||||
"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,
|
||||
"reunions": nb_reunions,
|
||||
"traces": nb_traces,
|
||||
"demo": groupe.nom == "Groupe Résilience",
|
||||
"protege": groupe.nom == "Groupe Résilience",
|
||||
"demo": groupe.nom == "Groupe Démonstration",
|
||||
"protege": groupe.nom == "Groupe Démonstration",
|
||||
})
|
||||
return out
|
||||
|
||||
|
|
|
|||
|
|
@ -29,8 +29,9 @@ async def health(db: AsyncSession = Depends(get_db)):
|
|||
@router.get("/groupes")
|
||||
async def liste_groupes(db: AsyncSession = Depends(get_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.sort(key=lambda g: (g.nom == "Groupe Démonstration", g.nom.lower()))
|
||||
return [{
|
||||
"id": str(g.id),
|
||||
"nom": g.nom,
|
||||
|
|
|
|||
|
|
@ -352,7 +352,6 @@ async def rapport_assemblee_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
|
||||
])
|
||||
html = f"""<!doctype html><html><head><meta charset="utf-8"><style>{css}</style></head><body>
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ async def rapport_finances(
|
|||
html_content = f"""<!DOCTYPE html><html><head><style>{_css()}</style></head><body>
|
||||
<div class="header">
|
||||
<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>
|
||||
<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>
|
||||
<div class="header">
|
||||
<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>
|
||||
<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>
|
||||
|
|
@ -391,7 +391,7 @@ async def rapport_resolutions(
|
|||
<style>.muted {{ color: #777; font-size: 8pt; }}</style>
|
||||
<div class="header">
|
||||
<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>
|
||||
<h1>Registre des résolutions — {annee}</h1>
|
||||
<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"
|
||||
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>
|
||||
<div class="header">
|
||||
<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>
|
||||
<h1>État des résultats — {MOIS_FR[mois]} {annee}</h1>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
175
deploy/seed.py
175
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__":
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<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">
|
||||
|
|
@ -17,7 +17,9 @@ export default function PageHeader({ titre, sousTitre, retour = true, action = n
|
|||
</button>
|
||||
)}
|
||||
<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>
|
||||
{sousTitre && <p className="text-sm text-blue-100 mt-1 truncate">{sousTitre}</p>}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ export default function Accueil() {
|
|||
modulesGestion,
|
||||
navContext,
|
||||
setPage,
|
||||
groupe,
|
||||
} = useStore();
|
||||
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="flex items-center justify-between gap-3">
|
||||
<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>
|
||||
<p className="text-sm text-blue-100 mt-1">{contexte.description}</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useStore } from '../stores/store';
|
|||
import PageHeader from '../components/PageHeader';
|
||||
|
||||
export default function GererMembres() {
|
||||
const { membre, estSysadmin, impersoner } = useStore();
|
||||
const { membre, groupe, estSysadmin, impersoner } = useStore();
|
||||
const [membres, setMembres] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [detail, setDetail] = useState(null);
|
||||
|
|
@ -54,7 +54,7 @@ export default function GererMembres() {
|
|||
};
|
||||
|
||||
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)}`);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const jourLabel = (jour) => {
|
|||
};
|
||||
|
||||
export default function GestionGroupes() {
|
||||
const { loadGroupes, logout } = useStore();
|
||||
const { loadGroupes } = useStore();
|
||||
const [groupes, setGroupes] = useState([]);
|
||||
const [ouvertId, setOuvertId] = useState('');
|
||||
const [editionId, setEditionId] = useState('');
|
||||
|
|
@ -35,30 +35,11 @@ export default function GestionGroupes() {
|
|||
|
||||
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 = () => {
|
||||
setEditionId('nouveau');
|
||||
setForm({
|
||||
nom: '',
|
||||
district: '87-16',
|
||||
region: '87',
|
||||
adresse_local: '',
|
||||
jour_reunion: 'mardi',
|
||||
heure_reunion: '19:30',
|
||||
|
|
@ -73,7 +54,6 @@ export default function GestionGroupes() {
|
|||
setForm({
|
||||
nom: groupe.nom || '',
|
||||
district: groupe.district || '87-16',
|
||||
region: groupe.region || '87',
|
||||
adresse_local: groupe.adresse_local || '',
|
||||
jour_reunion: groupe.jour_reunion || 'mardi',
|
||||
heure_reunion: groupe.heure_reunion || '19:30',
|
||||
|
|
@ -124,17 +104,20 @@ export default function GestionGroupes() {
|
|||
}
|
||||
};
|
||||
|
||||
const remettreAZero = async () => {
|
||||
const confirmation = window.prompt('Cette action efface toute la base et recrée seulement le compte Sysadmin. Tapez EFFACER pour continuer.');
|
||||
if (confirmation !== 'EFFACER') return;
|
||||
const reinitialiser = async (groupe) => {
|
||||
const confirmation = window.prompt(`Réinitialiser les données du groupe "${groupe.nom}"? Tapez REINIT pour continuer.`);
|
||||
if (confirmation !== 'REINIT') return;
|
||||
setBusy(true);
|
||||
setError('');
|
||||
setMsg('');
|
||||
try {
|
||||
await api.post('/admin/reset');
|
||||
alert('Base remise à zéro. Reconnectez-vous avec Sysadmin / 0000.');
|
||||
logout();
|
||||
const res = await api.post(`/admin/groupes/${groupe.id}/reset`);
|
||||
setMsg(res.message || 'Groupe réinitialisé');
|
||||
await charger();
|
||||
await loadGroupes();
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
|
@ -155,13 +138,6 @@ export default function GestionGroupes() {
|
|||
>
|
||||
Créer un groupe
|
||||
</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>
|
||||
|
||||
{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 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 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.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>
|
||||
<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>
|
||||
|
|
@ -210,14 +185,12 @@ export default function GestionGroupes() {
|
|||
<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>
|
||||
{groupe.demo && (
|
||||
<span
|
||||
onClick={(e) => { e.stopPropagation(); resetDemo(); }}
|
||||
onClick={(e) => { e.stopPropagation(); reinitialiser(groupe); }}
|
||||
className="shrink-0 rounded-xl bg-indigo-50 px-3 py-2 text-xs font-bold text-indigo-700"
|
||||
>
|
||||
Réinitialiser
|
||||
Réinit
|
||||
</span>
|
||||
)}
|
||||
<span className={`shrink-0 text-2xl text-aa-blue transition ${ouvert ? 'rotate-180' : ''}`}>⌄</span>
|
||||
</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.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">Région : </span>{groupe.region || '—'}</p>
|
||||
</div>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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="text-center mb-6">
|
||||
<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>
|
||||
|
||||
<form onSubmit={handleSubmit} className="w-full max-w-sm bg-white rounded-2xl shadow-xl p-6 space-y-3">
|
||||
|
|
|
|||
|
|
@ -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="text-center mb-5">
|
||||
<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>
|
||||
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@ export default function Parametres() {
|
|||
<>
|
||||
<Field label="Nom du groupe" value={groupe.nom} />
|
||||
<Field label="District" value={groupe.district} />
|
||||
<Field label="Région" value={groupe.region} />
|
||||
<Field label="Adresse du local" value={groupe.adresse_local} />
|
||||
<Field label="Jour de réunion" value={groupe.jour_reunion} />
|
||||
<Field label="Heure" value={groupe.heure_reunion} />
|
||||
|
|
|
|||
Loading…
Reference in a new issue