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 fastapi import APIRouter, Depends, HTTPException
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
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 create_token, get_current_membre, is_sysadmin, verify_pin
|
||||||
from app.services.admin_database import initialiser_demo, remettre_a_zero
|
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.groupe import Groupe
|
||||||
from app.models.membre import Membre
|
from app.models.membre import Membre
|
||||||
from app.models.reunion import Reunion
|
from app.models.reunion import Reunion
|
||||||
from app.models.historique_modification import HistoriqueModification
|
from app.models.historique_modification import HistoriqueModification
|
||||||
|
from app.schemas.schemas import MembrePublic, TokenResponse
|
||||||
|
|
||||||
router = APIRouter(prefix="/admin", tags=["Administration"])
|
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)):
|
async def require_sysadmin(membre=Depends(get_current_membre)):
|
||||||
if not is_sysadmin(membre):
|
if not is_sysadmin(membre):
|
||||||
raise HTTPException(status_code=403, detail="Réservé au sysadmin")
|
raise HTTPException(status_code=403, detail="Réservé au sysadmin")
|
||||||
return membre
|
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")
|
@router.post("/reset")
|
||||||
async def reset_database(
|
async def reset_database(
|
||||||
_sysadmin=Depends(require_sysadmin),
|
_sysadmin=Depends(require_sysadmin),
|
||||||
|
|
@ -36,11 +71,20 @@ async def demo_database(
|
||||||
return await initialiser_demo(db)
|
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")
|
@router.get("/groupes")
|
||||||
async def liste_groupes_admin(
|
async def liste_groupes_admin(
|
||||||
_sysadmin=Depends(require_sysadmin),
|
_sysadmin=Depends(require_sysadmin),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
|
await initialiser_demo(db)
|
||||||
groupes = (await db.execute(select(Groupe).order_by(Groupe.nom))).scalars().all()
|
groupes = (await db.execute(select(Groupe).order_by(Groupe.nom))).scalars().all()
|
||||||
out = []
|
out = []
|
||||||
for groupe in groupes:
|
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,
|
"heure_reunion": groupe.heure_reunion.strftime("%H:%M") if groupe.heure_reunion else None,
|
||||||
"assemblee_affaires_ordre": groupe.assemblee_affaires_ordre,
|
"assemblee_affaires_ordre": groupe.assemblee_affaires_ordre,
|
||||||
"format_reunion": groupe.format_reunion,
|
"format_reunion": groupe.format_reunion,
|
||||||
|
"lien_aa87": groupe.lien_aa87,
|
||||||
"cree_le": groupe.cree_le,
|
"cree_le": groupe.cree_le,
|
||||||
"membres": nb_membres,
|
"membres": nb_membres,
|
||||||
"reunions": nb_reunions,
|
"reunions": nb_reunions,
|
||||||
"traces": nb_traces,
|
"traces": nb_traces,
|
||||||
|
"demo": groupe.nom == "Groupe Résilience",
|
||||||
|
"protege": groupe.nom == "Groupe Résilience",
|
||||||
})
|
})
|
||||||
return out
|
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.groupe import Groupe
|
||||||
from app.models.invitation import Invitation
|
from app.models.invitation import Invitation
|
||||||
from app.schemas.schemas import ConnexionRequest, InscriptionRequest, TokenResponse, MembrePublic
|
from app.schemas.schemas import ConnexionRequest, InscriptionRequest, TokenResponse, MembrePublic
|
||||||
|
from app.services.admin_database import initialiser_demo
|
||||||
|
|
||||||
router = APIRouter(prefix="/auth", tags=["Authentification"])
|
router = APIRouter(prefix="/auth", tags=["Authentification"])
|
||||||
|
|
||||||
|
|
@ -27,6 +28,7 @@ async def health(db: AsyncSession = Depends(get_db)):
|
||||||
|
|
||||||
@router.get("/groupes")
|
@router.get("/groupes")
|
||||||
async def liste_groupes(db: AsyncSession = Depends(get_db)):
|
async def liste_groupes(db: AsyncSession = Depends(get_db)):
|
||||||
|
await initialiser_demo(db)
|
||||||
result = await db.execute(select(Groupe).order_by(Groupe.nom))
|
result = await db.execute(select(Groupe).order_by(Groupe.nom))
|
||||||
groupes = result.scalars().all()
|
groupes = result.scalars().all()
|
||||||
return [{
|
return [{
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ async def initialiser_demo(db: AsyncSession) -> dict:
|
||||||
if existant:
|
if existant:
|
||||||
membres_existants = await _membres_par_courriel(db, existant.id)
|
membres_existants = await _membres_par_courriel(db, existant.id)
|
||||||
return {
|
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,
|
"groupe_id": existant.id,
|
||||||
"membres": len(membres_existants),
|
"membres": len(membres_existants),
|
||||||
"periode_mois": 12,
|
"periode_mois": 12,
|
||||||
|
|
@ -120,6 +120,7 @@ async def initialiser_demo(db: AsyncSession) -> dict:
|
||||||
}
|
}
|
||||||
|
|
||||||
groupe, sysadmin, postes, litterature = await _creer_base(
|
groupe, sysadmin, postes, litterature = await _creer_base(
|
||||||
|
db,
|
||||||
nom_groupe="Groupe Résilience",
|
nom_groupe="Groupe Résilience",
|
||||||
adresse_local="Salle communautaire Saint-Louis, Montréal",
|
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(
|
async def _creer_base(
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
nom_groupe: str = "Groupe Méditation",
|
nom_groupe: str = "Groupe Méditation",
|
||||||
|
|
@ -157,7 +238,11 @@ async def _creer_base(
|
||||||
)
|
)
|
||||||
db.add(groupe)
|
db.add(groupe)
|
||||||
await db.flush()
|
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 = {}
|
postes = {}
|
||||||
for nom, categorie, description, prerequis, ordre, modules in POSTES:
|
for nom, categorie, description, prerequis, ordre, modules in POSTES:
|
||||||
poste = Poste(
|
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))
|
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)]:
|
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)))
|
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:
|
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)
|
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(
|
async def _trace(
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
groupe: Groupe,
|
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.database import engine, Base, async_session
|
||||||
from app.core.security import hash_pin
|
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
|
# Importer TOUS les modèles pour que create_all crée toutes les tables
|
||||||
from app.models.groupe import Groupe
|
from app.models.groupe import Groupe
|
||||||
from app.models.membre import Membre
|
from app.models.membre import Membre
|
||||||
|
|
@ -200,6 +201,9 @@ async def seed():
|
||||||
))
|
))
|
||||||
print(" ✓ Destinataire de contribution : District 87-16")
|
print(" ✓ Destinataire de contribution : District 87-16")
|
||||||
|
|
||||||
|
demo = await initialiser_demo(db)
|
||||||
|
print(f" ✓ Groupe de démonstration : {demo['message']}")
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
print(f"\n═══ Initialisation terminée ═══")
|
print(f"\n═══ Initialisation terminée ═══")
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ export default function GestionGroupes() {
|
||||||
const { loadGroupes, logout } = useStore();
|
const { loadGroupes, logout } = useStore();
|
||||||
const [groupes, setGroupes] = useState([]);
|
const [groupes, setGroupes] = useState([]);
|
||||||
const [ouvertId, setOuvertId] = useState('');
|
const [ouvertId, setOuvertId] = useState('');
|
||||||
|
const [editionId, setEditionId] = useState('');
|
||||||
|
const [form, setForm] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [msg, setMsg] = useState('');
|
const [msg, setMsg] = useState('');
|
||||||
|
|
@ -33,15 +35,86 @@ export default function GestionGroupes() {
|
||||||
|
|
||||||
useEffect(() => { charger(); }, []);
|
useEffect(() => { charger(); }, []);
|
||||||
|
|
||||||
const installerDemo = async () => {
|
const resetDemo = 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 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;
|
if (confirmation !== 'CONFIRMER') return;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError('');
|
setError('');
|
||||||
setMsg('');
|
setMsg('');
|
||||||
try {
|
try {
|
||||||
const res = await api.post('/admin/demo');
|
const res = await api.post('/admin/demo/reset');
|
||||||
setMsg(res.message || 'Groupe de démonstration installé');
|
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 charger();
|
||||||
await loadGroupes();
|
await loadGroupes();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -76,11 +149,11 @@ export default function GestionGroupes() {
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-2">
|
<div className="grid grid-cols-1 gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={installerDemo}
|
onClick={nouveauGroupe}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
className="w-full min-h-[48px] p-3 bg-aa-blue text-white rounded-xl text-sm font-bold disabled:opacity-40"
|
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>
|
||||||
<button
|
<button
|
||||||
onClick={remettreAZero}
|
onClick={remettreAZero}
|
||||||
|
|
@ -91,6 +164,34 @@ export default function GestionGroupes() {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>}
|
{loading && <p className="text-center text-gray-400 py-8">Chargement...</p>}
|
||||||
|
|
||||||
<div className="space-y-3">
|
<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"
|
className="flex w-full items-center justify-between gap-3 p-4 text-left active:bg-gray-50"
|
||||||
aria-expanded={ouvert}
|
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="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 className="mt-1 block text-sm font-semibold text-aa-blue">{horaire}</span>
|
||||||
</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>
|
<span className={`shrink-0 text-2xl text-aa-blue transition ${ouvert ? 'rotate-180' : ''}`}>⌄</span>
|
||||||
</button>
|
</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">District : </span>{groupe.district || '—'}</p>
|
||||||
<p><span className="font-semibold text-gray-900">Région : </span>{groupe.region || '—'}</p>
|
<p><span className="font-semibold text-gray-900">Région : </span>{groupe.region || '—'}</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useStore } from '../stores/store';
|
import { useStore } from '../stores/store';
|
||||||
|
import { api } from '../stores/api';
|
||||||
|
|
||||||
const jourLabel = (jour) => {
|
const jourLabel = (jour) => {
|
||||||
if (!jour) return 'Jour à préciser';
|
if (!jour) return 'Jour à préciser';
|
||||||
|
|
@ -7,7 +8,7 @@ const jourLabel = (jour) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Login() {
|
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 [courriel, setCourriel] = useState(localStorage.getItem('gm_courriel') || '');
|
||||||
const [pin, setPin] = useState('');
|
const [pin, setPin] = useState('');
|
||||||
const [groupeId, setGroupeId] = useState(localStorage.getItem('gm_groupe_id') || '');
|
const [groupeId, setGroupeId] = useState(localStorage.getItem('gm_groupe_id') || '');
|
||||||
|
|
@ -47,6 +48,25 @@ export default function Login() {
|
||||||
} catch {}
|
} 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 (
|
return (
|
||||||
<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">
|
||||||
|
|
@ -158,6 +178,13 @@ export default function Login() {
|
||||||
})}
|
})}
|
||||||
</div>
|
</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>
|
<p className="text-center text-xs text-blue-300">100% logiciel libre · Hébergé au Québec</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue