// node_api/routes/asset_components.js const express = require('express'); const { Pool } = require('pg'); const router = express.Router(); // Database connection pool const pool = new Pool({ host: process.env.DB_HOST || 'localhost', port: process.env.DB_PORT || 5432, database: process.env.DB_DATABASE || 'asset_management', user: process.env.DB_USERNAME || 'directus', password: process.env.DB_PASSWORD || 'directus', max: 10, idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000, }); // Helper function to format component data const formatComponentData = (component) => ({ id: component.id, component_identifier: component.component_identifier, name: component.name, description: component.description, component_type: component.component_type, component_type_name: component.component_type_name, model_number: component.model_number, serial_number: component.serial_number, manufacturer: component.manufacturer, acquisition_cost: parseFloat(component.acquisition_cost) || 0, net_book_value: parseFloat(component.net_book_value) || 0, total_accumulated_depreciation: parseFloat(component.total_accumulated_depreciation) || 0, depreciation_percentage_rate: parseFloat(component.depreciation_percentage_rate) || 0, expected_useful_life_months: parseInt(component.expected_useful_life_months) || 0, salvage_value: parseFloat(component.salvage_value) || 0, status: component.status, condition_rating: component.condition_rating, installation_date: component.installation_date, acquisition_date: component.acquisition_date, warranty_start_date: component.warranty_start_date, warranty_expiration_date: component.warranty_expiration_date, is_critical: component.is_critical, maintenance_status: component.maintenance_status, next_maintenance_date: component.next_maintenance_date, last_maintenance_date: component.last_maintenance_date, parent_asset_id: component.parent_asset_id, parent_asset_name: component.parent_asset_name, parent_asset_identifier: component.parent_asset_identifier, depreciation_status: component.depreciation_status, age_in_months: component.age_in_months, date_created: component.date_created, date_updated: component.date_updated }); /** * Get all asset components for a specific asset */ router.get('/asset/:assetId', async (req, res) => { try { const { assetId } = req.params; const query = ` SELECT * FROM get_asset_components($1) `; const result = await pool.query(query, [assetId]); const components = result.rows.map(formatComponentData); res.json({ success: true, data: components, total: components.length }); } catch (error) { console.error('Error fetching asset components:', error); res.status(500).json({ success: false, error: 'Failed to fetch asset components' }); } }); /** * Get single asset component by ID */ router.get('/:componentId', async (req, res) => { try { const { componentId } = req.params; const query = ` SELECT ac.*, a.name as parent_asset_name, a.asset_identifier as parent_asset_identifier, a.status as parent_asset_status, l.name as parent_location_name, act.name as component_type_name, act.is_critical as type_is_critical, afs.depreciation_status, afs.age_in_months FROM asset_components ac JOIN assets a ON ac.parent_asset_id = a.id LEFT JOIN locations l ON a.location_id = l.id LEFT JOIN asset_component_types act ON ac.component_type_id = act.id LEFT JOIN asset_components_financial_status afs ON ac.id = afs.id WHERE ac.id = $1 `; const result = await pool.query(query, [componentId]); if (result.rows.length === 0) { return res.status(404).json({ success: false, error: 'Asset component not found' }); } const component = formatComponentData(result.rows[0]); res.json({ success: true, data: component }); } catch (error) { console.error('Error fetching asset component:', error); res.status(500).json({ success: false, error: 'Failed to fetch asset component' }); } }); /** * Create new asset component */ router.post('/', async (req, res) => { console.log('🚀 Component creation route handler called'); console.log('🔍 Request headers:', req.headers); console.log('🔍 Request user:', req.user); const client = await pool.connect(); try { await client.query('BEGIN'); // Log the entire request body for debugging console.log('🔍 Full request body:', JSON.stringify(req.body, null, 2)); const { parent_asset_id, component_identifier, name, description, component_type_id, model_number, serial_number, manufacturer, acquisition_cost = 0, depreciation_method = 'straight_line', depreciation_percentage_rate = 0, expected_useful_life_months = 36, salvage_value = 0, status = 'active', condition_rating = 'good', installation_date, acquisition_date, warranty_start_date, warranty_expiration_date, warranty_provider, warranty_coverage, warranty_notes, is_critical = false, maintenance_interval_days = 0, maintenance_provider, maintenance_schedule, maintenance_status = 'up_to_date', last_maintenance_date, next_maintenance_date, compliance_status = 'compliant', usage_tracking_method, expected_total_units = 0, notes, // Fields that frontend sends but may not exist in database - ignore them installation_location, // Not in database schema part_number, // Not in database schema operating_hours, // Not in database schema cycle_count, // Not in database schema performance_rating, // Not in database schema efficiency, // Not in database schema maintenance_notes // Not in database schema // Note: organization_id is deliberately not extracted here as it's derived from parent asset } = req.body; // Validate required fields console.log('🔍 Validating required fields:', { parent_asset_id: !!parent_asset_id, component_identifier: !!component_identifier, name: !!name, component_type_id: !!component_type_id }); if (!parent_asset_id || !component_identifier || !name || !component_type_id) { console.log('❌ Required field validation failed'); return res.status(400).json({ success: false, error: 'Missing required fields: parent_asset_id, component_identifier, name, component_type_id' }); } console.log('✅ Required field validation passed'); // Check if parent asset exists const parentAssetQuery = 'SELECT id FROM assets WHERE id = $1'; const parentAssetResult = await client.query(parentAssetQuery, [parent_asset_id]); if (parentAssetResult.rows.length === 0) { return res.status(404).json({ success: false, error: 'Parent asset not found' }); } // Check for duplicate component identifier within the parent asset const duplicateQuery = ` SELECT id FROM asset_components WHERE parent_asset_id = $1 AND component_identifier = $2 `; const duplicateResult = await client.query(duplicateQuery, [parent_asset_id, component_identifier]); if (duplicateResult.rows.length > 0) { return res.status(400).json({ success: false, error: 'Component identifier already exists for this asset' }); } // Calculate initial net book value const net_book_value = acquisition_cost - 0; // No depreciation initially // Log request data for debugging console.log('🔍 Creating component with data:', { parent_asset_id, component_identifier, name, description, component_type_id, model_number, serial_number, manufacturer, acquisition_cost, depreciation_method, depreciation_percentage_rate, expected_useful_life_months, salvage_value, status, condition_rating, installation_date, acquisition_date, warranty_start_date, warranty_expiration_date, warranty_provider, warranty_coverage, is_critical, maintenance_interval_days, maintenance_provider, maintenance_schedule, compliance_status, usage_tracking_method, expected_total_units, notes }); // Insert new component const insertQuery = ` INSERT INTO asset_components ( organization_id, parent_asset_id, component_identifier, name, description, component_type_id, component_type, model_number, serial_number, manufacturer, acquisition_cost, depreciation_method, depreciation_percentage_rate, expected_useful_life_months, salvage_value, total_accumulated_depreciation, net_book_value, status, condition_rating, installation_date, acquisition_date, warranty_start_date, warranty_expiration_date, warranty_provider, warranty_coverage, warranty_notes, is_critical, maintenance_interval_days, maintenance_provider, maintenance_schedule, maintenance_status, compliance_status, usage_tracking_method, expected_total_units, last_maintenance_date, next_maintenance_date, notes ) VALUES ( (SELECT organization_id FROM assets WHERE id = $1), $1, $2, $3, $4, $5, (SELECT name FROM asset_component_types WHERE id = $5), $6, $7, $8, $9, $10, $11, $12, $13, 0, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34 ) RETURNING id `; const queryParams = [ parent_asset_id, // $1 component_identifier, // $2 name, // $3 description, // $4 component_type_id, // $5 model_number, // $6 serial_number, // $7 manufacturer, // $8 acquisition_cost, // $9 depreciation_method, // $10 depreciation_percentage_rate, // $11 expected_useful_life_months, // $12 salvage_value, // $13 net_book_value, // $14 status, // $15 condition_rating, // $16 installation_date, // $17 acquisition_date, // $18 warranty_start_date, // $19 warranty_expiration_date, // $20 warranty_provider, // $21 warranty_coverage, // $22 warranty_notes, // $23 is_critical, // $24 maintenance_interval_days, // $25 maintenance_provider, // $26 maintenance_schedule, // $27 maintenance_status, // $28 compliance_status, // $29 usage_tracking_method, // $30 expected_total_units, // $31 last_maintenance_date, // $32 next_maintenance_date, // $33 notes // $34 ]; console.log('🔍 SQL parameter count check:', { expectedParams: 34, actualParams: queryParams.length, paramsMatch: queryParams.length === 34 }); const insertResult = await client.query(insertQuery, queryParams); const componentId = insertResult.rows[0].id; await client.query('COMMIT'); // Fetch the created component with full details const fetchQuery = ` SELECT * FROM get_asset_components($1) WHERE id = $2 `; const fetchResult = await client.query(fetchQuery, [parent_asset_id, componentId]); const component = formatComponentData(fetchResult.rows[0]); res.status(201).json({ success: true, data: component, message: 'Asset component created successfully' }); } catch (error) { await client.query('ROLLBACK'); console.error('🚨 Error creating asset component:', { message: error.message, stack: error.stack, code: error.code, detail: error.detail, position: error.position, internalPosition: error.internalPosition, internalQuery: error.internalQuery, where: error.where, schema: error.schema, table: error.table, column: error.column, dataType: error.dataType, constraint: error.constraint }); res.status(500).json({ success: false, error: 'Failed to create asset component', details: process.env.NODE_ENV === 'development' ? error.message : undefined }); } finally { client.release(); } }); /** * Update asset component */ router.put('/:componentId', async (req, res) => { const client = await pool.connect(); try { await client.query('BEGIN'); const { componentId } = req.params; const updateFields = req.body; // Remove fields that shouldn't be updated directly delete updateFields.id; delete updateFields.organization_id; delete updateFields.parent_asset_id; delete updateFields.total_accumulated_depreciation; delete updateFields.date_created; // Set update timestamp updateFields.date_updated = new Date(); // Recalculate net book value if acquisition cost changed if (updateFields.acquisition_cost !== undefined) { const currentDepreciationQuery = ` SELECT total_accumulated_depreciation FROM asset_components WHERE id = $1 `; const currentDepreciationResult = await client.query(currentDepreciationQuery, [componentId]); if (currentDepreciationResult.rows.length > 0) { const accumulated = parseFloat(currentDepreciationResult.rows[0].total_accumulated_depreciation) || 0; updateFields.net_book_value = parseFloat(updateFields.acquisition_cost) - accumulated; } } // Build dynamic update query const setClause = Object.keys(updateFields) .map((key, index) => `${key} = $${index + 2}`) .join(', '); const updateQuery = ` UPDATE asset_components SET ${setClause} WHERE id = $1 RETURNING * `; const values = [componentId, ...Object.values(updateFields)]; const result = await client.query(updateQuery, values); if (result.rows.length === 0) { return res.status(404).json({ success: false, error: 'Asset component not found' }); } await client.query('COMMIT'); // Fetch updated component with full details const fetchQuery = ` SELECT * FROM get_asset_components($1) WHERE id = $2 `; const fetchResult = await client.query(fetchQuery, [result.rows[0].parent_asset_id, componentId]); const component = formatComponentData(fetchResult.rows[0]); res.json({ success: true, data: component, message: 'Asset component updated successfully' }); } catch (error) { await client.query('ROLLBACK'); console.error('Error updating asset component:', error); res.status(500).json({ success: false, error: 'Failed to update asset component' }); } finally { client.release(); } }); /** * Delete asset component */ router.delete('/:componentId', async (req, res) => { const client = await pool.connect(); try { await client.query('BEGIN'); const { componentId } = req.params; // Check if component exists const checkQuery = 'SELECT id, name FROM asset_components WHERE id = $1'; const checkResult = await client.query(checkQuery, [componentId]); if (checkResult.rows.length === 0) { return res.status(404).json({ success: false, error: 'Asset component not found' }); } const componentName = checkResult.rows[0].name; // Delete related records (CASCADE should handle this, but being explicit) await client.query('DELETE FROM asset_component_depreciation_records WHERE component_id = $1', [componentId]); await client.query('DELETE FROM asset_component_maintenance_records WHERE component_id = $1', [componentId]); await client.query('DELETE FROM asset_component_usage_records WHERE component_id = $1', [componentId]); await client.query('DELETE FROM asset_component_documents WHERE component_id = $1', [componentId]); // Delete the component const deleteQuery = 'DELETE FROM asset_components WHERE id = $1'; await client.query(deleteQuery, [componentId]); await client.query('COMMIT'); res.json({ success: true, message: `Asset component "${componentName}" deleted successfully` }); } catch (error) { await client.query('ROLLBACK'); console.error('Error deleting asset component:', error); res.status(500).json({ success: false, error: 'Failed to delete asset component' }); } finally { client.release(); } }); /** * Get asset component depreciation records */ router.get('/:componentId/depreciation', async (req, res) => { try { const { componentId } = req.params; const limit = parseInt(req.query.limit) || 50; const query = ` SELECT acdr.*, ac.name as component_name, ac.component_identifier, a.name as asset_name, a.asset_identifier FROM asset_component_depreciation_records acdr JOIN asset_components ac ON acdr.component_id = ac.id JOIN assets a ON acdr.asset_id = a.id WHERE acdr.component_id = $1 ORDER BY acdr.depreciation_date DESC LIMIT $2 `; const result = await pool.query(query, [componentId, limit]); res.json({ success: true, data: result.rows, total: result.rows.length }); } catch (error) { console.error('Error fetching component depreciation records:', error); res.status(500).json({ success: false, error: 'Failed to fetch depreciation records' }); } }); /** * Get asset component maintenance records */ router.get('/:componentId/maintenance', async (req, res) => { try { const { componentId } = req.params; const limit = parseInt(req.query.limit) || 50; const query = ` SELECT acmr.*, ac.name as component_name, ac.component_identifier, a.name as asset_name, a.asset_identifier FROM asset_component_maintenance_records acmr JOIN asset_components ac ON acmr.component_id = ac.id JOIN assets a ON acmr.asset_id = a.id WHERE acmr.component_id = $1 ORDER BY acmr.maintenance_date DESC LIMIT $2 `; const result = await pool.query(query, [componentId, limit]); res.json({ success: true, data: result.rows, total: result.rows.length }); } catch (error) { console.error('Error fetching component maintenance records:', error); res.status(500).json({ success: false, error: 'Failed to fetch maintenance records' }); } }); /** * Get asset component types */ router.get('/types/list', async (req, res) => { try { const query = ` SELECT act.*, at.name as asset_type_name FROM asset_component_types act LEFT JOIN asset_types at ON act.asset_type_id = at.id WHERE act.is_active = true ORDER BY act.name `; const result = await pool.query(query); res.json({ success: true, data: result.rows, total: result.rows.length }); } catch (error) { console.error('Error fetching component types:', error); res.status(500).json({ success: false, error: 'Failed to fetch component types' }); } }); module.exports = router;