import express from 'express'; import mysql from 'mysql2/promise'; import cors from 'cors'; import dotenv from 'dotenv'; dotenv.config(); const app = express(); app.use(cors()); app.use(express.json({ limit: '100mb' })); // Request Logger app.use((req, res, next) => { console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); next(); }); const pool = mysql.createPool({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASS, database: process.env.DB_NAME, port: parseInt(process.env.DB_PORT || '3306'), charset: 'utf8mb4' }); const handleError = (res, err, context, isGet = false) => { console.error(`❌ [${context}] Error:`, err.message); if (isGet) res.json([]); else res.status(500).json({ error: err.message }); }; // --- API Implementation --- /** * Generic Fetcher for Asset Tables */ const fetchAssets = async (tableName, res, context) => { try { const [rows] = await pool.query(`SELECT * FROM ${tableName}`); console.log(`📡 [GET ${context}] Returning ${rows.length} rows from ${tableName}`); res.json(rows); } catch (err) { handleError(res, err, context, true); } }; /** * Generic Batch Saver for Asset Tables */ const saveAssetsBatch = async (tableName, items, res, context) => { const connection = await pool.getConnection(); try { await connection.beginTransaction(); // Get valid columns for this table const [cols] = await connection.query(`DESCRIBE ${tableName}`); const validColumns = cols.map(c => c.Field); // 1. Clear existing (or we could use UPSERT logic, but existing code used DELETE-INSERT pattern) await connection.query(`DELETE FROM ${tableName}`); // 2. Insert new items for (const item of items) { const filteredRow = {}; validColumns.forEach(col => { // Exclude auto-managed timestamps from manual insertion if (col === 'created_at' || col === 'updated_at') return; if (item[col] !== undefined) filteredRow[col] = item[col]; }); // Auto-generate ID if missing if (!filteredRow.id) filteredRow.id = Math.random().toString(36).substring(2, 9); await connection.query(`INSERT INTO ${tableName} SET ?`, [filteredRow]); } await connection.commit(); res.json({ success: true, count: items.length }); } catch (err) { await connection.rollback(); handleError(res, err, context); } finally { connection.release(); } }; // --- Routes --- // 0. User Management app.get('/api/users', (req, res) => fetchAssets('system_users', res, 'USERS')); app.post('/api/users/batch', (req, res) => saveAssetsBatch('system_users', req.body, res, 'USERS BATCH')); // 1. Hardware Assets app.get('/api/pc', (req, res) => fetchAssets('asset_pc', res, 'PC')); app.post('/api/pc/batch', (req, res) => saveAssetsBatch('asset_pc', req.body, res, 'PC BATCH')); app.get('/api/server', (req, res) => fetchAssets('asset_server', res, 'SERVER')); app.post('/api/server/batch', (req, res) => saveAssetsBatch('asset_server', req.body, res, 'SERVER BATCH')); app.get('/api/storage', (req, res) => fetchAssets('asset_storage', res, 'STORAGE')); app.post('/api/storage/batch', (req, res) => saveAssetsBatch('asset_storage', req.body, res, 'STORAGE BATCH')); app.get('/api/network', (req, res) => fetchAssets('asset_network', res, 'NETWORK')); app.post('/api/network/batch', (req, res) => saveAssetsBatch('asset_network', req.body, res, 'NETWORK BATCH')); // 2. Software Assets app.get('/api/sw/internal', (req, res) => fetchAssets('asset_sw_internal', res, 'SW INTERNAL')); app.post('/api/sw/internal/batch', (req, res) => saveAssetsBatch('asset_sw_internal', req.body, res, 'SW INTERNAL BATCH')); app.get('/api/sw/external', (req, res) => fetchAssets('asset_sw_external', res, 'SW EXTERNAL')); app.post('/api/sw/external/batch', (req, res) => saveAssetsBatch('asset_sw_external', req.body, res, 'SW EXTERNAL BATCH')); // 3. Other Assets app.get('/api/survey', (req, res) => fetchAssets('asset_survey', res, 'SURVEY')); app.post('/api/survey/batch', (req, res) => saveAssetsBatch('asset_survey', req.body, res, 'SURVEY BATCH')); app.get('/api/pc-parts', (req, res) => fetchAssets('asset_pc_parts', res, 'PC PARTS')); app.post('/api/pc-parts/batch', (req, res) => saveAssetsBatch('asset_pc_parts', req.body, res, 'PC PARTS BATCH')); app.get('/api/equipment', (req, res) => fetchAssets('asset_equipment', res, 'EQUIPMENT')); app.post('/api/equipment/batch', (req, res) => saveAssetsBatch('asset_equipment', req.body, res, 'EQUIPMENT BATCH')); app.get('/api/office-supplies', (req, res) => fetchAssets('asset_office_supplies', res, 'OFFICE SUPPLIES')); app.post('/api/office-supplies/batch', (req, res) => saveAssetsBatch('asset_office_supplies', req.body, res, 'OFFICE SUPPLIES BATCH')); app.get('/api/cloud', (req, res) => fetchAssets('asset_cloud', res, 'CLOUD')); app.post('/api/cloud/batch', (req, res) => saveAssetsBatch('asset_cloud', req.body, res, 'CLOUD BATCH')); app.get('/api/domain', (req, res) => fetchAssets('asset_domain', res, 'DOMAIN')); app.post('/api/domain/batch', (req, res) => saveAssetsBatch('asset_domain', req.body, res, 'DOMAIN BATCH')); app.get('/api/cost', (req, res) => fetchAssets('asset_cost', res, 'COST')); app.post('/api/cost/batch', (req, res) => saveAssetsBatch('asset_cost', req.body, res, 'COST BATCH')); app.get('/api/vip', (req, res) => fetchAssets('asset_vip', res, 'VIP')); app.post('/api/vip/batch', (req, res) => saveAssetsBatch('asset_vip', req.body, res, 'VIP BATCH')); // 4. Legacy/Auxiliary (History & Assignment) app.get('/api/asset/history', (req, res) => fetchAssets('asset_history', res, 'HISTORY')); app.post('/api/asset/history/batch', async (req, res) => { // Custom logic for history as it might not follow the random-id pattern const connection = await pool.getConnection(); try { await connection.beginTransaction(); await connection.query('DELETE FROM asset_history'); for (const item of req.body) { const dbRow = { asset_id: item.assetId, log_date: item.date, log_user: item.user, details: item.details, cost: item.cost || 0 }; await connection.query('INSERT INTO asset_history SET ?', [dbRow]); } await connection.commit(); res.json({ success: true }); } catch (err) { await connection.rollback(); handleError(res, err, 'BATCH HISTORY'); } finally { connection.release(); } }); app.get('/api/asset/software/assignment', (req, res) => fetchAssets('asset_software_assignment', res, 'SW ASSIGN')); app.post('/api/asset/software/assignment/batch', (req, res) => saveAssetsBatch('asset_software_assignment', req.body, res, 'SW ASSIGN BATCH')); // 5. Utility app.get('/api/generate-asset-code', async (req, res) => { try { const { prefix } = req.query; if (!prefix) return res.status(400).json({ error: 'Prefix is required' }); // Search in multiple tables if necessary, but typically prefix-based tables are known const tables = ['asset_pc', 'asset_server', 'asset_storage', 'asset_network', 'asset_survey', 'asset_pc_parts', 'asset_equipment', 'asset_office_supplies', 'asset_vip']; let lastCode = ''; for (const table of tables) { const [rows] = await pool.query(`SELECT asset_code FROM ${table} WHERE asset_code LIKE ? ORDER BY asset_code DESC LIMIT 1`, [`${prefix}%`]); if (rows.length > 0 && rows[0].asset_code > lastCode) { lastCode = rows[0].asset_code; } } let nextNum = 1; if (lastCode) { const lastNum = parseInt(lastCode.split('-').pop() || '0'); nextNum = lastNum + 1; } res.json({ nextCode: `${prefix}${String(nextNum).padStart(3, '0')}` }); } catch (err) { handleError(res, err, 'GENERATE CODE'); } }); app.listen(3000, '0.0.0.0', () => { console.log('📡 ITAM BACKEND SERVER RUNNING ON PORT 3000 (Multi-Table Optimized)'); });