diff --git a/README.md b/README.md
index 32270ec..140af9d 100644
--- a/README.md
+++ b/README.md
@@ -52,7 +52,7 @@ Application mobile-first pour la gestion des groupes AA. 1 module = 1 tâche. 27
| DB | PostgreSQL 16 + SQLAlchemy async |
| Auth | JWT 30 jours + bcrypt 12 rounds |
| PDF | WeasyPrint |
-| Serveur | Apache (statiques) + NGINX (reverse proxy HTTPS) |
+| Serveur | NGINX (statiques + reverse proxy HTTPS) |
## Principes
diff --git a/backend/app/main.py b/backend/app/main.py
index 9ebe70b..ebba9ef 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -11,6 +11,9 @@ from app.core.config import settings
from app.core.database import engine, Base
from app.routers import auth, membres, postes, accueil, invitations, collectes, reunions, depenses, decisions, calendrier, rapports, rotations, parametres, presences, pv, rapports_rsg, jetons, anniversaires, commandes_jetons, litterature, ventes_litterature, contributions, config_repartition, sondages, jumelages
+# Import sync_log pour que create_all crée la table (aucun router ne l'importe)
+from app.models.sync_log import SyncLog # noqa: F401
+
@asynccontextmanager
async def lifespan(app: FastAPI):
diff --git a/deploy/apache-groupe-meditation.conf b/deploy/apache-groupe-meditation.conf
deleted file mode 100644
index 5afc064..0000000
--- a/deploy/apache-groupe-meditation.conf
+++ /dev/null
@@ -1,28 +0,0 @@
-
- ServerName groupe-meditation.chezlepro.ca
- DocumentRoot /var/www/groupe-meditation
-
-
- Options -Indexes
- AllowOverride None
- Require all granted
-
- # SPA: rediriger toutes les routes vers index.html
- RewriteEngine On
- RewriteBase /
- RewriteRule ^index\.html$ - [L]
- RewriteCond %{REQUEST_FILENAME} !-f
- RewriteCond %{REQUEST_FILENAME} !-d
- RewriteRule . /index.html [L]
-
-
- # Cache assets statiques
-
- Header set Cache-Control "public, max-age=31536000, immutable"
-
-
- # Pas de cache pour index.html (PWA)
-
- Header set Cache-Control "no-cache, no-store, must-revalidate"
-
-
diff --git a/deploy/nginx-groupe-meditation.conf b/deploy/nginx-groupe-meditation.conf
index 27dc6b8..8740d2f 100644
--- a/deploy/nginx-groupe-meditation.conf
+++ b/deploy/nginx-groupe-meditation.conf
@@ -1,32 +1,23 @@
-# Groupe Méditation — Configuration NGINX
-# Reverse proxy vers Apache (frontend) et FastAPI (backend)
+# Groupe Méditation — NGINX (seul, sans Apache)
+# Sert les fichiers statiques + reverse proxy vers FastAPI
+#
+# Installation :
+# sudo cp deploy/nginx-groupe-meditation.conf /etc/nginx/sites-available/groupe-meditation
+# sudo ln -s /etc/nginx/sites-available/groupe-meditation /etc/nginx/sites-enabled/
+# sudo nginx -t && sudo systemctl reload nginx
+# sudo certbot --nginx -d groupe-meditation.chezlepro.ca
server {
- listen 443 ssl http2;
+ listen 80;
server_name groupe-meditation.chezlepro.ca;
- # Certificat Let's Encrypt
- ssl_certificate /etc/letsencrypt/live/groupe-meditation.chezlepro.ca/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/groupe-meditation.chezlepro.ca/privkey.pem;
-
- # Headers de sécurité
- add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
+ # ── Headers de sécurité ──
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
- add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' fonts.googleapis.com; font-src fonts.gstatic.com; img-src 'self' data:; connect-src 'self'" always;
- # Frontend — Apache sert les fichiers statiques
- location / {
- proxy_pass http://127.0.0.1:8080;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- }
-
- # Backend API — FastAPI via Uvicorn
+ # ── Backend API → FastAPI (Uvicorn sur port 8000) ──
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
@@ -35,16 +26,31 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
# Rate limiting anti-brute force (PIN)
- limit_req zone=api_limit burst=10 nodelay;
+ # Décommenter après avoir ajouté dans nginx.conf section http :
+ # limit_req_zone $binary_remote_addr zone=api_limit:10m rate=5r/s;
+ # limit_req zone=api_limit burst=10 nodelay;
+ }
+
+ # ── Frontend — fichiers statiques (React build) ──
+ root /var/www/groupe-meditation;
+ index index.html;
+
+ # Cache longue durée sur les assets (JS, CSS, images)
+ location ~* \.(js|css|png|jpg|svg|woff2|ico)$ {
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ try_files $uri =404;
+ }
+
+ # SPA routing : toute route qui n'est pas un fichier → index.html
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ # Pas de cache sur index.html (pour que les mises à jour PWA passent)
+ location = /index.html {
+ add_header Cache-Control "no-cache, no-store, must-revalidate";
}
- # Taille max des uploads
client_max_body_size 5M;
}
-
-# Redirection HTTP → HTTPS
-server {
- listen 80;
- server_name groupe-meditation.chezlepro.ca;
- return 301 https://$host$request_uri;
-}
diff --git a/docs/DEPLOIEMENT.md b/docs/DEPLOIEMENT.md
index 2b1c664..bc5d4b1 100644
--- a/docs/DEPLOIEMENT.md
+++ b/docs/DEPLOIEMENT.md
@@ -1,6 +1,6 @@
# Guide de déploiement — Groupe Méditation
-## 27 modules · Phases 0 à 9 · Infrastructure Debian + Apache + NGINX + PostgreSQL
+## 27 modules · Phases 0 à 9 · Infrastructure Debian + NGINX + PostgreSQL
---
@@ -10,8 +10,7 @@
- PostgreSQL 16+
- Python 3.11+
- Node.js 20+ (pour le build frontend)
-- Apache 2.4+ avec mod_rewrite et mod_headers
-- NGINX 1.24+ (reverse proxy)
+- NGINX 1.24+
- Certbot (Let's Encrypt)
---
@@ -54,17 +53,6 @@ source venv/bin/activate
pip install -r requirements.txt
```
-### Dépendances Python (requirements.txt)
-
-| Paquet | Rôle |
-|--------|------|
-| fastapi | API REST async |
-| uvicorn | Serveur ASGI |
-| sqlalchemy[asyncio] + asyncpg | ORM + driver PostgreSQL |
-| pydantic-settings | Configuration .env |
-| python-jose + bcrypt | JWT + hachage PIN |
-| weasyprint | Génération PDF (rapports C1) |
-
## 3. Configurer le backend
```bash
@@ -95,10 +83,10 @@ python deploy/seed.py
```
Le script crée **interactivement** :
-- Les 23 tables (groupes, membres, postes, affectations, collectes, dépenses, réunions, présences, PV, décisions, rotations, rapports RSG, jetons, littérature, ventes, contributions, config_repartition, sondages, options_sondage, votes, jumelages, sync_log)
-- Les 10 postes prédéfinis (RSG, RSG Substitut, Trésorier, Secrétaire, Animateur, Accueil, Café, Littérature, Jetons, Clés)
+- Les 23 tables
+- Les 10 postes prédéfinis
- La répartition par défaut des contributions (30/30/30/10)
-- La littérature de base (Gros Livre, 12×12, Vivre sans alcool, brochures)
+- La littérature de base (5 titres)
- Le premier groupe et le premier membre avec son poste
- Un code d'invitation valide 7 jours
@@ -115,17 +103,6 @@ sudo systemctl start groupe-meditation
curl http://127.0.0.1:8000/api/health
```
-Le service expose 25 routers API sur le port 8000 :
-- Auth (login, register)
-- Membres, postes, accueil, invitations
-- Collectes, réunions, dépenses, décisions
-- Calendrier, rapports PDF, rotations, paramètres
-- Présences, PV, rapports RSG
-- Jetons, anniversaires, commandes jetons
-- Littérature, ventes littérature
-- Contributions, config répartition
-- Sondages (votes), jumelages (parrainage)
-
## 6. Construire le frontend
```bash
@@ -138,47 +115,27 @@ sudo cp -r dist/* /var/www/groupe-meditation/
sudo chown -R www-data:www-data /var/www/groupe-meditation
```
-Le build produit une PWA installable avec :
-- 30 pages React (27 modules + Login + Inscription + Accueil)
-- Tailwind CSS mobile-first
-- Service Worker pour le mode hors ligne
+## 7. Configurer NGINX
-## 7. Configurer Apache
-
-```bash
-sudo cp deploy/apache-groupe-meditation.conf /etc/apache2/sites-available/
-sudo a2enmod rewrite headers
-sudo a2ensite groupe-meditation
-sudo systemctl reload apache2
-```
-
-Apache écoute sur 127.0.0.1:8080 et sert :
-- Les fichiers statiques du build React
-- Le SPA routing (toutes les routes → index.html)
-- Le cache immutable sur les assets (JS, CSS, images)
-
-## 8. Configurer NGINX
+NGINX fait tout : servir les fichiers statiques du frontend ET proxy vers FastAPI. Pas besoin d'Apache.
```bash
sudo cp deploy/nginx-groupe-meditation.conf /etc/nginx/sites-available/groupe-meditation
sudo ln -s /etc/nginx/sites-available/groupe-meditation /etc/nginx/sites-enabled/
-# Ajouter dans /etc/nginx/nginx.conf (section http) :
-# limit_req_zone $binary_remote_addr zone=api_limit:10m rate=5r/s;
-
-# Obtenir le certificat Let's Encrypt
-sudo certbot certonly --nginx -d groupe-meditation.chezlepro.ca
-
sudo nginx -t
sudo systemctl reload nginx
+
+# Certificat TLS
+sudo certbot --nginx -d groupe-meditation.chezlepro.ca
```
-NGINX est le point d'entrée unique HTTPS :
-- `/` → Apache (fichiers statiques frontend)
-- `/api/*` → FastAPI (Uvicorn port 8000)
-- HSTS, CSP, X-Frame-Options, rate limiting
+NGINX sert :
+- `/api/*` → reverse proxy vers FastAPI (Uvicorn port 8000)
+- `/*` → fichiers statiques du build React (SPA routing via `try_files`)
+- Cache 1 an sur les assets (JS, CSS, images), pas de cache sur index.html
-## 9. Sauvegarde automatique
+## 8. Sauvegarde automatique
```bash
sudo cp deploy/backup.sh /etc/cron.daily/groupe-meditation-backup
@@ -187,14 +144,13 @@ sudo chmod +x /etc/cron.daily/groupe-meditation-backup
- pg_dump quotidien en format custom (-Fc)
- Rétention 30 jours
-- Stockage Ceph (réplication triple copie)
- **Test de restauration mensuel recommandé** :
```bash
pg_restore -U grpmed -d groupe_meditation_test /var/backups/groupe-meditation/grpmed_DERNIER.dump
```
-## 10. Vérification finale
+## 9. Vérification finale
```bash
# API
@@ -220,7 +176,6 @@ curl -s https://groupe-meditation.chezlepro.ca/api/health | python3 -m json.tool
| WeasyPrint échoue | `sudo apt install libpango-1.0-0 libpangocairo-1.0-0` |
| Mise à jour frontend | `cd frontend && npm run build && sudo cp -r dist/* /var/www/groupe-meditation/` |
| Mise à jour backend | `sudo systemctl restart groupe-meditation` |
-| Ajouter un groupe | Utiliser l'API : `POST /api/groupes/` ou ajouter via le seed |
## Mise à jour de l'application
diff --git a/frontend/public/icon-192.png b/frontend/public/icon-192.png
new file mode 100644
index 0000000..abeab51
Binary files /dev/null and b/frontend/public/icon-192.png differ
diff --git a/frontend/public/icon-512.png b/frontend/public/icon-512.png
new file mode 100644
index 0000000..22a94bb
Binary files /dev/null and b/frontend/public/icon-512.png differ
diff --git a/groupe-meditation-corrige/groupe-meditation/README.md b/groupe-meditation-corrige/groupe-meditation/README.md
deleted file mode 100644
index 32270ec..0000000
--- a/groupe-meditation-corrige/groupe-meditation/README.md
+++ /dev/null
@@ -1,67 +0,0 @@
-# Groupe Méditation — Application PWA
-
-**District 87-16 · Région 87 · Alcooliques anonymes**
-
-Application mobile-first pour la gestion des groupes AA. 1 module = 1 tâche. 27 modules.
-
-## Phases 0 à 9 — Complétées
-
-### Consultation (C1-C7) — Tous les membres
-- **C1** Rapports PDF (WeasyPrint) — finances, PV, rapport RSG, financier complet
-- **C2** Calendrier district — serviteurs de tous les groupes du 87-16
-- **C3** Anniversaires district — sobriété, prochains en évidence
-- **C4** Littérature — inventaire avec prix, alerte stock bas
-- **C5** Décisions en cours — suivi réunion d'affaires
-- **C6** Votes ouverts — sondages, barres de résultat, vote anonyme
-- **C7** Mon profil — modifier, PIN, notifications
-
-### Saisie hebdomadaire (S1-S5)
-- **S1** Collecte 7e Tradition (trésorier)
-- **S2** Remise de jeton (resp. jetons) — 9 types, historique
-- **S3** Vente littérature (resp. littérature) — stock auto-décrémenté
-- **S4** Rapport de réunion (animateur)
-- **S5** Dépense — 6 champs (trésorier)
-
-### Saisie mensuelle (M1-M6)
-- **M1** Rédiger PV (secrétaire) — décisions structurées, auto-création C5
-- **M2** Présences réunion d'affaires (secrétaire)
-- **M3** Rapport RSG — template 3 sections
-- **M4** Envoi contributions — répartition auto district/région/BSG/intergroupe
-- **M5** Ajuster inventaire — +/−, ajout catalogue
-- **M6** Commander jetons — état stock 30 jours
-
-### Gestion (G1-G9) — Exécutif
-- **G1** Inviter un membre — code 48h
-- **G2** Gérer les membres — fiche complète, droit à l'oubli
-- **G3** Affecter les postes
-- **G4** Terminer un mandat — transition manuelle
-- **G5** Planifier les rotations
-- **G6** Lancer un sondage — oui/non, choix multiple, unanimité
-- **G7** Jumeler parrain/filleul — confidentiel
-- **G8** Config. répartition — % contributions (défaut 30/30/30/10)
-- **G9** Paramètres du groupe
-
-## Stack technique
-
-| Couche | Technologie |
-|--------|-------------|
-| Frontend | React 18 + Vite (PWA) |
-| UI | Tailwind CSS |
-| État | Zustand |
-| Backend | FastAPI (Python) async |
-| DB | PostgreSQL 16 + SQLAlchemy async |
-| Auth | JWT 30 jours + bcrypt 12 rounds |
-| PDF | WeasyPrint |
-| Serveur | Apache (statiques) + NGINX (reverse proxy HTTPS) |
-
-## Principes
-
-- **Anonymat AA** — Prénom seul dans les écrans partagés
-- **Pas d'administrateur** — Permissions = exécutif du groupe
-- **Mandats manuels** — Aucune terminaison automatique
-- **Zéro traçage** — Pas d'analytics, pas de cookies tiers
-- **Hébergé au Québec** — Souveraineté numérique · 100% logiciel libre
-
----
-
-*«Nos leaders sont des serviteurs de confiance ; ils ne gouvernent pas.» — 2e Tradition*
diff --git a/groupe-meditation-corrige/groupe-meditation/REVISION_CORRECTIVE.txt b/groupe-meditation-corrige/groupe-meditation/REVISION_CORRECTIVE.txt
deleted file mode 100644
index 43a2a90..0000000
--- a/groupe-meditation-corrige/groupe-meditation/REVISION_CORRECTIVE.txt
+++ /dev/null
@@ -1,33 +0,0 @@
-Révision corrective appliquée
-
-Principales corrections intégrées :
-- backend/app/core/config.py
- - chargement robuste de backend/.env avec chemin absolu
- - CORS mis à jour pour groupe-meditation.87-16.org
-- deploy/apache-groupe-meditation.conf
- - Apache local sur *:80
- - ProxyPass /api vers 127.0.0.1:8000
- - exclusion de /api des règles SPA rewrite
-- deploy/groupe-meditation.service
- - EnvironmentFile ajouté
- - workers retirés pour simplifier le diagnostic
-- frontend/src/pages/Login.jsx
- - message clair si les groupes ne chargent pas
- - bouton Réessayer
-- frontend/src/stores/store.js
- - loadGroupes gère loading et error correctement
-- frontend/src/stores/api.js
- - erreurs non JSON mieux exposées
-- docs/DEPLOIEMENT.md
- - réécrit pour l’architecture réelle : proxy externe, pas de NGINX local
-- README.md et backend/.env.example
- - FQDN harmonisé vers groupe-meditation.87-16.org
-
-Points restant à valider sur la machine :
-1. a2enmod rewrite headers proxy proxy_http
-2. a2ensite groupe-meditation.conf
-3. systemctl daemon-reload && systemctl restart groupe-meditation
-4. build frontend puis copie vers /var/www/groupe-meditation
-5. tests :
- - curl http://127.0.0.1:8000/api/health
- - curl -H "Host: groupe-meditation.87-16.org" http://127.0.0.1/api/health
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/.env.example b/groupe-meditation-corrige/groupe-meditation/backend/.env.example
deleted file mode 100644
index d1fb352..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/.env.example
+++ /dev/null
@@ -1,7 +0,0 @@
-# Groupe Méditation — Configuration
-# Copier vers .env et ajuster les valeurs
-
-DATABASE_URL=postgresql+asyncpg://grpmed:CHANGEZ_MOI@localhost:5432/groupe_meditation
-JWT_SECRET=CHANGEZ_MOI_64_caracteres_aleatoires_generes_par_openssl_rand_base64_48
-CORS_ORIGINS=["https://groupe-meditation.87-16.org"]
-INVITATION_EXPIRE_HOURS=48
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/__init__.py b/groupe-meditation-corrige/groupe-meditation/backend/app/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/core/__init__.py b/groupe-meditation-corrige/groupe-meditation/backend/app/core/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/core/config.py b/groupe-meditation-corrige/groupe-meditation/backend/app/core/config.py
deleted file mode 100644
index e36a68d..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/core/config.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""Configuration de l'application."""
-from pathlib import Path
-from typing import List
-
-from pydantic_settings import BaseSettings, SettingsConfigDict
-
-BASE_DIR = Path(__file__).resolve().parents[2] # /opt/groupe-meditation/backend
-
-
-class Settings(BaseSettings):
- DATABASE_URL: str = "postgresql+asyncpg://grpmed:grpmed2026@localhost:5432/groupe_meditation"
- JWT_SECRET: str = "changez-moi-en-production-clef-secrete-64-caracteres-minimum"
- JWT_ALGORITHM: str = "HS256"
- JWT_EXPIRE_DAYS: int = 30
- CORS_ORIGINS: List[str] = [
- "http://localhost:5173",
- "http://localhost:3000",
- "http://groupe-meditation.87-16.org",
- "https://groupe-meditation.87-16.org",
- ]
- INVITATION_EXPIRE_HOURS: int = 48
-
- model_config = SettingsConfigDict(
- env_file=BASE_DIR / ".env",
- env_file_encoding="utf-8",
- extra="ignore",
- )
-
-
-settings = Settings()
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/core/database.py b/groupe-meditation-corrige/groupe-meditation/backend/app/core/database.py
deleted file mode 100644
index f73f377..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/core/database.py
+++ /dev/null
@@ -1,21 +0,0 @@
-"""Connexion PostgreSQL async."""
-from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
-from sqlalchemy.orm import DeclarativeBase
-from app.core.config import settings
-
-engine = create_async_engine(settings.DATABASE_URL, echo=False, pool_size=5, max_overflow=10)
-async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
-
-
-class Base(DeclarativeBase):
- pass
-
-
-async def get_db():
- async with async_session() as session:
- try:
- yield session
- await session.commit()
- except Exception:
- await session.rollback()
- raise
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/core/security.py b/groupe-meditation-corrige/groupe-meditation/backend/app/core/security.py
deleted file mode 100644
index 0c95e97..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/core/security.py
+++ /dev/null
@@ -1,68 +0,0 @@
-"""Authentification JWT + hachage PIN."""
-from datetime import datetime, timedelta, timezone
-from jose import jwt, JWTError
-from fastapi import Depends, HTTPException, status
-from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select
-import bcrypt
-
-from app.core.config import settings
-from app.core.database import get_db
-
-security = HTTPBearer()
-
-
-def hash_pin(pin: str) -> str:
- return bcrypt.hashpw(pin.encode("utf-8"), bcrypt.gensalt(rounds=12)).decode("utf-8")
-
-
-def verify_pin(pin: str, hashed: str) -> bool:
- return bcrypt.checkpw(pin.encode("utf-8"), hashed.encode("utf-8"))
-
-
-def create_token(membre_id: str, groupe_id: str) -> str:
- expire = datetime.now(timezone.utc) + timedelta(days=settings.JWT_EXPIRE_DAYS)
- payload = {"sub": str(membre_id), "grp": str(groupe_id), "exp": expire}
- return jwt.encode(payload, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM)
-
-
-def decode_token(token: str) -> dict:
- try:
- return jwt.decode(token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM])
- except JWTError:
- raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token invalide ou expiré")
-
-
-async def get_current_membre(
- credentials: HTTPAuthorizationCredentials = Depends(security),
- db: AsyncSession = Depends(get_db),
-):
- from app.models.membre import Membre
- payload = decode_token(credentials.credentials)
- membre_id = payload.get("sub")
- if not membre_id:
- raise HTTPException(status_code=401, detail="Token invalide")
- result = await db.execute(select(Membre).where(Membre.id == membre_id, Membre.actif == True))
- membre = result.scalar_one_or_none()
- if not membre:
- raise HTTPException(status_code=401, detail="Membre introuvable ou inactif")
- return membre
-
-
-async def require_executif(
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- from app.models.affectation import Affectation
- from app.models.poste import Poste
- result = await db.execute(
- select(Affectation).join(Poste).where(
- Affectation.membre_id == membre.id,
- Affectation.statut == "actif",
- Poste.categorie == "executif",
- )
- )
- if not result.scalar_one_or_none():
- raise HTTPException(status_code=403, detail="Réservé à l'exécutif")
- return membre
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/main.py b/groupe-meditation-corrige/groupe-meditation/backend/app/main.py
deleted file mode 100644
index 9ebe70b..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/main.py
+++ /dev/null
@@ -1,68 +0,0 @@
-"""
-Groupe Méditation — API Backend
-District 87-16, Région 87
-100% logiciel libre · Hébergé au Québec
-"""
-from contextlib import asynccontextmanager
-from fastapi import FastAPI
-from fastapi.middleware.cors import CORSMiddleware
-
-from app.core.config import settings
-from app.core.database import engine, Base
-from app.routers import auth, membres, postes, accueil, invitations, collectes, reunions, depenses, decisions, calendrier, rapports, rotations, parametres, presences, pv, rapports_rsg, jetons, anniversaires, commandes_jetons, litterature, ventes_litterature, contributions, config_repartition, sondages, jumelages
-
-
-@asynccontextmanager
-async def lifespan(app: FastAPI):
- async with engine.begin() as conn:
- await conn.run_sync(Base.metadata.create_all)
- yield
- await engine.dispose()
-
-
-app = FastAPI(
- title="Groupe Méditation API",
- description="API du Groupe Méditation — District 87-16, Région 87",
- version="0.1.0",
- lifespan=lifespan,
-)
-
-app.add_middleware(
- CORSMiddleware,
- allow_origins=settings.CORS_ORIGINS,
- allow_credentials=True,
- allow_methods=["*"],
- allow_headers=["*"],
-)
-
-# Montage des routes
-app.include_router(auth.router, prefix="/api")
-app.include_router(membres.router, prefix="/api")
-app.include_router(postes.router, prefix="/api")
-app.include_router(accueil.router, prefix="/api")
-app.include_router(invitations.router, prefix="/api")
-app.include_router(collectes.router, prefix="/api")
-app.include_router(reunions.router, prefix="/api")
-app.include_router(depenses.router, prefix="/api")
-app.include_router(decisions.router, prefix="/api")
-app.include_router(calendrier.router, prefix="/api")
-app.include_router(rapports.router, prefix="/api")
-app.include_router(rotations.router, prefix="/api")
-app.include_router(parametres.router, prefix="/api")
-app.include_router(presences.router, prefix="/api")
-app.include_router(pv.router, prefix="/api")
-app.include_router(rapports_rsg.router, prefix="/api")
-app.include_router(jetons.router, prefix="/api")
-app.include_router(anniversaires.router, prefix="/api")
-app.include_router(commandes_jetons.router, prefix="/api")
-app.include_router(litterature.router, prefix="/api")
-app.include_router(ventes_litterature.router, prefix="/api")
-app.include_router(contributions.router, prefix="/api")
-app.include_router(config_repartition.router, prefix="/api")
-app.include_router(sondages.router, prefix="/api")
-app.include_router(jumelages.router, prefix="/api")
-
-
-@app.get("/api/health")
-async def root_health():
- return {"status": "ok", "app": "Groupe Méditation", "version": "0.1.0"}
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/models/__init__.py b/groupe-meditation-corrige/groupe-meditation/backend/app/models/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/models/affectation.py b/groupe-meditation-corrige/groupe-meditation/backend/app/models/affectation.py
deleted file mode 100644
index c18612e..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/models/affectation.py
+++ /dev/null
@@ -1,23 +0,0 @@
-"""Table affectations."""
-import uuid
-from datetime import date, datetime, timezone
-from sqlalchemy import String, Date, Text, ForeignKey, Enum as SAEnum
-from sqlalchemy.orm import Mapped, mapped_column, relationship
-from app.core.database import Base
-
-
-class Affectation(Base):
- __tablename__ = "affectations"
-
- id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
- groupe_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("groupes.id"), nullable=False)
- membre_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("membres.id"), nullable=False)
- poste_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("postes.id"), nullable=False)
- date_debut: Mapped[date] = mapped_column(Date, nullable=False)
- date_fin: Mapped[date | None] = mapped_column(Date)
- statut: Mapped[str] = mapped_column(SAEnum("actif", "termine", name="statut_affectation"), default="actif")
- note_transition: Mapped[str | None] = mapped_column(Text)
- termine_par: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("membres.id"))
-
- membre = relationship("Membre", back_populates="affectations", foreign_keys=[membre_id])
- poste = relationship("Poste")
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/models/collecte.py b/groupe-meditation-corrige/groupe-meditation/backend/app/models/collecte.py
deleted file mode 100644
index f209660..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/models/collecte.py
+++ /dev/null
@@ -1,18 +0,0 @@
-"""Table collectes — 7e Tradition."""
-import uuid
-from datetime import date, datetime, timezone
-from sqlalchemy import Date, DateTime, ForeignKey, Numeric
-from sqlalchemy.orm import Mapped, mapped_column
-from app.core.database import Base
-
-
-class Collecte(Base):
- __tablename__ = "collectes"
-
- id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
- groupe_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("groupes.id"), nullable=False)
- date_collecte: Mapped[date] = mapped_column(Date, nullable=False)
- montant: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False)
- reunion_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("reunions.id"))
- saisi_par: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("membres.id"))
- cree_le: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/models/config_repartition.py b/groupe-meditation-corrige/groupe-meditation/backend/app/models/config_repartition.py
deleted file mode 100644
index db5040e..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/models/config_repartition.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""Table config_repartition — % de répartition des contributions."""
-import uuid
-from datetime import date
-from sqlalchemy import Date, ForeignKey, Numeric, Enum as SAEnum
-from sqlalchemy.orm import Mapped, mapped_column
-from app.core.database import Base
-
-
-class ConfigRepartition(Base):
- __tablename__ = "config_repartition"
-
- id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
- groupe_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("groupes.id"), nullable=False)
- destination: Mapped[str] = mapped_column(
- SAEnum("district", "region", "bsg", "intergroupe", name="destination_repartition", create_constraint=False),
- nullable=False,
- )
- pourcentage: Mapped[float] = mapped_column(Numeric(5, 2), nullable=False)
- actif_depuis: Mapped[date] = mapped_column(Date, nullable=False)
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/models/decision.py b/groupe-meditation-corrige/groupe-meditation/backend/app/models/decision.py
deleted file mode 100644
index 0f1ee4a..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/models/decision.py
+++ /dev/null
@@ -1,27 +0,0 @@
-"""Table decisions — suivi des décisions de réunion d'affaires."""
-import uuid
-from datetime import date, datetime, timezone
-from sqlalchemy import String, Date, DateTime, Text, ForeignKey, Enum as SAEnum
-from sqlalchemy.orm import Mapped, mapped_column
-from app.core.database import Base
-
-
-class Decision(Base):
- __tablename__ = "decisions"
-
- id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
- groupe_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("groupes.id"), nullable=False)
- reunion_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("reunions.id"))
- sujet: Mapped[str] = mapped_column(String(255), nullable=False)
- description: Mapped[str | None] = mapped_column(Text)
- statut: Mapped[str] = mapped_column(
- SAEnum("en_attente", "en_cours", "complete", name="statut_decision"),
- default="en_attente",
- )
- responsable_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("membres.id"))
- proposeur: Mapped[str | None] = mapped_column(String(100))
- secondeur: Mapped[str | None] = mapped_column(String(100))
- date_decision: Mapped[date] = mapped_column(Date, nullable=False)
- date_completion: Mapped[date | None] = mapped_column(Date)
- cree_par: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("membres.id"))
- cree_le: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/models/depense.py b/groupe-meditation-corrige/groupe-meditation/backend/app/models/depense.py
deleted file mode 100644
index 1dd66b6..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/models/depense.py
+++ /dev/null
@@ -1,24 +0,0 @@
-"""Table depenses — 6 champs selon l'architecture v4."""
-import uuid
-from datetime import date, datetime, timezone
-from sqlalchemy import String, Boolean, Date, DateTime, ForeignKey, Numeric, Enum as SAEnum
-from sqlalchemy.orm import Mapped, mapped_column
-from app.core.database import Base
-
-
-class Depense(Base):
- __tablename__ = "depenses"
-
- id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
- groupe_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("groupes.id"), nullable=False)
- date_depense: Mapped[date] = mapped_column(Date, nullable=False)
- montant: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False)
- qui: Mapped[uuid.UUID] = mapped_column(ForeignKey("membres.id"), nullable=False)
- quoi: Mapped[str] = mapped_column(String(255), nullable=False)
- paye: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
- methode: Mapped[str] = mapped_column(
- SAEnum("DU", "VIREMENT", "CHEQUE", "COLLECTE", name="methode_paiement"),
- nullable=False,
- )
- saisi_par: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("membres.id"))
- cree_le: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/models/envoi_contribution.py b/groupe-meditation-corrige/groupe-meditation/backend/app/models/envoi_contribution.py
deleted file mode 100644
index 792f135..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/models/envoi_contribution.py
+++ /dev/null
@@ -1,25 +0,0 @@
-"""Table envois_contributions — Envois aux organismes AA."""
-import uuid
-from datetime import date, datetime, timezone
-from sqlalchemy import Date, DateTime, ForeignKey, Numeric, Enum as SAEnum
-from sqlalchemy.orm import Mapped, mapped_column
-from app.core.database import Base
-
-
-class EnvoiContribution(Base):
- __tablename__ = "envois_contributions"
-
- id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
- groupe_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("groupes.id"), nullable=False)
- date_envoi: Mapped[date] = mapped_column(Date, nullable=False)
- destination: Mapped[str] = mapped_column(
- SAEnum("district", "region", "bsg", "intergroupe", name="destination_contribution"),
- nullable=False,
- )
- montant: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False)
- methode: Mapped[str] = mapped_column(
- SAEnum("DU", "VIREMENT", "CHEQUE", name="methode_contribution"),
- nullable=False,
- )
- saisi_par: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("membres.id"))
- cree_le: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/models/groupe.py b/groupe-meditation-corrige/groupe-meditation/backend/app/models/groupe.py
deleted file mode 100644
index abebad4..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/models/groupe.py
+++ /dev/null
@@ -1,23 +0,0 @@
-"""Table groupes."""
-import uuid
-from datetime import datetime, timezone
-from sqlalchemy import String, Text, Time, DateTime
-from sqlalchemy.orm import Mapped, mapped_column, relationship
-from app.core.database import Base
-
-
-class Groupe(Base):
- __tablename__ = "groupes"
-
- id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
- nom: Mapped[str] = mapped_column(String(100), nullable=False)
- district: Mapped[str] = mapped_column(String(20), default="87-16")
- region: Mapped[str] = mapped_column(String(20), default="87")
- adresse_local: Mapped[str | None] = mapped_column(Text)
- jour_reunion: Mapped[str | None] = mapped_column(String(10))
- heure_reunion: Mapped[datetime | None] = mapped_column(Time)
- format_reunion: Mapped[str | None] = mapped_column(Text)
- lien_aa87: Mapped[str] = mapped_column(String(255), default="https://aa87.org")
- cree_le: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
-
- membres = relationship("Membre", back_populates="groupe")
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/models/invitation.py b/groupe-meditation-corrige/groupe-meditation/backend/app/models/invitation.py
deleted file mode 100644
index a1ac47d..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/models/invitation.py
+++ /dev/null
@@ -1,18 +0,0 @@
-"""Table invitations."""
-import uuid
-from datetime import datetime, timezone
-from sqlalchemy import String, Boolean, DateTime, ForeignKey
-from sqlalchemy.orm import Mapped, mapped_column
-from app.core.database import Base
-
-
-class Invitation(Base):
- __tablename__ = "invitations"
-
- id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
- groupe_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("groupes.id"), nullable=False)
- code: Mapped[str] = mapped_column(String(12), unique=True, nullable=False)
- cree_par: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("membres.id"))
- utilise_par: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("membres.id"))
- expire_le: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
- utilise: Mapped[bool] = mapped_column(Boolean, default=False)
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/models/jeton.py b/groupe-meditation-corrige/groupe-meditation/backend/app/models/jeton.py
deleted file mode 100644
index 832682d..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/models/jeton.py
+++ /dev/null
@@ -1,26 +0,0 @@
-"""Table jetons — Remise de jetons de sobriété."""
-import uuid
-from datetime import date, datetime, timezone
-from sqlalchemy import String, Integer, Date, DateTime, ForeignKey, Enum as SAEnum
-from sqlalchemy.orm import Mapped, mapped_column
-from app.core.database import Base
-
-
-class Jeton(Base):
- __tablename__ = "jetons"
-
- id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
- groupe_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("groupes.id"), nullable=False)
- membre_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("membres.id"))
- type_jeton: Mapped[str] = mapped_column(
- SAEnum(
- "desir", "24h", "1mois", "2mois", "3mois",
- "6mois", "9mois", "1an", "multiple",
- name="type_jeton",
- ),
- nullable=False,
- )
- date_remise: Mapped[date] = mapped_column(Date, nullable=False)
- annees: Mapped[int | None] = mapped_column(Integer) # pour type_jeton='multiple'
- remis_par: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("membres.id"))
- cree_le: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/models/jumelage.py b/groupe-meditation-corrige/groupe-meditation/backend/app/models/jumelage.py
deleted file mode 100644
index 437f635..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/models/jumelage.py
+++ /dev/null
@@ -1,23 +0,0 @@
-"""Table jumelages — Parrainage discret."""
-import uuid
-from datetime import date, datetime, timezone
-from sqlalchemy import String, Date, DateTime, ForeignKey, Enum as SAEnum
-from sqlalchemy.orm import Mapped, mapped_column
-from app.core.database import Base
-
-
-class Jumelage(Base):
- __tablename__ = "jumelages"
-
- id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
- groupe_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("groupes.id"), nullable=False)
- parrain_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("membres.id"), nullable=False)
- filleul_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("membres.id"), nullable=False)
- date_jumelage: Mapped[date] = mapped_column(Date, nullable=False)
- statut: Mapped[str] = mapped_column(
- SAEnum("actif", "termine", name="statut_jumelage"),
- default="actif",
- )
- note: Mapped[str | None] = mapped_column(String(255))
- cree_par: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("membres.id"))
- cree_le: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/models/litterature.py b/groupe-meditation-corrige/groupe-meditation/backend/app/models/litterature.py
deleted file mode 100644
index e39967b..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/models/litterature.py
+++ /dev/null
@@ -1,21 +0,0 @@
-"""Table litterature — Inventaire AA."""
-import uuid
-from sqlalchemy import String, Integer, Numeric, ForeignKey, Enum as SAEnum
-from sqlalchemy.orm import Mapped, mapped_column
-from app.core.database import Base
-
-
-class Litterature(Base):
- __tablename__ = "litterature"
-
- id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
- groupe_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("groupes.id"), nullable=False)
- titre: Mapped[str] = mapped_column(String(200), nullable=False)
- categorie: Mapped[str] = mapped_column(
- SAEnum("livre", "brochure", "depliant", "pamphlet", name="categorie_litterature"),
- nullable=False,
- )
- prix_coutant: Mapped[float | None] = mapped_column(Numeric(8, 2))
- quantite_stock: Mapped[int] = mapped_column(Integer, default=0)
- seuil_commande: Mapped[int] = mapped_column(Integer, default=2)
- code_bsg: Mapped[str | None] = mapped_column(String(20))
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/models/membre.py b/groupe-meditation-corrige/groupe-meditation/backend/app/models/membre.py
deleted file mode 100644
index e26345d..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/models/membre.py
+++ /dev/null
@@ -1,26 +0,0 @@
-"""Table membres."""
-import uuid
-from datetime import datetime, date, timezone
-from sqlalchemy import String, Boolean, Date, DateTime, ForeignKey, JSON
-from sqlalchemy.orm import Mapped, mapped_column, relationship
-from app.core.database import Base
-
-
-class Membre(Base):
- __tablename__ = "membres"
-
- id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
- groupe_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("groupes.id"), nullable=False)
- prenom: Mapped[str] = mapped_column(String(50), nullable=False)
- nom: Mapped[str] = mapped_column(String(50), nullable=False)
- telephone: Mapped[str | None] = mapped_column(String(20))
- courriel: Mapped[str | None] = mapped_column(String(100))
- date_abstinence: Mapped[date | None] = mapped_column(Date)
- pin_hash: Mapped[str] = mapped_column(String(255), nullable=False)
- actif: Mapped[bool] = mapped_column(Boolean, default=True)
- push_sub: Mapped[dict | None] = mapped_column(JSON)
- cree_le: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
- modifie_le: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
-
- groupe = relationship("Groupe", back_populates="membres")
- affectations = relationship("Affectation", back_populates="membre", foreign_keys="Affectation.membre_id")
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/models/option_sondage.py b/groupe-meditation-corrige/groupe-meditation/backend/app/models/option_sondage.py
deleted file mode 100644
index 77b9fc1..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/models/option_sondage.py
+++ /dev/null
@@ -1,16 +0,0 @@
-"""Table options_sondage — Options de vote."""
-import uuid
-from sqlalchemy import String, Integer, ForeignKey
-from sqlalchemy.orm import Mapped, mapped_column, relationship
-from app.core.database import Base
-
-
-class OptionSondage(Base):
- __tablename__ = "options_sondage"
-
- id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
- sondage_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("sondages.id", ondelete="CASCADE"), nullable=False)
- texte: Mapped[str] = mapped_column(String(200), nullable=False)
- ordre: Mapped[int] = mapped_column(Integer, default=0)
-
- sondage = relationship("Sondage", back_populates="options")
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/models/poste.py b/groupe-meditation-corrige/groupe-meditation/backend/app/models/poste.py
deleted file mode 100644
index ad09b8b..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/models/poste.py
+++ /dev/null
@@ -1,17 +0,0 @@
-"""Table postes."""
-import uuid
-from sqlalchemy import String, Text, Integer, Interval, Enum as SAEnum
-from sqlalchemy.orm import Mapped, mapped_column
-from app.core.database import Base
-
-
-class Poste(Base):
- __tablename__ = "postes"
-
- id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
- nom: Mapped[str] = mapped_column(String(100), nullable=False)
- categorie: Mapped[str] = mapped_column(SAEnum("executif", "service", "physique", name="categorie_poste"), nullable=False)
- description: Mapped[str | None] = mapped_column(Text)
- duree_recommandee: Mapped[str | None] = mapped_column(Interval)
- prerequis: Mapped[str | None] = mapped_column(Text)
- ordre_affichage: Mapped[int | None] = mapped_column(Integer)
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/models/presence.py b/groupe-meditation-corrige/groupe-meditation/backend/app/models/presence.py
deleted file mode 100644
index 473746a..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/models/presence.py
+++ /dev/null
@@ -1,14 +0,0 @@
-"""Table presences — réunions d'affaires seulement."""
-import uuid
-from sqlalchemy import Boolean, ForeignKey
-from sqlalchemy.orm import Mapped, mapped_column
-from app.core.database import Base
-
-
-class Presence(Base):
- __tablename__ = "presences"
-
- id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
- reunion_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("reunions.id"), nullable=False)
- membre_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("membres.id"), nullable=False)
- present: Mapped[bool] = mapped_column(Boolean, default=True)
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/models/pv_reunion.py b/groupe-meditation-corrige/groupe-meditation/backend/app/models/pv_reunion.py
deleted file mode 100644
index 87aca74..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/models/pv_reunion.py
+++ /dev/null
@@ -1,18 +0,0 @@
-"""Table pv_reunions — procès-verbaux des réunions d'affaires."""
-import uuid
-from datetime import datetime, timezone
-from sqlalchemy import Text, Boolean, DateTime, ForeignKey, JSON
-from sqlalchemy.orm import Mapped, mapped_column
-from app.core.database import Base
-
-
-class PvReunion(Base):
- __tablename__ = "pv_reunions"
-
- id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
- reunion_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("reunions.id"), nullable=False)
- redacteur_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("membres.id"), nullable=False)
- contenu: Mapped[str] = mapped_column(Text, nullable=False)
- decisions: Mapped[list | None] = mapped_column(JSON) # [{sujet, decision, proposeur, secondeur, statut}]
- approuve: Mapped[bool] = mapped_column(Boolean, default=False)
- cree_le: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/models/rapport_rsg.py b/groupe-meditation-corrige/groupe-meditation/backend/app/models/rapport_rsg.py
deleted file mode 100644
index 4961370..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/models/rapport_rsg.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""Table rapports_rsg — compte-rendus de réunion de district."""
-import uuid
-from datetime import date, datetime, timezone
-from sqlalchemy import Date, DateTime, Text, ForeignKey, JSON
-from sqlalchemy.orm import Mapped, mapped_column
-from app.core.database import Base
-
-
-class RapportRsg(Base):
- __tablename__ = "rapports_rsg"
-
- id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
- groupe_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("groupes.id"), nullable=False)
- reunion_dist: Mapped[date] = mapped_column(Date, nullable=False)
- redacteur_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("membres.id"), nullable=False)
- decisions: Mapped[list | None] = mapped_column(JSON)
- infos_region: Mapped[str | None] = mapped_column(Text)
- sujets_groupe: Mapped[str | None] = mapped_column(Text)
- cree_le: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/models/reunion.py b/groupe-meditation-corrige/groupe-meditation/backend/app/models/reunion.py
deleted file mode 100644
index 4ec65bc..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/models/reunion.py
+++ /dev/null
@@ -1,20 +0,0 @@
-"""Table reunions."""
-import uuid
-from datetime import datetime, timezone
-from sqlalchemy import String, DateTime, Text, ForeignKey, JSON, Enum as SAEnum
-from sqlalchemy.orm import Mapped, mapped_column
-from app.core.database import Base
-
-
-class Reunion(Base):
- __tablename__ = "reunions"
-
- id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
- groupe_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("groupes.id"), nullable=False)
- type: Mapped[str] = mapped_column(SAEnum("reguliere", "affaires", name="type_reunion"), nullable=False)
- date_reunion: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
- animateur_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("membres.id"))
- theme: Mapped[str | None] = mapped_column(String(200))
- annonces: Mapped[dict | None] = mapped_column(JSON)
- note: Mapped[str | None] = mapped_column(Text)
- cree_le: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/models/rotation.py b/groupe-meditation-corrige/groupe-meditation/backend/app/models/rotation.py
deleted file mode 100644
index e6ef3bf..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/models/rotation.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""Table rotations — planification hebdomadaire des serviteurs."""
-import uuid
-from datetime import date, datetime, timezone
-from sqlalchemy import String, Date, DateTime, ForeignKey
-from sqlalchemy.orm import Mapped, mapped_column
-from app.core.database import Base
-
-
-class Rotation(Base):
- __tablename__ = "rotations"
-
- id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
- groupe_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("groupes.id"), nullable=False)
- date_reunion: Mapped[date] = mapped_column(Date, nullable=False)
- poste_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("postes.id"), nullable=False)
- membre_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("membres.id"), nullable=False)
- note: Mapped[str | None] = mapped_column(String(255))
- cree_par: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("membres.id"))
- cree_le: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/models/sondage.py b/groupe-meditation-corrige/groupe-meditation/backend/app/models/sondage.py
deleted file mode 100644
index b89834e..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/models/sondage.py
+++ /dev/null
@@ -1,29 +0,0 @@
-"""Table sondages — Votes de conscience de groupe."""
-import uuid
-from datetime import datetime, timezone
-from sqlalchemy import String, Text, DateTime, ForeignKey, Enum as SAEnum
-from sqlalchemy.orm import Mapped, mapped_column, relationship
-from app.core.database import Base
-
-
-class Sondage(Base):
- __tablename__ = "sondages"
-
- id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
- groupe_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("groupes.id"), nullable=False)
- titre: Mapped[str] = mapped_column(String(200), nullable=False)
- description: Mapped[str | None] = mapped_column(Text)
- type: Mapped[str] = mapped_column(
- SAEnum("oui_non", "choix_multiple", "unanimite", name="type_sondage"),
- nullable=False,
- )
- cree_par: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("membres.id"))
- date_ouverture: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
- date_fermeture: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
- statut: Mapped[str] = mapped_column(
- SAEnum("brouillon", "ouvert", "ferme", name="statut_sondage"),
- default="brouillon",
- )
- cree_le: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
-
- options = relationship("OptionSondage", back_populates="sondage", cascade="all, delete-orphan")
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/models/sync_log.py b/groupe-meditation-corrige/groupe-meditation/backend/app/models/sync_log.py
deleted file mode 100644
index e17d2fa..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/models/sync_log.py
+++ /dev/null
@@ -1,21 +0,0 @@
-"""Table sync_log — Journal de synchronisation."""
-import uuid
-from datetime import datetime, timezone
-from sqlalchemy import String, BigInteger, DateTime, ForeignKey, JSON, Enum as SAEnum
-from sqlalchemy.orm import Mapped, mapped_column
-from app.core.database import Base
-
-
-class SyncLog(Base):
- __tablename__ = "sync_log"
-
- id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
- groupe_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("groupes.id"))
- table_name: Mapped[str] = mapped_column(String(50), nullable=False)
- record_id: Mapped[uuid.UUID] = mapped_column(nullable=False)
- action: Mapped[str] = mapped_column(
- SAEnum("insert", "update", "delete", name="sync_action"),
- nullable=False,
- )
- timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
- data: Mapped[dict | None] = mapped_column(JSON)
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/models/vente_litterature.py b/groupe-meditation-corrige/groupe-meditation/backend/app/models/vente_litterature.py
deleted file mode 100644
index 4829d68..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/models/vente_litterature.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""Table ventes_litterature — Ventes de littérature AA."""
-import uuid
-from datetime import date, datetime, timezone
-from sqlalchemy import Integer, Date, DateTime, ForeignKey, Numeric
-from sqlalchemy.orm import Mapped, mapped_column
-from app.core.database import Base
-
-
-class VenteLitterature(Base):
- __tablename__ = "ventes_litterature"
-
- id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
- litterature_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("litterature.id"), nullable=False)
- groupe_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("groupes.id"), nullable=False)
- quantite: Mapped[int] = mapped_column(Integer, nullable=False)
- montant: Mapped[float] = mapped_column(Numeric(8, 2), nullable=False)
- date_vente: Mapped[date] = mapped_column(Date, nullable=False)
- vendu_par: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("membres.id"))
- cree_le: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/models/vote.py b/groupe-meditation-corrige/groupe-meditation/backend/app/models/vote.py
deleted file mode 100644
index e04d850..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/models/vote.py
+++ /dev/null
@@ -1,16 +0,0 @@
-"""Table votes — Votes anonymes."""
-import uuid
-from datetime import datetime, timezone
-from sqlalchemy import DateTime, ForeignKey
-from sqlalchemy.orm import Mapped, mapped_column
-from app.core.database import Base
-
-
-class Vote(Base):
- __tablename__ = "votes"
-
- id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
- sondage_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("sondages.id", ondelete="CASCADE"), nullable=False)
- option_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("options_sondage.id", ondelete="CASCADE"), nullable=False)
- membre_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("membres.id"), nullable=False)
- vote_le: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/__init__.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/accueil.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/accueil.py
deleted file mode 100644
index 23ca035..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/accueil.py
+++ /dev/null
@@ -1,112 +0,0 @@
-"""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
-from app.models.affectation import Affectation
-from app.models.poste import Poste
-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, rapport RSG", icone="📄", couleur="blue"),
- ModuleTuile(code="C2", nom="Calendrier district", description="Serviteurs de tous les groupes", icone="📅", couleur="green"),
- ModuleTuile(code="C3", nom="Anniversaires", description="Sobriété du district 87-16", icone="🎂", couleur="yellow"),
- ModuleTuile(code="C4", nom="Littérature", description="Inventaire et prix", icone="📚", couleur="purple"),
- ModuleTuile(code="C5", nom="Décisions en cours", description="Suivi réunion d'affaires", icone="📌", couleur="red"),
- ModuleTuile(code="C6", nom="Votes ouverts", description="Conscience de groupe", icone="🗳️", couleur="indigo"),
- ModuleTuile(code="C7", nom="Mon profil", description="Modifier, PIN, notifications", icone="👤", couleur="gray"),
-]
-
-# Modules de saisie par poste
-MODULES_SAISIE = {
- "Trésorier(ière)": [
- ModuleTuile(code="S1", nom="Collecte 7e", description="Saisir le panier", icone="💰", couleur="green"),
- ModuleTuile(code="S5", nom="Dépense", description="Enregistrer une dépense", icone="💸", couleur="red"),
- ModuleTuile(code="M4", nom="Envoi contributions", description="District, région, BSG", 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)": [
- ModuleTuile(code="S4", nom="Rapport réunion", description="Thème et annonces", icone="🎤", couleur="purple"),
- ],
- "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="G1", nom="Inviter un membre", description="Code d'invitation 48h", icone="➕", couleur="green"),
- ModuleTuile(code="G2", nom="Gérer les membres", description="Fiche complète, désactiver", icone="👥", couleur="blue"),
- ModuleTuile(code="G3", nom="Affecter les postes", description="Assigner un serviteur", icone="🎯", couleur="purple"),
- ModuleTuile(code="G4", nom="Terminer un mandat", description="Transition manuelle", icone="🔄", couleur="orange"),
- ModuleTuile(code="G5", nom="Planifier rotations", description="Calendrier serviteurs", icone="📅", couleur="green"),
- ModuleTuile(code="G6", nom="Lancer un sondage", description="Conscience de groupe", icone="📊", couleur="indigo"),
- ModuleTuile(code="G7", nom="Jumeler parrain", description="Parrain / filleul", icone="🤝", couleur="yellow"),
- ModuleTuile(code="G8", nom="Config. répartition", description="% contributions", icone="⚙️", couleur="gray"),
- ModuleTuile(code="G9", nom="Paramètres groupe", description="Nom, horaire, adresse", icone="🔧", couleur="gray"),
-]
-
-
-@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."""
- # 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 = []
- for a in affectations:
- poste = await db.get(Poste, a.poste_id)
- if poste:
- postes_noms.append(poste.nom)
- postes_categories.append(poste.categorie)
-
- # Modules de consultation : toujours visibles
- modules_consultation = MODULES_CONSULTATION
-
- # Modules de saisie : union des modules de tous les postes
- modules_saisie = []
- codes_vus = set()
- 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)
-
- # Modules de gestion : seulement si exécutif
- modules_gestion = MODULES_GESTION if "executif" in postes_categories else []
-
- return AccueilResponse(
- membre=MembrePublic.model_validate(membre),
- postes=postes_noms,
- modules_consultation=modules_consultation,
- modules_saisie=modules_saisie,
- modules_gestion=modules_gestion,
- )
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/anniversaires.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/anniversaires.py
deleted file mode 100644
index a50f8d2..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/anniversaires.py
+++ /dev/null
@@ -1,89 +0,0 @@
-"""Routes C3 — Anniversaires district 87-16."""
-from datetime import date, timedelta
-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
-from app.models.membre import Membre
-from app.models.groupe import Groupe
-from app.schemas.phase5 import AnniversaireOut
-
-router = APIRouter(prefix="/anniversaires", tags=["C3 — Anniversaires district"])
-
-
-def _calcul_anniversaire(date_abstinence: date, aujourdhui: date) -> tuple[int, date, int]:
- """Calcule les années de sobriété, le prochain anniversaire et les jours restants."""
- annees = aujourdhui.year - date_abstinence.year
- # Anniversaire cette année
- try:
- anniv_cette_annee = date_abstinence.replace(year=aujourdhui.year)
- except ValueError:
- # 29 février
- anniv_cette_annee = date(aujourdhui.year, 3, 1)
-
- if anniv_cette_annee < aujourdhui:
- # L'anniversaire de cette année est passé
- try:
- prochain = date_abstinence.replace(year=aujourdhui.year + 1)
- except ValueError:
- prochain = date(aujourdhui.year + 1, 3, 1)
- annees_sobriete = annees
- elif anniv_cette_annee == aujourdhui:
- prochain = anniv_cette_annee
- annees_sobriete = annees
- else:
- prochain = anniv_cette_annee
- annees_sobriete = annees - 1
-
- # Minimum 0
- annees_sobriete = max(0, annees_sobriete)
- jours_restants = (prochain - aujourdhui).days
-
- return annees_sobriete, prochain, jours_restants
-
-
-@router.get("/", response_model=list[AnniversaireOut])
-async def anniversaires_district(
- prochains_jours: int = 90,
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """C3 — Anniversaires de sobriété de tous les groupes du district 87-16."""
- aujourdhui = date.today()
-
- # Récupérer le district du groupe du membre
- groupe = await db.get(Groupe, membre.groupe_id)
- district = groupe.district if groupe else "87-16"
-
- # Tous les membres actifs du district ayant une date d'abstinence
- result = await db.execute(
- select(Membre, Groupe.nom)
- .join(Groupe, Membre.groupe_id == Groupe.id)
- .where(
- Groupe.district == district,
- Membre.actif == True,
- Membre.date_abstinence.isnot(None),
- )
- )
- rows = result.all()
-
- anniversaires = []
- for m, groupe_nom in rows:
- annees_sobriete, prochain, jours_restants = _calcul_anniversaire(m.date_abstinence, aujourdhui)
-
- # Filtrer : prochains X jours OU anniversaire aujourd'hui
- if 0 <= jours_restants <= prochains_jours:
- anniversaires.append(AnniversaireOut(
- membre_prenom=m.prenom,
- groupe_nom=groupe_nom,
- date_abstinence=m.date_abstinence,
- annees_sobriete=annees_sobriete + 1, # prochain anniversaire = +1
- prochain_anniversaire=prochain,
- jours_restants=jours_restants,
- ))
-
- # Trier par jours restants (prochains en premier)
- anniversaires.sort(key=lambda a: a.jours_restants)
- return anniversaires
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/auth.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/auth.py
deleted file mode 100644
index 67bdcce..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/auth.py
+++ /dev/null
@@ -1,107 +0,0 @@
-"""Routes d'authentification."""
-from datetime import datetime, timezone
-from fastapi import APIRouter, Depends, HTTPException
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select, func
-
-from app.core.database import get_db
-from app.core.security import hash_pin, verify_pin, create_token
-from app.models.membre import Membre
-from app.models.groupe import Groupe
-from app.models.invitation import Invitation
-from app.schemas.schemas import ConnexionRequest, InscriptionRequest, TokenResponse, MembrePublic
-
-router = APIRouter(prefix="/auth", tags=["Authentification"])
-
-
-@router.get("/health")
-async def health(db: AsyncSession = Depends(get_db)):
- result = await db.execute(select(Groupe).limit(1))
- groupe = result.scalar_one_or_none()
- return {
- "status": "ok",
- "groupe": groupe.nom if groupe else "Aucun",
- "district": groupe.district if groupe else "-",
- }
-
-
-@router.get("/groupes")
-async def liste_groupes(db: AsyncSession = Depends(get_db)):
- result = await db.execute(select(Groupe).order_by(Groupe.nom))
- groupes = result.scalars().all()
- return [{"id": str(g.id), "nom": g.nom, "district": g.district} for g in groupes]
-
-
-@router.post("/connexion", response_model=TokenResponse)
-async def connexion(req: ConnexionRequest, db: AsyncSession = Depends(get_db)):
- # Chercher le membre par prénom + groupe (insensible à la casse)
- result = await db.execute(
- select(Membre).where(
- func.lower(Membre.prenom) == func.lower(req.prenom),
- Membre.groupe_id == req.groupe_id,
- Membre.actif == True,
- )
- )
- membre = result.scalar_one_or_none()
- if not membre or not verify_pin(req.pin, membre.pin_hash):
- raise HTTPException(status_code=401, detail="Prénom ou PIN incorrect")
-
- token = create_token(str(membre.id), str(membre.groupe_id))
- return TokenResponse(
- access_token=token,
- membre=MembrePublic.model_validate(membre),
- )
-
-
-@router.post("/inscription", response_model=TokenResponse)
-async def inscription(req: InscriptionRequest, db: AsyncSession = Depends(get_db)):
- # Valider le code d'invitation
- result = await db.execute(
- select(Invitation).where(
- Invitation.code == req.code_invitation,
- Invitation.utilise == False,
- )
- )
- invitation = result.scalar_one_or_none()
- if not invitation:
- raise HTTPException(status_code=400, detail="Code d'invitation invalide")
- if invitation.expire_le < datetime.now(timezone.utc):
- raise HTTPException(status_code=400, detail="Code d'invitation expiré")
-
- # Vérifier unicité du prénom dans le groupe
- exists = await db.execute(
- select(Membre).where(
- func.lower(Membre.prenom) == func.lower(req.prenom),
- Membre.groupe_id == invitation.groupe_id,
- Membre.actif == True,
- )
- )
- if exists.scalar_one_or_none():
- raise HTTPException(status_code=400, detail="Ce prénom est déjà utilisé dans ce groupe")
-
- # Valider le PIN (4 chiffres)
- if not req.pin.isdigit() or len(req.pin) != 4:
- raise HTTPException(status_code=400, detail="Le PIN doit être composé de 4 chiffres")
-
- # Créer le membre
- membre = Membre(
- groupe_id=invitation.groupe_id,
- prenom=req.prenom.strip().title(),
- nom=req.nom.strip().title(),
- telephone=req.telephone,
- courriel=req.courriel,
- date_abstinence=req.date_abstinence,
- pin_hash=hash_pin(req.pin),
- )
- db.add(membre)
-
- # Marquer l'invitation utilisée
- invitation.utilise = True
- invitation.utilise_par = membre.id
- await db.flush()
-
- token = create_token(str(membre.id), str(membre.groupe_id))
- return TokenResponse(
- access_token=token,
- membre=MembrePublic.model_validate(membre),
- )
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/calendrier.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/calendrier.py
deleted file mode 100644
index c4ff35c..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/calendrier.py
+++ /dev/null
@@ -1,102 +0,0 @@
-"""Routes C2 — Calendrier district 87-16."""
-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
-from app.models.groupe import Groupe
-from app.models.affectation import Affectation
-from app.models.poste import Poste
-from app.models.membre import Membre
-from app.schemas.phase2 import CalendrierEntry
-
-router = APIRouter(prefix="/calendrier", tags=["C2 — Calendrier district"])
-
-
-@router.get("/", response_model=list[CalendrierEntry])
-async def calendrier_district(
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """C2 — Calendrier des serviteurs de tous les groupes du district 87-16."""
- # Récupérer le district du membre
- groupe = await db.get(Groupe, membre.groupe_id)
- district = groupe.district if groupe else "87-16"
-
- # Tous les groupes du même district
- result = await db.execute(
- select(Groupe).where(Groupe.district == district).order_by(Groupe.nom)
- )
- groupes = {g.id: g for g in result.scalars().all()}
-
- # Toutes les affectations actives de ces groupes
- result = await db.execute(
- select(Affectation).where(
- Affectation.groupe_id.in_(groupes.keys()),
- Affectation.statut == "actif",
- )
- )
- affectations = result.scalars().all()
-
- entries = []
- for a in affectations:
- poste = await db.get(Poste, a.poste_id)
- mbr = await db.get(Membre, a.membre_id)
- grp = groupes.get(a.groupe_id)
- if poste and mbr and grp:
- entries.append(CalendrierEntry(
- groupe_nom=grp.nom,
- groupe_id=grp.id,
- jour_reunion=grp.jour_reunion,
- poste_nom=poste.nom,
- membre_prenom=mbr.prenom,
- date_debut=a.date_debut,
- ))
-
- # Trier par groupe puis par ordre de poste
- entries.sort(key=lambda e: (e.groupe_nom, e.poste_nom))
- return entries
-
-
-@router.get("/groupe/{groupe_id}")
-async def calendrier_groupe(
- groupe_id: str,
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """Détail des serviteurs d'un groupe spécifique."""
- groupe = await db.get(Groupe, groupe_id)
- if not groupe:
- return {"groupe": None, "serviteurs": []}
-
- result = await db.execute(
- select(Affectation).where(
- Affectation.groupe_id == groupe_id,
- Affectation.statut == "actif",
- )
- )
- affectations = result.scalars().all()
-
- serviteurs = []
- for a in affectations:
- poste = await db.get(Poste, a.poste_id)
- mbr = await db.get(Membre, a.membre_id)
- if poste and mbr:
- serviteurs.append({
- "poste": poste.nom,
- "categorie": poste.categorie,
- "prenom": mbr.prenom,
- "depuis": str(a.date_debut),
- })
- serviteurs.sort(key=lambda s: s.get("poste", ""))
-
- return {
- "groupe": {
- "id": str(groupe.id),
- "nom": groupe.nom,
- "jour_reunion": groupe.jour_reunion,
- "adresse": groupe.adresse_local,
- },
- "serviteurs": serviteurs,
- }
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/collectes.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/collectes.py
deleted file mode 100644
index a849b1e..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/collectes.py
+++ /dev/null
@@ -1,80 +0,0 @@
-"""Routes S1 — Collecte 7e Tradition."""
-from fastapi import APIRouter, Depends, HTTPException
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select, desc
-
-from app.core.database import get_db
-from app.core.security import get_current_membre
-from app.models.collecte import Collecte
-from app.models.membre import Membre
-from app.models.affectation import Affectation
-from app.models.poste import Poste
-from app.schemas.phase1 import CollecteCreate, CollecteOut
-
-router = APIRouter(prefix="/collectes", tags=["S1 — Collecte 7e Tradition"])
-
-
-async def require_tresorier(membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db)):
- """Vérifie que le membre est trésorier."""
- result = await db.execute(
- select(Affectation).join(Poste).where(
- Affectation.membre_id == membre.id,
- Affectation.statut == "actif",
- Poste.nom.ilike("%trésorier%"),
- )
- )
- if not result.scalar_one_or_none():
- raise HTTPException(status_code=403, detail="Réservé au trésorier")
- return membre
-
-
-@router.post("/", response_model=CollecteOut)
-async def saisir_collecte(
- req: CollecteCreate,
- membre=Depends(require_tresorier),
- db: AsyncSession = Depends(get_db),
-):
- """S1 — Saisir le montant collecté au panier."""
- collecte = Collecte(
- groupe_id=membre.groupe_id,
- date_collecte=req.date_collecte,
- montant=req.montant,
- reunion_id=req.reunion_id,
- saisi_par=membre.id,
- )
- db.add(collecte)
- await db.flush()
- return CollecteOut(
- id=collecte.id,
- date_collecte=collecte.date_collecte,
- montant=float(collecte.montant),
- saisi_par_prenom=membre.prenom,
- cree_le=collecte.cree_le,
- )
-
-
-@router.get("/", response_model=list[CollecteOut])
-async def liste_collectes(
- limite: int = 20,
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """Lister les collectes récentes du groupe."""
- result = await db.execute(
- select(Collecte)
- .where(Collecte.groupe_id == membre.groupe_id)
- .order_by(desc(Collecte.date_collecte))
- .limit(limite)
- )
- collectes = result.scalars().all()
- out = []
- for c in collectes:
- saisi = await db.get(Membre, c.saisi_par) if c.saisi_par else None
- out.append(CollecteOut(
- id=c.id,
- date_collecte=c.date_collecte,
- montant=float(c.montant),
- saisi_par_prenom=saisi.prenom if saisi else None,
- cree_le=c.cree_le,
- ))
- return out
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/commandes_jetons.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/commandes_jetons.py
deleted file mode 100644
index 1e82e71..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/commandes_jetons.py
+++ /dev/null
@@ -1,66 +0,0 @@
-"""Routes M6 — Commander des jetons."""
-from datetime import date, timedelta
-from fastapi import APIRouter, Depends
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select, func
-
-from app.core.database import get_db
-from app.core.security import get_current_membre
-from app.models.jeton import Jeton
-from app.models.affectation import Affectation
-from app.models.poste import Poste
-from app.schemas.phase5 import StockJetonsOut, TYPES_JETONS
-from fastapi import HTTPException
-
-router = APIRouter(prefix="/commandes-jetons", tags=["M6 — Commander jetons"])
-
-
-async def require_resp_jetons(membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db)):
- """Vérifie que le membre est responsable des jetons."""
- result = await db.execute(
- select(Affectation).join(Poste).where(
- Affectation.membre_id == membre.id,
- Affectation.statut == "actif",
- Poste.nom.ilike("%jeton%"),
- )
- )
- if not result.scalar_one_or_none():
- raise HTTPException(status_code=403, detail="Réservé au responsable des jetons")
- return membre
-
-
-@router.get("/stock", response_model=list[StockJetonsOut])
-async def etat_stock_jetons(
- membre=Depends(require_resp_jetons),
- db: AsyncSession = Depends(get_db),
-):
- """M6 — État du stock de jetons (basé sur l'utilisation du dernier mois)."""
- il_y_a_30_jours = date.today() - timedelta(days=30)
-
- # Compter les jetons remis par type dans les 30 derniers jours
- result = await db.execute(
- select(Jeton.type_jeton, func.count(Jeton.id))
- .where(
- Jeton.groupe_id == membre.groupe_id,
- Jeton.date_remise >= il_y_a_30_jours,
- )
- .group_by(Jeton.type_jeton)
- )
- usage = {row[0]: row[1] for row in result.all()}
-
- stock = []
- for t in TYPES_JETONS:
- remis = usage.get(t["value"], 0)
- # Suggestion simple : si plus de 2 remis dans le mois, suggérer commande
- suggestion = "Bas — commander" if remis >= 2 else "OK"
- # Le jeton de désir est toujours à surveiller (remis chaque semaine)
- if t["value"] == "desir" and remis >= 4:
- suggestion = "Bas — commander"
-
- stock.append(StockJetonsOut(
- type_jeton=t["value"],
- label=f"{t['emoji']} {t['label']}",
- remis_dernier_mois=remis,
- stock_suggestion=suggestion,
- ))
- return stock
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/config_repartition.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/config_repartition.py
deleted file mode 100644
index 3031289..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/config_repartition.py
+++ /dev/null
@@ -1,103 +0,0 @@
-"""Routes G8 — Configurer la répartition des contributions."""
-from datetime import date
-from fastapi import APIRouter, Depends, HTTPException
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select, desc
-
-from app.core.database import get_db
-from app.core.security import require_executif
-from app.models.config_repartition import ConfigRepartition
-from app.schemas.phase7 import (
- ConfigRepartitionOut, ConfigRepartitionBatch,
- DESTINATIONS, DESTINATIONS_LABELS,
-)
-
-router = APIRouter(prefix="/config-repartition", tags=["G8 — Config répartition"])
-
-DEFAUT_REPARTITION = {
- "district": 30.0,
- "region": 30.0,
- "bsg": 30.0,
- "intergroupe": 10.0,
-}
-
-
-@router.get("/", response_model=list[ConfigRepartitionOut])
-async def voir_repartition(
- membre=Depends(require_executif),
- db: AsyncSession = Depends(get_db),
-):
- """G8 — Voir la répartition actuelle des contributions."""
- result = await db.execute(
- select(ConfigRepartition)
- .where(ConfigRepartition.groupe_id == membre.groupe_id)
- .order_by(desc(ConfigRepartition.actif_depuis))
- )
- configs = result.scalars().all()
-
- # Garder la plus récente par destination
- vu = {}
- out = []
- for c in configs:
- if c.destination not in vu:
- vu[c.destination] = True
- out.append(ConfigRepartitionOut(
- destination=c.destination,
- destination_label=DESTINATIONS_LABELS.get(c.destination, c.destination),
- pourcentage=float(c.pourcentage),
- actif_depuis=c.actif_depuis,
- ))
-
- # Compléter avec les défauts pour les destinations manquantes
- for dest in DESTINATIONS:
- if dest not in vu:
- out.append(ConfigRepartitionOut(
- destination=dest,
- destination_label=DESTINATIONS_LABELS[dest],
- pourcentage=DEFAUT_REPARTITION[dest],
- actif_depuis=date.today(),
- ))
-
- # Trier par destination
- ordre = {d: i for i, d in enumerate(DESTINATIONS)}
- out.sort(key=lambda x: ordre.get(x.destination, 99))
- return out
-
-
-@router.put("/", response_model=list[ConfigRepartitionOut])
-async def modifier_repartition(
- req: ConfigRepartitionBatch,
- membre=Depends(require_executif),
- db: AsyncSession = Depends(get_db),
-):
- """G8 — Modifier la répartition (toutes les destinations d'un coup)."""
- # Validation
- total = sum(item.pourcentage for item in req.items)
- if abs(total - 100.0) > 0.01:
- raise HTTPException(status_code=400, detail=f"Le total doit être 100%. Actuellement: {total:.2f}%")
-
- for item in req.items:
- if item.destination not in DESTINATIONS:
- raise HTTPException(status_code=400, detail=f"Destination invalide: {item.destination}")
- if item.pourcentage < 0:
- raise HTTPException(status_code=400, detail="Les pourcentages ne peuvent pas être négatifs")
-
- aujourdhui = date.today()
- out = []
- for item in req.items:
- config = ConfigRepartition(
- groupe_id=membre.groupe_id,
- destination=item.destination,
- pourcentage=item.pourcentage,
- actif_depuis=aujourdhui,
- )
- db.add(config)
- await db.flush()
- out.append(ConfigRepartitionOut(
- destination=config.destination,
- destination_label=DESTINATIONS_LABELS.get(config.destination, config.destination),
- pourcentage=float(config.pourcentage),
- actif_depuis=config.actif_depuis,
- ))
-
- return out
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/contributions.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/contributions.py
deleted file mode 100644
index 338e14a..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/contributions.py
+++ /dev/null
@@ -1,151 +0,0 @@
-"""Routes M4 — Envoi de contributions aux organismes AA."""
-from datetime import date
-from fastapi import APIRouter, Depends, HTTPException
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select, desc
-
-from app.core.database import get_db
-from app.core.security import get_current_membre
-from app.models.envoi_contribution import EnvoiContribution
-from app.models.config_repartition import ConfigRepartition
-from app.models.membre import Membre
-from app.models.affectation import Affectation
-from app.models.poste import Poste
-from app.schemas.phase7 import (
- EnvoiContributionCreate, EnvoiContributionOut,
- CalculRepartitionIn, CalculRepartitionOut,
- DESTINATIONS, DESTINATIONS_LABELS, METHODES_CONTRIBUTION,
-)
-
-router = APIRouter(prefix="/contributions", tags=["M4 — Envoi contributions"])
-
-# Répartition par défaut selon le Guide du groupe
-DEFAUT_REPARTITION = {
- "district": 30.0,
- "region": 30.0,
- "bsg": 30.0,
- "intergroupe": 10.0,
-}
-
-
-async def require_tresorier(membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db)):
- result = await db.execute(
- select(Affectation).join(Poste).where(
- Affectation.membre_id == membre.id,
- Affectation.statut == "actif",
- Poste.nom.ilike("%trésorier%"),
- )
- )
- if not result.scalar_one_or_none():
- raise HTTPException(status_code=403, detail="Réservé au trésorier")
- return membre
-
-
-async def _get_repartition(groupe_id, db: AsyncSession) -> dict[str, float]:
- """Récupère la répartition active ou utilise les défauts."""
- result = await db.execute(
- select(ConfigRepartition)
- .where(ConfigRepartition.groupe_id == groupe_id)
- .order_by(desc(ConfigRepartition.actif_depuis))
- )
- configs = result.scalars().all()
-
- # Garder la plus récente par destination
- repartition = {}
- for c in configs:
- if c.destination not in repartition:
- repartition[c.destination] = float(c.pourcentage)
-
- # Compléter avec les défauts
- for dest in DESTINATIONS:
- if dest not in repartition:
- repartition[dest] = DEFAUT_REPARTITION[dest]
-
- return repartition
-
-
-@router.post("/calculer", response_model=list[CalculRepartitionOut])
-async def calculer_repartition(
- req: CalculRepartitionIn,
- membre=Depends(require_tresorier),
- db: AsyncSession = Depends(get_db),
-):
- """Calculer la répartition d'un montant selon les % configurés."""
- if req.montant_total <= 0:
- raise HTTPException(status_code=400, detail="Le montant doit être positif")
-
- repartition = await _get_repartition(membre.groupe_id, db)
- return [
- CalculRepartitionOut(
- destination=dest,
- destination_label=DESTINATIONS_LABELS[dest],
- pourcentage=pct,
- montant=round(req.montant_total * pct / 100, 2),
- )
- for dest, pct in repartition.items()
- ]
-
-
-@router.post("/", response_model=EnvoiContributionOut)
-async def enregistrer_envoi(
- req: EnvoiContributionCreate,
- membre=Depends(require_tresorier),
- db: AsyncSession = Depends(get_db),
-):
- """M4 — Enregistrer un envoi de contribution à un organisme."""
- if req.destination not in DESTINATIONS:
- raise HTTPException(status_code=400, detail=f"Destination invalide. Choix: {DESTINATIONS}")
- if req.methode.upper() not in METHODES_CONTRIBUTION:
- raise HTTPException(status_code=400, detail=f"Méthode invalide. Choix: {METHODES_CONTRIBUTION}")
-
- envoi = EnvoiContribution(
- groupe_id=membre.groupe_id,
- date_envoi=req.date_envoi,
- destination=req.destination,
- montant=req.montant,
- methode=req.methode.upper(),
- saisi_par=membre.id,
- )
- db.add(envoi)
- await db.flush()
-
- return EnvoiContributionOut(
- id=envoi.id,
- date_envoi=envoi.date_envoi,
- destination=envoi.destination,
- destination_label=DESTINATIONS_LABELS[envoi.destination],
- montant=float(envoi.montant),
- methode=envoi.methode,
- saisi_par_prenom=membre.prenom,
- cree_le=envoi.cree_le,
- )
-
-
-@router.get("/", response_model=list[EnvoiContributionOut])
-async def liste_envois(
- limite: int = 30,
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """Lister les envois de contributions récents."""
- result = await db.execute(
- select(EnvoiContribution)
- .where(EnvoiContribution.groupe_id == membre.groupe_id)
- .order_by(desc(EnvoiContribution.date_envoi))
- .limit(limite)
- )
- envois = result.scalars().all()
- out = []
- for e in envois:
- saisi = await db.get(Membre, e.saisi_par) if e.saisi_par else None
- out.append(EnvoiContributionOut(
- id=e.id,
- date_envoi=e.date_envoi,
- destination=e.destination,
- destination_label=DESTINATIONS_LABELS.get(e.destination, e.destination),
- montant=float(e.montant),
- methode=e.methode,
- saisi_par_prenom=saisi.prenom if saisi else None,
- cree_le=e.cree_le,
- ))
- return out
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/decisions.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/decisions.py
deleted file mode 100644
index 52ade4a..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/decisions.py
+++ /dev/null
@@ -1,110 +0,0 @@
-"""Routes C5 — Décisions en cours."""
-from fastapi import APIRouter, Depends, HTTPException
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select, desc
-
-from app.core.database import get_db
-from app.core.security import get_current_membre, require_executif
-from app.models.decision import Decision
-from app.models.membre import Membre
-from app.schemas.phase2 import DecisionCreate, DecisionOut, DecisionUpdate
-
-router = APIRouter(prefix="/decisions", tags=["C5 — Décisions en cours"])
-
-
-async def _to_out(d: Decision, db: AsyncSession) -> DecisionOut:
- resp = await db.get(Membre, d.responsable_id) if d.responsable_id else None
- return DecisionOut(
- id=d.id,
- sujet=d.sujet,
- description=d.description,
- statut=d.statut,
- responsable_prenom=resp.prenom if resp else None,
- proposeur=d.proposeur,
- secondeur=d.secondeur,
- date_decision=d.date_decision,
- date_completion=d.date_completion,
- cree_le=d.cree_le,
- )
-
-
-@router.get("/", response_model=list[DecisionOut])
-async def liste_decisions(
- statut: str | None = None,
- limite: int = 50,
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """C5 — Lister les décisions du groupe avec filtre optionnel par statut."""
- query = select(Decision).where(Decision.groupe_id == membre.groupe_id)
- if statut:
- query = query.where(Decision.statut == statut)
- query = query.order_by(desc(Decision.date_decision)).limit(limite)
- result = await db.execute(query)
- return [await _to_out(d, db) for d in result.scalars().all()]
-
-
-@router.get("/compteur")
-async def compteur_decisions(
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """Nombre de décisions ouvertes (pour le badge sur la tuile)."""
- result = await db.execute(
- select(Decision).where(
- Decision.groupe_id == membre.groupe_id,
- Decision.statut.in_(["en_attente", "en_cours"]),
- )
- )
- return {"ouvertes": len(result.scalars().all())}
-
-
-@router.post("/", response_model=DecisionOut)
-async def creer_decision(
- req: DecisionCreate,
- executif=Depends(require_executif),
- db: AsyncSession = Depends(get_db),
-):
- """Créer une décision (exécutif — typiquement via le PV)."""
- decision = Decision(
- groupe_id=executif.groupe_id,
- reunion_id=req.reunion_id,
- sujet=req.sujet.strip(),
- description=req.description,
- statut=req.statut,
- responsable_id=req.responsable_id,
- proposeur=req.proposeur,
- secondeur=req.secondeur,
- date_decision=req.date_decision,
- cree_par=executif.id,
- )
- db.add(decision)
- await db.flush()
- return await _to_out(decision, db)
-
-
-@router.patch("/{decision_id}", response_model=DecisionOut)
-async def modifier_decision(
- decision_id: str,
- req: DecisionUpdate,
- executif=Depends(require_executif),
- db: AsyncSession = Depends(get_db),
-):
- """Modifier le statut d'une décision (exécutif)."""
- result = await db.execute(
- select(Decision).where(Decision.id == decision_id, Decision.groupe_id == executif.groupe_id)
- )
- decision = result.scalar_one_or_none()
- if not decision:
- raise HTTPException(status_code=404, detail="Décision introuvable")
-
- if req.statut is not None:
- decision.statut = req.statut
- if req.description is not None:
- decision.description = req.description
- if req.responsable_id is not None:
- decision.responsable_id = req.responsable_id
- if req.date_completion is not None:
- decision.date_completion = req.date_completion
- await db.flush()
- return await _to_out(decision, db)
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/depenses.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/depenses.py
deleted file mode 100644
index 25d24ab..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/depenses.py
+++ /dev/null
@@ -1,146 +0,0 @@
-"""Routes S5 — Dépenses (6 champs)."""
-from fastapi import APIRouter, Depends, HTTPException
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select, desc
-
-from app.core.database import get_db
-from app.core.security import get_current_membre
-from app.models.depense import Depense
-from app.models.membre import Membre
-from app.models.affectation import Affectation
-from app.models.poste import Poste
-from app.schemas.phase1 import DepenseCreate, DepenseOut, DepenseUpdate
-
-router = APIRouter(prefix="/depenses", tags=["S5 — Dépenses"])
-
-METHODES_VALIDES = {"DU", "VIREMENT", "CHEQUE", "COLLECTE"}
-
-
-async def require_tresorier(membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db)):
- """Vérifie que le membre est trésorier."""
- result = await db.execute(
- select(Affectation).join(Poste).where(
- Affectation.membre_id == membre.id,
- Affectation.statut == "actif",
- Poste.nom.ilike("%trésorier%"),
- )
- )
- if not result.scalar_one_or_none():
- raise HTTPException(status_code=403, detail="Réservé au trésorier")
- return membre
-
-
-@router.post("/", response_model=DepenseOut)
-async def saisir_depense(
- req: DepenseCreate,
- membre=Depends(require_tresorier),
- db: AsyncSession = Depends(get_db),
-):
- """S5 — Saisir une dépense avec les 6 champs."""
- # Valider la méthode
- if req.methode.upper() not in METHODES_VALIDES:
- raise HTTPException(status_code=400, detail=f"Méthode invalide. Choix : {', '.join(METHODES_VALIDES)}")
-
- # Valider que le membre 'qui' existe
- qui_membre = await db.get(Membre, req.qui)
- if not qui_membre or qui_membre.groupe_id != membre.groupe_id:
- raise HTTPException(status_code=400, detail="Membre introuvable")
-
- depense = Depense(
- groupe_id=membre.groupe_id,
- date_depense=req.date_depense,
- montant=req.montant,
- qui=req.qui,
- quoi=req.quoi.strip(),
- paye=req.paye,
- methode=req.methode.upper(),
- saisi_par=membre.id,
- )
- db.add(depense)
- await db.flush()
-
- return DepenseOut(
- id=depense.id,
- date_depense=depense.date_depense,
- montant=float(depense.montant),
- qui_prenom=qui_membre.prenom,
- quoi=depense.quoi,
- paye=depense.paye,
- methode=depense.methode,
- saisi_par_prenom=membre.prenom,
- cree_le=depense.cree_le,
- )
-
-
-@router.get("/", response_model=list[DepenseOut])
-async def liste_depenses(
- limite: int = 30,
- paye: bool | None = None,
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """Lister les dépenses du groupe avec filtre optionnel sur payé."""
- query = select(Depense).where(Depense.groupe_id == membre.groupe_id)
- if paye is not None:
- query = query.where(Depense.paye == paye)
- query = query.order_by(desc(Depense.date_depense)).limit(limite)
-
- result = await db.execute(query)
- depenses = result.scalars().all()
-
- out = []
- for d in depenses:
- qui = await db.get(Membre, d.qui)
- saisi = await db.get(Membre, d.saisi_par) if d.saisi_par else None
- out.append(DepenseOut(
- id=d.id,
- date_depense=d.date_depense,
- montant=float(d.montant),
- qui_prenom=qui.prenom if qui else "?",
- quoi=d.quoi,
- paye=d.paye,
- methode=d.methode,
- saisi_par_prenom=saisi.prenom if saisi else None,
- cree_le=d.cree_le,
- ))
- return out
-
-
-@router.patch("/{depense_id}", response_model=DepenseOut)
-async def modifier_depense(
- depense_id: str,
- req: DepenseUpdate,
- membre=Depends(require_tresorier),
- db: AsyncSession = Depends(get_db),
-):
- """Modifier une dépense (ex: marquer comme payé)."""
- result = await db.execute(
- select(Depense).where(Depense.id == depense_id, Depense.groupe_id == membre.groupe_id)
- )
- depense = result.scalar_one_or_none()
- if not depense:
- raise HTTPException(status_code=404, detail="Dépense introuvable")
-
- if req.paye is not None:
- depense.paye = req.paye
- if req.methode is not None:
- if req.methode.upper() not in METHODES_VALIDES:
- raise HTTPException(status_code=400, detail="Méthode invalide")
- depense.methode = req.methode.upper()
- if req.quoi is not None:
- depense.quoi = req.quoi.strip()
-
- await db.flush()
-
- qui = await db.get(Membre, depense.qui)
- return DepenseOut(
- id=depense.id,
- date_depense=depense.date_depense,
- montant=float(depense.montant),
- qui_prenom=qui.prenom if qui else "?",
- quoi=depense.quoi,
- paye=depense.paye,
- methode=depense.methode,
- saisi_par_prenom=membre.prenom,
- cree_le=depense.cree_le,
- )
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/invitations.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/invitations.py
deleted file mode 100644
index a48d8d9..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/invitations.py
+++ /dev/null
@@ -1,31 +0,0 @@
-"""Routes invitations."""
-import secrets
-from datetime import datetime, timedelta, timezone
-from fastapi import APIRouter, Depends
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from app.core.database import get_db
-from app.core.config import settings
-from app.core.security import require_executif
-from app.models.invitation import Invitation
-from app.schemas.schemas import InvitationOut
-
-router = APIRouter(prefix="/invitations", tags=["Invitations"])
-
-
-@router.post("/", response_model=InvitationOut)
-async def creer_invitation(
- executif=Depends(require_executif),
- db: AsyncSession = Depends(get_db),
-):
- """G1 — Générer un code d'invitation (expire après 48h)."""
- code = secrets.token_hex(6).upper() # 12 caractères hex
- invitation = Invitation(
- groupe_id=executif.groupe_id,
- code=code,
- cree_par=executif.id,
- expire_le=datetime.now(timezone.utc) + timedelta(hours=settings.INVITATION_EXPIRE_HOURS),
- )
- db.add(invitation)
- await db.flush()
- return InvitationOut.model_validate(invitation)
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/jetons.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/jetons.py
deleted file mode 100644
index ac29bcd..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/jetons.py
+++ /dev/null
@@ -1,109 +0,0 @@
-"""Routes S2 — Remise de jeton."""
-from fastapi import APIRouter, Depends, HTTPException
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select, desc
-
-from app.core.database import get_db
-from app.core.security import get_current_membre
-from app.models.jeton import Jeton
-from app.models.membre import Membre
-from app.models.affectation import Affectation
-from app.models.poste import Poste
-from app.schemas.phase5 import JetonCreate, JetonOut, TYPES_JETONS
-
-router = APIRouter(prefix="/jetons", tags=["S2 — Remise de jeton"])
-
-
-async def require_resp_jetons(membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db)):
- """Vérifie que le membre est responsable des jetons."""
- result = await db.execute(
- select(Affectation).join(Poste).where(
- Affectation.membre_id == membre.id,
- Affectation.statut == "actif",
- Poste.nom.ilike("%jeton%"),
- )
- )
- if not result.scalar_one_or_none():
- raise HTTPException(status_code=403, detail="Réservé au responsable des jetons")
- return membre
-
-
-@router.get("/types")
-async def types_jetons():
- """Liste des types de jetons disponibles."""
- return TYPES_JETONS
-
-
-@router.post("/", response_model=JetonOut)
-async def remettre_jeton(
- req: JetonCreate,
- membre=Depends(require_resp_jetons),
- db: AsyncSession = Depends(get_db),
-):
- """S2 — Enregistrer la remise d'un jeton."""
- # Validation
- types_valides = [t["value"] for t in TYPES_JETONS]
- if req.type_jeton not in types_valides:
- raise HTTPException(status_code=400, detail=f"Type de jeton invalide. Valeurs: {types_valides}")
-
- if req.type_jeton == "multiple" and (not req.annees or req.annees < 2):
- raise HTTPException(status_code=400, detail="Le nombre d'années est requis (≥ 2) pour un jeton multiple")
-
- # Vérifier que le membre existe si spécifié
- membre_prenom = None
- if req.membre_id:
- cible = await db.get(Membre, req.membre_id)
- if not cible or not cible.actif:
- raise HTTPException(status_code=404, detail="Membre introuvable")
- membre_prenom = cible.prenom
-
- jeton = Jeton(
- groupe_id=membre.groupe_id,
- membre_id=req.membre_id,
- type_jeton=req.type_jeton,
- date_remise=req.date_remise,
- annees=req.annees if req.type_jeton == "multiple" else None,
- remis_par=membre.id,
- )
- db.add(jeton)
- await db.flush()
-
- return JetonOut(
- id=jeton.id,
- membre_prenom=membre_prenom,
- type_jeton=jeton.type_jeton,
- date_remise=jeton.date_remise,
- annees=jeton.annees,
- remis_par_prenom=membre.prenom,
- cree_le=jeton.cree_le,
- )
-
-
-@router.get("/", response_model=list[JetonOut])
-async def liste_jetons(
- limite: int = 30,
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """Lister les jetons remis récemment (tout le groupe)."""
- result = await db.execute(
- select(Jeton)
- .where(Jeton.groupe_id == membre.groupe_id)
- .order_by(desc(Jeton.date_remise))
- .limit(limite)
- )
- jetons = result.scalars().all()
- out = []
- for j in jetons:
- m = await db.get(Membre, j.membre_id) if j.membre_id else None
- r = await db.get(Membre, j.remis_par) if j.remis_par else None
- out.append(JetonOut(
- id=j.id,
- membre_prenom=m.prenom if m else None,
- type_jeton=j.type_jeton,
- date_remise=j.date_remise,
- annees=j.annees,
- remis_par_prenom=r.prenom if r else None,
- cree_le=j.cree_le,
- ))
- return out
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/jumelages.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/jumelages.py
deleted file mode 100644
index ec18d0d..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/jumelages.py
+++ /dev/null
@@ -1,99 +0,0 @@
-"""Routes G7 — Jumelage parrain/filleul (confidentiel)."""
-from fastapi import APIRouter, Depends, HTTPException
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select, desc
-
-from app.core.database import get_db
-from app.core.security import require_executif
-from app.models.jumelage import Jumelage
-from app.models.membre import Membre
-from app.schemas.phase8 import JumelageCreate, JumelageOut, JumelageUpdate
-
-router = APIRouter(prefix="/jumelages", tags=["G7 — Parrainage"])
-
-
-async def _to_out(j: Jumelage, db: AsyncSession) -> JumelageOut:
- parrain = await db.get(Membre, j.parrain_id)
- filleul = await db.get(Membre, j.filleul_id)
- cree = await db.get(Membre, j.cree_par) if j.cree_par else None
- return JumelageOut(
- id=j.id,
- parrain_prenom=parrain.prenom if parrain else "?",
- filleul_prenom=filleul.prenom if filleul else "?",
- date_jumelage=j.date_jumelage,
- statut=j.statut,
- note=j.note,
- cree_par_prenom=cree.prenom if cree else None,
- )
-
-
-@router.post("/", response_model=JumelageOut)
-async def creer_jumelage(
- req: JumelageCreate,
- membre=Depends(require_executif),
- db: AsyncSession = Depends(get_db),
-):
- """G7 — Jumeler un parrain et un filleul."""
- if req.parrain_id == req.filleul_id:
- raise HTTPException(status_code=400, detail="Le parrain et le filleul doivent être différents")
-
- parrain = await db.get(Membre, req.parrain_id)
- filleul = await db.get(Membre, req.filleul_id)
- if not parrain or not parrain.actif:
- raise HTTPException(status_code=404, detail="Parrain introuvable ou inactif")
- if not filleul or not filleul.actif:
- raise HTTPException(status_code=404, detail="Filleul introuvable ou inactif")
-
- jumelage = Jumelage(
- groupe_id=membre.groupe_id,
- parrain_id=req.parrain_id,
- filleul_id=req.filleul_id,
- date_jumelage=req.date_jumelage,
- note=req.note,
- cree_par=membre.id,
- )
- db.add(jumelage)
- await db.flush()
-
- return await _to_out(jumelage, db)
-
-
-@router.get("/", response_model=list[JumelageOut])
-async def liste_jumelages(
- statut: str | None = None,
- membre=Depends(require_executif),
- db: AsyncSession = Depends(get_db),
-):
- """Liste des jumelages (confidentiel — exécutif seulement)."""
- query = select(Jumelage).where(Jumelage.groupe_id == membre.groupe_id)
- if statut:
- query = query.where(Jumelage.statut == statut)
- query = query.order_by(desc(Jumelage.date_jumelage))
-
- result = await db.execute(query)
- jumelages = result.scalars().all()
- return [await _to_out(j, db) for j in jumelages]
-
-
-@router.patch("/{jumelage_id}", response_model=JumelageOut)
-async def modifier_jumelage(
- jumelage_id: str,
- req: JumelageUpdate,
- membre=Depends(require_executif),
- db: AsyncSession = Depends(get_db),
-):
- """Modifier ou terminer un jumelage."""
- result = await db.execute(
- select(Jumelage).where(Jumelage.id == jumelage_id, Jumelage.groupe_id == membre.groupe_id)
- )
- jumelage = result.scalar_one_or_none()
- if not jumelage:
- raise HTTPException(status_code=404, detail="Jumelage introuvable")
-
- if req.statut and req.statut in ("actif", "termine"):
- jumelage.statut = req.statut
- if req.note is not None:
- jumelage.note = req.note
- await db.flush()
-
- return await _to_out(jumelage, db)
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/litterature.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/litterature.py
deleted file mode 100644
index a865960..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/litterature.py
+++ /dev/null
@@ -1,124 +0,0 @@
-"""Routes C4 / M5 — Littérature : inventaire, ajustement, gestion catalogue."""
-from fastapi import APIRouter, Depends, HTTPException
-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
-from app.models.litterature import Litterature
-from app.models.affectation import Affectation
-from app.models.poste import Poste
-from app.schemas.phase6 import LitteratureCreate, LitteratureOut, LitteratureUpdate, AjustementStock
-
-router = APIRouter(prefix="/litterature", tags=["C4/M5 — Littérature"])
-
-
-async def require_resp_litterature(membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db)):
- """Vérifie que le membre est responsable de la littérature."""
- result = await db.execute(
- select(Affectation).join(Poste).where(
- Affectation.membre_id == membre.id,
- Affectation.statut == "actif",
- Poste.nom.ilike("%littérature%"),
- )
- )
- if not result.scalar_one_or_none():
- raise HTTPException(status_code=403, detail="Réservé au responsable de la littérature")
- return membre
-
-
-def _to_out(item: Litterature) -> LitteratureOut:
- return LitteratureOut(
- id=item.id,
- titre=item.titre,
- categorie=item.categorie,
- prix_coutant=float(item.prix_coutant) if item.prix_coutant else None,
- quantite_stock=item.quantite_stock,
- seuil_commande=item.seuil_commande,
- code_bsg=item.code_bsg,
- stock_bas=item.quantite_stock <= item.seuil_commande,
- )
-
-
-@router.get("/", response_model=list[LitteratureOut])
-async def inventaire(
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """C4 — Inventaire de la littérature du groupe avec prix."""
- result = await db.execute(
- select(Litterature)
- .where(Litterature.groupe_id == membre.groupe_id)
- .order_by(Litterature.categorie, Litterature.titre)
- )
- return [_to_out(item) for item in result.scalars().all()]
-
-
-@router.post("/", response_model=LitteratureOut)
-async def ajouter_titre(
- req: LitteratureCreate,
- membre=Depends(require_resp_litterature),
- db: AsyncSession = Depends(get_db),
-):
- """Ajouter un titre au catalogue du groupe."""
- categories_valides = ["livre", "brochure", "depliant", "pamphlet"]
- if req.categorie not in categories_valides:
- raise HTTPException(status_code=400, detail=f"Catégorie invalide. Valeurs: {categories_valides}")
-
- item = Litterature(
- groupe_id=membre.groupe_id,
- titre=req.titre,
- categorie=req.categorie,
- prix_coutant=req.prix_coutant,
- quantite_stock=req.quantite_stock,
- seuil_commande=req.seuil_commande,
- code_bsg=req.code_bsg,
- )
- db.add(item)
- await db.flush()
- return _to_out(item)
-
-
-@router.patch("/{item_id}", response_model=LitteratureOut)
-async def modifier_titre(
- item_id: str,
- req: LitteratureUpdate,
- membre=Depends(require_resp_litterature),
- db: AsyncSession = Depends(get_db),
-):
- """Modifier les infos d'un titre."""
- result = await db.execute(
- select(Litterature).where(Litterature.id == item_id, Litterature.groupe_id == membre.groupe_id)
- )
- item = result.scalar_one_or_none()
- if not item:
- raise HTTPException(status_code=404, detail="Titre introuvable")
-
- for field, value in req.model_dump(exclude_none=True).items():
- setattr(item, field, value)
- await db.flush()
- return _to_out(item)
-
-
-@router.patch("/{item_id}/stock", response_model=LitteratureOut)
-async def ajuster_stock(
- item_id: str,
- req: AjustementStock,
- membre=Depends(require_resp_litterature),
- db: AsyncSession = Depends(get_db),
-):
- """M5 — Ajuster le stock après réception d'une commande ou correction."""
- result = await db.execute(
- select(Litterature).where(Litterature.id == item_id, Litterature.groupe_id == membre.groupe_id)
- )
- item = result.scalar_one_or_none()
- if not item:
- raise HTTPException(status_code=404, detail="Titre introuvable")
-
- nouveau_stock = item.quantite_stock + req.quantite
- if nouveau_stock < 0:
- raise HTTPException(status_code=400, detail="Le stock ne peut pas être négatif")
-
- item.quantite_stock = nouveau_stock
- await db.flush()
- return _to_out(item)
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/membres.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/membres.py
deleted file mode 100644
index 62c0c18..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/membres.py
+++ /dev/null
@@ -1,127 +0,0 @@
-"""Routes membres — profil et gestion."""
-from fastapi import APIRouter, Depends, HTTPException
-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, require_executif, hash_pin, verify_pin
-from app.models.membre import Membre
-from app.schemas.schemas import MembrePublic, MembreComplet, MembreUpdate, ChangerPinRequest
-
-router = APIRouter(prefix="/membres", tags=["Membres"])
-
-
-@router.get("/moi", response_model=MembreComplet)
-async def mon_profil(membre=Depends(get_current_membre)):
- """C7 — Mon profil."""
- return MembreComplet.model_validate(membre)
-
-
-@router.patch("/moi", response_model=MembreComplet)
-async def modifier_profil(
- updates: MembreUpdate,
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """C7 — Modifier mon profil."""
- if updates.telephone is not None:
- membre.telephone = updates.telephone
- if updates.courriel is not None:
- membre.courriel = updates.courriel
- if updates.date_abstinence is not None:
- membre.date_abstinence = updates.date_abstinence
- await db.flush()
- return MembreComplet.model_validate(membre)
-
-
-@router.post("/moi/pin")
-async def changer_pin(
- req: ChangerPinRequest,
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """C7 — Changer mon PIN."""
- if not verify_pin(req.ancien_pin, membre.pin_hash):
- raise HTTPException(status_code=400, detail="Ancien PIN incorrect")
- if not req.nouveau_pin.isdigit() or len(req.nouveau_pin) != 4:
- raise HTTPException(status_code=400, detail="Le PIN doit être composé de 4 chiffres")
- membre.pin_hash = hash_pin(req.nouveau_pin)
- await db.flush()
- return {"message": "PIN modifié avec succès"}
-
-
-@router.get("/actifs", response_model=list[MembrePublic])
-async def liste_membres_actifs(
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """Liste simplifiée des membres actifs (prénom + id) — pour les dropdowns."""
- result = await db.execute(
- select(Membre).where(
- Membre.groupe_id == membre.groupe_id,
- Membre.actif == True,
- ).order_by(Membre.prenom)
- )
- return [MembrePublic.model_validate(m) for m in result.scalars().all()]
-
-
-@router.get("/", response_model=list[MembreComplet])
-async def liste_membres(
- executif=Depends(require_executif),
- db: AsyncSession = Depends(get_db),
-):
- """G2 — Liste des membres (exécutif seulement)."""
- result = await db.execute(
- select(Membre).where(
- Membre.groupe_id == executif.groupe_id,
- Membre.actif == True,
- ).order_by(Membre.prenom)
- )
- return [MembreComplet.model_validate(m) for m in result.scalars().all()]
-
-
-@router.patch("/{membre_id}/desactiver")
-async def desactiver_membre(
- membre_id: str,
- executif=Depends(require_executif),
- db: AsyncSession = Depends(get_db),
-):
- """G2 — Désactiver un membre."""
- result = await db.execute(
- select(Membre).where(Membre.id == membre_id, Membre.groupe_id == executif.groupe_id)
- )
- membre = result.scalar_one_or_none()
- if not membre:
- raise HTTPException(status_code=404, detail="Membre introuvable")
- membre.actif = False
- await db.flush()
- return {"message": f"{membre.prenom} a été désactivé"}
-
-
-@router.delete("/{membre_id}/oubli")
-async def droit_oubli(
- membre_id: str,
- executif=Depends(require_executif),
- db: AsyncSession = Depends(get_db),
-):
- """G2 — Droit à l'oubli : anonymise les données personnelles."""
- result = await db.execute(
- select(Membre).where(Membre.id == membre_id, Membre.groupe_id == executif.groupe_id)
- )
- membre = result.scalar_one_or_none()
- if not membre:
- raise HTTPException(status_code=404, detail="Membre introuvable")
- if str(membre.id) == str(executif.id):
- raise HTTPException(status_code=400, detail="Impossible de supprimer votre propre compte")
-
- prenom_original = membre.prenom
- # Anonymiser
- membre.prenom = f"Ancien membre"
- membre.nom = "***"
- membre.telephone = None
- membre.courriel = None
- membre.pin_hash = "SUPPRIME"
- membre.actif = False
- membre.push_sub = None
- await db.flush()
- return {"message": f"Données de {prenom_original} anonymisées (droit à l'oubli)"}
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/parametres.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/parametres.py
deleted file mode 100644
index b1e0ef0..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/parametres.py
+++ /dev/null
@@ -1,44 +0,0 @@
-"""Routes G9 — Paramètres du groupe."""
-from fastapi import APIRouter, Depends
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from app.core.database import get_db
-from app.core.security import require_executif, get_current_membre
-from app.models.groupe import Groupe
-from app.schemas.phase3 import GroupeUpdate, GroupeDetail
-
-router = APIRouter(prefix="/groupe", tags=["G9 — Paramètres"])
-
-
-@router.get("/", response_model=GroupeDetail)
-async def detail_groupe(
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """Détail du groupe du membre connecté."""
- groupe = await db.get(Groupe, membre.groupe_id)
- return GroupeDetail.model_validate(groupe)
-
-
-@router.patch("/", response_model=GroupeDetail)
-async def modifier_groupe(
- req: GroupeUpdate,
- executif=Depends(require_executif),
- db: AsyncSession = Depends(get_db),
-):
- """G9 — Modifier les paramètres du groupe (exécutif)."""
- groupe = await db.get(Groupe, executif.groupe_id)
- if req.nom is not None:
- groupe.nom = req.nom.strip()
- if req.adresse_local is not None:
- groupe.adresse_local = req.adresse_local.strip()
- if req.jour_reunion is not None:
- groupe.jour_reunion = req.jour_reunion.strip()
- if req.heure_reunion is not None:
- groupe.heure_reunion = req.heure_reunion
- if req.format_reunion is not None:
- groupe.format_reunion = req.format_reunion.strip()
- if req.lien_aa87 is not None:
- groupe.lien_aa87 = req.lien_aa87.strip()
- await db.flush()
- return GroupeDetail.model_validate(groupe)
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/postes.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/postes.py
deleted file mode 100644
index 403167a..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/postes.py
+++ /dev/null
@@ -1,116 +0,0 @@
-"""Routes postes et affectations."""
-from datetime import date
-from fastapi import APIRouter, Depends, HTTPException
-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, require_executif
-from app.models.poste import Poste
-from app.models.affectation import Affectation
-from app.schemas.schemas import PosteOut, AffectationOut, AffectationCreate, TerminerMandatRequest
-
-router = APIRouter(prefix="/postes", tags=["Postes et affectations"])
-
-
-@router.get("/", response_model=list[PosteOut])
-async def liste_postes(db: AsyncSession = Depends(get_db), _=Depends(get_current_membre)):
- result = await db.execute(select(Poste).order_by(Poste.ordre_affichage))
- return [PosteOut.model_validate(p) for p in result.scalars().all()]
-
-
-@router.get("/affectations")
-async def mes_affectations(membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db)):
- """Retourne les affectations actives du membre connecté."""
- result = await db.execute(
- select(Affectation).join(Poste).where(
- Affectation.membre_id == membre.id,
- Affectation.statut == "actif",
- )
- )
- affectations = result.scalars().all()
- out = []
- for a in affectations:
- poste = await db.get(Poste, a.poste_id)
- out.append({
- "id": str(a.id),
- "poste_nom": poste.nom if poste else "?",
- "poste_categorie": poste.categorie if poste else "?",
- "date_debut": str(a.date_debut),
- "statut": a.statut,
- })
- return out
-
-
-@router.get("/affectations/groupe")
-async def affectations_groupe(
- executif=Depends(require_executif),
- db: AsyncSession = Depends(get_db),
-):
- """G3 — Toutes les affectations actives du groupe."""
- result = await db.execute(
- select(Affectation).where(
- Affectation.groupe_id == executif.groupe_id,
- Affectation.statut == "actif",
- )
- )
- affectations = result.scalars().all()
- out = []
- for a in affectations:
- from app.models.membre import Membre
- poste = await db.get(Poste, a.poste_id)
- mbr = await db.get(Membre, a.membre_id)
- out.append({
- "id": str(a.id),
- "membre_prenom": mbr.prenom if mbr else "?",
- "poste_nom": poste.nom if poste else "?",
- "poste_categorie": poste.categorie if poste else "?",
- "date_debut": str(a.date_debut),
- })
- return out
-
-
-@router.post("/affectations")
-async def creer_affectation(
- req: AffectationCreate,
- executif=Depends(require_executif),
- db: AsyncSession = Depends(get_db),
-):
- """G3 — Affecter un membre à un poste."""
- affectation = Affectation(
- groupe_id=executif.groupe_id,
- membre_id=req.membre_id,
- poste_id=req.poste_id,
- date_debut=req.date_debut,
- statut="actif",
- )
- db.add(affectation)
- await db.flush()
- return {"message": "Affectation créée", "id": str(affectation.id)}
-
-
-@router.post("/affectations/{affectation_id}/terminer")
-async def terminer_mandat(
- affectation_id: str,
- req: TerminerMandatRequest,
- executif=Depends(require_executif),
- db: AsyncSession = Depends(get_db),
-):
- """G4 — Terminer un mandat manuellement."""
- result = await db.execute(
- select(Affectation).where(
- Affectation.id == affectation_id,
- Affectation.groupe_id == executif.groupe_id,
- Affectation.statut == "actif",
- )
- )
- affectation = result.scalar_one_or_none()
- if not affectation:
- raise HTTPException(status_code=404, detail="Affectation introuvable")
-
- affectation.statut = "termine"
- affectation.date_fin = date.today()
- affectation.note_transition = req.note_transition
- affectation.termine_par = executif.id
- await db.flush()
- return {"message": "Mandat terminé"}
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/presences.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/presences.py
deleted file mode 100644
index 1cf0df8..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/presences.py
+++ /dev/null
@@ -1,128 +0,0 @@
-"""Routes M2 — Présences en réunion d'affaires."""
-from fastapi import APIRouter, Depends, HTTPException
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select, desc
-
-from app.core.database import get_db
-from app.core.security import get_current_membre
-from app.models.reunion import Reunion
-from app.models.presence import Presence
-from app.models.membre import Membre
-from app.models.affectation import Affectation
-from app.models.poste import Poste
-from app.schemas.phase4 import (
- PresencesBatch, PresenceOut, ReunionAffairesCreate, ReunionAffairesOut,
-)
-
-router = APIRouter(prefix="/presences", tags=["M2 — Présences affaires"])
-
-
-async def require_secretaire(membre=Depends(get_current_membre), db=Depends(get_db)):
- """Vérifie que le membre est secrétaire ou exécutif."""
- result = await db.execute(
- select(Affectation).join(Poste).where(
- Affectation.membre_id == membre.id,
- Affectation.statut == "actif",
- (Poste.nom.ilike("%secrétaire%") | (Poste.categorie == "executif")),
- )
- )
- if not result.scalar_one_or_none():
- raise HTTPException(status_code=403, detail="Réservé au secrétaire ou à l'exécutif")
- return membre
-
-
-@router.post("/reunion", response_model=ReunionAffairesOut)
-async def creer_reunion_affaires(
- req: ReunionAffairesCreate,
- membre=Depends(require_secretaire),
- db: AsyncSession = Depends(get_db),
-):
- """Créer une réunion d'affaires (point de départ pour présences et PV)."""
- reunion = Reunion(
- groupe_id=membre.groupe_id,
- type="affaires",
- date_reunion=req.date_reunion,
- note=req.note,
- )
- db.add(reunion)
- await db.flush()
- return ReunionAffairesOut(id=reunion.id, date_reunion=reunion.date_reunion, note=reunion.note)
-
-
-@router.get("/reunions", response_model=list[ReunionAffairesOut])
-async def liste_reunions_affaires(
- limite: int = 12,
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """Lister les réunions d'affaires récentes avec leurs présences."""
- result = await db.execute(
- select(Reunion).where(
- Reunion.groupe_id == membre.groupe_id,
- Reunion.type == "affaires",
- ).order_by(desc(Reunion.date_reunion)).limit(limite)
- )
- reunions = result.scalars().all()
-
- out = []
- for r in reunions:
- pres_result = await db.execute(
- select(Presence).where(Presence.reunion_id == r.id)
- )
- presences = pres_result.scalars().all()
- pres_out = []
- for p in presences:
- mbr = await db.get(Membre, p.membre_id)
- pres_out.append(PresenceOut(
- membre_id=p.membre_id,
- membre_prenom=mbr.prenom if mbr else "?",
- present=p.present,
- ))
- out.append(ReunionAffairesOut(
- id=r.id, date_reunion=r.date_reunion, note=r.note, presences=pres_out,
- ))
- return out
-
-
-@router.post("/", response_model=list[PresenceOut])
-async def enregistrer_presences(
- req: PresencesBatch,
- membre=Depends(require_secretaire),
- db: AsyncSession = Depends(get_db),
-):
- """M2 — Enregistrer les présences pour une réunion d'affaires."""
- # Vérifier que la réunion existe et est de type affaires
- result = await db.execute(
- select(Reunion).where(
- Reunion.id == req.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")
-
- # Supprimer les présences existantes pour cette réunion (pour pouvoir re-saisir)
- existing = await db.execute(
- select(Presence).where(Presence.reunion_id == req.reunion_id)
- )
- for p in existing.scalars().all():
- await db.delete(p)
-
- # Insérer les nouvelles
- out = []
- for item in req.presences:
- pres = Presence(
- reunion_id=req.reunion_id,
- membre_id=item.membre_id,
- present=item.present,
- )
- db.add(pres)
- mbr = await db.get(Membre, item.membre_id)
- out.append(PresenceOut(
- membre_id=item.membre_id,
- membre_prenom=mbr.prenom if mbr else "?",
- present=item.present,
- ))
- await db.flush()
- return out
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/pv.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/pv.py
deleted file mode 100644
index b4e0fa8..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/pv.py
+++ /dev/null
@@ -1,157 +0,0 @@
-"""Routes M1 — Rédiger un PV de réunion d'affaires."""
-from fastapi import APIRouter, Depends, HTTPException
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select, desc
-
-from app.core.database import get_db
-from app.core.security import get_current_membre
-from app.models.pv_reunion import PvReunion
-from app.models.reunion import Reunion
-from app.models.membre import Membre
-from app.models.decision import Decision
-from app.models.affectation import Affectation
-from app.models.poste import Poste
-from app.schemas.phase4 import PvCreate, PvOut, PvUpdate
-
-router = APIRouter(prefix="/pv", tags=["M1 — Procès-verbal"])
-
-
-async def require_secretaire(membre=Depends(get_current_membre), db=Depends(get_db)):
- result = await db.execute(
- select(Affectation).join(Poste).where(
- Affectation.membre_id == membre.id,
- Affectation.statut == "actif",
- (Poste.nom.ilike("%secrétaire%") | (Poste.categorie == "executif")),
- )
- )
- if not result.scalar_one_or_none():
- raise HTTPException(status_code=403, detail="Réservé au secrétaire ou à l'exécutif")
- return membre
-
-
-async def _to_out(pv: PvReunion, db: AsyncSession) -> PvOut:
- redacteur = await db.get(Membre, pv.redacteur_id)
- reunion = await db.get(Reunion, pv.reunion_id) if pv.reunion_id else None
- return PvOut(
- id=pv.id,
- reunion_id=pv.reunion_id,
- redacteur_prenom=redacteur.prenom if redacteur else "?",
- contenu=pv.contenu,
- decisions=pv.decisions,
- approuve=pv.approuve,
- cree_le=pv.cree_le,
- date_reunion=reunion.date_reunion if reunion else None,
- )
-
-
-@router.get("/", response_model=list[PvOut])
-async def liste_pv(
- limite: int = 12,
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """Lister les PV des réunions d'affaires."""
- # Récupérer les PV via les réunions du groupe
- result = await db.execute(
- select(PvReunion)
- .join(Reunion, PvReunion.reunion_id == Reunion.id)
- .where(Reunion.groupe_id == membre.groupe_id)
- .order_by(desc(PvReunion.cree_le))
- .limit(limite)
- )
- return [await _to_out(pv, db) for pv in result.scalars().all()]
-
-
-@router.get("/{pv_id}", response_model=PvOut)
-async def detail_pv(
- pv_id: str,
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- result = await db.execute(
- select(PvReunion)
- .join(Reunion, PvReunion.reunion_id == Reunion.id)
- .where(PvReunion.id == pv_id, Reunion.groupe_id == membre.groupe_id)
- )
- pv = result.scalar_one_or_none()
- if not pv:
- raise HTTPException(status_code=404, detail="PV introuvable")
- return await _to_out(pv, db)
-
-
-@router.post("/", response_model=PvOut)
-async def creer_pv(
- req: PvCreate,
- membre=Depends(require_secretaire),
- db: AsyncSession = Depends(get_db),
-):
- """M1 — Rédiger un procès-verbal."""
- # Vérifier la réunion
- result = await db.execute(
- select(Reunion).where(
- Reunion.id == req.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")
-
- decisions_data = None
- if req.decisions:
- decisions_data = [d.model_dump() for d in req.decisions]
-
- pv = PvReunion(
- reunion_id=req.reunion_id,
- redacteur_id=membre.id,
- contenu=req.contenu.strip(),
- decisions=decisions_data,
- )
- db.add(pv)
- await db.flush()
-
- # Créer les décisions dans la table decisions (pour C5)
- if req.decisions:
- for d in req.decisions:
- decision = Decision(
- groupe_id=membre.groupe_id,
- reunion_id=req.reunion_id,
- sujet=d.sujet,
- description=d.decision,
- statut=d.statut,
- proposeur=d.proposeur,
- secondeur=d.secondeur,
- date_decision=reunion.date_reunion.date() if reunion.date_reunion else None,
- cree_par=membre.id,
- )
- db.add(decision)
- await db.flush()
-
- return await _to_out(pv, db)
-
-
-@router.patch("/{pv_id}", response_model=PvOut)
-async def modifier_pv(
- pv_id: str,
- req: PvUpdate,
- membre=Depends(require_secretaire),
- db: AsyncSession = Depends(get_db),
-):
- """Modifier un PV existant."""
- result = await db.execute(
- select(PvReunion)
- .join(Reunion, PvReunion.reunion_id == Reunion.id)
- .where(PvReunion.id == pv_id, Reunion.groupe_id == membre.groupe_id)
- )
- pv = result.scalar_one_or_none()
- if not pv:
- raise HTTPException(status_code=404, detail="PV introuvable")
-
- if req.contenu is not None:
- pv.contenu = req.contenu.strip()
- if req.decisions is not None:
- pv.decisions = [d.model_dump() for d in req.decisions]
- if req.approuve is not None:
- pv.approuve = req.approuve
- await db.flush()
- return await _to_out(pv, db)
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/rapports.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/rapports.py
deleted file mode 100644
index 60fe2be..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/rapports.py
+++ /dev/null
@@ -1,306 +0,0 @@
-"""Routes C1 — Rapports PDF (finances, PV, rapport RSG)."""
-import io
-from datetime import date, datetime
-from fastapi import APIRouter, Depends, HTTPException
-from fastapi.responses import StreamingResponse
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select, extract
-
-from app.core.database import get_db
-from app.core.security import get_current_membre
-from app.models.groupe import Groupe
-from app.models.collecte import Collecte
-from app.models.depense import Depense
-from app.models.membre import Membre
-from app.models.reunion import Reunion
-from app.models.envoi_contribution import EnvoiContribution
-
-router = APIRouter(prefix="/rapports", tags=["C1 — Rapports PDF"])
-
-MOIS_FR = [
- "", "Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
- "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre",
-]
-
-
-def _css():
- return """
- body { font-family: 'DejaVu Sans', sans-serif; font-size: 11pt; color: #333; margin: 2cm; }
- h1 { color: #1a5276; font-size: 16pt; margin-bottom: 4pt; }
- h2 { color: #1a5276; font-size: 13pt; border-bottom: 2px solid #1a5276; padding-bottom: 4pt; margin-top: 20pt; }
- .header { text-align: center; margin-bottom: 20pt; }
- .header .groupe { font-size: 18pt; font-weight: bold; color: #1a5276; }
- .header .sub { font-size: 9pt; color: #888; }
- table { width: 100%; border-collapse: collapse; margin-top: 8pt; }
- th { background: #1a5276; color: white; padding: 6pt 8pt; text-align: left; font-size: 9pt; }
- td { padding: 5pt 8pt; border-bottom: 1px solid #ddd; font-size: 10pt; }
- tr:nth-child(even) { background: #f8f9fa; }
- .total { font-weight: bold; font-size: 12pt; margin-top: 10pt; }
- .footer { text-align: center; font-size: 8pt; color: #aaa; margin-top: 30pt; }
- .badge-paye { color: #27ae60; }
- .badge-impaye { color: #e74c3c; }
- """
-
-
-@router.get("/finances/{annee}/{mois}")
-async def rapport_finances(
- annee: int,
- mois: int,
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """C1 — Rapport financier mensuel en PDF."""
- try:
- from weasyprint import HTML
- except ImportError:
- raise HTTPException(status_code=500, detail="WeasyPrint non installé. Installer avec: pip install weasyprint")
-
- groupe = await db.get(Groupe, membre.groupe_id)
-
- # Collectes du mois
- result = await db.execute(
- select(Collecte).where(
- Collecte.groupe_id == membre.groupe_id,
- extract("year", Collecte.date_collecte) == annee,
- extract("month", Collecte.date_collecte) == mois,
- ).order_by(Collecte.date_collecte)
- )
- collectes = result.scalars().all()
-
- # Dépenses du mois
- result = await db.execute(
- select(Depense).where(
- Depense.groupe_id == membre.groupe_id,
- extract("year", Depense.date_depense) == annee,
- extract("month", Depense.date_depense) == mois,
- ).order_by(Depense.date_depense)
- )
- depenses = result.scalars().all()
-
- total_collectes = sum(float(c.montant) for c in collectes)
- total_depenses = sum(float(d.montant) for d in depenses)
- solde = total_collectes - total_depenses
-
- # Construire le HTML
- rows_collectes = ""
- for c in collectes:
- saisi = await db.get(Membre, c.saisi_par) if c.saisi_par else None
- rows_collectes += f"
{c.date_collecte} {float(c.montant):.2f} $ {saisi.prenom if saisi else '-'} "
-
- rows_depenses = ""
- for d in depenses:
- qui = await db.get(Membre, d.qui) if d.qui else None
- badge = '✓ ' if d.paye else '✗ DÛ '
- rows_depenses += f"{d.date_depense} {float(d.montant):.2f} $ {qui.prenom if qui else '-'} {d.quoi} {d.methode} {badge} "
-
- html_content = f"""
-
- Rapport financier — {MOIS_FR[mois]} {annee}
-
- Collectes (7e Tradition)
- Date Montant Saisi par
- {rows_collectes if rows_collectes else 'Aucune collecte ce mois '}
-
- Total collectes : {total_collectes:.2f} $
-
- Dépenses
- Date Montant Qui Quoi Méthode Payé
- {rows_depenses if rows_depenses else 'Aucune dépense ce mois '}
-
- Total dépenses : {total_depenses:.2f} $
-
- Sommaire
-
- Collectes {total_collectes:.2f} $
- Dépenses −{total_depenses:.2f} $
- Solde du mois {'+'if solde>=0 else ''}{solde:.2f} $
-
-
-
- """
-
- pdf_bytes = HTML(string=html_content).write_pdf()
- filename = f"rapport_finances_{groupe.nom.replace(' ','_')}_{annee}_{mois:02d}.pdf"
-
- return StreamingResponse(
- io.BytesIO(pdf_bytes),
- media_type="application/pdf",
- headers={"Content-Disposition": f'inline; filename="{filename}"'},
- )
-
-
-@router.get("/reunions/{annee}/{mois}")
-async def rapport_reunions(
- annee: int,
- mois: int,
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """C1 — Résumé des réunions du mois en PDF."""
- try:
- from weasyprint import HTML
- except ImportError:
- raise HTTPException(status_code=500, detail="WeasyPrint non installé")
-
- groupe = await db.get(Groupe, membre.groupe_id)
-
- result = await db.execute(
- select(Reunion).where(
- Reunion.groupe_id == membre.groupe_id,
- extract("year", Reunion.date_reunion) == annee,
- extract("month", Reunion.date_reunion) == mois,
- ).order_by(Reunion.date_reunion)
- )
- reunions = result.scalars().all()
-
- rows = ""
- for r in reunions:
- anim = await db.get(Membre, r.animateur_id) if r.animateur_id else None
- annonces_str = ", ".join(r.annonces) if r.annonces else "—"
- rows += f"{r.date_reunion.strftime('%Y-%m-%d')} {r.type} {anim.prenom if anim else '-'} {r.theme or '-'} {annonces_str} "
-
- html_content = f"""
-
- Réunions — {MOIS_FR[mois]} {annee}
- Date Type Animateur Thème Annonces
- {rows if rows else 'Aucune réunion enregistrée ce mois '}
-
- {len(reunions)} réunion(s) enregistrée(s)
-
- """
-
- pdf_bytes = HTML(string=html_content).write_pdf()
- filename = f"rapport_reunions_{annee}_{mois:02d}.pdf"
-
- return StreamingResponse(
- io.BytesIO(pdf_bytes),
- media_type="application/pdf",
- headers={"Content-Disposition": f'inline; filename="{filename}"'},
- )
-
-
-@router.get("/disponibles")
-async def rapports_disponibles(
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """Liste les mois pour lesquels des rapports sont disponibles."""
- # Mois avec collectes
- result = await db.execute(
- select(
- extract("year", Collecte.date_collecte).label("annee"),
- extract("month", Collecte.date_collecte).label("mois"),
- ).where(Collecte.groupe_id == membre.groupe_id)
- .group_by("annee", "mois")
- .order_by(extract("year", Collecte.date_collecte).desc(), extract("month", Collecte.date_collecte).desc())
- )
- mois_finances = [{"annee": int(r.annee), "mois": int(r.mois)} for r in result.all()]
-
- # Mois avec réunions
- result = await db.execute(
- select(
- extract("year", Reunion.date_reunion).label("annee"),
- extract("month", Reunion.date_reunion).label("mois"),
- ).where(Reunion.groupe_id == membre.groupe_id)
- .group_by("annee", "mois")
- .order_by(extract("year", Reunion.date_reunion).desc(), extract("month", Reunion.date_reunion).desc())
- )
- mois_reunions = [{"annee": int(r.annee), "mois": int(r.mois)} for r in result.all()]
-
- # Mois avec contributions
- result = await db.execute(
- select(
- extract("year", EnvoiContribution.date_envoi).label("annee"),
- extract("month", EnvoiContribution.date_envoi).label("mois"),
- ).where(EnvoiContribution.groupe_id == membre.groupe_id)
- .group_by("annee", "mois")
- )
- mois_contributions = [{"annee": int(r.annee), "mois": int(r.mois)} for r in result.all()]
-
- # Combiner et dédupliquer
- all_mois = {}
- for m in mois_finances:
- key = f"{m['annee']}-{m['mois']:02d}"
- all_mois[key] = {"annee": m["annee"], "mois": m["mois"], "finances": True, "reunions": False, "contributions": False}
- for m in mois_reunions:
- key = f"{m['annee']}-{m['mois']:02d}"
- if key in all_mois:
- all_mois[key]["reunions"] = True
- else:
- all_mois[key] = {"annee": m["annee"], "mois": m["mois"], "finances": False, "reunions": True, "contributions": False}
- for m in mois_contributions:
- key = f"{m['annee']}-{m['mois']:02d}"
- if key in all_mois:
- all_mois[key]["contributions"] = True
- else:
- all_mois[key] = {"annee": m["annee"], "mois": m["mois"], "finances": False, "reunions": False, "contributions": True}
-
- return sorted(all_mois.values(), key=lambda x: (x["annee"], x["mois"]), reverse=True)
-
-
-@router.get("/financier-complet/{annee}/{mois}")
-async def rapport_financier_complet(
- annee: int,
- mois: int,
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """Rapport financier complet incluant collectes, dépenses et contributions."""
- gid = membre.groupe_id
-
- # Collectes
- result = await db.execute(
- select(Collecte).where(
- Collecte.groupe_id == gid,
- extract("year", Collecte.date_collecte) == annee,
- extract("month", Collecte.date_collecte) == mois,
- )
- )
- collectes = result.scalars().all()
- total_collectes = sum(float(c.montant) for c in collectes)
-
- # Dépenses
- result = await db.execute(
- select(Depense).where(
- Depense.groupe_id == gid,
- extract("year", Depense.date_depense) == annee,
- extract("month", Depense.date_depense) == mois,
- )
- )
- depenses = result.scalars().all()
- total_depenses = sum(float(d.montant) for d in depenses)
- depenses_impayees = sum(float(d.montant) for d in depenses if not d.paye)
-
- # Contributions
- result = await db.execute(
- select(EnvoiContribution).where(
- EnvoiContribution.groupe_id == gid,
- extract("year", EnvoiContribution.date_envoi) == annee,
- extract("month", EnvoiContribution.date_envoi) == mois,
- )
- )
- contributions = result.scalars().all()
- total_contributions = sum(float(c.montant) for c in contributions)
-
- solde = total_collectes - total_depenses - total_contributions
-
- return {
- "mois": mois,
- "annee": annee,
- "total_collectes": round(total_collectes, 2),
- "total_depenses": round(total_depenses, 2),
- "total_contributions": round(total_contributions, 2),
- "solde_mois": round(solde, 2),
- "nb_collectes": len(collectes),
- "nb_depenses": len(depenses),
- "nb_contributions": len(contributions),
- "depenses_impayees": round(depenses_impayees, 2),
- }
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/rapports_rsg.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/rapports_rsg.py
deleted file mode 100644
index 9a206ba..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/rapports_rsg.py
+++ /dev/null
@@ -1,95 +0,0 @@
-"""Routes M3 — Rapport du RSG."""
-from fastapi import APIRouter, Depends, HTTPException
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select, desc
-
-from app.core.database import get_db
-from app.core.security import get_current_membre
-from app.models.rapport_rsg import RapportRsg
-from app.models.membre import Membre
-from app.models.affectation import Affectation
-from app.models.poste import Poste
-from app.schemas.phase4 import RapportRsgCreate, RapportRsgOut
-
-router = APIRouter(prefix="/rapports-rsg", tags=["M3 — Rapport RSG"])
-
-
-async def require_rsg(membre=Depends(get_current_membre), db=Depends(get_db)):
- """Vérifie que le membre est RSG, RSG substitut ou exécutif."""
- result = await db.execute(
- select(Affectation).join(Poste).where(
- Affectation.membre_id == membre.id,
- Affectation.statut == "actif",
- (Poste.nom.ilike("%RSG%") | (Poste.categorie == "executif")),
- )
- )
- if not result.scalar_one_or_none():
- raise HTTPException(status_code=403, detail="Réservé au RSG ou à l'exécutif")
- return membre
-
-
-async def _to_out(r: RapportRsg, db: AsyncSession) -> RapportRsgOut:
- redacteur = await db.get(Membre, r.redacteur_id)
- return RapportRsgOut(
- id=r.id,
- reunion_dist=r.reunion_dist,
- redacteur_prenom=redacteur.prenom if redacteur else "?",
- decisions=r.decisions,
- infos_region=r.infos_region,
- sujets_groupe=r.sujets_groupe,
- cree_le=r.cree_le,
- )
-
-
-@router.get("/", response_model=list[RapportRsgOut])
-async def liste_rapports_rsg(
- limite: int = 12,
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """Lister les rapports RSG du groupe."""
- result = await db.execute(
- select(RapportRsg)
- .where(RapportRsg.groupe_id == membre.groupe_id)
- .order_by(desc(RapportRsg.reunion_dist))
- .limit(limite)
- )
- return [await _to_out(r, db) for r in result.scalars().all()]
-
-
-@router.post("/", response_model=RapportRsgOut)
-async def creer_rapport_rsg(
- req: RapportRsgCreate,
- membre=Depends(require_rsg),
- db: AsyncSession = Depends(get_db),
-):
- """M3 — Rédiger un rapport de réunion de district."""
- rapport = RapportRsg(
- groupe_id=membre.groupe_id,
- reunion_dist=req.reunion_dist,
- redacteur_id=membre.id,
- decisions=req.decisions,
- infos_region=req.infos_region,
- sujets_groupe=req.sujets_groupe,
- )
- db.add(rapport)
- await db.flush()
- return await _to_out(rapport, db)
-
-
-@router.get("/{rapport_id}", response_model=RapportRsgOut)
-async def detail_rapport_rsg(
- rapport_id: str,
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- result = await db.execute(
- select(RapportRsg).where(
- RapportRsg.id == rapport_id,
- RapportRsg.groupe_id == membre.groupe_id,
- )
- )
- rapport = result.scalar_one_or_none()
- if not rapport:
- raise HTTPException(status_code=404, detail="Rapport introuvable")
- return await _to_out(rapport, db)
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/reunions.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/reunions.py
deleted file mode 100644
index da37a1d..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/reunions.py
+++ /dev/null
@@ -1,113 +0,0 @@
-"""Routes S4 — Rapport de réunion."""
-from fastapi import APIRouter, Depends, HTTPException
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select, desc
-
-from app.core.database import get_db
-from app.core.security import get_current_membre
-from app.models.reunion import Reunion
-from app.models.membre import Membre
-from app.models.affectation import Affectation
-from app.models.poste import Poste
-from app.schemas.phase1 import ReunionCreate, ReunionOut
-
-router = APIRouter(prefix="/reunions", tags=["S4 — Rapport de réunion"])
-
-
-async def require_animateur(membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db)):
- """Vérifie que le membre est animateur ou exécutif."""
- result = await db.execute(
- select(Affectation).join(Poste).where(
- Affectation.membre_id == membre.id,
- Affectation.statut == "actif",
- (Poste.nom.ilike("%animateur%") | (Poste.categorie == "executif")),
- )
- )
- if not result.scalar_one_or_none():
- raise HTTPException(status_code=403, detail="Réservé à l'animateur ou à l'exécutif")
- return membre
-
-
-@router.post("/", response_model=ReunionOut)
-async def creer_rapport_reunion(
- req: ReunionCreate,
- membre=Depends(require_animateur),
- db: AsyncSession = Depends(get_db),
-):
- """S4 — L'animateur note le thème et les annonces."""
- reunion = Reunion(
- groupe_id=membre.groupe_id,
- type=req.type,
- date_reunion=req.date_reunion,
- animateur_id=membre.id,
- 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,
- animateur_prenom=membre.prenom,
- theme=reunion.theme,
- annonces=reunion.annonces,
- note=reunion.note,
- cree_le=reunion.cree_le,
- )
-
-
-@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,
- ))
- return out
-
-
-@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,
- )
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/rotations.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/rotations.py
deleted file mode 100644
index 6492958..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/rotations.py
+++ /dev/null
@@ -1,125 +0,0 @@
-"""Routes G5 — Planifier les rotations hebdomadaires."""
-from datetime import date, timedelta
-from fastapi import APIRouter, Depends, HTTPException
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select, delete
-
-from app.core.database import get_db
-from app.core.security import require_executif, get_current_membre
-from app.models.rotation import Rotation
-from app.models.poste import Poste
-from app.models.membre import Membre
-from app.schemas.phase3 import RotationCreate, RotationOut
-
-router = APIRouter(prefix="/rotations", tags=["G5 — Rotations"])
-
-
-async def _to_out(r: Rotation, db: AsyncSession) -> RotationOut:
- poste = await db.get(Poste, r.poste_id)
- mbr = await db.get(Membre, r.membre_id)
- return RotationOut(
- id=r.id,
- date_reunion=r.date_reunion,
- poste_nom=poste.nom if poste else "?",
- membre_prenom=mbr.prenom if mbr else "?",
- note=r.note,
- cree_le=r.cree_le,
- )
-
-
-@router.get("/", response_model=list[RotationOut])
-async def liste_rotations(
- semaines: int = 4,
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """Rotations des prochaines semaines."""
- aujourdhui = date.today()
- fin = aujourdhui + timedelta(weeks=semaines)
- result = await db.execute(
- select(Rotation).where(
- Rotation.groupe_id == membre.groupe_id,
- Rotation.date_reunion >= aujourdhui,
- Rotation.date_reunion <= fin,
- ).order_by(Rotation.date_reunion, Rotation.poste_id)
- )
- return [await _to_out(r, db) for r in result.scalars().all()]
-
-
-@router.get("/historique", response_model=list[RotationOut])
-async def historique_rotations(
- semaines: int = 4,
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """Rotations passées."""
- aujourdhui = date.today()
- debut = aujourdhui - timedelta(weeks=semaines)
- result = await db.execute(
- select(Rotation).where(
- Rotation.groupe_id == membre.groupe_id,
- Rotation.date_reunion >= debut,
- Rotation.date_reunion < aujourdhui,
- ).order_by(Rotation.date_reunion.desc())
- )
- return [await _to_out(r, db) for r in result.scalars().all()]
-
-
-@router.post("/", response_model=RotationOut)
-async def creer_rotation(
- req: RotationCreate,
- executif=Depends(require_executif),
- db: AsyncSession = Depends(get_db),
-):
- """G5 — Assigner un serviteur pour une date de réunion."""
- rotation = Rotation(
- groupe_id=executif.groupe_id,
- date_reunion=req.date_reunion,
- poste_id=req.poste_id,
- membre_id=req.membre_id,
- note=req.note,
- cree_par=executif.id,
- )
- db.add(rotation)
- await db.flush()
- return await _to_out(rotation, db)
-
-
-@router.post("/batch", response_model=list[RotationOut])
-async def creer_rotations_batch(
- rotations: list[RotationCreate],
- executif=Depends(require_executif),
- db: AsyncSession = Depends(get_db),
-):
- """G5 — Assigner plusieurs serviteurs d'un coup pour une semaine."""
- created = []
- for req in rotations:
- rotation = Rotation(
- groupe_id=executif.groupe_id,
- date_reunion=req.date_reunion,
- poste_id=req.poste_id,
- membre_id=req.membre_id,
- note=req.note,
- cree_par=executif.id,
- )
- db.add(rotation)
- await db.flush()
- created.append(await _to_out(rotation, db))
- return created
-
-
-@router.delete("/{rotation_id}")
-async def supprimer_rotation(
- rotation_id: str,
- executif=Depends(require_executif),
- db: AsyncSession = Depends(get_db),
-):
- """Supprimer une rotation planifiée."""
- result = await db.execute(
- select(Rotation).where(Rotation.id == rotation_id, Rotation.groupe_id == executif.groupe_id)
- )
- rotation = result.scalar_one_or_none()
- if not rotation:
- raise HTTPException(status_code=404, detail="Rotation introuvable")
- await db.delete(rotation)
- return {"message": "Rotation supprimée"}
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/sondages.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/sondages.py
deleted file mode 100644
index 2bd81c2..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/sondages.py
+++ /dev/null
@@ -1,219 +0,0 @@
-"""Routes G6/C6 — Sondages et votes de conscience de groupe."""
-from datetime import datetime, timezone
-from fastapi import APIRouter, Depends, HTTPException
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select, desc, func
-
-from app.core.database import get_db
-from app.core.security import get_current_membre, require_executif
-from app.models.sondage import Sondage
-from app.models.option_sondage import OptionSondage
-from app.models.vote import Vote
-from app.models.membre import Membre
-from app.schemas.phase8 import SondageCreate, SondageOut, SondageUpdate, OptionOut, VoteCreate
-
-router = APIRouter(prefix="/sondages", tags=["G6/C6 — Sondages et votes"])
-
-TYPES_VALIDES = {"oui_non", "choix_multiple", "unanimite"}
-
-
-async def _build_sondage_out(sondage: Sondage, membre_id, db: AsyncSession) -> SondageOut:
- """Construit la sortie complète d'un sondage avec comptage des votes."""
- cree = await db.get(Membre, sondage.cree_par) if sondage.cree_par else None
-
- # Options avec comptage
- result = await db.execute(
- select(OptionSondage)
- .where(OptionSondage.sondage_id == sondage.id)
- .order_by(OptionSondage.ordre)
- )
- options = result.scalars().all()
-
- options_out = []
- total = 0
- for opt in options:
- count_result = await db.execute(
- select(func.count(Vote.id)).where(Vote.option_id == opt.id)
- )
- nb = count_result.scalar() or 0
- total += nb
- options_out.append(OptionOut(
- id=opt.id, texte=opt.texte, ordre=opt.ordre, nb_votes=nb
- ))
-
- # Le membre a-t-il voté?
- vote_result = await db.execute(
- select(Vote).where(Vote.sondage_id == sondage.id, Vote.membre_id == membre_id)
- )
- a_vote = vote_result.scalar_one_or_none() is not None
-
- return SondageOut(
- id=sondage.id,
- titre=sondage.titre,
- description=sondage.description,
- type=sondage.type,
- cree_par_prenom=cree.prenom if cree else None,
- date_ouverture=sondage.date_ouverture,
- date_fermeture=sondage.date_fermeture,
- statut=sondage.statut,
- options=options_out,
- total_votes=total,
- a_vote=a_vote,
- )
-
-
-@router.post("/", response_model=SondageOut)
-async def creer_sondage(
- req: SondageCreate,
- membre=Depends(require_executif),
- db: AsyncSession = Depends(get_db),
-):
- """G6 — Créer un sondage de conscience de groupe."""
- if req.type not in TYPES_VALIDES:
- raise HTTPException(status_code=400, detail=f"Type invalide. Choix: {list(TYPES_VALIDES)}")
-
- if req.date_fermeture <= req.date_ouverture:
- raise HTTPException(status_code=400, detail="La date de fermeture doit être après l'ouverture")
-
- # Pour oui_non, créer automatiquement les options
- options_data = req.options
- if req.type == "oui_non" and len(options_data) == 0:
- options_data = [
- SondageCreate.model_fields["options"].default.__class__(texte="Oui"),
- SondageCreate.model_fields["options"].default.__class__(texte="Non"),
- ]
- # Workaround: créer manuellement
- from app.schemas.phase8 import OptionCreate
- options_data = [OptionCreate(texte="Oui"), OptionCreate(texte="Non")]
-
- if len(options_data) < 2:
- raise HTTPException(status_code=400, detail="Au moins 2 options sont requises")
-
- sondage = Sondage(
- groupe_id=membre.groupe_id,
- titre=req.titre.strip(),
- description=req.description,
- type=req.type,
- cree_par=membre.id,
- date_ouverture=req.date_ouverture,
- date_fermeture=req.date_fermeture,
- statut="brouillon",
- )
- db.add(sondage)
- await db.flush()
-
- for i, opt in enumerate(options_data):
- option = OptionSondage(
- sondage_id=sondage.id,
- texte=opt.texte.strip(),
- ordre=i,
- )
- db.add(option)
- await db.flush()
-
- return await _build_sondage_out(sondage, membre.id, db)
-
-
-@router.get("/", response_model=list[SondageOut])
-async def liste_sondages(
- statut: str | None = None,
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """Lister les sondages du groupe."""
- query = select(Sondage).where(Sondage.groupe_id == membre.groupe_id)
- if statut:
- query = query.where(Sondage.statut == statut)
- query = query.order_by(desc(Sondage.date_ouverture))
-
- result = await db.execute(query)
- sondages = result.scalars().all()
-
- return [await _build_sondage_out(s, membre.id, db) for s in sondages]
-
-
-@router.get("/{sondage_id}", response_model=SondageOut)
-async def detail_sondage(
- sondage_id: str,
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- result = await db.execute(
- select(Sondage).where(Sondage.id == sondage_id, Sondage.groupe_id == membre.groupe_id)
- )
- sondage = result.scalar_one_or_none()
- if not sondage:
- raise HTTPException(status_code=404, detail="Sondage introuvable")
- return await _build_sondage_out(sondage, membre.id, db)
-
-
-@router.patch("/{sondage_id}", response_model=SondageOut)
-async def modifier_statut_sondage(
- sondage_id: str,
- req: SondageUpdate,
- membre=Depends(require_executif),
- db: AsyncSession = Depends(get_db),
-):
- """Ouvrir ou fermer un sondage."""
- result = await db.execute(
- select(Sondage).where(Sondage.id == sondage_id, Sondage.groupe_id == membre.groupe_id)
- )
- sondage = result.scalar_one_or_none()
- if not sondage:
- raise HTTPException(status_code=404, detail="Sondage introuvable")
-
- if req.statut and req.statut in ("brouillon", "ouvert", "ferme"):
- sondage.statut = req.statut
- await db.flush()
-
- return await _build_sondage_out(sondage, membre.id, db)
-
-
-@router.post("/{sondage_id}/voter")
-async def voter(
- sondage_id: str,
- req: VoteCreate,
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """C6 — Voter dans un sondage ouvert. Un membre = un vote."""
- result = await db.execute(
- select(Sondage).where(Sondage.id == sondage_id, Sondage.groupe_id == membre.groupe_id)
- )
- sondage = result.scalar_one_or_none()
- if not sondage:
- raise HTTPException(status_code=404, detail="Sondage introuvable")
-
- if sondage.statut != "ouvert":
- raise HTTPException(status_code=400, detail="Ce sondage n'est pas ouvert au vote")
-
- now = datetime.now(timezone.utc)
- if now < sondage.date_ouverture or now > sondage.date_fermeture:
- raise HTTPException(status_code=400, detail="La période de vote est terminée ou pas encore ouverte")
-
- # Vérifier qu'il n'a pas déjà voté
- existing = await db.execute(
- select(Vote).where(Vote.sondage_id == sondage.id, Vote.membre_id == membre.id)
- )
- if existing.scalar_one_or_none():
- raise HTTPException(status_code=400, detail="Vous avez déjà voté dans ce sondage")
-
- # Vérifier que l'option appartient au sondage
- opt_result = await db.execute(
- select(OptionSondage).where(
- OptionSondage.id == req.option_id,
- OptionSondage.sondage_id == sondage.id,
- )
- )
- if not opt_result.scalar_one_or_none():
- raise HTTPException(status_code=400, detail="Option invalide")
-
- vote = Vote(
- sondage_id=sondage.id,
- option_id=req.option_id,
- membre_id=membre.id,
- )
- db.add(vote)
- await db.flush()
-
- return {"message": "Vote enregistré", "option_id": str(req.option_id)}
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/ventes_litterature.py b/groupe-meditation-corrige/groupe-meditation/backend/app/routers/ventes_litterature.py
deleted file mode 100644
index 960845f..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/routers/ventes_litterature.py
+++ /dev/null
@@ -1,112 +0,0 @@
-"""Routes S3 — Vente de littérature."""
-from fastapi import APIRouter, Depends, HTTPException
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy import select, desc
-
-from app.core.database import get_db
-from app.core.security import get_current_membre
-from app.models.litterature import Litterature
-from app.models.vente_litterature import VenteLitterature
-from app.models.membre import Membre
-from app.models.affectation import Affectation
-from app.models.poste import Poste
-from app.schemas.phase6 import VenteCreate, VenteOut
-
-router = APIRouter(prefix="/ventes-litterature", tags=["S3 — Vente littérature"])
-
-
-async def require_resp_litterature(membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db)):
- """Vérifie que le membre est responsable de la littérature."""
- result = await db.execute(
- select(Affectation).join(Poste).where(
- Affectation.membre_id == membre.id,
- Affectation.statut == "actif",
- Poste.nom.ilike("%littérature%"),
- )
- )
- if not result.scalar_one_or_none():
- raise HTTPException(status_code=403, detail="Réservé au responsable de la littérature")
- return membre
-
-
-@router.post("/", response_model=VenteOut)
-async def enregistrer_vente(
- req: VenteCreate,
- membre=Depends(require_resp_litterature),
- db: AsyncSession = Depends(get_db),
-):
- """S3 — Enregistrer une vente de littérature. Le stock est décrémenté automatiquement."""
- # Vérifier le titre
- result = await db.execute(
- select(Litterature).where(
- Litterature.id == req.litterature_id,
- Litterature.groupe_id == membre.groupe_id,
- )
- )
- item = result.scalar_one_or_none()
- if not item:
- raise HTTPException(status_code=404, detail="Titre introuvable")
-
- if req.quantite <= 0:
- raise HTTPException(status_code=400, detail="La quantité doit être positive")
-
- if item.quantite_stock < req.quantite:
- raise HTTPException(
- status_code=400,
- detail=f"Stock insuffisant ({item.quantite_stock} disponible, {req.quantite} demandé)"
- )
-
- # Décrémenter le stock
- item.quantite_stock -= req.quantite
-
- # Créer la vente
- vente = VenteLitterature(
- litterature_id=req.litterature_id,
- groupe_id=membre.groupe_id,
- quantite=req.quantite,
- montant=req.montant,
- date_vente=req.date_vente,
- vendu_par=membre.id,
- )
- db.add(vente)
- await db.flush()
-
- return VenteOut(
- id=vente.id,
- litterature_titre=item.titre,
- quantite=vente.quantite,
- montant=float(vente.montant),
- date_vente=vente.date_vente,
- vendu_par_prenom=membre.prenom,
- cree_le=vente.cree_le,
- )
-
-
-@router.get("/", response_model=list[VenteOut])
-async def liste_ventes(
- limite: int = 20,
- membre=Depends(get_current_membre),
- db: AsyncSession = Depends(get_db),
-):
- """Lister les ventes récentes."""
- result = await db.execute(
- select(VenteLitterature)
- .where(VenteLitterature.groupe_id == membre.groupe_id)
- .order_by(desc(VenteLitterature.date_vente))
- .limit(limite)
- )
- ventes = result.scalars().all()
- out = []
- for v in ventes:
- item = await db.get(Litterature, v.litterature_id)
- vendeur = await db.get(Membre, v.vendu_par) if v.vendu_par else None
- out.append(VenteOut(
- id=v.id,
- litterature_titre=item.titre if item else "?",
- quantite=v.quantite,
- montant=float(v.montant),
- date_vente=v.date_vente,
- vendu_par_prenom=vendeur.prenom if vendeur else None,
- cree_le=v.cree_le,
- ))
- return out
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/__init__.py b/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/phase1.py b/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/phase1.py
deleted file mode 100644
index 508929f..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/phase1.py
+++ /dev/null
@@ -1,74 +0,0 @@
-"""Schémas Phase 1 — Saisies hebdomadaires."""
-from pydantic import BaseModel
-from datetime import date, datetime
-from uuid import UUID
-from typing import Optional
-
-
-# ── S1 Collecte ──
-class CollecteCreate(BaseModel):
- date_collecte: date
- montant: float
- reunion_id: Optional[UUID] = None
-
-
-class CollecteOut(BaseModel):
- id: UUID
- date_collecte: date
- montant: float
- saisi_par_prenom: Optional[str] = None
- cree_le: datetime
-
- model_config = {"from_attributes": True}
-
-
-# ── S4 Rapport de réunion ──
-class ReunionCreate(BaseModel):
- type: str = "reguliere"
- date_reunion: datetime
- theme: Optional[str] = None
- annonces: Optional[list[str]] = None
- note: Optional[str] = None
-
-
-class ReunionOut(BaseModel):
- id: UUID
- type: str
- date_reunion: datetime
- animateur_prenom: Optional[str] = None
- theme: Optional[str] = None
- annonces: Optional[list[str]] = None
- note: Optional[str] = None
- cree_le: datetime
-
- model_config = {"from_attributes": True}
-
-
-# ── S5 Dépense (6 champs) ──
-class DepenseCreate(BaseModel):
- date_depense: date
- montant: float
- qui: UUID # membre_id
- quoi: str
- paye: bool = False
- methode: str # DU, VIREMENT, CHEQUE, COLLECTE
-
-
-class DepenseOut(BaseModel):
- id: UUID
- date_depense: date
- montant: float
- qui_prenom: Optional[str] = None
- quoi: str
- paye: bool
- methode: str
- saisi_par_prenom: Optional[str] = None
- cree_le: datetime
-
- model_config = {"from_attributes": True}
-
-
-class DepenseUpdate(BaseModel):
- paye: Optional[bool] = None
- methode: Optional[str] = None
- quoi: Optional[str] = None
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/phase2.py b/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/phase2.py
deleted file mode 100644
index cbff8c6..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/phase2.py
+++ /dev/null
@@ -1,63 +0,0 @@
-"""Schémas Phase 2 — Consultation."""
-from pydantic import BaseModel
-from datetime import date, datetime
-from uuid import UUID
-from typing import Optional
-
-
-# ── C5 Décisions ──
-class DecisionCreate(BaseModel):
- sujet: str
- description: Optional[str] = None
- statut: str = "en_attente"
- responsable_id: Optional[UUID] = None
- proposeur: Optional[str] = None
- secondeur: Optional[str] = None
- date_decision: date
- reunion_id: Optional[UUID] = None
-
-
-class DecisionOut(BaseModel):
- id: UUID
- sujet: str
- description: Optional[str] = None
- statut: str
- responsable_prenom: Optional[str] = None
- proposeur: Optional[str] = None
- secondeur: Optional[str] = None
- date_decision: date
- date_completion: Optional[date] = None
- cree_le: datetime
-
- model_config = {"from_attributes": True}
-
-
-class DecisionUpdate(BaseModel):
- statut: Optional[str] = None
- description: Optional[str] = None
- responsable_id: Optional[UUID] = None
- date_completion: Optional[date] = None
-
-
-# ── C2 Calendrier district ──
-class CalendrierEntry(BaseModel):
- groupe_nom: str
- groupe_id: UUID
- jour_reunion: Optional[str] = None
- poste_nom: str
- membre_prenom: str
- date_debut: date
-
-
-# ── C1 Rapports PDF ──
-class RapportRequest(BaseModel):
- type: str # finances, pv, rsg
- mois: int
- annee: int
-
-
-class RapportOut(BaseModel):
- type: str
- mois: int
- annee: int
- url: str
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/phase3.py b/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/phase3.py
deleted file mode 100644
index 34be61b..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/phase3.py
+++ /dev/null
@@ -1,49 +0,0 @@
-"""Schémas Phase 3 — Gestion."""
-from pydantic import BaseModel
-from datetime import date, datetime
-from uuid import UUID
-from typing import Optional
-
-
-# ── G5 Rotations ──
-class RotationCreate(BaseModel):
- date_reunion: date
- poste_id: UUID
- membre_id: UUID
- note: Optional[str] = None
-
-
-class RotationOut(BaseModel):
- id: UUID
- date_reunion: date
- poste_nom: str
- membre_prenom: str
- note: Optional[str] = None
- cree_le: datetime
-
- model_config = {"from_attributes": True}
-
-
-# ── G9 Paramètres du groupe ──
-class GroupeUpdate(BaseModel):
- nom: Optional[str] = None
- adresse_local: Optional[str] = None
- jour_reunion: Optional[str] = None
- heure_reunion: Optional[str] = None
- format_reunion: Optional[str] = None
- lien_aa87: Optional[str] = None
-
-
-class GroupeDetail(BaseModel):
- id: UUID
- nom: str
- district: str
- region: str
- adresse_local: Optional[str] = None
- jour_reunion: Optional[str] = None
- heure_reunion: Optional[str] = None
- format_reunion: Optional[str] = None
- lien_aa87: str
- cree_le: datetime
-
- model_config = {"from_attributes": True}
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/phase4.py b/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/phase4.py
deleted file mode 100644
index c4e2be3..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/phase4.py
+++ /dev/null
@@ -1,91 +0,0 @@
-"""Schémas Phase 4 — PV, Présences, Rapport RSG."""
-from pydantic import BaseModel
-from datetime import date, datetime
-from uuid import UUID
-from typing import Optional
-
-
-# ── M2 Présences (réunion d'affaires) ──
-class PresenceItem(BaseModel):
- membre_id: UUID
- present: bool = True
-
-
-class PresencesBatch(BaseModel):
- reunion_id: UUID
- presences: list[PresenceItem]
-
-
-class PresenceOut(BaseModel):
- membre_id: UUID
- membre_prenom: str
- present: bool
-
-
-class ReunionAffairesCreate(BaseModel):
- """Créer une réunion d'affaires (pour les présences et le PV)."""
- date_reunion: datetime
- note: Optional[str] = None
-
-
-class ReunionAffairesOut(BaseModel):
- id: UUID
- date_reunion: datetime
- note: Optional[str] = None
- presences: list[PresenceOut] = []
-
- model_config = {"from_attributes": True}
-
-
-# ── M1 Procès-verbal ──
-class DecisionPV(BaseModel):
- sujet: str
- decision: str
- proposeur: Optional[str] = None
- secondeur: Optional[str] = None
- statut: str = "en_attente"
-
-
-class PvCreate(BaseModel):
- reunion_id: UUID
- contenu: str
- decisions: Optional[list[DecisionPV]] = None
-
-
-class PvOut(BaseModel):
- id: UUID
- reunion_id: UUID
- redacteur_prenom: str
- contenu: str
- decisions: Optional[list[dict]] = None
- approuve: bool
- cree_le: datetime
- date_reunion: Optional[datetime] = None
-
- model_config = {"from_attributes": True}
-
-
-class PvUpdate(BaseModel):
- contenu: Optional[str] = None
- decisions: Optional[list[DecisionPV]] = None
- approuve: Optional[bool] = None
-
-
-# ── M3 Rapport RSG ──
-class RapportRsgCreate(BaseModel):
- reunion_dist: date
- decisions: Optional[list[str]] = None
- infos_region: Optional[str] = None
- sujets_groupe: Optional[str] = None
-
-
-class RapportRsgOut(BaseModel):
- id: UUID
- reunion_dist: date
- redacteur_prenom: str
- decisions: Optional[list[str]] = None
- infos_region: Optional[str] = None
- sujets_groupe: Optional[str] = None
- cree_le: datetime
-
- model_config = {"from_attributes": True}
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/phase5.py b/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/phase5.py
deleted file mode 100644
index 845f3b5..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/phase5.py
+++ /dev/null
@@ -1,56 +0,0 @@
-"""Schémas Phase 5 — Jetons, Anniversaires, Commande jetons."""
-from pydantic import BaseModel
-from datetime import date, datetime
-from uuid import UUID
-from typing import Optional
-
-
-# ── S2 Remise de jeton ──
-class JetonCreate(BaseModel):
- membre_id: Optional[UUID] = None # None pour jeton de désir anonyme
- type_jeton: str # desir, 24h, 1mois, 2mois, 3mois, 6mois, 9mois, 1an, multiple
- date_remise: date
- annees: Optional[int] = None # requis si type_jeton='multiple'
-
-
-class JetonOut(BaseModel):
- id: UUID
- membre_prenom: Optional[str] = None
- type_jeton: str
- date_remise: date
- annees: Optional[int] = None
- remis_par_prenom: Optional[str] = None
- cree_le: datetime
-
- model_config = {"from_attributes": True}
-
-
-# ── C3 Anniversaires district ──
-class AnniversaireOut(BaseModel):
- membre_prenom: str
- groupe_nom: str
- date_abstinence: date
- annees_sobriete: int
- prochain_anniversaire: date
- jours_restants: int
-
-
-# ── M6 Commande de jetons ──
-class StockJetonsOut(BaseModel):
- type_jeton: str
- label: str
- remis_dernier_mois: int
- stock_suggestion: str # "OK" ou "Bas — commander"
-
-
-TYPES_JETONS = [
- {"value": "desir", "label": "Désir", "emoji": "💫", "ordre": 1},
- {"value": "24h", "label": "24 heures", "emoji": "🌅", "ordre": 2},
- {"value": "1mois", "label": "1 mois", "emoji": "🥉", "ordre": 3},
- {"value": "2mois", "label": "2 mois", "emoji": "🥉", "ordre": 4},
- {"value": "3mois", "label": "3 mois", "emoji": "🥈", "ordre": 5},
- {"value": "6mois", "label": "6 mois", "emoji": "🥈", "ordre": 6},
- {"value": "9mois", "label": "9 mois", "emoji": "🥇", "ordre": 7},
- {"value": "1an", "label": "1 an", "emoji": "⭐", "ordre": 8},
- {"value": "multiple", "label": "Multiple", "emoji": "🏆", "ordre": 9},
-]
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/phase6.py b/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/phase6.py
deleted file mode 100644
index fcef551..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/phase6.py
+++ /dev/null
@@ -1,61 +0,0 @@
-"""Schémas Phase 6 — Littérature : inventaire, vente, ajustement."""
-from pydantic import BaseModel
-from datetime import date, datetime
-from uuid import UUID
-from typing import Optional
-
-
-# ── Littérature (inventaire) ──
-class LitteratureCreate(BaseModel):
- titre: str
- categorie: str # livre, brochure, depliant, pamphlet
- prix_coutant: Optional[float] = None
- quantite_stock: int = 0
- seuil_commande: int = 2
- code_bsg: Optional[str] = None
-
-
-class LitteratureOut(BaseModel):
- id: UUID
- titre: str
- categorie: str
- prix_coutant: Optional[float] = None
- quantite_stock: int
- seuil_commande: int
- code_bsg: Optional[str] = None
- stock_bas: bool = False
-
- model_config = {"from_attributes": True}
-
-
-class LitteratureUpdate(BaseModel):
- titre: Optional[str] = None
- categorie: Optional[str] = None
- prix_coutant: Optional[float] = None
- seuil_commande: Optional[int] = None
- code_bsg: Optional[str] = None
-
-
-# ── M5 Ajuster inventaire ──
-class AjustementStock(BaseModel):
- quantite: int # positif = ajout, négatif = retrait
-
-
-# ── S3 Vente littérature ──
-class VenteCreate(BaseModel):
- litterature_id: UUID
- quantite: int
- montant: float
- date_vente: date
-
-
-class VenteOut(BaseModel):
- id: UUID
- litterature_titre: str
- quantite: int
- montant: float
- date_vente: date
- vendu_par_prenom: Optional[str] = None
- cree_le: datetime
-
- model_config = {"from_attributes": True}
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/phase7.py b/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/phase7.py
deleted file mode 100644
index ba6df1b..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/phase7.py
+++ /dev/null
@@ -1,82 +0,0 @@
-"""Schémas Phase 7 — Finances avancé : contributions, répartition, rapport complet."""
-from pydantic import BaseModel
-from datetime import date, datetime
-from uuid import UUID
-from typing import Optional
-
-
-DESTINATIONS = ["district", "region", "bsg", "intergroupe"]
-DESTINATIONS_LABELS = {
- "district": "District 87-16",
- "region": "Région 87",
- "bsg": "BSG",
- "intergroupe": "Intergroupe",
-}
-METHODES_CONTRIBUTION = ["DU", "VIREMENT", "CHEQUE"]
-
-
-# ── M4 Envoi de contributions ──
-class EnvoiContributionCreate(BaseModel):
- date_envoi: date
- destination: str # district, region, bsg, intergroupe
- montant: float
- methode: str # DU, VIREMENT, CHEQUE
-
-
-class EnvoiContributionOut(BaseModel):
- id: UUID
- date_envoi: date
- destination: str
- destination_label: str
- montant: float
- methode: str
- saisi_par_prenom: Optional[str] = None
- cree_le: datetime
-
- model_config = {"from_attributes": True}
-
-
-class CalculRepartitionIn(BaseModel):
- """Montant à répartir — le backend calcule les montants par destination."""
- montant_total: float
-
-
-class CalculRepartitionOut(BaseModel):
- destination: str
- destination_label: str
- pourcentage: float
- montant: float
-
-
-# ── G8 Config répartition ──
-class ConfigRepartitionItem(BaseModel):
- destination: str
- pourcentage: float
-
-
-class ConfigRepartitionOut(BaseModel):
- destination: str
- destination_label: str
- pourcentage: float
- actif_depuis: date
-
- model_config = {"from_attributes": True}
-
-
-class ConfigRepartitionBatch(BaseModel):
- """Mettre à jour toutes les répartitions d'un coup."""
- items: list[ConfigRepartitionItem]
-
-
-# ── Rapport financier complet ──
-class RapportFinancierComplet(BaseModel):
- mois: int
- annee: int
- total_collectes: float
- total_depenses: float
- total_contributions: float
- solde_mois: float
- nb_collectes: int
- nb_depenses: int
- nb_contributions: int
- depenses_impayees: float
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/phase8.py b/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/phase8.py
deleted file mode 100644
index 86ef565..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/phase8.py
+++ /dev/null
@@ -1,78 +0,0 @@
-"""Schémas Phase 8 — Votes (sondages) et parrainage (jumelages)."""
-from pydantic import BaseModel
-from datetime import date, datetime
-from uuid import UUID
-from typing import Optional
-
-
-# ── G6 Lancer un sondage ──
-class OptionCreate(BaseModel):
- texte: str
-
-
-class SondageCreate(BaseModel):
- titre: str
- description: Optional[str] = None
- type: str = "oui_non" # oui_non, choix_multiple, unanimite
- date_ouverture: datetime
- date_fermeture: datetime
- options: list[OptionCreate] = []
-
-
-class OptionOut(BaseModel):
- id: UUID
- texte: str
- ordre: int
- nb_votes: int = 0
-
- model_config = {"from_attributes": True}
-
-
-class SondageOut(BaseModel):
- id: UUID
- titre: str
- description: Optional[str] = None
- type: str
- cree_par_prenom: Optional[str] = None
- date_ouverture: datetime
- date_fermeture: datetime
- statut: str
- options: list[OptionOut] = []
- total_votes: int = 0
- a_vote: bool = False # le membre courant a-t-il voté?
-
- model_config = {"from_attributes": True}
-
-
-class SondageUpdate(BaseModel):
- statut: Optional[str] = None # ouvert, ferme
-
-
-# ── C6 Voter ──
-class VoteCreate(BaseModel):
- option_id: UUID
-
-
-# ── G7 Jumelage parrain/filleul ──
-class JumelageCreate(BaseModel):
- parrain_id: UUID
- filleul_id: UUID
- date_jumelage: date
- note: Optional[str] = None
-
-
-class JumelageOut(BaseModel):
- id: UUID
- parrain_prenom: str
- filleul_prenom: str
- date_jumelage: date
- statut: str
- note: Optional[str] = None
- cree_par_prenom: Optional[str] = None
-
- model_config = {"from_attributes": True}
-
-
-class JumelageUpdate(BaseModel):
- statut: Optional[str] = None # actif, termine
- note: Optional[str] = None
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/schemas.py b/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/schemas.py
deleted file mode 100644
index 32c7145..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/app/schemas/schemas.py
+++ /dev/null
@@ -1,134 +0,0 @@
-"""Schémas Pydantic pour validation et sérialisation."""
-from pydantic import BaseModel
-from datetime import date, datetime
-from uuid import UUID
-from typing import Optional
-
-
-# ── Auth ──
-class ConnexionRequest(BaseModel):
- prenom: str
- pin: str
- groupe_id: UUID
-
-
-class InscriptionRequest(BaseModel):
- prenom: str
- nom: str
- pin: str
- code_invitation: str
- telephone: Optional[str] = None
- courriel: Optional[str] = None
- date_abstinence: Optional[date] = None
-
-
-class TokenResponse(BaseModel):
- access_token: str
- token_type: str = "bearer"
- membre: "MembrePublic"
-
-
-# ── Membre ──
-class MembrePublic(BaseModel):
- id: UUID
- prenom: str
- groupe_id: UUID
- date_abstinence: Optional[date] = None
-
- model_config = {"from_attributes": True}
-
-
-class MembreComplet(MembrePublic):
- nom: str
- telephone: Optional[str] = None
- courriel: Optional[str] = None
- actif: bool
- cree_le: datetime
-
- model_config = {"from_attributes": True}
-
-
-class MembreUpdate(BaseModel):
- telephone: Optional[str] = None
- courriel: Optional[str] = None
- date_abstinence: Optional[date] = None
-
-
-class ChangerPinRequest(BaseModel):
- ancien_pin: str
- nouveau_pin: str
-
-
-# ── Poste / Affectation ──
-class PosteOut(BaseModel):
- id: UUID
- nom: str
- categorie: str
- description: Optional[str] = None
- ordre_affichage: Optional[int] = None
-
- model_config = {"from_attributes": True}
-
-
-class AffectationOut(BaseModel):
- id: UUID
- membre_id: UUID
- poste_id: UUID
- poste_nom: Optional[str] = None
- poste_categorie: Optional[str] = None
- date_debut: date
- date_fin: Optional[date] = None
- statut: str
-
- model_config = {"from_attributes": True}
-
-
-class AffectationCreate(BaseModel):
- membre_id: UUID
- poste_id: UUID
- date_debut: date
-
-
-class TerminerMandatRequest(BaseModel):
- note_transition: Optional[str] = None
- successeur_id: Optional[UUID] = None
-
-
-# ── Accueil ──
-class ModuleTuile(BaseModel):
- code: str
- nom: str
- description: str
- icone: str
- couleur: str
-
-
-class AccueilResponse(BaseModel):
- membre: MembrePublic
- postes: list[str]
- modules_consultation: list[ModuleTuile]
- modules_saisie: list[ModuleTuile]
- modules_gestion: list[ModuleTuile]
-
-
-# ── Invitation ──
-class InvitationCreate(BaseModel):
- pass # groupe_id vient du token
-
-
-class InvitationOut(BaseModel):
- code: str
- expire_le: datetime
-
- model_config = {"from_attributes": True}
-
-
-# ── Groupe ──
-class GroupeOut(BaseModel):
- id: UUID
- nom: str
- district: str
- region: str
- jour_reunion: Optional[str] = None
-
- model_config = {"from_attributes": True}
diff --git a/groupe-meditation-corrige/groupe-meditation/backend/requirements.txt b/groupe-meditation-corrige/groupe-meditation/backend/requirements.txt
deleted file mode 100644
index 8479f9e..0000000
--- a/groupe-meditation-corrige/groupe-meditation/backend/requirements.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-fastapi==0.115.6
-uvicorn[standard]==0.34.0
-sqlalchemy[asyncio]==2.0.36
-asyncpg==0.30.0
-pydantic-settings==2.7.1
-python-jose[cryptography]==3.3.0
-bcrypt==4.2.1
-weasyprint==62.3
-pywebpush==2.0.1
diff --git a/groupe-meditation-corrige/groupe-meditation/deploy/apache-groupe-meditation.conf b/groupe-meditation-corrige/groupe-meditation/deploy/apache-groupe-meditation.conf
deleted file mode 100644
index 5322c4a..0000000
--- a/groupe-meditation-corrige/groupe-meditation/deploy/apache-groupe-meditation.conf
+++ /dev/null
@@ -1,37 +0,0 @@
-
- ServerName groupe-meditation.87-16.org
- DocumentRoot /var/www/groupe-meditation
-
-
- Options -Indexes
- AllowOverride None
- Require all granted
-
- RewriteEngine On
- RewriteBase /
- RewriteRule ^index\.html$ - [L]
-
- # Ne jamais réécrire les appels API
- RewriteCond %{REQUEST_URI} !^/api/
- RewriteCond %{REQUEST_FILENAME} !-f
- RewriteCond %{REQUEST_FILENAME} !-d
- RewriteRule . /index.html [L]
-
-
- ProxyPreserveHost On
- ProxyRequests Off
-
- ProxyPass /api http://127.0.0.1:8000/api
- ProxyPassReverse /api http://127.0.0.1:8000/api
-
-
- Header set Cache-Control "public, max-age=31536000, immutable"
-
-
-
- Header set Cache-Control "no-cache, no-store, must-revalidate"
-
-
- ErrorLog ${APACHE_LOG_DIR}/groupe-meditation-error.log
- CustomLog ${APACHE_LOG_DIR}/groupe-meditation-access.log combined
-
diff --git a/groupe-meditation-corrige/groupe-meditation/deploy/backup.sh b/groupe-meditation-corrige/groupe-meditation/deploy/backup.sh
deleted file mode 100644
index ff9a6c8..0000000
--- a/groupe-meditation-corrige/groupe-meditation/deploy/backup.sh
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/bin/bash
-# Sauvegarde quotidienne PostgreSQL — Groupe Méditation
-# À placer dans /etc/cron.daily/ ou via crontab
-
-BACKUP_DIR="/var/backups/groupe-meditation"
-RETENTION_DAYS=30
-DB_NAME="groupe_meditation"
-DB_USER="grpmed"
-DATE=$(date +%Y-%m-%d_%H%M)
-
-mkdir -p "$BACKUP_DIR"
-
-# Dump
-pg_dump -U "$DB_USER" -Fc "$DB_NAME" > "$BACKUP_DIR/grpmed_${DATE}.dump"
-
-# Nettoyage des anciennes sauvegardes
-find "$BACKUP_DIR" -name "grpmed_*.dump" -mtime +$RETENTION_DAYS -delete
-
-echo "[$(date)] Sauvegarde terminée: grpmed_${DATE}.dump"
diff --git a/groupe-meditation-corrige/groupe-meditation/deploy/groupe-meditation.service b/groupe-meditation-corrige/groupe-meditation/deploy/groupe-meditation.service
deleted file mode 100644
index 6a76ccf..0000000
--- a/groupe-meditation-corrige/groupe-meditation/deploy/groupe-meditation.service
+++ /dev/null
@@ -1,19 +0,0 @@
-[Unit]
-Description=Groupe Méditation API
-After=network.target postgresql.service
-Requires=postgresql.service
-
-[Service]
-Type=simple
-User=www-data
-Group=www-data
-WorkingDirectory=/opt/groupe-meditation/backend
-Environment=PYTHONUNBUFFERED=1
-Environment=PATH=/opt/groupe-meditation/backend/venv/bin
-EnvironmentFile=-/opt/groupe-meditation/backend/.env
-ExecStart=/opt/groupe-meditation/backend/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000
-Restart=always
-RestartSec=5
-
-[Install]
-WantedBy=multi-user.target
diff --git a/groupe-meditation-corrige/groupe-meditation/deploy/nginx-groupe-meditation.conf b/groupe-meditation-corrige/groupe-meditation/deploy/nginx-groupe-meditation.conf
deleted file mode 100644
index 27dc6b8..0000000
--- a/groupe-meditation-corrige/groupe-meditation/deploy/nginx-groupe-meditation.conf
+++ /dev/null
@@ -1,50 +0,0 @@
-# Groupe Méditation — Configuration NGINX
-# Reverse proxy vers Apache (frontend) et FastAPI (backend)
-
-server {
- listen 443 ssl http2;
- server_name groupe-meditation.chezlepro.ca;
-
- # Certificat Let's Encrypt
- ssl_certificate /etc/letsencrypt/live/groupe-meditation.chezlepro.ca/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/groupe-meditation.chezlepro.ca/privkey.pem;
-
- # Headers de sécurité
- add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
- add_header X-Content-Type-Options nosniff always;
- add_header X-Frame-Options DENY always;
- add_header X-XSS-Protection "1; mode=block" always;
- add_header Referrer-Policy strict-origin-when-cross-origin always;
- add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' fonts.googleapis.com; font-src fonts.gstatic.com; img-src 'self' data:; connect-src 'self'" always;
-
- # Frontend — Apache sert les fichiers statiques
- location / {
- proxy_pass http://127.0.0.1:8080;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- }
-
- # Backend API — FastAPI via Uvicorn
- location /api/ {
- proxy_pass http://127.0.0.1:8000;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
-
- # Rate limiting anti-brute force (PIN)
- limit_req zone=api_limit burst=10 nodelay;
- }
-
- # Taille max des uploads
- client_max_body_size 5M;
-}
-
-# Redirection HTTP → HTTPS
-server {
- listen 80;
- server_name groupe-meditation.chezlepro.ca;
- return 301 https://$host$request_uri;
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/deploy/seed.py b/groupe-meditation-corrige/groupe-meditation/deploy/seed.py
deleted file mode 100644
index 73b128f..0000000
--- a/groupe-meditation-corrige/groupe-meditation/deploy/seed.py
+++ /dev/null
@@ -1,182 +0,0 @@
-"""
-Script d'initialisation — Groupe Méditation
-Crée les postes, le groupe initial et le premier membre.
-Usage: python deploy/seed.py
-"""
-import asyncio
-import sys
-import os
-import uuid
-import secrets
-from datetime import date, datetime, timedelta, timezone
-
-# 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
-# Importer TOUS les modèles pour que create_all crée toutes les tables
-from app.models.groupe import Groupe
-from app.models.membre import Membre
-from app.models.poste import Poste
-from app.models.affectation import Affectation
-from app.models.invitation import Invitation
-from app.models.collecte import Collecte
-from app.models.depense import Depense
-from app.models.reunion import Reunion
-from app.models.presence import Presence
-from app.models.pv_reunion import PvReunion
-from app.models.decision import Decision
-from app.models.rotation import Rotation
-from app.models.rapport_rsg import RapportRsg
-from app.models.jeton import Jeton
-from app.models.litterature import Litterature
-from app.models.vente_litterature import VenteLitterature
-from app.models.envoi_contribution import EnvoiContribution
-from app.models.config_repartition import ConfigRepartition
-from app.models.sondage import Sondage
-from app.models.option_sondage import OptionSondage
-from app.models.vote import Vote
-from app.models.jumelage import Jumelage
-from app.models.sync_log import SyncLog
-
-
-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),
-]
-
-
-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 Méditation ═══\n")
- groupe = Groupe(
- nom="Groupe Méditation",
- district="87-16",
- region="87",
- adresse_local="",
- jour_reunion="lundi",
- heure_reunion=None,
- 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(
- 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 = input(" Nom: ").strip().title()
- pin = input(" PIN (4 chiffres): ").strip()
- while not pin.isdigit() or len(pin) != 4:
- pin = input(" PIN invalide. 4 chiffres: ").strip()
-
- 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,
- pin_hash=hash_pin(pin),
- )
- db.add(membre)
- await db.flush()
-
- # ── Affectation ──
- 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)
-
- # ── Répartition par défaut (30/30/30/10) ──
- for dest, pct in [("district", 30), ("region", 30), ("bsg", 30), ("intergroupe", 10)]:
- db.add(ConfigRepartition(
- groupe_id=groupe.id,
- destination=dest,
- pourcentage=pct,
- actif_depuis=date.today(),
- ))
- print(" ✓ Répartition contributions : 30/30/30/10")
-
- # ── Littérature de base ──
- for titre, cat, prix, code in [
- ("Les Alcooliques anonymes (Gros Livre)", "livre", 12.00, "B-1"),
- ("Les Douze Étapes et les Douze Traditions", "livre", 11.00, "B-2"),
- ("Vivre sans alcool", "livre", 9.00, "B-7"),
- ("Les AA sont-ils pour vous?", "brochure", 0.00, "P-3"),
- ("Un nouveau arrive", "depliant", 0.00, "P-16"),
- ]:
- db.add(Litterature(
- groupe_id=groupe.id,
- titre=titre,
- categorie=cat,
- prix_coutant=prix,
- quantite_stock=0,
- seuil_commande=2,
- code_bsg=code,
- ))
- print(" ✓ Littérature de base : 5 titres")
-
- await db.commit()
-
- print(f"\n═══ Initialisation terminée ═══")
- print(f" Membre: {prenom} ({nom})")
- print(f" Groupe: Groupe Méditation")
- print(f" Poste: {poste_choisi}")
- 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" Prénom: {prenom}")
- print(f" PIN: {pin}")
-
-
-if __name__ == "__main__":
- asyncio.run(seed())
diff --git a/groupe-meditation-corrige/groupe-meditation/docs/DEPLOIEMENT.md b/groupe-meditation-corrige/groupe-meditation/docs/DEPLOIEMENT.md
deleted file mode 100644
index ab26515..0000000
--- a/groupe-meditation-corrige/groupe-meditation/docs/DEPLOIEMENT.md
+++ /dev/null
@@ -1,211 +0,0 @@
-# Guide de déploiement — Groupe Méditation
-
-## Infrastructure cible
-
-Déploiement validé pour une machine applicative Debian/Ubuntu avec :
-
-- PostgreSQL local
-- FastAPI/Uvicorn local sur `127.0.0.1:8000`
-- Apache local sur `*:80`
-- un proxy inverse externe sur une autre machine
-- FQDN public : `groupe-meditation.87-16.org`
-
-Le principe est simple :
-
-- Apache sert le frontend React sur `/`
-- Apache relaie `/api` vers FastAPI
-- le proxy externe publie ensuite `groupe-meditation.87-16.org`
-
----
-
-## Prérequis
-
-- Debian 12+ ou Ubuntu 24.04+
-- PostgreSQL
-- Python 3.11+
-- Node.js 20+ pour le build frontend
-- Apache 2.4+ avec `rewrite`, `headers`, `proxy`, `proxy_http`
-
----
-
-## 1. Installer PostgreSQL
-
-```bash
-sudo apt update
-sudo apt install -y postgresql postgresql-client
-
-sudo systemctl enable postgresql
-sudo systemctl start postgresql
-
-sudo -u postgres psql << 'SQL'
-CREATE USER grpmed WITH PASSWORD 'CHANGEZ_CE_MOT_DE_PASSE';
-CREATE DATABASE groupe_meditation OWNER grpmed;
-GRANT ALL PRIVILEGES ON DATABASE groupe_meditation TO grpmed;
-\c groupe_meditation
-GRANT ALL ON SCHEMA public TO grpmed;
-SQL
-```
-
----
-
-## 2. Installer Python et les dépendances
-
-```bash
-sudo apt install -y python3 python3-pip python3-venv
-sudo apt install -y libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf2.0-0 libffi-dev libcairo2
-
-sudo mkdir -p /opt/groupe-meditation
-sudo chown $USER:$USER /opt/groupe-meditation
-
-cp -r backend/ /opt/groupe-meditation/backend/
-cp -r deploy/ /opt/groupe-meditation/deploy/
-
-cd /opt/groupe-meditation/backend
-python3 -m venv venv
-source venv/bin/activate
-pip install -r requirements.txt
-```
-
----
-
-## 3. Configurer le backend
-
-```bash
-cd /opt/groupe-meditation/backend
-cp .env.example .env
-nano .env
-```
-
-Exemple minimal :
-
-```env
-DATABASE_URL=postgresql+asyncpg://grpmed:VOTRE_MOT_DE_PASSE@localhost:5432/groupe_meditation
-JWT_SECRET=GENEREZ_AVEC_openssl_rand_base64_48
-CORS_ORIGINS=["http://groupe-meditation.87-16.org","https://groupe-meditation.87-16.org"]
-INVITATION_EXPIRE_HOURS=48
-```
-
-Générer le secret JWT :
-
-```bash
-openssl rand -base64 48
-```
-
----
-
-## 4. Initialiser la base
-
-```bash
-cd /opt/groupe-meditation
-source backend/venv/bin/activate
-python deploy/seed.py
-```
-
----
-
-## 5. Créer le service systemd
-
-```bash
-sudo cp deploy/groupe-meditation.service /etc/systemd/system/
-sudo chown -R www-data:www-data /opt/groupe-meditation
-sudo systemctl daemon-reload
-sudo systemctl enable groupe-meditation
-sudo systemctl start groupe-meditation
-sudo systemctl status groupe-meditation --no-pager
-```
-
-Vérifier l'API locale :
-
-```bash
-curl -s http://127.0.0.1:8000/api/health | python3 -m json.tool
-```
-
----
-
-## 6. Construire le frontend
-
-```bash
-cd frontend/
-npm install
-npm run build
-
-sudo mkdir -p /var/www/groupe-meditation
-sudo cp -r dist/* /var/www/groupe-meditation/
-sudo chown -R www-data:www-data /var/www/groupe-meditation
-```
-
----
-
-## 7. Configurer Apache
-
-Activer les modules requis :
-
-```bash
-sudo a2enmod rewrite headers proxy proxy_http
-```
-
-Installer le vhost :
-
-```bash
-sudo cp deploy/apache-groupe-meditation.conf /etc/apache2/sites-available/groupe-meditation.conf
-sudo a2ensite groupe-meditation.conf
-sudo apachectl -t
-sudo systemctl reload apache2
-```
-
-Apache doit :
-
-- servir le frontend statique depuis `/var/www/groupe-meditation`
-- relayer `/api` vers `127.0.0.1:8000`
-- laisser le proxy externe publier le site
-
----
-
-## 8. Vérifications locales
-
-Tester directement FastAPI :
-
-```bash
-curl -s http://127.0.0.1:8000/api/health | python3 -m json.tool
-```
-
-Tester via Apache local :
-
-```bash
-curl -s -H "Host: groupe-meditation.87-16.org" http://127.0.0.1/api/health | python3 -m json.tool
-curl -I -H "Host: groupe-meditation.87-16.org" http://127.0.0.1/
-```
-
-Ensuite seulement, tester via le proxy externe :
-
-- `http://groupe-meditation.87-16.org`
-- ou `https://groupe-meditation.87-16.org` si le TLS est terminé sur le proxy externe
-
----
-
-## 9. Sauvegarde
-
-```bash
-sudo cp deploy/backup.sh /etc/cron.daily/groupe-meditation-backup
-sudo chmod +x /etc/cron.daily/groupe-meditation-backup
-```
-
----
-
-## Dépannage rapide
-
-| Problème | Commande |
-|----------|----------|
-| API ne démarre pas | `sudo journalctl -u groupe-meditation -f` |
-| Apache mal configuré | `sudo apachectl -t && sudo apachectl -S` |
-| API locale KO | `curl -v http://127.0.0.1:8000/api/health` |
-| API via Apache KO | `curl -v -H "Host: groupe-meditation.87-16.org" http://127.0.0.1/api/health` |
-| Frontend à reconstruire | `cd frontend && npm run build && sudo cp -r dist/* /var/www/groupe-meditation/` |
-
----
-
-## Notes importantes
-
-- pas de NGINX local dans cette variante
-- pas de Certbot local dans cette variante
-- le fichier `deploy/nginx-groupe-meditation.conf` peut servir d'exemple pour le proxy inverse externe, mais il n'est pas requis sur la machine applicative
diff --git a/groupe-meditation-corrige/groupe-meditation/docs/RUNBOOK.md b/groupe-meditation-corrige/groupe-meditation/docs/RUNBOOK.md
deleted file mode 100644
index 75c1274..0000000
--- a/groupe-meditation-corrige/groupe-meditation/docs/RUNBOOK.md
+++ /dev/null
@@ -1,164 +0,0 @@
-# Runbook opérationnel — Groupe Méditation
-
-## Opérations courantes
-
-### Inviter un nouveau membre
-1. Un membre de l'exécutif ouvre le module **G1 — Inviter**
-2. Un code à 12 caractères est généré (expire 48h)
-3. Le code est partagé en personne ou par SMS
-4. Le nouveau membre ouvre l'app → Inscription → entre le code + prénom + nom + PIN
-
-### Affecter un poste
-1. Exécutif ouvre **G3 — Affecter les postes**
-2. Sélectionne le membre et le poste
-3. Le membre voit immédiatement ses nouvelles tuiles sur l'accueil
-
-### Terminer un mandat
-1. Exécutif ouvre **G4 — Terminer un mandat**
-2. Sélectionne l'affectation active
-3. Saisit une note de transition
-4. Les permissions sont retirées instantanément
-5. **Rappel** : les mandats ne se terminent jamais automatiquement
-
-### Saisie hebdomadaire typique (pendant/après la réunion)
-1. **Trésorier** : S1 (collecte) + S5 (dépenses si applicable)
-2. **Animateur** : S4 (thème + annonces)
-3. **Resp. jetons** : S2 (si anniversaire ou nouveau)
-4. **Resp. littérature** : S3 (si vente)
-
-### Réunion d'affaires mensuelle
-1. **Secrétaire** : M2 (présences) → M1 (PV avec décisions)
-2. **RSG** : M3 (rapport du district)
-3. **Trésorier** : présente le rapport financier (C1)
-4. Si vote nécessaire : **G6** (créer sondage) → **C6** (voter)
-
-### Envoi trimestriel des contributions
-1. **Trésorier** ouvre **M4 — Envoi contributions**
-2. Mode « Calculer » : entre le surplus disponible → répartition auto
-3. Mode « Enregistrer » : enregistre chaque envoi individuel
-4. La config des % se fait dans **G8** (défaut 30/30/30/10)
-
----
-
-## Gestion des rapports PDF
-
-Les rapports sont disponibles dans **C1 — Rapports PDF** :
-
-| Rapport | URL API | Contenu |
-|---------|---------|---------|
-| Financier mensuel | `/api/rapports/finances/{annee}/{mois}` | Collectes + dépenses + solde |
-| Réunions du mois | `/api/rapports/reunions/{annee}/{mois}` | Liste des réunions + thèmes |
-| Financier complet | `/api/rapports/financier-complet/{annee}/{mois}` | Collectes + dépenses + contributions + impayés |
-
----
-
-## Surveillance
-
-### Vérifier que l'application tourne
-
-```bash
-# Service FastAPI
-sudo systemctl status groupe-meditation
-
-# Test santé API
-curl -s https://groupe-meditation.chezlepro.ca/api/health
-
-# Logs temps réel
-sudo journalctl -u groupe-meditation -f --no-pager
-
-# NGINX
-sudo systemctl status nginx
-sudo tail -f /var/log/nginx/error.log
-```
-
-### Vérifier les sauvegardes
-
-```bash
-# Dernière sauvegarde
-ls -la /var/backups/groupe-meditation/ | tail -5
-
-# Tester une restauration (sur une base test)
-createdb -U grpmed groupe_meditation_test
-pg_restore -U grpmed -d groupe_meditation_test /var/backups/groupe-meditation/grpmed_DERNIER.dump
-dropdb -U grpmed groupe_meditation_test
-```
-
-### Statistiques base de données
-
-```bash
-sudo -u postgres psql -d groupe_meditation -c "
-SELECT schemaname, tablename, n_live_tup AS lignes
-FROM pg_stat_user_tables
-ORDER BY n_live_tup DESC;
-"
-```
-
----
-
-## Opérations d'urgence
-
-### L'API ne répond plus
-
-```bash
-sudo systemctl restart groupe-meditation
-sudo journalctl -u groupe-meditation --since "5 minutes ago"
-```
-
-### Un membre a oublié son PIN
-
-Pas de récupération en libre-service (par design — sécurité). L'exécutif doit :
-1. Utiliser **G2 — Gérer les membres** pour désactiver le membre
-2. Recréer une invitation **G1**
-3. Le membre se réinscrit avec un nouveau PIN
-
-### Droit à l'oubli (RGPD/Loi 25)
-
-1. Exécutif ouvre **G2 — Gérer les membres**
-2. Sélectionne le membre → **Droit à l'oubli**
-3. Le système supprime les données personnelles
-4. Les données historiques (collectes, PV) sont anonymisées (prénom remplacé)
-
-### Restaurer la base après incident
-
-```bash
-sudo systemctl stop groupe-meditation
-
-# Restaurer la dernière sauvegarde
-sudo -u postgres dropdb groupe_meditation
-sudo -u postgres createdb -O grpmed groupe_meditation
-pg_restore -U grpmed -d groupe_meditation /var/backups/groupe-meditation/grpmed_DERNIER.dump
-
-sudo systemctl start groupe-meditation
-```
-
----
-
-## Matrice des 27 modules par rôle
-
-| Module | Trésorier | Secrétaire | RSG | Animateur | Litt. | Jetons | Tous |
-|--------|:---------:|:----------:|:---:|:---------:|:-----:|:------:|:----:|
-| C1-C7 Consultation | | | | | | | ✓ |
-| S1 Collecte | ✓ | | | | | | |
-| S2 Remise jeton | | | | | | ✓ | |
-| S3 Vente litt. | | | | | ✓ | | |
-| S4 Rapport réunion | | | | ✓ | | | |
-| S5 Dépense | ✓ | | | | | | |
-| M1 PV | | ✓ | | | | | |
-| M2 Présences | | ✓ | | | | | |
-| M3 Rapport RSG | | | ✓ | | | | |
-| M4 Contributions | ✓ | | | | | | |
-| M5 Inventaire | | | | | ✓ | | |
-| M6 Commander jetons | | | | | | ✓ | |
-| G1-G9 Gestion | ✓ | ✓ | ✓ | | | | |
-
----
-
-## Contacts et références
-
-- **aa87.org** — Site de la Région 87
-- **BSG** — Bureau des Services généraux (aa.org)
-- Documentation API : `https://groupe-meditation.chezlepro.ca/api/docs`
-
----
-
-*«Nos leaders sont des serviteurs de confiance ; ils ne gouvernent pas.» — 2e Tradition*
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/index.html b/groupe-meditation-corrige/groupe-meditation/frontend/index.html
deleted file mode 100644
index d92966f..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/index.html
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- Groupe Méditation
-
-
-
-
-
-
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/package.json b/groupe-meditation-corrige/groupe-meditation/frontend/package.json
deleted file mode 100644
index f9d8a0b..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/package.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "name": "groupe-meditation",
- "private": true,
- "version": "0.1.0",
- "description": "Application du Groupe Méditation — District 87-16",
- "type": "module",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "preview": "vite preview"
- },
- "dependencies": {
- "react": "^18.3.1",
- "react-dom": "^18.3.1",
- "zustand": "^5.0.0"
- },
- "devDependencies": {
- "@types/react": "^18.3.0",
- "@types/react-dom": "^18.3.0",
- "@vitejs/plugin-react": "^4.3.0",
- "tailwindcss": "^3.4.0",
- "autoprefixer": "^10.4.0",
- "postcss": "^8.4.0",
- "vite": "^5.4.0",
- "vite-plugin-pwa": "^0.20.0"
- }
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/postcss.config.js b/groupe-meditation-corrige/groupe-meditation/frontend/postcss.config.js
deleted file mode 100644
index 2aa7205..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/postcss.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-export default {
- plugins: {
- tailwindcss: {},
- autoprefixer: {},
- },
-};
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/public/favicon.svg b/groupe-meditation-corrige/groupe-meditation/frontend/public/favicon.svg
deleted file mode 100644
index 7523594..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/public/favicon.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- ☮
-
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/App.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/App.jsx
deleted file mode 100644
index 43b5c4b..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/App.jsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import React, { useEffect } from 'react';
-import { useStore } from './stores/store';
-import Login from './pages/Login';
-import Inscription from './pages/Inscription';
-import Accueil from './pages/Accueil';
-import Profil from './pages/Profil';
-import Collecte from './pages/Collecte';
-import RapportReunion from './pages/RapportReunion';
-import Depense from './pages/Depense';
-import Decisions from './pages/Decisions';
-import Calendrier from './pages/Calendrier';
-import Rapports from './pages/Rapports';
-import InviterMembre from './pages/InviterMembre';
-import GererMembres from './pages/GererMembres';
-import AffecterPostes from './pages/AffecterPostes';
-import TerminerMandat from './pages/TerminerMandat';
-import PlanifierRotations from './pages/PlanifierRotations';
-import Parametres from './pages/Parametres';
-import Presences from './pages/Presences';
-import RedigerPV from './pages/RedigerPV';
-import RapportRSG from './pages/RapportRSG';
-import RemiseJeton from './pages/RemiseJeton';
-import Anniversaires from './pages/Anniversaires';
-import CommanderJetons from './pages/CommanderJetons';
-import VenteLitterature from './pages/VenteLitterature';
-import Inventaire from './pages/Inventaire';
-import AjusterInventaire from './pages/AjusterInventaire';
-import EnvoiContributions from './pages/EnvoiContributions';
-import ConfigRepartition from './pages/ConfigRepartition';
-import LancerSondage from './pages/LancerSondage';
-import VotesOuverts from './pages/VotesOuverts';
-import JumelerParrain from './pages/JumelerParrain';
-
-export default function App() {
- const { token, page, loadAccueil, loadGroupes } = useStore();
-
- useEffect(() => {
- loadGroupes();
- if (token) {
- loadAccueil().then(() => {
- useStore.setState({ page: 'accueil' });
- }).catch(() => {
- useStore.setState({ page: 'login' });
- });
- }
- }, []);
-
- // Pages protégées
- if (token) {
- switch (page) {
- case 'accueil': return ;
- case 'profil': return ;
- case 'S1': return ;
- case 'S4': return ;
- case 'S5': return ;
- case 'C1': return ;
- case 'C2': return ;
- case 'C5': return ;
- case 'G1': return ;
- case 'G2': return ;
- case 'G3': return ;
- case 'G4': return ;
- case 'G5': return ;
- case 'G9': return ;
- case 'M1': return ;
- case 'M2': return ;
- case 'M3': return ;
- case 'S2': return ;
- case 'C3': return ;
- case 'M6': return ;
- case 'S3': return ;
- case 'C4': return ;
- case 'M5': return ;
- case 'M4': return ;
- case 'G8': return ;
- case 'G6': return ;
- case 'C6': return ;
- case 'G7': return ;
- default: return ;
- }
- }
-
- // Pages publiques
- switch (page) {
- case 'inscription': return ;
- default: return ;
- }
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/components/PageHeader.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/components/PageHeader.jsx
deleted file mode 100644
index 5edef2a..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/components/PageHeader.jsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from 'react';
-import { useStore } from '../stores/store';
-
-export default function PageHeader({ titre, sousTitre }) {
- const { setPage } = useStore();
- return (
-
-
setPage('accueil')} className="text-2xl active:opacity-60">←
-
-
{titre}
- {sousTitre &&
{sousTitre}
}
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/components/SuccessCheck.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/components/SuccessCheck.jsx
deleted file mode 100644
index 822c03e..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/components/SuccessCheck.jsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React, { useEffect } from 'react';
-import { useStore } from '../stores/store';
-
-export default function SuccessCheck({ message, delai = 1500 }) {
- const { setPage } = useStore();
-
- useEffect(() => {
- const t = setTimeout(() => setPage('accueil'), delai);
- return () => clearTimeout(t);
- }, []);
-
- return (
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/index.css b/groupe-meditation-corrige/groupe-meditation/frontend/src/index.css
deleted file mode 100644
index 46e5715..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/index.css
+++ /dev/null
@@ -1,22 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-* {
- box-sizing: border-box;
- -webkit-tap-highlight-color: transparent;
-}
-
-body {
- font-family: 'DM Sans', sans-serif;
- -webkit-font-smoothing: antialiased;
-}
-
-button:active {
- transform: scale(0.97);
-}
-
-@keyframes scaleIn {
- from { transform: scale(0.5); opacity: 0; }
- to { transform: scale(1); opacity: 1; }
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/main.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/main.jsx
deleted file mode 100644
index 47742c1..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/main.jsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react';
-import ReactDOM from 'react-dom/client';
-import App from './App';
-import './index.css';
-
-ReactDOM.createRoot(document.getElementById('root')).render(
-
-
-
-);
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Accueil.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Accueil.jsx
deleted file mode 100644
index 807cf7a..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Accueil.jsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import React from 'react';
-import { useStore } from '../stores/store';
-
-const COULEURS = {
- blue: 'bg-blue-50 border-blue-200 text-blue-700',
- green: 'bg-green-50 border-green-200 text-green-700',
- yellow: 'bg-yellow-50 border-yellow-200 text-yellow-700',
- red: 'bg-red-50 border-red-200 text-red-700',
- purple: 'bg-purple-50 border-purple-200 text-purple-700',
- indigo: 'bg-indigo-50 border-indigo-200 text-indigo-700',
- orange: 'bg-orange-50 border-orange-200 text-orange-700',
- gray: 'bg-gray-50 border-gray-200 text-gray-700',
-};
-
-function Tuile({ module, onClick }) {
- const cls = COULEURS[module.couleur] || COULEURS.gray;
- return (
-
-
-
{module.icone}
-
-
{module.nom}
-
{module.description}
-
-
{module.code}
-
-
- );
-}
-
-function Section({ titre, modules }) {
- if (!modules || modules.length === 0) return null;
- return (
-
-
{titre}
-
- {modules.map((m) => (
- {
- const navigable = ['C7', 'S1', 'S2', 'S3', 'S4', 'S5', 'C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'G1', 'G2', 'G3', 'G4', 'G5', 'G6', 'G7', 'G8', 'G9', 'M1', 'M2', 'M3', 'M4', 'M5', 'M6'];
- if (m.code === 'C7') useStore.setState({ page: 'profil' });
- else if (navigable.includes(m.code)) useStore.setState({ page: m.code });
- else alert(`Module ${m.code} — ${m.nom}\n\n(Sera disponible dans les prochaines phases)`);
- }} />
- ))}
-
-
- );
-}
-
-export default function Accueil() {
- const { membre, postes, modulesConsultation, modulesSaisie, modulesGestion, logout } = useStore();
-
- return (
-
- {/* Header */}
-
-
-
-
Bonjour
-
{membre?.prenom}
-
-
- Déconnexion
-
-
- {postes.length > 0 && (
-
- {postes.map((p) => (
-
- {p}
-
- ))}
-
- )}
- {/* Lien aa87.org */}
-
- 📅 Événements du district →
-
-
-
- {/* Modules */}
-
-
-
-
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/AffecterPostes.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/AffecterPostes.jsx
deleted file mode 100644
index 6669e4e..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/AffecterPostes.jsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { api } from '../stores/api';
-import PageHeader from '../components/PageHeader';
-import SuccessCheck from '../components/SuccessCheck';
-
-export default function AffecterPostes() {
- const [postes, setPostes] = useState([]);
- const [membres, setMembres] = useState([]);
- const [affectations, setAffectations] = useState([]);
- const [form, setForm] = useState({ membre_id: '', poste_id: '', date_debut: new Date().toISOString().slice(0, 10) });
- const [loading, setLoading] = useState(true);
- const [success, setSuccess] = useState(false);
- const [error, setError] = useState('');
-
- useEffect(() => {
- Promise.all([
- api.get('/postes/'),
- api.get('/membres/actifs'),
- api.get('/postes/affectations/groupe'),
- ]).then(([p, m, a]) => {
- setPostes(p);
- setMembres(m);
- setAffectations(a);
- setLoading(false);
- }).catch(() => setLoading(false));
- }, []);
-
- const handleSubmit = async () => {
- if (!form.membre_id || !form.poste_id) {
- setError('Sélectionnez un membre et un poste');
- return;
- }
- setError('');
- try {
- await api.post('/postes/affectations', form);
- setSuccess(true);
- } catch (e) {
- setError(e.message);
- }
- };
-
- if (success) return ;
-
- const CATEGORIES = { executif: '🔵', service: '🟣', physique: '🟢' };
-
- return (
-
-
-
-
- {/* Affectations actuelles */}
- {affectations.length > 0 && (
-
-
Affectations actives
-
- {affectations.map((a) => (
-
-
-
{CATEGORIES[a.poste_categorie] || '⚪'} {a.poste_nom}
-
{a.membre_prenom} · depuis {a.date_debut}
-
-
- ))}
-
-
- )}
-
- {/* Nouvelle affectation */}
-
-
Nouvelle affectation
-
-
- Membre
- setForm({ ...form, membre_id: e.target.value })}
- className="w-full p-3 bg-gray-50 rounded-xl text-lg border-0 focus:ring-2 focus:ring-aa-blue">
- Sélectionner un membre
- {membres.map((m) => {m.prenom} )}
-
-
-
-
- Poste
- setForm({ ...form, poste_id: e.target.value })}
- className="w-full p-3 bg-gray-50 rounded-xl text-lg border-0 focus:ring-2 focus:ring-aa-blue">
- Sélectionner un poste
- {postes.map((p) => (
- {CATEGORIES[p.categorie] || '⚪'} {p.nom}
- ))}
-
-
-
-
- Date de début
- setForm({ ...form, date_debut: e.target.value })}
- className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue" />
-
-
-
- {error &&
{error}
}
-
-
- 🎯 Affecter le poste
-
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/AjusterInventaire.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/AjusterInventaire.jsx
deleted file mode 100644
index 2c5c17c..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/AjusterInventaire.jsx
+++ /dev/null
@@ -1,226 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { api } from '../stores/api';
-import PageHeader from '../components/PageHeader';
-import SuccessCheck from '../components/SuccessCheck';
-
-const CATEGORIES = [
- { value: 'livre', label: 'Livre', emoji: '📕' },
- { value: 'brochure', label: 'Brochure', emoji: '📄' },
- { value: 'depliant', label: 'Dépliant', emoji: '📃' },
- { value: 'pamphlet', label: 'Pamphlet', emoji: '📑' },
-];
-
-export default function AjusterInventaire() {
- const [items, setItems] = useState([]);
- const [loading, setLoading] = useState(true);
- const [success, setSuccess] = useState('');
- const [error, setError] = useState('');
- const [showAjout, setShowAjout] = useState(false);
-
- const charger = async () => {
- try {
- const data = await api.get('/litterature/');
- setItems(data);
- } catch { setItems([]); }
- setLoading(false);
- };
-
- useEffect(() => { charger(); }, []);
-
- const ajusterStock = async (itemId, quantite) => {
- setError('');
- try {
- await api.patch(`/litterature/${itemId}/stock`, { quantite });
- setSuccess('Stock ajusté ✓');
- setTimeout(() => { setSuccess(''); charger(); }, 1000);
- } catch (e) {
- setError(e.message);
- }
- };
-
- if (success) return ;
-
- return (
-
-
-
-
- {/* Bouton ajouter un titre */}
-
setShowAjout(!showAjout)}
- className="w-full p-3 bg-purple-100 text-purple-700 rounded-2xl text-sm font-bold active:bg-purple-200 transition"
- >
- {showAjout ? '✕ Annuler' : '+ Ajouter un titre au catalogue'}
-
-
- {showAjout &&
{ setShowAjout(false); charger(); }} />}
-
- {error && {error}
}
-
- {loading && Chargement...
}
-
- {/* Liste des titres avec ajustement */}
- {items.map((item) => (
-
- ))}
-
- {!loading && items.length === 0 && (
-
-
📦
-
Aucun titre dans le catalogue
-
- )}
-
-
- );
-}
-
-function ItemStock({ item, onAjuster }) {
- const [ajustement, setAjustement] = useState('');
-
- return (
-
-
-
-
{item.titre}
-
{item.categorie}
-
-
-
- {item.quantite_stock}
-
-
en stock
-
-
-
- {/* Ajustement rapide */}
-
-
onAjuster(item.id, -1)}
- className="w-12 h-12 bg-red-100 text-red-700 rounded-xl text-xl font-bold active:bg-red-200"
- >
- −
-
-
- setAjustement(e.target.value)}
- className="flex-1 p-2 bg-gray-50 rounded-xl text-center border-0 focus:ring-2 focus:ring-purple-400"
- />
- {
- if (ajustement && parseInt(ajustement) !== 0) {
- onAjuster(item.id, parseInt(ajustement));
- setAjustement('');
- }
- }}
- disabled={!ajustement}
- className="px-4 bg-purple-600 text-white rounded-xl text-sm font-bold disabled:opacity-40 active:bg-purple-700"
- >
- OK
-
-
-
onAjuster(item.id, 1)}
- className="w-12 h-12 bg-green-100 text-green-700 rounded-xl text-xl font-bold active:bg-green-200"
- >
- +
-
-
-
- );
-}
-
-function FormAjoutTitre({ onSuccess }) {
- const [form, setForm] = useState({
- titre: '', categorie: 'livre', prix_coutant: '', quantite_stock: '0', code_bsg: '',
- });
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState('');
-
- const update = (k, v) => setForm({ ...form, [k]: v });
-
- const handleSubmit = async () => {
- if (!form.titre.trim()) { setError('Le titre est obligatoire'); return; }
- setLoading(true);
- setError('');
- try {
- await api.post('/litterature/', {
- titre: form.titre.trim(),
- categorie: form.categorie,
- prix_coutant: form.prix_coutant ? parseFloat(form.prix_coutant) : null,
- quantite_stock: parseInt(form.quantite_stock) || 0,
- code_bsg: form.code_bsg.trim() || null,
- });
- onSuccess();
- } catch (e) {
- setError(e.message);
- setLoading(false);
- }
- };
-
- return (
-
-
update('titre', e.target.value)}
- className="w-full p-3 bg-white rounded-xl border-0 focus:ring-2 focus:ring-purple-400"
- autoFocus
- />
-
- {CATEGORIES.map((c) => (
- update('categorie', c.value)}
- className={`p-2 rounded-xl text-xs font-bold border-2 ${
- form.categorie === c.value
- ? 'bg-purple-600 text-white border-purple-600'
- : 'bg-white text-gray-600 border-gray-200'
- }`}
- >
- {c.emoji} {c.label}
-
- ))}
-
-
-
update('code_bsg', e.target.value)}
- className="w-full p-2 bg-white rounded-xl border-0 focus:ring-2 focus:ring-purple-400"
- />
- {error &&
{error}
}
-
- {loading ? '...' : '📚 Ajouter au catalogue'}
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Anniversaires.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Anniversaires.jsx
deleted file mode 100644
index 2e2b499..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Anniversaires.jsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { api } from '../stores/api';
-import PageHeader from '../components/PageHeader';
-
-export default function Anniversaires() {
- const [anniversaires, setAnniversaires] = useState([]);
- const [loading, setLoading] = useState(true);
- const [filtre, setFiltre] = useState(90); // jours
-
- useEffect(() => {
- charger();
- }, [filtre]);
-
- const charger = async () => {
- setLoading(true);
- try {
- const data = await api.get(`/anniversaires/?prochains_jours=${filtre}`);
- setAnniversaires(data);
- } catch {
- setAnniversaires([]);
- }
- setLoading(false);
- };
-
- const emojiAnnees = (annees) => {
- if (annees >= 20) return '💎';
- if (annees >= 10) return '🏆';
- if (annees >= 5) return '⭐';
- if (annees >= 1) return '🥇';
- return '🌟';
- };
-
- const aujourdhui = anniversaires.filter((a) => a.jours_restants === 0);
- const prochains = anniversaires.filter((a) => a.jours_restants > 0);
-
- return (
-
-
-
-
- {/* Filtres */}
-
- {[30, 90, 365].map((j) => (
- setFiltre(j)}
- className={`flex-1 py-2 rounded-xl text-sm font-bold transition border-2 ${
- filtre === j
- ? 'bg-aa-blue text-white border-aa-blue'
- : 'bg-white text-gray-500 border-gray-200'
- }`}
- >
- {j === 30 ? '30 jours' : j === 90 ? '3 mois' : '1 an'}
-
- ))}
-
-
- {loading &&
Chargement...
}
-
- {/* Aujourd'hui */}
- {aujourdhui.length > 0 && (
-
-
- 🎉 Aujourd'hui
-
- {aujourdhui.map((a, i) => (
-
- ))}
-
- )}
-
- {/* Prochains */}
- {prochains.length > 0 && (
-
-
- Prochains anniversaires
-
- {prochains.map((a, i) => (
-
- ))}
-
- )}
-
- {!loading && anniversaires.length === 0 && (
-
-
🎂
-
Aucun anniversaire dans les prochains {filtre} jours
-
- )}
-
-
- );
-}
-
-function AnniversaireCard({ anniversaire: a, emoji, highlight }) {
- return (
-
-
-
{emoji}
-
-
- {a.membre_prenom}
- — {a.annees_sobriete} an{a.annees_sobriete > 1 ? 's' : ''}
-
-
{a.groupe_nom}
-
-
- {a.jours_restants === 0 ? (
-
- Aujourd'hui!
-
- ) : (
-
- dans {a.jours_restants} j
-
- )}
-
-
-
- {a.prochain_anniversaire}
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Calendrier.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Calendrier.jsx
deleted file mode 100644
index 80384d6..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Calendrier.jsx
+++ /dev/null
@@ -1,133 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { api } from '../stores/api';
-import { useStore } from '../stores/store';
-import PageHeader from '../components/PageHeader';
-
-const CATEGORIES = {
- executif: { label: 'Exécutif', bg: 'bg-blue-100', text: 'text-blue-700' },
- service: { label: 'Service', bg: 'bg-purple-100', text: 'text-purple-700' },
- physique: { label: 'Physique', bg: 'bg-green-100', text: 'text-green-700' },
-};
-
-export default function Calendrier() {
- const { groupeId } = useStore();
- const [entries, setEntries] = useState([]);
- const [detailGroupe, setDetailGroupe] = useState(null);
- const [loading, setLoading] = useState(true);
-
- useEffect(() => {
- api.get('/calendrier/').then((data) => {
- setEntries(data);
- setLoading(false);
- }).catch(() => setLoading(false));
- }, []);
-
- // Grouper par groupe
- const parGroupe = {};
- entries.forEach((e) => {
- if (!parGroupe[e.groupe_nom]) {
- parGroupe[e.groupe_nom] = { nom: e.groupe_nom, id: e.groupe_id, jour: e.jour_reunion, serviteurs: [] };
- }
- parGroupe[e.groupe_nom].serviteurs.push(e);
- });
- const groupes = Object.values(parGroupe);
-
- const voirDetail = async (gId) => {
- try {
- const data = await api.get(`/calendrier/groupe/${gId}`);
- setDetailGroupe(data);
- } catch {}
- };
-
- if (detailGroupe) {
- return (
-
-
-
setDetailGroupe(null)} className="text-2xl active:opacity-60">←
-
-
{detailGroupe.groupe?.nom}
-
- {detailGroupe.groupe?.jour_reunion ? `Réunion le ${detailGroupe.groupe.jour_reunion}` : 'Calendrier'}
-
-
-
-
- {detailGroupe.groupe?.adresse && (
-
-
Adresse
-
{detailGroupe.groupe.adresse}
-
- )}
- {detailGroupe.serviteurs?.map((s, i) => {
- const cat = CATEGORIES[s.categorie] || CATEGORIES.physique;
- return (
-
-
-
{s.poste}
-
{s.prenom}
-
-
- {cat.label}
-
-
- );
- })}
- {(!detailGroupe.serviteurs || detailGroupe.serviteurs.length === 0) && (
-
Aucun serviteur assigné
- )}
-
-
- );
- }
-
- return (
-
-
-
-
- {loading ? (
-
Chargement...
- ) : groupes.length === 0 ? (
-
-
📅
-
Aucune donnée de calendrier
-
- ) : (
- groupes.map((g) => {
- const isMon = g.id === groupeId;
- return (
-
voirDetail(g.id)}
- className={`w-full text-left rounded-2xl p-4 shadow-sm active:scale-[0.98] transition ${
- isMon ? 'bg-blue-50 border-2 border-blue-300' : 'bg-white border border-gray-100'
- }`}
- >
-
-
- {isMon && ⭐ }
-
{g.nom}
-
-
→
-
- {g.jour && (
- Réunion le {g.jour}
- )}
-
- {g.serviteurs.slice(0, 4).map((s, i) => (
-
- {s.poste_nom}: {s.membre_prenom}
-
- ))}
- {g.serviteurs.length > 4 && (
- +{g.serviteurs.length - 4}
- )}
-
-
- );
- })
- )}
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Collecte.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Collecte.jsx
deleted file mode 100644
index 39878a2..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Collecte.jsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import React, { useState } from 'react';
-import { api } from '../stores/api';
-import PageHeader from '../components/PageHeader';
-import SuccessCheck from '../components/SuccessCheck';
-
-export default function Collecte() {
- const [montant, setMontant] = useState('');
- const [dateCollecte, setDateCollecte] = useState(new Date().toISOString().slice(0, 10));
- const [loading, setLoading] = useState(false);
- const [success, setSuccess] = useState(false);
- const [error, setError] = useState('');
-
- const handleSubmit = async () => {
- if (!montant || parseFloat(montant) <= 0) {
- setError('Entrez un montant valide');
- return;
- }
- setLoading(true);
- setError('');
- try {
- await api.post('/collectes/', {
- date_collecte: dateCollecte,
- montant: parseFloat(montant),
- });
- setSuccess(true);
- } catch (e) {
- setError(e.message);
- setLoading(false);
- }
- };
-
- if (success) return ;
-
- return (
-
-
-
-
- {/* Date pré-remplie */}
-
- Date
- setDateCollecte(e.target.value)}
- className="w-full p-3 bg-gray-50 rounded-xl text-lg border-0 focus:ring-2 focus:ring-aa-blue"
- />
-
-
- {/* Montant — gros clavier numérique */}
-
-
Montant collecté
-
- $
- setMontant(e.target.value)}
- className="w-full p-4 pl-10 bg-gray-50 rounded-xl text-3xl font-bold text-center border-0 focus:ring-2 focus:ring-green-500"
- autoFocus
- />
-
-
-
- {error &&
{error}
}
-
- {/* Bouton enregistrer */}
-
- {loading ? '...' : '💰 Enregistrer la collecte'}
-
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/CommanderJetons.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/CommanderJetons.jsx
deleted file mode 100644
index 0d30a6a..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/CommanderJetons.jsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { api } from '../stores/api';
-import PageHeader from '../components/PageHeader';
-
-export default function CommanderJetons() {
- const [stock, setStock] = useState([]);
- const [loading, setLoading] = useState(true);
-
- useEffect(() => {
- api.get('/commandes-jetons/stock')
- .then(setStock)
- .catch(() => setStock([]))
- .finally(() => setLoading(false));
- }, []);
-
- const aCommander = stock.filter((s) => s.stock_suggestion !== 'OK');
-
- return (
-
-
-
-
- {loading &&
Chargement...
}
-
- {/* Alerte stock bas */}
- {aCommander.length > 0 && (
-
-
- ⚠️ Jetons à commander ({aCommander.length})
-
-
- {aCommander.map((s) => (
-
- {s.label} — {s.remis_dernier_mois} remis ce mois
-
- ))}
-
-
- )}
-
- {/* Vue complète du stock */}
- {!loading && (
-
-
- Utilisation (30 derniers jours)
-
- {stock.map((s) => (
-
-
{s.label.split(' ')[0]}
-
-
{s.label.split(' ').slice(1).join(' ')}
-
{s.remis_dernier_mois} remis
-
-
- {s.stock_suggestion}
-
-
- ))}
-
- )}
-
- {!loading && stock.length === 0 && (
-
-
🏅
-
Aucune donnée de jetons pour le moment
-
- )}
-
- {/* Info */}
-
-
- 💡 Les suggestions sont basées sur le nombre de jetons remis dans les 30 derniers jours.
- Commandez auprès de l'Intergroupe lorsque le stock est bas.
-
-
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/ConfigRepartition.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/ConfigRepartition.jsx
deleted file mode 100644
index 5c590ca..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/ConfigRepartition.jsx
+++ /dev/null
@@ -1,124 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { api } from '../stores/api';
-import PageHeader from '../components/PageHeader';
-import SuccessCheck from '../components/SuccessCheck';
-
-export default function ConfigRepartition() {
- const [items, setItems] = useState([]);
- const [loading, setLoading] = useState(true);
- const [saving, setSaving] = useState(false);
- const [success, setSuccess] = useState(false);
- const [error, setError] = useState('');
-
- useEffect(() => {
- api.get('/config-repartition/')
- .then(setItems)
- .catch(() => {})
- .finally(() => setLoading(false));
- }, []);
-
- const total = items.reduce((sum, i) => sum + (parseFloat(i.pourcentage) || 0), 0);
-
- const updatePct = (idx, val) => {
- const updated = [...items];
- updated[idx] = { ...updated[idx], pourcentage: val };
- setItems(updated);
- };
-
- const handleSave = async () => {
- if (Math.abs(total - 100) > 0.01) {
- setError(`Le total doit être 100%. Actuellement: ${total.toFixed(2)}%`);
- return;
- }
- setSaving(true);
- setError('');
- try {
- await api.request('PUT', '/config-repartition/', {
- items: items.map((i) => ({
- destination: i.destination,
- pourcentage: parseFloat(i.pourcentage),
- })),
- });
- setSuccess(true);
- } catch (e) {
- setError(e.message);
- setSaving(false);
- }
- };
-
- if (success) return ;
-
- return (
-
-
-
-
- {loading &&
Chargement...
}
-
-
-
- 💡 Répartition recommandée : District 30%, Région 30%, BSG 30%, Intergroupe 10%.
- Le total doit toujours être 100%.
-
-
-
- {items.map((item, idx) => (
-
-
-
{item.destination_label}
-
depuis {item.actif_depuis}
-
-
-
- ))}
-
- {/* Total */}
- {items.length > 0 && (
-
- Total : {total.toFixed(1)}%
- {Math.abs(total - 100) >= 0.01 && (
-
Le total doit être exactement 100%
- )}
-
- )}
-
- {error &&
{error}
}
-
-
= 0.01}
- className="w-full p-5 bg-aa-blue text-white rounded-2xl text-xl font-bold disabled:opacity-40 active:scale-[0.97] transition shadow-lg"
- >
- {saving ? '...' : '⚙️ Enregistrer la répartition'}
-
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Decisions.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Decisions.jsx
deleted file mode 100644
index 93300b2..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Decisions.jsx
+++ /dev/null
@@ -1,112 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { api } from '../stores/api';
-import PageHeader from '../components/PageHeader';
-
-const STATUTS = {
- en_attente: { label: 'En attente', bg: 'bg-yellow-100', text: 'text-yellow-800', emoji: '⏳' },
- en_cours: { label: 'En cours', bg: 'bg-blue-100', text: 'text-blue-800', emoji: '🔄' },
- complete: { label: 'Complété', bg: 'bg-green-100', text: 'text-green-800', emoji: '✓' },
-};
-
-function Badge({ statut }) {
- const s = STATUTS[statut] || STATUTS.en_attente;
- return (
-
- {s.emoji} {s.label}
-
- );
-}
-
-export default function Decisions() {
- const [decisions, setDecisions] = useState([]);
- const [filtre, setFiltre] = useState('ouvertes'); // ouvertes | toutes
- const [loading, setLoading] = useState(true);
-
- const charger = async () => {
- setLoading(true);
- try {
- let url = '/decisions/';
- if (filtre === 'ouvertes') {
- // Charger en_attente et en_cours
- const [att, enc] = await Promise.all([
- api.get('/decisions/?statut=en_attente'),
- api.get('/decisions/?statut=en_cours'),
- ]);
- setDecisions([...enc, ...att]);
- } else {
- const data = await api.get(url);
- setDecisions(data);
- }
- } catch (e) {
- console.error(e);
- }
- setLoading(false);
- };
-
- useEffect(() => { charger(); }, [filtre]);
-
- return (
-
-
-
- {/* Filtres */}
-
-
- {[
- { key: 'ouvertes', label: 'Ouvertes' },
- { key: 'toutes', label: 'Toutes' },
- ].map((f) => (
- setFiltre(f.key)}
- className={`px-4 py-2 rounded-full text-sm font-bold transition ${
- filtre === f.key
- ? 'bg-aa-blue text-white'
- : 'bg-white text-gray-600 border border-gray-200'
- }`}
- >
- {f.label}
-
- ))}
-
-
-
- {/* Liste */}
-
- {loading ? (
-
Chargement...
- ) : decisions.length === 0 ? (
-
-
📌
-
Aucune décision {filtre === 'ouvertes' ? 'ouverte' : ''}
-
- ) : (
- decisions.map((d) => (
-
-
-
-
{d.sujet}
- {d.description && (
-
{d.description}
- )}
-
-
- {d.responsable_prenom && (
- → {d.responsable_prenom}
- )}
- {d.date_decision}
-
- {d.proposeur && (
-
- Proposé par {d.proposeur}{d.secondeur ? `, secondé par ${d.secondeur}` : ''}
-
- )}
-
-
-
- ))
- )}
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Depense.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Depense.jsx
deleted file mode 100644
index 84919b8..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Depense.jsx
+++ /dev/null
@@ -1,178 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { api } from '../stores/api';
-import PageHeader from '../components/PageHeader';
-import SuccessCheck from '../components/SuccessCheck';
-
-const METHODES = [
- { value: 'DU', label: 'DÛ', emoji: '📋' },
- { value: 'VIREMENT', label: 'Virement', emoji: '🏦' },
- { value: 'CHEQUE', label: 'Chèque', emoji: '📝' },
- { value: 'COLLECTE', label: 'Collecte', emoji: '💰' },
-];
-
-export default function Depense() {
- const [form, setForm] = useState({
- date_depense: new Date().toISOString().slice(0, 10),
- montant: '',
- qui: '',
- quoi: '',
- paye: true,
- methode: '',
- });
- const [membres, setMembres] = useState([]);
- const [loading, setLoading] = useState(false);
- const [success, setSuccess] = useState(false);
- const [error, setError] = useState('');
-
- useEffect(() => {
- // Charger la liste des membres pour le dropdown "Qui"
- api.get('/membres/actifs').then(setMembres).catch(() => setMembres([]));
- }, []);
-
- const update = (k, v) => setForm({ ...form, [k]: v });
-
- const handleSubmit = async () => {
- if (!form.montant || !form.qui || !form.quoi.trim() || !form.methode) {
- setError('Tous les champs sont obligatoires');
- return;
- }
- setLoading(true);
- setError('');
- try {
- await api.post('/depenses/', {
- date_depense: form.date_depense,
- montant: parseFloat(form.montant),
- qui: form.qui,
- quoi: form.quoi.trim(),
- paye: form.paye,
- methode: form.methode,
- });
- setSuccess(true);
- } catch (e) {
- setError(e.message);
- setLoading(false);
- }
- };
-
- if (success) return ;
-
- return (
-
-
-
-
- {/* Date */}
-
- Date
- update('date_depense', e.target.value)}
- className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue"
- />
-
-
- {/* Montant */}
-
-
Montant
-
- $
- update('montant', e.target.value)}
- className="w-full p-4 pl-10 bg-gray-50 rounded-xl text-2xl font-bold text-center border-0 focus:ring-2 focus:ring-red-400"
- />
-
-
-
- {/* Qui (dropdown membres) */}
-
- Qui a fait la dépense
- update('qui', e.target.value)}
- className="w-full p-3 bg-gray-50 rounded-xl text-lg border-0 focus:ring-2 focus:ring-aa-blue"
- >
- Sélectionner un membre
- {membres.map((m) => (
- {m.prenom}
- ))}
-
-
-
- {/* Quoi (texte libre) */}
-
- Description
- update('quoi', e.target.value)}
- className="w-full p-3 bg-gray-50 rounded-xl text-lg border-0 focus:ring-2 focus:ring-aa-blue"
- />
-
-
- {/* Méthode (4 boutons radio) */}
-
-
Méthode de paiement
-
- {METHODES.map((m) => (
- update('methode', m.value)}
- className={`p-3 rounded-xl text-sm font-bold transition border-2 ${
- form.methode === m.value
- ? 'bg-aa-blue text-white border-aa-blue'
- : 'bg-gray-50 text-gray-600 border-gray-200 active:bg-gray-100'
- }`}
- >
- {m.emoji} {m.label}
-
- ))}
-
-
-
- {/* Payé (Oui/Non) */}
-
-
Payé ?
-
- update('paye', true)}
- className={`p-3 rounded-xl text-sm font-bold transition border-2 ${
- form.paye ? 'bg-green-600 text-white border-green-600' : 'bg-gray-50 text-gray-600 border-gray-200'
- }`}
- >
- ✓ Oui
-
- update('paye', false)}
- className={`p-3 rounded-xl text-sm font-bold transition border-2 ${
- !form.paye ? 'bg-orange-500 text-white border-orange-500' : 'bg-gray-50 text-gray-600 border-gray-200'
- }`}
- >
- ✗ Non (à rembourser)
-
-
-
-
- {error &&
{error}
}
-
-
- {loading ? '...' : '💸 Enregistrer la dépense'}
-
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/EnvoiContributions.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/EnvoiContributions.jsx
deleted file mode 100644
index 72c7749..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/EnvoiContributions.jsx
+++ /dev/null
@@ -1,243 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { api } from '../stores/api';
-import PageHeader from '../components/PageHeader';
-import SuccessCheck from '../components/SuccessCheck';
-
-const METHODES = [
- { value: 'VIREMENT', label: 'Virement', emoji: '🏦' },
- { value: 'CHEQUE', label: 'Chèque', emoji: '📝' },
- { value: 'DU', label: 'Dû', emoji: '📋' },
-];
-
-export default function EnvoiContributions() {
- const [mode, setMode] = useState('calculer'); // calculer | enregistrer
- const [montantTotal, setMontantTotal] = useState('');
- const [repartition, setRepartition] = useState([]);
- const [envois, setEnvois] = useState([]);
- const [loading, setLoading] = useState(false);
- const [success, setSuccess] = useState(false);
- const [error, setError] = useState('');
-
- // Charger les envois récents
- useEffect(() => {
- api.get('/contributions/?limite=10').then(setEnvois).catch(() => {});
- }, []);
-
- const calculer = async () => {
- if (!montantTotal || parseFloat(montantTotal) <= 0) {
- setError('Entrez un montant valide');
- return;
- }
- setError('');
- try {
- const data = await api.post('/contributions/calculer', {
- montant_total: parseFloat(montantTotal),
- });
- setRepartition(data);
- } catch (e) {
- setError(e.message);
- }
- };
-
- if (success) return ;
-
- return (
-
-
-
-
- {/* Tabs */}
-
- setMode('calculer')}
- className={`flex-1 py-2 rounded-xl text-sm font-bold border-2 ${
- mode === 'calculer' ? 'bg-aa-blue text-white border-aa-blue' : 'bg-white text-gray-500 border-gray-200'
- }`}
- >
- 🧮 Calculer
-
- setMode('enregistrer')}
- className={`flex-1 py-2 rounded-xl text-sm font-bold border-2 ${
- mode === 'enregistrer' ? 'bg-aa-blue text-white border-aa-blue' : 'bg-white text-gray-500 border-gray-200'
- }`}
- >
- 📮 Enregistrer
-
-
-
- {mode === 'calculer' && (
- <>
- {/* Montant à répartir */}
-
-
Montant à répartir
-
- $
- setMontantTotal(e.target.value)}
- className="w-full p-4 pl-10 bg-gray-50 rounded-xl text-2xl font-bold text-center border-0 focus:ring-2 focus:ring-aa-blue"
- />
-
-
-
-
- 🧮 Calculer la répartition
-
-
- {/* Résultat */}
- {repartition.length > 0 && (
-
- {repartition.map((r) => (
-
-
-
{r.destination_label}
-
{r.pourcentage}%
-
-
{r.montant.toFixed(2)} $
-
- ))}
-
- )}
- >
- )}
-
- {mode === 'enregistrer' && (
-
setSuccess(true)}
- onError={setError}
- />
- )}
-
- {error && {error}
}
-
- {/* Historique */}
- {envois.length > 0 && (
-
-
- Derniers envois
-
- {envois.map((e) => (
-
-
-
{e.destination_label}
-
{e.date_envoi} · {e.methode}
-
-
{e.montant.toFixed(2)} $
-
- ))}
-
- )}
-
-
- );
-}
-
-function FormEnvoi({ onSuccess, onError }) {
- const [form, setForm] = useState({
- date_envoi: new Date().toISOString().slice(0, 10),
- destination: '',
- montant: '',
- methode: '',
- });
- const [loading, setLoading] = useState(false);
-
- const DESTINATIONS = [
- { value: 'district', label: 'District 87-16' },
- { value: 'region', label: 'Région 87' },
- { value: 'bsg', label: 'BSG' },
- { value: 'intergroupe', label: 'Intergroupe' },
- ];
-
- const update = (k, v) => setForm({ ...form, [k]: v });
-
- const handleSubmit = async () => {
- if (!form.destination || !form.montant || !form.methode) {
- onError('Tous les champs sont obligatoires');
- return;
- }
- setLoading(true);
- onError('');
- try {
- await api.post('/contributions/', {
- date_envoi: form.date_envoi,
- destination: form.destination,
- montant: parseFloat(form.montant),
- methode: form.methode,
- });
- onSuccess();
- } catch (e) {
- onError(e.message);
- setLoading(false);
- }
- };
-
- return (
-
-
- Date
- update('date_envoi', e.target.value)}
- className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue" />
-
-
-
-
Destination
-
- {DESTINATIONS.map((d) => (
- update('destination', d.value)}
- className={`p-3 rounded-xl text-sm font-bold border-2 ${
- form.destination === d.value
- ? 'bg-aa-blue text-white border-aa-blue'
- : 'bg-gray-50 text-gray-600 border-gray-200'
- }`}
- >
- {d.label}
-
- ))}
-
-
-
-
-
Montant
-
- $
- update('montant', e.target.value)}
- className="w-full p-4 pl-10 bg-gray-50 rounded-xl text-2xl font-bold text-center border-0 focus:ring-2 focus:ring-blue-400" />
-
-
-
-
-
Méthode
-
- {METHODES.map((m) => (
- update('methode', m.value)}
- className={`p-3 rounded-xl text-sm font-bold border-2 ${
- form.methode === m.value
- ? 'bg-aa-blue text-white border-aa-blue'
- : 'bg-gray-50 text-gray-600 border-gray-200'
- }`}
- >
- {m.emoji} {m.label}
-
- ))}
-
-
-
-
- {loading ? '...' : '📮 Enregistrer l\'envoi'}
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/GererMembres.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/GererMembres.jsx
deleted file mode 100644
index 038f289..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/GererMembres.jsx
+++ /dev/null
@@ -1,145 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { api } from '../stores/api';
-import PageHeader from '../components/PageHeader';
-
-export default function GererMembres() {
- const [membres, setMembres] = useState([]);
- const [loading, setLoading] = useState(true);
- const [detail, setDetail] = useState(null);
- const [confirmAction, setConfirmAction] = useState(null);
- const [msg, setMsg] = useState('');
-
- const charger = async () => {
- setLoading(true);
- try {
- const data = await api.get('/membres/');
- setMembres(data);
- } catch {}
- setLoading(false);
- };
-
- useEffect(() => { charger(); }, []);
-
- const desactiver = async (id) => {
- try {
- const res = await api.patch(`/membres/${id}/desactiver`);
- setMsg(res.message);
- setConfirmAction(null);
- setDetail(null);
- charger();
- setTimeout(() => setMsg(''), 3000);
- } catch (e) { alert(e.message); }
- };
-
- const droitOubli = async (id) => {
- try {
- const res = await api.request('DELETE', `/membres/${id}/oubli`);
- setMsg(res.message);
- setConfirmAction(null);
- setDetail(null);
- charger();
- setTimeout(() => setMsg(''), 3000);
- } catch (e) { alert(e.message); }
- };
-
- // Vue détail d'un membre
- if (detail) {
- return (
-
-
- { setDetail(null); setConfirmAction(null); }} className="text-2xl">←
-
{detail.prenom} {detail.nom}
-
-
- {msg &&
{msg}
}
-
-
-
-
-
Téléphone
{detail.telephone || '—'}
-
Courriel
{detail.courriel || '—'}
-
Date d'abstinence
{detail.date_abstinence || '—'}
-
Statut
-
- {detail.actif ? 'Actif' : 'Inactif'}
-
-
-
-
- {/* Contact rapide */}
-
-
- {/* Actions */}
- {detail.actif && !confirmAction && (
-
setConfirmAction('desactiver')}
- className="w-full p-3 bg-orange-50 border border-orange-200 text-orange-700 rounded-xl font-bold text-sm">
- Désactiver ce membre
-
- )}
- {!confirmAction && (
-
setConfirmAction('oubli')}
- className="w-full p-3 bg-red-50 border border-red-200 text-red-700 rounded-xl font-bold text-sm">
- Droit à l'oubli (irréversible)
-
- )}
-
- {/* Confirmations */}
- {confirmAction === 'desactiver' && (
-
-
Désactiver {detail.prenom} ? Le membre ne pourra plus se connecter.
-
- desactiver(detail.id)} className="flex-1 p-3 bg-orange-500 text-white rounded-xl font-bold">Confirmer
- setConfirmAction(null)} className="flex-1 p-3 bg-gray-200 text-gray-700 rounded-xl font-bold">Annuler
-
-
- )}
- {confirmAction === 'oubli' && (
-
-
⚠️ Cette action est irréversible. Toutes les données personnelles de {detail.prenom} seront anonymisées.
-
- droitOubli(detail.id)} className="flex-1 p-3 bg-red-600 text-white rounded-xl font-bold">Supprimer
- setConfirmAction(null)} className="flex-1 p-3 bg-gray-200 text-gray-700 rounded-xl font-bold">Annuler
-
-
- )}
-
-
- );
- }
-
- return (
-
-
-
- {msg &&
{msg}
}
- {loading ? (
-
Chargement...
- ) : (
- membres.map((m) => (
-
setDetail(m)}
- className="w-full bg-white rounded-2xl p-4 shadow-sm flex items-center justify-between active:scale-[0.98] transition">
-
-
{m.prenom} {m.nom}
-
{m.telephone || m.courriel || '—'}
-
- →
-
- ))
- )}
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Inscription.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Inscription.jsx
deleted file mode 100644
index c4591a6..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Inscription.jsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import React, { useState } from 'react';
-import { useStore } from '../stores/store';
-
-export default function Inscription() {
- const { inscription, loading, error, setPage } = useStore();
- const [form, setForm] = useState({
- prenom: '', nom: '', pin: '', code_invitation: '',
- telephone: '', courriel: '', date_abstinence: '',
- });
-
- const update = (k, v) => setForm({ ...form, [k]: v });
-
- const handleSubmit = async (e) => {
- e.preventDefault();
- try {
- await inscription(form);
- } catch {}
- };
-
- return (
-
-
-
Nouveau membre
-
Groupe Méditation
-
-
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Inventaire.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Inventaire.jsx
deleted file mode 100644
index 2ff0d58..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Inventaire.jsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { api } from '../stores/api';
-import PageHeader from '../components/PageHeader';
-
-const CATEGORIES = {
- livre: { label: 'Livres', emoji: '📕' },
- brochure: { label: 'Brochures', emoji: '📄' },
- depliant: { label: 'Dépliants', emoji: '📃' },
- pamphlet: { label: 'Pamphlets', emoji: '📑' },
-};
-
-export default function Inventaire() {
- const [items, setItems] = useState([]);
- const [loading, setLoading] = useState(true);
-
- useEffect(() => {
- api.get('/litterature/')
- .then(setItems)
- .catch(() => setItems([]))
- .finally(() => setLoading(false));
- }, []);
-
- // Grouper par catégorie
- const parCategorie = {};
- for (const item of items) {
- if (!parCategorie[item.categorie]) parCategorie[item.categorie] = [];
- parCategorie[item.categorie].push(item);
- }
-
- const stockBas = items.filter((i) => i.stock_bas);
-
- return (
-
-
-
-
- {loading &&
Chargement...
}
-
- {/* Alerte stock bas */}
- {stockBas.length > 0 && (
-
-
- ⚠️ Stock bas ({stockBas.length} titre{stockBas.length > 1 ? 's' : ''})
-
-
- {stockBas.map((i) => i.titre).join(', ')}
-
-
- )}
-
- {/* Par catégorie */}
- {Object.entries(parCategorie).map(([cat, liste]) => {
- const info = CATEGORIES[cat] || { label: cat, emoji: '📚' };
- return (
-
-
- {info.emoji} {info.label}
-
- {liste.map((item) => (
-
-
-
-
{item.titre}
- {item.code_bsg && (
-
BSG: {item.code_bsg}
- )}
-
-
- {item.prix_coutant != null && (
-
{item.prix_coutant.toFixed(2)} $
- )}
-
- {item.quantite_stock} en stock
-
-
-
-
- ))}
-
- );
- })}
-
- {!loading && items.length === 0 && (
-
-
📚
-
Aucune littérature dans l'inventaire
-
Le responsable de la littérature peut ajouter des titres
-
- )}
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/InviterMembre.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/InviterMembre.jsx
deleted file mode 100644
index a7585a4..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/InviterMembre.jsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import React, { useState } from 'react';
-import { api } from '../stores/api';
-import PageHeader from '../components/PageHeader';
-
-export default function InviterMembre() {
- const [code, setCode] = useState(null);
- const [expiration, setExpiration] = useState(null);
- const [loading, setLoading] = useState(false);
- const [copied, setCopied] = useState(false);
-
- const generer = async () => {
- setLoading(true);
- try {
- const data = await api.post('/invitations/');
- setCode(data.code);
- setExpiration(new Date(data.expire_le).toLocaleString('fr-CA'));
- } catch (e) {
- alert(e.message);
- }
- setLoading(false);
- };
-
- const copier = () => {
- navigator.clipboard.writeText(code).then(() => {
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
- });
- };
-
- const partagerSMS = () => {
- const msg = `Rejoins le Groupe Méditation sur l'app ! Ton code d'invitation : ${code} (expire dans 48h)`;
- window.open(`sms:?body=${encodeURIComponent(msg)}`);
- };
-
- return (
-
-
-
-
- {!code ? (
-
-
➕
-
Générer un code d'invitation
-
Le code expire après 48 heures et ne peut être utilisé qu'une fois
-
- {loading ? '...' : 'Générer le code'}
-
-
- ) : (
-
-
-
Code d'invitation
-
{code}
-
Expire le {expiration}
-
-
-
- {copied ? '✓ Copié !' : '📋 Copier le code'}
-
-
-
- 💬 Partager par SMS
-
-
-
{ setCode(null); setExpiration(null); }}
- className="w-full p-3 text-gray-500 text-sm"
- >
- Générer un autre code
-
-
- )}
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/JumelerParrain.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/JumelerParrain.jsx
deleted file mode 100644
index d61748a..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/JumelerParrain.jsx
+++ /dev/null
@@ -1,173 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { api } from '../stores/api';
-import PageHeader from '../components/PageHeader';
-import SuccessCheck from '../components/SuccessCheck';
-
-export default function JumelerParrain() {
- const [membres, setMembres] = useState([]);
- const [jumelages, setJumelages] = useState([]);
- const [form, setForm] = useState({
- parrain_id: '', filleul_id: '',
- date_jumelage: new Date().toISOString().slice(0, 10),
- note: '',
- });
- const [loading, setLoading] = useState(false);
- const [success, setSuccess] = useState(false);
- const [error, setError] = useState('');
- const [showForm, setShowForm] = useState(false);
-
- useEffect(() => {
- api.get('/membres/actifs').then(setMembres).catch(() => []);
- chargerJumelages();
- }, []);
-
- const chargerJumelages = async () => {
- try {
- const data = await api.get('/jumelages/');
- setJumelages(data);
- } catch { setJumelages([]); }
- };
-
- const update = (k, v) => setForm({ ...form, [k]: v });
-
- const handleSubmit = async () => {
- if (!form.parrain_id || !form.filleul_id) {
- setError('Sélectionnez le parrain et le filleul');
- return;
- }
- if (form.parrain_id === form.filleul_id) {
- setError('Le parrain et le filleul doivent être différents');
- return;
- }
- setLoading(true);
- setError('');
- try {
- await api.post('/jumelages/', {
- parrain_id: form.parrain_id,
- filleul_id: form.filleul_id,
- date_jumelage: form.date_jumelage,
- note: form.note.trim() || null,
- });
- setSuccess(true);
- } catch (e) {
- setError(e.message);
- setLoading(false);
- }
- };
-
- const terminer = async (id) => {
- try {
- await api.patch(`/jumelages/${id}`, { statut: 'termine' });
- chargerJumelages();
- } catch (e) {
- setError(e.message);
- }
- };
-
- if (success) return ;
-
- const actifs = jumelages.filter((j) => j.statut === 'actif');
- const termines = jumelages.filter((j) => j.statut === 'termine');
-
- return (
-
-
-
-
-
-
- 🔒 Ces informations sont confidentielles et accessibles uniquement à l'exécutif.
-
-
-
-
setShowForm(!showForm)}
- className="w-full p-3 bg-yellow-100 text-yellow-700 rounded-2xl text-sm font-bold active:bg-yellow-200">
- {showForm ? '✕ Annuler' : '🤝 Nouveau jumelage'}
-
-
- {showForm && (
-
-
- Parrain
- update('parrain_id', e.target.value)}
- className="w-full p-3 bg-gray-50 rounded-xl text-lg border-0 focus:ring-2 focus:ring-yellow-400">
- Sélectionner le parrain
- {membres.map((m) => {m.prenom} )}
-
-
-
- Filleul
- update('filleul_id', e.target.value)}
- className="w-full p-3 bg-gray-50 rounded-xl text-lg border-0 focus:ring-2 focus:ring-yellow-400">
- Sélectionner le filleul
- {membres.map((m) => {m.prenom} )}
-
-
-
- Date
- update('date_jumelage', e.target.value)}
- className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-yellow-400" />
-
-
- Note (optionnel)
- update('note', e.target.value)}
- className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-yellow-400" />
-
-
- {error &&
{error}
}
-
-
- {loading ? '...' : '🤝 Créer le jumelage'}
-
-
- )}
-
- {/* Jumelages actifs */}
- {actifs.length > 0 && (
-
-
Actifs
- {actifs.map((j) => (
-
-
-
🤝
-
-
{j.parrain_prenom} → {j.filleul_prenom}
-
Depuis {j.date_jumelage}
- {j.note &&
{j.note}
}
-
-
terminer(j.id)}
- className="text-xs bg-red-50 text-red-500 px-3 py-1 rounded-lg font-bold">
- Terminer
-
-
-
- ))}
-
- )}
-
- {/* Jumelages terminés */}
- {termines.length > 0 && (
-
-
Terminés
- {termines.map((j) => (
-
-
{j.parrain_prenom} → {j.filleul_prenom}
-
{j.date_jumelage}
-
- ))}
-
- )}
-
- {jumelages.length === 0 && !showForm && (
-
-
🤝
-
Aucun jumelage enregistré
-
- )}
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/LancerSondage.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/LancerSondage.jsx
deleted file mode 100644
index 7472cb8..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/LancerSondage.jsx
+++ /dev/null
@@ -1,163 +0,0 @@
-import React, { useState } from 'react';
-import { api } from '../stores/api';
-import PageHeader from '../components/PageHeader';
-import SuccessCheck from '../components/SuccessCheck';
-
-const TYPES = [
- { value: 'oui_non', label: 'Oui / Non', emoji: '👍' },
- { value: 'choix_multiple', label: 'Choix multiple', emoji: '📋' },
- { value: 'unanimite', label: 'Unanimité', emoji: '🤝' },
-];
-
-export default function LancerSondage() {
- const [form, setForm] = useState({
- titre: '',
- description: '',
- type: 'oui_non',
- date_ouverture: new Date().toISOString().slice(0, 16),
- date_fermeture: '',
- options: ['', ''],
- });
- const [loading, setLoading] = useState(false);
- const [success, setSuccess] = useState(false);
- const [error, setError] = useState('');
-
- const update = (k, v) => setForm({ ...form, [k]: v });
-
- const updateOption = (idx, val) => {
- const opts = [...form.options];
- opts[idx] = val;
- setForm({ ...form, options: opts });
- };
-
- const addOption = () => setForm({ ...form, options: [...form.options, ''] });
-
- const removeOption = (idx) => {
- if (form.options.length <= 2) return;
- setForm({ ...form, options: form.options.filter((_, i) => i !== idx) });
- };
-
- const handleSubmit = async () => {
- if (!form.titre.trim()) { setError('Le titre est obligatoire'); return; }
- if (!form.date_fermeture) { setError('La date de fermeture est obligatoire'); return; }
-
- const options = form.type === 'oui_non'
- ? [] // auto-générées par le backend
- : form.options.filter((o) => o.trim()).map((o) => ({ texte: o.trim() }));
-
- if (form.type !== 'oui_non' && options.length < 2) {
- setError('Au moins 2 options sont requises');
- return;
- }
-
- setLoading(true);
- setError('');
- try {
- await api.post('/sondages/', {
- titre: form.titre.trim(),
- description: form.description.trim() || null,
- type: form.type,
- date_ouverture: new Date(form.date_ouverture).toISOString(),
- date_fermeture: new Date(form.date_fermeture).toISOString(),
- options,
- });
- setSuccess(true);
- } catch (e) {
- setError(e.message);
- setLoading(false);
- }
- };
-
- if (success) return ;
-
- return (
-
-
-
-
-
- Question / Titre
- update('titre', e.target.value)}
- className="w-full p-3 bg-gray-50 rounded-xl text-lg border-0 focus:ring-2 focus:ring-indigo-400" autoFocus />
-
-
-
- Description (optionnel)
-
-
- {/* Type de vote */}
-
-
Type de vote
-
- {TYPES.map((t) => (
- update('type', t.value)}
- className={`p-3 rounded-xl text-xs font-bold border-2 text-center ${
- form.type === t.value
- ? 'bg-indigo-600 text-white border-indigo-600'
- : 'bg-gray-50 text-gray-600 border-gray-200'
- }`}>
- {t.emoji}
- {t.label}
-
- ))}
-
-
-
- {/* Options (seulement si pas oui/non) */}
- {form.type !== 'oui_non' && (
-
-
Options
- {form.options.map((opt, idx) => (
-
- updateOption(idx, e.target.value)}
- className="flex-1 p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-indigo-400" />
- {form.options.length > 2 && (
- removeOption(idx)}
- className="w-10 bg-red-100 text-red-600 rounded-xl text-sm font-bold">✕
- )}
-
- ))}
-
- + Ajouter une option
-
-
- )}
-
- {/* Dates */}
-
-
- {error &&
{error}
}
-
-
- {loading ? '...' : '📊 Créer le sondage'}
-
-
-
-
- 💡 Le sondage est créé en mode brouillon. Ouvrez-le quand vous êtes prêt depuis le module « Votes ouverts ».
-
-
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Login.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Login.jsx
deleted file mode 100644
index f9389da..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Login.jsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { useStore } from '../stores/store';
-
-export default function Login() {
- const { login, loading, error, groupes, loadGroupes, setPage } = useStore();
- const [prenom, setPrenom] = useState('');
- const [pin, setPin] = useState('');
- const [groupeId, setGroupeId] = useState('');
-
- useEffect(() => {
- if (groupes.length === 0) loadGroupes();
- }, [groupes.length, loadGroupes]);
-
- useEffect(() => {
- if (groupes.length === 1) setGroupeId(groupes[0].id);
- }, [groupes]);
-
- const handleSubmit = async (e) => {
- e.preventDefault();
- if (!prenom || !pin || !groupeId) return;
- try {
- await login(prenom, pin, groupeId);
- } catch {}
- };
-
- return (
-
-
-
☮️
-
Groupe Méditation
-
District 87-16 · Région 87
-
-
-
-
-
100% logiciel libre · Hébergé au Québec
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Parametres.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Parametres.jsx
deleted file mode 100644
index ab85356..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Parametres.jsx
+++ /dev/null
@@ -1,123 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { api } from '../stores/api';
-import PageHeader from '../components/PageHeader';
-
-const JOURS = ['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche'];
-
-export default function Parametres() {
- const [groupe, setGroupe] = useState(null);
- const [editing, setEditing] = useState(false);
- const [form, setForm] = useState({});
- const [msg, setMsg] = useState('');
- const [error, setError] = useState('');
-
- useEffect(() => {
- api.get('/groupe/').then((data) => {
- setGroupe(data);
- setForm({
- nom: data.nom || '',
- adresse_local: data.adresse_local || '',
- jour_reunion: data.jour_reunion || '',
- heure_reunion: data.heure_reunion || '',
- format_reunion: data.format_reunion || '',
- lien_aa87: data.lien_aa87 || 'https://aa87.org',
- });
- });
- }, []);
-
- const sauvegarder = async () => {
- try {
- const updated = await api.patch('/groupe/', form);
- setGroupe(updated);
- setEditing(false);
- setMsg('Paramètres enregistrés ✓');
- setTimeout(() => setMsg(''), 2000);
- } catch (e) {
- setError(e.message);
- }
- };
-
- if (!groupe) return ;
-
- const Field = ({ label, value }) => (
-
-
{label}
-
{value || '—'}
-
- );
-
- return (
-
-
-
-
- {msg &&
{msg}
}
- {error &&
{error}
}
-
-
-
-
Informations du groupe
- {!editing && (
- setEditing(true)} className="text-aa-blue text-sm font-medium">Modifier
- )}
-
-
- {!editing ? (
- <>
-
-
-
-
-
-
-
-
- >
- ) : (
-
-
- Nom du groupe
- setForm({ ...form, nom: e.target.value })}
- className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue" />
-
-
- Adresse du local
-
-
- Jour de réunion
- setForm({ ...form, jour_reunion: e.target.value })}
- className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue">
- —
- {JOURS.map((j) => {j.charAt(0).toUpperCase() + j.slice(1)} )}
-
-
-
- Heure de réunion
- setForm({ ...form, heure_reunion: e.target.value })}
- className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue" />
-
-
- Format de réunion
- setForm({ ...form, format_reunion: e.target.value })}
- className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue" />
-
-
- Lien aa87.org
- setForm({ ...form, lien_aa87: e.target.value })}
- className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue" />
-
-
-
- Enregistrer
- setEditing(false)} className="flex-1 p-3 bg-gray-200 text-gray-700 rounded-xl font-bold">Annuler
-
-
- )}
-
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/PlanifierRotations.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/PlanifierRotations.jsx
deleted file mode 100644
index fc89c88..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/PlanifierRotations.jsx
+++ /dev/null
@@ -1,138 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { api } from '../stores/api';
-import PageHeader from '../components/PageHeader';
-import SuccessCheck from '../components/SuccessCheck';
-
-const JOURS_FR = ['Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'];
-
-// Postes typiques de rotation hebdomadaire
-const POSTES_ROTATION = ['Animateur(trice)', 'Responsable café', 'Responsable accueil', 'Responsable clés'];
-
-export default function PlanifierRotations() {
- const [rotations, setRotations] = useState([]);
- const [postes, setPostes] = useState([]);
- const [membres, setMembres] = useState([]);
- const [loading, setLoading] = useState(true);
- const [ajout, setAjout] = useState(false);
- const [form, setForm] = useState({ date_reunion: '', poste_id: '', membre_id: '' });
- const [success, setSuccess] = useState(false);
- const [error, setError] = useState('');
-
- const charger = async () => {
- try {
- const [rot, p, m] = await Promise.all([
- api.get('/rotations/?semaines=6'),
- api.get('/postes/'),
- api.get('/membres/actifs'),
- ]);
- setRotations(rot);
- // Filtrer les postes de rotation (service + physique)
- setPostes(p.filter((po) => POSTES_ROTATION.some((pr) => po.nom.includes(pr.split('(')[0].trim()))));
- setMembres(m);
- } catch {}
- setLoading(false);
- };
-
- useEffect(() => { charger(); }, []);
-
- const handleSubmit = async () => {
- if (!form.date_reunion || !form.poste_id || !form.membre_id) {
- setError('Tous les champs sont obligatoires');
- return;
- }
- setError('');
- try {
- await api.post('/rotations/', form);
- setSuccess(true);
- } catch (e) {
- setError(e.message);
- }
- };
-
- const supprimer = async (id) => {
- try {
- await api.request('DELETE', `/rotations/${id}`);
- charger();
- } catch (e) { alert(e.message); }
- };
-
- if (success) return ;
-
- // Grouper par date
- const parDate = {};
- rotations.forEach((r) => {
- if (!parDate[r.date_reunion]) parDate[r.date_reunion] = [];
- parDate[r.date_reunion].push(r);
- });
-
- return (
-
-
-
-
- {/* Bouton ajouter */}
- {!ajout ? (
-
setAjout(true)}
- className="w-full p-4 bg-green-50 border-2 border-dashed border-green-300 rounded-2xl text-green-700 font-bold active:scale-[0.98]">
- + Ajouter une rotation
-
- ) : (
-
-
Nouvelle rotation
-
setForm({ ...form, date_reunion: e.target.value })}
- className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue" />
-
setForm({ ...form, poste_id: e.target.value })}
- className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue">
- Poste
- {postes.map((p) => {p.nom} )}
-
-
setForm({ ...form, membre_id: e.target.value })}
- className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue">
- Membre
- {membres.map((m) => {m.prenom} )}
-
- {error &&
{error}
}
-
- Enregistrer
- { setAjout(false); setError(''); }} className="flex-1 p-3 bg-gray-200 text-gray-700 rounded-xl font-bold">Annuler
-
-
- )}
-
- {/* Rotations par date */}
- {loading ? (
-
Chargement...
- ) : Object.keys(parDate).length === 0 ? (
-
-
📅
-
Aucune rotation planifiée
-
- ) : (
- Object.entries(parDate).sort(([a], [b]) => a.localeCompare(b)).map(([dateStr, rots]) => {
- const d = new Date(dateStr + 'T12:00:00');
- const jourFr = JOURS_FR[d.getDay()];
- return (
-
-
- {jourFr} {d.toLocaleDateString('fr-CA')}
-
-
- {rots.map((r) => (
-
-
-
{r.poste_nom}
-
{r.membre_prenom}
-
-
supprimer(r.id)} className="text-red-300 active:text-red-500 text-sm">✕
-
- ))}
-
-
- );
- })
- )}
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Presences.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Presences.jsx
deleted file mode 100644
index 4f0eb6f..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Presences.jsx
+++ /dev/null
@@ -1,134 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { api } from '../stores/api';
-import PageHeader from '../components/PageHeader';
-import SuccessCheck from '../components/SuccessCheck';
-
-export default function Presences() {
- const [membres, setMembres] = useState([]);
- const [reunions, setReunions] = useState([]);
- const [reunionId, setReunionId] = useState('');
- const [presencesMap, setPresencesMap] = useState({});
- const [loading, setLoading] = useState(true);
- const [success, setSuccess] = useState(false);
- const [creatingReunion, setCreatingReunion] = useState(false);
- const [error, setError] = useState('');
-
- useEffect(() => {
- Promise.all([
- api.get('/membres/actifs'),
- api.get('/presences/reunions'),
- ]).then(([m, r]) => {
- setMembres(m);
- setReunions(r);
- // Pré-cocher tout le monde comme présent
- const map = {};
- m.forEach((mb) => { map[mb.id] = true; });
- setPresencesMap(map);
- setLoading(false);
- }).catch(() => setLoading(false));
- }, []);
-
- const creerReunion = async () => {
- setCreatingReunion(true);
- try {
- const res = await api.post('/presences/reunion', {
- date_reunion: new Date().toISOString(),
- });
- setReunionId(res.id);
- setReunions([res, ...reunions]);
- } catch (e) {
- setError(e.message);
- }
- setCreatingReunion(false);
- };
-
- const toggle = (membreId) => {
- setPresencesMap({ ...presencesMap, [membreId]: !presencesMap[membreId] });
- };
-
- const enregistrer = async () => {
- if (!reunionId) {
- setError('Sélectionnez ou créez une réunion d\'affaires');
- return;
- }
- try {
- const presences = Object.entries(presencesMap).map(([membre_id, present]) => ({
- membre_id,
- present,
- }));
- await api.post('/presences/', { reunion_id: reunionId, presences });
- setSuccess(true);
- } catch (e) {
- setError(e.message);
- }
- };
-
- if (success) return ;
-
- const nbPresents = Object.values(presencesMap).filter(Boolean).length;
-
- return (
-
-
-
-
- {/* Sélection / création de réunion */}
-
- Réunion d'affaires
- {reunions.length > 0 && (
- setReunionId(e.target.value)}
- className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue mb-2">
- Sélectionner une réunion
- {reunions.map((r) => (
-
- {new Date(r.date_reunion).toLocaleDateString('fr-CA')}
-
- ))}
-
- )}
-
- {creatingReunion ? '...' : '+ Nouvelle réunion d\'affaires (aujourd\'hui)'}
-
-
-
- {/* Liste des membres avec cases à cocher */}
-
-
-
Membres
- {nbPresents}/{membres.length} présents
-
- {loading ? (
-
Chargement...
- ) : (
-
- {membres.map((m) => (
- toggle(m.id)}
- className={`w-full flex items-center gap-3 p-3 rounded-xl transition ${
- presencesMap[m.id]
- ? 'bg-green-50 border border-green-200'
- : 'bg-gray-50 border border-gray-100 opacity-50'
- }`}>
-
- {presencesMap[m.id] ? '✓' : ''}
-
- {m.prenom}
-
- ))}
-
- )}
-
-
- {error &&
{error}
}
-
-
- 📋 Enregistrer les présences
-
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Profil.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Profil.jsx
deleted file mode 100644
index 83c71eb..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Profil.jsx
+++ /dev/null
@@ -1,151 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { useStore } from '../stores/store';
-import { api } from '../stores/api';
-
-export default function Profil() {
- const { setPage } = useStore();
- const [profil, setProfil] = useState(null);
- const [editing, setEditing] = useState(false);
- const [form, setForm] = useState({});
- const [pinForm, setPinForm] = useState({ ancien_pin: '', nouveau_pin: '' });
- const [showPin, setShowPin] = useState(false);
- const [msg, setMsg] = useState('');
- const [error, setError] = useState('');
-
- useEffect(() => {
- api.get('/membres/moi').then((data) => {
- setProfil(data);
- setForm({
- telephone: data.telephone || '',
- courriel: data.courriel || '',
- date_abstinence: data.date_abstinence || '',
- });
- });
- }, []);
-
- const saveProfile = async () => {
- try {
- const updated = await api.patch('/membres/moi', form);
- setProfil(updated);
- setEditing(false);
- setMsg('Profil mis à jour ✓');
- setTimeout(() => setMsg(''), 2000);
- } catch (e) {
- setError(e.message);
- }
- };
-
- const changePin = async () => {
- try {
- await api.post('/membres/moi/pin', pinForm);
- setPinForm({ ancien_pin: '', nouveau_pin: '' });
- setShowPin(false);
- setMsg('PIN modifié ✓');
- setTimeout(() => setMsg(''), 2000);
- } catch (e) {
- setError(e.message);
- setTimeout(() => setError(''), 3000);
- }
- };
-
- if (!profil) {
- return (
-
- );
- }
-
- const Field = ({ label, value }) => (
-
-
{label}
-
{value || '—'}
-
- );
-
- return (
-
-
- setPage('accueil')} className="text-2xl">←
-
Mon profil
-
-
-
- {msg &&
{msg}
}
- {error &&
{error}
}
-
- {/* Infos */}
-
-
-
Informations
- {!editing && (
- setEditing(true)} className="text-aa-blue text-sm font-medium">Modifier
- )}
-
-
- {!editing ? (
- <>
-
-
-
-
-
- >
- ) : (
-
-
Prénom : {profil.prenom}
-
setForm({ ...form, telephone: e.target.value })}
- className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue" />
-
setForm({ ...form, courriel: e.target.value })}
- className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue" />
-
- Date d'abstinence
- setForm({ ...form, date_abstinence: e.target.value })}
- className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue" />
-
-
- Enregistrer
- setEditing(false)}
- className="flex-1 p-3 bg-gray-200 text-gray-700 rounded-xl font-bold">Annuler
-
-
- )}
-
-
- {/* Changer PIN */}
-
- {!showPin ? (
-
setShowPin(true)}
- className="w-full text-left font-bold text-gray-700 flex items-center justify-between">
- Changer mon PIN
- →
-
- ) : (
-
-
Changer mon PIN
-
setPinForm({ ...pinForm, ancien_pin: e.target.value.replace(/\D/g, '').slice(0, 4) })}
- className="w-full p-3 bg-gray-50 rounded-xl text-center tracking-[0.5em] border-0 focus:ring-2 focus:ring-aa-blue" />
-
setPinForm({ ...pinForm, nouveau_pin: e.target.value.replace(/\D/g, '').slice(0, 4) })}
- className="w-full p-3 bg-gray-50 rounded-xl text-center tracking-[0.5em] border-0 focus:ring-2 focus:ring-aa-blue" />
-
- Confirmer
- { setShowPin(false); setPinForm({ ancien_pin: '', nouveau_pin: '' }); }}
- className="flex-1 p-3 bg-gray-200 text-gray-700 rounded-xl font-bold">Annuler
-
-
- )}
-
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/RapportRSG.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/RapportRSG.jsx
deleted file mode 100644
index b214d73..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/RapportRSG.jsx
+++ /dev/null
@@ -1,155 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { api } from '../stores/api';
-import PageHeader from '../components/PageHeader';
-import SuccessCheck from '../components/SuccessCheck';
-
-export default function RapportRSG() {
- const [rapports, setRapports] = useState([]);
- const [form, setForm] = useState({
- reunion_dist: new Date().toISOString().slice(0, 10),
- decisions_text: '',
- infos_region: '',
- sujets_groupe: '',
- });
- const [loading, setLoading] = useState(true);
- const [success, setSuccess] = useState(false);
- const [viewRapport, setViewRapport] = useState(null);
- const [error, setError] = useState('');
-
- useEffect(() => {
- api.get('/rapports-rsg/').then((data) => {
- setRapports(data);
- setLoading(false);
- }).catch(() => setLoading(false));
- }, []);
-
- const handleSubmit = async () => {
- if (!form.decisions_text.trim() && !form.infos_region.trim() && !form.sujets_groupe.trim()) {
- setError('Au moins une section doit être remplie');
- return;
- }
- setError('');
- try {
- const decisionsList = form.decisions_text
- .split('\n')
- .map((d) => d.trim())
- .filter((d) => d.length > 0);
-
- await api.post('/rapports-rsg/', {
- reunion_dist: form.reunion_dist,
- decisions: decisionsList.length > 0 ? decisionsList : null,
- infos_region: form.infos_region.trim() || null,
- sujets_groupe: form.sujets_groupe.trim() || null,
- });
- setSuccess(true);
- } catch (e) { setError(e.message); }
- };
-
- if (success) return ;
-
- // Vue lecture
- if (viewRapport) {
- return (
-
-
-
setViewRapport(null)} className="text-2xl">←
-
-
Rapport RSG
-
Réunion du {viewRapport.reunion_dist}
-
-
-
-
-
Rédigé par {viewRapport.redacteur_prenom}
-
- {viewRapport.decisions && viewRapport.decisions.length > 0 && (
-
-
Décisions du district
- {viewRapport.decisions.map((d, i) => (
-
- ))}
-
- )}
- {viewRapport.infos_region && (
-
-
Informations de la région
-
{viewRapport.infos_region}
-
- )}
- {viewRapport.sujets_groupe && (
-
-
Sujets pour le groupe
-
{viewRapport.sujets_groupe}
-
- )}
-
-
- );
- }
-
- return (
-
-
-
-
- {/* Rapports existants */}
- {rapports.length > 0 && (
-
-
Rapports récents
- {rapports.slice(0, 5).map((r) => (
-
setViewRapport(r)}
- className="w-full flex items-center justify-between py-2 border-b border-gray-50 last:border-0 active:bg-gray-50">
-
-
Réunion du {r.reunion_dist}
-
Par {r.redacteur_prenom}
-
- →
-
- ))}
-
- )}
-
- {/* Nouveau rapport */}
-
- Date de la réunion de district
- setForm({ ...form, reunion_dist: e.target.value })}
- className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue" />
-
-
-
- Décisions du district (une par ligne)
-
-
-
- Informations de la région 87
-
-
-
- Sujets à rapporter au groupe
-
-
- {error &&
{error}
}
-
-
- 📨 Enregistrer le rapport
-
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/RapportReunion.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/RapportReunion.jsx
deleted file mode 100644
index 409ac60..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/RapportReunion.jsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import React, { useState } from 'react';
-import { api } from '../stores/api';
-import PageHeader from '../components/PageHeader';
-import SuccessCheck from '../components/SuccessCheck';
-
-export default function RapportReunion() {
- const [theme, setTheme] = useState('');
- const [annonces, setAnnonces] = useState('');
- const [note, setNote] = useState('');
- const [dateReunion, setDateReunion] = useState(new Date().toISOString().slice(0, 16));
- const [loading, setLoading] = useState(false);
- const [success, setSuccess] = useState(false);
- const [error, setError] = useState('');
-
- const handleSubmit = async () => {
- if (!theme.trim()) {
- setError('Le thème est obligatoire');
- return;
- }
- setLoading(true);
- setError('');
- try {
- const annoncesList = annonces
- .split('\n')
- .map((a) => a.trim())
- .filter((a) => a.length > 0);
-
- await api.post('/reunions/', {
- type: 'reguliere',
- date_reunion: new Date(dateReunion).toISOString(),
- theme: theme.trim(),
- annonces: annoncesList.length > 0 ? annoncesList : null,
- note: note.trim() || null,
- });
- setSuccess(true);
- } catch (e) {
- setError(e.message);
- setLoading(false);
- }
- };
-
- if (success) return ;
-
- return (
-
-
-
-
- {/* Date */}
-
- Date et heure
- setDateReunion(e.target.value)}
- className="w-full p-3 bg-gray-50 rounded-xl text-lg border-0 focus:ring-2 focus:ring-aa-blue"
- />
-
-
- {/* Thème */}
-
- Thème abordé
- setTheme(e.target.value)}
- className="w-full p-3 bg-gray-50 rounded-xl text-lg border-0 focus:ring-2 focus:ring-aa-blue"
- autoFocus
- />
-
-
- {/* Annonces */}
-
- Annonces (une par ligne)
-
-
- {/* Notes */}
-
- Notes (optionnel)
-
-
- {error &&
{error}
}
-
-
- {loading ? '...' : '🎤 Enregistrer le rapport'}
-
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Rapports.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Rapports.jsx
deleted file mode 100644
index 83e23b6..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/Rapports.jsx
+++ /dev/null
@@ -1,103 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { api } from '../stores/api';
-import PageHeader from '../components/PageHeader';
-
-const MOIS_FR = [
- '', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
- 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre',
-];
-
-export default function Rapports() {
- const [disponibles, setDisponibles] = useState([]);
- const [loading, setLoading] = useState(true);
- const [generating, setGenerating] = useState(null);
-
- useEffect(() => {
- api.get('/rapports/disponibles').then((data) => {
- setDisponibles(data);
- setLoading(false);
- }).catch(() => setLoading(false));
- }, []);
-
- const telecharger = async (type, annee, mois) => {
- const key = `${type}-${annee}-${mois}`;
- setGenerating(key);
- try {
- const token = localStorage.getItem('gm_token');
- const url = `/api/rapports/${type}/${annee}/${mois}`;
- const res = await fetch(url, {
- headers: { Authorization: `Bearer ${token}` },
- });
- if (!res.ok) throw new Error('Erreur de génération');
- const blob = await res.blob();
- const a = document.createElement('a');
- a.href = URL.createObjectURL(blob);
- a.download = `rapport_${type}_${annee}_${mois.toString().padStart(2, '0')}.pdf`;
- a.click();
- URL.revokeObjectURL(a.href);
- } catch (e) {
- alert(`Erreur: ${e.message}`);
- }
- setGenerating(null);
- };
-
- return (
-
-
-
-
- {loading ? (
-
Chargement...
- ) : disponibles.length === 0 ? (
-
-
📄
-
Aucun rapport disponible
-
Les rapports apparaîtront après les premières saisies
-
- ) : (
- disponibles.map((m) => (
-
-
- {MOIS_FR[m.mois]} {m.annee}
-
-
- {m.finances && (
-
telecharger('finances', m.annee, m.mois)}
- disabled={generating === `finances-${m.annee}-${m.mois}`}
- className="flex items-center gap-3 p-3 bg-green-50 rounded-xl text-left active:scale-[0.98] transition border border-green-200"
- >
- 💰
-
-
Rapport financier
-
Collectes, dépenses, solde
-
-
- {generating === `finances-${m.annee}-${m.mois}` ? '⏳' : '↓'}
-
-
- )}
- {m.reunions && (
-
telecharger('reunions', m.annee, m.mois)}
- disabled={generating === `reunions-${m.annee}-${m.mois}`}
- className="flex items-center gap-3 p-3 bg-purple-50 rounded-xl text-left active:scale-[0.98] transition border border-purple-200"
- >
- 🎤
-
-
Résumé des réunions
-
Thèmes, annonces, animateurs
-
-
- {generating === `reunions-${m.annee}-${m.mois}` ? '⏳' : '↓'}
-
-
- )}
-
-
- ))
- )}
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/RedigerPV.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/RedigerPV.jsx
deleted file mode 100644
index a5bb385..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/RedigerPV.jsx
+++ /dev/null
@@ -1,195 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { api } from '../stores/api';
-import PageHeader from '../components/PageHeader';
-import SuccessCheck from '../components/SuccessCheck';
-
-export default function RedigerPV() {
- const [reunions, setReunions] = useState([]);
- const [pvList, setPvList] = useState([]);
- const [reunionId, setReunionId] = useState('');
- const [contenu, setContenu] = useState('');
- const [decisions, setDecisions] = useState([]);
- const [loading, setLoading] = useState(true);
- const [success, setSuccess] = useState(false);
- const [viewPv, setViewPv] = useState(null);
- const [error, setError] = useState('');
-
- useEffect(() => {
- Promise.all([
- api.get('/presences/reunions'),
- api.get('/pv/'),
- ]).then(([r, p]) => {
- setReunions(r);
- setPvList(p);
- setLoading(false);
- }).catch(() => setLoading(false));
- }, []);
-
- const ajouterDecision = () => {
- setDecisions([...decisions, { sujet: '', decision: '', proposeur: '', secondeur: '', statut: 'en_attente' }]);
- };
-
- const updateDecision = (idx, field, value) => {
- const copy = [...decisions];
- copy[idx][field] = value;
- setDecisions(copy);
- };
-
- const supprimerDecision = (idx) => {
- setDecisions(decisions.filter((_, i) => i !== idx));
- };
-
- const handleSubmit = async () => {
- if (!reunionId) { setError('Sélectionnez une réunion d\'affaires'); return; }
- if (!contenu.trim()) { setError('Le contenu du PV est obligatoire'); return; }
- setError('');
- try {
- const validDecisions = decisions.filter((d) => d.sujet.trim());
- await api.post('/pv/', {
- reunion_id: reunionId,
- contenu: contenu.trim(),
- decisions: validDecisions.length > 0 ? validDecisions : null,
- });
- setSuccess(true);
- } catch (e) { setError(e.message); }
- };
-
- if (success) return ;
-
- // Vue lecture d'un PV existant
- if (viewPv) {
- return (
-
-
-
setViewPv(null)} className="text-2xl">←
-
-
Procès-verbal
-
- {viewPv.date_reunion ? new Date(viewPv.date_reunion).toLocaleDateString('fr-CA') : ''}
-
-
-
-
-
-
-
Rédigé par {viewPv.redacteur_prenom}
- {viewPv.approuve && (
-
Approuvé
- )}
-
-
{viewPv.contenu}
-
- {viewPv.decisions && viewPv.decisions.length > 0 && (
-
-
Décisions
- {viewPv.decisions.map((d, i) => (
-
-
{d.sujet}
-
{d.decision}
-
- {d.proposeur && `Proposé par ${d.proposeur}`}
- {d.secondeur && `, secondé par ${d.secondeur}`}
-
-
- ))}
-
- )}
-
-
- );
- }
-
- return (
-
-
-
-
- {/* PV existants */}
- {pvList.length > 0 && (
-
-
PV récents
- {pvList.slice(0, 5).map((p) => (
-
setViewPv(p)}
- className="w-full flex items-center justify-between py-2 border-b border-gray-50 last:border-0 active:bg-gray-50">
-
-
- {p.date_reunion ? new Date(p.date_reunion).toLocaleDateString('fr-CA') : 'Sans date'}
-
-
Par {p.redacteur_prenom}
-
- →
-
- ))}
-
- )}
-
- {/* Nouveau PV */}
-
-
Nouveau procès-verbal
-
- Réunion d'affaires
- setReunionId(e.target.value)}
- className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue mb-3">
- Sélectionner
- {reunions.map((r) => (
-
- {new Date(r.date_reunion).toLocaleDateString('fr-CA')}
- {r.presences?.length > 0 ? ` (${r.presences.filter(p => p.present).length} présents)` : ''}
-
- ))}
-
-
- Contenu du PV
-
-
- {/* Décisions */}
-
-
-
Décisions
- + Ajouter
-
- {decisions.length === 0 ? (
-
Aucune décision (cliquez + Ajouter)
- ) : (
-
- {decisions.map((d, i) => (
-
- ))}
-
- )}
-
-
- {error &&
{error}
}
-
-
- 📝 Enregistrer le PV
-
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/RemiseJeton.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/RemiseJeton.jsx
deleted file mode 100644
index 8d38aa2..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/RemiseJeton.jsx
+++ /dev/null
@@ -1,208 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { api } from '../stores/api';
-import PageHeader from '../components/PageHeader';
-import SuccessCheck from '../components/SuccessCheck';
-
-const TYPES_JETONS = [
- { value: 'desir', label: 'Désir', emoji: '💫' },
- { value: '24h', label: '24 heures', emoji: '🌅' },
- { value: '1mois', label: '1 mois', emoji: '🥉' },
- { value: '2mois', label: '2 mois', emoji: '🥉' },
- { value: '3mois', label: '3 mois', emoji: '🥈' },
- { value: '6mois', label: '6 mois', emoji: '🥈' },
- { value: '9mois', label: '9 mois', emoji: '🥇' },
- { value: '1an', label: '1 an', emoji: '⭐' },
- { value: 'multiple', label: 'Multiple', emoji: '🏆' },
-];
-
-export default function RemiseJeton() {
- const [form, setForm] = useState({
- membre_id: '',
- type_jeton: '',
- date_remise: new Date().toISOString().slice(0, 10),
- annees: '',
- });
- const [membres, setMembres] = useState([]);
- const [loading, setLoading] = useState(false);
- const [success, setSuccess] = useState(false);
- const [error, setError] = useState('');
-
- useEffect(() => {
- api.get('/membres/actifs').then(setMembres).catch(() => setMembres([]));
- }, []);
-
- const update = (k, v) => setForm({ ...form, [k]: v });
-
- const handleSubmit = async () => {
- if (!form.type_jeton) {
- setError('Sélectionnez un type de jeton');
- return;
- }
- if (form.type_jeton !== 'desir' && !form.membre_id) {
- setError('Sélectionnez un membre');
- return;
- }
- if (form.type_jeton === 'multiple' && (!form.annees || parseInt(form.annees) < 2)) {
- setError("Entrez le nombre d'années (2 et +)");
- return;
- }
- setLoading(true);
- setError('');
- try {
- const body = {
- type_jeton: form.type_jeton,
- date_remise: form.date_remise,
- };
- if (form.membre_id) body.membre_id = form.membre_id;
- if (form.type_jeton === 'multiple') body.annees = parseInt(form.annees);
- await api.post('/jetons/', body);
- setSuccess(true);
- } catch (e) {
- setError(e.message);
- setLoading(false);
- }
- };
-
- if (success) return ;
-
- return (
-
-
-
-
- {/* Date */}
-
- Date
- update('date_remise', e.target.value)}
- className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue"
- />
-
-
- {/* Type de jeton */}
-
-
Type de jeton
-
- {TYPES_JETONS.map((t) => (
- update('type_jeton', t.value)}
- className={`p-3 rounded-xl text-xs font-bold transition border-2 text-center ${
- form.type_jeton === t.value
- ? 'bg-aa-gold text-white border-aa-gold'
- : 'bg-gray-50 text-gray-600 border-gray-200 active:bg-gray-100'
- }`}
- >
- {t.emoji}
- {t.label}
-
- ))}
-
-
-
- {/* Nombre d'années (si multiple) */}
- {form.type_jeton === 'multiple' && (
-
- Nombre d'années
- update('annees', e.target.value)}
- className="w-full p-4 bg-gray-50 rounded-xl text-2xl font-bold text-center border-0 focus:ring-2 focus:ring-aa-gold"
- autoFocus
- />
-
- )}
-
- {/* Membre (pas requis pour jeton de désir) */}
-
-
- Membre {form.type_jeton === 'desir' ? '(optionnel)' : ''}
-
- update('membre_id', e.target.value)}
- className="w-full p-3 bg-gray-50 rounded-xl text-lg border-0 focus:ring-2 focus:ring-aa-blue"
- >
-
- {form.type_jeton === 'desir' ? 'Anonyme (jeton de désir)' : 'Sélectionner un membre'}
-
- {membres.map((m) => (
- {m.prenom}
- ))}
-
-
-
- {error &&
{error}
}
-
-
- {loading ? '...' : '🏅 Remettre le jeton'}
-
-
- {/* Historique rapide */}
-
-
-
- );
-}
-
-function HistoriqueJetons() {
- const [jetons, setJetons] = useState([]);
- const [open, setOpen] = useState(false);
-
- const charger = async () => {
- if (jetons.length > 0) {
- setOpen(!open);
- return;
- }
- try {
- const data = await api.get('/jetons/?limite=10');
- setJetons(data);
- setOpen(true);
- } catch {
- // Silencieux
- }
- };
-
- const typeLabel = (type, annees) => {
- const t = TYPES_JETONS.find((x) => x.value === type);
- if (type === 'multiple' && annees) return `${t?.emoji || '🏆'} ${annees} ans`;
- return t ? `${t.emoji} ${t.label}` : type;
- };
-
- return (
-
-
- {open ? '▲ Masquer l\'historique' : '▼ Derniers jetons remis'}
-
- {open && jetons.length > 0 && (
-
- {jetons.map((j) => (
-
-
{typeLabel(j.type_jeton, j.annees).split(' ')[0]}
-
-
- {j.membre_prenom || 'Anonyme'} — {typeLabel(j.type_jeton, j.annees)}
-
-
{j.date_remise}
-
-
- ))}
-
- )}
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/TerminerMandat.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/TerminerMandat.jsx
deleted file mode 100644
index f8a7310..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/TerminerMandat.jsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { api } from '../stores/api';
-import PageHeader from '../components/PageHeader';
-import SuccessCheck from '../components/SuccessCheck';
-
-export default function TerminerMandat() {
- const [affectations, setAffectations] = useState([]);
- const [selected, setSelected] = useState(null);
- const [noteTransition, setNoteTransition] = useState('');
- const [loading, setLoading] = useState(true);
- const [success, setSuccess] = useState(false);
- const [confirm, setConfirm] = useState(false);
- const [error, setError] = useState('');
-
- useEffect(() => {
- api.get('/postes/affectations/groupe').then((data) => {
- setAffectations(data);
- setLoading(false);
- }).catch(() => setLoading(false));
- }, []);
-
- const terminer = async () => {
- try {
- await api.post(`/postes/affectations/${selected.id}/terminer`, {
- note_transition: noteTransition.trim() || null,
- });
- setSuccess(true);
- } catch (e) {
- setError(e.message);
- setConfirm(false);
- }
- };
-
- if (success) return ;
-
- if (selected) {
- return (
-
-
- { setSelected(null); setConfirm(false); setError(''); }} className="text-2xl">←
-
Terminer un mandat
-
-
-
-
Poste
-
{selected.poste_nom}
-
Membre
-
{selected.membre_prenom}
-
Depuis
-
{selected.date_debut}
-
-
-
- Note de transition (optionnel)
-
-
- {error &&
{error}
}
-
- {!confirm ? (
-
setConfirm(true)}
- className="w-full p-5 bg-orange-500 text-white rounded-2xl text-xl font-bold active:scale-[0.97] transition shadow-lg">
- 🔄 Terminer ce mandat
-
- ) : (
-
-
- Confirmer la fin du mandat de {selected.membre_prenom} comme {selected.poste_nom} ?
- Les permissions seront retirées immédiatement.
-
-
- Confirmer
- setConfirm(false)} className="flex-1 p-3 bg-gray-200 text-gray-700 rounded-xl font-bold">Annuler
-
-
- )}
-
-
- );
- }
-
- return (
-
-
-
- {loading ? (
-
Chargement...
- ) : affectations.length === 0 ? (
-
-
🔄
-
Aucune affectation active
-
- ) : (
- affectations.map((a) => (
-
setSelected(a)}
- className="w-full bg-white rounded-2xl p-4 shadow-sm flex items-center justify-between active:scale-[0.98] transition">
-
-
{a.poste_nom}
-
{a.membre_prenom} · depuis {a.date_debut}
-
- →
-
- ))
- )}
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/VenteLitterature.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/VenteLitterature.jsx
deleted file mode 100644
index 8d33553..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/VenteLitterature.jsx
+++ /dev/null
@@ -1,145 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { api } from '../stores/api';
-import PageHeader from '../components/PageHeader';
-import SuccessCheck from '../components/SuccessCheck';
-
-export default function VenteLitterature() {
- const [form, setForm] = useState({
- litterature_id: '',
- quantite: '1',
- montant: '',
- date_vente: new Date().toISOString().slice(0, 10),
- });
- const [catalogue, setCatalogue] = useState([]);
- const [loading, setLoading] = useState(false);
- const [success, setSuccess] = useState(false);
- const [error, setError] = useState('');
-
- useEffect(() => {
- api.get('/litterature/').then(setCatalogue).catch(() => setCatalogue([]));
- }, []);
-
- const update = (k, v) => setForm({ ...form, [k]: v });
-
- // Auto-remplir le montant quand on sélectionne un titre
- const onSelectTitre = (id) => {
- update('litterature_id', id);
- const item = catalogue.find((c) => c.id === id);
- if (item && item.prix_coutant) {
- setForm((f) => ({ ...f, litterature_id: id, montant: (item.prix_coutant * parseInt(f.quantite || '1')).toFixed(2) }));
- }
- };
-
- const onChangeQuantite = (q) => {
- const item = catalogue.find((c) => c.id === form.litterature_id);
- const newForm = { ...form, quantite: q };
- if (item && item.prix_coutant && q) {
- newForm.montant = (item.prix_coutant * parseInt(q)).toFixed(2);
- }
- setForm(newForm);
- };
-
- const selectedItem = catalogue.find((c) => c.id === form.litterature_id);
-
- const handleSubmit = async () => {
- if (!form.litterature_id) { setError('Sélectionnez un titre'); return; }
- if (!form.quantite || parseInt(form.quantite) <= 0) { setError('Quantité invalide'); return; }
- if (!form.montant || parseFloat(form.montant) <= 0) { setError('Montant invalide'); return; }
- setLoading(true);
- setError('');
- try {
- await api.post('/ventes-litterature/', {
- litterature_id: form.litterature_id,
- quantite: parseInt(form.quantite),
- montant: parseFloat(form.montant),
- date_vente: form.date_vente,
- });
- setSuccess(true);
- } catch (e) {
- setError(e.message);
- setLoading(false);
- }
- };
-
- if (success) return ;
-
- return (
-
-
-
-
- {/* Date */}
-
- Date
- update('date_vente', e.target.value)}
- className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue"
- />
-
-
- {/* Titre (dropdown) */}
-
-
Titre
-
onSelectTitre(e.target.value)}
- className="w-full p-3 bg-gray-50 rounded-xl text-lg border-0 focus:ring-2 focus:ring-aa-blue"
- >
- Sélectionner un titre
- {catalogue.map((c) => (
-
- {c.titre} ({c.quantite_stock} en stock){c.prix_coutant ? ` — ${c.prix_coutant.toFixed(2)}$` : ''}
-
- ))}
-
- {selectedItem && selectedItem.quantite_stock <= selectedItem.seuil_commande && (
-
⚠️ Stock bas ({selectedItem.quantite_stock} restant)
- )}
-
-
- {/* Quantité */}
-
- Quantité
- onChangeQuantite(e.target.value)}
- className="w-full p-4 bg-gray-50 rounded-xl text-2xl font-bold text-center border-0 focus:ring-2 focus:ring-purple-400"
- />
-
-
- {/* Montant */}
-
-
Montant total
-
- $
- update('montant', e.target.value)}
- className="w-full p-4 pl-10 bg-gray-50 rounded-xl text-2xl font-bold text-center border-0 focus:ring-2 focus:ring-purple-400"
- />
-
-
-
- {error &&
{error}
}
-
-
- {loading ? '...' : '📖 Enregistrer la vente'}
-
-
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/VotesOuverts.jsx b/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/VotesOuverts.jsx
deleted file mode 100644
index 3f4214d..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/pages/VotesOuverts.jsx
+++ /dev/null
@@ -1,176 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { api } from '../stores/api';
-import { useStore } from '../stores/store';
-import PageHeader from '../components/PageHeader';
-
-export default function VotesOuverts() {
- const [sondages, setSondages] = useState([]);
- const [filtre, setFiltre] = useState('ouvert');
- const [loading, setLoading] = useState(true);
- const postes = useStore((s) => s.postes);
- const isExecutif = postes.some((p) => ['RSG', 'RSG Substitut(e)', 'Trésorier(ière)', 'Secrétaire'].includes(p));
-
- const charger = async () => {
- setLoading(true);
- try {
- const url = filtre ? `/sondages/?statut=${filtre}` : '/sondages/';
- const data = await api.get(url);
- setSondages(data);
- } catch { setSondages([]); }
- setLoading(false);
- };
-
- useEffect(() => { charger(); }, [filtre]);
-
- return (
-
-
-
-
- {/* Filtres */}
-
- {['ouvert', 'ferme', ''].map((f) => (
- setFiltre(f)}
- className={`flex-1 py-2 rounded-xl text-sm font-bold border-2 ${
- filtre === f ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white text-gray-500 border-gray-200'
- }`}>
- {f === 'ouvert' ? 'Ouverts' : f === 'ferme' ? 'Fermés' : 'Tous'}
-
- ))}
-
-
- {loading &&
Chargement...
}
-
- {sondages.map((s) => (
-
- ))}
-
- {!loading && sondages.length === 0 && (
-
-
🗳️
-
Aucun sondage {filtre === 'ouvert' ? 'ouvert' : ''}
-
- )}
-
-
- );
-}
-
-function SondageCard({ sondage: s, isExecutif, onUpdate }) {
- const [voting, setVoting] = useState(false);
- const [selected, setSelected] = useState(null);
- const [error, setError] = useState('');
-
- const maxVotes = Math.max(...s.options.map((o) => o.nb_votes), 1);
-
- const handleVote = async () => {
- if (!selected) return;
- setVoting(true);
- setError('');
- try {
- await api.post(`/sondages/${s.id}/voter`, { option_id: selected });
- onUpdate();
- } catch (e) {
- setError(e.message);
- }
- setVoting(false);
- };
-
- const changerStatut = async (statut) => {
- try {
- await api.patch(`/sondages/${s.id}`, { statut });
- onUpdate();
- } catch (e) {
- setError(e.message);
- }
- };
-
- const statutColor = {
- brouillon: 'bg-gray-100 text-gray-600',
- ouvert: 'bg-green-100 text-green-700',
- ferme: 'bg-red-100 text-red-600',
- };
-
- return (
-
-
-
-
{s.titre}
- {s.description &&
{s.description}
}
-
-
- {s.statut}
-
-
-
-
- {s.type === 'oui_non' ? '👍 Oui/Non' : s.type === 'unanimite' ? '🤝 Unanimité' : '📋 Choix multiple'}
- {' · '}{s.total_votes} vote{s.total_votes !== 1 ? 's' : ''}
- {s.cree_par_prenom && ` · par ${s.cree_par_prenom}`}
-
-
- {/* Options avec barres de résultat */}
-
- {s.options.map((opt) => {
- const pct = s.total_votes > 0 ? Math.round((opt.nb_votes / s.total_votes) * 100) : 0;
- const canSelect = s.statut === 'ouvert' && !s.a_vote;
- return (
-
canSelect && setSelected(opt.id)}
- disabled={!canSelect}
- className={`w-full text-left rounded-xl p-3 transition border-2 ${
- selected === opt.id
- ? 'border-indigo-500 bg-indigo-50'
- : 'border-gray-100 bg-gray-50'
- }`}
- >
-
- {opt.texte}
- {opt.nb_votes} ({pct}%)
-
-
-
- );
- })}
-
-
- {/* Bouton voter */}
- {s.statut === 'ouvert' && !s.a_vote && (
-
- {voting ? '...' : '🗳️ Voter'}
-
- )}
-
- {s.a_vote && s.statut === 'ouvert' && (
-
✓ Vous avez voté
- )}
-
- {error &&
{error}
}
-
- {/* Actions exécutif */}
- {isExecutif && (
-
- {s.statut === 'brouillon' && (
- changerStatut('ouvert')}
- className="flex-1 p-2 text-xs font-bold bg-green-100 text-green-700 rounded-lg">
- Ouvrir
-
- )}
- {s.statut === 'ouvert' && (
- changerStatut('ferme')}
- className="flex-1 p-2 text-xs font-bold bg-red-100 text-red-600 rounded-lg">
- Fermer
-
- )}
-
- )}
-
- );
-}
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/stores/api.js b/groupe-meditation-corrige/groupe-meditation/frontend/src/stores/api.js
deleted file mode 100644
index 686e246..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/stores/api.js
+++ /dev/null
@@ -1,61 +0,0 @@
-/**
- * Client API — Groupe Méditation
- */
-const BASE = '/api';
-
-class ApiClient {
- constructor() {
- this.token = localStorage.getItem('gm_token');
- }
-
- setToken(token) {
- this.token = token;
- if (token) {
- localStorage.setItem('gm_token', token);
- } else {
- localStorage.removeItem('gm_token');
- }
- }
-
- async request(method, path, body = null) {
- const headers = { 'Content-Type': 'application/json' };
- if (this.token) {
- headers.Authorization = `Bearer ${this.token}`;
- }
-
- const opts = { method, headers };
- if (body) opts.body = JSON.stringify(body);
-
- const res = await fetch(`${BASE}${path}`, opts);
-
- if (res.status === 401) {
- this.setToken(null);
- window.location.reload();
- throw new Error('Session expirée');
- }
-
- if (!res.ok) {
- const contentType = res.headers.get('content-type') || '';
- let err;
-
- if (contentType.includes('application/json')) {
- err = await res.json().catch(() => ({ detail: 'Erreur API' }));
- } else {
- const text = await res.text().catch(() => '');
- err = {
- detail: text ? `Réponse non JSON (${res.status})` : `Erreur HTTP ${res.status}`,
- };
- }
-
- throw new Error(err.detail || 'Erreur');
- }
-
- return res.json();
- }
-
- get(path) { return this.request('GET', path); }
- post(path, body) { return this.request('POST', path, body); }
- patch(path, body) { return this.request('PATCH', path, body); }
-}
-
-export const api = new ApiClient();
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/src/stores/store.js b/groupe-meditation-corrige/groupe-meditation/frontend/src/stores/store.js
deleted file mode 100644
index 909a2cb..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/src/stores/store.js
+++ /dev/null
@@ -1,109 +0,0 @@
-import { create } from 'zustand';
-import { api } from './api';
-
-export const useStore = create((set, get) => ({
- // Auth
- token: localStorage.getItem('gm_token'),
- membre: null,
- groupeId: localStorage.getItem('gm_groupe_id'),
- groupes: [],
-
- // Accueil
- postes: [],
- modulesConsultation: [],
- modulesSaisie: [],
- modulesGestion: [],
-
- // UI
- loading: false,
- error: null,
- page: 'login', // login | inscription | accueil | profil
-
- setPage: (page) => set({ page, error: null }),
- setError: (error) => set({ error }),
-
- loadGroupes: async () => {
- set({ loading: true, error: null });
- try {
- const groupes = await api.get('/auth/groupes');
- set({ groupes, loading: false });
- } catch (e) {
- set({
- groupes: [],
- loading: false,
- error: e.message || 'Impossible de charger les groupes',
- });
- }
- },
-
- login: async (prenom, pin, groupeId) => {
- set({ loading: true, error: null });
- try {
- const data = await api.post('/auth/connexion', {
- prenom, pin, groupe_id: groupeId,
- });
- api.setToken(data.access_token);
- localStorage.setItem('gm_groupe_id', groupeId);
- set({
- token: data.access_token,
- membre: data.membre,
- groupeId,
- loading: false,
- page: 'accueil',
- });
- await get().loadAccueil();
- } catch (e) {
- set({ loading: false, error: e.message });
- throw e;
- }
- },
-
- inscription: async (data) => {
- set({ loading: true, error: null });
- try {
- const res = await api.post('/auth/inscription', data);
- api.setToken(res.access_token);
- localStorage.setItem('gm_groupe_id', data.groupe_id || '');
- set({
- token: res.access_token,
- membre: res.membre,
- loading: false,
- page: 'accueil',
- });
- await get().loadAccueil();
- } catch (e) {
- set({ loading: false, error: e.message });
- throw e;
- }
- },
-
- loadAccueil: async () => {
- try {
- const data = await api.get('/accueil/');
- set({
- postes: data.postes,
- modulesConsultation: data.modules_consultation,
- modulesSaisie: data.modules_saisie,
- modulesGestion: data.modules_gestion,
- membre: data.membre,
- });
- } catch (e) {
- set({ error: e.message });
- }
- },
-
- logout: () => {
- api.setToken(null);
- localStorage.removeItem('gm_groupe_id');
- set({
- token: null,
- membre: null,
- groupeId: null,
- postes: [],
- modulesConsultation: [],
- modulesSaisie: [],
- modulesGestion: [],
- page: 'login',
- });
- },
-}));
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/tailwind.config.js b/groupe-meditation-corrige/groupe-meditation/frontend/tailwind.config.js
deleted file mode 100644
index 7e75123..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/tailwind.config.js
+++ /dev/null
@@ -1,12 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-export default {
- content: ['./index.html', './src/**/*.{js,jsx}'],
- theme: {
- extend: {
- colors: {
- aa: { blue: '#1a5276', gold: '#d4a843' },
- },
- },
- },
- plugins: [],
-};
diff --git a/groupe-meditation-corrige/groupe-meditation/frontend/vite.config.js b/groupe-meditation-corrige/groupe-meditation/frontend/vite.config.js
deleted file mode 100644
index bfdd753..0000000
--- a/groupe-meditation-corrige/groupe-meditation/frontend/vite.config.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import { defineConfig } from 'vite';
-import react from '@vitejs/plugin-react';
-import { VitePWA } from 'vite-plugin-pwa';
-
-export default defineConfig({
- plugins: [
- react(),
- VitePWA({
- registerType: 'autoUpdate',
- includeAssets: ['favicon.svg'],
- manifest: {
- name: 'Groupe Méditation',
- short_name: 'Méditation',
- description: 'Application du Groupe Méditation — District 87-16',
- theme_color: '#1a5276',
- background_color: '#f8f9fa',
- display: 'standalone',
- orientation: 'portrait',
- start_url: '/',
- icons: [
- { src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
- { src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
- ],
- },
- workbox: {
- globPatterns: ['**/*.{js,css,html,svg,png,woff2}'],
- runtimeCaching: [
- {
- urlPattern: /\/api\//,
- handler: 'NetworkFirst',
- options: { cacheName: 'api-cache', networkTimeoutSeconds: 5 },
- },
- ],
- },
- }),
- ],
- server: {
- proxy: { '/api': 'http://127.0.0.1:8000' },
- },
-});