Files
gestion-stock/public/index.html
T
2026-05-28 09:35:43 +00:00

1052 lines
32 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gestion de Stock — Delvarre Creations</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<style>
/* ── Reset & base ─────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f7fafa;
color: #555454;
font-size: 16px;
line-height: 1.5;
-webkit-text-size-adjust: 100%;
}
/* ── Header ───────────────────────────────────────────────────── */
.header {
background: #fff;
border-bottom: 1px solid #e0e6e6;
padding: 16px 24px;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 100;
}
.header h1 {
font-size: 20px;
font-weight: 700;
color: #555454;
margin: 0;
}
.header .subtitle {
font-size: 13px;
color: #aaa;
margin-left: 12px;
}
.btn-logout {
background: none;
border: 1px solid #d0dcdc;
border-radius: 6px;
padding: 6px 14px;
font-size: 13px;
color: #999;
cursor: pointer;
font-family: inherit;
}
.btn-logout:hover { border-color: #aac5c5; color: #555; }
/* ── Tabs ─────────────────────────────────────────────────────── */
.tabs {
display: flex;
background: #fff;
border-bottom: 2px solid #e0e6e6;
padding: 0 24px;
gap: 0;
position: sticky;
top: 61px;
z-index: 99;
}
.tab {
padding: 14px 24px;
font-size: 15px;
font-weight: 600;
color: #999;
cursor: pointer;
border-bottom: 3px solid transparent;
margin-bottom: -2px;
transition: color 0.2s, border-color 0.2s;
user-select: none;
}
.tab:hover { color: #555; }
.tab.active {
color: #555454;
border-bottom-color: #aac5c5;
}
/* ── Content ──────────────────────────────────────────────────── */
.content {
max-width: 960px;
margin: 0 auto;
padding: 24px;
}
.panel { display: none; }
.panel.active { display: block; }
/* ── Section card ─────────────────────────────────────────────── */
.section {
background: #fff;
border: 1px solid #e0e6e6;
border-radius: 10px;
padding: 20px 24px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
}
.section h2 {
font-size: 17px;
font-weight: 700;
color: #555454;
margin: 0 0 16px;
padding-bottom: 10px;
border-bottom: 2px solid #e0e6e6;
text-transform: uppercase;
letter-spacing: 0.6px;
}
/* ── Product row (Stock tab) ──────────────────────────────────── */
.product-list { display: flex; flex-direction: column; gap: 8px; }
.product-row {
display: flex;
align-items: center;
gap: 14px;
padding: 10px 14px;
border: 1px solid #f0f0f0;
border-radius: 8px;
background: #fafbfc;
transition: border-color 0.2s;
}
.product-row:hover { border-color: #d0dcdc; }
.product-row img {
width: 56px;
height: 56px;
object-fit: cover;
border-radius: 6px;
flex-shrink: 0;
background: #eee;
}
.product-row .no-img {
width: 56px;
height: 56px;
border-radius: 6px;
flex-shrink: 0;
background: #eee;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
color: #bbb;
}
.product-row .info {
flex: 1;
min-width: 0;
}
.product-row .name {
font-weight: 600;
font-size: 15px;
color: #555454;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.product-row .sku {
font-size: 12px;
color: #aaa;
}
.product-row .stock-val {
font-size: 20px;
font-weight: 700;
color: #555454;
min-width: 50px;
text-align: center;
}
.product-row .stock-val.low { color: #e53935; }
.product-row .stock-val.zero { color: #c62828; }
/* ── Mouvement tab ────────────────────────────────────────────── */
.mouvement-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.form-row {
display: flex;
gap: 14px;
align-items: flex-end;
flex-wrap: wrap;
}
.field {
display: flex;
flex-direction: column;
gap: 5px;
flex: 1;
min-width: 160px;
}
.field label {
font-size: 13px;
font-weight: 700;
color: #999;
text-transform: uppercase;
letter-spacing: 0.4px;
}
.field select,
.field input {
padding: 10px 14px;
font-size: 16px;
border: 1px solid #d0dcdc;
border-radius: 6px;
background: #fafbfc;
color: #555454;
font-family: inherit;
width: 100%;
transition: border-color 0.2s, box-shadow 0.2s;
}
.field select:focus,
.field input:focus {
outline: none;
border-color: #aac5c5;
background: #fff;
box-shadow: 0 0 0 3px rgba(170,197,197,0.2);
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
opacity: 1;
cursor: pointer;
}
.btn-primary {
background: #aac5c5;
color: #fff;
border: none;
border-radius: 6px;
padding: 12px 28px;
font-size: 15px;
font-weight: 700;
cursor: pointer;
font-family: inherit;
transition: background 0.2s;
white-space: nowrap;
}
.btn-primary:hover { background: #8ab0b0; }
.btn-primary:disabled { background: #ccc; cursor: not-allowed; }
.btn-secondary {
background: #f0f5f5;
color: #555454;
border: 1px solid #d0dcdc;
border-radius: 6px;
padding: 12px 28px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
font-family: inherit;
transition: background 0.2s, border-color 0.2s;
white-space: nowrap;
}
.btn-secondary:hover { background: #e0eaea; border-color: #aac5c5; }
.motif-chips {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.motif-chip {
padding: 8px 16px;
border: 2px solid #e0e6e6;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
color: #777;
cursor: pointer;
transition: all 0.15s;
user-select: none;
background: #fff;
}
.motif-chip:hover { border-color: #aac5c5; color: #555; }
.motif-chip.selected {
border-color: #aac5c5;
background: #f0f5f5;
color: #555454;
}
.type-toggle {
display: flex;
border: 2px solid #e0e6e6;
border-radius: 8px;
overflow: hidden;
}
.type-btn {
flex: 1;
padding: 10px 20px;
font-size: 15px;
font-weight: 600;
text-align: center;
cursor: pointer;
border: none;
background: #fff;
color: #999;
font-family: inherit;
transition: all 0.15s;
}
.type-btn.active-entree { background: #e8f5e9; color: #2e7d32; }
.type-btn.active-sortie { background: #fce4ec; color: #c62828; }
/* ── Inventaire tab ───────────────────────────────────────────── */
.inventaire-row {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
border: 1px solid #f0f0f0;
border-radius: 8px;
background: #fafbfc;
margin-bottom: 6px;
}
.inventaire-row img {
width: 56px;
height: 56px;
object-fit: cover;
border-radius: 6px;
flex-shrink: 0;
}
.inventaire-row .no-img {
width: 56px;
height: 56px;
border-radius: 6px;
flex-shrink: 0;
background: #eee;
}
.inventaire-row .info {
flex: 1;
min-width: 0;
}
.inventaire-row .name {
font-weight: 600;
font-size: 14px;
color: #555;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.inventaire-row .current {
font-size: 12px;
color: #aaa;
}
.inventaire-row input {
width: 100px;
padding: 8px 12px;
font-size: 17px;
font-weight: 600;
text-align: center;
border: 1px solid #d0dcdc;
border-radius: 6px;
background: #fff;
color: #555454;
font-family: inherit;
}
.inventaire-row input:focus {
outline: none;
border-color: #aac5c5;
box-shadow: 0 0 0 3px rgba(170,197,197,0.2);
}
.inventaire-row .diff {
min-width: 60px;
text-align: center;
font-size: 14px;
font-weight: 700;
}
.inventaire-row .diff.plus { color: #2e7d32; }
.inventaire-row .diff.minus { color: #c62828; }
.inventaire-row .diff.zero { color: #ccc; }
.inventaire-row input::-webkit-inner-spin-button,
.inventaire-row input::-webkit-outer-spin-button {
opacity: 1;
cursor: pointer;
}
.inventaire-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 16px;
}
/* ── History table ────────────────────────────────────────────── */
.history-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.history-table th {
text-align: left;
font-size: 12px;
font-weight: 700;
color: #aaa;
text-transform: uppercase;
letter-spacing: 0.4px;
padding: 8px 10px;
border-bottom: 2px solid #e0e6e6;
}
.history-table td {
padding: 8px 10px;
border-bottom: 1px solid #f0f0f0;
vertical-align: middle;
}
.history-table tr:last-child td { border-bottom: none; }
.badge-entree {
display: inline-block;
padding: 2px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 700;
background: #e8f5e9;
color: #2e7d32;
}
.badge-sortie {
display: inline-block;
padding: 2px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 700;
background: #fce4ec;
color: #c62828;
}
/* ── Pagination ───────────────────────────────────────────────── */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
margin-top: 18px;
padding-top: 14px;
border-top: 1px solid #f0f0f0;
}
.pagination button {
padding: 8px 20px;
font-size: 14px;
font-weight: 600;
border: 1px solid #d0dcdc;
border-radius: 6px;
background: #fff;
color: #555454;
cursor: pointer;
font-family: inherit;
transition: all 0.15s;
}
.pagination button:hover { border-color: #aac5c5; background: #f0f5f5; }
.pagination button:disabled { opacity: 0.4; cursor: not-allowed; }
.pagination .page-info {
font-size: 13px;
color: #999;
}
/* ── Toast notification ───────────────────────────────────────── */
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: #333;
color: #fff;
padding: 14px 28px;
border-radius: 10px;
font-size: 15px;
font-weight: 600;
z-index: 1000;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
transition: transform 0.3s ease;
white-space: nowrap;
}
.toast.visible { transform: translateX(-50%) translateY(0); }
.toast.success { background: #2e7d32; }
.toast.error { background: #c62828; }
/* ── Edit note ───────────────────────────────────────────────── */
.note-cell { display: flex; align-items: center; gap: 6px; }
.note-text { flex: 1; }
.btn-edit-note {
background: none;
border: 1px solid #e0e6e6;
border-radius: 4px;
padding: 2px 8px;
font-size: 11px;
color: #aaa;
cursor: pointer;
font-family: inherit;
white-space: nowrap;
flex-shrink: 0;
}
.btn-edit-note:hover { border-color: #aac5c5; color: #555; }
.note-edit-input {
flex: 1;
padding: 4px 8px;
font-size: 13px;
border: 1px solid #aac5c5;
border-radius: 4px;
font-family: inherit;
color: #555;
}
.note-edit-input:focus { outline: none; box-shadow: 0 0 0 2px rgba(170,197,197,0.3); }
.btn-save-note {
background: #aac5c5;
border: none;
border-radius: 4px;
padding: 4px 10px;
font-size: 11px;
font-weight: 700;
color: #fff;
cursor: pointer;
font-family: inherit;
white-space: nowrap;
flex-shrink: 0;
}
.btn-save-note:hover { background: #8ab0b0; }
/* ── Loading ──────────────────────────────────────────────────── */
.loading {
text-align: center;
padding: 40px;
color: #aaa;
font-size: 15px;
}
/* ── Responsive ───────────────────────────────────────────────── */
@media (max-width: 680px) {
.header { padding: 12px 16px; }
.header h1 { font-size: 17px; }
.header .subtitle { display: none; }
.tabs { padding: 0 8px; }
.tab { padding: 12px 16px; font-size: 14px; }
.content { padding: 16px; }
.section { padding: 16px; }
.form-row { flex-direction: column; }
.product-row img { width: 44px; height: 44px; }
.product-row .stock-val { font-size: 18px; min-width: 44px; }
.product-row .name { font-size: 14px; }
}
</style>
</head>
<body>
<div class="header">
<div style="display:flex;align-items:baseline;">
<h1>Gestion de Stock</h1>
<span class="subtitle">Delvarre Creations</span>
</div>
<button class="btn-logout" onclick="logout()">Deconnexion</button>
</div>
<div class="tabs">
<div class="tab active" data-tab="stock">Stock</div>
<div class="tab" data-tab="mouvement">Mouvement</div>
<div class="tab" data-tab="historique">Historique</div>
<div class="tab" data-tab="inventaire">Inventaire</div>
</div>
<div class="content">
<!-- ═══ STOCK TAB ═══════════════════════════════════════════ -->
<div class="panel active" id="panel-stock">
<div class="section">
<h2>Stock actuel</h2>
<div id="stock-list" class="product-list">
<div class="loading">Chargement...</div>
</div>
</div>
</div>
<!-- ═══ MOUVEMENT TAB ══════════════════════════════════════ -->
<div class="panel" id="panel-mouvement">
<div class="section">
<h2>Enregistrer un mouvement</h2>
<div class="mouvement-form">
<div class="field">
<label>Produit</label>
<select id="mv-produit">
<option value="">-- Choisir un produit --</option>
</select>
</div>
<div class="form-row">
<div class="field" style="max-width:140px;">
<label>Quantite</label>
<input type="number" id="mv-qty" min="1" value="1" inputmode="numeric">
</div>
<div class="field">
<label>Motif</label>
<div class="motif-chips" id="mv-motifs">
<div class="motif-chip selected" data-motif="Réception">Reception</div>
<div class="motif-chip" data-motif="Défectueux">Defectueux</div>
<div class="motif-chip" data-motif="Ajustement">Ajustement</div>
</div>
</div>
</div>
<div class="field">
<label>Type</label>
<div class="type-toggle" id="mv-type-toggle">
<div class="type-btn active-entree" data-type="Entrée">+ Entree</div>
<div class="type-btn" data-type="Sortie">- Sortie</div>
</div>
</div>
<div class="field">
<label>Note (facultatif)</label>
<input type="text" id="mv-note" placeholder="Ex : SAV fournisseur">
</div>
<div style="display:flex;gap:12px;">
<button class="btn-primary" id="mv-submit" onclick="submitMouvement()">Valider le mouvement</button>
</div>
</div>
</div>
</div>
<!-- ═══ INVENTAIRE TAB ═════════════════════════════════════ -->
<div class="panel" id="panel-inventaire">
<div class="section">
<h2>Inventaire physique</h2>
<p style="font-size:14px;color:#999;margin:-8px 0 16px;">
Saisissez le stock reel de chaque produit. Le systeme calculera et enregistrera les ecarts.
</p>
<div id="inventaire-list"></div>
<div class="inventaire-actions">
<button class="btn-secondary" onclick="resetInventaire()">Reinitialiser</button>
<button class="btn-primary" id="inv-submit" onclick="submitInventaire()">Valider l'inventaire</button>
</div>
</div>
</div>
<!-- ═══ HISTORIQUE TAB ═════════════════════════════════════ -->
<div class="panel" id="panel-historique">
<div class="section">
<h2>Historique des mouvements</h2>
<div id="history-container">
<div class="loading">Chargement...</div>
</div>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
(function() {
'use strict';
let products = [];
let currentType = 'Entrée';
let currentMotif = 'Réception';
let historyPage = 1;
var PAGE_SIZE = 50;
// ── Tabs ──────────────────────────────────────────────────
document.querySelectorAll('.tab').forEach(function(tab) {
tab.addEventListener('click', function() {
document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
document.querySelectorAll('.panel').forEach(function(p) { p.classList.remove('active'); });
tab.classList.add('active');
document.getElementById('panel-' + tab.dataset.tab).classList.add('active');
if (tab.dataset.tab === 'historique') loadHistory();
if (tab.dataset.tab === 'stock') loadProducts();
if (tab.dataset.tab === 'inventaire') buildInventaire();
if (tab.dataset.tab === 'mouvement') populateProductSelect();
});
});
// ── Toast ─────────────────────────────────────────────────
function toast(msg, type) {
var el = document.getElementById('toast');
el.textContent = msg;
el.className = 'toast ' + (type || '') + ' visible';
setTimeout(function() { el.classList.remove('visible'); }, 3000);
}
// ── API helper ────────────────────────────────────────────
async function api(method, url, body) {
var opts = { method: method, headers: { 'Content-Type': 'application/json' } };
if (body) opts.body = JSON.stringify(body);
var res = await fetch(url, opts);
if (res.status === 401) { window.location.href = '/login.html'; return null; }
return res.json();
}
// ── Load products ─────────────────────────────────────────
async function loadProducts() {
var data = await api('GET', '/api/produits');
if (!data || !data.list) return;
products = data.list;
renderStock();
}
function renderStock() {
var container = document.getElementById('stock-list');
if (!products.length) {
container.innerHTML = '<div class="loading">Aucun produit</div>';
return;
}
var totalStock = 0;
var html = '';
products.forEach(function(p) {
var stock = p.Stock || 0;
totalStock += stock;
var cls = stock === 0 ? 'zero' : (stock <= 5 ? 'low' : '');
var imgHtml = p.Image
? '<img src="' + p.Image + '" alt="' + esc(p.Nom) + '" loading="lazy">'
: '<div class="no-img">--</div>';
html += '<div class="product-row">'
+ imgHtml
+ '<div class="info">'
+ '<div class="name">' + esc(p.Nom) + '</div>'
+ '<div class="sku">' + esc(p.SKU) + '</div>'
+ '</div>'
+ '<div class="stock-val ' + cls + '">' + stock + '</div>'
+ '</div>';
});
html += '<div style="display:flex;justify-content:flex-end;padding:12px 14px 0;font-size:14px;color:#999;">'
+ products.length + ' references — <strong style="color:#555454;margin-left:4px;">' + totalStock + ' produits en stock</strong>'
+ '</div>';
container.innerHTML = html;
}
// ── Mouvement ─────────────────────────────────────────────
function populateProductSelect() {
var sel = document.getElementById('mv-produit');
var current = sel.value;
sel.innerHTML = '<option value="">-- Choisir un produit --</option>';
products.forEach(function(p) {
sel.innerHTML += '<option value="' + p.Id + '" data-sku="' + esc(p.SKU) + '">'
+ esc(p.Nom) + ' (' + esc(p.SKU) + ') — stock : ' + (p.Stock || 0)
+ '</option>';
});
if (current) sel.value = current;
}
// Type toggle (respects motif constraints)
document.querySelectorAll('#mv-type-toggle .type-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
setType(btn.dataset.type);
});
});
// Motif → type constraints
var motifTypeRules = {
'Réception': 'Entrée',
'Défectueux': 'Sortie',
'Ajustement': null // both allowed
};
function setType(type) {
currentType = type;
document.querySelectorAll('#mv-type-toggle .type-btn').forEach(function(b) {
b.className = 'type-btn';
if (b.dataset.type === type) {
b.classList.add(type === 'Entrée' ? 'active-entree' : 'active-sortie');
}
});
}
function updateTypeToggle() {
var forced = motifTypeRules[currentMotif];
var btns = document.querySelectorAll('#mv-type-toggle .type-btn');
if (forced) {
setType(forced);
btns.forEach(function(b) {
b.style.opacity = b.dataset.type === forced ? '1' : '0.3';
b.style.pointerEvents = b.dataset.type === forced ? 'auto' : 'none';
});
} else {
btns.forEach(function(b) {
b.style.opacity = '1';
b.style.pointerEvents = 'auto';
});
}
}
// Motif chips
document.querySelectorAll('#mv-motifs .motif-chip').forEach(function(chip) {
chip.addEventListener('click', function() {
document.querySelectorAll('#mv-motifs .motif-chip').forEach(function(c) {
c.classList.remove('selected');
});
chip.classList.add('selected');
currentMotif = chip.dataset.motif;
updateTypeToggle();
});
});
// Init: apply constraint for default motif
updateTypeToggle();
window.submitMouvement = async function() {
var sel = document.getElementById('mv-produit');
var productId = parseInt(sel.value);
var sku = sel.selectedOptions[0] ? sel.selectedOptions[0].dataset.sku : '';
var qty = parseInt(document.getElementById('mv-qty').value) || 0;
var note = document.getElementById('mv-note').value.trim();
if (!productId) { toast('Choisissez un produit', 'error'); return; }
if (qty <= 0) { toast('La quantite doit etre positive', 'error'); return; }
var btn = document.getElementById('mv-submit');
btn.disabled = true;
btn.textContent = 'Enregistrement...';
var res = await api('POST', '/api/mouvement', {
productId: productId,
sku: sku,
type: currentType,
quantite: qty,
source: currentMotif,
reference: note,
});
btn.disabled = false;
btn.textContent = 'Valider le mouvement';
if (res && res.ok) {
toast(currentType + ' de ' + qty + ' enregistree — nouveau stock : ' + res.newStock, 'success');
// Refresh products
await loadProducts();
populateProductSelect();
// Reset form
document.getElementById('mv-qty').value = 1;
document.getElementById('mv-note').value = '';
} else {
toast('Erreur : ' + (res ? res.error : 'connexion'), 'error');
}
};
// ── Inventaire ────────────────────────────────────────────
function buildInventaire() {
var container = document.getElementById('inventaire-list');
if (!products.length) {
container.innerHTML = '<div class="loading">Chargement...</div>';
return;
}
var html = '';
products.forEach(function(p, i) {
var imgHtml = p.Image
? '<img src="' + p.Image + '" alt="" loading="lazy">'
: '<div class="no-img"></div>';
html += '<div class="inventaire-row" data-id="' + p.Id + '" data-sku="' + esc(p.SKU) + '" data-stock="' + (p.Stock || 0) + '">'
+ imgHtml
+ '<div class="info">'
+ '<div class="name">' + esc(p.Nom) + '</div>'
+ '<div class="current">Stock actuel : ' + (p.Stock || 0) + '</div>'
+ '</div>'
+ '<input type="number" class="inv-input" min="0" value="' + (p.Stock || 0) + '" inputmode="numeric" data-idx="' + i + '">'
+ '<div class="diff zero" id="diff-' + i + '">—</div>'
+ '</div>';
});
container.innerHTML = html;
// Bind change events
container.querySelectorAll('.inv-input').forEach(function(input) {
input.addEventListener('input', function() {
var idx = parseInt(input.dataset.idx);
var row = input.closest('.inventaire-row');
var current = parseInt(row.dataset.stock) || 0;
var real = parseInt(input.value);
var diffEl = document.getElementById('diff-' + idx);
if (isNaN(real)) {
diffEl.textContent = '—';
diffEl.className = 'diff zero';
} else {
var d = real - current;
if (d > 0) {
diffEl.textContent = '+' + d;
diffEl.className = 'diff plus';
} else if (d < 0) {
diffEl.textContent = '' + d;
diffEl.className = 'diff minus';
} else {
diffEl.textContent = '=';
diffEl.className = 'diff zero';
}
}
});
});
}
window.resetInventaire = function() {
buildInventaire();
};
window.submitInventaire = async function() {
var rows = document.querySelectorAll('#inventaire-list .inventaire-row');
var items = [];
rows.forEach(function(row) {
var input = row.querySelector('.inv-input');
var real = parseInt(input.value);
var current = parseInt(row.dataset.stock) || 0;
if (!isNaN(real) && real !== current) {
items.push({
productId: parseInt(row.dataset.id),
sku: row.dataset.sku,
realStock: real,
});
}
});
if (items.length === 0) {
toast('Aucun ecart detecte', 'error');
return;
}
var btn = document.getElementById('inv-submit');
btn.disabled = true;
btn.textContent = 'Enregistrement...';
var res = await api('POST', '/api/inventaire', { items: items });
btn.disabled = false;
btn.textContent = "Valider l'inventaire";
if (res && res.ok) {
var changes = res.results.filter(function(r) { return r.diff; });
toast(changes.length + ' produit(s) ajuste(s)', 'success');
await loadProducts();
buildInventaire();
} else {
toast('Erreur : ' + (res ? res.error : 'connexion'), 'error');
}
};
// ── History ───────────────────────────────────────────────
async function loadHistory(page) {
if (page !== undefined) historyPage = page;
var container = document.getElementById('history-container');
container.innerHTML = '<div class="loading">Chargement...</div>';
var offset = (historyPage - 1) * PAGE_SIZE;
var data = await api('GET', '/api/mouvements?limit=' + PAGE_SIZE + '&offset=' + offset);
if (!data || !data.list) {
container.innerHTML = '<div class="loading">Erreur de chargement</div>';
return;
}
if (data.list.length === 0 && historyPage === 1) {
container.innerHTML = '<div class="loading">Aucun mouvement enregistre</div>';
return;
}
var totalRows = data.pageInfo ? data.pageInfo.totalRows : 0;
var totalPages = Math.ceil(totalRows / PAGE_SIZE) || 1;
var html = '<table class="history-table"><thead><tr>'
+ '<th>Date</th><th>Produit</th><th>Type</th><th>Qte</th><th>Motif</th><th>Note</th>'
+ '</tr></thead><tbody>';
data.list.forEach(function(m) {
var date = m.Date ? new Date(m.Date).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : '—';
var skuLabel = esc(m._sku || '—');
var typeBadge = m.Type === 'Entrée'
? '<span class="badge-entree">+ Entree</span>'
: '<span class="badge-sortie">- Sortie</span>';
var noteVal = m['Référence'] || m.Reference || '';
var mId = m.Id || '';
html += '<tr>'
+ '<td>' + date + '</td>'
+ '<td>' + skuLabel + '</td>'
+ '<td>' + typeBadge + '</td>'
+ '<td style="font-weight:700;">' + (m['Quantité'] || m.Quantite || '—') + '</td>'
+ '<td>' + esc(m.Source || '—') + '</td>'
+ '<td style="font-size:13px;color:#999;">'
+ '<div class="note-cell" id="note-' + mId + '">'
+ '<span class="note-text">' + esc(noteVal) + '</span>'
+ '<button class="btn-edit-note" onclick="editNote(' + mId + ', this)">modifier</button>'
+ '</div></td>'
+ '</tr>';
});
html += '</tbody></table>';
if (totalPages > 1) {
html += '<div class="pagination">'
+ '<button onclick="historyPrev()"' + (historyPage <= 1 ? ' disabled' : '') + '>Precedent</button>'
+ '<span class="page-info">Page ' + historyPage + ' / ' + totalPages + '</span>'
+ '<button onclick="historyNext()"' + (historyPage >= totalPages ? ' disabled' : '') + '>Suivant</button>'
+ '</div>';
}
container.innerHTML = html;
}
window.historyPrev = function() { if (historyPage > 1) loadHistory(historyPage - 1); };
window.historyNext = function() { loadHistory(historyPage + 1); };
window.editNote = function(id, btn) {
var cell = document.getElementById('note-' + id);
var currentText = cell.querySelector('.note-text').textContent;
cell.innerHTML = '<input class="note-edit-input" type="text" value="' + esc(currentText) + '">'
+ '<button class="btn-save-note" onclick="saveNote(' + id + ')">OK</button>'
+ '<button class="btn-edit-note" onclick="cancelEditNote(' + id + ', \'' + esc(currentText).replace(/'/g, "\\'") + '\')">annuler</button>';
var input = cell.querySelector('.note-edit-input');
input.focus();
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') saveNote(id);
if (e.key === 'Escape') cancelEditNote(id, currentText);
});
};
window.saveNote = async function(id) {
var cell = document.getElementById('note-' + id);
var input = cell.querySelector('.note-edit-input');
var newVal = input.value.trim();
var res = await api('PATCH', '/api/mouvement/' + id, { reference: newVal });
if (res && res.ok) {
cell.innerHTML = '<span class="note-text">' + esc(newVal) + '</span>'
+ '<button class="btn-edit-note" onclick="editNote(' + id + ', this)">modifier</button>';
toast('Note modifiee', 'success');
} else {
toast('Erreur : ' + (res ? res.error : 'connexion'), 'error');
}
};
window.cancelEditNote = function(id, originalText) {
var cell = document.getElementById('note-' + id);
cell.innerHTML = '<span class="note-text">' + esc(originalText) + '</span>'
+ '<button class="btn-edit-note" onclick="editNote(' + id + ', this)">modifier</button>';
};
// ── Logout ────────────────────────────────────────────────
window.logout = async function() {
await fetch('/auth/logout', { method: 'POST' });
window.location.href = '/login.html';
};
// ── Helpers ───────────────────────────────────────────────
function esc(s) {
if (!s) return '';
var d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
// ── Init ──────────────────────────────────────────────────
loadProducts().then(function() {
populateProductSelect();
buildInventaire();
});
})();
</script>
</body>
</html>