266 lines
13 KiB
JavaScript
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>
|
|
);
|
|
}
|