Thêm cột sn_custom vào items và items_mix

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
vo truong 2026-05-22 16:01:07 +07:00
parent d7bf701d0e
commit 72b4db8a94
1 changed files with 202 additions and 133 deletions

329
server.js
View File

@ -1,27 +1,27 @@
const express = require('express'); const express = require("express");
const sqlite3 = require('sqlite3').verbose(); const sqlite3 = require("sqlite3").verbose();
const path = require('path'); const path = require("path");
const app = express(); const app = express();
// Middleware // Middleware
app.use(express.json()); app.use(express.json());
app.use(express.static('public')); app.use(express.static("public"));
// CORS middleware // CORS middleware
app.use((req, res, next) => { app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*'); res.header("Access-Control-Allow-Origin", "*");
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.header('Access-Control-Allow-Headers', 'Content-Type'); res.header("Access-Control-Allow-Headers", "Content-Type");
if (req.method === 'OPTIONS') { if (req.method === "OPTIONS") {
return res.sendStatus(200); return res.sendStatus(200);
} }
next(); 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.");
}); });
function runInitAsync(sql, params = []) { function runInitAsync(sql, params = []) {
@ -52,10 +52,12 @@ async function initializeDatabase() {
// Migration for existing databases: add status column if missing // Migration for existing databases: add status column if missing
const columns = await allInitAsync(`PRAGMA table_info(batches)`); 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) { if (!hasStatusColumn) {
await runInitAsync(`ALTER TABLE batches ADD COLUMN status INTEGER NOT NULL DEFAULT 0`); await runInitAsync(
console.log('Added status column to batches table.'); `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 ( await runInitAsync(`CREATE TABLE IF NOT EXISTS items (
@ -65,6 +67,7 @@ async function initializeDatabase() {
mpn TEXT NOT NULL, mpn TEXT NOT NULL,
mpn_custom TEXT, mpn_custom TEXT,
sn TEXT NOT NULL, sn TEXT NOT NULL,
sn_custom TEXT,
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (batch_id) REFERENCES batches(id) ON DELETE CASCADE FOREIGN KEY (batch_id) REFERENCES batches(id) ON DELETE CASCADE
)`); )`);
@ -76,18 +79,42 @@ async function initializeDatabase() {
mpn TEXT NOT NULL, mpn TEXT NOT NULL,
mpn_custom TEXT, mpn_custom TEXT,
sn TEXT NOT NULL, sn TEXT NOT NULL,
sn_custom TEXT,
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (batch_id) REFERENCES batches(id) ON DELETE CASCADE 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 // Create indexes for better performance
await runInitAsync(`CREATE INDEX IF NOT EXISTS idx_items_batch_id ON items(batch_id)`); await runInitAsync(
await runInitAsync(`CREATE INDEX IF NOT EXISTS idx_items_mix_batch_id ON items_mix(batch_id)`); `CREATE INDEX IF NOT EXISTS idx_items_batch_id ON items(batch_id)`,
await runInitAsync(`CREATE INDEX IF NOT EXISTS idx_batches_status ON batches(status)`); );
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_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_sn ON items(sn)`);
await runInitAsync(`CREATE INDEX IF NOT EXISTS idx_items_brand ON items(brand)`); await runInitAsync(
await runInitAsync(`CREATE INDEX IF NOT EXISTS idx_items_mix_brand ON items_mix(brand)`); `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 ==================== // ==================== BATCH API ROUTES ====================
@ -117,15 +144,17 @@ function finalizeAsync(stmt) {
} }
// Save batch with items and items_mix // 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; const { batch_name, items, items_mix } = req.body;
if (!batch_name) { 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) { 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; let insertedItems = 0;
@ -134,20 +163,20 @@ app.post('/api/batch/save', async (req, res) => {
try { try {
// BEGIN // BEGIN
await runAsync(db, 'BEGIN TRANSACTION'); await runAsync(db, "BEGIN TRANSACTION");
// Insert batch // Insert batch
const batchResult = await runAsync( const batchResult = await runAsync(
db, db,
'INSERT INTO batches (batch_name) VALUES (?)', "INSERT INTO batches (batch_name) VALUES (?)",
[batch_name] [batch_name],
); );
const batchId = batchResult.lastID; const batchId = batchResult.lastID;
// ===== Insert items ===== // ===== Insert items =====
const itemStmt = db.prepare( 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++) { for (let i = 0; i < items.length; i++) {
@ -164,7 +193,8 @@ app.post('/api/batch/save', async (req, res) => {
item.brand || null, item.brand || null,
item.mpn, item.mpn,
item.mpn_custom || null, item.mpn_custom || null,
item.sn item.sn,
item.sn_custom || null,
]); ]);
insertedItems++; insertedItems++;
} catch (err) { } catch (err) {
@ -177,7 +207,7 @@ app.post('/api/batch/save', async (req, res) => {
// ===== Insert items_mix (optional) ===== // ===== Insert items_mix (optional) =====
if (Array.isArray(items_mix) && items_mix.length > 0) { if (Array.isArray(items_mix) && items_mix.length > 0) {
const mixStmt = db.prepare( 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++) { 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.brand || null,
item.mpn, item.mpn,
item.mpn_custom || null, item.mpn_custom || null,
item.sn item.sn,
item.sn_custom || null,
]); ]);
insertedMixItems++; insertedMixItems++;
} catch (err) { } 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 // COMMIT
await runAsync(db, 'COMMIT'); await runAsync(db, "COMMIT");
return res.json({ return res.json({
success: true, success: true,
@ -214,37 +247,35 @@ app.post('/api/batch/save', async (req, res) => {
batch_name, batch_name,
inserted_items: insertedItems, inserted_items: insertedItems,
inserted_mix_items: insertedMixItems, inserted_mix_items: insertedMixItems,
errors: errors.length ? errors : undefined errors: errors.length ? errors : undefined,
}); });
} catch (err) { } catch (err) {
// ROLLBACK nếu có lỗi nghiêm trọng // ROLLBACK nếu có lỗi nghiêm trọng
try { try {
await runAsync(db, 'ROLLBACK'); await runAsync(db, "ROLLBACK");
} catch (_) { } } catch (_) {}
return res.status(500).json({ return res.status(500).json({
error: err.message error: err.message,
}); });
} }
}); });
// Get all batches with their items and items_mix // 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 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 || "";
const sortBy = req.query.sortBy || 'id'; const sortBy = req.query.sortBy || "id";
const sortOrder = req.query.sortOrder || 'DESC'; const sortOrder = req.query.sortOrder || "DESC";
const offset = (page - 1) * limit; const offset = (page - 1) * limit;
// Validate sortBy // Validate sortBy
const validColumns = ['id', 'batch_name', 'createdAt']; 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";
const whereConditions = ['status = 0']; const whereConditions = ["status = 0"];
let searchParams = []; let searchParams = [];
if (search) { if (search) {
@ -270,7 +301,7 @@ app.get('/api/batch/get-all', (req, res) => {
searchParams = [searchParam, searchParam, searchParam, searchParam]; 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 countQuery = `SELECT COUNT(*) as total FROM batches${whereClause}`;
const query = `SELECT * FROM batches${whereClause} ORDER BY ${column} ${order} LIMIT ? OFFSET ?`; const query = `SELECT * FROM batches${whereClause} ORDER BY ${column} ${order} LIMIT ? OFFSET ?`;
const countParams = [...searchParams]; const countParams = [...searchParams];
@ -292,7 +323,7 @@ app.get('/api/batch/get-all', (req, res) => {
total: 0, total: 0,
page, page,
limit, limit,
totalPages: 0 totalPages: 0,
}); });
} }
@ -300,107 +331,131 @@ app.get('/api/batch/get-all', (req, res) => {
let processed = 0; let processed = 0;
const batchesWithItems = []; const batchesWithItems = [];
batches.forEach(batch => { batches.forEach((batch) => {
// Get items // Get items
db.all('SELECT brand, mpn, mpn_custom, sn, createdAt FROM items WHERE batch_id = ?', [batch.id], (err, items) => { db.all(
if (err) { "SELECT brand, mpn, mpn_custom, sn, sn_custom, createdAt FROM items WHERE batch_id = ?",
console.error('Error fetching items:', err); [batch.id],
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) => {
if (err) { if (err) {
console.error('Error fetching items_mix:', err); console.error("Error fetching items:", err);
items_mix = []; items = [];
} }
batchesWithItems.push({ // Get items_mix
id: batch.id, db.all(
batch_name: batch.batch_name, "SELECT brand, mpn, mpn_custom, sn, sn_custom, createdAt FROM items_mix WHERE batch_id = ?",
createdAt: batch.createdAt, [batch.id],
items: items || [], (err, items_mix) => {
items_mix: 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) { processed++;
res.json({
batches: batchesWithItems, if (processed === batches.length) {
total: countRow.total, res.json({
page, batches: batchesWithItems,
limit, total: countRow.total,
totalPages: Math.ceil(countRow.total / limit) page,
}); limit,
} totalPages: Math.ceil(countRow.total / limit),
}); });
}); }
},
);
},
);
}); });
}); });
}); });
}); });
// Get single batch by ID // 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; const id = req.params.id;
db.get('SELECT * FROM batches WHERE id = ? AND status = 0', [id], (err, batch) => { db.get(
if (err) { "SELECT * FROM batches WHERE id = ? AND status = 0",
return res.status(500).json({ error: err.message }); [id],
} (err, batch) => {
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) => {
if (err) { if (err) {
return res.status(500).json({ error: err.message }); return res.status(500).json({ error: err.message });
} }
// Get items_mix if (!batch) {
db.all('SELECT brand, mpn, mpn_custom, sn, createdAt FROM items_mix WHERE batch_id = ?', [id], (err, items_mix) => { return res.status(404).json({ error: "Batch not found" });
if (err) { }
return res.status(500).json({ error: err.message });
}
res.json({ // Get items
id: batch.id, db.all(
batch_name: batch.batch_name, "SELECT brand, mpn, mpn_custom, sn, sn_custom, createdAt FROM items WHERE batch_id = ?",
createdAt: batch.createdAt, [id],
items: items || [], (err, items) => {
items_mix: items_mix || [] 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) // 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; const id = req.params.id;
db.run('UPDATE batches SET status = 1 WHERE id = ? AND status = 0', [id], function (err) { db.run(
if (err) { "UPDATE batches SET status = 1 WHERE id = ? AND status = 0",
return res.status(500).json({ error: err.message }); [id],
} function (err) {
if (err) {
return res.status(500).json({ error: err.message });
}
if (this.changes === 0) { if (this.changes === 0) {
return res.status(404).json({ error: 'Batch not found' }); 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 // Search items across all batches
app.get('/api/items/search', (req, res) => { app.get("/api/items/search", (req, res) => {
const search = req.query.q || ''; const search = req.query.q || "";
if (!search) { 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 searchParam = `%${search}%`;
@ -413,11 +468,12 @@ app.get('/api/items/search', (req, res) => {
i.mpn, i.mpn,
i.mpn_custom, i.mpn_custom,
i.sn, i.sn,
i.sn_custom,
i.createdAt, i.createdAt,
'items' as type 'items' as type
FROM items i FROM items i
JOIN batches b ON i.batch_id = b.id 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 UNION ALL
SELECT SELECT
b.id as batch_id, b.id as batch_id,
@ -426,30 +482,43 @@ app.get('/api/items/search', (req, res) => {
im.mpn, im.mpn,
im.mpn_custom, im.mpn_custom,
im.sn, im.sn,
im.sn_custom,
im.createdAt, im.createdAt,
'items_mix' as type 'items_mix' as type
FROM items_mix im FROM items_mix im
JOIN batches b ON im.batch_id = b.id 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 ORDER BY createdAt DESC
LIMIT 100 LIMIT 100
`; `;
db.all(query, [ db.all(
searchParam, searchParam, searchParam, searchParam, query,
searchParam, searchParam, searchParam, searchParam [
], (err, results) => { searchParam,
if (err) { searchParam,
return res.status(500).json({ error: err.message }); 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 // 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"));
}); });
const PORT = process.env.PORT || 4444; const PORT = process.env.PORT || 4444;
@ -460,6 +529,6 @@ initializeDatabase()
}); });
}) })
.catch((err) => { .catch((err) => {
console.error('Database initialization failed:', err.message); console.error("Database initialization failed:", err.message);
process.exit(1); process.exit(1);
}); });