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>
|
|
|
|
|
);
|
|
|
|
|
}
|