manage_ram_scanner/public/index.html

823 lines
20 KiB
HTML
Raw 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>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1600px;
margin: 0 auto;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 28px;
font-weight: 600;
margin-bottom: 10px;
}
.controls {
padding: 25px 30px;
background: #f8f9fa;
display: flex;
gap: 15px;
flex-wrap: wrap;
align-items: center;
border-bottom: 1px solid #e9ecef;
}
.search-box {
flex: 1;
min-width: 250px;
position: relative;
}
.search-box input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 14px;
transition: all 0.3s;
}
.search-box input:focus {
outline: none;
border-color: #2c3e50;
box-shadow: 0 0 0 3px rgba(44, 62, 80, 0.1);
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(44, 62, 80, 0.4);
}
.btn-success {
background: linear-gradient(135deg, #27ae60 0%, #229954 100%);
color: white;
}
.btn-success:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(39, 174, 96, 0.4);
}
.table-container {
overflow-x: auto;
padding: 0 30px 30px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
thead {
background: #f8f9fa;
}
th {
padding: 16px;
text-align: left;
font-weight: 600;
color: #495057;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.5px;
cursor: pointer;
user-select: none;
position: relative;
}
th:hover {
background: #e9ecef;
}
th.sorted::after {
content: ' ▼';
font-size: 10px;
}
th.sorted.asc::after {
content: ' ▲';
}
td {
padding: 16px;
border-bottom: 1px solid #f1f3f5;
color: #495057;
font-size: 14px;
}
tbody tr {
transition: background 0.2s;
}
tbody tr:hover {
background: #f8f9fa;
}
.batch-row {
cursor: pointer;
font-weight: 500;
}
.batch-row:hover {
background: #e3f2fd !important;
}
.batch-row.expanded {
background: #e8f5e9 !important;
}
.items-row {
display: none;
background: #fafafa;
}
.items-row.show {
display: table-row;
}
.items-cell {
padding: 20px !important;
border-left: 4px solid #27ae60;
}
.items-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 15px;
}
.items-section {
background: white;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.items-section h4 {
color: #2c3e50;
margin-bottom: 10px;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.items-section.mix h4 {
color: #e74c3c;
}
.item-card {
background: #f8f9fa;
padding: 10px;
border-radius: 6px;
margin-bottom: 8px;
border-left: 3px solid #27ae60;
font-size: 13px;
}
.items-section.mix .item-card {
border-left-color: #e74c3c;
}
.item-card .label {
color: #6c757d;
font-weight: 500;
display: inline-block;
width: 40px;
}
.item-card .value {
color: #495057;
}
.badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.badge-success {
background: #d4edda;
color: #155724;
}
.badge-warning {
background: #fff3cd;
color: #856404;
}
.delete-btn {
background: #dc3545;
color: white;
border: none;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
transition: all 0.3s;
}
.delete-btn:hover {
background: #c82333;
transform: scale(1.05);
}
.expand-icon {
display: inline-block;
transition: transform 0.3s;
margin-right: 8px;
}
.expand-icon.expanded {
transform: rotate(90deg);
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
padding: 30px;
flex-wrap: wrap;
}
.page-btn {
padding: 8px 16px;
border: 2px solid #e9ecef;
background: white;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.page-btn:hover:not(:disabled) {
border-color: #2c3e50;
color: #2c3e50;
}
.page-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.page-btn.active {
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
color: white;
border-color: transparent;
}
.page-info {
color: #6c757d;
font-size: 14px;
}
.loading {
text-align: center;
padding: 40px;
color: #6c757d;
}
.empty {
text-align: center;
padding: 60px;
color: #adb5bd;
}
.empty h3 {
margin-bottom: 10px;
color: #6c757d;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal.active {
display: flex;
}
.modal-content {
background: white;
padding: 30px;
border-radius: 12px;
width: 90%;
max-width: 800px;
max-height: 80vh;
overflow-y: auto;
}
.modal h2 {
margin-bottom: 20px;
color: #495057;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
color: #495057;
font-size: 14px;
font-weight: 500;
}
.form-group input, .form-group textarea {
width: 100%;
padding: 10px;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 14px;
font-family: inherit;
}
.form-group textarea {
min-height: 150px;
resize: vertical;
}
.modal-buttons {
display: flex;
gap: 10px;
margin-top: 20px;
}
.btn-secondary {
background: #6c757d;
color: white;
flex: 1;
}
.btn-secondary:hover {
background: #5a6268;
}
.help-text {
font-size: 12px;
color: #6c757d;
margin-top: 5px;
}
.json-example {
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 12px;
overflow-x: auto;
margin-top: 10px;
}
@media (max-width: 768px) {
.items-container {
grid-template-columns: 1fr;
}
.controls {
padding: 20px;
}
.search-box {
min-width: 100%;
}
.table-container {
padding: 0 20px 20px;
}
table {
font-size: 12px;
}
th, td {
padding: 10px 8px;
}
.header h1 {
font-size: 22px;
}
.pagination {
padding: 20px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📦 Batch Management System</h1>
<p>Manage batches with items and mixed items</p>
</div>
<div class="controls">
<div class="search-box">
<input type="text" id="searchInput" placeholder="Search by batch name or ID...">
</div>
<button class="btn btn-success" onclick="openCreateModal()">
Create Batch
</button>
</div>
<div class="table-container">
<div id="loading" class="loading" style="display: none;">
Loading batches...
</div>
<div id="empty" class="empty" style="display: none;">
<h3>No batches found</h3>
<p>Create your first batch to get started!</p>
</div>
<table id="batchTable" style="display: none;">
<thead>
<tr>
<th onclick="sortTable('id')" style="width: 80px;">ID</th>
<th onclick="sortTable('batch_name')">Batch Name</th>
<th style="width: 120px;">Items</th>
<th style="width: 120px;">Mixed Items</th>
<th onclick="sortTable('createdAt')" style="width: 180px;">Created At</th>
<th style="width: 100px;">Actions</th>
</tr>
</thead>
<tbody id="batchBody"></tbody>
</table>
</div>
<div class="pagination" id="pagination"></div>
</div>
<!-- Create Batch Modal -->
<div id="createModal" class="modal">
<div class="modal-content">
<h2>Create New Batch</h2>
<div class="form-group">
<label>Batch Name *</label>
<input type="text" id="batchNameInput" placeholder="e.g., BATCH_20250210141530">
<div class="help-text">Format: BATCH_yyyymmddhhmmss or any unique name</div>
</div>
<div class="form-group">
<label>Items (Valid - Same MPN) *</label>
<textarea id="itemsInput" placeholder='Enter JSON array of items...'></textarea>
<div class="json-example">
Example:
[
{"mpn": "CN_M378A5143DB0-CPB_1620", "sn": "U03A00062074544398"},
{"mpn": "CN_M378A5143DB0-CPB_1620", "sn": "U03A00062074544399"}
]</div>
</div>
<div class="form-group">
<label>Mixed Items (Different MPN) - Optional</label>
<textarea id="itemsMixInput" placeholder='Enter JSON array of mixed items...'></textarea>
<div class="json-example">
Example:
[
{"mpn": "CN_M378A5143DB0-CPB_1621", "sn": "U03A00062074544403"}
]</div>
</div>
<div class="modal-buttons">
<button class="btn btn-secondary" onclick="closeCreateModal()">Cancel</button>
<button class="btn btn-success" onclick="createBatch()">Create Batch</button>
</div>
</div>
</div>
<script>
let currentPage = 1;
let currentSort = 'id';
let currentOrder = 'DESC';
let searchTerm = '';
let URL = window.location.hostname === 'localhost'
? 'http://localhost:4444/api/'
: 'https://logs1.danielvu.com/mobile-api/';
let expandedBatches = new Set();
async function loadBatches() {
document.getElementById('loading').style.display = 'block';
document.getElementById('batchTable').style.display = 'none';
document.getElementById('empty').style.display = 'none';
try {
const response = await fetch(`${URL}batch/get-all?page=${currentPage}&sortBy=${currentSort}&sortOrder=${currentOrder}&search=${encodeURIComponent(searchTerm)}`);
const data = await response.json();
document.getElementById('loading').style.display = 'none';
if (data.batches.length === 0) {
document.getElementById('empty').style.display = 'block';
} else {
document.getElementById('batchTable').style.display = 'table';
renderBatches(data.batches);
renderPagination(data);
}
updateSortIndicators();
} catch (error) {
console.error('Error loading batches:', error);
document.getElementById('loading').style.display = 'none';
alert('Error loading batches. Please check if the server is running.');
}
}
function renderBatches(batches) {
const tbody = document.getElementById('batchBody');
tbody.innerHTML = batches.map(batch => {
const date = new Date(batch.createdAt + "Z");
const formattedDate = date.toLocaleString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
const isExpanded = expandedBatches.has(batch.id);
return `
<tr class="batch-row ${isExpanded ? 'expanded' : ''}" onclick="toggleBatch(${batch.id})">
<td>
<span class="expand-icon ${isExpanded ? 'expanded' : ''}">▶</span>
${batch.id}
</td>
<td><strong>${batch.batch_name}</strong></td>
<td><span class="badge badge-success">${batch.items.length}</span></td>
<td><span class="badge badge-warning">${batch.items_mix.length}</span></td>
<td>${formattedDate}</td>
<td>
<button class="delete-btn" onclick="event.stopPropagation(); deleteBatch(${batch.id})">Delete</button>
</td>
</tr>
<tr class="items-row ${isExpanded ? 'show' : ''}" id="items-${batch.id}">
<td colspan="6" class="items-cell">
<div class="items-container">
<div class="items-section">
<h4>✅ Valid Items (${batch.items.length})</h4>
${batch.items.length > 0 ? batch.items.map(item => `
<div class="item-card">
<div><span class="label">MPN:</span> <span class="value">${item.mpn}</span></div>
<div><span class="label">SN:</span> <span class="value">${item.sn}</span></div>
</div>
`).join('') : '<p style="color: #999;">No items</p>'}
</div>
<div class="items-section mix">
<h4>⚠️ Mixed Items (${batch.items_mix.length})</h4>
${batch.items_mix.length > 0 ? batch.items_mix.map(item => `
<div class="item-card">
<div><span class="label">MPN:</span> <span class="value">${item.mpn}</span></div>
<div><span class="label">SN:</span> <span class="value">${item.sn}</span></div>
</div>
`).join('') : '<p style="color: #999;">No mixed items</p>'}
</div>
</div>
</td>
</tr>
`;
}).join('');
}
function toggleBatch(batchId) {
if (expandedBatches.has(batchId)) {
expandedBatches.delete(batchId);
} else {
expandedBatches.add(batchId);
}
loadBatches();
}
function renderPagination(data) {
const pagination = document.getElementById('pagination');
let html = '';
html += `<button class="page-btn" ${currentPage === 1 ? 'disabled' : ''} onclick="changePage(${currentPage - 1})">Previous</button>`;
const startPage = Math.max(1, currentPage - 2);
const endPage = Math.min(data.totalPages, currentPage + 2);
for (let i = startPage; i <= endPage; i++) {
html += `<button class="page-btn ${i === currentPage ? 'active' : ''}" onclick="changePage(${i})">${i}</button>`;
}
html += `<button class="page-btn" ${currentPage === data.totalPages ? 'disabled' : ''} onclick="changePage(${currentPage + 1})">Next</button>`;
html += `<span class="page-info">Total: ${data.total} batches</span>`;
pagination.innerHTML = html;
}
function changePage(page) {
currentPage = page;
loadBatches();
}
function sortTable(column) {
if (currentSort === column) {
currentOrder = currentOrder === 'DESC' ? 'ASC' : 'DESC';
} else {
currentSort = column;
currentOrder = 'DESC';
}
currentPage = 1;
loadBatches();
}
function updateSortIndicators() {
document.querySelectorAll('th').forEach(th => {
th.classList.remove('sorted', 'asc');
});
const columns = ['id', 'batch_name', 'createdAt'];
const index = columns.indexOf(currentSort);
if (index !== -1) {
const th = document.querySelectorAll('th')[index];
th.classList.add('sorted');
if (currentOrder === 'ASC') {
th.classList.add('asc');
}
}
}
let searchTimeout;
document.getElementById('searchInput').addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
searchTerm = e.target.value;
currentPage = 1;
expandedBatches.clear();
loadBatches();
}, 300);
});
async function deleteBatch(id) {
if (!confirm('Are you sure you want to delete this batch and all its items?')) return;
try {
const response = await fetch(`${URL}batch/delete/${id}`, {
method: 'DELETE'
});
if (response.ok) {
expandedBatches.delete(id);
loadBatches();
} else {
alert('Error deleting batch');
}
} catch (error) {
console.error('Error deleting batch:', error);
alert('Error deleting batch');
}
}
function openCreateModal() {
document.getElementById('createModal').classList.add('active');
// Auto-generate batch name with timestamp
const now = new Date();
const timestamp = now.getFullYear() +
String(now.getMonth() + 1).padStart(2, '0') +
String(now.getDate()).padStart(2, '0') +
String(now.getHours()).padStart(2, '0') +
String(now.getMinutes()).padStart(2, '0') +
String(now.getSeconds()).padStart(2, '0');
document.getElementById('batchNameInput').value = `BATCH_${timestamp}`;
}
function closeCreateModal() {
document.getElementById('createModal').classList.remove('active');
document.getElementById('batchNameInput').value = '';
document.getElementById('itemsInput').value = '';
document.getElementById('itemsMixInput').value = '';
}
async function createBatch() {
const batch_name = document.getElementById('batchNameInput').value.trim();
const itemsText = document.getElementById('itemsInput').value.trim();
const itemsMixText = document.getElementById('itemsMixInput').value.trim();
if (!batch_name) {
alert('Please enter a batch name');
return;
}
if (!itemsText) {
alert('Please enter at least one item');
return;
}
let items, items_mix;
try {
items = JSON.parse(itemsText);
if (!Array.isArray(items) || items.length === 0) {
alert('Items must be a non-empty array');
return;
}
} catch (e) {
alert('Invalid JSON format for items: ' + e.message);
return;
}
try {
items_mix = itemsMixText ? JSON.parse(itemsMixText) : [];
if (!Array.isArray(items_mix)) {
alert('Mixed items must be an array');
return;
}
} catch (e) {
alert('Invalid JSON format for mixed items: ' + e.message);
return;
}
try {
const response = await fetch(URL + 'batch/save', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ batch_name, items, items_mix })
});
const result = await response.json();
if (response.ok) {
closeCreateModal();
expandedBatches.clear();
loadBatches();
alert(`Batch created successfully!\nItems: ${result.inserted_items}\nMixed Items: ${result.inserted_mix_items}`);
} else {
alert('Error creating batch: ' + result.error);
}
} catch (error) {
console.error('Error creating batch:', error);
alert('Error creating batch: ' + error.message);
}
}
// Load batches on page load
loadBatches();
</script>
</body>
</html>