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 (
{msg &&
{msg}
} {error &&
{error}
} {estExecutif && ( )} {planifier && estExecutif && ( setNote(e.target.value)} placeholder="Note optionnelle" className={INPUT} /> )}
{reunions.length === 0 && (

Aucune assemblée planifiée.

{estExecutif &&

Utilisez le bouton en haut de la page pour créer la première assemblée.

}
)} {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 (
{ouvert && (
{pvPrecedent && } {estExecutif && } {estExecutif && }
{action === 'PV précédent' && pvPrecedent && (

{new Date(pvPrecedent.date_reunion).toLocaleDateString('fr-CA')}

{estExecutif && !pvPrecedent.approuve ? (