diff --git a/backend/app/routers/accueil.py b/backend/app/routers/accueil.py
index 26379e4..566482e 100644
--- a/backend/app/routers/accueil.py
+++ b/backend/app/routers/accueil.py
@@ -68,6 +68,7 @@ MODULES_GESTION = [
ModuleTuile(code="G12", nom="Gestion des postes", description="Postes, candidatures, accĂšs", icone="đ", couleur="purple"),
ModuleTuile(code="G13", nom="Journal", description="Changements enregistrĂ©s", icone="đ§Ÿ", couleur="gray"),
]
+MODULE_SYSADMIN = ModuleTuile(code="G14", nom="Gestion des groupes", description="Groupes et dĂ©monstration", icone="đ·ïž", couleur="indigo")
for _modules in [MODULES_SAISIE_TOUS, MODULES_GESTION, *MODULES_SAISIE.values()]:
for _m in _modules:
@@ -104,7 +105,7 @@ async def accueil(
est_executif=True,
modules_consultation=MODULES_CONSULTATION,
modules_saisie=modules_saisie_sysadmin,
- modules_gestion=MODULES_GESTION,
+ modules_gestion=[MODULE_SYSADMIN, *MODULES_GESTION],
groupe=groupe_info,
)
diff --git a/backend/app/routers/admin.py b/backend/app/routers/admin.py
index 2173e8b..17a1515 100644
--- a/backend/app/routers/admin.py
+++ b/backend/app/routers/admin.py
@@ -1,10 +1,15 @@
"""Routes d'administration reservees au sysadmin."""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy import func, select
from app.core.database import get_db
from app.core.security import get_current_membre, is_sysadmin
from app.services.admin_database import initialiser_demo, remettre_a_zero
+from app.models.groupe import Groupe
+from app.models.membre import Membre
+from app.models.reunion import Reunion
+from app.models.historique_modification import HistoriqueModification
router = APIRouter(prefix="/admin", tags=["Administration"])
@@ -29,3 +34,38 @@ async def demo_database(
db: AsyncSession = Depends(get_db),
):
return await initialiser_demo(db)
+
+
+@router.get("/groupes")
+async def liste_groupes_admin(
+ _sysadmin=Depends(require_sysadmin),
+ db: AsyncSession = Depends(get_db),
+):
+ groupes = (await db.execute(select(Groupe).order_by(Groupe.nom))).scalars().all()
+ out = []
+ for groupe in groupes:
+ nb_membres = (await db.execute(
+ select(func.count()).select_from(Membre).where(Membre.groupe_id == groupe.id)
+ )).scalar_one()
+ nb_reunions = (await db.execute(
+ select(func.count()).select_from(Reunion).where(Reunion.groupe_id == groupe.id)
+ )).scalar_one()
+ nb_traces = (await db.execute(
+ select(func.count()).select_from(HistoriqueModification).where(HistoriqueModification.groupe_id == groupe.id)
+ )).scalar_one()
+ out.append({
+ "id": str(groupe.id),
+ "nom": groupe.nom,
+ "district": groupe.district,
+ "region": groupe.region,
+ "adresse_local": groupe.adresse_local,
+ "jour_reunion": groupe.jour_reunion,
+ "heure_reunion": groupe.heure_reunion.strftime("%H:%M") if groupe.heure_reunion else None,
+ "assemblee_affaires_ordre": groupe.assemblee_affaires_ordre,
+ "format_reunion": groupe.format_reunion,
+ "cree_le": groupe.cree_le,
+ "membres": nb_membres,
+ "reunions": nb_reunions,
+ "traces": nb_traces,
+ })
+ return out
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 0338828..0982148 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -30,6 +30,7 @@ import ReunionHebdomadaire from './pages/ReunionHebdomadaire';
import AssembleeAffaires from './pages/AssembleeAffaires';
import AssembleeInvite from './pages/AssembleeInvite';
import JournalActions from './pages/JournalActions';
+import GestionGroupes from './pages/GestionGroupes';
import PrincipesUtilisation from './pages/PrincipesUtilisation';
import ModePapier from './pages/ModePapier';
import BottomNav from './components/BottomNav';
@@ -83,6 +84,7 @@ export default function App() {
case 'G11': pageActive = ; break;
case 'G12': pageActive = ; break;
case 'G13': pageActive = ; break;
+ case 'G14': pageActive = ; break;
case 'M1': pageActive = ; break;
case 'M2': pageActive = ; break;
case 'M3': pageActive = ; break;
diff --git a/frontend/src/pages/Accueil.jsx b/frontend/src/pages/Accueil.jsx
index e13ab9a..f70dede 100644
--- a/frontend/src/pages/Accueil.jsx
+++ b/frontend/src/pages/Accueil.jsx
@@ -12,7 +12,7 @@ const COULEURS = {
gray: 'bg-gray-50 border-gray-200 text-gray-700',
};
-const NAVIGABLE = ['C7', 'S1', 'S1T', 'S2', 'S3', 'S4', 'S5', 'C1', 'C2', 'C4', 'C5', 'C9', 'C11', 'C12', 'G2', 'G9', 'G11', 'G12', 'G13', 'M0', 'M1', 'M2', 'M3', 'M4', 'M5', 'M6'];
+const NAVIGABLE = ['C7', 'S1', 'S1T', 'S2', 'S3', 'S4', 'S5', 'C1', 'C2', 'C4', 'C5', 'C9', 'C11', 'C12', 'G2', 'G9', 'G11', 'G12', 'G13', 'G14', 'M0', 'M1', 'M2', 'M3', 'M4', 'M5', 'M6'];
const MODULES_CONTEXTE = new Set(['S0', 'M0', 'M1', 'M2']);
const MODULE_INVENTAIRES = {
code: 'M5',
@@ -47,7 +47,7 @@ const CONTEXTES = {
description: 'Modules de gestion et administration.',
sections: [
{ titre: 'Finances', codes: ['S1T', 'S5', 'M4'] },
- { titre: 'Membres et structure', codes: ['G2', 'G11', 'G12', 'G9', 'G13'] },
+ { titre: 'Membres et structure', codes: ['G14', 'G2', 'G11', 'G12', 'G9', 'G13'] },
{ titre: 'Inventaire', codes: ['M5'] },
],
},
diff --git a/frontend/src/pages/GestionGroupes.jsx b/frontend/src/pages/GestionGroupes.jsx
new file mode 100644
index 0000000..f100d59
--- /dev/null
+++ b/frontend/src/pages/GestionGroupes.jsx
@@ -0,0 +1,146 @@
+import React, { useEffect, useState } from 'react';
+import PageHeader from '../components/PageHeader';
+import { api } from '../stores/api';
+import { useStore } from '../stores/store';
+
+const jourLabel = (jour) => {
+ if (!jour) return 'Jour à préciser';
+ return jour.charAt(0).toUpperCase() + jour.slice(1);
+};
+
+export default function GestionGroupes() {
+ const { loadGroupes, logout } = useStore();
+ const [groupes, setGroupes] = useState([]);
+ const [ouvertId, setOuvertId] = useState('');
+ const [loading, setLoading] = useState(true);
+ const [busy, setBusy] = useState(false);
+ const [msg, setMsg] = useState('');
+ const [error, setError] = useState('');
+
+ const charger = async () => {
+ setLoading(true);
+ setError('');
+ try {
+ const data = await api.get('/admin/groupes');
+ setGroupes(data);
+ if (!ouvertId && data.length) setOuvertId(data[0].id);
+ } catch (e) {
+ setError(e.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => { charger(); }, []);
+
+ const installerDemo = async () => {
+ const confirmation = window.prompt('Cette action ajoute le groupe de démonstration "Groupe Résilience" sans effacer les groupes existants. Tapez CONFIRMER pour continuer.');
+ if (confirmation !== 'CONFIRMER') return;
+ setBusy(true);
+ setError('');
+ setMsg('');
+ try {
+ const res = await api.post('/admin/demo');
+ setMsg(res.message || 'Groupe de démonstration installé');
+ await charger();
+ await loadGroupes();
+ } catch (e) {
+ setError(e.message);
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ const remettreAZero = async () => {
+ const confirmation = window.prompt('Cette action efface toute la base et recrée seulement le compte Sysadmin. Tapez EFFACER pour continuer.');
+ if (confirmation !== 'EFFACER') return;
+ setBusy(true);
+ setError('');
+ try {
+ await api.post('/admin/reset');
+ alert('Base remise à zéro. Reconnectez-vous avec Sysadmin / 0000.');
+ logout();
+ } catch (e) {
+ setError(e.message);
+ setBusy(false);
+ }
+ };
+
+ return (
+
+
+
+
+ {msg &&
{msg}
}
+ {error &&
{error}
}
+
+
+
+
+
+
+ {loading &&
Chargement...
}
+
+
+ {groupes.map((groupe) => {
+ const ouvert = ouvertId === groupe.id;
+ const horaire = `${jourLabel(groupe.jour_reunion)}${groupe.heure_reunion ? ` · ${groupe.heure_reunion}` : ''}`;
+ return (
+
+
+
+ {ouvert && (
+
+
+
+
+
+
+
+ {groupe.adresse_local &&
Lieu : {groupe.adresse_local}
}
+ {groupe.format_reunion &&
Format : {groupe.format_reunion}
}
+
District : {groupe.district || 'â'}
+
RĂ©gion : {groupe.region || 'â'}
+
+
+ )}
+
+ );
+ })}
+
+
+
+ );
+}
+
+function Compteur({ label, valeur }) {
+ return (
+
+
{valeur ?? 0}
+
{label}
+
+ );
+}
diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx
index afc571a..c8bab04 100644
--- a/frontend/src/pages/Login.jsx
+++ b/frontend/src/pages/Login.jsx
@@ -51,9 +51,8 @@ export default function Login() {
- {estSysadmin && (
-
-
-
Administration sysadmin
-
- La remise à zéro efface la base courante. Le groupe de démonstration est ajouté séparément, sans remplacer les données existantes.
-
-
-
-
-
- )}
-
{/* Changer PIN */}
{!showPin ? (