Ajouter acces sysadmin aux groupes
- bouton Sysadmin public avec PIN - CRUD sysadmin des groupes - groupe demo protege et reinitialisable
This commit is contained in:
parent
c4c7dc7b64
commit
1946b8741e
6 changed files with 379 additions and 12 deletions
|
|
@ -2,24 +2,59 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import func, select
|
||||
from pydantic import BaseModel
|
||||
|
||||
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.core.security import create_token, get_current_membre, is_sysadmin, verify_pin
|
||||
from app.services.admin_database import (
|
||||
creer_groupe,
|
||||
initialiser_demo,
|
||||
modifier_groupe,
|
||||
reinitialiser_demo,
|
||||
remettre_a_zero,
|
||||
supprimer_groupe,
|
||||
)
|
||||
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
|
||||
from app.schemas.schemas import MembrePublic, TokenResponse
|
||||
|
||||
router = APIRouter(prefix="/admin", tags=["Administration"])
|
||||
|
||||
|
||||
class SysadminLoginRequest(BaseModel):
|
||||
pin: str
|
||||
|
||||
|
||||
class GroupeAdminRequest(BaseModel):
|
||||
nom: str
|
||||
district: str = "87-16"
|
||||
region: str = "87"
|
||||
adresse_local: str | None = None
|
||||
jour_reunion: str | None = "mardi"
|
||||
heure_reunion: str | None = "19:30"
|
||||
assemblee_affaires_ordre: str = "premiere"
|
||||
format_reunion: str | None = "Discussion ouverte"
|
||||
lien_aa87: str = "https://aa87.org"
|
||||
|
||||
|
||||
async def require_sysadmin(membre=Depends(get_current_membre)):
|
||||
if not is_sysadmin(membre):
|
||||
raise HTTPException(status_code=403, detail="Réservé au sysadmin")
|
||||
return membre
|
||||
|
||||
|
||||
@router.post("/sysadmin-login", response_model=TokenResponse)
|
||||
async def sysadmin_login(req: SysadminLoginRequest, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(Membre).where(Membre.courriel == "sysadmin", Membre.actif == True))
|
||||
for membre in result.scalars().all():
|
||||
if verify_pin(req.pin, membre.pin_hash):
|
||||
token = create_token(str(membre.id), str(membre.groupe_id), expire_jours=1)
|
||||
return TokenResponse(access_token=token, membre=MembrePublic.model_validate(membre))
|
||||
raise HTTPException(status_code=401, detail="PIN Sysadmin invalide")
|
||||
|
||||
|
||||
@router.post("/reset")
|
||||
async def reset_database(
|
||||
_sysadmin=Depends(require_sysadmin),
|
||||
|
|
@ -36,11 +71,20 @@ async def demo_database(
|
|||
return await initialiser_demo(db)
|
||||
|
||||
|
||||
@router.post("/demo/reset")
|
||||
async def reset_demo_database(
|
||||
_sysadmin=Depends(require_sysadmin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
return await reinitialiser_demo(db)
|
||||
|
||||
|
||||
@router.get("/groupes")
|
||||
async def liste_groupes_admin(
|
||||
_sysadmin=Depends(require_sysadmin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
await initialiser_demo(db)
|
||||
groupes = (await db.execute(select(Groupe).order_by(Groupe.nom))).scalars().all()
|
||||
out = []
|
||||
for groupe in groupes:
|
||||
|
|
@ -63,9 +107,51 @@ async def liste_groupes_admin(
|
|||
"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,
|
||||
"lien_aa87": groupe.lien_aa87,
|
||||
"cree_le": groupe.cree_le,
|
||||
"membres": nb_membres,
|
||||
"reunions": nb_reunions,
|
||||
"traces": nb_traces,
|
||||
"demo": groupe.nom == "Groupe Résilience",
|
||||
"protege": groupe.nom == "Groupe Résilience",
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
@router.post("/groupes")
|
||||
async def creer_groupe_admin(
|
||||
req: GroupeAdminRequest,
|
||||
_sysadmin=Depends(require_sysadmin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
try:
|
||||
groupe = await creer_groupe(db, req.model_dump())
|
||||
return {"message": "Groupe créé", "id": str(groupe.id)}
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
|
||||
|
||||
@router.patch("/groupes/{groupe_id}")
|
||||
async def modifier_groupe_admin(
|
||||
groupe_id: str,
|
||||
req: GroupeAdminRequest,
|
||||
_sysadmin=Depends(require_sysadmin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
try:
|
||||
groupe = await modifier_groupe(db, groupe_id, req.model_dump())
|
||||
return {"message": "Groupe modifié", "id": str(groupe.id)}
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
|
||||
|
||||
@router.delete("/groupes/{groupe_id}")
|
||||
async def supprimer_groupe_admin(
|
||||
groupe_id: str,
|
||||
_sysadmin=Depends(require_sysadmin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
try:
|
||||
return await supprimer_groupe(db, groupe_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ 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
|
||||
from app.services.admin_database import initialiser_demo
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["Authentification"])
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ async def health(db: AsyncSession = Depends(get_db)):
|
|||
|
||||
@router.get("/groupes")
|
||||
async def liste_groupes(db: AsyncSession = Depends(get_db)):
|
||||
await initialiser_demo(db)
|
||||
result = await db.execute(select(Groupe).order_by(Groupe.nom))
|
||||
groupes = result.scalars().all()
|
||||
return [{
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ async def initialiser_demo(db: AsyncSession) -> dict:
|
|||
if existant:
|
||||
membres_existants = await _membres_par_courriel(db, existant.id)
|
||||
return {
|
||||
"message": "Le groupe de démonstration existe déjà. Aucune donnée existante n'a été modifiée.",
|
||||
"message": "Le groupe de démonstration existe déjà.",
|
||||
"groupe_id": existant.id,
|
||||
"membres": len(membres_existants),
|
||||
"periode_mois": 12,
|
||||
|
|
@ -120,6 +120,7 @@ async def initialiser_demo(db: AsyncSession) -> dict:
|
|||
}
|
||||
|
||||
groupe, sysadmin, postes, litterature = await _creer_base(
|
||||
db,
|
||||
nom_groupe="Groupe Résilience",
|
||||
adresse_local="Salle communautaire Saint-Louis, Montréal",
|
||||
)
|
||||
|
|
@ -139,6 +140,86 @@ async def initialiser_demo(db: AsyncSession) -> dict:
|
|||
}
|
||||
|
||||
|
||||
async def reinitialiser_demo(db: AsyncSession) -> dict:
|
||||
groupe = (await db.execute(select(Groupe).where(Groupe.nom == "Groupe Résilience"))).scalar_one_or_none()
|
||||
if not groupe:
|
||||
return await initialiser_demo(db)
|
||||
await _vider_groupe(db, groupe.id)
|
||||
sysadmin, postes, litterature = await _peupler_groupe_base(db, groupe)
|
||||
membres = {sysadmin.courriel: sysadmin}
|
||||
await _ajouter_membres_demo(db, groupe, membres, postes)
|
||||
membres = await _membres_par_courriel(db, groupe.id)
|
||||
await _ajouter_historique_demo(db, groupe, membres, postes, litterature)
|
||||
await db.flush()
|
||||
return {
|
||||
"message": "Données fictives du groupe de démonstration réinitialisées",
|
||||
"groupe_id": groupe.id,
|
||||
"membres": len(membres),
|
||||
"periode_mois": 12,
|
||||
"rencontres": 52,
|
||||
"assemblees": 12,
|
||||
}
|
||||
|
||||
|
||||
async def creer_groupe(db: AsyncSession, data: dict) -> Groupe:
|
||||
nom = (data.get("nom") or "").strip()
|
||||
if not nom:
|
||||
raise ValueError("Nom requis")
|
||||
if nom == "Groupe Résilience":
|
||||
raise ValueError("Ce nom est réservé au groupe de démonstration")
|
||||
groupe = Groupe(
|
||||
nom=nom,
|
||||
district=(data.get("district") or "87-16").strip(),
|
||||
region=(data.get("region") or "87").strip(),
|
||||
adresse_local=(data.get("adresse_local") or "").strip() or None,
|
||||
jour_reunion=(data.get("jour_reunion") or "mardi").strip() or None,
|
||||
heure_reunion=_parse_heure(data.get("heure_reunion")),
|
||||
assemblee_affaires_ordre=(data.get("assemblee_affaires_ordre") or "premiere").strip(),
|
||||
format_reunion=(data.get("format_reunion") or "Discussion ouverte").strip() or None,
|
||||
lien_aa87=(data.get("lien_aa87") or "https://aa87.org").strip(),
|
||||
)
|
||||
db.add(groupe)
|
||||
await db.flush()
|
||||
await _peupler_groupe_base(db, groupe)
|
||||
await db.flush()
|
||||
return groupe
|
||||
|
||||
|
||||
async def modifier_groupe(db: AsyncSession, groupe_id, data: dict) -> Groupe:
|
||||
groupe = await db.get(Groupe, groupe_id)
|
||||
if not groupe:
|
||||
raise ValueError("Groupe introuvable")
|
||||
nom = (data.get("nom") or groupe.nom).strip()
|
||||
if groupe.nom == "Groupe Résilience" and nom != "Groupe Résilience":
|
||||
raise ValueError("Le groupe de démonstration ne peut pas être renommé")
|
||||
if nom == "Groupe Résilience" and groupe.nom != "Groupe Résilience":
|
||||
raise ValueError("Ce nom est réservé au groupe de démonstration")
|
||||
groupe.nom = nom
|
||||
for attr in ["district", "region", "adresse_local", "jour_reunion", "assemblee_affaires_ordre", "format_reunion", "lien_aa87"]:
|
||||
if attr in data:
|
||||
valeur = data.get(attr)
|
||||
setattr(groupe, attr, valeur.strip() if isinstance(valeur, str) and valeur.strip() else None)
|
||||
if "heure_reunion" in data:
|
||||
groupe.heure_reunion = _parse_heure(data.get("heure_reunion"))
|
||||
await db.flush()
|
||||
return groupe
|
||||
|
||||
|
||||
async def supprimer_groupe(db: AsyncSession, groupe_id) -> dict:
|
||||
groupe = await db.get(Groupe, groupe_id)
|
||||
if not groupe:
|
||||
raise ValueError("Groupe introuvable")
|
||||
if groupe.nom == "Groupe Résilience":
|
||||
raise ValueError("Le groupe de démonstration est protégé")
|
||||
nb_groupes = (await db.execute(select(Groupe))).scalars().all()
|
||||
if len(nb_groupes) <= 1:
|
||||
raise ValueError("Il doit rester au moins un groupe")
|
||||
await _vider_groupe(db, groupe.id)
|
||||
await db.delete(groupe)
|
||||
await db.flush()
|
||||
return {"message": f"Groupe {groupe.nom} supprimé"}
|
||||
|
||||
|
||||
async def _creer_base(
|
||||
db: AsyncSession,
|
||||
nom_groupe: str = "Groupe Méditation",
|
||||
|
|
@ -157,7 +238,11 @@ async def _creer_base(
|
|||
)
|
||||
db.add(groupe)
|
||||
await db.flush()
|
||||
sysadmin, postes, litterature = await _peupler_groupe_base(db, groupe)
|
||||
return groupe, sysadmin, postes, litterature
|
||||
|
||||
|
||||
async def _peupler_groupe_base(db: AsyncSession, groupe: Groupe):
|
||||
postes = {}
|
||||
for nom, categorie, description, prerequis, ordre, modules in POSTES:
|
||||
poste = Poste(
|
||||
|
|
@ -206,7 +291,7 @@ async def _creer_base(
|
|||
db.add(DestinataireContribution(groupe_id=groupe.id, nom="Bureau des services généraux", protege=False, actif=True))
|
||||
for destination, pct in [("district", 50), ("region", 20), ("bsg", 20), ("intergroupe", 10)]:
|
||||
db.add(ConfigRepartition(groupe_id=groupe.id, destination=destination, pourcentage=pct, actif_depuis=date.today().replace(day=1)))
|
||||
return groupe, sysadmin, postes, litterature
|
||||
return sysadmin, postes, litterature
|
||||
|
||||
|
||||
async def _ajouter_membres_demo(db: AsyncSession, groupe: Groupe, membres: dict, postes: dict) -> None:
|
||||
|
|
@ -587,6 +672,54 @@ def _moment(jour: date, heure: int = 20, minute: int = 0) -> datetime:
|
|||
return datetime.combine(jour, time(heure, minute)).replace(tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def _parse_heure(valeur) -> time | None:
|
||||
if isinstance(valeur, time):
|
||||
return valeur
|
||||
if not valeur:
|
||||
return None
|
||||
heures, minutes = str(valeur).split(":", 1)
|
||||
return time(int(heures), int(minutes[:2]))
|
||||
|
||||
|
||||
async def _vider_groupe(db: AsyncSession, groupe_id) -> None:
|
||||
params = {"gid": str(groupe_id)}
|
||||
await db.execute(text("DELETE FROM presences WHERE reunion_id IN (SELECT id FROM reunions WHERE groupe_id = :gid)"), params)
|
||||
await db.execute(text("DELETE FROM pv_reunions WHERE reunion_id IN (SELECT id FROM reunions WHERE groupe_id = :gid)"), params)
|
||||
await db.execute(text("DELETE FROM postes_modules WHERE poste_id IN (SELECT id FROM postes WHERE groupe_id = :gid)"), params)
|
||||
await db.execute(text("UPDATE candidatures SET proposition_id = NULL WHERE groupe_id = :gid"), params)
|
||||
await db.execute(text("UPDATE propositions SET candidature_id = NULL WHERE groupe_id = :gid"), params)
|
||||
for table in [
|
||||
"journal_actions",
|
||||
"historique_modifications",
|
||||
"push_subscriptions",
|
||||
"mouvements_reserves",
|
||||
"transactions_comptables",
|
||||
"rapports_adoptes",
|
||||
"rapports_rsg",
|
||||
"ventes_litterature",
|
||||
"ventes_jetons",
|
||||
"jetons",
|
||||
"envois_contributions",
|
||||
"destinataires_contributions",
|
||||
"config_repartition",
|
||||
"operations_bancaires",
|
||||
"reserves",
|
||||
"evenements",
|
||||
"depenses",
|
||||
"collectes",
|
||||
"propositions",
|
||||
"candidatures",
|
||||
"rotations",
|
||||
"affectations",
|
||||
"litterature",
|
||||
"invitations",
|
||||
"reunions",
|
||||
"postes",
|
||||
"membres",
|
||||
]:
|
||||
await db.execute(text(f"DELETE FROM {table} WHERE groupe_id = :gid"), params)
|
||||
|
||||
|
||||
async def _trace(
|
||||
db: AsyncSession,
|
||||
groupe: Groupe,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ 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
|
||||
from app.services.admin_database import initialiser_demo
|
||||
# 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
|
||||
|
|
@ -200,6 +201,9 @@ async def seed():
|
|||
))
|
||||
print(" ✓ Destinataire de contribution : District 87-16")
|
||||
|
||||
demo = await initialiser_demo(db)
|
||||
print(f" ✓ Groupe de démonstration : {demo['message']}")
|
||||
|
||||
await db.commit()
|
||||
|
||||
print(f"\n═══ Initialisation terminée ═══")
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ export default function GestionGroupes() {
|
|||
const { loadGroupes, logout } = useStore();
|
||||
const [groupes, setGroupes] = useState([]);
|
||||
const [ouvertId, setOuvertId] = useState('');
|
||||
const [editionId, setEditionId] = useState('');
|
||||
const [form, setForm] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [msg, setMsg] = useState('');
|
||||
|
|
@ -33,15 +35,86 @@ export default function GestionGroupes() {
|
|||
|
||||
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.');
|
||||
const resetDemo = async () => {
|
||||
const confirmation = window.prompt('Cette action réinitialise seulement les données fictives du groupe de démonstration. 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é');
|
||||
const res = await api.post('/admin/demo/reset');
|
||||
setMsg(res.message || 'Groupe de démonstration réinitialisé');
|
||||
await charger();
|
||||
await loadGroupes();
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const nouveauGroupe = () => {
|
||||
setEditionId('nouveau');
|
||||
setForm({
|
||||
nom: '',
|
||||
district: '87-16',
|
||||
region: '87',
|
||||
adresse_local: '',
|
||||
jour_reunion: 'mardi',
|
||||
heure_reunion: '19:30',
|
||||
assemblee_affaires_ordre: 'premiere',
|
||||
format_reunion: 'Discussion ouverte',
|
||||
lien_aa87: 'https://aa87.org',
|
||||
});
|
||||
};
|
||||
|
||||
const modifier = (groupe) => {
|
||||
setEditionId(groupe.id);
|
||||
setForm({
|
||||
nom: groupe.nom || '',
|
||||
district: groupe.district || '87-16',
|
||||
region: groupe.region || '87',
|
||||
adresse_local: groupe.adresse_local || '',
|
||||
jour_reunion: groupe.jour_reunion || 'mardi',
|
||||
heure_reunion: groupe.heure_reunion || '19:30',
|
||||
assemblee_affaires_ordre: groupe.assemblee_affaires_ordre || 'premiere',
|
||||
format_reunion: groupe.format_reunion || '',
|
||||
lien_aa87: groupe.lien_aa87 || 'https://aa87.org',
|
||||
});
|
||||
setOuvertId(groupe.id);
|
||||
};
|
||||
|
||||
const sauverGroupe = async () => {
|
||||
if (!form?.nom?.trim()) { setError('Nom requis'); return; }
|
||||
setBusy(true);
|
||||
setError('');
|
||||
try {
|
||||
if (editionId === 'nouveau') {
|
||||
await api.post('/admin/groupes', form);
|
||||
setMsg('Groupe créé');
|
||||
} else {
|
||||
await api.patch(`/admin/groupes/${editionId}`, form);
|
||||
setMsg('Groupe modifié');
|
||||
}
|
||||
setEditionId('');
|
||||
setForm(null);
|
||||
await charger();
|
||||
await loadGroupes();
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const supprimer = async (groupe) => {
|
||||
const confirmation = window.prompt(`Supprimer le groupe "${groupe.nom}" et ses données? Tapez SUPPRIMER pour continuer.`);
|
||||
if (confirmation !== 'SUPPRIMER') return;
|
||||
setBusy(true);
|
||||
setError('');
|
||||
try {
|
||||
await api.delete(`/admin/groupes/${groupe.id}`);
|
||||
setMsg('Groupe supprimé');
|
||||
await charger();
|
||||
await loadGroupes();
|
||||
} catch (e) {
|
||||
|
|
@ -76,11 +149,11 @@ export default function GestionGroupes() {
|
|||
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
<button
|
||||
onClick={installerDemo}
|
||||
onClick={nouveauGroupe}
|
||||
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
|
||||
Créer un groupe
|
||||
</button>
|
||||
<button
|
||||
onClick={remettreAZero}
|
||||
|
|
@ -91,6 +164,34 @@ export default function GestionGroupes() {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
{form && (
|
||||
<div className="bg-white rounded-2xl p-4 shadow-sm space-y-3">
|
||||
<h2 className="font-bold text-gray-800">{editionId === 'nouveau' ? 'Nouveau groupe' : 'Modifier le groupe'}</h2>
|
||||
<input value={form.nom} onChange={(e) => setForm({ ...form, nom: e.target.value })} placeholder="Nom du groupe" className="w-full p-3 bg-gray-50 rounded-xl border-0" />
|
||||
<input value={form.adresse_local} onChange={(e) => setForm({ ...form, adresse_local: e.target.value })} placeholder="Adresse / coordonnées" className="w-full p-3 bg-gray-50 rounded-xl border-0" />
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<input value={form.jour_reunion} onChange={(e) => setForm({ ...form, jour_reunion: e.target.value })} placeholder="Jour" className="w-full p-3 bg-gray-50 rounded-xl border-0" />
|
||||
<input type="time" value={form.heure_reunion} onChange={(e) => setForm({ ...form, heure_reunion: e.target.value })} className="w-full p-3 bg-gray-50 rounded-xl border-0" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<input value={form.district} onChange={(e) => setForm({ ...form, district: e.target.value })} placeholder="District" className="w-full p-3 bg-gray-50 rounded-xl border-0" />
|
||||
<input value={form.region} onChange={(e) => setForm({ ...form, region: e.target.value })} placeholder="Région" className="w-full p-3 bg-gray-50 rounded-xl border-0" />
|
||||
</div>
|
||||
<select value={form.assemblee_affaires_ordre} onChange={(e) => setForm({ ...form, assemblee_affaires_ordre: e.target.value })} className="w-full p-3 bg-gray-50 rounded-xl border-0">
|
||||
<option value="premiere">Assemblée la première réunion du mois</option>
|
||||
<option value="deuxieme">Assemblée la deuxième réunion du mois</option>
|
||||
<option value="troisieme">Assemblée la troisième réunion du mois</option>
|
||||
<option value="quatrieme">Assemblée la quatrième réunion du mois</option>
|
||||
<option value="derniere">Assemblée la dernière réunion du mois</option>
|
||||
</select>
|
||||
<textarea value={form.format_reunion} onChange={(e) => setForm({ ...form, format_reunion: e.target.value })} placeholder="Format de réunion" rows={2} className="w-full p-3 bg-gray-50 rounded-xl border-0" />
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button onClick={sauverGroupe} disabled={busy} className="p-3 bg-green-600 text-white rounded-xl text-sm font-bold disabled:opacity-40">Enregistrer</button>
|
||||
<button onClick={() => { setEditionId(''); setForm(null); }} className="p-3 bg-gray-100 text-gray-600 rounded-xl text-sm font-bold">Annuler</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && <p className="text-center text-gray-400 py-8">Chargement...</p>}
|
||||
|
||||
<div className="space-y-3">
|
||||
|
|
@ -105,10 +206,18 @@ export default function GestionGroupes() {
|
|||
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="min-w-0 flex-1">
|
||||
<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>
|
||||
{groupe.demo && (
|
||||
<span
|
||||
onClick={(e) => { e.stopPropagation(); resetDemo(); }}
|
||||
className="shrink-0 rounded-xl bg-indigo-50 px-3 py-2 text-xs font-bold text-indigo-700"
|
||||
>
|
||||
Réinitialiser
|
||||
</span>
|
||||
)}
|
||||
<span className={`shrink-0 text-2xl text-aa-blue transition ${ouvert ? 'rotate-180' : ''}`}>⌄</span>
|
||||
</button>
|
||||
|
||||
|
|
@ -125,6 +234,12 @@ export default function GestionGroupes() {
|
|||
<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 className="grid grid-cols-2 gap-2">
|
||||
<button onClick={() => modifier(groupe)} className="p-3 bg-gray-100 text-gray-700 rounded-xl text-sm font-bold">Modifier</button>
|
||||
<button onClick={() => supprimer(groupe)} disabled={groupe.protege || busy} className="p-3 bg-red-50 text-red-700 rounded-xl text-sm font-bold disabled:opacity-40">
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useStore } from '../stores/store';
|
||||
import { api } from '../stores/api';
|
||||
|
||||
const jourLabel = (jour) => {
|
||||
if (!jour) return 'Jour à préciser';
|
||||
|
|
@ -7,7 +8,7 @@ const jourLabel = (jour) => {
|
|||
};
|
||||
|
||||
export default function Login() {
|
||||
const { login, loading, error, groupes, loadGroupes, setPage } = useStore();
|
||||
const { login, loadAccueil, loading, error, groupes, loadGroupes, setPage } = useStore();
|
||||
const [courriel, setCourriel] = useState(localStorage.getItem('gm_courriel') || '');
|
||||
const [pin, setPin] = useState('');
|
||||
const [groupeId, setGroupeId] = useState(localStorage.getItem('gm_groupe_id') || '');
|
||||
|
|
@ -47,6 +48,25 @@ export default function Login() {
|
|||
} catch {}
|
||||
};
|
||||
|
||||
const ouvrirSysadmin = async () => {
|
||||
const pin = window.prompt('PIN Sysadmin');
|
||||
if (!pin) return;
|
||||
try {
|
||||
const data = await api.post('/admin/sysadmin-login', { pin });
|
||||
api.setToken(data.access_token, false);
|
||||
useStore.setState({
|
||||
token: data.access_token,
|
||||
membre: data.membre,
|
||||
groupeId: data.membre.groupe_id,
|
||||
error: null,
|
||||
});
|
||||
await loadAccueil();
|
||||
setPage('G14');
|
||||
} catch (e) {
|
||||
useStore.setState({ error: e.message });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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">
|
||||
|
|
@ -158,6 +178,13 @@ export default function Login() {
|
|||
})}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={ouvrirSysadmin}
|
||||
className="mx-auto mb-3 block px-3 py-2 text-xs font-semibold text-blue-200/80"
|
||||
>
|
||||
Sysadmin
|
||||
</button>
|
||||
<p className="text-center text-xs text-blue-300">100% logiciel libre · Hébergé au Québec</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue