groupe-meditation/frontend/src/pages/AjusterInventaire.jsx
Daniel Allaire 1ee392e25d Ajouter scenario demo resilience
- fermer les tuiles d assemblees par defaut

- unifier la gestion des inventaires litterature et jetons

- enrichir les donnees demo sur 12 mois avec rencontres, assemblees, rotations et journalisation
2026-05-31 19:28:55 -04:00

293 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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([]);
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'),
]);
setItems(data);
setJetons(stockJetons);
} catch {
setItems([]);
setJetons([]);
}
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 });
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" />
<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>
{/* 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>
</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>
);
}
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>
);
}