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

318 lines
14 KiB
React
Raw Normal View History

2026-03-04 14:01:00 -05:00
import React, { useState, useEffect } from 'react';
import { api } from '../stores/api';
2026-05-25 17:39:38 -04:00
import { useStore } from '../stores/store';
2026-03-04 14:01:00 -05:00
import PageHeader from '../components/PageHeader';
const STATUTS = {
2026-05-25 17:39:38 -04:00
proposee: { label: 'À seconder', cls: 'bg-yellow-100 text-yellow-700' },
secondee: { label: 'Secondée', cls: 'bg-blue-100 text-blue-700' },
adoptee: { label: 'Résolution', cls: 'bg-green-100 text-green-700' },
rejetee: { label: 'Rejetée', cls: 'bg-red-100 text-red-700' },
2026-03-04 14:01:00 -05:00
};
2026-05-25 17:39:38 -04:00
const TYPES = {
proposition: 'Proposition',
resolution: 'Résolution',
generale: 'Résolution',
nomination: 'Nomination',
contribution: 'Contribution',
};
2026-03-04 14:01:00 -05:00
export default function Decisions() {
2026-05-25 17:39:38 -04:00
const { postes } = useStore();
const [propositions, setPropositions] = useState([]);
const [resolutions, setResolutions] = useState([]);
const [tab, setTab] = useState('cours');
2026-03-04 14:01:00 -05:00
const [loading, setLoading] = useState(true);
2026-05-25 17:39:38 -04:00
const [error, setError] = useState('');
const [msg, setMsg] = useState('');
const [sujet, setSujet] = useState('');
const [description, setDescription] = useState('');
const [resolutionId, setResolutionId] = useState('');
const [resolutionForm, setResolutionForm] = useState({});
const estExecutif = postes.some((p) =>
['RSG', 'RSG Substitut(e)', 'Trésorier(ière)', 'Secrétaire'].includes(p)
);
2026-03-04 14:01:00 -05:00
const charger = async () => {
setLoading(true);
try {
2026-05-25 17:39:38 -04:00
const [props, res] = await Promise.all([
api.get('/propositions/?limite=100'),
api.get('/propositions/resolutions'),
]);
setPropositions(props);
setResolutions(res);
if (res.length > 0 && !resolutionId) setResolutionId(res[0].id);
} catch (e) { setError(e.message); }
2026-03-04 14:01:00 -05:00
setLoading(false);
};
2026-05-25 17:39:38 -04:00
useEffect(() => { charger(); }, []);
const flash = (m) => { setMsg(m); setTimeout(() => setMsg(''), 2500); };
const soumettre = async () => {
if (!sujet.trim()) { setError('Le sujet est obligatoire'); return; }
setError('');
try {
await api.post('/propositions/', {
sujet: sujet.trim(),
description: description.trim() || null,
});
setSujet('');
setDescription('');
setTab('cours');
flash('Proposition soumise');
await charger();
} catch (e) { setError(e.message); }
};
const seconder = async (id) => {
setError('');
try {
await api.post(`/propositions/${id}/seconder`);
flash('Proposition secondée');
await charger();
} catch (e) { setError(e.message); }
};
const mettreAJourResolution = (id, champ, valeur) => {
setResolutionForm({
...resolutionForm,
[id]: { ...(resolutionForm[id] || {}), [champ]: valeur },
});
};
const transformer = async (id) => {
setError('');
const form = resolutionForm[id] || {};
const etantDonneQue = (form.etant_donne_que || '').trim();
const groupeADecideDe = (form.groupe_a_decide_de || '').trim();
if (!etantDonneQue || !groupeADecideDe) {
setError('Les sections "ÉTANT DONNÉ QUE" et "LE GROUPE A DÉCIDÉ DE" sont obligatoires');
return;
}
try {
await api.patch(`/propositions/${id}/voter`, {
statut: 'adoptee',
etant_donne_que: etantDonneQue,
groupe_a_decide_de: groupeADecideDe,
});
setResolutionForm({ ...resolutionForm, [id]: { etant_donne_que: '', groupe_a_decide_de: '' } });
flash('Résolution créée');
await charger();
} catch (e) { setError(e.message); }
};
const rejeter = async (id) => {
setError('');
try {
await api.patch(`/propositions/${id}/voter`, { statut: 'rejetee' });
flash('Proposition rejetée');
await charger();
} catch (e) { setError(e.message); }
};
const enCours = propositions.filter((p) => ['proposee', 'secondee'].includes(p.statut));
const resolutionChoisie = resolutions.find((r) => r.id === resolutionId);
if (loading) return <div className="flex items-center justify-center min-h-screen"><p className="text-gray-400">Chargement...</p></div>;
2026-03-04 14:01:00 -05:00
return (
<div className="min-h-screen bg-gray-100">
2026-05-25 17:39:38 -04:00
<PageHeader titre="Gouvernance" sousTitre="Décisions du groupe" />
<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>}
2026-03-04 14:01:00 -05:00
<div className="flex gap-2">
{[
2026-05-25 17:39:38 -04:00
['cours', `En cours (${enCours.length})`],
['nouvelle', 'Soumettre'],
['historique', `Résolutions (${resolutions.length})`],
].map(([val, label]) => (
<button key={val} onClick={() => { setTab(val); setError(''); }}
className={`flex-1 py-2 rounded-xl text-sm font-bold transition ${
tab === val ? 'bg-aa-blue text-white' : 'bg-white text-gray-500'
}`}>
{label}
2026-03-04 14:01:00 -05:00
</button>
))}
</div>
2026-05-25 17:39:38 -04:00
{tab === 'cours' && (
enCours.length === 0 ? (
<div className="text-center py-12">
<p className="text-4xl mb-2">📌</p>
<p className="text-gray-500">Aucune décision en cours</p>
<button onClick={() => setTab('nouvelle')} className="mt-3 text-aa-blue font-bold text-sm">
Soumettre une proposition
</button>
</div>
) : (
<div className="space-y-3">
{enCours.map((p) => {
const statut = STATUTS[p.statut] || STATUTS.proposee;
const form = resolutionForm[p.id] || {};
const resolutionComplete = (form.etant_donne_que || '').trim() && (form.groupe_a_decide_de || '').trim();
return (
<div key={p.id} className="bg-white rounded-2xl p-4 shadow-sm">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="font-bold text-gray-800">{p.sujet}</p>
{p.description && <p className="text-sm text-gray-500 mt-1">{p.description}</p>}
</div>
<span className={`text-xs px-2 py-1 rounded-full font-medium whitespace-nowrap ${statut.cls}`}>
{statut.label}
</span>
</div>
<div className="mt-3 text-xs text-gray-400 space-y-1">
<p>Proposée par <span className="font-medium text-gray-600">{p.proposeur_prenom}</span></p>
{p.secondeur_prenom && <p>Secondée par <span className="font-medium text-gray-600">{p.secondeur_prenom}</span></p>}
</div>
{p.statut === 'proposee' && (
<div className="grid grid-cols-2 gap-2 mt-3">
{estExecutif && (
<button onClick={() => rejeter(p.id)}
className="py-2 bg-red-50 text-red-700 rounded-xl text-sm font-bold border border-red-200 active:scale-[0.97] transition">
Rejeter
</button>
)}
<button onClick={() => seconder(p.id)}
className={`${estExecutif ? '' : 'col-span-2'} py-2 bg-blue-50 text-blue-700 rounded-xl text-sm font-bold border border-blue-200 active:scale-[0.97] transition`}>
Seconder
</button>
</div>
2026-03-04 14:01:00 -05:00
)}
2026-05-25 17:39:38 -04:00
{p.statut === 'secondee' && estExecutif && (
<div className="mt-3 space-y-3">
<div>
<label className="text-xs text-gray-400 block mb-1">ÉTANT DONNÉ QUE</label>
<textarea value={form.etant_donne_que || ''}
onChange={(e) => mettreAJourResolution(p.id, 'etant_donne_que', e.target.value)}
placeholder="Contexte et considérants de la résolution"
rows={3}
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue resize-none text-sm" />
</div>
<div>
<label className="text-xs text-gray-400 block mb-1">LE GROUPE A DÉCIDÉ DE</label>
<textarea value={form.groupe_a_decide_de || ''}
onChange={(e) => mettreAJourResolution(p.id, 'groupe_a_decide_de', e.target.value)}
placeholder="Décision adoptée par le groupe"
rows={3}
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue resize-none text-sm" />
</div>
<div className="grid grid-cols-2 gap-2">
<button onClick={() => rejeter(p.id)}
className="py-2 bg-red-50 text-red-700 rounded-xl text-sm font-bold border border-red-200 active:scale-[0.97] transition">
Rejeter
</button>
<button onClick={() => transformer(p.id)} disabled={!resolutionComplete}
className="py-2 bg-green-50 text-green-700 rounded-xl text-sm font-bold border border-green-200 disabled:opacity-40 active:scale-[0.97] transition">
Transformer
</button>
</div>
</div>
)}
</div>
);
})}
</div>
)
)}
{tab === 'nouvelle' && (
<div className="space-y-4">
<div className="bg-white rounded-2xl p-4 shadow-sm">
<label className="text-xs text-gray-400 block mb-1">Sujet</label>
<input type="text" value={sujet} onChange={(e) => setSujet(e.target.value)}
placeholder="Sujet de la proposition"
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue"
autoFocus />
</div>
<div className="bg-white rounded-2xl p-4 shadow-sm">
<label className="text-xs text-gray-400 block mb-1">Détails (optionnel)</label>
<textarea value={description} onChange={(e) => setDescription(e.target.value)}
placeholder="Contexte, justification, informations utiles"
rows={3}
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue resize-none" />
</div>
<button onClick={soumettre} disabled={!sujet.trim()}
className="w-full p-5 bg-aa-blue text-white rounded-2xl text-lg font-bold disabled:opacity-40 active:scale-[0.97] transition shadow-lg">
Soumettre la proposition
</button>
</div>
)}
{tab === 'historique' && (
resolutions.length === 0 ? (
<div className="text-center py-12">
<p className="text-4xl mb-2">📜</p>
<p className="text-gray-500">Aucune résolution</p>
</div>
) : (
<div className="space-y-4">
<div className="bg-white rounded-2xl p-4 shadow-sm">
<label className="text-xs text-gray-400 block mb-1">Historique des résolutions</label>
<select value={resolutionId} onChange={(e) => setResolutionId(e.target.value)}
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue">
{resolutions.map((r) => (
<option key={r.id} value={r.id}>
{(r.date_adoption || r.date_vote || r.cree_le?.slice(0, 10) || 'Sans date')} · {r.numero_resolution || 'RES'} · {r.sujet}
</option>
))}
</select>
</div>
{resolutionChoisie && (
<div className="bg-white rounded-2xl p-4 shadow-sm">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-xs font-mono text-aa-blue font-bold">{resolutionChoisie.numero_resolution || 'Résolution'}</p>
<p className="font-bold text-gray-800">{resolutionChoisie.sujet}</p>
</div>
<span className="text-xs px-2 py-1 rounded-full font-medium bg-green-100 text-green-700">
Adoptée
</span>
</div>
{resolutionChoisie.description && <p className="text-sm text-gray-500 mt-3">{resolutionChoisie.description}</p>}
<div className="mt-3 text-xs text-gray-400 space-y-1">
<p>Type : {TYPES[resolutionChoisie.type_proposition] || resolutionChoisie.type_proposition}</p>
<p>Date : {resolutionChoisie.date_adoption || resolutionChoisie.date_vote || resolutionChoisie.cree_le?.slice(0, 10) || '—'}</p>
<p>Proposée par {resolutionChoisie.proposeur_prenom}</p>
{resolutionChoisie.secondeur_prenom && <p>Secondée par {resolutionChoisie.secondeur_prenom}</p>}
2026-03-04 14:01:00 -05:00
</div>
2026-05-25 17:39:38 -04:00
{(resolutionChoisie.etant_donne_que || resolutionChoisie.groupe_a_decide_de || resolutionChoisie.note) && (
<div className="mt-3 bg-gray-50 rounded-xl p-3">
<p className="text-xs text-gray-400 mb-1">Texte de la résolution</p>
{resolutionChoisie.etant_donne_que && (
<p className="text-sm text-gray-700 whitespace-pre-wrap"><strong>ÉTANT DONNÉ QUE</strong>{'\n'}{resolutionChoisie.etant_donne_que}</p>
)}
{resolutionChoisie.groupe_a_decide_de && (
<p className="text-sm text-gray-700 whitespace-pre-wrap mt-3"><strong>LE GROUPE A DÉCIDÉ DE</strong>{'\n'}{resolutionChoisie.groupe_a_decide_de}</p>
)}
{!resolutionChoisie.etant_donne_que && !resolutionChoisie.groupe_a_decide_de && (
<p className="text-sm text-gray-700 whitespace-pre-wrap">{resolutionChoisie.note}</p>
)}
</div>
2026-03-04 14:01:00 -05:00
)}
</div>
2026-05-25 17:39:38 -04:00
)}
2026-03-04 14:01:00 -05:00
</div>
2026-05-25 17:39:38 -04:00
)
2026-03-04 14:01:00 -05:00
)}
</div>
</div>
);
}