938 lines
28 KiB
HTML
938 lines
28 KiB
HTML
<!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> |