From 0eb3ac05a55b215196a5b09622a1fa1c4de9439c Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Mon, 1 Jun 2026 14:42:37 -0400 Subject: [PATCH] Ajouter section services RSG --- backend/app/main.py | 5 + backend/app/models/rapport_rsg.py | 5 + backend/app/routers/accueil.py | 4 +- backend/app/routers/rapports_rsg.py | 18 +- backend/app/schemas/phase4.py | 14 +- docs/10_MANUEL_UTILISATEUR.md | 44 +++-- docs/50_MODELE_DONNEES.md | 3 +- frontend/src/pages/Accueil.jsx | 3 +- frontend/src/pages/GererPostes.jsx | 2 +- frontend/src/pages/RapportRSG.jsx | 247 +++++++++++++++++----------- 10 files changed, 220 insertions(+), 125 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index 4893253..e6b04f3 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -61,6 +61,11 @@ async def lifespan(app: FastAPI): await conn.execute(text("ALTER TABLE ventes_litterature ADD COLUMN IF NOT EXISTS reunion_id UUID REFERENCES reunions(id)")) await conn.execute(text("ALTER TABLE invitations ADD COLUMN IF NOT EXISTS cible_type VARCHAR(40)")) await conn.execute(text("ALTER TABLE invitations ADD COLUMN IF NOT EXISTS cible_id UUID")) + await conn.execute(text("ALTER TABLE rapports_rsg ADD COLUMN IF NOT EXISTS infos_district TEXT")) + await conn.execute(text("ALTER TABLE rapports_rsg ADD COLUMN IF NOT EXISTS positions_groupe TEXT")) + await conn.execute(text("ALTER TABLE rapports_rsg ADD COLUMN IF NOT EXISTS suivis TEXT")) + await conn.execute(text("ALTER TABLE rapports_rsg ADD COLUMN IF NOT EXISTS prochaine_echeance TEXT")) + await conn.execute(text("ALTER TABLE rapports_rsg ADD COLUMN IF NOT EXISTS liens_utiles TEXT")) for table in ( "propositions", "depenses", diff --git a/backend/app/models/rapport_rsg.py b/backend/app/models/rapport_rsg.py index 4961370..fff6b81 100644 --- a/backend/app/models/rapport_rsg.py +++ b/backend/app/models/rapport_rsg.py @@ -14,6 +14,11 @@ class RapportRsg(Base): reunion_dist: Mapped[date] = mapped_column(Date, nullable=False) redacteur_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("membres.id"), nullable=False) decisions: Mapped[list | None] = mapped_column(JSON) + infos_district: Mapped[str | None] = mapped_column(Text) infos_region: Mapped[str | None] = mapped_column(Text) sujets_groupe: Mapped[str | None] = mapped_column(Text) + positions_groupe: Mapped[str | None] = mapped_column(Text) + suivis: Mapped[str | None] = mapped_column(Text) + prochaine_echeance: Mapped[str | None] = mapped_column(Text) + liens_utiles: Mapped[str | None] = mapped_column(Text) cree_le: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) diff --git a/backend/app/routers/accueil.py b/backend/app/routers/accueil.py index 319cb28..7a25da7 100644 --- a/backend/app/routers/accueil.py +++ b/backend/app/routers/accueil.py @@ -46,10 +46,10 @@ MODULES_SAISIE = { ModuleTuile(code="M2", nom="Présences", description="Réunion d'affaires", icone="✅", couleur="green"), ], "RSG": [ - ModuleTuile(code="M3", nom="Rapport RSG", description="Compte-rendu district", icone="📋", couleur="blue"), + ModuleTuile(code="M3", nom="Services", description="District, région, sujets et suivis", icone="📋", couleur="blue"), ], "RSG Substitut(e)": [ - ModuleTuile(code="M3", nom="Rapport RSG", description="Compte-rendu district", icone="📋", couleur="blue"), + ModuleTuile(code="M3", nom="Services", description="District, région, sujets et suivis", icone="📋", couleur="blue"), ], "Animateur(trice)": [], "Responsable littérature": [ diff --git a/backend/app/routers/rapports_rsg.py b/backend/app/routers/rapports_rsg.py index 411e180..aa10e41 100644 --- a/backend/app/routers/rapports_rsg.py +++ b/backend/app/routers/rapports_rsg.py @@ -1,4 +1,4 @@ -"""Routes M3 — Rapport du RSG.""" +"""Routes M3 — Services généraux.""" from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, desc @@ -11,7 +11,7 @@ from app.models.affectation import Affectation from app.models.poste import Poste from app.schemas.phase4 import RapportRsgCreate, RapportRsgOut -router = APIRouter(prefix="/rapports-rsg", tags=["M3 — Rapport RSG"]) +router = APIRouter(prefix="/rapports-rsg", tags=["M3 — Services"]) async def require_rsg(membre=Depends(get_current_membre), db=Depends(get_db)): @@ -39,8 +39,13 @@ async def _to_out(r: RapportRsg, db: AsyncSession) -> RapportRsgOut: reunion_dist=r.reunion_dist, redacteur_prenom=redacteur.prenom if redacteur else "?", decisions=r.decisions, + infos_district=r.infos_district, infos_region=r.infos_region, sujets_groupe=r.sujets_groupe, + positions_groupe=r.positions_groupe, + suivis=r.suivis, + prochaine_echeance=r.prochaine_echeance, + liens_utiles=r.liens_utiles, cree_le=r.cree_le, ) @@ -51,7 +56,7 @@ async def liste_rapports_rsg( membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db), ): - """Lister les rapports RSG du groupe.""" + """Lister les renseignements de service du groupe.""" result = await db.execute( select(RapportRsg) .where(RapportRsg.groupe_id == membre.groupe_id) @@ -67,14 +72,19 @@ async def creer_rapport_rsg( membre=Depends(require_rsg), db: AsyncSession = Depends(get_db), ): - """M3 — Rédiger un rapport de réunion de district.""" + """M3 — Renseigner les services généraux.""" rapport = RapportRsg( groupe_id=membre.groupe_id, reunion_dist=req.reunion_dist, redacteur_id=membre.id, decisions=req.decisions, + infos_district=req.infos_district, infos_region=req.infos_region, sujets_groupe=req.sujets_groupe, + positions_groupe=req.positions_groupe, + suivis=req.suivis, + prochaine_echeance=req.prochaine_echeance, + liens_utiles=req.liens_utiles, ) db.add(rapport) await db.flush() diff --git a/backend/app/schemas/phase4.py b/backend/app/schemas/phase4.py index e351bf8..2479ca8 100644 --- a/backend/app/schemas/phase4.py +++ b/backend/app/schemas/phase4.py @@ -1,4 +1,4 @@ -"""Schémas Phase 4 — PV, Présences, Rapport RSG.""" +"""Schémas Phase 4 — PV, Présences, Services.""" from pydantic import BaseModel from datetime import date, datetime from uuid import UUID @@ -79,12 +79,17 @@ class PvAdoption(BaseModel): contenu: Optional[str] = None -# ── M3 Rapport RSG ── +# ── M3 Services ── class RapportRsgCreate(BaseModel): reunion_dist: date decisions: Optional[list[str]] = None + infos_district: Optional[str] = None infos_region: Optional[str] = None sujets_groupe: Optional[str] = None + positions_groupe: Optional[str] = None + suivis: Optional[str] = None + prochaine_echeance: Optional[str] = None + liens_utiles: Optional[str] = None class RapportRsgOut(BaseModel): @@ -92,8 +97,13 @@ class RapportRsgOut(BaseModel): reunion_dist: date redacteur_prenom: str decisions: Optional[list[str]] = None + infos_district: Optional[str] = None infos_region: Optional[str] = None sujets_groupe: Optional[str] = None + positions_groupe: Optional[str] = None + suivis: Optional[str] = None + prochaine_echeance: Optional[str] = None + liens_utiles: Optional[str] = None cree_le: datetime model_config = {"from_attributes": True} diff --git a/docs/10_MANUEL_UTILISATEUR.md b/docs/10_MANUEL_UTILISATEUR.md index 5eeeed0..56cf80c 100644 --- a/docs/10_MANUEL_UTILISATEUR.md +++ b/docs/10_MANUEL_UTILISATEUR.md @@ -132,7 +132,24 @@ La page de l'assemblée sert d'ordre du jour. Le rapport PDF sert de procès-ver Dans une assemblée, ouvrir `Présences`, cocher les membres présents et enregistrer. Les présences alimentent le rapport PDF. -## 11. Procès-verbal et rapport du trésorier +## 11. Services + +Le module `Services` est destiné au RSG, au RSG substitut et aux exécutifs. Il sert à garder la mémoire des informations de service à transmettre entre le district, la région et le groupe. + +On peut y renseigner: + +- les décisions du district; +- les informations du district; +- les informations de la région; +- les sujets à rapporter au groupe; +- les positions du groupe à transmettre; +- les suivis à faire; +- les prochaines échéances; +- les liens ou références utiles. + +Les renseignements précédents restent consultables par date de réunion de district. + +## 12. Procès-verbal et rapport du trésorier L'adoption d'un procès-verbal ou d'un rapport du trésorier est faite par un exécutif avec un bouton `Adopter`. L'adoption demande un proposeur et un secondeur. @@ -140,7 +157,7 @@ Avant adoption, le rapport peut être corrigé. Après adoption, il est cristall Le rapport du trésorier doit être adopté avant de décider du montant des contributions. -## 12. Gouvernance +## 13. Gouvernance Le module `Gouvernance` rassemble les propositions, réflexions en cours, rejets et décisions. @@ -169,7 +186,7 @@ Les types de décisions sont: - `Nomination`; - `Contribution`. -## 13. Dépenses +## 14. Dépenses Un membre peut soumettre une dépense en son nom. Un exécutif peut soumettre pour un autre membre. @@ -179,7 +196,7 @@ La dépense est ensuite traitée dans `Trésorerie`: - `Chèque` ou `Virement`: crée un engagement jusqu'au débit bancaire; - `Rejeter`: efface la demande. -## 14. Trésorerie +## 15. Trésorerie Le module `Trésorerie` présente d'abord la position du groupe: @@ -203,7 +220,7 @@ Les exécutifs peuvent: La page `Résultats` présente l'état des résultats par mois. -## 15. Donner au suivant +## 16. Donner au suivant Le module `Donner au suivant` sert à préparer une contribution. @@ -217,7 +234,7 @@ Il faut choisir: La contribution crée une décision de groupe. Elle doit ensuite être approuvée et traitée par la trésorerie avant de modifier les soldes. -## 16. Postes à pourvoir +## 17. Postes à pourvoir Les postes disponibles sont affichés en tuiles. En ouvrant une tuile, le membre voit: @@ -228,7 +245,7 @@ Les postes disponibles sont affichés en tuiles. En ouvrant une tuile, le membre `Postuler` sert pour soi-même. `Proposer` permet à un exécutif de proposer un autre membre. -## 17. Gestion des membres +## 18. Gestion des membres Les exécutifs peuvent: @@ -240,7 +257,7 @@ Les exécutifs peuvent: Sysadmin peut impersonifier un membre pour aider ou vérifier un problème. -## 18. Gestion des postes et mandats +## 19. Gestion des postes et mandats `Gestion des postes` permet de définir les postes et leurs permissions. @@ -248,15 +265,15 @@ Sysadmin peut impersonifier un membre pour aider ou vérifier un problème. Une affectation produit une décision de type `Nomination` avec proposeur et secondeur. -## 19. Littérature et jetons +## 20. Littérature et jetons Les membres peuvent consulter l'inventaire de littérature. Les responsables peuvent ajuster les stocks. Les ventes diminuent les stocks; les remises statistiques de jetons ne les diminuent pas. -## 20. Événements +## 21. Événements Le module `Événements` affiche les événements passés et futurs. Les filtres peuvent être combinés. -## 21. Rapports PDF et mode papier +## 22. Rapports PDF et mode papier Les rapports PDF servent à imprimer ou conserver: @@ -268,11 +285,11 @@ Les rapports PDF servent à imprimer ou conserver: `Mode papier` soutient une reprise en main si l'informatique n'est pas disponible. -## 22. Journal +## 23. Journal Le journal affiche les changements enregistrés. Une ligne condensée montre date, heure et domaine; l'ouverture donne les détails. Il sert à comprendre ce qui a changé, pas à surveiller les membres. -## 23. Gestion des groupes +## 24. Gestion des groupes Sur la page de sélection des groupes, le bouton discret `Sysadmin` demande le PIN sysadmin. Il ouvre la gestion des groupes. @@ -285,4 +302,3 @@ Sysadmin peut: - réinitialiser le groupe de démonstration. `Groupe Démonstration` est toujours présent sur une installation vanille et ne peut pas être supprimé. - diff --git a/docs/50_MODELE_DONNEES.md b/docs/50_MODELE_DONNEES.md index 31c9d47..d7a3e4b 100644 --- a/docs/50_MODELE_DONNEES.md +++ b/docs/50_MODELE_DONNEES.md @@ -42,7 +42,7 @@ Ce document décrit les tables SQLAlchemy de l'application et leur rôle. Il ne | `reunions` | Rencontres hebdomadaires et assemblées d'affaires. | | `presences` | Présences rattachées à une réunion. | | `pv_reunions` | Procès-verbaux préparés ou adoptés. | -| `rapport_rsg` / `rapports_rsg` | Rapports RSG. | +| `rapport_rsg` / `rapports_rsg` | Renseignements de service portés par le RSG: district, région, sujets au groupe, positions transmises et suivis. | | `rapports_adoptes` | Rapports cristallisés après adoption. | ## Tables de gouvernance @@ -108,4 +108,3 @@ Le démarrage supprime les anciennes tables devenues obsolètes: - Une relation 1:1 ne justifie pas à elle seule la fusion de deux tables. Il faut aussi vérifier le cycle de vie, le niveau d'accès, l'historisation, la fréquence d'usage et la signification métier. - Les caches de montant ou de statut ne doivent pas remplacer les registres d'origine. - Toute nouvelle table métier doit documenter son rattachement à un groupe. - diff --git a/frontend/src/pages/Accueil.jsx b/frontend/src/pages/Accueil.jsx index 639f52d..6e7e84c 100644 --- a/frontend/src/pages/Accueil.jsx +++ b/frontend/src/pages/Accueil.jsx @@ -28,7 +28,7 @@ const CONTEXTES = { description: "État général et consultations courantes.", sections: [ { titre: 'À faire maintenant', codes: ['S5', 'C5', 'C9'] }, - { titre: 'État', codes: ['S1T', 'C2', 'C1', 'C4'] }, + { titre: 'État', codes: ['S1T', 'M3', 'C2', 'C1', 'C4'] }, { titre: 'Mon espace', codes: ['C7', 'C11', 'C13', 'C12'] }, ], }, @@ -48,6 +48,7 @@ const CONTEXTES = { sections: [ { titre: 'Finances', codes: ['S1T', 'S5', 'M4'] }, { titre: 'Membres et structure', codes: ['G14', 'G2', 'G11', 'G12', 'G9', 'G13'] }, + { titre: 'Services', codes: ['M3'] }, { titre: 'Inventaire', codes: ['M5'] }, ], }, diff --git a/frontend/src/pages/GererPostes.jsx b/frontend/src/pages/GererPostes.jsx index 9a3b548..85e3983 100644 --- a/frontend/src/pages/GererPostes.jsx +++ b/frontend/src/pages/GererPostes.jsx @@ -12,7 +12,7 @@ const MODULES = [ ['S0', 'Rencontres'], ['M0', "Assemblée d'affaires"], ['C5', 'Gouvernance'], ['S1T', 'Trésorerie'], ['M4', 'Donner au suivant'], - ['M1', 'Rédiger PV'], ['M2', 'Présences'], ['M3', 'Rapport RSG'], + ['M1', 'Rédiger PV'], ['M2', 'Présences'], ['M3', 'Services'], ['S2', 'Remise jeton'], ['M6', 'Commander jetons'], ['S3', 'Vente littérature'], ['M5', 'Ajuster inventaire'], ['G2', 'Gestion membres'], ['G9', 'Paramètres'], diff --git a/frontend/src/pages/RapportRSG.jsx b/frontend/src/pages/RapportRSG.jsx index b214d73..c2504a9 100644 --- a/frontend/src/pages/RapportRSG.jsx +++ b/frontend/src/pages/RapportRSG.jsx @@ -3,151 +3,200 @@ import { api } from '../stores/api'; import PageHeader from '../components/PageHeader'; import SuccessCheck from '../components/SuccessCheck'; +const champsTexte = [ + ['infos_district', 'Informations du district', 'Informations reçues au district, comités, besoins de service...'], + ['infos_region', 'Informations de la région', "Nouvelles de la région, ateliers, assemblées, documents utiles..."], + ['sujets_groupe', 'Sujets à rapporter au groupe', "Questions, consultations ou points à amener à la prochaine assemblée d'affaires..."], + ['positions_groupe', 'Positions du groupe à transmettre', 'Décisions, orientations ou messages que le RSG doit porter au district...'], + ['suivis', 'Suivis à faire', 'Actions à ne pas oublier, personnes à relancer, documents à apporter...'], + ['prochaine_echeance', 'Prochaine échéance', 'Date, rencontre ou action attendue...'], + ['liens_utiles', 'Liens ou références utiles', 'Liens, noms de documents, coordonnées ou références à conserver...'], +]; + +function formatDate(valeur) { + if (!valeur) return ''; + return new Date(`${valeur}T12:00:00`).toLocaleDateString('fr-CA', { + year: 'numeric', + month: 'long', + day: 'numeric', + }); +} + +function SectionLecture({ titre, children }) { + if (!children) return null; + return ( +
+

{titre}

+
{children}
+
+ ); +} + +function RapportDetail({ rapport, onBack }) { + return ( +
+ +
+ +
+

Renseigné par

+

{rapport.redacteur_prenom}

+
+ {rapport.decisions?.length > 0 && ( +
+

Décisions du district

+
+ {rapport.decisions.map((decision, index) => ( +

{decision}

+ ))} +
+
+ )} + {champsTexte.map(([cle, label]) => ( + {rapport[cle]} + ))} +
+
+ ); +} + export default function RapportRSG() { const [rapports, setRapports] = useState([]); const [form, setForm] = useState({ reunion_dist: new Date().toISOString().slice(0, 10), decisions_text: '', + infos_district: '', infos_region: '', sujets_groupe: '', + positions_groupe: '', + suivis: '', + prochaine_echeance: '', + liens_utiles: '', }); const [loading, setLoading] = useState(true); const [success, setSuccess] = useState(false); const [viewRapport, setViewRapport] = useState(null); const [error, setError] = useState(''); - useEffect(() => { + const chargerRapports = () => { + setLoading(true); api.get('/rapports-rsg/').then((data) => { setRapports(data); setLoading(false); }).catch(() => setLoading(false)); + }; + + useEffect(() => { + chargerRapports(); }, []); const handleSubmit = async () => { - if (!form.decisions_text.trim() && !form.infos_region.trim() && !form.sujets_groupe.trim()) { + const sectionsRemplies = champsTexte.some(([cle]) => form[cle].trim()); + if (!form.decisions_text.trim() && !sectionsRemplies) { setError('Au moins une section doit être remplie'); return; } setError(''); - try { - const decisionsList = form.decisions_text - .split('\n') - .map((d) => d.trim()) - .filter((d) => d.length > 0); + const decisionsList = form.decisions_text + .split('\n') + .map((d) => d.trim()) + .filter(Boolean); + + try { await api.post('/rapports-rsg/', { reunion_dist: form.reunion_dist, decisions: decisionsList.length > 0 ? decisionsList : null, + infos_district: form.infos_district.trim() || null, infos_region: form.infos_region.trim() || null, sujets_groupe: form.sujets_groupe.trim() || null, + positions_groupe: form.positions_groupe.trim() || null, + suivis: form.suivis.trim() || null, + prochaine_echeance: form.prochaine_echeance.trim() || null, + liens_utiles: form.liens_utiles.trim() || null, }); setSuccess(true); - } catch (e) { setError(e.message); } + } catch (e) { + setError(e.message); + } }; - if (success) return ; - - // Vue lecture - if (viewRapport) { - return ( -
-
- -
-

Rapport RSG

-

Réunion du {viewRapport.reunion_dist}

-
-
-
-
-

Rédigé par {viewRapport.redacteur_prenom}

-
- {viewRapport.decisions && viewRapport.decisions.length > 0 && ( -
-

Décisions du district

- {viewRapport.decisions.map((d, i) => ( -
-

• {d}

-
- ))} -
- )} - {viewRapport.infos_region && ( -
-

Informations de la région

-

{viewRapport.infos_region}

-
- )} - {viewRapport.sujets_groupe && ( -
-

Sujets pour le groupe

-

{viewRapport.sujets_groupe}

-
- )} -
-
- ); - } + if (success) return ; + if (viewRapport) return setViewRapport(null)} />; return (
- + -
- {/* Rapports existants */} - {rapports.length > 0 && ( -
-

Rapports récents

- {rapports.slice(0, 5).map((r) => ( - - ))} -
- )} +
+
+

Renseignements récents

+ {loading ? ( +

Chargement...

+ ) : rapports.length === 0 ? ( +

Aucun renseignement de service enregistré.

+ ) : ( +
+ {rapports.slice(0, 8).map((rapport) => ( + + ))} +
+ )} +
- {/* Nouveau rapport */} -
+
- setForm({ ...form, reunion_dist: e.target.value })} - className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue" /> -
+ className="w-full min-h-[48px] p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue" + /> + -
- -