manage_ram_scanner/public/index.html

938 lines
28 KiB
HTML
Raw Permalink 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.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Batch Management</title>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg: #f4f3f0;
--surface: #ffffff;
--border: #e2e0db;
--border-strong: #c8c5be;
--text-primary: #1a1917;
--text-secondary: #6b6860;
--text-muted: #9b9890;
--accent: #2563eb;
--accent-light: #eff6ff;
--green: #16a34a;
--green-light: #f0fdf4;
--amber: #d97706;
--amber-light: #fffbeb;
--red: #dc2626;
--red-light: #fef2f2;
--red-row: #fff5f5;
--selected: #eff6ff;
--selected-border: #bfdbfe;
--mono: 'IBM Plex Mono', monospace;
--sans: 'DM Sans', sans-serif;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--sans);
background: var(--bg);
min-height: 100vh;
padding: 24px 20px;
color: var(--text-primary);
}
.wrap {
max-width: 1400px;
margin: 0 auto;
}
/* Header */
.top-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
gap: 12px;
}
.top-bar h1 {
font-size: 18px;
font-weight: 600;
letter-spacing: -0.3px;
display: flex;
align-items: center;
gap: 8px;
}
.top-bar h1 span { color: var(--text-muted); font-weight: 400; font-size: 14px; }
/* Controls bar */
.controls {
display: flex;
gap: 8px;
margin-bottom: 12px;
align-items: center;
flex-wrap: wrap;
}
.search-wrap {
position: relative;
flex: 1;
min-width: 200px;
}
.search-wrap input {
width: 100%;
padding: 7px 12px 7px 32px;
border: 1px solid var(--border-strong);
border-radius: 6px;
font-size: 13px;
font-family: var(--sans);
background: var(--surface);
color: var(--text-primary);
transition: border-color 0.15s, box-shadow 0.15s;
}
.search-wrap input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(37,99,235,0.1);
}
.search-icon {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
font-size: 13px;
pointer-events: none;
}
/* Buttons */
.btn {
padding: 7px 14px;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
font-family: var(--sans);
cursor: pointer;
transition: all 0.15s;
display: inline-flex;
align-items: center;
gap: 5px;
white-space: nowrap;
}
.btn-primary { background: var(--accent); color: #fff; }
.btn-primary:hover { background: #1d4ed8; }
.btn-success { background: var(--green); color: #fff; }
.btn-success:hover { background: #15803d; }
.btn-ghost {
background: var(--surface);
color: var(--text-secondary);
border: 1px solid var(--border-strong);
}
.btn-ghost:hover { background: var(--bg); color: var(--text-primary); }
.btn-danger { background: var(--red); color: #fff; }
.btn-danger:hover { background: #b91c1c; }
.btn-sm { padding: 4px 10px; font-size: 12px; }
/* Selection bar */
.selection-bar {
display: none;
align-items: center;
gap: 8px;
padding: 8px 14px;
background: var(--accent-light);
border: 1px solid var(--selected-border);
border-radius: 6px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.selection-bar.show { display: flex; }
.selection-count {
font-size: 13px;
font-weight: 500;
color: var(--accent);
flex: 1;
}
/* Table */
.table-wrap {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
thead {
background: #fafaf8;
border-bottom: 1px solid var(--border);
}
th {
padding: 9px 12px;
text-align: left;
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.6px;
cursor: pointer;
user-select: none;
white-space: nowrap;
}
th:hover { color: var(--text-secondary); }
th.sort-asc::after { content: ' ↑'; color: var(--accent); }
th.sort-desc::after { content: ' ↓'; color: var(--accent); }
td {
padding: 9px 12px;
border-bottom: 1px solid #f5f4f1;
vertical-align: top;
line-height: 1.4;
}
tbody tr:last-child td { border-bottom: none; }
tbody tr {
transition: background 0.1s;
cursor: pointer;
}
tbody tr:hover:not(.diff-row) { background: #fafaf8; }
/* Row with different MPN => red highlight */
tr.diff-row {
background: #fff8f8;
}
tr.diff-row:hover { background: #fff0f0; }
tr.diff-row td { border-bottom-color: #fee2e2; }
/* Selected row */
tr.selected:not(.diff-row) { background: var(--selected) !important; }
tr.selected.diff-row { background: #fce7e7 !important; }
/* Checkbox col */
.cb-col { width: 32px; padding: 9px 6px 9px 12px !important; }
input[type="checkbox"] {
width: 14px;
height: 14px;
accent-color: var(--accent);
cursor: pointer;
}
/* ID col */
.id-val {
font-family: var(--mono);
font-size: 11px;
color: var(--text-muted);
}
/* Batch name */
.batch-name {
font-weight: 600;
font-size: 13px;
font-family: var(--mono);
color: var(--text-primary);
}
/* Diff indicator */
.diff-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 7px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.diff-badge.ok { background: var(--green-light); color: var(--green); }
.diff-badge.mix { background: var(--red-light); color: var(--red); }
/* Products cell */
.products-cell {
vertical-align: top;
}
.product-list {
display: flex;
flex-direction: column;
gap: 2px;
}
.product-tag {
font-size: 11px;
font-family: var(--mono);
color: var(--text-secondary);
background: #f5f4f0;
padding: 2px 6px;
border-radius: 3px;
white-space: nowrap;
/* overflow: hidden; */
/* text-overflow: ellipsis;
max-width: 240px; */
}
.product-tag.red-tag {
background: #fee2e2;
color: #991b1b;
}
.more-btn {
font-size: 11px;
color: var(--accent);
background: none;
border: none;
cursor: pointer;
padding: 2px 0;
text-align: left;
text-decoration: underline;
text-decoration-style: dotted;
font-family: var(--sans);
font-weight: 500;
}
.more-btn:hover { color: #1d4ed8; }
.product-expanded {
max-height: 150px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 2px;
margin-top: 2px;
padding-right: 4px;
}
.product-expanded::-webkit-scrollbar { width: 4px; }
.product-expanded::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 2px; }
/* SN list in cell */
.sn-list {
display: flex;
flex-direction: column;
gap: 1px;
}
.sn-val {
font-family: var(--mono);
font-size: 11px;
color: var(--text-secondary);
}
/* Date */
.date-val {
font-size: 12px;
color: var(--text-muted);
white-space: nowrap;
}
/* Delete btn */
.del-btn {
background: none;
border: 1px solid var(--border-strong);
color: var(--text-muted);
padding: 4px 10px;
border-radius: 5px;
font-size: 11px;
cursor: pointer;
font-family: var(--sans);
font-weight: 500;
transition: all 0.15s;
}
.del-btn:hover {
background: var(--red-light);
border-color: #fca5a5;
color: var(--red);
}
/* Pagination */
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 16px;
flex-wrap: wrap;
}
.pg-btn {
padding: 5px 11px;
border: 1px solid var(--border-strong);
background: var(--surface);
border-radius: 5px;
cursor: pointer;
font-size: 12px;
font-family: var(--sans);
font-weight: 500;
transition: all 0.12s;
color: var(--text-secondary);
}
.pg-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.pg-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.pg-btn.active { background: var(--accent); color: #fff; border-color: var(--accent); }
.pg-info { font-size: 12px; color: var(--text-muted); padding: 0 8px; }
/* Empty / loading */
.state-msg {
text-align: center;
padding: 60px 20px;
color: var(--text-muted);
font-size: 14px;
}
/* Modal */
.modal-backdrop {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.35);
z-index: 100;
justify-content: center;
align-items: center;
padding: 20px;
}
.modal-backdrop.open { display: flex; }
.modal-box {
background: var(--surface);
border-radius: 10px;
padding: 26px 28px;
width: 100%;
max-width: 820px;
max-height: 88vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0,0,0,0.2);
}
.modal-box h2 {
font-size: 16px;
font-weight: 600;
margin-bottom: 20px;
color: var(--text-primary);
}
.form-group { margin-bottom: 16px; }
.form-group label {
display: block;
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 6px;
}
.form-group input, .form-group textarea {
width: 100%;
padding: 9px 12px;
border: 1px solid var(--border-strong);
border-radius: 6px;
font-size: 13px;
font-family: var(--sans);
background: var(--surface);
color: var(--text-primary);
transition: border-color 0.15s;
}
.form-group input:focus, .form-group textarea:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(37,99,235,0.1);
}
.form-group textarea {
min-height: 130px;
resize: vertical;
font-family: var(--mono);
font-size: 12px;
}
.json-hint {
background: #fafaf8;
border: 1px solid var(--border);
border-radius: 5px;
padding: 10px 12px;
font-family: var(--mono);
font-size: 11px;
color: var(--text-secondary);
margin-top: 6px;
overflow-x: auto;
white-space: pre;
line-height: 1.5;
}
.help { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
.modal-footer {
display: flex;
gap: 8px;
margin-top: 20px;
justify-content: flex-end;
}
/* Toast */
.toast {
position: fixed;
bottom: 24px;
right: 24px;
background: var(--text-primary);
color: #fff;
padding: 10px 16px;
border-radius: 7px;
font-size: 13px;
font-family: var(--sans);
z-index: 999;
opacity: 0;
transform: translateY(8px);
transition: all 0.2s;
pointer-events: none;
}
.toast.show {
opacity: 1;
transform: translateY(0);
}
</style>
</head>
<body>
<div class="wrap">
<!-- Header -->
<div class="top-bar">
<h1>📦 Batch Management <span id="totalCount"></span></h1>
<button class="btn btn-success" onclick="openModal()"> New Batch</button>
</div>
<!-- Controls -->
<div class="controls">
<div class="search-wrap">
<span class="search-icon">🔍</span>
<input id="searchInput" type="text" placeholder="Search batch name or ID or SN">
</div>
<button class="btn btn-ghost" onclick="selectAll()">Select All</button>
<button class="btn btn-ghost" onclick="clearSel()">Clear</button>
</div>
<!-- Selection action bar -->
<div class="selection-bar" id="selBar">
<span class="selection-count" id="selCount">0 selected</span>
<button class="btn btn-ghost btn-sm" onclick="copySN()">📋 Copy SN</button>
<button class="btn btn-ghost btn-sm" onclick="copyModel()">📋 Copy Model</button>
<button class="btn btn-ghost btn-sm" onclick="copyAll()">📋 Copy All Fields</button>
</div>
<!-- Table -->
<div class="table-wrap">
<div id="stateMsg" class="state-msg">⏳ Loading…</div>
<table id="mainTable" style="display:none;">
<thead>
<tr>
<th class="cb-col"><input type="checkbox" id="masterCb" onchange="toggleAll(this)"></th>
<th
style="width:50px;">ID</th>
<!-- onclick="doSort('id')" -->
<th
style="min-width:160px;">Batch Name</th>
<!-- onclick="doSort('batch_name')" -->
<th style="width:110px;">Summary</th>
<th style="min-width:180px;">Different MPN</th>
<th style="min-width:240px;">Same MPN Products</th>
<th
style="width:130px;">Created</th>
<!-- onclick="doSort('createdAt')" -->
<th style="width:70px;"></th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
</div>
<div class="pagination" id="pagination"></div>
</div>
<!-- Create Modal -->
<div class="modal-backdrop" id="modal" onclick="bgClose(event)">
<div class="modal-box">
<h2>Create New Batch</h2>
<div class="form-group">
<label>Batch Name *</label>
<input id="fName" type="text" placeholder="BATCH_20250210141530">
<div class="help">Format: BATCH_yyyymmddhhmmss</div>
</div>
<div class="form-group">
<label>Items — Same MPN *</label>
<textarea id="fItems" placeholder='JSON array…'></textarea>
<div class="json-hint">[{"brand":"Samsung","mpn":"CN_M378A5143DB0-CPB_1620","mpn_custom":"Custom 1","sn":"U03A..."}]</div>
</div>
<div class="form-group">
<label>Mixed Items — Different MPN (optional)</label>
<textarea id="fMix" placeholder='JSON array…'></textarea>
<div class="json-hint">[{"brand":"Micron","mpn":"CN_M378A5143DB0-CPB_1621","mpn_custom":"Custom 3","sn":"U03A..."}]</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModal()">Cancel</button>
<button class="btn btn-success" onclick="createBatch()">Create</button>
</div>
</div>
</div>
<!-- Toast -->
<div class="toast" id="toast"></div>
<script>
const API = window.location.hostname === 'localhost'
? 'http://localhost:4444/api/'
: 'https://logs1.danielvu.com/mobile-api/';
let page = 1, sortBy = 'id', sortOrder = 'DESC', search = '';
let allBatches = [];
let selectedIds = new Set();
// ── Load ────────────────────────────────────────────
async function load() {
document.getElementById('stateMsg').textContent = '⏳ Loading…';
document.getElementById('stateMsg').style.display = 'block';
document.getElementById('mainTable').style.display = 'none';
try {
const r = await fetch(`${API}batch/get-all?page=${page}&sortBy=${sortBy}&sortOrder=${sortOrder}&search=${encodeURIComponent(search)}`);
const d = await r.json();
document.getElementById('stateMsg').style.display = 'none';
if (!d.batches || d.batches.length === 0) {
document.getElementById('stateMsg').textContent = 'No batches found.';
document.getElementById('stateMsg').style.display = 'block';
document.getElementById('totalCount').textContent = '';
renderPagination({ total: 0, totalPages: 1 });
return;
}
allBatches = d.batches;
document.getElementById('mainTable').style.display = 'table';
document.getElementById('totalCount').textContent = `(${d.total})`;
render();
renderPagination(d);
updateSortHeaders();
} catch (e) {
document.getElementById('stateMsg').textContent = '⚠️ Failed to load. Check server.';
document.getElementById('stateMsg').style.display = 'block';
}
}
// ── Render rows ────────────────────────────────────
function productTag(it, cls) {
cls = cls || '';
const label = (it.brand ? it.brand + ' ' : '') +" - "+ (it.mpn_custom ? it.mpn_custom + ' ' : '') +" - "+ (it.sn || '');
return `<span class="product-tag ${cls}">${label}</span>`;
}
function render() {
const tbody = document.getElementById('tbody');
tbody.innerHTML = '';
allBatches.sort((a, b) => b.id - a.id).forEach(b => {
const hasMix = b.items_mix && b.items_mix.length > 0;
const isSelected = selectedIds.has(b.id);
const sameItems = b.items || [];
const mixItems = b.items_mix || [];
const total = sameItems.length + mixItems.length;
const PREVIEW = 5;
const cellKey = `c${b.id}`;
const tr = document.createElement('tr');
tr.className = (hasMix ? 'diff-row' : '') + (isSelected ? ' selected' : '');
tr.onclick = (e) => {
if (e.target.type === 'checkbox' || e.target.classList.contains('del-btn') || e.target.classList.contains('more-btn')) return;
toggleRow(b.id);
};
const dt = new Date(b.createdAt + 'Z');
const dateFmt = dt.toLocaleString('en-US', { year:'numeric', month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit', hour12:false });
// col: Summary
const summaryHtml = `
<div style="display:flex;flex-direction:column;gap:3px;white-space:nowrap;">
<span style="font-size:12px;font-weight:600;">${total} total</span>
<span style="font-size:11px;color:var(--green);">✓ ${sameItems.length} same</span>
${mixItems.length > 0 ? `<span style="font-size:11px;color:var(--red);">⚠ ${mixItems.length} diff</span>` : ''}
</div>`;
// col: Different MPN
let diffHtml = hasMix
? '<div class="product-list">' + mixItems.map(it => productTag(it,'red-tag')).join('') + '</div>'
: '<span style="color:var(--text-muted);font-size:11px;">—</span>';
// col: Same MPN — preview 5 + expandable rest (no re-render, pure DOM toggle)
let sameHtml = '';
if (sameItems.length === 0) {
sameHtml = '<span style="color:var(--text-muted);font-size:11px;">—</span>';
} else {
const previewTags = sameItems.slice(0, PREVIEW).map(it => productTag(it,'')).join('');
sameHtml = `<div class="product-list">${previewTags}</div>`;
if (sameItems.length > PREVIEW) {
const restCount = sameItems.length - PREVIEW;
const restTags = sameItems.slice(PREVIEW).map(it => productTag(it,'')).join('');
sameHtml += `<div class="product-expanded" id="${cellKey}-exp" style="display:none;">${restTags}</div>`;
sameHtml += `<button class="more-btn" id="${cellKey}-btn" onclick="toggleMore(event,'${cellKey}',${restCount})"> ${restCount} more</button>`;
}
}
tr.innerHTML = `
<td class="cb-col"><input type="checkbox" ${isSelected ? 'checked' : ''} onchange="toggleById(${b.id})"></td>
<td><span class="id-val">#${b.id}</span></td>
<td>
<div class="batch-name">${b.batch_name}</div>
<div style="margin-top:4px;"><span class="diff-badge ${hasMix?'mix':'ok'}">${hasMix?'⚠ Mixed':'✓ Uniform'}</span></div>
</td>
<td style="vertical-align:middle;">${summaryHtml}</td>
<td class="products-cell">${diffHtml}</td>
<td class="products-cell">${sameHtml}</td>
<td><span class="date-val">${dateFmt}</span></td>
<td><button class="del-btn" onclick="event.stopPropagation();deleteBatch(${b.id})">Delete</button></td>
`;
tbody.appendChild(tr);
});
updateSelBar();
updateMasterCb();
}
function toggleMore(e, cellKey, restCount) {
e.stopPropagation();
const exp = document.getElementById(cellKey + '-exp');
const btn = document.getElementById(cellKey + '-btn');
if (!exp || !btn) return;
const opening = exp.style.display === 'none';
exp.style.display = opening ? 'flex' : 'none';
btn.textContent = opening ? '↑ Show less' : (' ' + restCount + ' more');
}
// ── Selection ────────────────────────────────────
function toggleRow(id) {
if (selectedIds.has(id)) selectedIds.delete(id);
else selectedIds.add(id);
render();
}
function toggleById(id) {
if (selectedIds.has(id)) selectedIds.delete(id);
else selectedIds.add(id);
updateSelBar();
updateMasterCb();
}
function toggleAll(cb) {
if (cb.checked) allBatches.forEach(b => selectedIds.add(b.id));
else allBatches.forEach(b => selectedIds.delete(b.id));
render();
}
function selectAll() {
allBatches.forEach(b => selectedIds.add(b.id));
render();
}
function clearSel() {
selectedIds.clear();
render();
}
function updateSelBar() {
const bar = document.getElementById('selBar');
const count = document.getElementById('selCount');
if (selectedIds.size > 0) {
bar.classList.add('show');
count.textContent = `${selectedIds.size} selected`;
} else {
bar.classList.remove('show');
}
}
function updateMasterCb() {
const master = document.getElementById('masterCb');
master.checked = allBatches.length > 0 && allBatches.every(b => selectedIds.has(b.id));
master.indeterminate = selectedIds.size > 0 && !master.checked;
}
// ── Copy functions ────────────────────────────────
function getSelectedBatches() {
return allBatches.filter(b => selectedIds.has(b.id));
}
function copySN() {
const batches = getSelectedBatches();
const sns = [];
batches.forEach(b => {
(b.items || []).forEach(it => sns.push(it.sn));
(b.items_mix || []).forEach(it => sns.push(it.sn));
});
copyText(sns.join('\n'), `Copied ${sns.length} SN(s)`);
}
function copyModel() {
const batches = getSelectedBatches();
const models = new Set();
batches.forEach(b => {
(b.items || []).forEach(it => models.add(it.mpn_custom));
(b.items_mix || []).forEach(it => models.add(it.mpn_custom));
});
copyText([...models].join('\n'), `Copied ${models.size} model(s)`);
}
function copyAll() {
const batches = getSelectedBatches();
const lines = [];
batches.forEach(b => {
(b.items || []).forEach(it => {
lines.push([b.batch_name, it.brand || '', it.mpn, it.mpn_custom || '', it.sn].join('\t'));
});
(b.items_mix || []).forEach(it => {
lines.push([b.batch_name, it.brand || '', it.mpn, it.mpn_custom || '', it.sn, '[MIX]'].join('\t'));
});
});
copyText(lines.join('\n'), `Copied ${lines.length} row(s)`);
}
function copyText(text, msg) {
navigator.clipboard.writeText(text).then(() => toast(msg)).catch(() => {
const el = document.createElement('textarea');
el.value = text;
document.body.appendChild(el);
el.select();
document.execCommand('copy');
el.remove();
toast(msg);
});
}
function toast(msg) {
const el = document.getElementById('toast');
el.textContent = msg;
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), 2000);
}
// ── Sort ──────────────────────────────────────────
function doSort(col) {
if (sortBy === col) sortOrder = sortOrder === 'DESC' ? 'ASC' : 'DESC';
else { sortBy = col; sortOrder = 'DESC'; }
page = 1;
load();
}
function updateSortHeaders() {
document.querySelectorAll('thead th').forEach(th => {
th.classList.remove('sort-asc', 'sort-desc');
});
// map col to index
const map = { id: 1, batch_name: 2, createdAt: 6 };
const idx = map[sortBy];
if (idx !== undefined) {
const ths = document.querySelectorAll('thead th');
ths[idx].classList.add(sortOrder === 'ASC' ? 'sort-asc' : 'sort-desc');
}
}
// ── Pagination ────────────────────────────────────
function renderPagination(d) {
const el = document.getElementById('pagination');
if (!d.totalPages || d.totalPages <= 1) { el.innerHTML = ''; return; }
let html = `<button class="pg-btn" ${page === 1 ? 'disabled' : ''} onclick="goPage(${page - 1})">Prev</button>`;
const s = Math.max(1, page - 2), e = Math.min(d.totalPages, page + 2);
for (let i = s; i <= e; i++) {
html += `<button class="pg-btn ${i === page ? 'active' : ''}" onclick="goPage(${i})">${i}</button>`;
}
html += `<button class="pg-btn" ${page === d.totalPages ? 'disabled' : ''} onclick="goPage(${page + 1})">Next</button>`;
html += `<span class="pg-info">${d.total} batches</span>`;
el.innerHTML = html;
}
function goPage(p) { page = p; load(); }
// ── Delete ────────────────────────────────────────
async function deleteBatch(id) {
if (!confirm('Delete this batch and all its items?')) return;
try {
const r = await fetch(`${API}batch/delete/${id}`, { method: 'DELETE' });
if (r.ok) { selectedIds.delete(id); load(); toast('Batch deleted'); }
else toast('Error deleting batch');
} catch { toast('Error deleting batch'); }
}
// ── Create modal ──────────────────────────────────
function openModal() {
const now = new Date();
const ts = now.getFullYear() + pad(now.getMonth()+1) + pad(now.getDate()) + pad(now.getHours()) + pad(now.getMinutes()) + pad(now.getSeconds());
document.getElementById('fName').value = `BATCH_${ts}`;
document.getElementById('fItems').value = '';
document.getElementById('fMix').value = '';
document.getElementById('modal').classList.add('open');
}
function closeModal() { document.getElementById('modal').classList.remove('open'); }
function bgClose(e) { if (e.target.id === 'modal') closeModal(); }
function pad(n) { return String(n).padStart(2, '0'); }
async function createBatch() {
const batch_name = document.getElementById('fName').value.trim();
const itemsTxt = document.getElementById('fItems').value.trim();
const mixTxt = document.getElementById('fMix').value.trim();
if (!batch_name) { alert('Batch name required'); return; }
if (!itemsTxt) { alert('Items required'); return; }
let items, items_mix;
try { items = JSON.parse(itemsTxt); } catch(e) { alert('Invalid JSON for items: ' + e.message); return; }
try { items_mix = mixTxt ? JSON.parse(mixTxt) : []; } catch(e) { alert('Invalid JSON for mixed items: ' + e.message); return; }
try {
const r = await fetch(API + 'batch/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ batch_name, items, items_mix })
});
const res = await r.json();
if (r.ok) {
closeModal();
load();
toast(`✅ Batch created — ${res.inserted_items} items, ${res.inserted_mix_items} mixed`);
} else { alert('Error: ' + res.error); }
} catch(e) { alert('Error: ' + e.message); }
}
// ── Search debounce ───────────────────────────────
let st;
document.getElementById('searchInput').addEventListener('input', e => {
clearTimeout(st);
st = setTimeout(() => { search = e.target.value; page = 1; selectedIds.clear(); load(); }, 300);
});
load();
</script>
</body>
</html>