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="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"),
|
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 _modules in [MODULES_SAISIE_TOUS, MODULES_GESTION, *MODULES_SAISIE.values()]:
|
||||||
for _m in _modules:
|
for _m in _modules:
|
||||||
|
|
@ -104,7 +105,7 @@ async def accueil(
|
||||||
est_executif=True,
|
est_executif=True,
|
||||||
modules_consultation=MODULES_CONSULTATION,
|
modules_consultation=MODULES_CONSULTATION,
|
||||||
modules_saisie=modules_saisie_sysadmin,
|
modules_saisie=modules_saisie_sysadmin,
|
||||||
modules_gestion=MODULES_GESTION,
|
modules_gestion=[MODULE_SYSADMIN, *MODULES_GESTION],
|
||||||
groupe=groupe_info,
|
groupe=groupe_info,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
"""Routes d'administration reservees au sysadmin."""
|
"""Routes d'administration reservees au sysadmin."""
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import func, select
|
||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.core.security import get_current_membre, is_sysadmin
|
from app.core.security import get_current_membre, is_sysadmin
|
||||||
from app.services.admin_database import initialiser_demo, remettre_a_zero
|
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"])
|
router = APIRouter(prefix="/admin", tags=["Administration"])
|
||||||
|
|
||||||
|
|
@ -29,3 +34,38 @@ async def demo_database(
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
return await initialiser_demo(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 AssembleeAffaires from './pages/AssembleeAffaires';
|
||||||
import AssembleeInvite from './pages/AssembleeInvite';
|
import AssembleeInvite from './pages/AssembleeInvite';
|
||||||
import JournalActions from './pages/JournalActions';
|
import JournalActions from './pages/JournalActions';
|
||||||
|
import GestionGroupes from './pages/GestionGroupes';
|
||||||
import PrincipesUtilisation from './pages/PrincipesUtilisation';
|
import PrincipesUtilisation from './pages/PrincipesUtilisation';
|
||||||
import ModePapier from './pages/ModePapier';
|
import ModePapier from './pages/ModePapier';
|
||||||
import BottomNav from './components/BottomNav';
|
import BottomNav from './components/BottomNav';
|
||||||
|
|
@ -83,6 +84,7 @@ export default function App() {
|
||||||
case 'G11': pageActive = <GestionMandats />; break;
|
case 'G11': pageActive = <GestionMandats />; break;
|
||||||
case 'G12': pageActive = <GererPostes />; break;
|
case 'G12': pageActive = <GererPostes />; break;
|
||||||
case 'G13': pageActive = <JournalActions />; break;
|
case 'G13': pageActive = <JournalActions />; break;
|
||||||
|
case 'G14': pageActive = <GestionGroupes />; break;
|
||||||
case 'M1': pageActive = <RedigerPV />; break;
|
case 'M1': pageActive = <RedigerPV />; break;
|
||||||
case 'M2': pageActive = <Presences />; break;
|
case 'M2': pageActive = <Presences />; break;
|
||||||
case 'M3': pageActive = <RapportRSG />; break;
|
case 'M3': pageActive = <RapportRSG />; break;
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ const COULEURS = {
|
||||||
gray: 'bg-gray-50 border-gray-200 text-gray-700',
|
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 MODULES_CONTEXTE = new Set(['S0', 'M0', 'M1', 'M2']);
|
||||||
const MODULE_INVENTAIRES = {
|
const MODULE_INVENTAIRES = {
|
||||||
code: 'M5',
|
code: 'M5',
|
||||||
|
|
@ -47,7 +47,7 @@ const CONTEXTES = {
|
||||||
description: 'Modules de gestion et administration.',
|
description: 'Modules de gestion et administration.',
|
||||||
sections: [
|
sections: [
|
||||||
{ titre: 'Finances', codes: ['S1T', 'S5', 'M4'] },
|
{ 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'] },
|
{ 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="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="mx-auto flex min-h-screen w-full max-w-md flex-col py-6">
|
||||||
<div className="text-center mb-5">
|
<div className="text-center mb-5">
|
||||||
<div className="text-5xl mb-3">☮️</div>
|
<h1 className="text-2xl font-bold text-white">Choisir un groupe</h1>
|
||||||
<h1 className="text-2xl font-bold text-white">Groupe Méditation</h1>
|
<p className="text-blue-200 text-sm mt-1">District 87-16 · Région 87</p>
|
||||||
<p className="text-blue-200 text-sm mt-1">Choisir un groupe</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ export default function Profil() {
|
||||||
const {
|
const {
|
||||||
setPage,
|
setPage,
|
||||||
estSysadmin,
|
estSysadmin,
|
||||||
logout,
|
|
||||||
postes,
|
postes,
|
||||||
modulesConsultation,
|
modulesConsultation,
|
||||||
modulesSaisie,
|
modulesSaisie,
|
||||||
|
|
@ -31,7 +30,6 @@ export default function Profil() {
|
||||||
const [notificationTypes, setNotificationTypes] = useState([]);
|
const [notificationTypes, setNotificationTypes] = useState([]);
|
||||||
const [notificationPrefs, setNotificationPrefs] = useState({});
|
const [notificationPrefs, setNotificationPrefs] = useState({});
|
||||||
const [savingPrefs, setSavingPrefs] = useState(false);
|
const [savingPrefs, setSavingPrefs] = useState(false);
|
||||||
const [adminBusy, setAdminBusy] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.get('/membres/moi').then((data) => {
|
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>;
|
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 }) => (
|
const Field = ({ label, value }) => (
|
||||||
|
|
@ -362,31 +339,6 @@ export default function Profil() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</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 */}
|
{/* Changer PIN */}
|
||||||
<div className="bg-white rounded-2xl p-4 shadow-sm">
|
<div className="bg-white rounded-2xl p-4 shadow-sm">
|
||||||
{!showPin ? (
|
{!showPin ? (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue