enterprise_assest_managemen.../node_api/routes/asset_components.js

663 lines
20 KiB
JavaScript

// 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;