1052 lines
32 KiB
HTML
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>
|