import { Router, Request, Response } from 'express'; import { readFile, writeFile } from 'fs/promises'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { logger } from '../services/logger.js'; import { clearEffortCalculationConfigCache, getEffortCalculationConfigV25 } from '../services/effortCalculation.js'; import { requireAuth, requirePermission } from '../middleware/authorization.js'; import type { EffortCalculationConfig, EffortCalculationConfigV25 } from '../config/effortCalculation.js'; import type { DataCompletenessConfig } from '../types/index.js'; // Get __dirname equivalent for ES modules const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const router = Router(); // All routes require authentication and manage_settings permission router.use(requireAuth); router.use(requirePermission('manage_settings')); // Path to the configuration files const CONFIG_FILE_PATH = join(__dirname, '../../data/effort-calculation-config.json'); const CONFIG_FILE_PATH_V25 = join(__dirname, '../../data/effort-calculation-config.json'); const COMPLETENESS_CONFIG_FILE_PATH = join(__dirname, '../../data/data-completeness-config.json'); /** * Get the current effort calculation configuration (legacy) */ router.get('/effort-calculation', async (req: Request, res: Response) => { try { // Try to read from JSON file, fallback to default config try { const fileContent = await readFile(CONFIG_FILE_PATH, 'utf-8'); const config = JSON.parse(fileContent) as EffortCalculationConfig; res.json(config); } catch (fileError) { // If file doesn't exist, return default config from code const { EFFORT_CALCULATION_CONFIG } = await import('../config/effortCalculation.js'); res.json(EFFORT_CALCULATION_CONFIG); } } catch (error) { logger.error('Failed to get effort calculation configuration', error); res.status(500).json({ error: 'Failed to get configuration' }); } }); /** * Update the effort calculation configuration (legacy) */ router.put('/effort-calculation', async (req: Request, res: Response) => { try { const config = req.body as EffortCalculationConfig; // Validate the configuration structure if (!config.governanceModelRules || !Array.isArray(config.governanceModelRules)) { res.status(400).json({ error: 'Invalid configuration: governanceModelRules must be an array' }); return; } if (!config.default || typeof config.default.result !== 'number') { res.status(400).json({ error: 'Invalid configuration: default.result must be a number' }); return; } // Write to JSON file await writeFile(CONFIG_FILE_PATH, JSON.stringify(config, null, 2), 'utf-8'); // Clear the cache so the new config is loaded on next request clearEffortCalculationConfigCache(); logger.info('Effort calculation configuration updated'); res.json({ success: true, message: 'Configuration saved successfully' }); } catch (error) { logger.error('Failed to update effort calculation configuration', error); res.status(500).json({ error: 'Failed to save configuration' }); } }); /** * Get the v25 effort calculation configuration */ router.get('/effort-calculation-v25', async (req: Request, res: Response) => { try { // Try to read from JSON file, fallback to default config try { const fileContent = await readFile(CONFIG_FILE_PATH_V25, 'utf-8'); const config = JSON.parse(fileContent) as EffortCalculationConfigV25; res.json(config); } catch (fileError) { // If file doesn't exist, return default config from code const config = getEffortCalculationConfigV25(); res.json(config); } } catch (error) { logger.error('Failed to get effort calculation configuration v25', error); res.status(500).json({ error: 'Failed to get configuration' }); } }); /** * Update the v25 effort calculation configuration */ router.put('/effort-calculation-v25', async (req: Request, res: Response) => { try { const config = req.body as EffortCalculationConfigV25; // Validate the configuration structure if (!config.regiemodellen || typeof config.regiemodellen !== 'object') { res.status(400).json({ error: 'Invalid configuration: regiemodellen must be an object' }); return; } if (!config.validationRules || typeof config.validationRules !== 'object') { res.status(400).json({ error: 'Invalid configuration: validationRules must be an object' }); return; } // Write to JSON file await writeFile(CONFIG_FILE_PATH_V25, JSON.stringify(config, null, 2), 'utf-8'); // Clear the cache so the new config is loaded on next request clearEffortCalculationConfigCache(); logger.info('Effort calculation configuration v25 updated'); res.json({ success: true, message: 'Configuration v25 saved successfully' }); } catch (error) { logger.error('Failed to update effort calculation configuration v25', error); res.status(500).json({ error: 'Failed to save configuration' }); } }); /** * Get the data completeness configuration */ router.get('/data-completeness', async (req: Request, res: Response) => { try { // Try to read from JSON file, fallback to default config try { const fileContent = await readFile(COMPLETENESS_CONFIG_FILE_PATH, 'utf-8'); const config = JSON.parse(fileContent) as DataCompletenessConfig; res.json(config); } catch (fileError) { // If file doesn't exist, return default config const defaultConfig: DataCompletenessConfig = { metadata: { version: '1.0.0', description: 'Configuration for Data Completeness Score fields', lastUpdated: new Date().toISOString(), }, categories: [ { id: 'general', name: 'General', description: 'General application information fields', fields: [ { id: 'organisation', name: 'Organisation', fieldPath: 'organisation', enabled: true }, { id: 'applicationFunctions', name: 'ApplicationFunction', fieldPath: 'applicationFunctions', enabled: true }, { id: 'status', name: 'Status', fieldPath: 'status', enabled: true }, { id: 'businessImpactAnalyse', name: 'Business Impact Analyse', fieldPath: 'businessImpactAnalyse', enabled: true }, { id: 'hostingType', name: 'Application Component Hosting Type', fieldPath: 'hostingType', enabled: true }, { id: 'supplierProduct', name: 'Supplier Product', fieldPath: 'supplierProduct', enabled: true }, { id: 'businessOwner', name: 'Business Owner', fieldPath: 'businessOwner', enabled: true }, { id: 'systemOwner', name: 'System Owner', fieldPath: 'systemOwner', enabled: true }, { id: 'functionalApplicationManagement', name: 'Functional Application Management', fieldPath: 'functionalApplicationManagement', enabled: true }, { id: 'technicalApplicationManagement', name: 'Technical Application Management', fieldPath: 'technicalApplicationManagement', enabled: true }, ], }, { id: 'applicationManagement', name: 'Application Management', description: 'Application management classification fields', fields: [ { id: 'governanceModel', name: 'ICT Governance Model', fieldPath: 'governanceModel', enabled: true }, { id: 'applicationType', name: 'Application Management - Application Type', fieldPath: 'applicationType', enabled: true }, { id: 'applicationManagementHosting', name: 'Application Management - Hosting', fieldPath: 'applicationManagementHosting', enabled: true }, { id: 'applicationManagementTAM', name: 'Application Management - TAM', fieldPath: 'applicationManagementTAM', enabled: true }, { id: 'dynamicsFactor', name: 'Application Management - Dynamics Factor', fieldPath: 'dynamicsFactor', enabled: true }, { id: 'complexityFactor', name: 'Application Management - Complexity Factor', fieldPath: 'complexityFactor', enabled: true }, { id: 'numberOfUsers', name: 'Application Management - Number of Users', fieldPath: 'numberOfUsers', enabled: true }, ], }, ], }; res.json(defaultConfig); } } catch (error) { logger.error('Failed to get data completeness configuration', error); res.status(500).json({ error: 'Failed to get configuration' }); } }); /** * Update the data completeness configuration */ router.put('/data-completeness', async (req: Request, res: Response) => { try { const config = req.body as DataCompletenessConfig; // Validate the configuration structure if (!config.categories || !Array.isArray(config.categories)) { res.status(400).json({ error: 'Invalid configuration: categories must be an array' }); return; } if (config.categories.length === 0) { res.status(400).json({ error: 'Invalid configuration: must have at least one category' }); return; } // Validate each category for (const category of config.categories) { if (!category.id || typeof category.id !== 'string') { res.status(400).json({ error: 'Invalid configuration: each category must have an id' }); return; } if (!category.name || typeof category.name !== 'string') { res.status(400).json({ error: 'Invalid configuration: each category must have a name' }); return; } if (!Array.isArray(category.fields)) { res.status(400).json({ error: 'Invalid configuration: category fields must be arrays' }); return; } // Validate each field for (const field of category.fields) { if (!field.id || typeof field.id !== 'string') { res.status(400).json({ error: 'Invalid configuration: each field must have an id' }); return; } if (!field.name || typeof field.name !== 'string') { res.status(400).json({ error: 'Invalid configuration: each field must have a name' }); return; } if (!field.fieldPath || typeof field.fieldPath !== 'string') { res.status(400).json({ error: 'Invalid configuration: each field must have a fieldPath' }); return; } if (typeof field.enabled !== 'boolean') { res.status(400).json({ error: 'Invalid configuration: each field must have an enabled boolean' }); return; } } } // Update metadata config.metadata = { ...config.metadata, lastUpdated: new Date().toISOString(), }; // Write to JSON file await writeFile(COMPLETENESS_CONFIG_FILE_PATH, JSON.stringify(config, null, 2), 'utf-8'); // Clear the cache so the new config is loaded on next request const { clearDataCompletenessConfigCache } = await import('../services/dataCompletenessConfig.js'); clearDataCompletenessConfigCache(); logger.info('Data completeness configuration updated'); res.json({ success: true, message: 'Configuration saved successfully' }); } catch (error) { logger.error('Failed to update data completeness configuration', error); res.status(500).json({ error: 'Failed to save configuration' }); } }); export default router;