first commit
This commit is contained in:
commit
81a1448ed3
|
|
@ -0,0 +1 @@
|
||||||
|
node_modules
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "scanramproducts",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"sqlite3": "^5.1.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,604 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Product 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: 1400px;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
body {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 8px 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: 500px;
|
||||||
|
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 {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>Product Management</h1>
|
||||||
|
<p>Manage your products efficiently</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<div class="search-box">
|
||||||
|
<input type="text" id="searchInput" placeholder="Search by ID, MPN, SN, or creator...">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" onclick="openCreateModal()">
|
||||||
|
Add Product
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-container">
|
||||||
|
<div id="loading" class="loading" style="display: none;">
|
||||||
|
Loading data...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="empty" class="empty" style="display: none;">
|
||||||
|
<h3>No products found</h3>
|
||||||
|
<p>Add your first product to get started!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table id="productTable" style="display: none;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th onclick="sortTable('id')">ID</th>
|
||||||
|
<th onclick="sortTable('mpn')">MPN</th>
|
||||||
|
<th onclick="sortTable('sn')">SN</th>
|
||||||
|
<th onclick="sortTable('createdAt')">Created At</th>
|
||||||
|
<th onclick="sortTable('createdBy')">Created By</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="productBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination" id="pagination"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Create Product Modal -->
|
||||||
|
<div id="createModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h2>Add New Product</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>MPN *</label>
|
||||||
|
<input type="text" id="mpnInput" placeholder="Enter MPN code">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Serial Number (SN) *</label>
|
||||||
|
<input type="text" id="snInput" placeholder="Enter serial number">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Created By *</label>
|
||||||
|
<input type="text" id="createdByInput" placeholder="Enter creator name">
|
||||||
|
</div>
|
||||||
|
<div class="modal-buttons">
|
||||||
|
<button class="btn btn-secondary" onclick="closeCreateModal()">Cancel</button>
|
||||||
|
<button class="btn btn-primary" onclick="createProduct()">Create</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentPage = 1;
|
||||||
|
let currentSort = 'id';
|
||||||
|
let currentOrder = 'DESC';
|
||||||
|
let searchTerm = '';
|
||||||
|
let URL = "https://logs1.danielvu.com/mobile-api/"
|
||||||
|
async function loadProducts() {
|
||||||
|
document.getElementById('loading').style.display = 'block';
|
||||||
|
document.getElementById('productTable').style.display = 'none';
|
||||||
|
document.getElementById('empty').style.display = 'none';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${URL}product/get?page=${currentPage}&sortBy=${currentSort}&sortOrder=${currentOrder}&search=${encodeURIComponent(searchTerm)}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
document.getElementById('loading').style.display = 'none';
|
||||||
|
|
||||||
|
if (data.products.length === 0) {
|
||||||
|
document.getElementById('empty').style.display = 'block';
|
||||||
|
} else {
|
||||||
|
document.getElementById('productTable').style.display = 'table';
|
||||||
|
renderProducts(data.products);
|
||||||
|
renderPagination(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSortIndicators();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading products:', error);
|
||||||
|
document.getElementById('loading').style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderProducts(products) {
|
||||||
|
const tbody = document.getElementById('productBody');
|
||||||
|
tbody.innerHTML = products.map(product => {
|
||||||
|
// Convert UTC to client's local timezone
|
||||||
|
const date = new Date(product.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
|
||||||
|
});
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>${product.id}</td>
|
||||||
|
<td>${product.mpn}</td>
|
||||||
|
<td>${product.sn}</td>
|
||||||
|
<td>${formattedDate}</td>
|
||||||
|
<td>${product.createdBy}</td>
|
||||||
|
<td>
|
||||||
|
<button class="delete-btn" onclick="deleteProduct(${product.id})">Delete</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
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} products</span>`;
|
||||||
|
|
||||||
|
pagination.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePage(page) {
|
||||||
|
currentPage = page;
|
||||||
|
loadProducts();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortTable(column) {
|
||||||
|
if (currentSort === column) {
|
||||||
|
currentOrder = currentOrder === 'DESC' ? 'ASC' : 'DESC';
|
||||||
|
} else {
|
||||||
|
currentSort = column;
|
||||||
|
currentOrder = 'DESC';
|
||||||
|
}
|
||||||
|
currentPage = 1;
|
||||||
|
loadProducts();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSortIndicators() {
|
||||||
|
document.querySelectorAll('th').forEach(th => {
|
||||||
|
th.classList.remove('sorted', 'asc');
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = ['id', 'mpn', 'sn', 'createdAt', 'createdBy'];
|
||||||
|
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;
|
||||||
|
loadProducts();
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function deleteProduct(id) {
|
||||||
|
if (!confirm('Are you sure you want to delete this product?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${URL}product/delete/${id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
loadProducts();
|
||||||
|
} else {
|
||||||
|
alert('Error deleting product');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting product:', error);
|
||||||
|
alert('Error deleting product');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateModal() {
|
||||||
|
document.getElementById('createModal').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCreateModal() {
|
||||||
|
document.getElementById('createModal').classList.remove('active');
|
||||||
|
document.getElementById('mpnInput').value = '';
|
||||||
|
document.getElementById('snInput').value = '';
|
||||||
|
document.getElementById('createdByInput').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createProduct() {
|
||||||
|
const mpn = document.getElementById('mpnInput').value.trim();
|
||||||
|
const sn = document.getElementById('snInput').value.trim();
|
||||||
|
const createdBy = document.getElementById('createdByInput').value.trim();
|
||||||
|
|
||||||
|
if (!mpn || !sn || !createdBy) {
|
||||||
|
alert('Please fill in all fields');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(URL+'product/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify([{ mpn, sn, createdBy }])
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
closeCreateModal();
|
||||||
|
loadProducts();
|
||||||
|
} else {
|
||||||
|
alert('Error creating product');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating product:', error);
|
||||||
|
alert('Error creating product');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load products on page load
|
||||||
|
loadProducts();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
const express = require('express');
|
||||||
|
const sqlite3 = require('sqlite3').verbose();
|
||||||
|
const path = require('path');
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.static('public'));
|
||||||
|
|
||||||
|
// Initialize SQLite database
|
||||||
|
const db = new sqlite3.Database('./products.db', (err) => {
|
||||||
|
if (err) console.error(err.message);
|
||||||
|
console.log('Connected to products database.');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create products table
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS products (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
mpn TEXT NOT NULL,
|
||||||
|
sn TEXT NOT NULL,
|
||||||
|
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
createdBy TEXT NOT NULL
|
||||||
|
)`);
|
||||||
|
|
||||||
|
// API Routes
|
||||||
|
|
||||||
|
// Create products (batch insert)
|
||||||
|
app.post('/api/product/create', (req, res) => {
|
||||||
|
const products = req.body;
|
||||||
|
|
||||||
|
if (!Array.isArray(products) || products.length === 0) {
|
||||||
|
return res.status(400).json({ error: 'Expected an array of products' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const stmt = db.prepare('INSERT INTO products (mpn, sn, createdBy) VALUES (?, ?, ?)');
|
||||||
|
let inserted = 0;
|
||||||
|
let errors = [];
|
||||||
|
|
||||||
|
products.forEach((product, index) => {
|
||||||
|
if (!product.mpn || !product.sn || !product.createdBy) {
|
||||||
|
errors.push(`Product at index ${index} is missing required fields`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt.run(product.mpn, product.sn, product.createdBy, (err) => {
|
||||||
|
if (err) errors.push(`Error inserting product at index ${index}: ${err.message}`);
|
||||||
|
else inserted++;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
stmt.finalize(() => {
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
inserted,
|
||||||
|
errors: errors.length > 0 ? errors : undefined
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get products with pagination, search, and sort
|
||||||
|
app.get('/api/product/get', (req, res) => {
|
||||||
|
const page = parseInt(req.query.page) || 1;
|
||||||
|
const limit = parseInt(req.query.limit) || 50;
|
||||||
|
const search = req.query.search || '';
|
||||||
|
const sortBy = req.query.sortBy || 'id';
|
||||||
|
const sortOrder = req.query.sortOrder || 'DESC';
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
// Validate sortBy to prevent SQL injection
|
||||||
|
const validColumns = ['id', 'mpn', 'sn', 'createdAt', 'createdBy'];
|
||||||
|
const column = validColumns.includes(sortBy) ? sortBy : 'id';
|
||||||
|
const order = sortOrder.toUpperCase() === 'ASC' ? 'ASC' : 'DESC';
|
||||||
|
|
||||||
|
let query = 'SELECT * FROM products';
|
||||||
|
let countQuery = 'SELECT COUNT(*) as total FROM products';
|
||||||
|
let params = [];
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
const searchCondition = ` WHERE mpn LIKE ? OR sn LIKE ? OR createdBy LIKE ? OR id LIKE ?`;
|
||||||
|
query += searchCondition;
|
||||||
|
countQuery += searchCondition;
|
||||||
|
const searchParam = `%${search}%`;
|
||||||
|
params = [searchParam, searchParam, searchParam, searchParam];
|
||||||
|
}
|
||||||
|
|
||||||
|
query += ` ORDER BY ${column} ${order} LIMIT ? OFFSET ?`;
|
||||||
|
params.push(limit, offset);
|
||||||
|
|
||||||
|
db.get(countQuery, search ? params.slice(0, 4) : [], (err, countRow) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
db.all(query, params, (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
products: rows,
|
||||||
|
total: countRow.total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages: Math.ceil(countRow.total / limit)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete product
|
||||||
|
app.delete('/api/product/delete/:id', (req, res) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
|
||||||
|
db.run('DELETE FROM products WHERE id = ?', id, function(err) {
|
||||||
|
if (err) {
|
||||||
|
return res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.changes === 0) {
|
||||||
|
return res.status(404).json({ error: 'Product not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, deleted: this.changes });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Serve HTML UI
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 4444;
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Server is running on http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue