groupe-meditation/frontend/src/pages/Collecte.jsx
2026-05-25 17:39:38 -04:00

266 lines
13 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import { api } from '../stores/api';
import { useStore } from '../stores/store';
import PageHeader from '../components/PageHeader';
import { dateLocale, dernieresDatesReunion, indexJourReunion, labelDateReunion } from '../utils/datesReunion';
const STATUT = {
en_attente: { label: 'À confirmer', cls: 'bg-yellow-100 text-yellow-700' },
confirmee: { label: 'Confirmée', cls: 'bg-blue-100 text-blue-700' },
recu: { label: '✓ Reçue', cls: 'bg-green-100 text-green-700' },
};
export default function Collecte() {
const { membre, groupe, estSysadmin } = useStore();
const [montant, setMontant] = useState('');
const [dateCollecte, setDateCollecte] = useState(new Date().toISOString().slice(0, 10));
const [detenteurId, setDetenteurId] = useState('');
const [membres, setMembres] = useState([]);
const [collectesMois, setCollectesMois] = useState([]);
const [collectesToutes, setCollectesToutes] = useState([]);
const [collecteSelectionneeId, setCollecteSelectionneeId] = useState('');
const [tab, setTab] = useState('mois');
const [loading, setLoading] = useState(false);
const [msg, setMsg] = useState('');
const [error, setError] = useState('');
const aujourdhui = new Date();
const mois = aujourdhui.getMonth() + 1;
const annee = aujourdhui.getFullYear();
const charger = async () => {
try {
const [mbrs, cols, toutes] = await Promise.all([
api.get('/membres/actifs'),
api.get(`/collectes/?mois=${mois}&annee=${annee}&limite=50`),
api.get('/collectes/?limite=500'),
]);
setMembres(mbrs);
setCollectesMois(cols);
setCollectesToutes(toutes);
if (membre && !detenteurId) setDetenteurId(membre.id);
if (!collecteSelectionneeId && toutes.length > 0) setCollecteSelectionneeId(toutes[0].id);
} catch (e) { setError(e.message); }
};
useEffect(() => { charger(); }, []);
const flash = (m) => { setMsg(m); setTimeout(() => setMsg(''), 2500); };
const handleSubmit = async () => {
if (!montant || parseFloat(montant) <= 0) { setError('Entrez un montant valide'); return; }
if (!detenteurId) { setError("Sélectionnez qui a l'argent"); return; }
if (!dateCollecte) { setError("Sélectionnez une date"); return; }
setLoading(true); setError('');
try {
await api.post('/collectes/', {
date_collecte: dateCollecte,
montant: parseFloat(montant),
detenteur_id: detenteurId,
});
setMontant('');
flash('Collecte enregistrée ✓');
setTab('mois');
await charger();
} catch (e) { setError(e.message); }
setLoading(false);
};
const confirmer = async (id) => {
setError('');
try {
await api.post(`/collectes/${id}/confirmer`);
flash('Montant confirmé ✓');
await charger();
} catch (e) { setError(e.message); }
};
const totalMois = collectesMois.reduce((sum, c) => sum + parseFloat(c.montant), 0);
const collecteSelectionnee = collectesToutes.find((c) => c.id === collecteSelectionneeId);
const MOIS_NOMS = ['', 'janvier', 'février', 'mars', 'avril', 'mai', 'juin',
'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'];
const labelDate = (d, avecAnnee = false) => {
return labelDateReunion(d, avecAnnee);
};
const jourReunion = indexJourReunion(groupe);
const datesAvecCollecte = new Set(collectesToutes.map((c) => c.date_collecte));
const datesReunion = dernieresDatesReunion(groupe, { nombre: 52, exclure: datesAvecCollecte });
useEffect(() => {
if (!estSysadmin && datesReunion.length > 0 && !datesReunion.includes(dateCollecte)) {
setDateCollecte(datesReunion[0]);
}
}, [collectesToutes, groupe?.jour_reunion, estSysadmin]);
return (
<div className="min-h-screen bg-gray-100">
<PageHeader titre="Collecte 7e Tradition" sousTitre="S1 — Saisir et confirmer" />
<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>}
<div className="flex gap-2">
<button onClick={() => { setTab('mois'); setError(''); }}
className={`flex-1 py-2 rounded-xl text-sm font-bold transition ${tab === 'mois' ? 'bg-aa-blue text-white' : 'bg-white text-gray-500'}`}>
📋 {MOIS_NOMS[mois]} ({collectesMois.length})
</button>
<button onClick={() => { setTab('saisie'); setError(''); }}
className={`flex-1 py-2 rounded-xl text-sm font-bold transition ${tab === 'saisie' ? 'bg-aa-blue text-white' : 'bg-white text-gray-500'}`}>
💰 Saisir
</button>
</div>
{tab === 'mois' && (
<>
<div className="bg-white rounded-2xl p-4 shadow-sm">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-gray-400">Total {MOIS_NOMS[mois]} {annee}</p>
<p className="text-2xl font-bold text-green-700">{totalMois.toFixed(2)} $</p>
</div>
<div className="text-right">
<p className="text-xs text-gray-400">{collectesMois.length} collecte{collectesMois.length > 1 ? 's' : ''}</p>
</div>
</div>
</div>
{collectesToutes.length > 0 && (
<div className="bg-white rounded-2xl p-4 shadow-sm space-y-3">
<div>
<label className="text-xs text-gray-400 block mb-1">Toutes les collectes</label>
<select value={collecteSelectionneeId} onChange={(e) => setCollecteSelectionneeId(e.target.value)}
className="w-full p-3 bg-gray-50 rounded-xl text-sm border-0 focus:ring-2 focus:ring-aa-blue">
{collectesToutes.map((c) => (
<option key={c.id} value={c.id}>
{c.date_collecte} · {parseFloat(c.montant).toFixed(2)} $ · {STATUT[c.statut]?.label || c.statut}
</option>
))}
</select>
</div>
{collecteSelectionnee && (
<div className="rounded-xl bg-gray-50 p-3 text-sm">
<div className="flex items-center justify-between gap-2">
<p className="font-bold text-gray-800">{labelDate(collecteSelectionnee.date_collecte, true)}</p>
<span className={`text-xs px-2 py-1 rounded-full font-medium ${STATUT[collecteSelectionnee.statut]?.cls || STATUT.en_attente.cls}`}>
{STATUT[collecteSelectionnee.statut]?.label || collecteSelectionnee.statut}
</span>
</div>
<p className="mt-1 text-lg font-bold text-green-700">{parseFloat(collecteSelectionnee.montant).toFixed(2)} $</p>
<p className="text-xs text-gray-500">
Saisi par {collecteSelectionnee.saisi_par_prenom || '—'} · Chez {collecteSelectionnee.detenteur_prenom || '—'}
</p>
{collecteSelectionnee.confirme_par_prenom && (
<p className="text-xs text-blue-500">Confirmé par {collecteSelectionnee.confirme_par_prenom}</p>
)}
{collecteSelectionnee.recu_par_prenom && (
<p className="text-xs text-green-500">Reçu par {collecteSelectionnee.recu_par_prenom}</p>
)}
</div>
)}
</div>
)}
{collectesMois.length === 0 ? (
<div className="text-center py-12">
<p className="text-4xl mb-2">💰</p>
<p className="text-gray-500">Aucune collecte ce mois-ci</p>
</div>
) : (
<div className="space-y-3">
{collectesMois.map((c) => {
const st = STATUT[c.statut] || STATUT.en_attente;
const peutConfirmer = c.statut === 'en_attente' && c.saisi_par_prenom !== membre?.prenom;
return (
<div key={c.id} className="bg-white rounded-2xl p-4 shadow-sm">
<div className="flex items-start justify-between gap-2">
<div className="flex-1">
<p className="font-bold text-gray-800 text-lg">{parseFloat(c.montant).toFixed(2)} $</p>
<p className="text-xs text-gray-500 mt-1">📅 {c.date_collecte}</p>
<p className="text-xs text-gray-400">
Saisi par <span className="font-medium">{c.saisi_par_prenom}</span>
{' · '}Chez <span className="font-medium">{c.detenteur_prenom}</span>
</p>
{c.confirme_par_prenom && <p className="text-xs text-blue-500">Confirmé par {c.confirme_par_prenom}</p>}
{c.recu_par_prenom && <p className="text-xs text-green-500">Reçu par {c.recu_par_prenom}</p>}
</div>
<span className={`text-xs px-2 py-1 rounded-full font-medium whitespace-nowrap ${st.cls}`}>{st.label}</span>
</div>
{peutConfirmer && (
<button onClick={() => confirmer(c.id)}
className="w-full mt-3 py-2 bg-yellow-50 text-yellow-700 rounded-xl text-sm font-bold border border-yellow-200 active:scale-[0.97] transition">
✋ Confirmer le montant
</button>
)}
</div>
);
})}
</div>
)}
</>
)}
{tab === 'saisie' && (
<div className="space-y-4">
<div className="bg-white rounded-2xl p-4 shadow-sm">
<label className="text-xs text-gray-400 block mb-1">Date de la réunion</label>
{estSysadmin ? (
<input type="date" value={dateCollecte} onChange={(e) => setDateCollecte(e.target.value)}
className="w-full p-3 bg-gray-50 rounded-xl text-lg border-0 focus:ring-2 focus:ring-aa-blue" />
) : datesReunion.length > 0 ? (
<select value={dateCollecte} onChange={(e) => setDateCollecte(e.target.value)}
className="w-full p-3 bg-gray-50 rounded-xl text-lg border-0 focus:ring-2 focus:ring-aa-blue">
{datesReunion.map((d) => {
const label = labelDate(d);
const estAujourdhui = d === dateLocale();
return <option key={d} value={d}>{estAujourdhui ? '● Aujourd\'hui' : label}</option>;
})}
</select>
) : (
<div className="rounded-xl bg-gray-50 p-3 text-sm text-gray-500">
{jourReunion === null
? "Configurez d'abord le jour de réunion dans les paramètres du groupe."
: `Aucune des 52 dernières rencontres n'est sans collecte enregistrée.`}
</div>
)}
<p className="text-xs text-gray-400 mt-1">
{estSysadmin
? 'Le sysadmin peut renseigner une date antérieure.'
: 'Seules les dates des 52 dernières rencontres sans collecte enregistrée sont proposées.'}
</p>
</div>
<div className="bg-white rounded-2xl p-4 shadow-sm">
<label className="text-xs text-gray-400 block mb-1">Montant collecté</label>
<div className="relative">
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-2xl text-gray-400">$</span>
<input type="number" inputMode="decimal" step="0.01" min="0" placeholder="0.00"
value={montant} onChange={(e) => setMontant(e.target.value)}
className="w-full p-4 pl-10 bg-gray-50 rounded-xl text-3xl font-bold text-center border-0 focus:ring-2 focus:ring-green-500"
autoFocus />
</div>
</div>
<div className="bg-white rounded-2xl p-4 shadow-sm">
<label className="text-xs text-gray-400 block mb-1">Qui a l'argent?</label>
<select value={detenteurId} onChange={(e) => setDetenteurId(e.target.value)}
className="w-full p-3 bg-gray-50 rounded-xl text-lg border-0 focus:ring-2 focus:ring-aa-blue">
<option value="">— Sélectionner —</option>
{membres.map((m) => (
<option key={m.id} value={m.id}>{m.prenom}{m.id === membre?.id ? ' (moi)' : ''}</option>
))}
</select>
</div>
<button onClick={handleSubmit} disabled={loading || !montant || !detenteurId || (!estSysadmin && datesReunion.length === 0 && jourReunion !== null)}
className="w-full p-5 bg-green-600 text-white rounded-2xl text-xl font-bold disabled:opacity-40 active:scale-[0.97] transition shadow-lg">
{loading ? '...' : '💰 Enregistrer la collecte'}
</button>
</div>
)}
</div>
</div>
);
}