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

294 lines
14 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 [collectesToutes, setCollectesToutes] = useState([]);
const [collecteSelectionneeId, setCollecteSelectionneeId] = useState('');
2026-04-17 04:04:04 -04:00
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, toutes] = await Promise.all([
2026-04-17 04:04:04 -04:00
api.get('/membres/actifs'),
api.get(`/collectes/?mois=${mois}&annee=${annee}&limite=50`),
api.get('/collectes/?limite=500'),
2026-04-17 04:04:04 -04:00
]);
setMembres(mbrs);
setCollectesMois(cols);
setCollectesToutes(toutes);
2026-04-17 04:04:04 -04:00
if (membre && !detenteurId) setDetenteurId(membre.id);
if (!collecteSelectionneeId && toutes.length > 0) setCollecteSelectionneeId(toutes[0].id);
2026-04-17 04:04:04 -04:00
} 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 collecteSelectionnee = collectesToutes.find((c) => c.id === collecteSelectionneeId);
2026-04-17 04:04:04 -04:00
const MOIS_NOMS = ['', 'janvier', 'février', 'mars', 'avril', 'mai', 'juin',
'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'];
const dateLocale = (d = new Date()) => {
const an = d.getFullYear();
const mo = String(d.getMonth() + 1).padStart(2, '0');
const jo = String(d.getDate()).padStart(2, '0');
return `${an}-${mo}-${jo}`;
};
const labelDate = (d, avecAnnee = false) => {
const dt = new Date(d + 'T12:00:00');
return dt.toLocaleDateString('fr-CA', {
weekday: 'long',
day: 'numeric',
month: 'long',
...(avecAnnee ? { year: 'numeric' } : {}),
});
};
2026-04-17 04:04:04 -04:00
// 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 datesAvecCollecte = new Set(collectesToutes.map((c) => c.date_collecte));
2026-04-17 04:04:04 -04:00
const dates = [];
const d = new Date();
for (let i = 0; i < 180; i++) {
2026-04-17 04:04:04 -04:00
const test = new Date(d);
test.setDate(d.getDate() - i);
const valeur = dateLocale(test);
2026-04-17 04:04:04 -04:00
if (test.getDay() === jourReunion) {
if (!datesAvecCollecte.has(valeur)) dates.push(valeur);
2026-04-17 04:04:04 -04:00
}
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
useEffect(() => {
if (datesReunion.length > 0 && !datesReunion.includes(dateCollecte)) {
setDateCollecte(datesReunion[0]);
}
}, [collectesToutes, groupe?.jour_reunion]);
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>
{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>
)}
2026-04-17 04:04:04 -04:00
{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 label = labelDate(d);
const estAujourdhui = d === dateLocale();
2026-04-17 04:04:04 -04:00
return <option key={d} value={d}>{estAujourdhui ? '● Aujourd\'hui' : label}</option>;
})}
</select>
) : jourReunion === null ? (
2026-04-17 04:04:04 -04:00
<input type="date" value={dateCollecte} onChange={(e) => setDateCollecte(e.target.value)}
max={dateLocale()}
2026-04-17 04:04:04 -04:00
className="w-full p-3 bg-gray-50 rounded-xl text-lg border-0 focus:ring-2 focus:ring-aa-blue" />
) : (
<div className="rounded-xl bg-gray-50 p-3 text-sm text-gray-500">
Aucun {groupe?.jour_reunion || 'jour de réunion'} sans collecte enregistrée dans les derniers mois.
</div>
2026-04-17 04:04:04 -04:00
)}
<p className="text-xs text-gray-400 mt-1">
Seuls les {groupe?.jour_reunion || 'jours de réunion'} sans collecte enregistrée sont proposés.
</p>
2026-04-17 04:04:04 -04:00
</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 || (datesReunion.length === 0 && jourReunion !== null)}
2026-04-17 04:04:04 -04:00
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>
);
}