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

359 lines
17 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() {
const { estExecutif } = useStore();
2026-05-25 17:39:38 -04:00
const [propositions, setPropositions] = useState([]);
const [resolutions, setResolutions] = useState([]);
const [membres, setMembres] = useState([]);
2026-05-25 17:39:38 -04:00
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 [resolutionForm, setResolutionForm] = useState({});
const [seconderPour, setSeconderPour] = useState({});
const [decisionOuverte, setDecisionOuverte] = useState(null);
2026-05-25 17:39:38 -04:00
const peutExecutif = estExecutif;
2026-03-04 14:01:00 -05:00
const charger = async () => {
setLoading(true);
try {
const [props, res, membresActifs] = await Promise.all([
2026-05-25 17:39:38 -04:00
api.get('/propositions/?limite=100'),
api.get('/propositions/resolutions'),
api.get('/membres/actifs'),
2026-05-25 17:39:38 -04:00
]);
setPropositions(props);
setResolutions(res);
setMembres(membresActifs);
2026-05-25 17:39:38 -04:00
} 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("L'objet est obligatoire"); return; }
2026-05-25 17:39:38 -04:00
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, membreId = '') => {
2026-05-25 17:39:38 -04:00
setError('');
try {
const proposition = propositions.find((p) => p.id === id);
const query = membreId ? `?membre_id=${membreId}` : '';
await api.post(`/propositions/${id}/seconder${query}`, null, { version: proposition?.modifie_le });
2026-05-25 17:39:38 -04:00
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 {
const proposition = propositions.find((p) => p.id === id);
2026-05-25 17:39:38 -04:00
await api.patch(`/propositions/${id}/voter`, {
statut: 'adoptee',
etant_donne_que: etantDonneQue,
groupe_a_decide_de: groupeADecideDe,
}, { version: proposition?.modifie_le });
2026-05-25 17:39:38 -04:00
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('');
const form = resolutionForm[id] || {};
const etantDonneQue = (form.etant_donne_que || '').trim();
if (!etantDonneQue) {
setError('La section "ÉTANT DONNÉ QUE" est obligatoire pour rejeter une proposition');
return;
}
2026-05-25 17:39:38 -04:00
try {
const proposition = propositions.find((p) => p.id === id);
await api.patch(`/propositions/${id}/voter`, {
statut: 'rejetee',
etant_donne_que: etantDonneQue,
}, { version: proposition?.modifie_le });
2026-05-25 17:39:38 -04:00
flash('Proposition rejetée');
await charger();
} catch (e) { setError(e.message); }
};
const enCours = propositions.filter((p) => ['proposee', 'secondee'].includes(p.statut));
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();
const motifRejetPresent = (form.etant_donne_que || '').trim();
2026-05-25 17:39:38 -04:00
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="mt-3 space-y-2">
{peutExecutif && (
<textarea value={form.etant_donne_que || ''}
onChange={(e) => mettreAJourResolution(p.id, 'etant_donne_que', e.target.value)}
placeholder="ÉTANT DONNÉ QUE..."
rows={2}
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue resize-none text-sm" />
)}
<div className="grid grid-cols-2 gap-2">
{peutExecutif && (
<button onClick={() => rejeter(p.id)} disabled={!motifRejetPresent}
2026-05-25 17:39:38 -04:00
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={`${peutExecutif ? '' : '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`}>
Je seconde
</button>
</div>
</div>
)}
{p.statut === 'proposee' && peutExecutif && (
<div className="mt-2 grid grid-cols-[1fr_auto] gap-2">
<select value={seconderPour[p.id] || ''} onChange={(e) => setSeconderPour({ ...seconderPour, [p.id]: e.target.value })}
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue text-sm">
<option value="">Seconder pour...</option>
{membres.map((m) => <option key={m.id} value={m.id}>{m.prenom}</option>)}
</select>
<button onClick={() => seconder(p.id, seconderPour[p.id])} disabled={!seconderPour[p.id]}
className="px-4 bg-blue-600 text-white rounded-xl text-sm font-bold disabled:opacity-40">
OK
2026-05-25 17:39:38 -04:00
</button>
</div>
2026-03-04 14:01:00 -05:00
)}
2026-05-25 17:39:38 -04:00
{p.statut === 'secondee' && peutExecutif && (
2026-05-25 17:39:38 -04:00
<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)} disabled={!motifRejetPresent}
className="py-2 bg-red-50 text-red-700 rounded-xl text-sm font-bold border border-red-200 disabled:opacity-40 active:scale-[0.97] transition">
2026-05-25 17:39:38 -04:00
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">Objet</label>
2026-05-25 17:39:38 -04:00
<input type="text" value={sujet} onChange={(e) => setSujet(e.target.value)}
placeholder="Objet de la proposition"
2026-05-25 17:39:38 -04:00
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-3">
{resolutions.map((decision) => {
const ouverte = decisionOuverte === decision.id;
const statut = STATUTS[decision.statut] || STATUTS.adoptee;
return (
<div key={decision.id} className={`bg-white rounded-xl p-4 shadow-sm border ${ouverte ? 'border-aa-blue/30' : 'border-transparent'}`}>
<button type="button" onClick={() => setDecisionOuverte(ouverte ? null : decision.id)}
className="w-full text-left">
2026-05-25 17:39:38 -04:00
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-xs font-mono text-aa-blue font-bold">
{decision.numero_resolution || (decision.statut === 'rejetee' ? 'REJETÉE' : 'Résolution')}
</p>
<p className="font-bold text-gray-800">{decision.sujet}</p>
<p className="text-xs text-gray-400 mt-1">
{decision.date_adoption || decision.date_vote || decision.cree_le?.slice(0, 10) || 'Sans date'}
</p>
2026-05-25 17:39:38 -04:00
</div>
<span className={`text-xs px-2 py-1 rounded-full font-medium whitespace-nowrap ${statut.cls}`}>
{decision.statut === 'adoptee' ? 'Adoptée' : statut.label}
2026-05-25 17:39:38 -04:00
</span>
</div>
</button>
{ouverte && (
<>
{decision.description && <p className="text-sm text-gray-500 mt-3">{decision.description}</p>}
2026-05-25 17:39:38 -04:00
<div className="mt-3 text-xs text-gray-400 space-y-1">
2026-05-26 09:05:58 -04:00
<p>Type : {TYPES[decision.type_decision] || decision.type_decision}</p>
<p>Date : {decision.date_adoption || decision.date_vote || decision.cree_le?.slice(0, 10) || '—'}</p>
<p>Proposée par {decision.proposeur_prenom}</p>
{decision.secondeur_prenom && <p>Secondée par {decision.secondeur_prenom}</p>}
2026-03-04 14:01:00 -05:00
</div>
{(decision.etant_donne_que || decision.groupe_a_decide_de || decision.note) && (
2026-05-25 17:39:38 -04:00
<div className="mt-3 bg-gray-50 rounded-xl p-3">
<p className="text-xs text-gray-400 mb-1">{decision.statut === 'rejetee' ? 'Motif conservé' : 'Texte de la résolution'}</p>
{decision.etant_donne_que && (
<p className="text-sm text-gray-700 whitespace-pre-wrap"><strong>ÉTANT DONNÉ QUE</strong>{'\n'}{decision.etant_donne_que}</p>
2026-05-25 17:39:38 -04:00
)}
{decision.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'}{decision.groupe_a_decide_de}</p>
2026-05-25 17:39:38 -04:00
)}
{!decision.etant_donne_que && !decision.groupe_a_decide_de && (
<p className="text-sm text-gray-700 whitespace-pre-wrap">{decision.note}</p>
2026-05-25 17:39:38 -04:00
)}
</div>
2026-03-04 14:01:00 -05:00
)}
</>
)}
2026-03-04 14:01:00 -05:00
</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>
</div>
);
}