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

511 lines
28 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';
import { heure, moisPrecedent, textePvAuto } from '../features/assemblees/assembleeUtils';
import { boutonAction, Card, Champ, DecisionItem, INPUT, MembreSelect, SelectDate } from '../features/assemblees/assembleeUi';
2026-05-25 17:39:38 -04:00
const METHODES = [['VIREMENT', 'Virement'], ['CHEQUE', 'Chèque']];
2026-05-25 17:39:38 -04:00
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();
2026-05-25 17:39:38 -04:00
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);
2026-05-25 17:39:38 -04:00
const [note, setNote] = useState('');
const [presencesMap, setPresencesMap] = useState({});
const [contenu, setContenu] = useState('');
const [pvPrecedent, setPvPrecedent] = useState(null);
const [pvCorrection, setPvCorrection] = useState('');
2026-05-26 09:05:58 -04:00
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({});
2026-05-25 17:39:38 -04:00
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([
2026-05-25 17:39:38 -04:00
api.get('/membres/actifs'),
api.get('/presences/reunions'),
api.get('/pv/'),
api.get('/propositions/?limite=100'),
2026-05-25 17:39:38 -04:00
api.get('/propositions/resolutions'),
api.get('/postes/disponibles'),
api.get('/collectes/soldes'),
api.get('/contributions/destinataires'),
api.get('/rapports/adoptes?type_rapport=tresorerie'),
2026-05-25 17:39:38 -04:00
]);
setMembres(m);
setReunions(r);
setPvs(pv);
setPropositions(props);
setResolutions(res);
setPostesVacants(postesDispo);
setSoldes(s);
setDestinataires(dests);
setRapportsTresorerie(rapports);
2026-05-25 17:39:38 -04:00
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);
}
2026-05-25 17:39:38 -04:00
};
useEffect(() => { charger(); }, []);
useEffect(() => {
if (choixDates.length > 0 && !choixDates.includes(dateReunion)) setDateReunion(prochaineDate);
2026-05-25 17:39:38 -04:00
}, [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);
2026-05-25 17:39:38 -04:00
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]);
2026-05-25 17:39:38 -04:00
const ouvrir = (id) => {
setOuverteId(ouverteId === id ? '' : id);
setAction('');
};
2026-05-25 17:39:38 -04:00
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);
}
};
2026-05-25 17:39:38 -04:00
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);
2026-05-25 17:39:38 -04:00
};
const enregistrer = async () => {
if (!reunionOuverte) return;
2026-05-25 17:39:38 -04:00
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 });
2026-05-25 17:39:38 -04:00
if (contenu.trim()) {
await api.post('/pv/', {
reunion_id: reunionOuverte.id,
2026-05-25 17:39:38 -04:00
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();
2026-05-25 17:39:38 -04:00
} 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); }
};
2026-05-25 17:39:38 -04:00
const adopterPvPrecedent = async () => {
if (!pvPrecedent) return;
2026-05-26 09:05:58 -04:00
if (!adoption.proposeur_id || !adoption.secondeur_id) {
setError('Proposeur et secondeur requis');
return;
}
2026-05-25 17:39:38 -04:00
setError('');
try {
await api.patch(`/pv/${pvPrecedent.id}/adopter`, {
...adoption,
contenu: pvCorrection.trim(),
});
2026-05-25 17:39:38 -04:00
setMsg('PV précédent adopté');
await charger();
} catch (e) {
setError(e.message);
}
2026-05-25 17:39:38 -04:00
};
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>
);
}