groupe-meditation/frontend/src/pages/AssembleeAffaires.jsx

256 lines
12 KiB
React
Raw Normal View History

2026-05-25 17:39:38 -04:00
import React, { useEffect, useState } from 'react';
import { api } from '../stores/api';
import { useStore } from '../stores/store';
import PageHeader from '../components/PageHeader';
import { dateHeureReunionIso, datesAssembleesAffaires, dateLocale, labelDateReunion } from '../utils/datesReunion';
const INPUT = 'w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue';
export default function AssembleeAffaires() {
const { postes, groupe } = useStore();
const estExecutif = postes.some((p) => ['RSG', 'RSG Substitut(e)', 'Trésorier(ière)', 'Secrétaire'].includes(p));
const choixDates = datesAssembleesAffaires(groupe);
const [membres, setMembres] = useState([]);
const [reunions, setReunions] = useState([]);
const [pvs, setPvs] = useState([]);
const [propositions, setPropositions] = useState([]);
const [resolutions, setResolutions] = useState([]);
const [postesVacants, setPostesVacants] = useState([]);
const [reunionId, setReunionId] = useState('');
const [dateSelection, setDateSelection] = useState(dateLocale());
const [dateReunion, setDateReunion] = useState(dateLocale());
const [note, setNote] = useState('');
const [presencesMap, setPresencesMap] = useState({});
const [contenu, setContenu] = useState('');
const [pvPrecedent, setPvPrecedent] = useState(null);
const [showPvPrecedent, setShowPvPrecedent] = useState(false);
const [adoption, setAdoption] = useState({ proposeur: '', secondeur: '' });
const [msg, setMsg] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const charger = async () => {
try {
const [m, r, pv, props, res, postesDispo] = await Promise.all([
api.get('/membres/actifs'),
api.get('/presences/reunions'),
api.get('/pv/'),
api.get('/propositions/?statut=secondee&limite=100'),
api.get('/propositions/resolutions'),
api.get('/postes/disponibles'),
]);
setMembres(m);
setReunions(r);
setPvs(pv);
setPropositions(props);
setResolutions(res);
setPostesVacants(postesDispo);
const map = {};
m.forEach((mb) => { map[mb.id] = true; });
setPresencesMap(map);
const duJour = r.find((x) => x.date_reunion?.slice(0, 10) === dateSelection);
if (duJour) setReunionId(duJour.id);
} catch (e) { setError(e.message); }
};
useEffect(() => { charger(); }, []);
useEffect(() => {
if (choixDates.length > 0) {
if (!choixDates.includes(dateSelection)) setDateSelection(choixDates.find((d) => d >= dateLocale()) || choixDates[choixDates.length - 1]);
if (!choixDates.includes(dateReunion)) setDateReunion(choixDates.find((d) => d >= dateLocale()) || choixDates[choixDates.length - 1]);
}
}, [groupe?.jour_reunion, groupe?.assemblee_affaires_ordre]);
useEffect(() => {
const duJour = reunions.find((x) => x.date_reunion?.slice(0, 10) === dateSelection);
setReunionId(duJour?.id || '');
const precedents = pvs.filter((p) => p.date_reunion && p.date_reunion.slice(0, 10) < dateSelection);
setPvPrecedent(precedents[0] || null);
}, [dateSelection, reunions, pvs]);
const reunion = reunions.find((r) => r.id === reunionId);
const pvJour = pvs.find((p) => p.date_reunion?.slice(0, 10) === dateSelection);
const resolutionsJour = resolutions.filter((r) => (r.date_vote || r.cree_le?.slice(0, 10)) === dateSelection);
const nbPresents = Object.values(presencesMap).filter(Boolean).length;
const toggle = (membreId) => setPresencesMap({ ...presencesMap, [membreId]: !presencesMap[membreId] });
const imprimer = () => window.print();
const creerRapport = async () => {
const res = await api.post('/presences/reunion', {
date_reunion: dateHeureReunionIso(dateReunion, groupe),
note: note.trim() || null,
});
setReunionId(res.id);
setReunions([res, ...reunions]);
setDateSelection(res.date_reunion.slice(0, 10));
return res.id;
};
const enregistrer = async () => {
setError('');
setMsg('');
setLoading(true);
try {
const id = reunionId || await creerRapport();
const presences = Object.entries(presencesMap).map(([membre_id, present]) => ({ membre_id, present }));
await api.post('/presences/', { reunion_id: id, presences });
if (contenu.trim()) {
await api.post('/pv/', {
reunion_id: id,
contenu: contenu.trim(),
decisions: null,
});
}
setMsg(contenu.trim() ? 'Présences et PV enregistrés' : 'Présences enregistrées');
await charger();
} catch (e) { setError(e.message); }
setLoading(false);
};
const adopterPvPrecedent = async () => {
if (!pvPrecedent) return;
setError('');
try {
await api.patch(`/pv/${pvPrecedent.id}/adopter`, adoption);
setMsg('PV précédent adopté');
await charger();
} catch (e) { setError(e.message); }
};
const textePvAuto = [
'Ouverture de l’assemblée.',
'',
pvPrecedent ? `Lecture du procès-verbal précédent (${new Date(pvPrecedent.date_reunion).toLocaleDateString('fr-CA')}).` : 'Lecture du procès-verbal précédent.',
'',
'Réflexions en cours:',
...propositions.map((p) => `- ${p.sujet} (${p.proposeur_prenom} / ${p.secondeur_prenom || 'à confirmer'})`),
'',
'Décisions prises pendant cette assemblée:',
...resolutionsJour.map((r) => `- ${r.numero_resolution || ''} ${r.sujet}`.trim()),
'',
'Postes disponibles:',
...postesVacants.map((p) => `- ${p.nom}`),
].join('\n');
return (
<div className="min-h-screen bg-gray-100">
<PageHeader titre="Assemblée d'affaires" sousTitre="Assistant et rapport imprimable" />
<div className="p-4 space-y-4">
{msg && <div className="bg-green-50 text-green-700 p-3 rounded-xl text-center text-sm font-medium">{msg}</div>}
{error && <div className="bg-red-50 text-red-600 p-3 rounded-xl text-center text-sm">{error}</div>}
<Card titre="Consulter le rapport">
<Champ label="Date">
<SelectDate value={dateSelection} onChange={setDateSelection} dates={choixDates} />
</Champ>
<div className="grid grid-cols-2 gap-2">
<button onClick={imprimer} className="p-3 bg-gray-900 text-white rounded-xl text-sm font-bold">Imprimer</button>
{pvPrecedent && (
<button onClick={() => setShowPvPrecedent(!showPvPrecedent)} className="p-3 bg-blue-50 text-blue-700 rounded-xl text-sm font-bold border border-blue-100">
PV précédent
</button>
)}
</div>
</Card>
{showPvPrecedent && pvPrecedent && (
<Card titre="Procès-verbal précédent">
<p className="text-xs text-gray-400">{new Date(pvPrecedent.date_reunion).toLocaleDateString('fr-CA')}</p>
<p className="text-sm whitespace-pre-wrap text-gray-700">{pvPrecedent.contenu}</p>
{estExecutif && !pvPrecedent.approuve && (
<div className="space-y-2 pt-2 border-t border-gray-100">
<div className="grid grid-cols-2 gap-2">
<input value={adoption.proposeur} onChange={(e) => setAdoption({ ...adoption, proposeur: e.target.value })} placeholder="Proposeur" className={INPUT} />
<input value={adoption.secondeur} onChange={(e) => setAdoption({ ...adoption, secondeur: e.target.value })} placeholder="Secondeur" className={INPUT} />
</div>
<button onClick={adopterPvPrecedent} className="w-full p-3 bg-green-50 text-green-700 rounded-xl text-sm font-bold border border-green-100">Adopter le PV précédent</button>
</div>
)}
</Card>
)}
<div className="bg-white rounded-2xl p-5 shadow-sm print:shadow-none print:rounded-none print:p-0">
<h2 className="text-xl font-bold text-gray-900">Rapport d’assemblée d’affaires</h2>
<p className="text-sm text-gray-500">{dateSelection}</p>
<Section titre="Présences">
{reunion?.presences?.length ? reunion.presences.filter((p) => p.present).map((p) => <li key={p.membre_id}>{p.membre_prenom}</li>) : <li>Présences à cocher sur papier</li>}
</Section>
<Section titre="Réflexions en cours">
{propositions.length ? propositions.map((p) => <li key={p.id}>{p.sujet} ({p.proposeur_prenom} / {p.secondeur_prenom || 'à confirmer'})</li>) : <li>Aucune proposition secondée non résolue</li>}
</Section>
<Section titre="Décisions prises">
{resolutionsJour.length ? resolutionsJour.map((r) => <li key={r.id}>{r.numero_resolution} — {r.sujet}</li>) : <li>Aucune décision enregistrée pour cette date</li>}
</Section>
<Section titre="Postes disponibles">
{postesVacants.length ? postesVacants.map((p) => <li key={p.id}>{p.nom}</li>) : <li>Aucun poste vacant</li>}
</Section>
<div className="mt-4">
<h3 className="font-bold text-gray-800">Procès-verbal</h3>
<p className="text-sm whitespace-pre-wrap mt-1">{pvJour?.contenu || textePvAuto}</p>
</div>
</div>
{estExecutif && (
<>
<Card titre="Créer ou renseigner l’assemblée">
{reunions.length > 0 && (
<Champ label="Assemblée existante">
<select value={reunionId} onChange={(e) => setReunionId(e.target.value)} className={INPUT}>
<option value="">Nouvelle assemblée</option>
{reunions.map((r) => <option key={r.id} value={r.id}>{new Date(r.date_reunion).toLocaleDateString('fr-CA')}</option>)}
</select>
</Champ>
)}
{!reunionId && (
<>
<Champ label="Date"><SelectDate value={dateReunion} onChange={setDateReunion} dates={choixDates} /></Champ>
<Champ label="Note"><input value={note} onChange={(e) => setNote(e.target.value)} placeholder="Note optionnelle" className={INPUT} /></Champ>
<button onClick={creerRapport} className="w-full p-3 bg-indigo-50 text-indigo-700 rounded-xl text-sm font-bold border border-indigo-100">Créer le rapport d’assemblée</button>
</>
)}
</Card>
<Card titre={`Présences (${nbPresents}/${membres.length})`}>
<div className="space-y-1">{membres.map((m) => (
<button key={m.id} onClick={() => toggle(m.id)} className={`w-full flex items-center gap-3 p-3 rounded-xl transition ${presencesMap[m.id] ? 'bg-green-50 border border-green-200' : 'bg-gray-50 border border-gray-100 opacity-50'}`}>
<span className={`w-6 h-6 rounded-lg flex items-center justify-center text-sm font-bold ${presencesMap[m.id] ? 'bg-green-500 text-white' : 'bg-gray-200 text-gray-400'}`}>{presencesMap[m.id] ? '✓' : ''}</span>
<span className="font-medium text-gray-800">{m.prenom}</span>
</button>
))}</div>
</Card>
<Card titre="Procès-verbal">
<textarea value={contenu} onChange={(e) => setContenu(e.target.value)} rows={10} placeholder={textePvAuto} className={`${INPUT} resize-none text-sm`} />
</Card>
<button onClick={enregistrer} disabled={loading} className="w-full p-5 bg-indigo-600 text-white rounded-2xl text-lg font-bold disabled:opacity-40 active:scale-[0.97] transition shadow-lg">{loading ? '...' : 'Enregistrer l’assemblée'}</button>
</>
)}
</div>
</div>
);
}
function Card({ titre, children }) {
return <div className="bg-white rounded-2xl p-4 shadow-sm space-y-3"><h2 className="font-bold text-gray-800">{titre}</h2>{children}</div>;
}
function Champ({ label, children }) {
return <label className="block"><span className="text-xs text-gray-400 block mb-1">{label}</span>{children}</label>;
}
function Section({ titre, children }) {
return <div className="mt-4"><h3 className="font-bold text-gray-800">{titre}</h3><ul className="list-disc ml-5 text-sm mt-1 space-y-1">{children}</ul></div>;
}
function SelectDate({ value, onChange, dates }) {
if (!dates.length) {
return <div className="rounded-xl bg-gray-50 p-3 text-sm text-gray-500">Configurez le jour de réunion dans les paramètres du groupe.</div>;
}
return (
<select value={value} onChange={(e) => onChange(e.target.value)} className={INPUT}>
{dates.map((d) => <option key={d} value={d}>{labelDateReunion(d)}</option>)}
</select>
);
}