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

212 lines
11 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, datesReunion, dateLocale, labelDateReunion } from '../utils/datesReunion';
const TYPES_JETONS = [['desir', 'Désir'], ['24h', '24 heures'], ['1mois', '1 mois'], ['2mois', '2 mois'], ['3mois', '3 mois'], ['6mois', '6 mois'], ['9mois', '9 mois'], ['1an', '1 an'], ['multiple', 'Multiple']];
const INPUT = 'w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue';
export default function ReunionHebdomadaire() {
const { postes, groupe } = useStore();
const estExecutif = postes.some((p) => ['RSG', 'RSG Substitut(e)', 'Trésorier(ière)', 'Secrétaire'].includes(p));
const choixDates = datesReunion(groupe, { joursAvant: 365, joursApres: 180 });
const [membres, setMembres] = useState([]);
const [catalogue, setCatalogue] = useState([]);
const [reunions, setReunions] = useState([]);
const [dateSelection, setDateSelection] = useState(dateLocale());
const [dateReunion, setDateReunion] = useState(dateLocale());
const [theme, setTheme] = useState('');
const [annonces, setAnnonces] = useState('');
const [note, setNote] = useState('');
const [collecteMontant, setCollecteMontant] = useState('');
const [detenteurId, setDetenteurId] = useState('');
const [jetonType, setJetonType] = useState('');
const [jetonMembreId, setJetonMembreId] = useState('');
const [jetonAnnees, setJetonAnnees] = useState('');
const [litteratureId, setLitteratureId] = useState('');
const [quantite, setQuantite] = useState('1');
const [venteMontant, setVenteMontant] = useState('');
const [msg, setMsg] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [rapportVisible, setRapportVisible] = useState(false);
2026-05-25 17:39:38 -04:00
const charger = async () => {
try {
const [m, l, r] = await Promise.all([
api.get('/membres/actifs'),
api.get('/litterature/'),
api.get('/reunions/?limite=80'),
]);
setMembres(m);
setCatalogue(l);
setReunions(r.filter((x) => x.type === 'reguliere'));
if (m.length > 0 && !detenteurId) setDetenteurId(m[0].id);
} catch (e) { setError(e.message); }
};
useEffect(() => { charger(); }, []);
useEffect(() => {
if (choixDates.length > 0) {
if (!choixDates.includes(dateSelection)) setDateSelection(choixDates.find((d) => d >= dateLocale()) || choixDates[choixDates.length - 1]);
if (!choixDates.includes(dateReunion)) setDateReunion(choixDates.find((d) => d >= dateLocale()) || choixDates[choixDates.length - 1]);
}
}, [groupe?.jour_reunion]);
const rapport = reunions.find((r) => r.date_reunion?.slice(0, 10) === dateSelection);
const dateSimple = () => dateReunion;
const choisirLitterature = (id) => {
setLitteratureId(id);
const item = catalogue.find((c) => c.id === id);
if (item?.prix_coutant) setVenteMontant((item.prix_coutant * parseInt(quantite || '1')).toFixed(2));
};
const changerQuantite = (valeur) => {
setQuantite(valeur);
const item = catalogue.find((c) => c.id === litteratureId);
if (item?.prix_coutant && valeur) setVenteMontant((item.prix_coutant * parseInt(valeur)).toFixed(2));
};
const imprimer = () => window.print();
const enregistrer = async () => {
setError('');
setMsg('');
setLoading(true);
const faits = [];
try {
if (theme.trim() || annonces.trim() || note.trim()) {
const annoncesList = annonces.split('\n').map((a) => a.trim()).filter(Boolean);
await api.post('/reunions/', {
type: 'reguliere',
date_reunion: dateHeureReunionIso(dateReunion, groupe),
theme: theme.trim() || 'Rencontre hebdomadaire',
annonces: annoncesList.length ? annoncesList : null,
note: note.trim() || null,
});
faits.push('rapport');
}
if (collecteMontant) {
await api.post('/collectes/', { date_collecte: dateSimple(), montant: parseFloat(collecteMontant), detenteur_id: detenteurId });
faits.push('collecte');
}
if (jetonType) {
const body = { type_jeton: jetonType, date_remise: dateSimple() };
if (jetonMembreId) body.membre_id = jetonMembreId;
if (jetonType === 'multiple') body.annees = parseInt(jetonAnnees);
await api.post('/jetons/', body);
faits.push('jeton');
}
if (litteratureId) {
await api.post('/ventes-litterature/', { litterature_id: litteratureId, quantite: parseInt(quantite), montant: parseFloat(venteMontant), date_vente: dateSimple() });
faits.push('littérature');
}
if (faits.length === 0) setError('Remplissez au moins une section');
else {
setMsg(`Enregistré : ${faits.join(', ')}`);
setDateSelection(dateSimple());
await charger();
}
} catch (e) { setError(e.message); }
setLoading(false);
};
return (
<div className="min-h-screen bg-gray-100">
<PageHeader titre="Rencontre hebdomadaire" sousTitre="Assistant et rapport imprimable" />
<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>}
<Card titre="Rapport imprimable">
2026-05-25 17:39:38 -04:00
<Champ label="Date">
<SelectDate value={dateSelection} onChange={setDateSelection} dates={choixDates} />
</Champ>
<button onClick={() => setRapportVisible(true)} className="w-full p-3 bg-aa-blue text-white rounded-xl text-sm font-bold">
Produire le rapport
</button>
{rapportVisible && <button onClick={imprimer} className="w-full p-3 bg-gray-900 text-white rounded-xl text-sm font-bold">Imprimer format lettre</button>}
2026-05-25 17:39:38 -04:00
</Card>
{rapportVisible && <div className="bg-white rounded-2xl p-5 shadow-sm print:shadow-none print:rounded-none print:p-0">
2026-05-25 17:39:38 -04:00
<h2 className="text-xl font-bold text-gray-900">Rapport de réunion hebdomadaire</h2>
<p className="text-sm text-gray-500">{dateSelection}</p>
{rapport ? (
<div className="mt-4 space-y-3 text-sm">
<p><strong>Thème :</strong> {rapport.theme || '____________________________'}</p>
<div>
<strong>Annonces :</strong>
{(rapport.annonces || []).length > 0 ? (
<ul className="list-disc ml-5 mt-1">{rapport.annonces.map((a, i) => <li key={i}>{a}</li>)}</ul>
) : <p className="mt-1 whitespace-pre-wrap">{"\n\n\n"}</p>}
</div>
<p><strong>Notes :</strong> {rapport.note || '____________________________________________'}</p>
</div>
) : (
<div className="mt-4 space-y-4 text-sm">
<p><strong>Thème :</strong> ____________________________________________</p>
<p><strong>Annonces :</strong></p>
<div className="h-28 border border-gray-200 rounded-xl" />
<p><strong>Notes :</strong></p>
<div className="h-20 border border-gray-200 rounded-xl" />
</div>
)}
<div className="grid grid-cols-2 gap-4 mt-6 text-sm">
<p>Collecte : __________ $</p>
<p>Détenteur : _______________</p>
<p>Jeton : __________________</p>
<p>Littérature : __________ $</p>
</div>
</div>}
2026-05-25 17:39:38 -04:00
{estExecutif && (
<>
<Card titre="Renseigner le rapport">
<Champ label="Date"><SelectDate value={dateReunion} onChange={setDateReunion} dates={choixDates} /></Champ>
<Champ label="Thème"><input value={theme} onChange={(e) => setTheme(e.target.value)} placeholder="Thème abordé" className={INPUT} /></Champ>
<Champ label="Annonces"><textarea value={annonces} onChange={(e) => setAnnonces(e.target.value)} rows={3} placeholder="Une annonce par ligne" className={`${INPUT} resize-none`} /></Champ>
<Champ label="Notes"><textarea value={note} onChange={(e) => setNote(e.target.value)} rows={2} placeholder="Notes optionnelles" className={`${INPUT} resize-none`} /></Champ>
</Card>
<Card titre="Saisies liées">
<div className="grid grid-cols-2 gap-3">
<Champ label="Collecte"><input type="number" inputMode="decimal" step="0.01" value={collecteMontant} onChange={(e) => setCollecteMontant(e.target.value)} placeholder="0.00" className={INPUT} /></Champ>
<Champ label="Détenteur"><select value={detenteurId} onChange={(e) => setDetenteurId(e.target.value)} className={INPUT}>{membres.map((m) => <option key={m.id} value={m.id}>{m.prenom}</option>)}</select></Champ>
</div>
<Champ label="Jeton"><select value={jetonType} onChange={(e) => setJetonType(e.target.value)} className={INPUT}><option value="">Aucun jeton</option>{TYPES_JETONS.map(([value, label]) => <option key={value} value={value}>{label}</option>)}</select></Champ>
<Champ label="Membre jeton"><select value={jetonMembreId} onChange={(e) => setJetonMembreId(e.target.value)} className={INPUT}><option value="">Anonyme / non applicable</option>{membres.map((m) => <option key={m.id} value={m.id}>{m.prenom}</option>)}</select></Champ>
{jetonType === 'multiple' && <Champ label="Années"><input type="number" min="2" value={jetonAnnees} onChange={(e) => setJetonAnnees(e.target.value)} className={INPUT} /></Champ>}
<Champ label="Littérature"><select value={litteratureId} onChange={(e) => choisirLitterature(e.target.value)} className={INPUT}><option value="">Aucune vente</option>{catalogue.map((c) => <option key={c.id} value={c.id}>{c.titre} ({c.quantite_stock})</option>)}</select></Champ>
<div className="grid grid-cols-2 gap-3">
<Champ label="Quantité"><input type="number" min="1" value={quantite} onChange={(e) => changerQuantite(e.target.value)} className={INPUT} /></Champ>
<Champ label="Montant"><input type="number" inputMode="decimal" step="0.01" value={venteMontant} onChange={(e) => setVenteMontant(e.target.value)} placeholder="0.00" className={INPUT} /></Champ>
</div>
</Card>
<button onClick={enregistrer} disabled={loading} 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">{loading ? '...' : 'Enregistrer les sections remplies'}</button>
</>
)}
</div>
</div>
);
}
function Card({ titre, children }) {
return <div className="bg-white rounded-2xl p-4 shadow-sm space-y-3"><h2 className="font-bold text-gray-800">{titre}</h2>{children}</div>;
}
function Champ({ label, children }) {
return <label className="block"><span className="text-xs text-gray-400 block mb-1">{label}</span>{children}</label>;
}
function SelectDate({ value, onChange, dates }) {
if (!dates.length) {
return <div className="rounded-xl bg-gray-50 p-3 text-sm text-gray-500">Configurez le jour de réunion dans les paramètres du groupe.</div>;
}
return (
<select value={value} onChange={(e) => onChange(e.target.value)} className={INPUT}>
{dates.map((d) => <option key={d} value={d}>{labelDateReunion(d)}</option>)}
</select>
);
}