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

230 lines
10 KiB
React
Raw Normal View History

2026-04-17 04:04:04 -04:00
import React, { useState, useEffect } from 'react';
2026-03-04 14:01:00 -05:00
import { api } from '../stores/api';
2026-04-17 04:04:04 -04:00
import { useStore } from '../stores/store';
2026-03-04 14:01:00 -05:00
import PageHeader from '../components/PageHeader';
2026-04-17 04:04:04 -04:00
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' },
};
2026-03-04 14:01:00 -05:00
export default function Collecte() {
2026-04-17 04:04:04 -04:00
const { membre, groupe } = useStore();
2026-03-04 14:01:00 -05:00
const [montant, setMontant] = useState('');
const [dateCollecte, setDateCollecte] = useState(new Date().toISOString().slice(0, 10));
2026-04-17 04:04:04 -04:00
const [detenteurId, setDetenteurId] = useState('');
const [membres, setMembres] = useState([]);
const [collectesMois, setCollectesMois] = useState([]);
const [tab, setTab] = useState('mois');
2026-03-04 14:01:00 -05:00
const [loading, setLoading] = useState(false);
2026-04-17 04:04:04 -04:00
const [msg, setMsg] = useState('');
2026-03-04 14:01:00 -05:00
const [error, setError] = useState('');
2026-04-17 04:04:04 -04:00
const aujourdhui = new Date();
const mois = aujourdhui.getMonth() + 1;
const annee = aujourdhui.getFullYear();
const charger = async () => {
try {
const [mbrs, cols] = await Promise.all([
api.get('/membres/actifs'),
api.get(`/collectes/?mois=${mois}&annee=${annee}&limite=50`),
]);
setMembres(mbrs);
setCollectesMois(cols);
if (membre && !detenteurId) setDetenteurId(membre.id);
} catch (e) { setError(e.message); }
};
useEffect(() => { charger(); }, []);
const flash = (m) => { setMsg(m); setTimeout(() => setMsg(''), 2500); };
2026-03-04 14:01:00 -05:00
const handleSubmit = async () => {
2026-04-17 04:04:04 -04:00
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('');
2026-03-04 14:01:00 -05:00
try {
await api.post('/collectes/', {
date_collecte: dateCollecte,
montant: parseFloat(montant),
2026-04-17 04:04:04 -04:00
detenteur_id: detenteurId,
2026-03-04 14:01:00 -05:00
});
2026-04-17 04:04:04 -04:00
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 MOIS_NOMS = ['', 'janvier', 'février', 'mars', 'avril', 'mai', 'juin',
'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'];
// Calculer les dernières dates de réunion pour le sélecteur
const joursSemaine = { 'lundi': 1, 'mardi': 2, 'mercredi': 3, 'jeudi': 4, 'vendredi': 5, 'samedi': 6, 'dimanche': 0 };
const jourReunion = groupe?.jour_reunion ? joursSemaine[groupe.jour_reunion.toLowerCase()] : null;
const getDatesReunion = () => {
if (jourReunion === null) return [];
const dates = [];
const d = new Date();
for (let i = 0; i < 12; i++) {
const test = new Date(d);
test.setDate(d.getDate() - i);
if (test.getDay() === jourReunion) {
dates.push(test.toISOString().slice(0, 10));
}
}
// Aussi vérifier les 30 jours suivants dans le passé
for (let i = 12; i < 60; i++) {
const test = new Date(d);
test.setDate(d.getDate() - i);
if (test.getDay() === jourReunion) {
dates.push(test.toISOString().slice(0, 10));
}
2026-03-04 14:01:00 -05:00
}
2026-04-17 04:04:04 -04:00
return dates;
2026-03-04 14:01:00 -05:00
};
2026-04-17 04:04:04 -04:00
const datesReunion = getDatesReunion();
2026-03-04 14:01:00 -05:00
return (
<div className="min-h-screen bg-gray-100">
2026-04-17 04:04:04 -04:00
<PageHeader titre="Collecte 7e Tradition" sousTitre="S1 — Saisir et confirmer" />
2026-03-04 14:01:00 -05:00
<div className="p-4 space-y-4">
2026-04-17 04:04:04 -04:00
{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>}
2026-03-04 14:01:00 -05:00
2026-04-17 04:04:04 -04:00
<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>
2026-03-04 14:01:00 -05:00
</div>
2026-04-17 04:04:04 -04:00
{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>
{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>
{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 dt = new Date(d + 'T12:00:00');
const label = dt.toLocaleDateString('fr-CA', { weekday: 'long', day: 'numeric', month: 'long' });
const estAujourdhui = d === new Date().toISOString().slice(0, 10);
return <option key={d} value={d}>{estAujourdhui ? '● Aujourd\'hui' : label}</option>;
})}
</select>
) : (
<input type="date" value={dateCollecte} onChange={(e) => setDateCollecte(e.target.value)}
max={new Date().toISOString().slice(0, 10)}
className="w-full p-3 bg-gray-50 rounded-xl text-lg border-0 focus:ring-2 focus:ring-aa-blue" />
)}
<p className="text-xs text-gray-400 mt-1">Saisie rétroactive permise aux dates de réunion</p>
</div>
2026-03-04 14:01:00 -05:00
2026-04-17 04:04:04 -04:00
<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}
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>
)}
2026-03-04 14:01:00 -05:00
</div>
</div>
);
}