import express from 'express'; import mysql from 'mysql2/promise'; import cors from 'cors'; import dotenv from 'dotenv'; import fs from 'fs'; dotenv.config(); const app = express(); app.use(cors()); app.use(express.json({ limit: '50mb' })); // MySQL Pool Configuration 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'), waitForConnections: true, connectionLimit: 10, queueLimit: 0 }); // Error Handler const handleError = (res, err, label) => { console.error(`❌ [${label}] Error:`, err); res.status(500).json({ error: err.message }); }; // --- API Endpoints --- // 1. Generic Batch Save (Dynamic Table Detection) app.post('/api/:table/batch', async (req, res) => { const { table } = req.params; const data = req.body; if (!Array.isArray(data)) return res.status(400).json({ error: 'Data must be an array' }); let connection; try { connection = await pool.getConnection(); await connection.beginTransaction(); // 1. Get Table Schema const [columns] = await connection.query(`DESCRIBE ${table}`); const validFields = columns.map(c => c.Field); // 2. Clear Existing Data (Optional - depending on strategy) // For now, we use REPLACE INTO or similar. But user requested batch save as sync. await connection.query(`DELETE FROM ${table}`); // 3. Insert New Data if (data.length > 0) { const placeholders = validFields.map(() => '?').join(', '); const sql = `INSERT INTO ${table} (${validFields.join(', ')}) VALUES (${placeholders})`; for (const item of data) { const values = validFields.map(field => { const val = item[field]; return val === undefined ? null : val; }); await connection.query(sql, values); } } await connection.commit(); console.log(`✅ [BATCH SAVE] Table: ${table}, Count: ${data.length}`); res.json({ success: true, count: data.length }); } catch (err) { if (connection) await connection.rollback(); handleError(res, err, 'BATCH SAVE'); } finally { if (connection) connection.release(); } }); // 2. Get All Assets (Integrated Master Data) app.get('/api/assets/master', async (req, res) => { try { const connection = await pool.getConnection(); const tables = { pc: 'asset_pc', server: 'asset_server', storage: 'asset_storage', network: 'asset_network', equipment: 'asset_equipment', officeSupplies: 'asset_office_supplies', survey: 'asset_survey', vip: 'asset_vip', swInternal: 'sw_internal', swExternal: 'sw_external', cloud: 'asset_cloud', users: 'user_master', swUsers: 'sw_assignment', logs: 'asset_history' }; const masterData = {}; for (const [key, tableName] of Object.entries(tables)) { try { const [rows] = await connection.query(`SELECT * FROM ${tableName}`); masterData[key] = rows; } catch (err) { console.warn(`[MASTER DATA] Skipping ${tableName}: ${err.message}`); masterData[key] = []; } } connection.release(); res.json(masterData); } catch (err) { handleError(res, err, 'MASTER DATA'); } }); // 3. Single Asset Save (Update or Insert) app.post('/api/asset/:category/save', async (req, res) => { const { category } = req.params; const asset = req.body; const tableMap = { pc: 'asset_pc', server: 'asset_server', storage: 'asset_storage', network: 'asset_network', equipment: 'asset_equipment', officeSupplies: 'asset_office_supplies', survey: 'asset_survey', vip: 'asset_vip', swInternal: 'sw_internal', swExternal: 'sw_external', cloud: 'asset_cloud' }; const table = tableMap[category]; if (!table) return res.status(400).json({ error: 'Invalid category' }); try { const connection = await pool.getConnection(); const [columns] = await connection.query(`DESCRIBE ${table}`); const validFields = columns.map(c => c.Field); const dataObj = {}; validFields.forEach(f => { if (asset[f] !== undefined) dataObj[f] = asset[f]; }); const keys = Object.keys(dataObj); const values = Object.values(dataObj); const placeholders = keys.map(() => '?').join(', '); const updates = keys.map(k => `${k} = VALUES(${k})`).join(', '); const sql = `INSERT INTO ${table} (${keys.join(', ')}) VALUES (${placeholders}) ON DUPLICATE KEY UPDATE ${updates}`; await connection.query(sql, values); connection.release(); console.log(`💾 [ASSET SAVE] Category: ${category}, ID: ${asset.id}`); res.json({ success: true }); } catch (err) { handleError(res, err, 'ASSET SAVE'); } }); // 4. Asset Delete app.delete('/api/asset/:category/:id', async (req, res) => { const { category, id } = req.params; const tableMap = { pc: 'asset_pc', server: 'asset_server', storage: 'asset_storage', network: 'asset_network', equipment: 'asset_equipment', officeSupplies: 'asset_office_supplies', survey: 'asset_survey', vip: 'asset_vip', swInternal: 'sw_internal', swExternal: 'sw_external', cloud: 'asset_cloud' }; const table = tableMap[category]; if (!table) return res.status(400).json({ error: 'Invalid category' }); try { const connection = await pool.getConnection(); await connection.query(`DELETE FROM ${table} WHERE id = ?`, [id]); connection.release(); console.log(`🗑️ [ASSET DELETE] Category: ${category}, ID: ${id}`); res.json({ success: true }); } catch (err) { handleError(res, err, 'ASSET DELETE'); } }); // 5. Generate Next Asset Code app.get('/api/generate-asset-code', async (req, res) => { const { prefix, purchaseDate } = req.query; if (!prefix) return res.status(400).json({ error: 'Prefix is required' }); try { const connection = await pool.getConnection(); const tables = [ 'asset_pc', 'asset_server', 'asset_storage', 'asset_network', 'asset_equipment', 'asset_office_supplies', 'asset_survey', 'asset_vip' ]; // Extract YYYYMM from purchaseDate (format: YYYY-MM-DD) const datePart = purchaseDate ? purchaseDate.toString().replace(/-/g, '').substring(0, 6) : ''; const searchPattern = datePart ? `${prefix}-${datePart}-%` : `${prefix}-%`; let maxNum = 0; for (const table of tables) { try { const [rows] = await connection.query( `SELECT asset_code FROM ${table} WHERE asset_code LIKE ?`, [searchPattern] ); rows.forEach(row => { const parts = row.asset_code.split('-'); const seqPart = parts[parts.length - 1]; // Last part is sequence const num = parseInt(seqPart); if (!isNaN(num) && num > maxNum) maxNum = num; }); } catch (err) { // Table might not exist or column missing } } const nextNum = maxNum + 1; const nextCode = datePart ? `${prefix}-${datePart}-${String(nextNum).padStart(4, '0')}` : `${prefix}-${String(nextNum).padStart(4, '0')}`; // Fallback if no date connection.release(); console.log(`🆕 [GENERATE CODE] Prefix: ${prefix}, Date: ${datePart}, Next: ${nextCode}`); res.json({ nextCode }); } catch (err) { handleError(res, err, 'GENERATE CODE'); } }); // 6. Map Config API (Real-time Save) app.get('/api/maps', (req, res) => { try { if (!fs.existsSync('map_config.json')) { return res.json({}); } const data = fs.readFileSync('map_config.json', 'utf8'); res.json(JSON.parse(data || '{}')); } catch (err) { handleError(res, err, 'GET MAPS'); } }); app.post('/api/maps/save', (req, res) => { try { const { path, boxes } = req.body; if (!path) return res.status(400).json({ error: 'Path is required' }); let config = {}; if (fs.existsSync('map_config.json')) { config = JSON.parse(fs.readFileSync('map_config.json', 'utf8') || '{}'); } config[path] = boxes; fs.writeFileSync('map_config.json', JSON.stringify(config, null, 2)); console.log(`💾 [MAP SAVE] Updated config for: ${path}`); res.json({ success: true }); } catch (err) { handleError(res, err, 'SAVE MAPS'); } }); app.listen(3000, '0.0.0.0', () => { console.log('📡 ITAM BACKEND SERVER RUNNING ON PORT 3000 (Multi-Table Optimized)'); });