Ajouter section services RSG
This commit is contained in:
parent
f04871c3b3
commit
0eb3ac05a5
10 changed files with 220 additions and 125 deletions
|
|
@ -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 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_type VARCHAR(40)"))
|
||||||
await conn.execute(text("ALTER TABLE invitations ADD COLUMN IF NOT EXISTS cible_id UUID"))
|
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 (
|
for table in (
|
||||||
"propositions",
|
"propositions",
|
||||||
"depenses",
|
"depenses",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,11 @@ class RapportRsg(Base):
|
||||||
reunion_dist: Mapped[date] = mapped_column(Date, nullable=False)
|
reunion_dist: Mapped[date] = mapped_column(Date, nullable=False)
|
||||||
redacteur_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("membres.id"), nullable=False)
|
redacteur_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("membres.id"), nullable=False)
|
||||||
decisions: Mapped[list | None] = mapped_column(JSON)
|
decisions: Mapped[list | None] = mapped_column(JSON)
|
||||||
|
infos_district: Mapped[str | None] = mapped_column(Text)
|
||||||
infos_region: Mapped[str | None] = mapped_column(Text)
|
infos_region: Mapped[str | None] = mapped_column(Text)
|
||||||
sujets_groupe: 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))
|
cree_le: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||||
|
|
|
||||||
|
|
@ -46,10 +46,10 @@ MODULES_SAISIE = {
|
||||||
ModuleTuile(code="M2", nom="Présences", description="Réunion d'affaires", icone="✅", couleur="green"),
|
ModuleTuile(code="M2", nom="Présences", description="Réunion d'affaires", icone="✅", couleur="green"),
|
||||||
],
|
],
|
||||||
"RSG": [
|
"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)": [
|
"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)": [],
|
"Animateur(trice)": [],
|
||||||
"Responsable littérature": [
|
"Responsable littérature": [
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
"""Routes M3 — Rapport du RSG."""
|
"""Routes M3 — Services généraux."""
|
||||||
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 select, desc
|
from sqlalchemy import select, desc
|
||||||
|
|
@ -11,7 +11,7 @@ from app.models.affectation import Affectation
|
||||||
from app.models.poste import Poste
|
from app.models.poste import Poste
|
||||||
from app.schemas.phase4 import RapportRsgCreate, RapportRsgOut
|
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)):
|
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,
|
reunion_dist=r.reunion_dist,
|
||||||
redacteur_prenom=redacteur.prenom if redacteur else "?",
|
redacteur_prenom=redacteur.prenom if redacteur else "?",
|
||||||
decisions=r.decisions,
|
decisions=r.decisions,
|
||||||
|
infos_district=r.infos_district,
|
||||||
infos_region=r.infos_region,
|
infos_region=r.infos_region,
|
||||||
sujets_groupe=r.sujets_groupe,
|
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,
|
cree_le=r.cree_le,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -51,7 +56,7 @@ async def liste_rapports_rsg(
|
||||||
membre=Depends(get_current_membre),
|
membre=Depends(get_current_membre),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Lister les rapports RSG du groupe."""
|
"""Lister les renseignements de service du groupe."""
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(RapportRsg)
|
select(RapportRsg)
|
||||||
.where(RapportRsg.groupe_id == membre.groupe_id)
|
.where(RapportRsg.groupe_id == membre.groupe_id)
|
||||||
|
|
@ -67,14 +72,19 @@ async def creer_rapport_rsg(
|
||||||
membre=Depends(require_rsg),
|
membre=Depends(require_rsg),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""M3 — Rédiger un rapport de réunion de district."""
|
"""M3 — Renseigner les services généraux."""
|
||||||
rapport = RapportRsg(
|
rapport = RapportRsg(
|
||||||
groupe_id=membre.groupe_id,
|
groupe_id=membre.groupe_id,
|
||||||
reunion_dist=req.reunion_dist,
|
reunion_dist=req.reunion_dist,
|
||||||
redacteur_id=membre.id,
|
redacteur_id=membre.id,
|
||||||
decisions=req.decisions,
|
decisions=req.decisions,
|
||||||
|
infos_district=req.infos_district,
|
||||||
infos_region=req.infos_region,
|
infos_region=req.infos_region,
|
||||||
sujets_groupe=req.sujets_groupe,
|
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)
|
db.add(rapport)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
|
|
|
||||||
|
|
@ -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 pydantic import BaseModel
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
@ -79,12 +79,17 @@ class PvAdoption(BaseModel):
|
||||||
contenu: Optional[str] = None
|
contenu: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
# ── M3 Rapport RSG ──
|
# ── M3 Services ──
|
||||||
class RapportRsgCreate(BaseModel):
|
class RapportRsgCreate(BaseModel):
|
||||||
reunion_dist: date
|
reunion_dist: date
|
||||||
decisions: Optional[list[str]] = None
|
decisions: Optional[list[str]] = None
|
||||||
|
infos_district: Optional[str] = None
|
||||||
infos_region: Optional[str] = None
|
infos_region: Optional[str] = None
|
||||||
sujets_groupe: 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):
|
class RapportRsgOut(BaseModel):
|
||||||
|
|
@ -92,8 +97,13 @@ class RapportRsgOut(BaseModel):
|
||||||
reunion_dist: date
|
reunion_dist: date
|
||||||
redacteur_prenom: str
|
redacteur_prenom: str
|
||||||
decisions: Optional[list[str]] = None
|
decisions: Optional[list[str]] = None
|
||||||
|
infos_district: Optional[str] = None
|
||||||
infos_region: Optional[str] = None
|
infos_region: Optional[str] = None
|
||||||
sujets_groupe: 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
|
cree_le: datetime
|
||||||
|
|
||||||
model_config = {"from_attributes": True}
|
model_config = {"from_attributes": True}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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.
|
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.
|
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.
|
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`;
|
- `Nomination`;
|
||||||
- `Contribution`.
|
- `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.
|
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;
|
- `Chèque` ou `Virement`: crée un engagement jusqu'au débit bancaire;
|
||||||
- `Rejeter`: efface la demande.
|
- `Rejeter`: efface la demande.
|
||||||
|
|
||||||
## 14. Trésorerie
|
## 15. Trésorerie
|
||||||
|
|
||||||
Le module `Trésorerie` présente d'abord la position du groupe:
|
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.
|
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.
|
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.
|
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:
|
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.
|
`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:
|
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.
|
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.
|
`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.
|
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.
|
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.
|
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:
|
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.
|
`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.
|
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.
|
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.
|
- réinitialiser le groupe de démonstration.
|
||||||
|
|
||||||
`Groupe Démonstration` est toujours présent sur une installation vanille et ne peut pas être supprimé.
|
`Groupe Démonstration` est toujours présent sur une installation vanille et ne peut pas être supprimé.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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. |
|
| `reunions` | Rencontres hebdomadaires et assemblées d'affaires. |
|
||||||
| `presences` | Présences rattachées à une réunion. |
|
| `presences` | Présences rattachées à une réunion. |
|
||||||
| `pv_reunions` | Procès-verbaux préparés ou adoptés. |
|
| `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. |
|
| `rapports_adoptes` | Rapports cristallisés après adoption. |
|
||||||
|
|
||||||
## Tables de gouvernance
|
## 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.
|
- 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.
|
- 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.
|
- Toute nouvelle table métier doit documenter son rattachement à un groupe.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ const CONTEXTES = {
|
||||||
description: "État général et consultations courantes.",
|
description: "État général et consultations courantes.",
|
||||||
sections: [
|
sections: [
|
||||||
{ titre: 'À faire maintenant', codes: ['S5', 'C5', 'C9'] },
|
{ 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'] },
|
{ titre: 'Mon espace', codes: ['C7', 'C11', 'C13', 'C12'] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -48,6 +48,7 @@ const CONTEXTES = {
|
||||||
sections: [
|
sections: [
|
||||||
{ titre: 'Finances', codes: ['S1T', 'S5', 'M4'] },
|
{ titre: 'Finances', codes: ['S1T', 'S5', 'M4'] },
|
||||||
{ titre: 'Membres et structure', codes: ['G14', 'G2', 'G11', 'G12', 'G9', 'G13'] },
|
{ titre: 'Membres et structure', codes: ['G14', 'G2', 'G11', 'G12', 'G9', 'G13'] },
|
||||||
|
{ titre: 'Services', codes: ['M3'] },
|
||||||
{ titre: 'Inventaire', codes: ['M5'] },
|
{ titre: 'Inventaire', codes: ['M5'] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ const MODULES = [
|
||||||
['S0', 'Rencontres'], ['M0', "Assemblée d'affaires"],
|
['S0', 'Rencontres'], ['M0', "Assemblée d'affaires"],
|
||||||
['C5', 'Gouvernance'],
|
['C5', 'Gouvernance'],
|
||||||
['S1T', 'Trésorerie'], ['M4', 'Donner au suivant'],
|
['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'],
|
['S2', 'Remise jeton'], ['M6', 'Commander jetons'],
|
||||||
['S3', 'Vente littérature'], ['M5', 'Ajuster inventaire'],
|
['S3', 'Vente littérature'], ['M5', 'Ajuster inventaire'],
|
||||||
['G2', 'Gestion membres'], ['G9', 'Paramètres'],
|
['G2', 'Gestion membres'], ['G9', 'Paramètres'],
|
||||||
|
|
|
||||||
|
|
@ -3,151 +3,200 @@ import { api } from '../stores/api';
|
||||||
import PageHeader from '../components/PageHeader';
|
import PageHeader from '../components/PageHeader';
|
||||||
import SuccessCheck from '../components/SuccessCheck';
|
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 (
|
||||||
|
<section className="bg-white rounded-xl p-4 shadow-sm">
|
||||||
|
<h3 className="text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">{titre}</h3>
|
||||||
|
<div className="text-sm text-gray-700 whitespace-pre-wrap leading-relaxed">{children}</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RapportDetail({ rapport, onBack }) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-100">
|
||||||
|
<PageHeader titre="Services" sousTitre={`Réunion du ${formatDate(rapport.reunion_dist)}`} />
|
||||||
|
<div className="p-3 space-y-3">
|
||||||
|
<button onClick={onBack} className="tap-target px-4 rounded-xl bg-white text-aa-blue text-sm font-bold shadow-sm">
|
||||||
|
Retour
|
||||||
|
</button>
|
||||||
|
<section className="bg-white rounded-xl p-4 shadow-sm">
|
||||||
|
<p className="text-xs text-gray-400">Renseigné par</p>
|
||||||
|
<p className="text-base font-bold text-gray-800">{rapport.redacteur_prenom}</p>
|
||||||
|
</section>
|
||||||
|
{rapport.decisions?.length > 0 && (
|
||||||
|
<section className="bg-white rounded-xl p-4 shadow-sm">
|
||||||
|
<h3 className="text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Décisions du district</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{rapport.decisions.map((decision, index) => (
|
||||||
|
<p key={index} className="text-sm text-gray-700 leading-relaxed">{decision}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
{champsTexte.map(([cle, label]) => (
|
||||||
|
<SectionLecture key={cle} titre={label}>{rapport[cle]}</SectionLecture>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function RapportRSG() {
|
export default function RapportRSG() {
|
||||||
const [rapports, setRapports] = useState([]);
|
const [rapports, setRapports] = useState([]);
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
reunion_dist: new Date().toISOString().slice(0, 10),
|
reunion_dist: new Date().toISOString().slice(0, 10),
|
||||||
decisions_text: '',
|
decisions_text: '',
|
||||||
|
infos_district: '',
|
||||||
infos_region: '',
|
infos_region: '',
|
||||||
sujets_groupe: '',
|
sujets_groupe: '',
|
||||||
|
positions_groupe: '',
|
||||||
|
suivis: '',
|
||||||
|
prochaine_echeance: '',
|
||||||
|
liens_utiles: '',
|
||||||
});
|
});
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [success, setSuccess] = useState(false);
|
const [success, setSuccess] = useState(false);
|
||||||
const [viewRapport, setViewRapport] = useState(null);
|
const [viewRapport, setViewRapport] = useState(null);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
const chargerRapports = () => {
|
||||||
|
setLoading(true);
|
||||||
api.get('/rapports-rsg/').then((data) => {
|
api.get('/rapports-rsg/').then((data) => {
|
||||||
setRapports(data);
|
setRapports(data);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}).catch(() => setLoading(false));
|
}).catch(() => setLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
chargerRapports();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
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');
|
setError('Au moins une section doit être remplie');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
|
||||||
const decisionsList = form.decisions_text
|
const decisionsList = form.decisions_text
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map((d) => d.trim())
|
.map((d) => d.trim())
|
||||||
.filter((d) => d.length > 0);
|
.filter(Boolean);
|
||||||
|
|
||||||
|
try {
|
||||||
await api.post('/rapports-rsg/', {
|
await api.post('/rapports-rsg/', {
|
||||||
reunion_dist: form.reunion_dist,
|
reunion_dist: form.reunion_dist,
|
||||||
decisions: decisionsList.length > 0 ? decisionsList : null,
|
decisions: decisionsList.length > 0 ? decisionsList : null,
|
||||||
|
infos_district: form.infos_district.trim() || null,
|
||||||
infos_region: form.infos_region.trim() || null,
|
infos_region: form.infos_region.trim() || null,
|
||||||
sujets_groupe: form.sujets_groupe.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);
|
setSuccess(true);
|
||||||
} catch (e) { setError(e.message); }
|
} catch (e) {
|
||||||
|
setError(e.message);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (success) return <SuccessCheck message="Rapport RSG enregistré ✓" />;
|
if (success) return <SuccessCheck message="Renseignement de service enregistré" />;
|
||||||
|
if (viewRapport) return <RapportDetail rapport={viewRapport} onBack={() => setViewRapport(null)} />;
|
||||||
// Vue lecture
|
|
||||||
if (viewRapport) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-100">
|
|
||||||
<div className="bg-aa-blue text-white p-4 flex items-center gap-3">
|
|
||||||
<button onClick={() => setViewRapport(null)} className="text-2xl">←</button>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-lg font-bold">Rapport RSG</h1>
|
|
||||||
<p className="text-blue-200 text-xs">Réunion du {viewRapport.reunion_dist}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 space-y-4">
|
|
||||||
<div className="bg-white rounded-2xl p-4 shadow-sm">
|
|
||||||
<p className="text-xs text-gray-400 mb-1">Rédigé par {viewRapport.redacteur_prenom}</p>
|
|
||||||
</div>
|
|
||||||
{viewRapport.decisions && viewRapport.decisions.length > 0 && (
|
|
||||||
<div className="bg-white rounded-2xl p-4 shadow-sm">
|
|
||||||
<h3 className="font-bold text-gray-700 mb-2">Décisions du district</h3>
|
|
||||||
{viewRapport.decisions.map((d, i) => (
|
|
||||||
<div key={i} className="py-2 border-b border-gray-100 last:border-0">
|
|
||||||
<p className="text-sm text-gray-800">• {d}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{viewRapport.infos_region && (
|
|
||||||
<div className="bg-white rounded-2xl p-4 shadow-sm">
|
|
||||||
<h3 className="font-bold text-gray-700 mb-2">Informations de la région</h3>
|
|
||||||
<p className="text-sm text-gray-600 whitespace-pre-wrap">{viewRapport.infos_region}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{viewRapport.sujets_groupe && (
|
|
||||||
<div className="bg-white rounded-2xl p-4 shadow-sm">
|
|
||||||
<h3 className="font-bold text-gray-700 mb-2">Sujets pour le groupe</h3>
|
|
||||||
<p className="text-sm text-gray-600 whitespace-pre-wrap">{viewRapport.sujets_groupe}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-100">
|
<div className="min-h-screen bg-gray-100">
|
||||||
<PageHeader titre="Rapport du RSG" sousTitre="M3 — Réunion de district" />
|
<PageHeader titre="Services" sousTitre="District, région, sujets et suivis" />
|
||||||
|
|
||||||
<div className="p-4 space-y-4">
|
<div className="p-3 space-y-4">
|
||||||
{/* Rapports existants */}
|
<section className="bg-white rounded-xl p-4 shadow-sm">
|
||||||
{rapports.length > 0 && (
|
<h2 className="text-xs font-bold text-gray-400 uppercase tracking-wider mb-3">Renseignements récents</h2>
|
||||||
<div className="bg-white rounded-2xl p-4 shadow-sm">
|
{loading ? (
|
||||||
<h2 className="text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Rapports récents</h2>
|
<p className="text-sm text-gray-500">Chargement...</p>
|
||||||
{rapports.slice(0, 5).map((r) => (
|
) : rapports.length === 0 ? (
|
||||||
<button key={r.id} onClick={() => setViewRapport(r)}
|
<p className="text-sm text-gray-500">Aucun renseignement de service enregistré.</p>
|
||||||
className="w-full flex items-center justify-between py-2 border-b border-gray-50 last:border-0 active:bg-gray-50">
|
) : (
|
||||||
<div>
|
<div className="divide-y divide-gray-100">
|
||||||
<p className="font-medium text-sm text-gray-800">Réunion du {r.reunion_dist}</p>
|
{rapports.slice(0, 8).map((rapport) => (
|
||||||
<p className="text-xs text-gray-400">Par {r.redacteur_prenom}</p>
|
<button
|
||||||
|
key={rapport.id}
|
||||||
|
onClick={() => setViewRapport(rapport)}
|
||||||
|
className="w-full py-3 flex items-center justify-between gap-3 text-left active:bg-gray-50"
|
||||||
|
>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="font-bold text-sm text-gray-800 truncate">{formatDate(rapport.reunion_dist)}</p>
|
||||||
|
<p className="text-xs text-gray-400 truncate">Par {rapport.redacteur_prenom}</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-gray-300">→</span>
|
<span className="text-gray-300 text-lg">›</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Nouveau rapport */}
|
<section className="bg-white rounded-xl p-4 shadow-sm">
|
||||||
<div className="bg-white rounded-2xl p-4 shadow-sm">
|
|
||||||
<label className="text-xs text-gray-400 block mb-1">Date de la réunion de district</label>
|
<label className="text-xs text-gray-400 block mb-1">Date de la réunion de district</label>
|
||||||
<input type="date" value={form.reunion_dist}
|
<input
|
||||||
|
type="date"
|
||||||
|
value={form.reunion_dist}
|
||||||
onChange={(e) => setForm({ ...form, reunion_dist: e.target.value })}
|
onChange={(e) => 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"
|
||||||
</div>
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div className="bg-white rounded-2xl p-4 shadow-sm">
|
<section className="bg-white rounded-xl p-4 shadow-sm">
|
||||||
<label className="text-xs text-gray-400 block mb-1">Décisions du district (une par ligne)</label>
|
<label className="text-xs text-gray-400 block mb-1">Décisions du district</label>
|
||||||
<textarea value={form.decisions_text}
|
<textarea
|
||||||
onChange={(e) => setForm({ ...form, decisions_text: e.target.value })} rows={4}
|
value={form.decisions_text}
|
||||||
placeholder="Vote sur le budget annuel approuvé Nouveau format d'atelier proposé Date du prochain congrès confirmée"
|
onChange={(e) => setForm({ ...form, decisions_text: e.target.value })}
|
||||||
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue resize-none text-sm" />
|
rows={4}
|
||||||
</div>
|
placeholder="Une décision par ligne"
|
||||||
|
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue resize-none text-sm"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div className="bg-white rounded-2xl p-4 shadow-sm">
|
{champsTexte.map(([cle, label, placeholder]) => (
|
||||||
<label className="text-xs text-gray-400 block mb-1">Informations de la région 87</label>
|
<section key={cle} className="bg-white rounded-xl p-4 shadow-sm">
|
||||||
<textarea value={form.infos_region}
|
<label className="text-xs text-gray-400 block mb-1">{label}</label>
|
||||||
onChange={(e) => setForm({ ...form, infos_region: e.target.value })} rows={3}
|
<textarea
|
||||||
placeholder="Nouvelles de l'assemblée régionale, rapports..."
|
value={form[cle]}
|
||||||
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue resize-none text-sm" />
|
onChange={(e) => setForm({ ...form, [cle]: e.target.value })}
|
||||||
</div>
|
rows={cle === 'liens_utiles' || cle === 'prochaine_echeance' ? 2 : 3}
|
||||||
|
placeholder={placeholder}
|
||||||
<div className="bg-white rounded-2xl p-4 shadow-sm">
|
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue resize-none text-sm"
|
||||||
<label className="text-xs text-gray-400 block mb-1">Sujets à rapporter au groupe</label>
|
/>
|
||||||
<textarea value={form.sujets_groupe}
|
</section>
|
||||||
onChange={(e) => setForm({ ...form, sujets_groupe: e.target.value })} rows={3}
|
))}
|
||||||
placeholder="Points à discuter à la prochaine réunion d'affaires..."
|
|
||||||
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue resize-none text-sm" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && <p className="text-red-500 text-sm text-center bg-red-50 p-3 rounded-lg">{error}</p>}
|
{error && <p className="text-red-500 text-sm text-center bg-red-50 p-3 rounded-lg">{error}</p>}
|
||||||
|
|
||||||
<button onClick={handleSubmit}
|
<button
|
||||||
className="w-full p-5 bg-blue-700 text-white rounded-2xl text-xl font-bold disabled:opacity-40 active:scale-[0.97] transition shadow-lg">
|
onClick={handleSubmit}
|
||||||
📨 Enregistrer le rapport
|
className="w-full p-5 bg-blue-700 text-white rounded-xl text-lg font-bold disabled:opacity-40 active:scale-[0.98] transition shadow-lg"
|
||||||
|
>
|
||||||
|
Enregistrer
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue