663 lines
20 KiB
JavaScript
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; |