update product batch
This commit is contained in:
parent
81a1448ed3
commit
8b4f817d49
262
README.md
262
README.md
|
|
@ -0,0 +1,262 @@
|
|||
# Batch Management System
|
||||
|
||||
A comprehensive system for managing batches with items and mixed items.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ Create batches with valid items (same MPN) and mixed items (different MPN)
|
||||
- 📊 View all batches with expandable item lists
|
||||
- 🔍 Search batches by name or ID
|
||||
- 🗑️ Delete batches (cascade deletes all items)
|
||||
- 📄 Pagination and sorting
|
||||
- 🎨 Modern, responsive UI
|
||||
|
||||
## Database Structure
|
||||
|
||||
### Tables
|
||||
|
||||
**batches**
|
||||
- `id` - Auto-increment primary key
|
||||
- `batch_name` - Unique batch identifier
|
||||
- `createdAt` - Timestamp
|
||||
|
||||
**items** (Valid items with same MPN)
|
||||
- `id` - Auto-increment primary key
|
||||
- `batch_id` - Foreign key to batches
|
||||
- `mpn` - Manufacturer Part Number
|
||||
- `sn` - Serial Number
|
||||
- `createdAt` - Timestamp
|
||||
|
||||
**items_mix** (Mixed items with different MPN)
|
||||
- `id` - Auto-increment primary key
|
||||
- `batch_id` - Foreign key to batches
|
||||
- `mpn` - Manufacturer Part Number
|
||||
- `sn` - Serial Number
|
||||
- `createdAt` - Timestamp
|
||||
|
||||
## Installation
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Start the server:
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
3. For development with auto-reload:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
4. Open browser to:
|
||||
```
|
||||
http://localhost:4444
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
### 1. Save Batch
|
||||
|
||||
**POST** `/api/batch/save`
|
||||
|
||||
Create a new batch with items and optional mixed items.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"batch_name": "BATCH_20250210141530",
|
||||
"items": [
|
||||
{
|
||||
"mpn": "CN_M378A5143DB0-CPB_1620",
|
||||
"sn": "U03A00062074544398"
|
||||
},
|
||||
{
|
||||
"mpn": "CN_M378A5143DB0-CPB_1620",
|
||||
"sn": "U03A00062074544399"
|
||||
}
|
||||
],
|
||||
"items_mix": [
|
||||
{
|
||||
"mpn": "CN_M378A5143DB0-CPB_1621",
|
||||
"sn": "U03A00062074544403"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"batch_id": 1,
|
||||
"batch_name": "BATCH_20250210141530",
|
||||
"inserted_items": 2,
|
||||
"inserted_mix_items": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Get All Batches
|
||||
|
||||
**GET** `/api/batch/get-all`
|
||||
|
||||
Retrieve all batches with pagination, search, and sorting.
|
||||
|
||||
**Query Parameters:**
|
||||
- `page` (default: 1) - Page number
|
||||
- `limit` (default: 50) - Items per page
|
||||
- `search` - Search by batch name or ID
|
||||
- `sortBy` (default: "id") - Column to sort by (id, batch_name, createdAt)
|
||||
- `sortOrder` (default: "DESC") - Sort order (ASC, DESC)
|
||||
|
||||
**Example:**
|
||||
```
|
||||
GET /api/batch/get-all?page=1&limit=50&sortBy=createdAt&sortOrder=DESC
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"batches": [
|
||||
{
|
||||
"id": 1,
|
||||
"batch_name": "BATCH_20250210141530",
|
||||
"createdAt": "2025-02-10 14:15:30",
|
||||
"items": [
|
||||
{
|
||||
"mpn": "CN_M378A5143DB0-CPB_1620",
|
||||
"sn": "U03A00062074544398",
|
||||
"createdAt": "2025-02-10 14:15:30"
|
||||
}
|
||||
],
|
||||
"items_mix": [
|
||||
{
|
||||
"mpn": "CN_M378A5143DB0-CPB_1621",
|
||||
"sn": "U03A00062074544403",
|
||||
"createdAt": "2025-02-10 14:15:30"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"limit": 50,
|
||||
"totalPages": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Get Single Batch
|
||||
|
||||
**GET** `/api/batch/get/:id`
|
||||
|
||||
Get a specific batch by ID with all its items.
|
||||
|
||||
**Example:**
|
||||
```
|
||||
GET /api/batch/get/1
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"batch_name": "BATCH_20250210141530",
|
||||
"createdAt": "2025-02-10 14:15:30",
|
||||
"items": [...],
|
||||
"items_mix": [...]
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Delete Batch
|
||||
|
||||
**DELETE** `/api/batch/delete/:id`
|
||||
|
||||
Delete a batch and all its items (cascade delete).
|
||||
|
||||
**Example:**
|
||||
```
|
||||
DELETE /api/batch/delete/1
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"deleted": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Search Items
|
||||
|
||||
**GET** `/api/items/search`
|
||||
|
||||
Search for items across all batches.
|
||||
|
||||
**Query Parameters:**
|
||||
- `q` (required) - Search query for MPN or SN
|
||||
|
||||
**Example:**
|
||||
```
|
||||
GET /api/items/search?q=U03A00062074544398
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"batch_id": 1,
|
||||
"batch_name": "BATCH_20250210141530",
|
||||
"mpn": "CN_M378A5143DB0-CPB_1620",
|
||||
"sn": "U03A00062074544398",
|
||||
"createdAt": "2025-02-10 14:15:30",
|
||||
"type": "items"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Frontend Features
|
||||
|
||||
### Batch Table
|
||||
- Click on batch row to expand and view items
|
||||
- Items are displayed in two sections:
|
||||
- ✅ Valid Items (same MPN)
|
||||
- ⚠️ Mixed Items (different MPN)
|
||||
|
||||
### Create Batch Modal
|
||||
- Auto-generates batch name with timestamp
|
||||
- JSON input for items and mixed items
|
||||
- Validation for required fields and JSON format
|
||||
|
||||
### Search & Sort
|
||||
- Real-time search with debouncing
|
||||
- Sort by ID, Batch Name, or Created At
|
||||
- Toggle between ASC/DESC order
|
||||
|
||||
## Technologies
|
||||
|
||||
- **Backend:** Node.js, Express.js
|
||||
- **Database:** SQLite3
|
||||
- **Frontend:** Vanilla JavaScript, HTML5, CSS3
|
||||
- **Features:** RESTful API, Responsive Design
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── server.js # Express server with API routes
|
||||
├── index.html # Frontend UI
|
||||
├── package.json # Node.js dependencies
|
||||
├── products.db # SQLite database (auto-generated)
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Database is automatically created on first run
|
||||
- All timestamps are stored in UTC and converted to local time in UI
|
||||
- Cascade delete ensures all items are removed when batch is deleted
|
||||
- Foreign key constraints maintain data integrity
|
||||
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
|
|
@ -1,16 +1,20 @@
|
|||
{
|
||||
"name": "scanramproducts",
|
||||
"name": "batch-management-system",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"description": "Batch management system with items and mixed items",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"keywords": ["batch", "management", "sqlite", "express"],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^5.2.1",
|
||||
"sqlite3": "^5.1.7"
|
||||
"express": "^4.18.2",
|
||||
"sqlite3": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Product Management</title>
|
||||
<title>Batch Management</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
|
|
@ -94,6 +94,16 @@
|
|||
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;
|
||||
|
|
@ -150,6 +160,100 @@
|
|||
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;
|
||||
|
|
@ -166,6 +270,16 @@
|
|||
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;
|
||||
|
|
@ -223,7 +337,101 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -252,163 +460,87 @@
|
|||
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>
|
||||
<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 ID, MPN, SN, or creator...">
|
||||
<input type="text" id="searchInput" placeholder="Search by batch name or ID...">
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="openCreateModal()">
|
||||
Add Product
|
||||
<button class="btn btn-success" onclick="openCreateModal()">
|
||||
➕ Create Batch
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<div id="loading" class="loading" style="display: none;">
|
||||
Loading data...
|
||||
Loading batches...
|
||||
</div>
|
||||
|
||||
<div id="empty" class="empty" style="display: none;">
|
||||
<h3>No products found</h3>
|
||||
<p>Add your first product to get started!</p>
|
||||
<h3>No batches found</h3>
|
||||
<p>Create your first batch to get started!</p>
|
||||
</div>
|
||||
|
||||
<table id="productTable" style="display: none;">
|
||||
<table id="batchTable" 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>
|
||||
<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="productBody"></tbody>
|
||||
<tbody id="batchBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pagination" id="pagination"></div>
|
||||
</div>
|
||||
|
||||
<!-- Create Product Modal -->
|
||||
<!-- Create Batch Modal -->
|
||||
<div id="createModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>Add New Product</h2>
|
||||
<h2>Create New Batch</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label>MPN *</label>
|
||||
<input type="text" id="mpnInput" placeholder="Enter MPN code">
|
||||
<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>Serial Number (SN) *</label>
|
||||
<input type="text" id="snInput" placeholder="Enter serial number">
|
||||
<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>Created By *</label>
|
||||
<input type="text" id="createdByInput" placeholder="Enter creator name">
|
||||
<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-primary" onclick="createProduct()">Create</button>
|
||||
<button class="btn btn-success" onclick="createBatch()">Create Batch</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -418,38 +550,43 @@
|
|||
let currentSort = 'id';
|
||||
let currentOrder = 'DESC';
|
||||
let searchTerm = '';
|
||||
let URL = "https://logs1.danielvu.com/mobile-api/"
|
||||
async function loadProducts() {
|
||||
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('productTable').style.display = 'none';
|
||||
document.getElementById('batchTable').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 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.products.length === 0) {
|
||||
if (data.batches.length === 0) {
|
||||
document.getElementById('empty').style.display = 'block';
|
||||
} else {
|
||||
document.getElementById('productTable').style.display = 'table';
|
||||
renderProducts(data.products);
|
||||
document.getElementById('batchTable').style.display = 'table';
|
||||
renderBatches(data.batches);
|
||||
renderPagination(data);
|
||||
}
|
||||
|
||||
updateSortIndicators();
|
||||
} catch (error) {
|
||||
console.error('Error loading products:', 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 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");
|
||||
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',
|
||||
|
|
@ -460,21 +597,59 @@
|
|||
hour12: false
|
||||
});
|
||||
|
||||
const isExpanded = expandedBatches.has(batch.id);
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>${product.id}</td>
|
||||
<td>${product.mpn}</td>
|
||||
<td>${product.sn}</td>
|
||||
<td>${formattedDate}</td>
|
||||
<td>${product.createdBy}</td>
|
||||
<tr class="batch-row ${isExpanded ? 'expanded' : ''}" onclick="toggleBatch(${batch.id})">
|
||||
<td>
|
||||
<button class="delete-btn" onclick="deleteProduct(${product.id})">Delete</button>
|
||||
<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 = '';
|
||||
|
|
@ -489,14 +664,14 @@
|
|||
}
|
||||
|
||||
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>`;
|
||||
html += `<span class="page-info">Total: ${data.total} batches</span>`;
|
||||
|
||||
pagination.innerHTML = html;
|
||||
}
|
||||
|
||||
function changePage(page) {
|
||||
currentPage = page;
|
||||
loadProducts();
|
||||
loadBatches();
|
||||
}
|
||||
|
||||
function sortTable(column) {
|
||||
|
|
@ -507,7 +682,7 @@
|
|||
currentOrder = 'DESC';
|
||||
}
|
||||
currentPage = 1;
|
||||
loadProducts();
|
||||
loadBatches();
|
||||
}
|
||||
|
||||
function updateSortIndicators() {
|
||||
|
|
@ -515,7 +690,7 @@
|
|||
th.classList.remove('sorted', 'asc');
|
||||
});
|
||||
|
||||
const columns = ['id', 'mpn', 'sn', 'createdAt', 'createdBy'];
|
||||
const columns = ['id', 'batch_name', 'createdAt'];
|
||||
const index = columns.indexOf(currentSort);
|
||||
if (index !== -1) {
|
||||
const th = document.querySelectorAll('th')[index];
|
||||
|
|
@ -532,73 +707,117 @@
|
|||
searchTimeout = setTimeout(() => {
|
||||
searchTerm = e.target.value;
|
||||
currentPage = 1;
|
||||
loadProducts();
|
||||
expandedBatches.clear();
|
||||
loadBatches();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
async function deleteProduct(id) {
|
||||
if (!confirm('Are you sure you want to delete this product?')) return;
|
||||
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}product/delete/${id}`, {
|
||||
const response = await fetch(`${URL}batch/delete/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
loadProducts();
|
||||
expandedBatches.delete(id);
|
||||
loadBatches();
|
||||
} else {
|
||||
alert('Error deleting product');
|
||||
alert('Error deleting batch');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting product:', error);
|
||||
alert('Error deleting product');
|
||||
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('mpnInput').value = '';
|
||||
document.getElementById('snInput').value = '';
|
||||
document.getElementById('createdByInput').value = '';
|
||||
document.getElementById('batchNameInput').value = '';
|
||||
document.getElementById('itemsInput').value = '';
|
||||
document.getElementById('itemsMixInput').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();
|
||||
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 (!mpn || !sn || !createdBy) {
|
||||
alert('Please fill in all fields');
|
||||
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 {
|
||||
const response = await fetch(URL+'product/create', {
|
||||
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([{ mpn, sn, createdBy }])
|
||||
body: JSON.stringify({ batch_name, items, items_mix })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
closeCreateModal();
|
||||
loadProducts();
|
||||
expandedBatches.clear();
|
||||
loadBatches();
|
||||
alert(`Batch created successfully!\nItems: ${result.inserted_items}\nMixed Items: ${result.inserted_mix_items}`);
|
||||
} else {
|
||||
alert('Error creating product');
|
||||
alert('Error creating batch: ' + result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating product:', error);
|
||||
alert('Error creating product');
|
||||
console.error('Error creating batch:', error);
|
||||
alert('Error creating batch: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Load products on page load
|
||||
loadProducts();
|
||||
// Load batches on page load
|
||||
loadBatches();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
311
server.js
311
server.js
|
|
@ -7,58 +7,139 @@ const app = express();
|
|||
app.use(express.json());
|
||||
app.use(express.static('public'));
|
||||
|
||||
// CORS middleware
|
||||
app.use((req, res, next) => {
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.header('Access-Control-Allow-Headers', 'Content-Type');
|
||||
if (req.method === 'OPTIONS') {
|
||||
return res.sendStatus(200);
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// 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
|
||||
)`);
|
||||
// Create tables
|
||||
db.serialize(() => {
|
||||
// Batches table
|
||||
db.run(`CREATE TABLE IF NOT EXISTS batches (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
batch_name TEXT NOT NULL UNIQUE,
|
||||
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`);
|
||||
|
||||
// API Routes
|
||||
// Items table (for valid items)
|
||||
db.run(`CREATE TABLE IF NOT EXISTS items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
batch_id INTEGER NOT NULL,
|
||||
mpn TEXT NOT NULL,
|
||||
sn TEXT NOT NULL,
|
||||
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (batch_id) REFERENCES batches(id) ON DELETE CASCADE
|
||||
)`);
|
||||
|
||||
// Create products (batch insert)
|
||||
app.post('/api/product/create', (req, res) => {
|
||||
const products = req.body;
|
||||
// Items_mix table (for mixed MPN items)
|
||||
db.run(`CREATE TABLE IF NOT EXISTS items_mix (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
batch_id INTEGER NOT NULL,
|
||||
mpn TEXT NOT NULL,
|
||||
sn TEXT NOT NULL,
|
||||
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (batch_id) REFERENCES batches(id) ON DELETE CASCADE
|
||||
)`);
|
||||
|
||||
if (!Array.isArray(products) || products.length === 0) {
|
||||
return res.status(400).json({ error: 'Expected an array of products' });
|
||||
// Create indexes for better performance
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_items_batch_id ON items(batch_id)`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_items_mix_batch_id ON items_mix(batch_id)`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_items_mpn ON items(mpn)`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_items_sn ON items(sn)`);
|
||||
});
|
||||
|
||||
// ==================== BATCH API ROUTES ====================
|
||||
|
||||
// Save batch with items and items_mix
|
||||
app.post('/api/batch/save', (req, res) => {
|
||||
const { batch_name, items, items_mix } = req.body;
|
||||
|
||||
if (!batch_name) {
|
||||
return res.status(400).json({ error: 'batch_name is required' });
|
||||
}
|
||||
|
||||
const stmt = db.prepare('INSERT INTO products (mpn, sn, createdBy) VALUES (?, ?, ?)');
|
||||
let inserted = 0;
|
||||
let errors = [];
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
return res.status(400).json({ error: 'items array is required and must not be empty' });
|
||||
}
|
||||
|
||||
products.forEach((product, index) => {
|
||||
if (!product.mpn || !product.sn || !product.createdBy) {
|
||||
errors.push(`Product at index ${index} is missing required fields`);
|
||||
return;
|
||||
}
|
||||
db.serialize(() => {
|
||||
db.run('BEGIN TRANSACTION');
|
||||
|
||||
stmt.run(product.mpn, product.sn, product.createdBy, (err) => {
|
||||
if (err) errors.push(`Error inserting product at index ${index}: ${err.message}`);
|
||||
else inserted++;
|
||||
});
|
||||
});
|
||||
// Insert batch
|
||||
db.run('INSERT INTO batches (batch_name) VALUES (?)', [batch_name], function(err) {
|
||||
if (err) {
|
||||
db.run('ROLLBACK');
|
||||
return res.status(500).json({ error: 'Batch name already exists or database error: ' + err.message });
|
||||
}
|
||||
|
||||
stmt.finalize(() => {
|
||||
res.json({
|
||||
success: true,
|
||||
inserted,
|
||||
errors: errors.length > 0 ? errors : undefined
|
||||
const batchId = this.lastID;
|
||||
let insertedItems = 0;
|
||||
let insertedMixItems = 0;
|
||||
let errors = [];
|
||||
|
||||
// Insert items
|
||||
const itemStmt = db.prepare('INSERT INTO items (batch_id, mpn, sn) VALUES (?, ?, ?)');
|
||||
items.forEach((item, index) => {
|
||||
if (!item.mpn || !item.sn) {
|
||||
errors.push(`Item at index ${index} is missing mpn or sn`);
|
||||
return;
|
||||
}
|
||||
itemStmt.run(batchId, item.mpn, item.sn, (err) => {
|
||||
if (err) errors.push(`Error inserting item at index ${index}: ${err.message}`);
|
||||
else insertedItems++;
|
||||
});
|
||||
});
|
||||
itemStmt.finalize();
|
||||
|
||||
// Insert items_mix if provided
|
||||
if (Array.isArray(items_mix) && items_mix.length > 0) {
|
||||
const mixStmt = db.prepare('INSERT INTO items_mix (batch_id, mpn, sn) VALUES (?, ?, ?)');
|
||||
items_mix.forEach((item, index) => {
|
||||
if (!item.mpn || !item.sn) {
|
||||
errors.push(`Mixed item at index ${index} is missing mpn or sn`);
|
||||
return;
|
||||
}
|
||||
mixStmt.run(batchId, item.mpn, item.sn, (err) => {
|
||||
if (err) errors.push(`Error inserting mixed item at index ${index}: ${err.message}`);
|
||||
else insertedMixItems++;
|
||||
});
|
||||
});
|
||||
mixStmt.finalize();
|
||||
}
|
||||
|
||||
db.run('COMMIT', (err) => {
|
||||
if (err) {
|
||||
db.run('ROLLBACK');
|
||||
return res.status(500).json({ error: 'Transaction failed: ' + err.message });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
batch_id: batchId,
|
||||
batch_name,
|
||||
inserted_items: insertedItems,
|
||||
inserted_mix_items: insertedMixItems,
|
||||
errors: errors.length > 0 ? errors : undefined
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Get products with pagination, search, and sort
|
||||
app.get('/api/product/get', (req, res) => {
|
||||
// Get all batches with their items and items_mix
|
||||
app.get('/api/batch/get-all', (req, res) => {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 50;
|
||||
const search = req.query.search || '';
|
||||
|
|
@ -66,64 +147,190 @@ app.get('/api/product/get', (req, res) => {
|
|||
const sortOrder = req.query.sortOrder || 'DESC';
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
// Validate sortBy to prevent SQL injection
|
||||
const validColumns = ['id', 'mpn', 'sn', 'createdAt', 'createdBy'];
|
||||
// Validate sortBy
|
||||
const validColumns = ['id', 'batch_name', 'createdAt'];
|
||||
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 query = 'SELECT * FROM batches';
|
||||
let countQuery = 'SELECT COUNT(*) as total FROM batches';
|
||||
let params = [];
|
||||
|
||||
if (search) {
|
||||
const searchCondition = ` WHERE mpn LIKE ? OR sn LIKE ? OR createdBy LIKE ? OR id LIKE ?`;
|
||||
const searchCondition = ` WHERE batch_name LIKE ? OR id LIKE ?`;
|
||||
query += searchCondition;
|
||||
countQuery += searchCondition;
|
||||
const searchParam = `%${search}%`;
|
||||
params = [searchParam, searchParam, searchParam, searchParam];
|
||||
params = [searchParam, searchParam];
|
||||
}
|
||||
|
||||
query += ` ORDER BY ${column} ${order} LIMIT ? OFFSET ?`;
|
||||
params.push(limit, offset);
|
||||
|
||||
db.get(countQuery, search ? params.slice(0, 4) : [], (err, countRow) => {
|
||||
db.get(countQuery, search ? params.slice(0, 2) : [], (err, countRow) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
|
||||
db.all(query, params, (err, rows) => {
|
||||
db.all(query, params, (err, batches) => {
|
||||
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)
|
||||
if (batches.length === 0) {
|
||||
return res.json({
|
||||
batches: [],
|
||||
total: 0,
|
||||
page,
|
||||
limit,
|
||||
totalPages: 0
|
||||
});
|
||||
}
|
||||
|
||||
// Get items for each batch
|
||||
let processed = 0;
|
||||
const batchesWithItems = [];
|
||||
|
||||
batches.forEach(batch => {
|
||||
// Get items
|
||||
db.all('SELECT mpn, sn, createdAt FROM items WHERE batch_id = ?', [batch.id], (err, items) => {
|
||||
if (err) {
|
||||
console.error('Error fetching items:', err);
|
||||
items = [];
|
||||
}
|
||||
|
||||
// Get items_mix
|
||||
db.all('SELECT mpn, sn, createdAt FROM items_mix WHERE batch_id = ?', [batch.id], (err, items_mix) => {
|
||||
if (err) {
|
||||
console.error('Error fetching items_mix:', err);
|
||||
items_mix = [];
|
||||
}
|
||||
|
||||
batchesWithItems.push({
|
||||
id: batch.id,
|
||||
batch_name: batch.batch_name,
|
||||
createdAt: batch.createdAt,
|
||||
items: items || [],
|
||||
items_mix: items_mix || []
|
||||
});
|
||||
|
||||
processed++;
|
||||
|
||||
if (processed === batches.length) {
|
||||
res.json({
|
||||
batches: batchesWithItems,
|
||||
total: countRow.total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(countRow.total / limit)
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Delete product
|
||||
app.delete('/api/product/delete/:id', (req, res) => {
|
||||
// Get single batch by ID
|
||||
app.get('/api/batch/get/:id', (req, res) => {
|
||||
const id = req.params.id;
|
||||
|
||||
db.run('DELETE FROM products WHERE id = ?', id, function(err) {
|
||||
db.get('SELECT * FROM batches WHERE id = ?', [id], (err, batch) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
|
||||
if (!batch) {
|
||||
return res.status(404).json({ error: 'Batch not found' });
|
||||
}
|
||||
|
||||
// Get items
|
||||
db.all('SELECT mpn, sn, createdAt FROM items WHERE batch_id = ?', [id], (err, items) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
|
||||
// Get items_mix
|
||||
db.all('SELECT mpn, sn, createdAt FROM items_mix WHERE batch_id = ?', [id], (err, items_mix) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
|
||||
res.json({
|
||||
id: batch.id,
|
||||
batch_name: batch.batch_name,
|
||||
createdAt: batch.createdAt,
|
||||
items: items || [],
|
||||
items_mix: items_mix || []
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Delete batch (cascade delete items and items_mix)
|
||||
app.delete('/api/batch/delete/:id', (req, res) => {
|
||||
const id = req.params.id;
|
||||
|
||||
db.run('DELETE FROM batches 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' });
|
||||
return res.status(404).json({ error: 'Batch not found' });
|
||||
}
|
||||
|
||||
res.json({ success: true, deleted: this.changes });
|
||||
});
|
||||
});
|
||||
|
||||
// Search items across all batches
|
||||
app.get('/api/items/search', (req, res) => {
|
||||
const search = req.query.q || '';
|
||||
|
||||
if (!search) {
|
||||
return res.status(400).json({ error: 'Search query is required' });
|
||||
}
|
||||
|
||||
const searchParam = `%${search}%`;
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
b.id as batch_id,
|
||||
b.batch_name,
|
||||
i.mpn,
|
||||
i.sn,
|
||||
i.createdAt,
|
||||
'items' as type
|
||||
FROM items i
|
||||
JOIN batches b ON i.batch_id = b.id
|
||||
WHERE i.mpn LIKE ? OR i.sn LIKE ?
|
||||
UNION ALL
|
||||
SELECT
|
||||
b.id as batch_id,
|
||||
b.batch_name,
|
||||
im.mpn,
|
||||
im.sn,
|
||||
im.createdAt,
|
||||
'items_mix' as type
|
||||
FROM items_mix im
|
||||
JOIN batches b ON im.batch_id = b.id
|
||||
WHERE im.mpn LIKE ? OR im.sn LIKE ?
|
||||
ORDER BY createdAt DESC
|
||||
LIMIT 100
|
||||
`;
|
||||
|
||||
db.all(query, [searchParam, searchParam, searchParam, searchParam], (err, results) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
|
||||
res.json({ results });
|
||||
});
|
||||
});
|
||||
|
||||
// Serve HTML UI
|
||||
app.get('/', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
// Test script for Batch Management API
|
||||
// Run with: node test-api.js
|
||||
|
||||
const BASE_URL = 'http://localhost:4444/api';
|
||||
|
||||
async function testAPI() {
|
||||
console.log('🚀 Testing Batch Management API\n');
|
||||
|
||||
// Test 1: Create Batch
|
||||
console.log('1️⃣ Creating a new batch...');
|
||||
try {
|
||||
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');
|
||||
|
||||
const createResponse = await fetch(`${BASE_URL}/batch/save`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
batch_name: `TEST_BATCH_${timestamp}`,
|
||||
items: [
|
||||
{ mpn: 'CN_M378A5143DB0-CPB_1620', sn: 'U03A00062074544398' },
|
||||
{ mpn: 'CN_M378A5143DB0-CPB_1620', sn: 'U03A00062074544399' },
|
||||
{ mpn: 'CN_M378A5143DB0-CPB_1620', sn: 'U03A00062074544400' }
|
||||
],
|
||||
items_mix: [
|
||||
{ mpn: 'CN_M378A5143DB0-CPB_1621', sn: 'U03A00062074544403' },
|
||||
{ mpn: 'CN_M378A5143DB0-CPB_1622', sn: 'U03A00062074544404' }
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
const createData = await createResponse.json();
|
||||
console.log('✅ Batch created:', createData);
|
||||
console.log(` - Batch ID: ${createData.batch_id}`);
|
||||
console.log(` - Items inserted: ${createData.inserted_items}`);
|
||||
console.log(` - Mixed items inserted: ${createData.inserted_mix_items}\n`);
|
||||
|
||||
const batchId = createData.batch_id;
|
||||
|
||||
// Test 2: Get All Batches
|
||||
console.log('2️⃣ Getting all batches...');
|
||||
const getAllResponse = await fetch(`${BASE_URL}/batch/get-all?page=1&limit=10`);
|
||||
const getAllData = await getAllResponse.json();
|
||||
console.log(`✅ Found ${getAllData.total} batches`);
|
||||
console.log(` - Page ${getAllData.page} of ${getAllData.totalPages}\n`);
|
||||
|
||||
// Test 3: Get Single Batch
|
||||
console.log(`3️⃣ Getting batch #${batchId}...`);
|
||||
const getSingleResponse = await fetch(`${BASE_URL}/batch/get/${batchId}`);
|
||||
const getSingleData = await getSingleResponse.json();
|
||||
console.log('✅ Batch details:');
|
||||
console.log(` - Batch Name: ${getSingleData.batch_name}`);
|
||||
console.log(` - Items: ${getSingleData.items.length}`);
|
||||
console.log(` - Mixed Items: ${getSingleData.items_mix.length}`);
|
||||
console.log(` - Created At: ${getSingleData.createdAt}\n`);
|
||||
|
||||
// Test 4: Search Items
|
||||
console.log('4️⃣ Searching for items with SN containing "544398"...');
|
||||
const searchResponse = await fetch(`${BASE_URL}/items/search?q=544398`);
|
||||
const searchData = await searchResponse.json();
|
||||
console.log(`✅ Found ${searchData.results.length} items`);
|
||||
if (searchData.results.length > 0) {
|
||||
console.log(' First result:', searchData.results[0]);
|
||||
}
|
||||
console.log();
|
||||
|
||||
// Test 5: Search Batches
|
||||
console.log('5️⃣ Searching for batches with "TEST" in name...');
|
||||
const searchBatchResponse = await fetch(`${BASE_URL}/batch/get-all?search=TEST`);
|
||||
const searchBatchData = await searchBatchResponse.json();
|
||||
console.log(`✅ Found ${searchBatchData.total} batches matching "TEST"\n`);
|
||||
|
||||
// Test 6: Delete Batch
|
||||
console.log(`6️⃣ Deleting batch #${batchId}...`);
|
||||
const deleteResponse = await fetch(`${BASE_URL}/batch/delete/${batchId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
const deleteData = await deleteResponse.json();
|
||||
console.log('✅ Batch deleted:', deleteData);
|
||||
console.log(` - Deleted count: ${deleteData.deleted}\n`);
|
||||
|
||||
console.log('🎉 All tests completed successfully!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Test failed:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Run tests
|
||||
testAPI();
|
||||
Loading…
Reference in New Issue