update UI batch

This commit is contained in:
Joseph Le 2026-03-13 18:18:20 +11:00
parent 8b4f817d49
commit def35fceff
4 changed files with 925 additions and 719 deletions

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

221
server.js
View File

@ -37,7 +37,9 @@ db.serialize(() => {
db.run(`CREATE TABLE IF NOT EXISTS items ( db.run(`CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
batch_id INTEGER NOT NULL, batch_id INTEGER NOT NULL,
brand TEXT,
mpn TEXT NOT NULL, mpn TEXT NOT NULL,
mpn_custom TEXT,
sn TEXT NOT NULL, sn TEXT NOT NULL,
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
@ -47,7 +49,9 @@ db.serialize(() => {
db.run(`CREATE TABLE IF NOT EXISTS items_mix ( db.run(`CREATE TABLE IF NOT EXISTS items_mix (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
batch_id INTEGER NOT NULL, batch_id INTEGER NOT NULL,
brand TEXT,
mpn TEXT NOT NULL, mpn TEXT NOT NULL,
mpn_custom TEXT,
sn TEXT NOT NULL, sn TEXT NOT NULL,
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
@ -58,12 +62,38 @@ db.serialize(() => {
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_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_mpn ON items(mpn)`);
db.run(`CREATE INDEX IF NOT EXISTS idx_items_sn ON items(sn)`); db.run(`CREATE INDEX IF NOT EXISTS idx_items_sn ON items(sn)`);
db.run(`CREATE INDEX IF NOT EXISTS idx_items_brand ON items(brand)`);
db.run(`CREATE INDEX IF NOT EXISTS idx_items_mix_brand ON items_mix(brand)`);
}); });
// ==================== BATCH API ROUTES ==================== // ==================== BATCH API ROUTES ====================
function runAsync(db, sql, params = []) {
return new Promise((resolve, reject) => {
db.run(sql, params, function (err) {
if (err) reject(err);
else resolve(this); // this.lastID, this.changes
});
});
}
function prepareRunAsync(stmt, params = []) {
return new Promise((resolve, reject) => {
stmt.run(params, function (err) {
if (err) reject(err);
else resolve();
});
});
}
function finalizeAsync(stmt) {
return new Promise((resolve, reject) => {
stmt.finalize((err) => (err ? reject(err) : resolve()));
});
}
// Save batch with items and items_mix // Save batch with items and items_mix
app.post('/api/batch/save', (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) {
@ -74,70 +104,108 @@ app.post('/api/batch/save', (req, res) => {
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' });
} }
db.serialize(() => { let insertedItems = 0;
db.run('BEGIN TRANSACTION'); let insertedMixItems = 0;
let errors = [];
try {
// BEGIN
await runAsync(db, 'BEGIN TRANSACTION');
// Insert batch // Insert batch
db.run('INSERT INTO batches (batch_name) VALUES (?)', [batch_name], function(err) { const batchResult = await runAsync(
if (err) { db,
db.run('ROLLBACK'); 'INSERT INTO batches (batch_name) VALUES (?)',
return res.status(500).json({ error: 'Batch name already exists or database error: ' + err.message }); [batch_name]
);
const batchId = batchResult.lastID;
// ===== Insert items =====
const itemStmt = db.prepare(
'INSERT INTO items (batch_id, brand, mpn, mpn_custom, sn) VALUES (?, ?, ?, ?, ?)'
);
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (!item.mpn || !item.sn) {
errors.push(`Item at index ${i} is missing mpn or sn`);
continue;
} }
const batchId = this.lastID; try {
let insertedItems = 0; await prepareRunAsync(itemStmt, [
let insertedMixItems = 0; batchId,
let errors = []; item.brand || null,
item.mpn,
item.mpn_custom || null,
item.sn
]);
insertedItems++;
} catch (err) {
errors.push(`Error inserting item at index ${i}: ${err.message}`);
}
}
await finalizeAsync(itemStmt);
// ===== 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 (?, ?, ?, ?, ?)'
);
for (let i = 0; i < items_mix.length; i++) {
const item = items_mix[i];
// Insert items
const itemStmt = db.prepare('INSERT INTO items (batch_id, mpn, sn) VALUES (?, ?, ?)');
items.forEach((item, index) => {
if (!item.mpn || !item.sn) { if (!item.mpn || !item.sn) {
errors.push(`Item at index ${index} is missing mpn or sn`); errors.push(`Mixed item at index ${i} is missing mpn or sn`);
return; continue;
} }
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 try {
if (Array.isArray(items_mix) && items_mix.length > 0) { await prepareRunAsync(mixStmt, [
const mixStmt = db.prepare('INSERT INTO items_mix (batch_id, mpn, sn) VALUES (?, ?, ?)'); batchId,
items_mix.forEach((item, index) => { item.brand || null,
if (!item.mpn || !item.sn) { item.mpn,
errors.push(`Mixed item at index ${index} is missing mpn or sn`); item.mpn_custom || null,
return; item.sn
} ]);
mixStmt.run(batchId, item.mpn, item.sn, (err) => { insertedMixItems++;
if (err) errors.push(`Error inserting mixed item at index ${index}: ${err.message}`); } catch (err) {
else insertedMixItems++; errors.push(`Error inserting mixed item at index ${i}: ${err.message}`);
}); }
});
mixStmt.finalize();
} }
db.run('COMMIT', (err) => { await finalizeAsync(mixStmt);
if (err) { }
db.run('ROLLBACK');
return res.status(500).json({ error: 'Transaction failed: ' + err.message });
}
res.json({ // COMMIT
success: true, await runAsync(db, 'COMMIT');
batch_id: batchId,
batch_name, return res.json({
inserted_items: insertedItems, success: true,
inserted_mix_items: insertedMixItems, batch_id: batchId,
errors: errors.length > 0 ? errors : undefined batch_name,
}); inserted_items: insertedItems,
}); inserted_mix_items: insertedMixItems,
errors: errors.length ? errors : undefined
}); });
});
} catch (err) {
// ROLLBACK nếu có lỗi nghiêm trọng
try {
await runAsync(db, 'ROLLBACK');
} catch (_) { }
return res.status(500).json({
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;
@ -157,12 +225,28 @@ app.get('/api/batch/get-all', (req, res) => {
let params = []; let params = [];
if (search) { if (search) {
const searchCondition = ` WHERE batch_name LIKE ? OR id LIKE ?`; const searchCondition = `
query += searchCondition; WHERE
countQuery += searchCondition; batch_name LIKE ?
const searchParam = `%${search}%`; OR id LIKE ?
params = [searchParam, searchParam]; OR EXISTS (
} SELECT 1 FROM items
WHERE items.batch_id = batches.id
AND sn LIKE ?
)
OR EXISTS (
SELECT 1 FROM items_mix
WHERE items_mix.batch_id = batches.id
AND sn LIKE ?
)
`;
query += searchCondition;
countQuery += searchCondition;
const searchParam = `%${search}%`;
params = [searchParam, searchParam, searchParam, searchParam];
}
query += ` ORDER BY ${column} ${order} LIMIT ? OFFSET ?`; query += ` ORDER BY ${column} ${order} LIMIT ? OFFSET ?`;
params.push(limit, offset); params.push(limit, offset);
@ -193,14 +277,14 @@ app.get('/api/batch/get-all', (req, res) => {
batches.forEach(batch => { batches.forEach(batch => {
// Get items // Get items
db.all('SELECT mpn, sn, createdAt FROM items WHERE batch_id = ?', [batch.id], (err, items) => { db.all('SELECT brand, mpn, mpn_custom, sn, createdAt FROM items WHERE batch_id = ?', [batch.id], (err, items) => {
if (err) { if (err) {
console.error('Error fetching items:', err); console.error('Error fetching items:', err);
items = []; items = [];
} }
// Get items_mix // Get items_mix
db.all('SELECT mpn, sn, createdAt FROM items_mix WHERE batch_id = ?', [batch.id], (err, 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_mix:', err);
items_mix = []; items_mix = [];
@ -246,13 +330,13 @@ app.get('/api/batch/get/:id', (req, res) => {
} }
// Get items // Get items
db.all('SELECT mpn, sn, createdAt FROM items WHERE batch_id = ?', [id], (err, 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 // Get items_mix
db.all('SELECT mpn, sn, createdAt FROM items_mix WHERE batch_id = ?', [id], (err, items_mix) => { db.all('SELECT brand, mpn, mpn_custom, sn, createdAt FROM items_mix WHERE batch_id = ?', [id], (err, items_mix) => {
if (err) { if (err) {
return res.status(500).json({ error: err.message }); return res.status(500).json({ error: err.message });
} }
@ -273,7 +357,7 @@ app.get('/api/batch/get/:id', (req, res) => {
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('DELETE FROM batches WHERE id = ?', id, function(err) { 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 });
} }
@ -300,29 +384,36 @@ app.get('/api/items/search', (req, res) => {
SELECT SELECT
b.id as batch_id, b.id as batch_id,
b.batch_name, b.batch_name,
i.brand,
i.mpn, i.mpn,
i.mpn_custom,
i.sn, i.sn,
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 ? WHERE i.mpn LIKE ? OR i.sn LIKE ? OR i.brand LIKE ? OR i.mpn_custom LIKE ?
UNION ALL UNION ALL
SELECT SELECT
b.id as batch_id, b.id as batch_id,
b.batch_name, b.batch_name,
im.brand,
im.mpn, im.mpn,
im.mpn_custom,
im.sn, im.sn,
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 ? WHERE im.mpn LIKE ? OR im.sn LIKE ? OR im.brand LIKE ? OR im.mpn_custom LIKE ?
ORDER BY createdAt DESC ORDER BY createdAt DESC
LIMIT 100 LIMIT 100
`; `;
db.all(query, [searchParam, searchParam, searchParam, searchParam], (err, results) => { db.all(query, [
searchParam, searchParam, searchParam, searchParam,
searchParam, searchParam, searchParam, searchParam
], (err, results) => {
if (err) { if (err) {
return res.status(500).json({ error: err.message }); return res.status(500).json({ error: err.message });
} }