groupe-meditation/frontend/src/pages/AssembleeAffaires.jsx
Daniel Allaire a8afaffc0e Refactoriser les pages d'assemblées
Extrait les utilitaires et composants UI propres aux assemblées pour alléger la page principale sans changer le comportement.

Centralise l'usage de estExecutif depuis le store dans les pages Assemblées, Rencontres, Événements et Décisions afin d'éviter les règles d'accès dupliquées côté frontend.
2026-05-31 09:15:12 -04:00

510 lines
28 KiB
JavaScript

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';
import { heure, moisPrecedent, textePvAuto } from '../features/assemblees/assembleeUtils';
import { boutonAction, Card, Champ, DecisionItem, INPUT, MembreSelect, SelectDate } from '../features/assemblees/assembleeUi';
const METHODES = [['VIREMENT', 'Virement'], ['CHEQUE', 'Chèque']];
export default function AssembleeAffaires({ contexte = false }) {
const { estExecutif, groupe } = useStore();
const choixDatesChrono = datesAssembleesAffaires(groupe);
const choixDates = [...choixDatesChrono].reverse();
const prochaineDate = choixDatesChrono.find((d) => d >= dateLocale()) || choixDatesChrono[choixDatesChrono.length - 1] || dateLocale();
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 [soldes, setSoldes] = useState(null);
const [destinataires, setDestinataires] = useState([]);
const [rapportsTresorerie, setRapportsTresorerie] = useState([]);
const [ouverteId, setOuverteId] = useState('');
const [action, setAction] = useState('');
const [planifier, setPlanifier] = useState(false);
const [dateReunion, setDateReunion] = useState(prochaineDate);
const [note, setNote] = useState('');
const [presencesMap, setPresencesMap] = useState({});
const [contenu, setContenu] = useState('');
const [pvPrecedent, setPvPrecedent] = useState(null);
const [pvCorrection, setPvCorrection] = useState('');
const [adoption, setAdoption] = useState({ proposeur_id: '', secondeur_id: '' });
const [don, setDon] = useState({ destination: 'District 87-16', montant: '', methode: 'VIREMENT', proposeur_id: '', secondeur_id: '' });
const [adoptionTresorier, setAdoptionTresorier] = useState({ proposeur_id: '', secondeur_id: '', note: '' });
const [invitationTexte, setInvitationTexte] = useState('');
const [posteOuvert, setPosteOuvert] = useState('');
const [posteMode, setPosteMode] = useState('');
const [motivation, setMotivation] = useState('');
const [membreProposeId, setMembreProposeId] = useState('');
const [nouvelleProp, setNouvelleProp] = useState({ sujet: '', description: '' });
const [resolutionForm, setResolutionForm] = useState({});
const [seconderPour, setSeconderPour] = useState({});
const [msg, setMsg] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const charger = async () => {
try {
const [m, r, pv, props, res, postesDispo, s, dests, rapports] = await Promise.all([
api.get('/membres/actifs'),
api.get('/presences/reunions'),
api.get('/pv/'),
api.get('/propositions/?limite=100'),
api.get('/propositions/resolutions'),
api.get('/postes/disponibles'),
api.get('/collectes/soldes'),
api.get('/contributions/destinataires'),
api.get('/rapports/adoptes?type_rapport=tresorerie'),
]);
setMembres(m);
setReunions(r);
setPvs(pv);
setPropositions(props);
setResolutions(res);
setPostesVacants(postesDispo);
setSoldes(s);
setDestinataires(dests);
setRapportsTresorerie(rapports);
const map = {};
m.forEach((mb) => { map[mb.id] = true; });
setPresencesMap(map);
const defaut = dests.find((x) => x.nom === 'District 87-16') || dests[0];
setDon((f) => ({
...f,
destination: f.destination || defaut?.nom || 'District 87-16',
montant: f.montant || (s?.disponible > 0 ? s.disponible.toFixed(2) : ''),
proposeur_id: f.proposeur_id || m[0]?.id || '',
secondeur_id: f.secondeur_id || m[1]?.id || '',
}));
setAdoptionTresorier((f) => ({
...f,
proposeur_id: f.proposeur_id || m[0]?.id || '',
secondeur_id: f.secondeur_id || m[1]?.id || '',
}));
if (!membreProposeId && m[0]) setMembreProposeId(m[0].id);
if (!ouverteId && r.length) setOuverteId(r[0].id);
} catch (e) {
setError(e.message);
}
};
useEffect(() => { charger(); }, []);
useEffect(() => {
if (choixDates.length > 0 && !choixDates.includes(dateReunion)) setDateReunion(prochaineDate);
}, [groupe?.jour_reunion, groupe?.assemblee_affaires_ordre]);
const reunionOuverte = reunions.find((r) => r.id === ouverteId);
const dateOuverte = reunionOuverte?.date_reunion?.slice(0, 10) || prochaineDate;
const pvJour = pvs.find((p) => p.date_reunion?.slice(0, 10) === dateOuverte);
const resolutionsJour = resolutions.filter((r) => (r.date_vote || r.cree_le?.slice(0, 10)) === dateOuverte);
const nbPresents = Object.values(presencesMap).filter(Boolean).length;
const periodeTresorier = moisPrecedent(dateOuverte);
const rapportTresorierAdopte = rapportsTresorerie.find((r) => r.annee === periodeTresorier.annee && r.mois === periodeTresorier.mois);
useEffect(() => {
if (!reunionOuverte) return;
const precedents = pvs
.filter((p) => p.date_reunion && p.date_reunion.slice(0, 10) < dateOuverte)
.sort((a, b) => b.date_reunion.localeCompare(a.date_reunion));
const precedent = precedents[0] || null;
setPvPrecedent(precedent);
setPvCorrection(precedent?.contenu || '');
setContenu(pvJour?.contenu || textePvAuto(precedent, propositions, resolutionsJour, postesVacants));
}, [ouverteId, pvs.length, propositions.length, resolutions.length, postesVacants.length]);
const ouvrir = (id) => {
setOuverteId(ouverteId === id ? '' : id);
setAction('');
};
const toggle = (membreId) => setPresencesMap({ ...presencesMap, [membreId]: !presencesMap[membreId] });
const telechargerRapport = async (assemblee) => {
setLoading(true);
setError('');
try {
const token = api.getToken();
const res = await fetch(`/api/presences/reunions/${assemblee.id}/rapport.pdf`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error('Erreur de génération du rapport PDF');
const blob = await res.blob();
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `rapport_assemblee_${assemblee.date_reunion?.slice(0, 10)}.pdf`;
a.click();
URL.revokeObjectURL(a.href);
} catch (e) {
setError(e.message);
} finally {
setLoading(false);
}
};
const creerRapport = async () => {
setError('');
setMsg('');
setLoading(true);
try {
const res = await api.post('/presences/reunion', {
date_reunion: dateHeureReunionIso(dateReunion, groupe),
note: note.trim() || null,
});
setMsg('Assemblée créée');
setPlanifier(false);
setNote('');
await charger();
setOuverteId(res.id);
} catch (e) {
setError(e.message);
}
setLoading(false);
};
const enregistrer = async () => {
if (!reunionOuverte) return;
setError('');
setMsg('');
setLoading(true);
try {
const presences = Object.entries(presencesMap).map(([membre_id, present]) => ({ membre_id, present }));
await api.post('/presences/', { reunion_id: reunionOuverte.id, presences });
if (contenu.trim()) {
await api.post('/pv/', {
reunion_id: reunionOuverte.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 enregistrerDon = async () => {
if (!rapportTresorierAdopte) { setError("Le rapport du trésorier doit être adopté avant de décider des contributions"); return; }
if (!don.montant || parseFloat(don.montant) <= 0) { setError('Montant invalide'); return; }
if (!don.proposeur_id || !don.secondeur_id) { setError('Proposeur et secondeur requis'); return; }
if (don.proposeur_id === don.secondeur_id) { setError('Le proposeur et le secondeur doivent être différents'); return; }
setLoading(true); setError('');
try {
await api.post('/contributions/', {
date_envoi: dateOuverte,
destination: don.destination,
montant: parseFloat(don.montant),
methode: don.methode,
proposeur_id: don.proposeur_id,
secondeur_id: don.secondeur_id,
});
setMsg('Don engagé');
setDon({ ...don, montant: '' });
await charger();
} catch (e) { setError(e.message); }
setLoading(false);
};
const adopterRapportTresorier = async () => {
if (!adoptionTresorier.proposeur_id || !adoptionTresorier.secondeur_id) { setError('Proposeur et secondeur requis'); return; }
setLoading(true); setError('');
try {
await api.post(`/rapports/etat-resultats/${periodeTresorier.annee}/${periodeTresorier.mois}/adopter`, adoptionTresorier);
setMsg('Rapport du trésorier adopté');
await charger();
} catch (e) { setError(e.message); }
setLoading(false);
};
const genererInvitation = async (assemblee) => {
setError('');
try {
const inv = await api.post(`/presences/reunions/${assemblee.id}/invitation`);
const url = `${window.location.origin}${inv.path}`;
setInvitationTexte([
`Invitation à l'assemblée d'affaires du ${labelDateReunion(assemblee.date_reunion.slice(0, 10))}`,
'',
"Vous pouvez consulter l'ordre du jour en lecture seule ici :",
url,
'',
`Lien valide jusqu'au ${new Date(inv.expire_le).toLocaleString('fr-CA')}.`,
].join('\n'));
setAction('Invitation');
} catch (e) { setError(e.message); }
};
const soumettreCandidature = async (posteId) => {
setError('');
try {
if (posteMode === 'proposer') {
await api.post(`/postes/candidatures/proposer?poste_id=${posteId}&membre_id=${membreProposeId}&motivation=${encodeURIComponent(motivation.trim())}`);
setMsg('Membre proposé');
} else {
await api.post(`/postes/candidatures?poste_id=${posteId}&motivation=${encodeURIComponent(motivation.trim())}`);
setMsg('Candidature soumise');
}
setPosteMode('');
setMotivation('');
await charger();
} catch (e) { setError(e.message); }
};
const soumettreProposition = async () => {
if (!nouvelleProp.sujet.trim()) { setError("L'objet est obligatoire"); return; }
setError('');
try {
await api.post('/propositions/', {
sujet: nouvelleProp.sujet.trim(),
description: nouvelleProp.description.trim() || null,
});
setNouvelleProp({ sujet: '', description: '' });
setMsg('Proposition soumise');
await charger();
} catch (e) { setError(e.message); }
};
const seconder = async (id, membreId = '') => {
setError('');
try {
const query = membreId ? `?membre_id=${membreId}` : '';
await api.post(`/propositions/${id}/seconder${query}`);
setMsg('Proposition secondée');
await charger();
} catch (e) { setError(e.message); }
};
const voter = async (id, statut) => {
const form = resolutionForm[id] || {};
const etantDonneQue = (form.etant_donne_que || '').trim();
const groupeADecideDe = (form.groupe_a_decide_de || '').trim();
if (!etantDonneQue || (statut === 'adoptee' && !groupeADecideDe)) {
setError(statut === 'adoptee' ? 'Les deux sections de décision sont obligatoires' : 'La section "ÉTANT DONNÉ QUE" est obligatoire');
return;
}
setError('');
try {
await api.patch(`/propositions/${id}/voter`, {
statut,
etant_donne_que: etantDonneQue,
groupe_a_decide_de: groupeADecideDe || null,
});
setResolutionForm({ ...resolutionForm, [id]: {} });
setMsg(statut === 'adoptee' ? 'Décision créée' : 'Proposition rejetée');
await charger();
} catch (e) { setError(e.message); }
};
const adopterPvPrecedent = async () => {
if (!pvPrecedent) return;
if (!adoption.proposeur_id || !adoption.secondeur_id) {
setError('Proposeur et secondeur requis');
return;
}
setError('');
try {
await api.patch(`/pv/${pvPrecedent.id}/adopter`, {
...adoption,
contenu: pvCorrection.trim(),
});
setMsg('PV précédent adopté');
await charger();
} catch (e) {
setError(e.message);
}
};
return (
<div className="min-h-screen bg-gray-100">
<PageHeader titre="Assemblées" sousTitre="Assemblées d'affaires et procès-verbaux" retour={!contexte} />
<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>}
{estExecutif && (
<button onClick={() => setPlanifier(!planifier)} className="w-full p-3 bg-aa-blue text-white rounded-xl text-sm font-bold">
Planifier une assemblée
</button>
)}
{planifier && estExecutif && (
<Card titre="Nouvelle assemblée">
<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} disabled={loading} className="w-full p-3 bg-green-600 text-white rounded-xl text-sm font-bold disabled:opacity-40">Créer</button>
</Card>
)}
<div className="space-y-3">
{reunions.length === 0 && (
<div className="rounded-2xl bg-white p-5 text-center text-sm text-gray-500 shadow-sm">
<p>Aucune assemblée planifiée.</p>
{estExecutif && <p className="mt-1 text-gray-400">Utilisez le bouton en haut de la page pour créer la première assemblée.</p>}
</div>
)}
{reunions.map((r) => {
const date = r.date_reunion?.slice(0, 10);
const ouvert = ouverteId === r.id;
const pv = pvs.find((p) => p.date_reunion?.slice(0, 10) === date);
return (
<div key={r.id} className={`bg-white rounded-2xl shadow-sm overflow-hidden ${ouvert ? 'print:block' : 'print:hidden'}`}>
<div className="flex items-start gap-2 p-4">
<button onClick={() => ouvrir(r.id)} className="min-w-0 flex-1 text-left active:bg-gray-50">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="font-extrabold text-gray-900">{labelDateReunion(date)} · {heure(r.date_reunion)}</p>
<p className="text-sm text-gray-500 truncate">{pv?.approuve ? 'PV adopté' : pv ? 'PV à adopter' : 'PV à renseigner'} · {r.presences?.filter((p) => p.present).length || 0} présence(s)</p>
</div>
<span className="text-xl text-gray-300">{ouvert ? '⌃' : '⌄'}</span>
</div>
</button>
<button onClick={() => telechargerRapport(r)} disabled={loading}
className="shrink-0 rounded-xl bg-gray-900 px-3 py-2 text-xs font-bold text-white disabled:opacity-40 print:hidden">
Rapport PDF
</button>
</div>
{ouvert && (
<div className="p-4 pt-0 space-y-3 border-t border-gray-100">
<div className="grid grid-cols-2 gap-2 print:hidden">
<button onClick={() => genererInvitation(r)} className={boutonAction(action === 'Invitation')}>Invitation</button>
{pvPrecedent && <button onClick={() => setAction(action === 'PV précédent' ? '' : 'PV précédent')} className={boutonAction(action === 'PV précédent')}>PV précédent</button>}
{estExecutif && <button onClick={() => setAction(action === 'Présences' ? '' : 'Présences')} className={boutonAction(action === 'Présences')}>Présences</button>}
{estExecutif && <button onClick={() => setAction(action === 'Trésorier' ? '' : 'Trésorier')} className={boutonAction(action === 'Trésorier')}>Rapport trésorier</button>}
<button onClick={() => setAction(action === 'Donner' ? '' : 'Donner')} className={boutonAction(action === 'Donner')}>Donner au suivant</button>
<button onClick={() => setAction(action === 'Postes' ? '' : 'Postes')} className={boutonAction(action === 'Postes')}>Postes à pourvoir</button>
<button onClick={() => setAction(action === 'Décisions' ? '' : 'Décisions')} className={boutonAction(action === 'Décisions')}>Décisions</button>
</div>
{action === 'PV précédent' && 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>
{estExecutif && !pvPrecedent.approuve ? (
<div className="space-y-2 pt-2 border-t border-gray-100">
<textarea value={pvCorrection} onChange={(e) => setPvCorrection(e.target.value)} rows={8} className={`${INPUT} resize-none text-sm`} />
<div className="grid grid-cols-2 gap-2">
<MembreSelect label="Proposeur" value={adoption.proposeur_id} onChange={(v) => setAdoption({ ...adoption, proposeur_id: v })} membres={membres} />
<MembreSelect label="Secondeur" value={adoption.secondeur_id} onChange={(v) => setAdoption({ ...adoption, secondeur_id: v })} membres={membres} />
</div>
<button onClick={adopterPvPrecedent} disabled={!adoption.proposeur_id || !adoption.secondeur_id} className="w-full p-3 bg-green-50 text-green-700 rounded-xl text-sm font-bold border border-green-100 disabled:opacity-40">Adopter</button>
</div>
) : (
<p className="text-sm whitespace-pre-wrap text-gray-700">{pvPrecedent.contenu}</p>
)}
</Card>
)}
{action === 'Invitation' && (
<Card titre="Invitation">
<textarea readOnly value={invitationTexte} rows={7} className={`${INPUT} resize-none text-sm`} />
<button onClick={() => navigator.clipboard?.writeText(invitationTexte)} className="w-full p-3 bg-aa-blue text-white rounded-xl text-sm font-bold">Copier le texte</button>
</Card>
)}
{action === 'Présences' && estExecutif && (
<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>
<button onClick={enregistrer} disabled={loading} className="w-full p-3 bg-green-600 text-white rounded-xl text-sm font-bold disabled:opacity-40">Enregistrer les présences</button>
</Card>
)}
{action === 'Trésorier' && estExecutif && (
<Card titre={`Rapport du trésorier - ${periodeTresorier.libelle}`}>
{rapportTresorierAdopte ? (
<p className="text-sm text-green-700 bg-green-50 rounded-xl p-3">Rapport adopté. Les contributions peuvent être décidées.</p>
) : (
<>
<p className="text-sm text-gray-600">Ce rapport doit être adopté avant de décider du montant des contributions.</p>
<div className="grid grid-cols-2 gap-2">
<Champ label="Proposeur"><MembreSelect label="Proposeur" value={adoptionTresorier.proposeur_id} onChange={(v) => setAdoptionTresorier({ ...adoptionTresorier, proposeur_id: v })} membres={membres} /></Champ>
<Champ label="Secondeur"><MembreSelect label="Secondeur" value={adoptionTresorier.secondeur_id} onChange={(v) => setAdoptionTresorier({ ...adoptionTresorier, secondeur_id: v })} membres={membres} /></Champ>
</div>
<Champ label="Correction ou note"><textarea value={adoptionTresorier.note} onChange={(e) => setAdoptionTresorier({ ...adoptionTresorier, note: e.target.value })} rows={2} className={`${INPUT} resize-none text-sm`} /></Champ>
<button onClick={adopterRapportTresorier} disabled={loading} className="w-full p-3 bg-green-600 text-white rounded-xl text-sm font-bold disabled:opacity-40">Adopter le rapport</button>
</>
)}
</Card>
)}
{action === 'Donner' && (
<Card titre="Donner au suivant">
{!rapportTresorierAdopte && <p className="text-sm text-red-700 bg-red-50 rounded-xl p-3">Adoptez d'abord le rapport du trésorier.</p>}
{soldes && <p className="text-sm text-blue-700 bg-blue-50 rounded-xl p-3">Disponible : <span className="font-bold">{soldes.disponible.toFixed(2)} $</span></p>}
<Champ label="Destinataire"><select value={don.destination} onChange={(e) => setDon({ ...don, destination: e.target.value })} className={INPUT}>{destinataires.map((d) => <option key={d.id} value={d.nom}>{d.nom}</option>)}</select></Champ>
<div className="grid grid-cols-2 gap-2">
<Champ label="Montant"><input type="number" inputMode="decimal" step="0.01" value={don.montant} onChange={(e) => setDon({ ...don, montant: e.target.value })} className={INPUT} /></Champ>
<Champ label="Méthode"><select value={don.methode} onChange={(e) => setDon({ ...don, methode: e.target.value })} className={INPUT}>{METHODES.map(([v, l]) => <option key={v} value={v}>{l}</option>)}</select></Champ>
</div>
<div className="grid grid-cols-2 gap-2">
<Champ label="Proposeur"><MembreSelect label="Proposeur" value={don.proposeur_id} onChange={(v) => setDon({ ...don, proposeur_id: v })} membres={membres} /></Champ>
<Champ label="Secondeur"><MembreSelect label="Secondeur" value={don.secondeur_id} onChange={(v) => setDon({ ...don, secondeur_id: v })} membres={membres} /></Champ>
</div>
<button onClick={enregistrerDon} disabled={loading || !rapportTresorierAdopte} className="w-full p-3 bg-green-600 text-white rounded-xl text-sm font-bold disabled:opacity-40">Créer l'envoi</button>
</Card>
)}
{action === 'Postes' && (
<Card titre="Postes à pourvoir">
{postesVacants.length === 0 ? <p className="text-sm text-gray-500">Aucun poste vacant.</p> : postesVacants.map((p) => {
const ouvertPoste = posteOuvert === p.id;
return (
<div key={p.id} className="rounded-xl bg-gray-50 overflow-hidden">
<button onClick={() => { setPosteOuvert(ouvertPoste ? '' : p.id); setPosteMode(''); }} className="w-full p-3 text-left">
<p className="font-bold text-gray-800">{p.nom}</p>
{p.description && <p className="text-sm text-gray-500 line-clamp-2">{p.description}</p>}
</button>
{ouvertPoste && (
<div className="p-3 pt-0 space-y-2">
{p.candidats?.length ? <p className="text-xs text-gray-500">Candidats : {p.candidats.map((c) => c.prenom).join(', ')}</p> : <p className="text-xs text-gray-500">Aucun candidat.</p>}
{posteMode === 'proposer' && <MembreSelect label="Membre" value={membreProposeId} onChange={setMembreProposeId} membres={membres} />}
{posteMode && <textarea value={motivation} onChange={(e) => setMotivation(e.target.value)} rows={2} placeholder="Motivation optionnelle" className={`${INPUT} resize-none text-sm`} />}
{posteMode ? (
<div className="grid grid-cols-2 gap-2">
<button onClick={() => soumettreCandidature(p.id)} className="p-2 bg-green-50 text-green-700 rounded-xl text-sm font-bold">Soumettre</button>
<button onClick={() => { setPosteMode(''); setMotivation(''); }} className="p-2 bg-gray-100 text-gray-600 rounded-xl text-sm font-bold">Annuler</button>
</div>
) : (
<div className="grid grid-cols-2 gap-2">
<button onClick={() => setPosteMode('postuler')} className="p-2 bg-yellow-50 text-yellow-700 rounded-xl text-sm font-bold">Postuler</button>
<button onClick={() => setPosteMode('proposer')} className="p-2 bg-blue-50 text-blue-700 rounded-xl text-sm font-bold">Proposer</button>
</div>
)}
</div>
)}
</div>
);
})}
</Card>
)}
{action === 'Décisions' && (
<Card titre="Décisions">
<Champ label="Objet"><input value={nouvelleProp.sujet} onChange={(e) => setNouvelleProp({ ...nouvelleProp, sujet: e.target.value })} className={INPUT} placeholder="Nouvelle proposition" /></Champ>
<Champ label="Détails"><textarea value={nouvelleProp.description} onChange={(e) => setNouvelleProp({ ...nouvelleProp, description: e.target.value })} rows={2} className={`${INPUT} resize-none text-sm`} /></Champ>
<button onClick={soumettreProposition} className="w-full p-3 bg-aa-blue text-white rounded-xl text-sm font-bold">Soumettre la proposition</button>
<div className="space-y-2 pt-2 border-t border-gray-100">
{propositions.filter((p) => ['proposee', 'secondee', 'rejetee'].includes(p.statut)).map((p) => (
<DecisionItem key={p.id} p={p} membres={membres} estExecutif={estExecutif} form={resolutionForm[p.id] || {}} setForm={(champ, valeur) => setResolutionForm({ ...resolutionForm, [p.id]: { ...(resolutionForm[p.id] || {}), [champ]: valeur } })} seconderPour={seconderPour[p.id] || ''} setSeconderPour={(v) => setSeconderPour({ ...seconderPour, [p.id]: v })} seconder={seconder} voter={voter} />
))}
{propositions.filter((p) => ['proposee', 'secondee', 'rejetee'].includes(p.statut)).length === 0 && <p className="text-sm text-gray-500">Aucune proposition à traiter.</p>}
</div>
</Card>
)}
</div>
)}
</div>
);
})}
</div>
</div>
</div>
);
}