- 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
293 lines
10 KiB
JavaScript
293 lines
10 KiB
JavaScript
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>
|
||
);
|
||
}
|