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>
|
|
|
|
|
);
|
|
|
|
|
}
|