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

294 lines
10 KiB
React
Raw Normal View History

2026-03-04 14:01:00 -05:00
import React, { useState, useEffect } from 'react';
import { api } from '../stores/api';
import PageHeader from '../components/PageHeader';
import SuccessCheck from '../components/SuccessCheck';
const CATEGORIES = [
{ value: 'livre', label: 'Livre', emoji: '📕' },
{ value: 'brochure', label: 'Brochure', emoji: '📄' },
{ value: 'depliant', label: 'Dépliant', emoji: '📃' },
{ value: 'pamphlet', label: 'Pamphlet', emoji: '📑' },
];
export default function AjusterInventaire() {
const [items, setItems] = useState([]);
const [jetons, setJetons] = useState([]);
2026-03-04 14:01:00 -05:00
const [loading, setLoading] = useState(true);
const [success, setSuccess] = useState('');
const [error, setError] = useState('');
const [showAjout, setShowAjout] = useState(false);
const charger = async () => {
try {
const [data, stockJetons] = await Promise.all([
api.get('/litterature/'),
api.get('/commandes-jetons/stock'),
]);
2026-03-04 14:01:00 -05:00
setItems(data);
setJetons(stockJetons);
} catch {
setItems([]);
setJetons([]);
}
2026-03-04 14:01:00 -05:00
setLoading(false);
};
useEffect(() => { charger(); }, []);
const ajusterStock = async (itemId, quantite) => {
setError('');
try {
const item = items.find((i) => i.id === itemId);
await api.patch(`/litterature/${itemId}/stock`, { quantite }, { version: item?.modifie_le });
2026-03-04 14:01:00 -05:00
setSuccess('Stock ajusté ✓');
setTimeout(() => { setSuccess(''); charger(); }, 1000);
} catch (e) {
setError(e.message);
}
};
if (success) return <SuccessCheck message={success} delai={1000} />;
return (
<div className="min-h-screen bg-gray-100">
<PageHeader titre="Gestion des inventaires" sousTitre="Littérature et jetons" />
2026-03-04 14:01:00 -05:00
<div className="p-4 space-y-4">
<section className="space-y-2">
<h2 className="text-xs font-bold text-gray-400 uppercase tracking-wider px-1">Jetons</h2>
<StockJetons stock={jetons} loading={loading} />
</section>
<section className="space-y-2">
<h2 className="text-xs font-bold text-gray-400 uppercase tracking-wider px-1">Littérature</h2>
2026-03-04 14:01:00 -05:00
{/* Bouton ajouter un titre */}
<button
onClick={() => setShowAjout(!showAjout)}
className="w-full p-3 bg-purple-100 text-purple-700 rounded-2xl text-sm font-bold active:bg-purple-200 transition"
>
{showAjout ? '✕ Annuler' : '+ Ajouter un titre au catalogue'}
</button>
{showAjout && <FormAjoutTitre onSuccess={() => { setShowAjout(false); charger(); }} />}
{error && <p className="text-red-500 text-sm text-center bg-red-50 p-3 rounded-lg">{error}</p>}
{loading && <p className="text-center text-gray-400 py-8">Chargement...</p>}
{/* Liste des titres avec ajustement */}
{items.map((item) => (
<ItemStock key={item.id} item={item} onAjuster={ajusterStock} />
))}
{!loading && items.length === 0 && (
<div className="text-center py-12">
<span className="text-4xl block mb-2">📦</span>
<p className="text-gray-400">Aucun titre dans le catalogue</p>
</div>
)}
</section>
2026-03-04 14:01:00 -05:00
</div>
</div>
);
}
function StockJetons({ stock, loading }) {
const aCommander = stock.filter((s) => s.stock_suggestion !== 'OK');
if (loading) return null;
return (
<div className="space-y-2">
{aCommander.length > 0 && (
<div className="bg-orange-50 border-2 border-orange-300 rounded-2xl p-4">
<p className="font-bold text-orange-700 text-sm mb-2">
Jetons à commander ({aCommander.length})
</p>
<div className="space-y-1">
{aCommander.map((s) => (
<p key={s.type_jeton} className="text-sm text-orange-600">
{s.label} — {s.remis_dernier_mois} remis ce mois
</p>
))}
</div>
</div>
)}
{stock.map((s) => (
<div
key={s.type_jeton}
className={`bg-white rounded-2xl p-4 shadow-sm flex items-center gap-3 border-2 ${
s.stock_suggestion !== 'OK' ? 'border-orange-300' : 'border-gray-100'
}`}
>
<span className="text-2xl">{s.label.split(' ')[0]}</span>
<div className="flex-1 min-w-0">
<p className="font-bold text-sm">{s.label.split(' ').slice(1).join(' ')}</p>
<p className="text-xs text-gray-400">{s.remis_dernier_mois} remis ou vendu récemment</p>
</div>
<span className={`text-xs font-bold px-3 py-1 rounded-full ${
s.stock_suggestion === 'OK'
? 'bg-green-100 text-green-700'
: 'bg-orange-100 text-orange-700'
}`}>
{s.stock_suggestion}
</span>
</div>
))}
{stock.length === 0 && (
<div className="text-center py-8 bg-white rounded-2xl shadow-sm">
<span className="text-4xl block mb-2">🏅</span>
<p className="text-gray-400">Aucune donnée de jetons pour le moment</p>
</div>
)}
</div>
);
}
2026-03-04 14:01:00 -05:00
function ItemStock({ item, onAjuster }) {
const [ajustement, setAjustement] = useState('');
return (
<div className={`bg-white rounded-2xl p-4 shadow-sm border-2 ${
item.stock_bas ? 'border-orange-300' : 'border-gray-100'
}`}>
<div className="flex items-center gap-3 mb-3">
<div className="flex-1 min-w-0">
<p className="font-bold text-sm truncate">{item.titre}</p>
<p className="text-xs text-gray-400">{item.categorie}</p>
</div>
<div className="text-center">
<p className={`text-2xl font-bold ${item.stock_bas ? 'text-orange-600' : 'text-gray-700'}`}>
{item.quantite_stock}
</p>
<p className="text-xs text-gray-400">en stock</p>
</div>
</div>
{/* Ajustement rapide */}
<div className="flex gap-2">
<button
onClick={() => onAjuster(item.id, -1)}
className="w-12 h-12 bg-red-100 text-red-700 rounded-xl text-xl font-bold active:bg-red-200"
>
−
</button>
<div className="flex-1 flex gap-1">
<input
type="number"
inputMode="numeric"
placeholder="+5"
value={ajustement}
onChange={(e) => setAjustement(e.target.value)}
className="flex-1 p-2 bg-gray-50 rounded-xl text-center border-0 focus:ring-2 focus:ring-purple-400"
/>
<button
onClick={() => {
if (ajustement && parseInt(ajustement) !== 0) {
onAjuster(item.id, parseInt(ajustement));
setAjustement('');
}
}}
disabled={!ajustement}
className="px-4 bg-purple-600 text-white rounded-xl text-sm font-bold disabled:opacity-40 active:bg-purple-700"
>
OK
</button>
</div>
<button
onClick={() => onAjuster(item.id, 1)}
className="w-12 h-12 bg-green-100 text-green-700 rounded-xl text-xl font-bold active:bg-green-200"
>
+
</button>
</div>
</div>
);
}
function FormAjoutTitre({ onSuccess }) {
const [form, setForm] = useState({
titre: '', categorie: 'livre', prix_coutant: '', quantite_stock: '0', code_bsg: '',
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const update = (k, v) => setForm({ ...form, [k]: v });
const handleSubmit = async () => {
if (!form.titre.trim()) { setError('Le titre est obligatoire'); return; }
setLoading(true);
setError('');
try {
await api.post('/litterature/', {
titre: form.titre.trim(),
categorie: form.categorie,
prix_coutant: form.prix_coutant ? parseFloat(form.prix_coutant) : null,
quantite_stock: parseInt(form.quantite_stock) || 0,
code_bsg: form.code_bsg.trim() || null,
});
onSuccess();
} catch (e) {
setError(e.message);
setLoading(false);
}
};
return (
<div className="bg-purple-50 rounded-2xl p-4 space-y-3 border-2 border-purple-200">
<input
type="text"
placeholder="Titre (ex: Gros Livre, 12x12...)"
value={form.titre}
onChange={(e) => update('titre', e.target.value)}
className="w-full p-3 bg-white rounded-xl border-0 focus:ring-2 focus:ring-purple-400"
autoFocus
/>
<div className="grid grid-cols-2 gap-2">
{CATEGORIES.map((c) => (
<button
key={c.value}
type="button"
onClick={() => update('categorie', c.value)}
className={`p-2 rounded-xl text-xs font-bold border-2 ${
form.categorie === c.value
? 'bg-purple-600 text-white border-purple-600'
: 'bg-white text-gray-600 border-gray-200'
}`}
>
{c.emoji} {c.label}
</button>
))}
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="text-xs text-gray-400 block mb-1">Prix coûtant ($)</label>
<input
type="number" inputMode="decimal" step="0.01" placeholder="0.00"
value={form.prix_coutant} onChange={(e) => update('prix_coutant', e.target.value)}
className="w-full p-2 bg-white rounded-xl border-0 focus:ring-2 focus:ring-purple-400 text-center"
/>
</div>
<div>
<label className="text-xs text-gray-400 block mb-1">Stock initial</label>
<input
type="number" inputMode="numeric" min="0" placeholder="0"
value={form.quantite_stock} onChange={(e) => update('quantite_stock', e.target.value)}
className="w-full p-2 bg-white rounded-xl border-0 focus:ring-2 focus:ring-purple-400 text-center"
/>
</div>
</div>
<input
type="text" placeholder="Code BSG (optionnel)"
value={form.code_bsg} onChange={(e) => update('code_bsg', e.target.value)}
className="w-full p-2 bg-white rounded-xl border-0 focus:ring-2 focus:ring-purple-400"
/>
{error && <p className="text-red-500 text-xs">{error}</p>}
<button
onClick={handleSubmit}
disabled={loading || !form.titre.trim()}
className="w-full p-3 bg-purple-600 text-white rounded-xl font-bold disabled:opacity-40 active:scale-[0.97] transition"
>
{loading ? '...' : '📚 Ajouter au catalogue'}
</button>
</div>
);
}