diff --git a/backend/app/routers/accueil.py b/backend/app/routers/accueil.py index f99d105..26379e4 100644 --- a/backend/app/routers/accueil.py +++ b/backend/app/routers/accueil.py @@ -53,11 +53,11 @@ MODULES_SAISIE = { "Animateur(trice)": [], "Responsable littérature": [ ModuleTuile(code="S3", nom="Vente littérature", description="Enregistrer une vente", icone="📖", couleur="purple"), - ModuleTuile(code="M5", nom="Ajuster inventaire", description="Mettre à jour le stock", icone="📦", couleur="orange"), + ModuleTuile(code="M5", nom="Gestion des inventaires", description="Littérature et jetons", icone="📦", couleur="orange"), ], "Responsable jetons": [ ModuleTuile(code="S2", nom="Remise jeton", description="Enregistrer un jeton", icone="🏅", couleur="yellow"), - ModuleTuile(code="M6", nom="Commander jetons", description="Commander à l'Intergroupe", icone="📦", couleur="orange"), + ModuleTuile(code="M5", nom="Gestion des inventaires", description="Littérature et jetons", icone="📦", couleur="orange"), ], } diff --git a/backend/app/services/admin_database.py b/backend/app/services/admin_database.py index a38c811..8d2653f 100644 --- a/backend/app/services/admin_database.py +++ b/backend/app/services/admin_database.py @@ -19,7 +19,9 @@ from app.models.destinataire_contribution import DestinataireContribution from app.models.envoi_contribution import EnvoiContribution from app.models.evenement import Evenement from app.models.groupe import Groupe +from app.models.historique_modification import HistoriqueModification from app.models.jeton import Jeton +from app.models.journal_action import JournalAction from app.models.litterature import Litterature from app.models.membre import Membre from app.models.mouvement_reserve import MouvementReserve @@ -33,6 +35,7 @@ from app.models.rapport_adopte import RapportAdopte from app.models.rapport_rsg import RapportRsg from app.models.reserve import Reserve from app.models.reunion import Reunion +from app.models.rotation import Rotation from app.models.transaction_comptable import TransactionComptable from app.models.vente_litterature import VenteLitterature from app.models.vente_jeton import VenteJeton @@ -46,8 +49,8 @@ POSTES = [ ("Animateur(trice)", "service", "Anime les rencontres hebdomadaires.", "Disponibilité régulière", 5, ["S0"]), ("Responsable accueil", "physique", "Accueille les nouveaux et veille à l'atmosphère de la salle.", "Fiabilité", 6, ["C9"]), ("Responsable café", "physique", "Prépare le café et remet les petites dépenses au trésorier.", "Aucun", 7, ["S5"]), - ("Responsable littérature", "service", "Maintient l'inventaire et remet les ventes au trésorier.", "Connaissance de la littérature", 8, ["S3", "C4", "M5"]), - ("Responsable jetons", "service", "Prépare les jetons et les anniversaires.", "Aucun", 9, ["S2", "M6"]), + ("Responsable littérature", "service", "Maintient l'inventaire et remet les ventes au trésorier.", "Connaissance de la littérature", 8, ["S3", "C4", "M5", "M6"]), + ("Responsable jetons", "service", "Prépare les jetons et les anniversaires.", "Aucun", 9, ["S2", "M5", "M6"]), ("Responsable clés", "physique", "Ouvre et ferme le local.", "Fiabilité", 10, []), ] @@ -119,15 +122,17 @@ async def initialiser_demo(db: AsyncSession) -> dict: "groupe_id": groupe.id, "membres": len(membres), "periode_mois": 12, + "rencontres": 52, + "assemblees": 12, } async def _creer_base(db: AsyncSession): groupe = Groupe( - nom="Groupe Méditation", + nom="Groupe Résilience", district="87-16", region="87", - adresse_local="Salle communautaire, Montréal", + adresse_local="Salle communautaire Saint-Louis, Montréal", jour_reunion="mardi", heure_reunion=time(19, 30), assemblee_affaires_ordre="premiere", @@ -242,6 +247,7 @@ async def _ajouter_historique_demo(db: AsyncSession, groupe: Groupe, membres: di banque = 0.0 encaisse = 0.0 compteurs = {"RES": 0, "NOM": 0, "DON": 0} + rotations_postes = ["Animateur(trice)", "Responsable accueil", "Responsable café", "Responsable clés"] for index, jour in enumerate(dates_reunions): type_reunion = "affaires" if jour.month not in mois_vus else "reguliere" @@ -254,12 +260,52 @@ async def _ajouter_historique_demo(db: AsyncSession, groupe: Groupe, membres: di theme=["Acceptation", "Service", "Gratitude", "Honnêteté"][index % 4], annonces={"district": index % 5 == 0, "anniversaire": index % 8 == 0}, note="Réunion de démonstration réaliste.", + cree_le=_moment(jour, 18, 30), + modifie_le=_moment(jour, 18, 30), ) db.add(reunion) await db.flush() + await _trace( + db, groupe, secretaire, jour, "reunions", "reunion.planifiee", + f"{'Assemblée d affaires' if type_reunion == 'affaires' else 'Rencontre hebdomadaire'} planifiée.", + reunion.id, + apres={"date": jour.isoformat(), "type": type_reunion}, + chemin="/reunions" if type_reunion == "reguliere" else "/presences/reunion", + ) + + for decalage, poste_nom in enumerate(rotations_postes): + poste = postes.get(poste_nom) + if not poste: + continue + serviteur = membres_actifs[(index + decalage) % len(membres_actifs)] + rotation = Rotation( + groupe_id=groupe.id, + date_reunion=jour, + poste_id=poste.id, + membre_id=serviteur.id, + note="Rotation planifiée automatiquement pour la démonstration.", + cree_par=secretaire.id, + cree_le=_moment(jour, 18, 35), + ) + db.add(rotation) + await db.flush() + await _trace( + db, groupe, secretaire, jour, "rotations", "rotations.planifiees", + "Rotations hebdomadaires planifiées.", + reunion.id, + apres={"postes": rotations_postes}, + chemin="/rotations/batch", + ) for membre in membres_actifs: db.add(Presence(reunion_id=reunion.id, membre_id=membre.id, present=(index + len(membre.prenom)) % 7 != 0)) + await _trace( + db, groupe, secretaire, jour, "presences", "presences.enregistrees", + "Présences de la rencontre enregistrées.", + reunion.id, + apres={"presents": sum(1 for m in membres_actifs if (index + len(m.prenom)) % 7 != 0)}, + chemin="/presences/", + ) montant_collecte = round(82 + (index % 6) * 9 + (index % 3) * 4, 2) collecte = Collecte( @@ -274,16 +320,32 @@ async def _ajouter_historique_demo(db: AsyncSession, groupe: Groupe, membres: di date_confirmation=datetime.combine(jour, time(21, 15)).replace(tzinfo=timezone.utc), recu_par=tresorier.id, date_reception=jour, + cree_le=_moment(jour, 20, 55), + modifie_le=_moment(jour, 21, 5), ) db.add(collecte) await db.flush() encaisse += montant_collecte db.add(TransactionComptable(groupe_id=groupe.id, date_transaction=jour, type="collecte", compte="encaisse", montant=montant_collecte, sens="debit", source_table="collectes", source_id=collecte.id, note="Collecte 7e Tradition", cree_par=tresorier.id)) + await _trace( + db, groupe, tresorier, jour, "collectes", "collecte.recue", + "Collecte saisie puis reçue par le trésorier.", + collecte.id, + apres={"montant": montant_collecte, "statut": "recu"}, + chemin="/collectes", + ) if index % 4 == 0: don = 40 + (index % 5) * 10 banque += don db.add(TransactionComptable(groupe_id=groupe.id, date_transaction=jour, type="don", compte="banque", montant=don, sens="debit", note="Don direct au compte bancaire", cree_par=tresorier.id)) + await _trace( + db, groupe, tresorier, jour, "transactions_comptables", "don.direct", + "Don reçu directement au compte bancaire.", + reunion.id, + apres={"montant": don, "compte": "banque"}, + chemin="/operations-bancaires/don", + ) if index % 4 == 3: depot = round(encaisse - 25, 2) if encaisse > 50 else round(encaisse, 2) @@ -293,6 +355,14 @@ async def _ajouter_historique_demo(db: AsyncSession, groupe: Groupe, membres: di db.add(OperationBancaire(groupe_id=groupe.id, type="depot", montant=depot, date_operation=jour, effectue_par=tresorier.id, note="Dépôt mensuel de l'encaisse")) db.add(TransactionComptable(groupe_id=groupe.id, date_transaction=jour, type="banque_depot", compte="banque", montant=depot, sens="debit", note="Dépôt de l'encaisse", cree_par=tresorier.id)) db.add(TransactionComptable(groupe_id=groupe.id, date_transaction=jour, type="banque_depot", compte="encaisse", montant=depot, sens="credit", note="Dépôt de l'encaisse", cree_par=tresorier.id)) + await _trace( + db, groupe, tresorier, jour, "operations_bancaires", "encaisse.deposee", + "Dépôt de l'encaisse au compte bancaire.", + collecte.id, + avant={"encaisse": round(encaisse + depot, 2), "banque": round(banque - depot, 2)}, + apres={"encaisse": round(encaisse, 2), "banque": round(banque, 2)}, + chemin="/operations-bancaires", + ) if type_reunion == "affaires": await _decision(db, groupe, reunion, compteurs, "RES", jour, secretaire, rsg, "Adopter l'ordre du jour", "L'assemblée doit traiter les affaires courantes.", "L'ordre du jour est adopté tel que présenté.") @@ -314,7 +384,22 @@ async def _ajouter_historique_demo(db: AsyncSession, groupe: Groupe, membres: di mois=jour.month, contenu_html=f"
Rapport adopté pour {jour.strftime('%Y-%m')}.
Banque: {banque:.2f} $
Encaisse: {encaisse:.2f} $
", donnees={"banque": round(banque, 2), "encaisse": round(encaisse, 2)}, + adopte_le=_moment(jour, 21, 35), )) + await _trace( + db, groupe, secretaire, jour, "pv_reunions", "pv.redige", + "Procès-verbal d'assemblée rédigé et classé.", + pv.id, + apres={"approuve": pv.approuve}, + chemin="/pv/", + ) + await _trace( + db, groupe, tresorier, jour, "rapports_adoptes", "rapport_tresorier.adopte", + "Rapport du trésorier adopté par la conscience de groupe.", + reunion.id, + apres={"annee": jour.year, "mois": jour.month, "banque": round(banque, 2), "encaisse": round(encaisse, 2)}, + chemin="/rapports/etat-resultats/adopter", + ) if index % 6 == 1: montant = 32 + (index % 4) * 6 @@ -329,6 +414,13 @@ async def _ajouter_historique_demo(db: AsyncSession, groupe: Groupe, membres: di db.add(depense) await db.flush() db.add(TransactionComptable(groupe_id=groupe.id, date_transaction=jour, type="depense", compte=compte, montant=montant, sens="credit", source_table="depenses", source_id=depense.id, note=depense.quoi, cree_par=tresorier.id)) + await _trace( + db, groupe, tresorier, jour, "depenses", "depense.confirmee", + "Dépense soumise puis confirmée par le trésorier.", + depense.id, + apres={"montant": montant, "methode": methode, "quoi": depense.quoi}, + chemin="/depenses", + ) if index % 8 == 2 and litterature: item = litterature[index % len(litterature)] @@ -339,6 +431,13 @@ async def _ajouter_historique_demo(db: AsyncSession, groupe: Groupe, membres: di await db.flush() encaisse += montant db.add(TransactionComptable(groupe_id=groupe.id, date_transaction=jour, type="vente_litterature", compte="encaisse", montant=montant, sens="debit", source_table="ventes_litterature", source_id=vente.id, note="Vente de littérature", cree_par=tresorier.id)) + await _trace( + db, groupe, membres["karim.demo@example.test"], jour, "ventes_litterature", "vente_litterature.recue", + "Vente de littérature enregistrée et remise au trésorier.", + vente.id, + apres={"titre": item.titre, "quantite": 2, "montant": montant}, + chemin="/ventes-litterature", + ) if index % 12 == 6: montant = 6 @@ -358,6 +457,13 @@ async def _ajouter_historique_demo(db: AsyncSession, groupe: Groupe, membres: di await db.flush() encaisse += montant db.add(TransactionComptable(groupe_id=groupe.id, date_transaction=jour, type="vente_jeton", compte="encaisse", montant=montant, sens="debit", source_table="ventes_jetons", source_id=vente_jeton.id, note="Vente de jetons", cree_par=tresorier.id)) + await _trace( + db, groupe, membres["melanie.demo@example.test"], jour, "ventes_jetons", "vente_jeton.recue", + "Vente de jetons enregistrée et remise au trésorier.", + vente_jeton.id, + apres={"type_jeton": vente_jeton.type_jeton, "quantite": 2, "montant": montant}, + chemin="/jetons/ventes", + ) if index % 13 == 4: montant = 125 @@ -368,6 +474,13 @@ async def _ajouter_historique_demo(db: AsyncSession, groupe: Groupe, membres: di db.add(contribution) await db.flush() db.add(TransactionComptable(groupe_id=groupe.id, date_transaction=jour, type="contribution", compte="banque", montant=montant, sens="credit", source_table="envois_contributions", source_id=contribution.id, note="Contribution au district", cree_par=tresorier.id)) + await _trace( + db, groupe, tresorier, jour, "envois_contributions", "contribution.debitee", + "Contribution décidée, engagée et débitée.", + contribution.id, + apres={"destination": contribution.destination, "montant": montant}, + chemin="/contributions", + ) if index in {9, 22, 35}: candidat = membres_actifs[index % len(membres_actifs)] @@ -378,9 +491,26 @@ async def _ajouter_historique_demo(db: AsyncSession, groupe: Groupe, membres: di decision = await _decision(db, groupe, reunion, compteurs, "NOM", jour, rsg, secretaire, f"Nomination de {candidat.prenom}", "Un poste doit être pourvu pour soutenir le groupe.", f"Le groupe nomme {candidat.prenom} au poste de {poste.nom}.") decision.candidature_id = candidature.id candidature.proposition_id = decision.id + db.add(Affectation(groupe_id=groupe.id, membre_id=candidat.id, poste_id=poste.id, date_debut=jour, statut="actif")) + await _trace( + db, groupe, rsg, jour, "candidatures", "nomination.adoptee", + "Nomination proposée, secondée et adoptée.", + candidature.id, + apres={"membre": candidat.prenom, "poste": poste.nom, "resolution": decision.numero_resolution}, + chemin="/postes/candidatures", + ) if index % 10 == 5: - db.add(Jeton(groupe_id=groupe.id, reunion_id=reunion.id, membre_id=membres_actifs[index % len(membres_actifs)].id, type_jeton="multiple", date_remise=jour, annees=1 + index % 5, remis_par=membres["melanie.demo@example.test"].id)) + jeton = Jeton(groupe_id=groupe.id, reunion_id=reunion.id, membre_id=membres_actifs[index % len(membres_actifs)].id, type_jeton="multiple", date_remise=jour, annees=1 + index % 5, remis_par=membres["melanie.demo@example.test"].id, cree_le=_moment(jour, 20, 5)) + db.add(jeton) + await db.flush() + await _trace( + db, groupe, membres["melanie.demo@example.test"], jour, "jetons", "jeton.remis", + "Remise de jeton enregistrée pour statistiques.", + jeton.id, + apres={"annees": jeton.annees}, + chemin="/jetons", + ) if index % 17 == 7: reserve = reserves[["Loyer", "Littérature", "Prudentielle"][index % 3]] @@ -388,6 +518,14 @@ async def _ajouter_historique_demo(db: AsyncSession, groupe: Groupe, membres: di if banque >= montant: reserve.montant = float(reserve.montant or 0) + montant db.add(MouvementReserve(groupe_id=groupe.id, source_id=None, destination_id=reserve.id, montant=montant, type="reserve_virement", note="Affectation au solde réservé", cree_par=tresorier.id, cree_le=datetime.combine(jour, time(21, 0)).replace(tzinfo=timezone.utc))) + await _trace( + db, groupe, tresorier, jour, "mouvements_reserves", "reserve.transfert", + "Montant transféré du disponible vers une réserve.", + reserve.id, + avant={"reserve": round(float(reserve.montant or 0) - montant, 2)}, + apres={"reserve": round(float(reserve.montant or 0), 2), "montant": montant}, + chemin="/operations-bancaires/reserves/transfert", + ) db.add(Collecte(groupe_id=groupe.id, date_collecte=dates_reunions[-1] + timedelta(days=7), montant=96, detenteur_id=membres["luc.demo@example.test"].id, statut="en_attente", saisi_par=membres["luc.demo@example.test"].id)) db.add(Depense(groupe_id=groupe.id, date_depense=date.today(), montant=48, qui=membres["anne.demo@example.test"].id, quoi="Fournitures à confirmer", paye=False, methode=None, saisi_par=membres["anne.demo@example.test"].id)) @@ -414,12 +552,65 @@ async def _decision(db, groupe, reunion, compteurs, prefixe, jour, proposeur, se date_adoption=jour, etant_donne_que=attendu, groupe_a_decide_de=decide, + cree_le=_moment(jour, 20, 30), + modifie_le=_moment(jour, 20, 45), ) db.add(proposition) await db.flush() + await _trace( + db, groupe, proposeur, jour, "propositions", f"decision.{prefixe.lower()}", + "Décision adoptée par la conscience de groupe.", + proposition.id, + apres={"numero": numero, "sujet": sujet, "decision": decide}, + chemin="/propositions/voter", + ) return proposition +def _moment(jour: date, heure: int = 20, minute: int = 0) -> datetime: + return datetime.combine(jour, time(heure, minute)).replace(tzinfo=timezone.utc) + + +async def _trace( + db: AsyncSession, + groupe: Groupe, + membre: Membre, + jour: date, + table: str, + action: str, + raison: str, + objet_id=None, + avant: dict | None = None, + apres: dict | None = None, + chemin: str | None = None, +): + moment = _moment(jour, 21, 50) + cible = objet_id or groupe.id + db.add(HistoriqueModification( + groupe_id=groupe.id, + table_cible=table, + objet_id=cible, + action=action, + raison=raison, + membre_id=membre.id if membre else None, + chemin_api=chemin, + avant=avant, + apres=apres, + cree_le=moment, + )) + db.add(JournalAction( + groupe_id=groupe.id, + membre_id=membre.id if membre else None, + methode="POST", + chemin=chemin or f"/demo/{table}", + statut_http=200, + adresse_ip="127.0.0.1", + user_agent="DemoSeeder/1.0", + details={"action": action, "raison": raison}, + cree_le=moment, + )) + + def _premier_mardi(jour: date) -> date: delta = (1 - jour.weekday()) % 7 return jour + timedelta(days=delta) diff --git a/deploy/seed.py b/deploy/seed.py index 4fccb64..e036d18 100644 --- a/deploy/seed.py +++ b/deploy/seed.py @@ -1,5 +1,5 @@ """ -Script d'initialisation — Groupe Méditation +Script d'initialisation — Groupe Résilience Crée les postes, le groupe initial et le premier membre. Usage: cd /opt/groupe-meditation/backend && venv/bin/python ../deploy/seed.py """ @@ -92,14 +92,14 @@ async def seed(): async with async_session() as db: # ── Créer le groupe ── - print("\n═══ Initialisation du Groupe Méditation ═══\n") + print("\n═══ Initialisation du Groupe Résilience ═══\n") groupe = Groupe( - nom="Groupe Méditation", + nom="Groupe Résilience", district="87-16", region="87", - adresse_local="", + adresse_local="Salle communautaire Saint-Louis, Montréal", jour_reunion="mardi", - heure_reunion=None, + heure_reunion=datetime.strptime("19:30", "%H:%M").time(), format_reunion="Discussion ouverte", lien_aa87="https://aa87.org", ) diff --git a/frontend/src/pages/Accueil.jsx b/frontend/src/pages/Accueil.jsx index 1233d24..e13ab9a 100644 --- a/frontend/src/pages/Accueil.jsx +++ b/frontend/src/pages/Accueil.jsx @@ -14,6 +14,13 @@ const COULEURS = { 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 MODULES_CONTEXTE = new Set(['S0', 'M0', 'M1', 'M2']); +const MODULE_INVENTAIRES = { + code: 'M5', + nom: 'Gestion des inventaires', + description: 'Littérature et jetons', + icone: '📦', + couleur: 'orange', +}; const CONTEXTES = { accueil: { @@ -41,7 +48,7 @@ const CONTEXTES = { sections: [ { titre: 'Finances', codes: ['S1T', 'S5', 'M4'] }, { titre: 'Membres et structure', codes: ['G2', 'G11', 'G12', 'G9', 'G13'] }, - { titre: 'Inventaire', codes: ['M5', 'M6'] }, + { titre: 'Inventaire', codes: ['M5'] }, ], }, }; @@ -96,6 +103,7 @@ export default function Accueil() { const moduleParCode = useMemo(() => { const map = new Map(); tousModules.forEach((m) => map.set(m.code, m)); + if (map.has('M5') || map.has('M6')) map.set('M5', MODULE_INVENTAIRES); return map; }, [tousModules]); diff --git a/frontend/src/pages/AjusterInventaire.jsx b/frontend/src/pages/AjusterInventaire.jsx index f3840bb..e2fcc62 100644 --- a/frontend/src/pages/AjusterInventaire.jsx +++ b/frontend/src/pages/AjusterInventaire.jsx @@ -12,6 +12,7 @@ const CATEGORIES = [ export default function AjusterInventaire() { const [items, setItems] = useState([]); + const [jetons, setJetons] = useState([]); const [loading, setLoading] = useState(true); const [success, setSuccess] = useState(''); const [error, setError] = useState(''); @@ -19,9 +20,16 @@ export default function AjusterInventaire() { const charger = async () => { try { - const data = await api.get('/litterature/'); + const [data, stockJetons] = await Promise.all([ + api.get('/litterature/'), + api.get('/commandes-jetons/stock'), + ]); setItems(data); - } catch { setItems([]); } + setJetons(stockJetons); + } catch { + setItems([]); + setJetons([]); + } setLoading(false); }; @@ -43,9 +51,16 @@ export default function AjusterInventaire() { return (+ Jetons à commander ({aCommander.length}) +
++ {s.label} — {s.remis_dernier_mois} remis ce mois +
+ ))} +{s.label.split(' ').slice(1).join(' ')}
+{s.remis_dernier_mois} remis ou vendu récemment
+Aucune donnée de jetons pour le moment
+