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([]);
|
2026-05-31 19:28:55 -04:00
|
|
|
|
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 {
|
2026-05-31 19:28:55 -04:00
|
|
|
|
const [data, stockJetons] = await Promise.all([
|
|
|
|
|
|
api.get('/litterature/'),
|
|
|
|
|
|
api.get('/commandes-jetons/stock'),
|
|
|
|
|
|
]);
|
2026-03-04 14:01:00 -05:00
|
|
|
|
setItems(data);
|
2026-05-31 19:28:55 -04:00
|
|
|
|
setJetons(stockJetons);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
setItems([]);
|
|
|
|
|
|
setJetons([]);
|
|
|
|
|
|
}
|
2026-03-04 14:01:00 -05:00
|
|
|
|
setLoading(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { charger(); }, []);
|
|
|
|
|
|
|
|
|
|
|
|
const ajusterStock = async (itemId, quantite) => {
|
|
|
|
|
|
setError('');
|
|
|
|
|
|
try {
|
2026-05-31 10:59:41 -04:00
|
|
|
|
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">
|
2026-05-31 19:28:55 -04:00
|
|
|
|
<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">
|
2026-05-31 19:28:55 -04:00
|
|
|
|
<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>
|
|
|
|
|
|
)}
|
2026-05-31 19:28:55 -04:00
|
|
|
|
</section>
|
2026-03-04 14:01:00 -05:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-31 19:28:55 -04:00
|
|
|
|
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>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|