208 lines
11 KiB
React
208 lines
11 KiB
React
|
|
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 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="Consulter le rapport">
|
||
|
|
<Champ label="Date">
|
||
|
|
<SelectDate value={dateSelection} onChange={setDateSelection} dates={choixDates} />
|
||
|
|
</Champ>
|
||
|
|
<button onClick={imprimer} className="w-full p-3 bg-gray-900 text-white rounded-xl text-sm font-bold">Imprimer format lettre</button>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<div className="bg-white rounded-2xl p-5 shadow-sm print:shadow-none print:rounded-none print:p-0">
|
||
|
|
<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>
|
||
|
|
|
||
|
|
{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>
|
||
|
|
);
|
||
|
|
}
|