From 72b4db8a947c9c5947fe61565ee03bf5aefbeffe Mon Sep 17 00:00:00 2001 From: vo truong <41848815+vmtruong301296@users.noreply.github.com> Date: Fri, 22 May 2026 16:01:07 +0700 Subject: [PATCH] =?UTF-8?q?Th=C3=AAm=20c=E1=BB=99t=20sn=5Fcustom=20v=C3=A0?= =?UTF-8?q?o=20items=20v=C3=A0=20items=5Fmix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- server.js | 335 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 202 insertions(+), 133 deletions(-) diff --git a/server.js b/server.js index ccd62c9..38ac181 100644 --- a/server.js +++ b/server.js @@ -1,27 +1,27 @@ -const express = require('express'); -const sqlite3 = require('sqlite3').verbose(); -const path = require('path'); +const express = require("express"); +const sqlite3 = require("sqlite3").verbose(); +const path = require("path"); const app = express(); // Middleware app.use(express.json()); -app.use(express.static('public')); +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') { + 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) => { +const db = new sqlite3.Database("./products.db", (err) => { if (err) console.error(err.message); - console.log('Connected to products database.'); + console.log("Connected to products database."); }); function runInitAsync(sql, params = []) { @@ -52,10 +52,12 @@ async function initializeDatabase() { // Migration for existing databases: add status column if missing const columns = await allInitAsync(`PRAGMA table_info(batches)`); - const hasStatusColumn = columns.some((col) => col.name === 'status'); + const hasStatusColumn = columns.some((col) => col.name === "status"); if (!hasStatusColumn) { - await runInitAsync(`ALTER TABLE batches ADD COLUMN status INTEGER NOT NULL DEFAULT 0`); - console.log('Added status column to batches table.'); + await runInitAsync( + `ALTER TABLE batches ADD COLUMN status INTEGER NOT NULL DEFAULT 0`, + ); + console.log("Added status column to batches table."); } await runInitAsync(`CREATE TABLE IF NOT EXISTS items ( @@ -65,6 +67,7 @@ async function initializeDatabase() { mpn TEXT NOT NULL, mpn_custom TEXT, sn TEXT NOT NULL, + sn_custom TEXT, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (batch_id) REFERENCES batches(id) ON DELETE CASCADE )`); @@ -76,18 +79,42 @@ async function initializeDatabase() { mpn TEXT NOT NULL, mpn_custom TEXT, sn TEXT NOT NULL, + sn_custom TEXT, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (batch_id) REFERENCES batches(id) ON DELETE CASCADE )`); + // Migration for existing databases: add sn_custom column if missing + const itemsColumns = await allInitAsync(`PRAGMA table_info(items)`); + if (!itemsColumns.some((col) => col.name === "sn_custom")) { + await runInitAsync(`ALTER TABLE items ADD COLUMN sn_custom TEXT`); + console.log("Added sn_custom column to items table."); + } + + const itemsMixColumns = await allInitAsync(`PRAGMA table_info(items_mix)`); + if (!itemsMixColumns.some((col) => col.name === "sn_custom")) { + await runInitAsync(`ALTER TABLE items_mix ADD COLUMN sn_custom TEXT`); + console.log("Added sn_custom column to items_mix table."); + } + // Create indexes for better performance - await runInitAsync(`CREATE INDEX IF NOT EXISTS idx_items_batch_id ON items(batch_id)`); - await runInitAsync(`CREATE INDEX IF NOT EXISTS idx_items_mix_batch_id ON items_mix(batch_id)`); - await runInitAsync(`CREATE INDEX IF NOT EXISTS idx_batches_status ON batches(status)`); + await runInitAsync( + `CREATE INDEX IF NOT EXISTS idx_items_batch_id ON items(batch_id)`, + ); + await runInitAsync( + `CREATE INDEX IF NOT EXISTS idx_items_mix_batch_id ON items_mix(batch_id)`, + ); + await runInitAsync( + `CREATE INDEX IF NOT EXISTS idx_batches_status ON batches(status)`, + ); await runInitAsync(`CREATE INDEX IF NOT EXISTS idx_items_mpn ON items(mpn)`); await runInitAsync(`CREATE INDEX IF NOT EXISTS idx_items_sn ON items(sn)`); - await runInitAsync(`CREATE INDEX IF NOT EXISTS idx_items_brand ON items(brand)`); - await runInitAsync(`CREATE INDEX IF NOT EXISTS idx_items_mix_brand ON items_mix(brand)`); + await runInitAsync( + `CREATE INDEX IF NOT EXISTS idx_items_brand ON items(brand)`, + ); + await runInitAsync( + `CREATE INDEX IF NOT EXISTS idx_items_mix_brand ON items_mix(brand)`, + ); } // ==================== BATCH API ROUTES ==================== @@ -117,15 +144,17 @@ function finalizeAsync(stmt) { } // Save batch with items and items_mix -app.post('/api/batch/save', async (req, res) => { +app.post("/api/batch/save", async (req, res) => { const { batch_name, items, items_mix } = req.body; if (!batch_name) { - return res.status(400).json({ error: 'batch_name is required' }); + return res.status(400).json({ error: "batch_name is required" }); } if (!Array.isArray(items) || items.length === 0) { - return res.status(400).json({ error: 'items array is required and must not be empty' }); + return res + .status(400) + .json({ error: "items array is required and must not be empty" }); } let insertedItems = 0; @@ -134,20 +163,20 @@ app.post('/api/batch/save', async (req, res) => { try { // BEGIN - await runAsync(db, 'BEGIN TRANSACTION'); + await runAsync(db, "BEGIN TRANSACTION"); // Insert batch const batchResult = await runAsync( db, - 'INSERT INTO batches (batch_name) VALUES (?)', - [batch_name] + "INSERT INTO batches (batch_name) VALUES (?)", + [batch_name], ); const batchId = batchResult.lastID; // ===== Insert items ===== const itemStmt = db.prepare( - 'INSERT INTO items (batch_id, brand, mpn, mpn_custom, sn) VALUES (?, ?, ?, ?, ?)' + "INSERT INTO items (batch_id, brand, mpn, mpn_custom, sn, sn_custom) VALUES (?, ?, ?, ?, ?, ?)", ); for (let i = 0; i < items.length; i++) { @@ -164,7 +193,8 @@ app.post('/api/batch/save', async (req, res) => { item.brand || null, item.mpn, item.mpn_custom || null, - item.sn + item.sn, + item.sn_custom || null, ]); insertedItems++; } catch (err) { @@ -177,7 +207,7 @@ app.post('/api/batch/save', async (req, res) => { // ===== Insert items_mix (optional) ===== if (Array.isArray(items_mix) && items_mix.length > 0) { const mixStmt = db.prepare( - 'INSERT INTO items_mix (batch_id, brand, mpn, mpn_custom, sn) VALUES (?, ?, ?, ?, ?)' + "INSERT INTO items_mix (batch_id, brand, mpn, mpn_custom, sn, sn_custom) VALUES (?, ?, ?, ?, ?, ?)", ); for (let i = 0; i < items_mix.length; i++) { @@ -194,11 +224,14 @@ app.post('/api/batch/save', async (req, res) => { item.brand || null, item.mpn, item.mpn_custom || null, - item.sn + item.sn, + item.sn_custom || null, ]); insertedMixItems++; } catch (err) { - errors.push(`Error inserting mixed item at index ${i}: ${err.message}`); + errors.push( + `Error inserting mixed item at index ${i}: ${err.message}`, + ); } } @@ -206,7 +239,7 @@ app.post('/api/batch/save', async (req, res) => { } // COMMIT - await runAsync(db, 'COMMIT'); + await runAsync(db, "COMMIT"); return res.json({ success: true, @@ -214,37 +247,35 @@ app.post('/api/batch/save', async (req, res) => { batch_name, inserted_items: insertedItems, inserted_mix_items: insertedMixItems, - errors: errors.length ? errors : undefined + errors: errors.length ? errors : undefined, }); - } catch (err) { // ROLLBACK nếu có lỗi nghiêm trọng try { - await runAsync(db, 'ROLLBACK'); - } catch (_) { } + await runAsync(db, "ROLLBACK"); + } catch (_) {} return res.status(500).json({ - error: err.message + error: err.message, }); } }); - // Get all batches with their items and items_mix -app.get('/api/batch/get-all', (req, res) => { +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 || ''; - const sortBy = req.query.sortBy || 'id'; - const sortOrder = req.query.sortOrder || 'DESC'; + const search = req.query.search || ""; + const sortBy = req.query.sortBy || "id"; + const sortOrder = req.query.sortOrder || "DESC"; const offset = (page - 1) * limit; // Validate sortBy - const validColumns = ['id', 'batch_name', 'createdAt']; - const column = validColumns.includes(sortBy) ? sortBy : 'id'; - const order = sortOrder.toUpperCase() === 'ASC' ? 'ASC' : 'DESC'; + const validColumns = ["id", "batch_name", "createdAt"]; + const column = validColumns.includes(sortBy) ? sortBy : "id"; + const order = sortOrder.toUpperCase() === "ASC" ? "ASC" : "DESC"; - const whereConditions = ['status = 0']; + const whereConditions = ["status = 0"]; let searchParams = []; if (search) { @@ -270,7 +301,7 @@ app.get('/api/batch/get-all', (req, res) => { searchParams = [searchParam, searchParam, searchParam, searchParam]; } - const whereClause = ` WHERE ${whereConditions.join(' AND ')}`; + const whereClause = ` WHERE ${whereConditions.join(" AND ")}`; const countQuery = `SELECT COUNT(*) as total FROM batches${whereClause}`; const query = `SELECT * FROM batches${whereClause} ORDER BY ${column} ${order} LIMIT ? OFFSET ?`; const countParams = [...searchParams]; @@ -292,7 +323,7 @@ app.get('/api/batch/get-all', (req, res) => { total: 0, page, limit, - totalPages: 0 + totalPages: 0, }); } @@ -300,156 +331,194 @@ app.get('/api/batch/get-all', (req, res) => { let processed = 0; const batchesWithItems = []; - batches.forEach(batch => { + batches.forEach((batch) => { // Get items - db.all('SELECT brand, mpn, mpn_custom, 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 brand, mpn, mpn_custom, sn, createdAt FROM items_mix WHERE batch_id = ?', [batch.id], (err, items_mix) => { + db.all( + "SELECT brand, mpn, mpn_custom, sn, sn_custom, createdAt FROM items WHERE batch_id = ?", + [batch.id], + (err, items) => { if (err) { - console.error('Error fetching items_mix:', err); - items_mix = []; + console.error("Error fetching items:", err); + items = []; } - batchesWithItems.push({ - id: batch.id, - batch_name: batch.batch_name, - createdAt: batch.createdAt, - items: items || [], - items_mix: items_mix || [] - }); + // Get items_mix + db.all( + "SELECT brand, mpn, mpn_custom, sn, sn_custom, createdAt FROM items_mix WHERE batch_id = ?", + [batch.id], + (err, items_mix) => { + if (err) { + console.error("Error fetching items_mix:", err); + items_mix = []; + } - processed++; + batchesWithItems.push({ + id: batch.id, + batch_name: batch.batch_name, + createdAt: batch.createdAt, + items: items || [], + items_mix: items_mix || [], + }); - if (processed === batches.length) { - res.json({ - batches: batchesWithItems, - total: countRow.total, - page, - limit, - totalPages: Math.ceil(countRow.total / limit) - }); - } - }); - }); + processed++; + + if (processed === batches.length) { + res.json({ + batches: batchesWithItems, + total: countRow.total, + page, + limit, + totalPages: Math.ceil(countRow.total / limit), + }); + } + }, + ); + }, + ); }); }); }); }); // Get single batch by ID -app.get('/api/batch/get/:id', (req, res) => { +app.get("/api/batch/get/:id", (req, res) => { const id = req.params.id; - db.get('SELECT * FROM batches WHERE id = ? AND status = 0', [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 brand, mpn, mpn_custom, sn, createdAt FROM items WHERE batch_id = ?', [id], (err, items) => { + db.get( + "SELECT * FROM batches WHERE id = ? AND status = 0", + [id], + (err, batch) => { if (err) { return res.status(500).json({ error: err.message }); } - // Get items_mix - db.all('SELECT brand, mpn, mpn_custom, sn, createdAt FROM items_mix WHERE batch_id = ?', [id], (err, items_mix) => { - if (err) { - return res.status(500).json({ error: err.message }); - } + if (!batch) { + return res.status(404).json({ error: "Batch not found" }); + } - res.json({ - id: batch.id, - batch_name: batch.batch_name, - createdAt: batch.createdAt, - items: items || [], - items_mix: items_mix || [] - }); - }); - }); - }); + // Get items + db.all( + "SELECT brand, mpn, mpn_custom, sn, sn_custom, 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 brand, mpn, mpn_custom, sn, sn_custom, 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 || [], + }); + }, + ); + }, + ); + }, + ); }); // Soft delete batch (update status from 0 to 1) -app.delete('/api/batch/delete/:id', (req, res) => { +app.delete("/api/batch/delete/:id", (req, res) => { const id = req.params.id; - db.run('UPDATE batches SET status = 1 WHERE id = ? AND status = 0', [id], function (err) { - if (err) { - return res.status(500).json({ error: err.message }); - } + db.run( + "UPDATE batches SET status = 1 WHERE id = ? AND status = 0", + [id], + function (err) { + if (err) { + return res.status(500).json({ error: err.message }); + } - if (this.changes === 0) { - return res.status(404).json({ error: 'Batch not found' }); - } + if (this.changes === 0) { + return res.status(404).json({ error: "Batch not found" }); + } - res.json({ success: true, updated: this.changes, deleted: this.changes }); - }); + res.json({ success: true, updated: this.changes, deleted: this.changes }); + }, + ); }); // Search items across all batches -app.get('/api/items/search', (req, res) => { - const search = req.query.q || ''; +app.get("/api/items/search", (req, res) => { + const search = req.query.q || ""; if (!search) { - return res.status(400).json({ error: 'Search query is required' }); + return res.status(400).json({ error: "Search query is required" }); } const searchParam = `%${search}%`; const query = ` - SELECT + SELECT b.id as batch_id, b.batch_name, i.brand, i.mpn, i.mpn_custom, i.sn, + i.sn_custom, 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 ? OR i.brand LIKE ? OR i.mpn_custom LIKE ? + WHERE i.mpn LIKE ? OR i.sn LIKE ? OR i.brand LIKE ? OR i.mpn_custom LIKE ? OR i.sn_custom LIKE ? UNION ALL - SELECT + SELECT b.id as batch_id, b.batch_name, im.brand, im.mpn, im.mpn_custom, im.sn, + im.sn_custom, 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 ? OR im.brand LIKE ? OR im.mpn_custom LIKE ? + WHERE im.mpn LIKE ? OR im.sn LIKE ? OR im.brand LIKE ? OR im.mpn_custom LIKE ? OR im.sn_custom LIKE ? ORDER BY createdAt DESC LIMIT 100 `; - db.all(query, [ - searchParam, searchParam, searchParam, searchParam, - searchParam, searchParam, searchParam, searchParam - ], (err, results) => { - if (err) { - return res.status(500).json({ error: err.message }); - } + db.all( + query, + [ + searchParam, + searchParam, + searchParam, + searchParam, + searchParam, + searchParam, + searchParam, + searchParam, + searchParam, + searchParam, + ], + (err, results) => { + if (err) { + return res.status(500).json({ error: err.message }); + } - res.json({ results }); - }); + res.json({ results }); + }, + ); }); // Serve HTML UI -app.get('/', (req, res) => { - res.sendFile(path.join(__dirname, 'public', 'index.html')); +app.get("/", (req, res) => { + res.sendFile(path.join(__dirname, "public", "index.html")); }); const PORT = process.env.PORT || 4444; @@ -460,6 +529,6 @@ initializeDatabase() }); }) .catch((err) => { - console.error('Database initialization failed:', err.message); + console.error("Database initialization failed:", err.message); process.exit(1); - }); \ No newline at end of file + });