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",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "Batch management system with items and mixed items",
|
||||||
"main": "index.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"start": "node server.js",
|
||||||
|
"dev": "nodemon server.js"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": ["batch", "management", "sqlite", "express"],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^5.2.1",
|
"express": "^4.18.2",
|
||||||
"sqlite3": "^5.1.7"
|
"sqlite3": "^5.1.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Product Management</title>
|
<title>Batch Management</title>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 1400px;
|
max-width: 1600px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
|
|
@ -94,6 +94,16 @@
|
||||||
box-shadow: 0 4px 12px rgba(44, 62, 80, 0.4);
|
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 {
|
.table-container {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
padding: 0 30px 30px;
|
padding: 0 30px 30px;
|
||||||
|
|
@ -150,6 +160,100 @@
|
||||||
background: #f8f9fa;
|
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 {
|
.delete-btn {
|
||||||
background: #dc3545;
|
background: #dc3545;
|
||||||
color: white;
|
color: white;
|
||||||
|
|
@ -166,6 +270,16 @@
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.expand-icon {
|
||||||
|
display: inline-block;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-icon.expanded {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
@ -223,7 +337,101 @@
|
||||||
color: #6c757d;
|
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) {
|
@media (max-width: 768px) {
|
||||||
|
.items-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
@ -252,163 +460,87 @@
|
||||||
padding: 20px;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>Product Management</h1>
|
<h1>📦 Batch Management System</h1>
|
||||||
<p>Manage your products efficiently</p>
|
<p>Manage batches with items and mixed items</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="search-box">
|
<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>
|
</div>
|
||||||
<button class="btn btn-primary" onclick="openCreateModal()">
|
<button class="btn btn-success" onclick="openCreateModal()">
|
||||||
Add Product
|
➕ Create Batch
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<div id="loading" class="loading" style="display: none;">
|
<div id="loading" class="loading" style="display: none;">
|
||||||
Loading data...
|
Loading batches...
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="empty" class="empty" style="display: none;">
|
<div id="empty" class="empty" style="display: none;">
|
||||||
<h3>No products found</h3>
|
<h3>No batches found</h3>
|
||||||
<p>Add your first product to get started!</p>
|
<p>Create your first batch to get started!</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table id="productTable" style="display: none;">
|
<table id="batchTable" style="display: none;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th onclick="sortTable('id')">ID</th>
|
<th onclick="sortTable('id')" style="width: 80px;">ID</th>
|
||||||
<th onclick="sortTable('mpn')">MPN</th>
|
<th onclick="sortTable('batch_name')">Batch Name</th>
|
||||||
<th onclick="sortTable('sn')">SN</th>
|
<th style="width: 120px;">Items</th>
|
||||||
<th onclick="sortTable('createdAt')">Created At</th>
|
<th style="width: 120px;">Mixed Items</th>
|
||||||
<th onclick="sortTable('createdBy')">Created By</th>
|
<th onclick="sortTable('createdAt')" style="width: 180px;">Created At</th>
|
||||||
<th>Actions</th>
|
<th style="width: 100px;">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="productBody"></tbody>
|
<tbody id="batchBody"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pagination" id="pagination"></div>
|
<div class="pagination" id="pagination"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Create Product Modal -->
|
<!-- Create Batch Modal -->
|
||||||
<div id="createModal" class="modal">
|
<div id="createModal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<h2>Add New Product</h2>
|
<h2>Create New Batch</h2>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>MPN *</label>
|
<label>Batch Name *</label>
|
||||||
<input type="text" id="mpnInput" placeholder="Enter MPN code">
|
<input type="text" id="batchNameInput" placeholder="e.g., BATCH_20250210141530">
|
||||||
|
<div class="help-text">Format: BATCH_yyyymmddhhmmss or any unique name</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Serial Number (SN) *</label>
|
<label>Items (Valid - Same MPN) *</label>
|
||||||
<input type="text" id="snInput" placeholder="Enter serial number">
|
<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>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Created By *</label>
|
<label>Mixed Items (Different MPN) - Optional</label>
|
||||||
<input type="text" id="createdByInput" placeholder="Enter creator name">
|
<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>
|
||||||
|
|
||||||
<div class="modal-buttons">
|
<div class="modal-buttons">
|
||||||
<button class="btn btn-secondary" onclick="closeCreateModal()">Cancel</button>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -418,38 +550,43 @@
|
||||||
let currentSort = 'id';
|
let currentSort = 'id';
|
||||||
let currentOrder = 'DESC';
|
let currentOrder = 'DESC';
|
||||||
let searchTerm = '';
|
let searchTerm = '';
|
||||||
let URL = "https://logs1.danielvu.com/mobile-api/"
|
let URL = window.location.hostname === 'localhost'
|
||||||
async function loadProducts() {
|
? '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('loading').style.display = 'block';
|
||||||
document.getElementById('productTable').style.display = 'none';
|
document.getElementById('batchTable').style.display = 'none';
|
||||||
document.getElementById('empty').style.display = 'none';
|
document.getElementById('empty').style.display = 'none';
|
||||||
|
|
||||||
try {
|
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();
|
const data = await response.json();
|
||||||
|
|
||||||
document.getElementById('loading').style.display = 'none';
|
document.getElementById('loading').style.display = 'none';
|
||||||
|
|
||||||
if (data.products.length === 0) {
|
if (data.batches.length === 0) {
|
||||||
document.getElementById('empty').style.display = 'block';
|
document.getElementById('empty').style.display = 'block';
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('productTable').style.display = 'table';
|
document.getElementById('batchTable').style.display = 'table';
|
||||||
renderProducts(data.products);
|
renderBatches(data.batches);
|
||||||
renderPagination(data);
|
renderPagination(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSortIndicators();
|
updateSortIndicators();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading products:', error);
|
console.error('Error loading batches:', error);
|
||||||
document.getElementById('loading').style.display = 'none';
|
document.getElementById('loading').style.display = 'none';
|
||||||
|
alert('Error loading batches. Please check if the server is running.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderProducts(products) {
|
function renderBatches(batches) {
|
||||||
const tbody = document.getElementById('productBody');
|
const tbody = document.getElementById('batchBody');
|
||||||
tbody.innerHTML = products.map(product => {
|
tbody.innerHTML = batches.map(batch => {
|
||||||
// Convert UTC to client's local timezone
|
const date = new Date(batch.createdAt + "Z");
|
||||||
const date = new Date(product.createdAt+"Z");
|
|
||||||
const formattedDate = date.toLocaleString('en-US', {
|
const formattedDate = date.toLocaleString('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
|
|
@ -460,21 +597,59 @@
|
||||||
hour12: false
|
hour12: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isExpanded = expandedBatches.has(batch.id);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr class="batch-row ${isExpanded ? 'expanded' : ''}" onclick="toggleBatch(${batch.id})">
|
||||||
<td>${product.id}</td>
|
|
||||||
<td>${product.mpn}</td>
|
|
||||||
<td>${product.sn}</td>
|
|
||||||
<td>${formattedDate}</td>
|
|
||||||
<td>${product.createdBy}</td>
|
|
||||||
<td>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleBatch(batchId) {
|
||||||
|
if (expandedBatches.has(batchId)) {
|
||||||
|
expandedBatches.delete(batchId);
|
||||||
|
} else {
|
||||||
|
expandedBatches.add(batchId);
|
||||||
|
}
|
||||||
|
loadBatches();
|
||||||
|
}
|
||||||
|
|
||||||
function renderPagination(data) {
|
function renderPagination(data) {
|
||||||
const pagination = document.getElementById('pagination');
|
const pagination = document.getElementById('pagination');
|
||||||
let html = '';
|
let html = '';
|
||||||
|
|
@ -489,14 +664,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
html += `<button class="page-btn" ${currentPage === data.totalPages ? 'disabled' : ''} onclick="changePage(${currentPage + 1})">Next</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>`;
|
html += `<span class="page-info">Total: ${data.total} batches</span>`;
|
||||||
|
|
||||||
pagination.innerHTML = html;
|
pagination.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePage(page) {
|
function changePage(page) {
|
||||||
currentPage = page;
|
currentPage = page;
|
||||||
loadProducts();
|
loadBatches();
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortTable(column) {
|
function sortTable(column) {
|
||||||
|
|
@ -507,7 +682,7 @@
|
||||||
currentOrder = 'DESC';
|
currentOrder = 'DESC';
|
||||||
}
|
}
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
loadProducts();
|
loadBatches();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSortIndicators() {
|
function updateSortIndicators() {
|
||||||
|
|
@ -515,7 +690,7 @@
|
||||||
th.classList.remove('sorted', 'asc');
|
th.classList.remove('sorted', 'asc');
|
||||||
});
|
});
|
||||||
|
|
||||||
const columns = ['id', 'mpn', 'sn', 'createdAt', 'createdBy'];
|
const columns = ['id', 'batch_name', 'createdAt'];
|
||||||
const index = columns.indexOf(currentSort);
|
const index = columns.indexOf(currentSort);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
const th = document.querySelectorAll('th')[index];
|
const th = document.querySelectorAll('th')[index];
|
||||||
|
|
@ -532,73 +707,117 @@
|
||||||
searchTimeout = setTimeout(() => {
|
searchTimeout = setTimeout(() => {
|
||||||
searchTerm = e.target.value;
|
searchTerm = e.target.value;
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
loadProducts();
|
expandedBatches.clear();
|
||||||
|
loadBatches();
|
||||||
}, 300);
|
}, 300);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function deleteProduct(id) {
|
async function deleteBatch(id) {
|
||||||
if (!confirm('Are you sure you want to delete this product?')) return;
|
if (!confirm('Are you sure you want to delete this batch and all its items?')) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${URL}product/delete/${id}`, {
|
const response = await fetch(`${URL}batch/delete/${id}`, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
loadProducts();
|
expandedBatches.delete(id);
|
||||||
|
loadBatches();
|
||||||
} else {
|
} else {
|
||||||
alert('Error deleting product');
|
alert('Error deleting batch');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting product:', error);
|
console.error('Error deleting batch:', error);
|
||||||
alert('Error deleting product');
|
alert('Error deleting batch');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openCreateModal() {
|
function openCreateModal() {
|
||||||
document.getElementById('createModal').classList.add('active');
|
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() {
|
function closeCreateModal() {
|
||||||
document.getElementById('createModal').classList.remove('active');
|
document.getElementById('createModal').classList.remove('active');
|
||||||
document.getElementById('mpnInput').value = '';
|
document.getElementById('batchNameInput').value = '';
|
||||||
document.getElementById('snInput').value = '';
|
document.getElementById('itemsInput').value = '';
|
||||||
document.getElementById('createdByInput').value = '';
|
document.getElementById('itemsMixInput').value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createProduct() {
|
async function createBatch() {
|
||||||
const mpn = document.getElementById('mpnInput').value.trim();
|
const batch_name = document.getElementById('batchNameInput').value.trim();
|
||||||
const sn = document.getElementById('snInput').value.trim();
|
const itemsText = document.getElementById('itemsInput').value.trim();
|
||||||
const createdBy = document.getElementById('createdByInput').value.trim();
|
const itemsMixText = document.getElementById('itemsMixInput').value.trim();
|
||||||
|
|
||||||
if (!mpn || !sn || !createdBy) {
|
if (!batch_name) {
|
||||||
alert('Please fill in all fields');
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'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) {
|
if (response.ok) {
|
||||||
closeCreateModal();
|
closeCreateModal();
|
||||||
loadProducts();
|
expandedBatches.clear();
|
||||||
|
loadBatches();
|
||||||
|
alert(`Batch created successfully!\nItems: ${result.inserted_items}\nMixed Items: ${result.inserted_mix_items}`);
|
||||||
} else {
|
} else {
|
||||||
alert('Error creating product');
|
alert('Error creating batch: ' + result.error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating product:', error);
|
console.error('Error creating batch:', error);
|
||||||
alert('Error creating product');
|
alert('Error creating batch: ' + error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load products on page load
|
// Load batches on page load
|
||||||
loadProducts();
|
loadBatches();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
279
server.js
279
server.js
|
|
@ -7,58 +7,139 @@ const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.static('public'));
|
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
|
// Initialize SQLite database
|
||||||
const db = new sqlite3.Database('./products.db', (err) => {
|
const db = new sqlite3.Database('./products.db', (err) => {
|
||||||
if (err) console.error(err.message);
|
if (err) console.error(err.message);
|
||||||
console.log('Connected to products database.');
|
console.log('Connected to products database.');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create products table
|
// Create tables
|
||||||
db.run(`CREATE TABLE IF NOT EXISTS products (
|
db.serialize(() => {
|
||||||
|
// Batches table
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS batches (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
batch_name TEXT NOT NULL UNIQUE,
|
||||||
|
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)`);
|
||||||
|
|
||||||
|
// 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,
|
mpn TEXT NOT NULL,
|
||||||
sn TEXT NOT NULL,
|
sn TEXT NOT NULL,
|
||||||
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
createdBy TEXT NOT NULL
|
FOREIGN KEY (batch_id) REFERENCES batches(id) ON DELETE CASCADE
|
||||||
)`);
|
)`);
|
||||||
|
|
||||||
// API Routes
|
// 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
|
||||||
|
)`);
|
||||||
|
|
||||||
// Create products (batch insert)
|
// Create indexes for better performance
|
||||||
app.post('/api/product/create', (req, res) => {
|
db.run(`CREATE INDEX IF NOT EXISTS idx_items_batch_id ON items(batch_id)`);
|
||||||
const products = req.body;
|
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)`);
|
||||||
|
});
|
||||||
|
|
||||||
if (!Array.isArray(products) || products.length === 0) {
|
// ==================== BATCH API ROUTES ====================
|
||||||
return res.status(400).json({ error: 'Expected an array of products' });
|
|
||||||
|
// 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 (?, ?, ?)');
|
if (!Array.isArray(items) || items.length === 0) {
|
||||||
let inserted = 0;
|
return res.status(400).json({ error: 'items array is required and must not be empty' });
|
||||||
|
}
|
||||||
|
|
||||||
|
db.serialize(() => {
|
||||||
|
db.run('BEGIN TRANSACTION');
|
||||||
|
|
||||||
|
// 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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const batchId = this.lastID;
|
||||||
|
let insertedItems = 0;
|
||||||
|
let insertedMixItems = 0;
|
||||||
let errors = [];
|
let errors = [];
|
||||||
|
|
||||||
products.forEach((product, index) => {
|
// Insert items
|
||||||
if (!product.mpn || !product.sn || !product.createdBy) {
|
const itemStmt = db.prepare('INSERT INTO items (batch_id, mpn, sn) VALUES (?, ?, ?)');
|
||||||
errors.push(`Product at index ${index} is missing required fields`);
|
items.forEach((item, index) => {
|
||||||
|
if (!item.mpn || !item.sn) {
|
||||||
|
errors.push(`Item at index ${index} is missing mpn or sn`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
itemStmt.run(batchId, item.mpn, item.sn, (err) => {
|
||||||
stmt.run(product.mpn, product.sn, product.createdBy, (err) => {
|
if (err) errors.push(`Error inserting item at index ${index}: ${err.message}`);
|
||||||
if (err) errors.push(`Error inserting product at index ${index}: ${err.message}`);
|
else insertedItems++;
|
||||||
else inserted++;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
|
||||||
stmt.finalize(() => {
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
inserted,
|
batch_id: batchId,
|
||||||
|
batch_name,
|
||||||
|
inserted_items: insertedItems,
|
||||||
|
inserted_mix_items: insertedMixItems,
|
||||||
errors: errors.length > 0 ? errors : undefined
|
errors: errors.length > 0 ? errors : undefined
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get products with pagination, search, and sort
|
// Get all batches with their items and items_mix
|
||||||
app.get('/api/product/get', (req, res) => {
|
app.get('/api/batch/get-all', (req, res) => {
|
||||||
const page = parseInt(req.query.page) || 1;
|
const page = parseInt(req.query.page) || 1;
|
||||||
const limit = parseInt(req.query.limit) || 50;
|
const limit = parseInt(req.query.limit) || 50;
|
||||||
const search = req.query.search || '';
|
const search = req.query.search || '';
|
||||||
|
|
@ -66,64 +147,190 @@ app.get('/api/product/get', (req, res) => {
|
||||||
const sortOrder = req.query.sortOrder || 'DESC';
|
const sortOrder = req.query.sortOrder || 'DESC';
|
||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
// Validate sortBy to prevent SQL injection
|
// Validate sortBy
|
||||||
const validColumns = ['id', 'mpn', 'sn', 'createdAt', 'createdBy'];
|
const validColumns = ['id', 'batch_name', 'createdAt'];
|
||||||
const column = validColumns.includes(sortBy) ? sortBy : 'id';
|
const column = validColumns.includes(sortBy) ? sortBy : 'id';
|
||||||
const order = sortOrder.toUpperCase() === 'ASC' ? 'ASC' : 'DESC';
|
const order = sortOrder.toUpperCase() === 'ASC' ? 'ASC' : 'DESC';
|
||||||
|
|
||||||
let query = 'SELECT * FROM products';
|
let query = 'SELECT * FROM batches';
|
||||||
let countQuery = 'SELECT COUNT(*) as total FROM products';
|
let countQuery = 'SELECT COUNT(*) as total FROM batches';
|
||||||
let params = [];
|
let params = [];
|
||||||
|
|
||||||
if (search) {
|
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;
|
query += searchCondition;
|
||||||
countQuery += searchCondition;
|
countQuery += searchCondition;
|
||||||
const searchParam = `%${search}%`;
|
const searchParam = `%${search}%`;
|
||||||
params = [searchParam, searchParam, searchParam, searchParam];
|
params = [searchParam, searchParam];
|
||||||
}
|
}
|
||||||
|
|
||||||
query += ` ORDER BY ${column} ${order} LIMIT ? OFFSET ?`;
|
query += ` ORDER BY ${column} ${order} LIMIT ? OFFSET ?`;
|
||||||
params.push(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) {
|
if (err) {
|
||||||
return res.status(500).json({ error: err.message });
|
return res.status(500).json({ error: err.message });
|
||||||
}
|
}
|
||||||
|
|
||||||
db.all(query, params, (err, rows) => {
|
db.all(query, params, (err, batches) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return res.status(500).json({ error: err.message });
|
return res.status(500).json({ error: err.message });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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({
|
res.json({
|
||||||
products: rows,
|
batches: batchesWithItems,
|
||||||
total: countRow.total,
|
total: countRow.total,
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
totalPages: Math.ceil(countRow.total / limit)
|
totalPages: Math.ceil(countRow.total / limit)
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete product
|
// Get single batch by ID
|
||||||
app.delete('/api/product/delete/:id', (req, res) => {
|
app.get('/api/batch/get/:id', (req, res) => {
|
||||||
const id = req.params.id;
|
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) {
|
if (err) {
|
||||||
return res.status(500).json({ error: err.message });
|
return res.status(500).json({ error: err.message });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.changes === 0) {
|
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 });
|
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
|
// Serve HTML UI
|
||||||
app.get('/', (req, res) => {
|
app.get('/', (req, res) => {
|
||||||
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
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