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() {
-
â˜źïž
-

Groupe Méditation

-

Choisir un groupe

+

Choisir un groupe

+

District 87-16 · Région 87

{ api.get('/membres/moi').then((data) => { @@ -149,27 +147,6 @@ export default function Profil() { } }; - const operationAdminBd = async (mode) => { - const message = mode === 'demo' - ? 'Cette action va créer le groupe de démonstration "Groupe Résilience" sans effacer les groupes existants. Tapez CONFIRMER pour continuer.' - : 'Cette action va remettre la base à zéro et effacer les données actuelles. Tapez CONFIRMER pour continuer.'; - const confirmation = window.prompt(message); - if (confirmation !== 'CONFIRMER') return; - setAdminBusy(true); - setError(''); - try { - await api.post(mode === 'demo' ? '/admin/demo' : '/admin/reset'); - alert(mode === 'demo' - ? 'Groupe de démonstration installé. Vous pouvez le choisir à la page de connexion.' - : 'Base remise à zéro. Reconnectez-vous avec Sysadmin / 0000.'); - if (mode === 'reset') logout(); - } catch (e) { - setError(e.message); - } finally { - setAdminBusy(false); - } - }; - if (!profil) return

Chargement...

; const Field = ({ label, value }) => ( @@ -362,31 +339,6 @@ export default function Profil() { )}
- {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 ? (