Ajouter gestion sysadmin des groupes
- retirer les actions BD du profil - ajouter module Gestion des groupes - neutraliser le titre de la page de selection
This commit is contained in:
parent
0f69e80c77
commit
c4c7dc7b64
7 changed files with 194 additions and 54 deletions
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = <GestionMandats />; break;
|
||||
case 'G12': pageActive = <GererPostes />; break;
|
||||
case 'G13': pageActive = <JournalActions />; break;
|
||||
case 'G14': pageActive = <GestionGroupes />; break;
|
||||
case 'M1': pageActive = <RedigerPV />; break;
|
||||
case 'M2': pageActive = <Presences />; break;
|
||||
case 'M3': pageActive = <RapportRSG />; break;
|
||||
|
|
|
|||
|
|
@ -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'] },
|
||||
],
|
||||
},
|
||||
|
|
|
|||
146
frontend/src/pages/GestionGroupes.jsx
Normal file
146
frontend/src/pages/GestionGroupes.jsx
Normal file
|
|
@ -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 (
|
||||
<div className="min-h-screen bg-gray-100">
|
||||
<PageHeader titre="Gestion des groupes" sousTitre="Groupes, démo et opérations sysadmin" />
|
||||
|
||||
<div className="p-4 space-y-4">
|
||||
{msg && <div className="bg-green-50 text-green-700 p-3 rounded-xl text-center text-sm font-medium">{msg}</div>}
|
||||
{error && <div className="bg-red-50 text-red-600 p-3 rounded-xl text-center text-sm">{error}</div>}
|
||||
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
<button
|
||||
onClick={installerDemo}
|
||||
disabled={busy}
|
||||
className="w-full min-h-[48px] p-3 bg-aa-blue text-white rounded-xl text-sm font-bold disabled:opacity-40"
|
||||
>
|
||||
Installer le groupe de démonstration
|
||||
</button>
|
||||
<button
|
||||
onClick={remettreAZero}
|
||||
disabled={busy}
|
||||
className="w-full min-h-[48px] p-3 bg-red-50 border border-red-200 text-red-700 rounded-xl text-sm font-bold disabled:opacity-40"
|
||||
>
|
||||
Remettre toute la BD à vide
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading && <p className="text-center text-gray-400 py-8">Chargement...</p>}
|
||||
|
||||
<div className="space-y-3">
|
||||
{groupes.map((groupe) => {
|
||||
const ouvert = ouvertId === groupe.id;
|
||||
const horaire = `${jourLabel(groupe.jour_reunion)}${groupe.heure_reunion ? ` · ${groupe.heure_reunion}` : ''}`;
|
||||
return (
|
||||
<section key={groupe.id} className="overflow-hidden rounded-2xl bg-white shadow-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOuvertId(ouvert ? '' : groupe.id)}
|
||||
className="flex w-full items-center justify-between gap-3 p-4 text-left active:bg-gray-50"
|
||||
aria-expanded={ouvert}
|
||||
>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-lg font-bold text-gray-900">{groupe.nom}</span>
|
||||
<span className="mt-1 block text-sm font-semibold text-aa-blue">{horaire}</span>
|
||||
</span>
|
||||
<span className={`shrink-0 text-2xl text-aa-blue transition ${ouvert ? 'rotate-180' : ''}`}>⌄</span>
|
||||
</button>
|
||||
|
||||
{ouvert && (
|
||||
<div className="border-t border-gray-100 p-4 pt-3 space-y-3">
|
||||
<div className="grid grid-cols-3 gap-2 text-center">
|
||||
<Compteur label="Membres" valeur={groupe.membres} />
|
||||
<Compteur label="Rencontres" valeur={groupe.reunions} />
|
||||
<Compteur label="Traces" valeur={groupe.traces} />
|
||||
</div>
|
||||
<div className="space-y-2 rounded-xl bg-gray-50 p-3 text-sm text-gray-700">
|
||||
{groupe.adresse_local && <p><span className="font-semibold text-gray-900">Lieu : </span>{groupe.adresse_local}</p>}
|
||||
{groupe.format_reunion && <p><span className="font-semibold text-gray-900">Format : </span>{groupe.format_reunion}</p>}
|
||||
<p><span className="font-semibold text-gray-900">District : </span>{groupe.district || '—'}</p>
|
||||
<p><span className="font-semibold text-gray-900">Région : </span>{groupe.region || '—'}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Compteur({ label, valeur }) {
|
||||
return (
|
||||
<div className="rounded-xl bg-gray-50 p-3">
|
||||
<p className="text-lg font-extrabold text-gray-900">{valeur ?? 0}</p>
|
||||
<p className="text-xs text-gray-400">{label}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -51,9 +51,8 @@ export default function Login() {
|
|||
<div className="min-h-screen bg-gradient-to-b from-aa-blue to-blue-900 p-4">
|
||||
<div className="mx-auto flex min-h-screen w-full max-w-md flex-col py-6">
|
||||
<div className="text-center mb-5">
|
||||
<div className="text-5xl mb-3">☮️</div>
|
||||
<h1 className="text-2xl font-bold text-white">Groupe Méditation</h1>
|
||||
<p className="text-blue-200 text-sm mt-1">Choisir un groupe</p>
|
||||
<h1 className="text-2xl font-bold text-white">Choisir un groupe</h1>
|
||||
<p className="text-blue-200 text-sm mt-1">District 87-16 · Région 87</p>
|
||||
</div>
|
||||
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ export default function Profil() {
|
|||
const {
|
||||
setPage,
|
||||
estSysadmin,
|
||||
logout,
|
||||
postes,
|
||||
modulesConsultation,
|
||||
modulesSaisie,
|
||||
|
|
@ -31,7 +30,6 @@ export default function Profil() {
|
|||
const [notificationTypes, setNotificationTypes] = useState([]);
|
||||
const [notificationPrefs, setNotificationPrefs] = useState({});
|
||||
const [savingPrefs, setSavingPrefs] = useState(false);
|
||||
const [adminBusy, setAdminBusy] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
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 <div className="flex items-center justify-center min-h-screen"><p className="text-gray-500">Chargement...</p></div>;
|
||||
|
||||
const Field = ({ label, value }) => (
|
||||
|
|
@ -362,31 +339,6 @@ export default function Profil() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{estSysadmin && (
|
||||
<div className="bg-white rounded-2xl p-4 shadow-sm space-y-3 border border-red-100">
|
||||
<div>
|
||||
<h2 className="font-bold text-red-700">Administration sysadmin</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
La remise à zéro efface la base courante. Le groupe de démonstration est ajouté séparément, sans remplacer les données existantes.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => operationAdminBd('reset')}
|
||||
disabled={adminBusy}
|
||||
className="w-full min-h-[44px] p-3 bg-red-50 border border-red-200 text-red-700 rounded-xl font-bold disabled:opacity-40"
|
||||
>
|
||||
Remettre la BD à vide
|
||||
</button>
|
||||
<button
|
||||
onClick={() => operationAdminBd('demo')}
|
||||
disabled={adminBusy}
|
||||
className="w-full min-h-[44px] p-3 bg-aa-blue text-white rounded-xl font-bold disabled:opacity-40"
|
||||
>
|
||||
Installer le groupe de démonstration
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Changer PIN */}
|
||||
<div className="bg-white rounded-2xl p-4 shadow-sm">
|
||||
{!showPin ? (
|
||||
|
|
|
|||
Loading…
Reference in a new issue