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')); // CORS middleware app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { return res.sendStatus(200); } next(); }); // Initialize SQLite database const db = new sqlite3.Database('./products.db', (err) => { if (err) console.error(err.message); console.log('Connected to products database.'); }); // Create tables db.serialize(() => { // Batches table db.run(`CREATE TABLE IF NOT EXISTS batches ( id INTEGER PRIMARY KEY AUTOINCREMENT, batch_name TEXT NOT NULL UNIQUE, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP )`); // Items table (for valid items) db.run(`CREATE TABLE IF NOT EXISTS items ( id INTEGER PRIMARY KEY AUTOINCREMENT, batch_id INTEGER NOT NULL, mpn TEXT NOT NULL, sn TEXT NOT NULL, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (batch_id) REFERENCES batches(id) ON DELETE CASCADE )`); // 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 indexes for better performance db.run(`CREATE INDEX IF NOT EXISTS idx_items_batch_id ON items(batch_id)`); db.run(`CREATE INDEX IF NOT EXISTS idx_items_mix_batch_id ON items_mix(batch_id)`); db.run(`CREATE INDEX IF NOT EXISTS idx_items_mpn ON items(mpn)`); db.run(`CREATE INDEX IF NOT EXISTS idx_items_sn ON items(sn)`); }); // ==================== BATCH API ROUTES ==================== // Save batch with items and items_mix app.post('/api/batch/save', (req, res) => { const { batch_name, items, items_mix } = req.body; if (!batch_name) { return res.status(400).json({ error: 'batch_name is required' }); } if (!Array.isArray(items) || items.length === 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 = []; // Insert items const itemStmt = db.prepare('INSERT INTO items (batch_id, mpn, sn) VALUES (?, ?, ?)'); items.forEach((item, index) => { if (!item.mpn || !item.sn) { errors.push(`Item at index ${index} is missing mpn or sn`); return; } itemStmt.run(batchId, item.mpn, item.sn, (err) => { if (err) errors.push(`Error inserting item at index ${index}: ${err.message}`); else insertedItems++; }); }); itemStmt.finalize(); // Insert items_mix if provided if (Array.isArray(items_mix) && items_mix.length > 0) { const mixStmt = db.prepare('INSERT INTO items_mix (batch_id, mpn, sn) VALUES (?, ?, ?)'); items_mix.forEach((item, index) => { if (!item.mpn || !item.sn) { errors.push(`Mixed item at index ${index} is missing mpn or sn`); return; } mixStmt.run(batchId, item.mpn, item.sn, (err) => { if (err) errors.push(`Error inserting mixed item at index ${index}: ${err.message}`); else insertedMixItems++; }); }); mixStmt.finalize(); } db.run('COMMIT', (err) => { if (err) { db.run('ROLLBACK'); return res.status(500).json({ error: 'Transaction failed: ' + err.message }); } res.json({ success: true, batch_id: batchId, batch_name, inserted_items: insertedItems, inserted_mix_items: insertedMixItems, errors: errors.length > 0 ? errors : undefined }); }); }); }); }); // Get all batches with their items and items_mix app.get('/api/batch/get-all', (req, res) => { const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 50; const search = req.query.search || ''; 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'; let query = 'SELECT * FROM batches'; let countQuery = 'SELECT COUNT(*) as total FROM batches'; let params = []; if (search) { const searchCondition = ` WHERE batch_name LIKE ? OR id LIKE ?`; query += searchCondition; countQuery += searchCondition; const searchParam = `%${search}%`; params = [searchParam, searchParam]; } query += ` ORDER BY ${column} ${order} LIMIT ? OFFSET ?`; params.push(limit, offset); db.get(countQuery, search ? params.slice(0, 2) : [], (err, countRow) => { if (err) { return res.status(500).json({ error: err.message }); } db.all(query, params, (err, batches) => { if (err) { 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({ 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) => { const id = req.params.id; db.get('SELECT * FROM batches WHERE id = ?', [id], (err, batch) => { if (err) { return res.status(500).json({ error: err.message }); } if (!batch) { return res.status(404).json({ error: 'Batch not found' }); } // Get items db.all('SELECT mpn, sn, createdAt FROM items WHERE batch_id = ?', [id], (err, items) => { if (err) { return res.status(500).json({ error: err.message }); } // Get items_mix db.all('SELECT mpn, sn, createdAt FROM items_mix WHERE batch_id = ?', [id], (err, items_mix) => { if (err) { return res.status(500).json({ error: err.message }); } res.json({ id: batch.id, batch_name: batch.batch_name, createdAt: batch.createdAt, items: items || [], items_mix: items_mix || [] }); }); }); }); }); // Delete batch (cascade delete items and items_mix) app.delete('/api/batch/delete/:id', (req, res) => { const id = req.params.id; db.run('DELETE FROM batches WHERE id = ?', id, function(err) { if (err) { return res.status(500).json({ error: err.message }); } if (this.changes === 0) { return res.status(404).json({ error: 'Batch not found' }); } res.json({ success: true, deleted: this.changes }); }); }); // Search items across all batches app.get('/api/items/search', (req, res) => { const search = req.query.q || ''; if (!search) { return res.status(400).json({ error: 'Search query is required' }); } const searchParam = `%${search}%`; const query = ` SELECT b.id as batch_id, b.batch_name, i.mpn, i.sn, i.createdAt, 'items' as type FROM items i JOIN batches b ON i.batch_id = b.id WHERE i.mpn LIKE ? OR i.sn LIKE ? UNION ALL SELECT b.id as batch_id, b.batch_name, im.mpn, im.sn, im.createdAt, 'items_mix' as type FROM items_mix im JOIN batches b ON im.batch_id = b.id WHERE im.mpn LIKE ? OR im.sn LIKE ? ORDER BY createdAt DESC LIMIT 100 `; db.all(query, [searchParam, searchParam, searchParam, searchParam], (err, results) => { if (err) { return res.status(500).json({ error: err.message }); } res.json({ results }); }); }); // Serve HTML UI app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); const PORT = process.env.PORT || 4444; app.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); });