Initial commit: ZiRA Classification Tool for Zuyderland CMDB

This commit is contained in:
2026-01-06 15:32:28 +01:00
commit 0b27adc2fb
55 changed files with 24310 additions and 0 deletions

View File

@@ -0,0 +1,720 @@
/**
* Configuration for Required Effort Application Management calculation (v25)
* Based on Dienstencatalogus Applicatiebeheer v25
*
* Hierarchy:
* - ICT Governance Model (niveau 1)
* > Application Type (niveau 2) - Application Management - Application Type
* > Business Impact (niveau 3) - Business Impact Analyse
* > Hosting (niveau 4) - Application Management - Hosting
*
* Each level can have a default rule if no specific configuration matches.
*
* Hosting values (Application Management - Hosting):
* - ON-PREM: On-Premises (Azure - Eigen beheer → mapped to ON-PREM for ICMT beheer)
* - AZURE: Azure - Eigen beheer
* - AZURE-DM: Azure - Delegated Management
* - EXTERN: Extern (SaaS)
*/
// FTE range with min/max values
export interface FTERange {
min: number;
max: number;
}
// Hosting rule with multiselect hosting values
export interface HostingRule {
hostingValues: string[]; // e.g., ['ON-PREM', 'AZURE'] or ['AZURE-DM', 'EXTERN']
fte: FTERange;
}
// BIA level configuration
export interface BIALevelConfig {
description?: string;
defaultFte?: FTERange;
hosting: {
[key: string]: HostingRule; // e.g., 'OnPrem', 'SaaS', '_all'
};
}
// Application Type configuration
export interface ApplicationTypeConfig {
defaultFte?: FTERange;
note?: string;
requiresManualAssessment?: boolean;
fixedFte?: boolean;
notRecommended?: boolean;
biaLevels: {
[key: string]: BIALevelConfig; // e.g., 'F', 'E', 'D', 'C', 'B', 'A', '_all'
};
}
// Governance Model (Regiemodel) configuration
export interface GovernanceModelConfig {
name: string;
description?: string;
allowedBia: string[]; // Allowed BIA levels for this regiemodel
defaultFte: FTERange;
note?: string;
applicationTypes: {
[key: string]: ApplicationTypeConfig; // e.g., 'Applicatie', 'Platform', 'Workload', 'Connected Device'
};
}
// Complete configuration structure
export interface EffortCalculationConfigV25 {
metadata: {
version: string;
description: string;
date: string;
formula: string;
};
regiemodellen: {
[key: string]: GovernanceModelConfig; // e.g., 'A', 'B', 'B+', 'C', 'D', 'E'
};
validationRules: {
biaRegieModelConstraints: {
[regiemodel: string]: string[]; // e.g., 'A': ['D', 'E', 'F']
};
platformRestrictions: Array<{
regiemodel: string;
applicationType: string;
warning: string;
}>;
};
}
// Legacy types for backward compatibility
export interface EffortRule {
result: number;
conditions?: {
businessImpactAnalyse?: string | string[];
applicationManagementHosting?: string | string[];
};
}
export interface ApplicationTypeRule {
applicationTypes: string | string[];
businessImpactRules: {
[key: string]: EffortRule | EffortRule[];
};
default?: EffortRule | EffortRule[];
}
export interface GovernanceModelRule {
governanceModel: string;
applicationTypeRules: {
[key: string]: ApplicationTypeRule | EffortRule;
};
default?: EffortRule;
}
export interface EffortCalculationConfig {
governanceModelRules: GovernanceModelRule[];
default: EffortRule;
}
/**
* New configuration structure (v25)
* Based on Dienstencatalogus Applicatiebeheer v25
*/
export const EFFORT_CALCULATION_CONFIG_V25: EffortCalculationConfigV25 = {
metadata: {
version: '25',
description: 'FTE-configuratie Dienstencatalogus Applicatiebeheer v25',
date: '2025-12-23',
formula: 'Werkelijke FTE = basis_fte_avg * schaalfactor * complexiteit * dynamiek',
},
regiemodellen: {
'A': {
name: 'Centraal Beheer ICMT',
description: 'ICMT voert volledig beheer uit (TAB + FAB + IM)',
allowedBia: ['D', 'E', 'F'],
defaultFte: { min: 0.15, max: 0.30 },
applicationTypes: {
'Applicatie': {
defaultFte: { min: 0.15, max: 0.30 },
biaLevels: {
'F': {
description: 'Zeer kritiek - Levensbedreigende impact',
defaultFte: { min: 0.30, max: 0.50 },
hosting: {
'OnPrem': { hostingValues: ['On-Premises', 'Azure - Eigen beheer'], fte: { min: 0.50, max: 1.00 } },
'SaaS': { hostingValues: ['Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.30, max: 0.50 } },
},
},
'E': {
description: 'Kritiek - Grote impact op zorgprocessen',
defaultFte: { min: 0.20, max: 0.30 },
hosting: {
'OnPrem': { hostingValues: ['On-Premises', 'Azure - Eigen beheer'], fte: { min: 0.30, max: 0.50 } },
'SaaS': { hostingValues: ['Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.20, max: 0.30 } },
},
},
'D': {
description: 'Belangrijk - Significante impact',
defaultFte: { min: 0.10, max: 0.20 },
hosting: {
'OnPrem': { hostingValues: ['On-Premises', 'Azure - Eigen beheer'], fte: { min: 0.15, max: 0.30 } },
'SaaS': { hostingValues: ['Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.10, max: 0.20 } },
},
},
},
},
'Platform': {
defaultFte: { min: 0.20, max: 0.40 },
biaLevels: {
'F': {
description: 'Zeer kritiek - Levensbedreigende impact',
defaultFte: { min: 0.40, max: 0.60 },
hosting: {
'OnPrem': { hostingValues: ['On-Premises', 'Azure - Eigen beheer'], fte: { min: 0.60, max: 1.00 } },
'SaaS': { hostingValues: ['Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.40, max: 0.60 } },
},
},
'E': {
description: 'Kritiek - Grote impact op zorgprocessen',
defaultFte: { min: 0.25, max: 0.40 },
hosting: {
'OnPrem': { hostingValues: ['On-Premises', 'Azure - Eigen beheer'], fte: { min: 0.40, max: 0.60 } },
'SaaS': { hostingValues: ['Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.25, max: 0.40 } },
},
},
'D': {
description: 'Belangrijk - Significante impact',
defaultFte: { min: 0.15, max: 0.25 },
hosting: {
'OnPrem': { hostingValues: ['On-Premises', 'Azure - Eigen beheer'], fte: { min: 0.25, max: 0.40 } },
'SaaS': { hostingValues: ['Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.15, max: 0.25 } },
},
},
},
},
'Workload': {
defaultFte: { min: 0.08, max: 0.15 },
biaLevels: {
'F': {
description: 'Zeer kritiek - Levensbedreigende impact',
defaultFte: { min: 0.20, max: 0.35 },
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.20, max: 0.35 } },
},
},
'E': {
description: 'Kritiek - Grote impact op zorgprocessen',
defaultFte: { min: 0.12, max: 0.20 },
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.12, max: 0.20 } },
},
},
'D': {
description: 'Belangrijk - Significante impact',
defaultFte: { min: 0.08, max: 0.15 },
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.08, max: 0.15 } },
},
},
'C': {
description: 'Standaard - Gemiddelde impact',
defaultFte: { min: 0.04, max: 0.08 },
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.04, max: 0.08 } },
},
},
},
},
'Connected Device': {
note: 'Handmatige beoordeling vereist conform Beheer Readiness Checklist sectie J',
requiresManualAssessment: true,
defaultFte: { min: 0.05, max: 0.15 },
biaLevels: {
'_all': {
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.05, max: 0.15 } },
},
},
},
},
},
},
'B': {
name: 'Federatief Beheer',
description: 'ICMT doet TAB + IM, business doet FAB met ICMT-coaching',
allowedBia: ['C', 'D', 'E'],
defaultFte: { min: 0.05, max: 0.15 },
applicationTypes: {
'Applicatie': {
defaultFte: { min: 0.05, max: 0.15 },
biaLevels: {
'E': {
description: 'Kritiek - Grote impact op zorgprocessen',
defaultFte: { min: 0.10, max: 0.20 },
hosting: {
'OnPrem': { hostingValues: ['On-Premises', 'Azure - Eigen beheer'], fte: { min: 0.15, max: 0.30 } },
'SaaS': { hostingValues: ['Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.10, max: 0.20 } },
},
},
'D': {
description: 'Belangrijk - Significante impact',
defaultFte: { min: 0.08, max: 0.15 },
hosting: {
'OnPrem': { hostingValues: ['On-Premises', 'Azure - Eigen beheer'], fte: { min: 0.10, max: 0.20 } },
'SaaS': { hostingValues: ['Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.08, max: 0.15 } },
},
},
'C': {
description: 'Standaard - Gemiddelde impact',
defaultFte: { min: 0.04, max: 0.08 },
hosting: {
'OnPrem': { hostingValues: ['On-Premises', 'Azure - Eigen beheer'], fte: { min: 0.05, max: 0.10 } },
'SaaS': { hostingValues: ['Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.04, max: 0.08 } },
},
},
},
},
'Platform': {
defaultFte: { min: 0.08, max: 0.18 },
biaLevels: {
'E': {
description: 'Kritiek - Grote impact op zorgprocessen',
defaultFte: { min: 0.15, max: 0.25 },
hosting: {
'OnPrem': { hostingValues: ['On-Premises', 'Azure - Eigen beheer'], fte: { min: 0.20, max: 0.35 } },
'SaaS': { hostingValues: ['Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.15, max: 0.25 } },
},
},
'D': {
description: 'Belangrijk - Significante impact',
defaultFte: { min: 0.10, max: 0.18 },
hosting: {
'OnPrem': { hostingValues: ['On-Premises', 'Azure - Eigen beheer'], fte: { min: 0.15, max: 0.25 } },
'SaaS': { hostingValues: ['Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.10, max: 0.18 } },
},
},
'C': {
description: 'Standaard - Gemiddelde impact',
defaultFte: { min: 0.06, max: 0.12 },
hosting: {
'OnPrem': { hostingValues: ['On-Premises', 'Azure - Eigen beheer'], fte: { min: 0.08, max: 0.15 } },
'SaaS': { hostingValues: ['Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.06, max: 0.12 } },
},
},
},
},
'Workload': {
note: 'ICMT-aandeel; business levert aanvullend eigen FTE voor FAB',
defaultFte: { min: 0.03, max: 0.08 },
biaLevels: {
'E': {
description: 'Kritiek - Grote impact op zorgprocessen',
defaultFte: { min: 0.08, max: 0.12 },
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.08, max: 0.12 } },
},
},
'D': {
description: 'Belangrijk - Significante impact',
defaultFte: { min: 0.05, max: 0.08 },
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.05, max: 0.08 } },
},
},
'C': {
description: 'Standaard - Gemiddelde impact',
defaultFte: { min: 0.02, max: 0.05 },
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.02, max: 0.05 } },
},
},
},
},
'Connected Device': {
note: 'Handmatige beoordeling vereist conform Beheer Readiness Checklist sectie J',
requiresManualAssessment: true,
defaultFte: { min: 0.03, max: 0.10 },
biaLevels: {
'_all': {
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.03, max: 0.10 } },
},
},
},
},
},
},
'B+': {
name: 'Gescheiden Beheer',
description: 'ICMT doet TAB + IM, business doet FAB zelfstandig (zonder coaching)',
allowedBia: ['C', 'D', 'E'],
defaultFte: { min: 0.04, max: 0.12 },
note: 'FTE-waarden zijn circa 20-30% lager dan Model B (geen coaching)',
applicationTypes: {
'Applicatie': {
defaultFte: { min: 0.04, max: 0.12 },
biaLevels: {
'E': {
description: 'Kritiek - Grote impact op zorgprocessen',
defaultFte: { min: 0.08, max: 0.15 },
hosting: {
'OnPrem': { hostingValues: ['On-Premises', 'Azure - Eigen beheer'], fte: { min: 0.12, max: 0.22 } },
'SaaS': { hostingValues: ['Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.08, max: 0.15 } },
},
},
'D': {
description: 'Belangrijk - Significante impact',
defaultFte: { min: 0.06, max: 0.11 },
hosting: {
'OnPrem': { hostingValues: ['On-Premises', 'Azure - Eigen beheer'], fte: { min: 0.08, max: 0.15 } },
'SaaS': { hostingValues: ['Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.06, max: 0.11 } },
},
},
'C': {
description: 'Standaard - Gemiddelde impact',
defaultFte: { min: 0.03, max: 0.06 },
hosting: {
'OnPrem': { hostingValues: ['On-Premises', 'Azure - Eigen beheer'], fte: { min: 0.04, max: 0.08 } },
'SaaS': { hostingValues: ['Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.03, max: 0.06 } },
},
},
},
},
'Platform': {
defaultFte: { min: 0.06, max: 0.14 },
biaLevels: {
'E': {
description: 'Kritiek - Grote impact op zorgprocessen',
defaultFte: { min: 0.12, max: 0.19 },
hosting: {
'OnPrem': { hostingValues: ['On-Premises', 'Azure - Eigen beheer'], fte: { min: 0.15, max: 0.26 } },
'SaaS': { hostingValues: ['Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.12, max: 0.19 } },
},
},
'D': {
description: 'Belangrijk - Significante impact',
defaultFte: { min: 0.08, max: 0.14 },
hosting: {
'OnPrem': { hostingValues: ['On-Premises', 'Azure - Eigen beheer'], fte: { min: 0.12, max: 0.19 } },
'SaaS': { hostingValues: ['Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.08, max: 0.14 } },
},
},
'C': {
description: 'Standaard - Gemiddelde impact',
defaultFte: { min: 0.05, max: 0.09 },
hosting: {
'OnPrem': { hostingValues: ['On-Premises', 'Azure - Eigen beheer'], fte: { min: 0.06, max: 0.11 } },
'SaaS': { hostingValues: ['Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.05, max: 0.09 } },
},
},
},
},
'Workload': {
note: 'ICMT-aandeel; business levert volledig eigen FTE voor FAB (geen coaching)',
defaultFte: { min: 0.02, max: 0.05 },
biaLevels: {
'E': {
description: 'Kritiek - Grote impact op zorgprocessen',
defaultFte: { min: 0.05, max: 0.08 },
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.05, max: 0.08 } },
},
},
'D': {
description: 'Belangrijk - Significante impact',
defaultFte: { min: 0.03, max: 0.05 },
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.03, max: 0.05 } },
},
},
'C': {
description: 'Standaard - Gemiddelde impact',
defaultFte: { min: 0.01, max: 0.03 },
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.01, max: 0.03 } },
},
},
},
},
'Connected Device': {
note: 'Handmatige beoordeling vereist conform Beheer Readiness Checklist sectie J',
requiresManualAssessment: true,
defaultFte: { min: 0.02, max: 0.08 },
biaLevels: {
'_all': {
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.02, max: 0.08 } },
},
},
},
},
},
},
'C': {
name: 'Uitbesteed met ICMT-Regie',
description: 'Leverancier doet TAB, ICMT doet IM/regie + FAB (BIA-afhankelijk)',
allowedBia: ['C', 'D', 'E', 'F'],
defaultFte: { min: 0.06, max: 0.15 },
note: 'FAB-niveau: Volledig (E-F), Uitgebreid (D), Basis (C)',
applicationTypes: {
'Applicatie': {
defaultFte: { min: 0.06, max: 0.15 },
biaLevels: {
'F': {
description: 'Zeer kritiek - Levensbedreigende impact (FAB-niveau: Volledig)',
defaultFte: { min: 0.25, max: 0.50 },
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.25, max: 0.50 } },
},
},
'E': {
description: 'Kritiek - Grote impact op zorgprocessen (FAB-niveau: Volledig)',
defaultFte: { min: 0.15, max: 0.25 },
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.15, max: 0.25 } },
},
},
'D': {
description: 'Belangrijk - Significante impact (FAB-niveau: Uitgebreid)',
defaultFte: { min: 0.08, max: 0.15 },
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.08, max: 0.15 } },
},
},
'C': {
description: 'Standaard - Gemiddelde impact (FAB-niveau: Basis)',
defaultFte: { min: 0.04, max: 0.08 },
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.04, max: 0.08 } },
},
},
},
},
'Platform': {
defaultFte: { min: 0.10, max: 0.25 },
biaLevels: {
'F': {
description: 'Zeer kritiek - Levensbedreigende impact (IM/Regie focus: Intensief)',
defaultFte: { min: 0.35, max: 0.50 },
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.35, max: 0.50 } },
},
},
'E': {
description: 'Kritiek - Grote impact op zorgprocessen (IM/Regie focus: Hoog)',
defaultFte: { min: 0.20, max: 0.35 },
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.20, max: 0.35 } },
},
},
'D': {
description: 'Belangrijk - Significante impact (IM/Regie focus: Standaard)',
defaultFte: { min: 0.12, max: 0.20 },
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.12, max: 0.20 } },
},
},
'C': {
description: 'Standaard - Gemiddelde impact (IM/Regie focus: Basis)',
defaultFte: { min: 0.06, max: 0.12 },
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.06, max: 0.12 } },
},
},
},
},
'Workload': {
defaultFte: { min: 0.04, max: 0.10 },
biaLevels: {
'F': {
description: 'Zeer kritiek - Levensbedreigende impact',
defaultFte: { min: 0.15, max: 0.25 },
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.15, max: 0.25 } },
},
},
'E': {
description: 'Kritiek - Grote impact op zorgprocessen',
defaultFte: { min: 0.08, max: 0.15 },
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.08, max: 0.15 } },
},
},
'D': {
description: 'Belangrijk - Significante impact',
defaultFte: { min: 0.05, max: 0.10 },
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.05, max: 0.10 } },
},
},
'C': {
description: 'Standaard - Gemiddelde impact',
defaultFte: { min: 0.03, max: 0.06 },
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.03, max: 0.06 } },
},
},
},
},
'Connected Device': {
note: 'Handmatige beoordeling vereist conform Beheer Readiness Checklist sectie J',
requiresManualAssessment: true,
defaultFte: { min: 0.03, max: 0.10 },
biaLevels: {
'_all': {
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.03, max: 0.10 } },
},
},
},
},
},
},
'D': {
name: 'Decentraal met Business-Regie',
description: 'Business of decentrale IT regisseert, leverancier doet TAB, ICMT alleen CMDB + advies',
allowedBia: ['A', 'B', 'C'],
defaultFte: { min: 0.01, max: 0.02 },
note: 'Vaste FTE ongeacht BIA en Hosting - alleen CMDB-registratie en review',
applicationTypes: {
'Applicatie': {
fixedFte: true,
defaultFte: { min: 0.01, max: 0.02 },
biaLevels: {
'_all': {
description: 'Alle BIA-niveaus (A, B, C)',
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.01, max: 0.02 } },
},
},
},
},
'Platform': {
fixedFte: true,
notRecommended: true,
note: 'Niet aanbevolen voor Platforms vanwege governance-risico',
defaultFte: { min: 0.02, max: 0.04 },
biaLevels: {
'_all': {
description: 'Alle BIA-niveaus (A, B, C)',
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.02, max: 0.04 } },
},
},
},
},
'Workload': {
fixedFte: true,
defaultFte: { min: 0.01, max: 0.02 },
biaLevels: {
'_all': {
description: 'Alle BIA-niveaus (A, B, C)',
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.01, max: 0.02 } },
},
},
},
},
'Connected Device': {
fixedFte: true,
defaultFte: { min: 0.01, max: 0.02 },
biaLevels: {
'_all': {
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.01, max: 0.02 } },
},
},
},
},
},
},
'E': {
name: 'Volledig Decentraal Beheer',
description: 'Business voert volledig beheer uit, ICMT alleen CMDB + jaarlijkse review',
allowedBia: ['A', 'B'],
defaultFte: { min: 0.01, max: 0.02 },
note: 'Vaste FTE ongeacht BIA en Hosting - alleen CMDB-registratie en jaarlijkse review',
applicationTypes: {
'Applicatie': {
fixedFte: true,
defaultFte: { min: 0.01, max: 0.02 },
biaLevels: {
'_all': {
description: 'Alle BIA-niveaus (A, B)',
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.01, max: 0.02 } },
},
},
},
},
'Platform': {
fixedFte: true,
notRecommended: true,
note: 'Model E is niet geschikt voor Platforms',
defaultFte: { min: 0.01, max: 0.02 },
biaLevels: {
'_all': {
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.01, max: 0.02 } },
},
},
},
},
'Workload': {
fixedFte: true,
notRecommended: true,
note: 'Model E is niet geschikt voor Workloads (vereist Platform)',
defaultFte: { min: 0.01, max: 0.02 },
biaLevels: {
'_all': {
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.01, max: 0.02 } },
},
},
},
},
'Connected Device': {
fixedFte: true,
defaultFte: { min: 0.01, max: 0.02 },
biaLevels: {
'_all': {
hosting: {
'_all': { hostingValues: ['On-Premises', 'Azure - Eigen beheer', 'Azure - Delegated Management', 'Extern (SaaS)'], fte: { min: 0.01, max: 0.02 } },
},
},
},
},
},
},
},
validationRules: {
biaRegieModelConstraints: {
'A': ['D', 'E', 'F'],
'B': ['C', 'D', 'E'],
'B+': ['C', 'D', 'E'],
'C': ['C', 'D', 'E', 'F'],
'D': ['A', 'B', 'C'],
'E': ['A', 'B'],
},
platformRestrictions: [
{ regiemodel: 'D', applicationType: 'Platform', warning: 'Niet aanbevolen vanwege governance-risico' },
{ regiemodel: 'E', applicationType: 'Platform', warning: 'Niet geschikt voor Platforms' },
{ regiemodel: 'E', applicationType: 'Workload', warning: 'Niet geschikt voor Workloads' },
],
},
};
/**
* Legacy configuration for backward compatibility
* This is used by the existing calculation logic until fully migrated
*/
export const EFFORT_CALCULATION_CONFIG: EffortCalculationConfig = {
governanceModelRules: [],
default: { result: 0.01 },
};

144
backend/src/config/env.ts Normal file
View File

@@ -0,0 +1,144 @@
import dotenv from 'dotenv';
import path from 'path';
// Load .env from project root
dotenv.config({ path: path.resolve(__dirname, '../../../.env') });
interface Config {
// Jira Assets
jiraHost: string;
jiraPat: string;
jiraSchemaId: string;
// Object Type IDs
jiraApplicationComponentTypeId: string;
jiraApplicationFunctionTypeId: string;
jiraDynamicsFactorTypeId: string;
jiraComplexityFactorTypeId: string;
jiraNumberOfUsersTypeId: string;
jiraGovernanceModelTypeId: string;
jiraApplicationClusterTypeId: string;
jiraApplicationTypeTypeId: string;
jiraHostingTypeTypeId: string;
jiraBusinessImpactAnalyseTypeId: string;
jiraApplicationManagementHostingTypeId: string; // Object Type ID for "Application Management - Hosting"
jiraApplicationManagementTAMTypeId: string; // Object Type ID for "Application Management - TAM"
// Attribute IDs
jiraAttrApplicationFunction: string;
jiraAttrDynamicsFactor: string;
jiraAttrComplexityFactor: string;
jiraAttrNumberOfUsers: string;
jiraAttrGovernanceModel: string;
jiraAttrApplicationCluster: string;
jiraAttrApplicationType: string;
jiraAttrPlatform: string;
jiraAttrHostingType: string;
jiraAttrBusinessImpactAnalyse: string;
jiraAttrTechnischeArchitectuur: string; // Attribute ID for "Technische Architectuur (TA)"
jiraAttrTechnicalApplicationManagementPrimary: string; // Attribute ID for "Technical Application Management Primary"
jiraAttrTechnicalApplicationManagementSecondary: string; // Attribute ID for "Technical Application Management Secondary"
jiraAttrOverrideFTE: string; // Attribute ID for "Application Management - Override FTE"
jiraAttrApplicationManagementHosting: string; // Attribute ID for "Application Management - Hosting" (4939)
jiraAttrApplicationManagementTAM: string; // Attribute ID for "Application Management - TAM" (4945)
// AI API Keys
anthropicApiKey: string;
openaiApiKey: string;
defaultAIProvider: 'claude' | 'openai';
// Web Search API (Tavily)
tavilyApiKey: string;
enableWebSearch: boolean;
// Application
port: number;
nodeEnv: string;
isDevelopment: boolean;
isProduction: boolean;
// API Configuration
jiraApiBatchSize: number;
}
function getEnvVar(name: string, defaultValue?: string): string {
const value = process.env[name] || defaultValue;
if (!value) {
throw new Error(`Environment variable ${name} is required but not set`);
}
return value;
}
function getOptionalEnvVar(name: string, defaultValue: string = ''): string {
return process.env[name] || defaultValue;
}
export const config: Config = {
// Jira Assets
jiraHost: getOptionalEnvVar('JIRA_HOST', 'https://jira.zuyderland.nl'),
jiraPat: getOptionalEnvVar('JIRA_PAT'),
jiraSchemaId: getOptionalEnvVar('JIRA_SCHEMA_ID'),
// Object Type IDs
jiraApplicationComponentTypeId: getOptionalEnvVar('JIRA_APPLICATION_COMPONENT_TYPE_ID'),
jiraApplicationFunctionTypeId: getOptionalEnvVar('JIRA_APPLICATION_FUNCTION_TYPE_ID'),
jiraDynamicsFactorTypeId: getOptionalEnvVar('JIRA_DYNAMICS_FACTOR_TYPE_ID'),
jiraComplexityFactorTypeId: getOptionalEnvVar('JIRA_COMPLEXITY_FACTOR_TYPE_ID'),
jiraNumberOfUsersTypeId: getOptionalEnvVar('JIRA_NUMBER_OF_USERS_TYPE_ID'),
jiraGovernanceModelTypeId: getOptionalEnvVar('JIRA_GOVERNANCE_MODEL_TYPE_ID'),
jiraApplicationClusterTypeId: getOptionalEnvVar('JIRA_APPLICATION_CLUSTER_TYPE_ID'),
jiraApplicationTypeTypeId: getOptionalEnvVar('JIRA_APPLICATION_TYPE_TYPE_ID'),
jiraHostingTypeTypeId: getOptionalEnvVar('JIRA_HOSTING_TYPE_TYPE_ID', '39'),
jiraBusinessImpactAnalyseTypeId: getOptionalEnvVar('JIRA_BUSINESS_IMPACT_ANALYSE_TYPE_ID', '41'),
jiraApplicationManagementHostingTypeId: getOptionalEnvVar('JIRA_APPLICATION_MANAGEMENT_HOSTING_TYPE_ID', '438'),
jiraApplicationManagementTAMTypeId: getOptionalEnvVar('JIRA_APPLICATION_MANAGEMENT_TAM_TYPE_ID', '439'),
// Attribute IDs
jiraAttrApplicationFunction: getOptionalEnvVar('JIRA_ATTR_APPLICATION_FUNCTION'),
jiraAttrDynamicsFactor: getOptionalEnvVar('JIRA_ATTR_DYNAMICS_FACTOR'),
jiraAttrComplexityFactor: getOptionalEnvVar('JIRA_ATTR_COMPLEXITY_FACTOR'),
jiraAttrNumberOfUsers: getOptionalEnvVar('JIRA_ATTR_NUMBER_OF_USERS'),
jiraAttrGovernanceModel: getOptionalEnvVar('JIRA_ATTR_GOVERNANCE_MODEL'),
jiraAttrApplicationCluster: getOptionalEnvVar('JIRA_ATTR_APPLICATION_CLUSTER'),
jiraAttrApplicationType: getOptionalEnvVar('JIRA_ATTR_APPLICATION_TYPE'),
jiraAttrPlatform: getOptionalEnvVar('JIRA_ATTR_PLATFORM'),
jiraAttrHostingType: getOptionalEnvVar('JIRA_ATTR_HOSTING_TYPE', '355'),
jiraAttrBusinessImpactAnalyse: getOptionalEnvVar('JIRA_ATTR_BUSINESS_IMPACT_ANALYSE', '368'),
jiraAttrTechnischeArchitectuur: getOptionalEnvVar('JIRA_ATTR_TECHNISCHE_ARCHITECTUUR', '572'),
jiraAttrTechnicalApplicationManagementPrimary: getOptionalEnvVar('JIRA_ATTR_TECHNICAL_APPLICATION_MANAGEMENT_PRIMARY', '377'),
jiraAttrTechnicalApplicationManagementSecondary: getOptionalEnvVar('JIRA_ATTR_TECHNICAL_APPLICATION_MANAGEMENT_SECONDARY', '1330'),
jiraAttrOverrideFTE: getOptionalEnvVar('JIRA_ATTR_OVERRIDE_FTE', '4932'),
jiraAttrApplicationManagementHosting: getOptionalEnvVar('JIRA_ATTR_APPLICATION_MANAGEMENT_HOSTING', '4939'),
jiraAttrApplicationManagementTAM: getOptionalEnvVar('JIRA_ATTR_APPLICATION_MANAGEMENT_TAM', '4945'),
// AI API Keys
anthropicApiKey: getOptionalEnvVar('ANTHROPIC_API_KEY'),
openaiApiKey: getOptionalEnvVar('OPENAI_API_KEY'),
defaultAIProvider: (getOptionalEnvVar('DEFAULT_AI_PROVIDER', 'claude') as 'claude' | 'openai'),
// Web Search API (Tavily)
tavilyApiKey: getOptionalEnvVar('TAVILY_API_KEY'),
enableWebSearch: getOptionalEnvVar('ENABLE_WEB_SEARCH', 'false').toLowerCase() === 'true',
// Application
port: parseInt(getOptionalEnvVar('PORT', '3001'), 10),
nodeEnv: getOptionalEnvVar('NODE_ENV', 'development'),
isDevelopment: getOptionalEnvVar('NODE_ENV', 'development') === 'development',
isProduction: getOptionalEnvVar('NODE_ENV', 'development') === 'production',
// API Configuration
jiraApiBatchSize: parseInt(getOptionalEnvVar('JIRA_API_BATCH_SIZE', '15'), 10),
};
export function validateConfig(): void {
const missingVars: string[] = [];
if (!config.jiraPat) missingVars.push('JIRA_PAT');
if (!config.jiraSchemaId) missingVars.push('JIRA_SCHEMA_ID');
if (!config.anthropicApiKey) missingVars.push('ANTHROPIC_API_KEY');
if (missingVars.length > 0) {
console.warn(`Warning: Missing environment variables: ${missingVars.join(', ')}`);
console.warn('Some features may not work correctly. Using mock data where possible.');
}
}

View File

@@ -0,0 +1,284 @@
{
"version": "2024.1",
"source": "Zuyderland ICMT - Application Management Framework",
"lastUpdated": "2024-12-19",
"referenceData": {
"applicationStatuses": [
{
"key": "status",
"name": "Status",
"description": "Algemene status",
"order": 0,
"color": "#6b7280",
"includeInFilter": true
},
{
"key": "prod",
"name": "In Production",
"description": "Productie - actief in gebruik",
"order": 1,
"color": "#22c55e",
"includeInFilter": true
},
{
"key": "impl",
"name": "Implementation",
"description": "In implementatie",
"order": 2,
"color": "#3b82f6",
"includeInFilter": true
},
{
"key": "poc",
"name": "Proof of Concept",
"description": "Proefproject",
"order": 3,
"color": "#8b5cf6",
"includeInFilter": true
},
{
"key": "eos",
"name": "End of support",
"description": "Geen ondersteuning meer van leverancier",
"order": 4,
"color": "#f97316",
"includeInFilter": true
},
{
"key": "eol",
"name": "End of life",
"description": "Einde levensduur, wordt uitgefaseerd",
"order": 5,
"color": "#ef4444",
"includeInFilter": true
},
{
"key": "deprecated",
"name": "Deprecated",
"description": "Verouderd, wordt uitgefaseerd",
"order": 6,
"color": "#f97316",
"includeInFilter": true
},
{
"key": "shadow",
"name": "Shadow IT",
"description": "Niet-geautoriseerde IT",
"order": 7,
"color": "#eab308",
"includeInFilter": true
},
{
"key": "closed",
"name": "Closed",
"description": "Afgesloten",
"order": 8,
"color": "#6b7280",
"includeInFilter": true
},
{
"key": "undefined",
"name": "Undefined",
"description": "Niet gedefinieerd",
"order": 9,
"color": "#9ca3af",
"includeInFilter": true
}
],
"dynamicsFactors": [
{
"key": "1",
"name": "Stabiel",
"description": "Weinig wijzigingen, uitgekristalliseerd systeem, < 2 releases/jaar",
"order": 1,
"color": "#22c55e"
},
{
"key": "2",
"name": "Gemiddeld",
"description": "Regelmatige wijzigingen, 2-4 releases/jaar, incidentele projecten",
"order": 2,
"color": "#eab308"
},
{
"key": "3",
"name": "Hoog",
"description": "Veel wijzigingen, > 4 releases/jaar, continue doorontwikkeling",
"order": 3,
"color": "#f97316"
},
{
"key": "4",
"name": "Zeer hoog",
"description": "Continu in beweging, grote transformatieprojecten, veel nieuwe functionaliteit",
"order": 4,
"color": "#ef4444"
}
],
"complexityFactors": [
{
"key": "1",
"name": "Laag",
"description": "Standalone applicatie, geen/weinig integraties, standaard configuratie",
"order": 1,
"color": "#22c55e"
},
{
"key": "2",
"name": "Gemiddeld",
"description": "Enkele integraties, beperkt maatwerk, standaard governance",
"order": 2,
"color": "#eab308"
},
{
"key": "3",
"name": "Hoog",
"description": "Veel integraties, significant maatwerk, meerdere stakeholdergroepen",
"order": 3,
"color": "#f97316"
},
{
"key": "4",
"name": "Zeer hoog",
"description": "Platform met meerdere workloads, uitgebreide governance, veel maatwerk",
"order": 4,
"color": "#ef4444"
}
],
"numberOfUsers": [
{
"key": "1",
"name": "< 100",
"minUsers": 0,
"maxUsers": 99,
"order": 1
},
{
"key": "2",
"name": "100 - 500",
"minUsers": 100,
"maxUsers": 500,
"order": 2
},
{
"key": "3",
"name": "500 - 2.000",
"minUsers": 500,
"maxUsers": 2000,
"order": 3
},
{
"key": "4",
"name": "2.000 - 5.000",
"minUsers": 2000,
"maxUsers": 5000,
"order": 4
},
{
"key": "5",
"name": "5.000 - 10.000",
"minUsers": 5000,
"maxUsers": 10000,
"order": 5
},
{
"key": "6",
"name": "10.000 - 15.000",
"minUsers": 10000,
"maxUsers": 15000,
"order": 6
},
{
"key": "7",
"name": "> 15.000",
"minUsers": 15000,
"maxUsers": null,
"order": 7
}
],
"governanceModels": [
{
"key": "A",
"name": "Centraal Beheer",
"shortDescription": "ICMT voert volledig beheer uit",
"description": "Volledige dienstverlening door ICMT. Dit is het standaardmodel voor kernapplicaties.",
"applicability": "Kernapplicaties met BIA-classificatie D, E of F (belangrijk tot zeer kritiek). Voorbeelden: EPD (HiX), ERP (AFAS), Microsoft 365, kritieke zorgapplicaties.",
"icmtInvolvement": "Volledig",
"businessInvolvement": "Minimaal",
"supplierInvolvement": "Via ICMT",
"order": 1,
"color": "#3b82f6"
},
{
"key": "B",
"name": "Federatief Beheer",
"shortDescription": "ICMT + business delen beheer",
"description": "ICMT en business delen de verantwoordelijkheid. Geschikt voor applicaties met een sterke key user organisatie.",
"applicability": "Kernapplicaties met BIA-classificatie D, E of F (belangrijk tot zeer kritiek). Voorbeelden: EPD (HiX), ERP (AFAS), Microsoft 365, kritieke zorgapplicaties.",
"icmtInvolvement": "Gedeeld",
"businessInvolvement": "Gedeeld",
"supplierInvolvement": "Via ICMT/Business",
"order": 2,
"color": "#8b5cf6"
},
{
"key": "C",
"name": "Uitbesteed met ICMT-Regie",
"shortDescription": "Leverancier beheert, ICMT regisseert",
"description": "Leverancier voert beheer uit, ICMT houdt regie. Dit is het standaardmodel voor SaaS waar ICMT contractpartij is.",
"applicability": "SaaS-applicaties waar ICMT het contract beheert. Voorbeelden: AFAS, diverse zorg-SaaS oplossingen. De mate van FAB-dienstverlening hangt af van de BIA-classificatie.",
"icmtInvolvement": "Regie",
"businessInvolvement": "Gebruiker",
"supplierInvolvement": "Volledig beheer",
"contractHolder": "ICMT",
"order": 3,
"color": "#06b6d4"
},
{
"key": "D",
"name": "Uitbesteed met Business-Regie",
"shortDescription": "Leverancier beheert, business regisseert",
"description": "Business onderhoudt de leveranciersrelatie. ICMT heeft beperkte betrokkenheid.",
"applicability": "SaaS-applicaties waar de business zelf het contract en de leveranciersrelatie beheert. Voorbeelden: niche SaaS tools, afdelingsspecifieke oplossingen, tools waar de business expertise heeft die ICMT niet heeft.",
"icmtInvolvement": "Beperkt",
"businessInvolvement": "Regie",
"supplierInvolvement": "Volledig beheer",
"contractHolder": "Business",
"order": 4,
"color": "#14b8a6"
},
{
"key": "E",
"name": "Volledig Decentraal Beheer",
"shortDescription": "Business voert volledig beheer uit",
"description": "Business voert zelf beheer uit. ICMT heeft minimale betrokkenheid.",
"applicability": "Afdelingsspecifieke tools met beperkte impact, Shadow IT die in kaart is gebracht. Voorbeelden: standalone afdelingstools, pilotapplicaties, persoonlijke productiviteitstools.",
"icmtInvolvement": "Minimaal",
"businessInvolvement": "Volledig",
"supplierInvolvement": "Direct met business",
"order": 5,
"color": "#6b7280"
}
]
},
"visualizations": {
"capacityMatrix": {
"description": "Matrix voor capaciteitsplanning gebaseerd op Dynamiek x Complexiteit",
"formula": "Beheerlast = Dynamiek * Complexiteit * log(Gebruikers)",
"weightings": {
"dynamics": 1.0,
"complexity": 1.2,
"users": 0.3
}
},
"governanceDecisionTree": {
"description": "Beslisboom voor keuze regiemodel",
"factors": [
"BIA-classificatie",
"Hosting type (SaaS/On-prem)",
"Contracthouder",
"Key user maturity"
]
}
}
}

View File

@@ -0,0 +1,649 @@
{
"version": "2024.1",
"source": "ZiRA - Ziekenhuis Referentie Architectuur (Nictiz)",
"lastUpdated": "2024-12-19",
"domains": [
{
"code": "STU",
"name": "Sturing",
"description": "Applicatiefuncties ter ondersteuning van besturing en management",
"functions": [
{
"code": "STU-001",
"name": "Beleid & Innovatie",
"description": "Functionaliteit voor ondersteuning van het bepalen en beheren van beleid, ontwikkeling producten & diensten, planning & control cyclus en ondersteunende managementinformatie",
"keywords": ["beleid", "innovatie", "strategie", "planning", "control", "managementinformatie", "BI", "business intelligence"]
},
{
"code": "STU-002",
"name": "Proces & Architectuur",
"description": "Functionaliteit voor het ontwikkelen en beheren van de enterprise architectuur (organisatie, processen, informatie, applicatie, techniek)",
"keywords": ["architectuur", "proces", "enterprise", "TOGAF", "ArchiMate", "modellering", "BPM"]
},
{
"code": "STU-003",
"name": "Project & Portfoliomanagement",
"description": "Functionaliteit voor het beheren van projecten en programma's",
"keywords": ["project", "portfolio", "programma", "PMO", "planning", "resource", "Jira", "MS Project"]
},
{
"code": "STU-004",
"name": "Kwaliteitsinformatiemanagement",
"description": "Functionaliteit voor de ondersteuning van het maken, verwerken en beheren van kwaliteitsdocumenten (inclusief protocollen)",
"keywords": ["kwaliteit", "protocol", "procedure", "document", "QMS", "ISO", "accreditatie", "Zenya"]
},
{
"code": "STU-005",
"name": "Performance & Verantwoording",
"description": "Functionaliteit voor het beheren van productieafspraken, KPI's inclusief beheer van de verantwoording in het kader van wet & regelgeving alsmede prestaties en maatschappelijk verantwoordschap",
"keywords": ["KPI", "dashboard", "verantwoording", "rapportage", "compliance", "prestatie", "IGJ"]
},
{
"code": "STU-006",
"name": "Marketing & Contractmanagement",
"description": "Functionaliteit voor ondersteuning van marktanalyses en contractmanagement",
"keywords": ["marketing", "contract", "leverancier", "SLA", "marktanalyse", "CRM"]
}
]
},
{
"code": "ONZ",
"name": "Onderzoek",
"description": "Applicatiefuncties ter ondersteuning van wetenschappelijk onderzoek",
"functions": [
{
"code": "ONZ-001",
"name": "Onderzoek ontwikkeling",
"description": "Functionaliteit voor de administratieve ondersteuning voor het indienen van een onderzoeksaanvraag, het opstellen van een onderzoeksprotocol, het opstellen van een onderzoeksvoorstel en de medisch etische keuring",
"keywords": ["onderzoek", "protocol", "METC", "ethiek", "aanvraag", "voorstel"]
},
{
"code": "ONZ-002",
"name": "Onderzoekvoorbereiding",
"description": "Functionaliteit voor de administratieve voorbereiding van het onderzoek als aanvraag van vergunningen en financieringen",
"keywords": ["vergunning", "financiering", "subsidie", "grant", "voorbereiding"]
},
{
"code": "ONZ-003",
"name": "Onderzoeksmanagement",
"description": "Functionaliteit voor de administratieve uitvoering van het onderzoek als aanvraag patientenselectie, verkrijgen consent",
"keywords": ["consent", "inclusie", "patientselectie", "trial", "studie", "CTMS"]
},
{
"code": "ONZ-004",
"name": "Researchdatamanagement",
"description": "Functionaliteit voor het verzamelen, bewerken, analyseren en publiceren van onderzoeksdata",
"keywords": ["research", "data", "analyse", "statistiek", "SPSS", "R", "Castor", "REDCap"]
},
{
"code": "ONZ-005",
"name": "Onderzoekpublicatie",
"description": "Functionaliteit voor de opslag van publicaties van onderzoeksresultaten",
"keywords": ["publicatie", "artikel", "repository", "Pure", "bibliografie"]
}
]
},
{
"code": "ZRG-SAM",
"name": "Zorg - Samenwerking",
"description": "Applicatiefuncties ter ondersteuning van samenwerking met patiënt en ketenpartners",
"functions": [
{
"code": "ZRG-SAM-001",
"name": "Dossier inzage",
"description": "Functionaliteit die het mogelijk maakt voor patiënten om digitale inzage te krijgen in medische dossiers die de zorgverleners over hen bijhouden",
"keywords": ["portaal", "inzage", "dossier", "patient", "MijnZuyderland", "toegang"]
},
{
"code": "ZRG-SAM-002",
"name": "Behandelondersteuning",
"description": "Functionaliteit voor het voorlichten en coachen van en communiceren met de patiënt over zijn zorg met als doel de patiënt te helpen bij het bereiken van de behandeldoelen en (mede)verantwoordelijkheid te geven voor behandelkeuzes en behandeling (patientempowerment)",
"keywords": ["voorlichting", "coaching", "empowerment", "educatie", "patient", "zelfmanagement"]
},
{
"code": "ZRG-SAM-003",
"name": "Interactie PGO",
"description": "Functionaliteit voor ondersteuning en integraties met een persoonlijke gezondheidsomgeving",
"keywords": ["PGO", "PHR", "persoonlijk", "gezondheidsomgeving", "MedMij"]
},
{
"code": "ZRG-SAM-004",
"name": "Patientenforum",
"description": "Functionaliteit voor het aanbieden van een online omgeving voor patienten (bv discussieforum voor patienten onderling)",
"keywords": ["forum", "community", "patient", "discussie", "lotgenoten"]
},
{
"code": "ZRG-SAM-005",
"name": "Preventie",
"description": "Functionaliteit ter bevordering van de gezondheid en ter voorkoming van klachten en problemen",
"keywords": ["preventie", "screening", "gezondheid", "vroegdetectie", "risico"]
},
{
"code": "ZRG-SAM-006",
"name": "Gezondheidsvragen",
"description": "Functionaliteit voor het on-line invullen van vragenlijsten bijvoorbeeld anamnestische vragenlijsten of gezondheidsvragenlijsten",
"keywords": ["vragenlijst", "anamnese", "intake", "PROM", "ePRO", "formulier"]
},
{
"code": "ZRG-SAM-007",
"name": "Kwaliteit en tevredenheidsmeting",
"description": "Functionaliteit om de effecten van behandelingen en de patiënttevredenheid te kunnen meten en vaststellen",
"keywords": ["tevredenheid", "kwaliteit", "PREM", "CQI", "NPS", "enquete", "feedback"]
},
{
"code": "ZRG-SAM-008",
"name": "Tele-consultatie",
"description": "Functionaliteit om een zorgprofessional remote (niet in elkaars fysieke aanwezigheid) te raadplegen in het kader van een gezondheidsvraag",
"keywords": ["teleconsultatie", "videoconsult", "beeldbellen", "remote", "consult"]
},
{
"code": "ZRG-SAM-009",
"name": "Zelfmonitoring",
"description": "Functionaliteit om de eigen gezondheidstoestand te bewaken",
"keywords": ["zelfmonitoring", "thuismeten", "wearable", "app", "meten"]
},
{
"code": "ZRG-SAM-010",
"name": "Tele-monitoring",
"description": "Functionaliteit waarmee de patient op afstand (tele) gevolgd en begeleid (monitoring) wordt door de zorgverlener met behulp van bij de patient aanwezige meetapparatuur",
"keywords": ["telemonitoring", "remote", "monitoring", "thuiszorg", "hartfalen", "COPD"]
},
{
"code": "ZRG-SAM-011",
"name": "On-line afspraken",
"description": "Functionaliteit voor het on-line maken van afspraken",
"keywords": ["afspraak", "online", "boeken", "reserveren", "planning"]
},
{
"code": "ZRG-SAM-012",
"name": "Dossieruitwisseling",
"description": "Functionaliteit voor het versturen en ontvangen en verwerken van dossierinformatie door bijvoorbeeld verwijzer, overdragende of consulterend arts",
"keywords": ["uitwisseling", "overdracht", "verwijzing", "XDS", "LSP", "Zorgplatform"]
},
{
"code": "ZRG-SAM-013",
"name": "Interactie externe bronnen",
"description": "Functionaliteit voor informatieuitwisseling met derden voor het verzamelen van additionele gegevens",
"keywords": ["extern", "koppeling", "integratie", "bron", "register"]
},
{
"code": "ZRG-SAM-014",
"name": "Samenwerking betrokken zorgverleners",
"description": "Functionaliteit voor het coördineren van zorg met andere zorgverleners en het documenteren daarvan",
"keywords": ["samenwerking", "keten", "MDO", "multidisciplinair", "consult"]
}
]
},
{
"code": "ZRG-CON",
"name": "Zorg - Consultatie & Behandeling",
"description": "Applicatiefuncties ter ondersteuning van het primaire zorgproces",
"functions": [
{
"code": "ZRG-CON-001",
"name": "Dossierraadpleging",
"description": "Functionaliteit voor het raadplegen van het dossier via verschillende views als patiëntgeschiedenis, decursus, samenvatting, problemen, diagnoses en allergieën",
"keywords": ["dossier", "raadplegen", "EPD", "decursus", "samenvatting", "overzicht"]
},
{
"code": "ZRG-CON-002",
"name": "Dossiervoering",
"description": "Functionaliteit voor het bijwerken van het dossier aan de hand van gegevens uit consult, behandeling en input vanuit andere bronnen",
"keywords": ["dossier", "registratie", "EPD", "notitie", "verslag", "brief"]
},
{
"code": "ZRG-CON-003",
"name": "Medicatie",
"description": "Functionaliteit van de ondersteuning van de medicamenteuze behandeling",
"keywords": ["medicatie", "voorschrijven", "EVS", "apotheek", "recept", "CPOE"]
},
{
"code": "ZRG-CON-004",
"name": "Operatie",
"description": "Functionaliteit voor de ondersteuning van het operatieve proces",
"keywords": ["OK", "operatie", "chirurgie", "planning", "anesthesie", "perioperatief"]
},
{
"code": "ZRG-CON-005",
"name": "Patientbewaking",
"description": "Functionaliteit voor bewaking van de patienten (bv medische alarmering, monitoring, dwaaldetectie, valdetectie)",
"keywords": ["monitoring", "bewaking", "alarm", "IC", "telemetrie", "vitale functies"]
},
{
"code": "ZRG-CON-006",
"name": "Beslissingsondersteuning",
"description": "Functionaliteit voor de ondersteuning van besluiten van de zorgverlener",
"keywords": ["CDSS", "beslissing", "advies", "alert", "waarschuwing", "protocol"]
},
{
"code": "ZRG-CON-007",
"name": "Verzorgingondersteuning",
"description": "Functionaliteit voor de ondersteuning van het verzorgingsproces als aanvragen van verzorgingsdiensten",
"keywords": ["verzorging", "verpleging", "zorgplan", "ADL", "voeding"]
},
{
"code": "ZRG-CON-008",
"name": "Ordermanagement",
"description": "Functionaliteit voor de uitvoering van de closed order loop van onderzoeken (aanvraag, planning, oplevering, acceptatie)",
"keywords": ["order", "aanvraag", "lab", "onderzoek", "workflow", "ORM"]
},
{
"code": "ZRG-CON-009",
"name": "Resultaat afhandeling",
"description": "Functionaliteit voor de analyse en rapportage van resultaten en notificatie naar zorgverleners en/of patient",
"keywords": ["resultaat", "uitslag", "notificatie", "rapport", "bevinding"]
},
{
"code": "ZRG-CON-010",
"name": "Kwaliteitsbewaking",
"description": "Functionaliteit voor de bewaking en signalering van (mogelijke) fouten (verkeerde patient, verkeerde dosis, verkeerde tijd, verkeerde vervolgstap)",
"keywords": ["kwaliteit", "veiligheid", "controle", "check", "alert", "CDSS"]
}
]
},
{
"code": "ZRG-AOZ",
"name": "Zorg - Aanvullend onderzoek",
"description": "Applicatiefuncties ter ondersteuning van diagnostisch onderzoek",
"functions": [
{
"code": "ZRG-AOZ-001",
"name": "Laboratoriumonderzoek",
"description": "Functionaliteit voor de ondersteuning van processen op laboratoria (kcl, microbiologie, pathologie, klinische genetica, apotheeklab, etc)",
"keywords": ["lab", "LIMS", "laboratorium", "KCL", "microbiologie", "pathologie", "genetica"]
},
{
"code": "ZRG-AOZ-002",
"name": "Beeldvormend onderzoek",
"description": "Functionaliteit voor de ondersteuning van Beeldvormend onderzoek voor bijvoorbeeld Radiologie, Nucleair, Cardologie inclusief beeldmanagement (zoals VNA)",
"keywords": ["PACS", "RIS", "radiologie", "CT", "MRI", "echo", "VNA", "DICOM"]
},
{
"code": "ZRG-AOZ-003",
"name": "Functieonderzoek",
"description": "Functionaliteit voor de ondersteuning van Functieonderzoek (voorbeelden ECG, Longfunctie, Audiologie)",
"keywords": ["ECG", "longfunctie", "audiologie", "functie", "EEG", "EMG"]
}
]
},
{
"code": "ZRG-ZON",
"name": "Zorg - Zorgondersteuning",
"description": "Applicatiefuncties ter ondersteuning van de zorglogistiek",
"functions": [
{
"code": "ZRG-ZON-001",
"name": "Zorgrelatiebeheer",
"description": "Functionaliteit voor beheren van alle gegevens van zorgrelaties (zorgaanbieders, zorgverleners, zorgverzekeraars e.d.)",
"keywords": ["AGB", "zorgverlener", "verwijzer", "huisarts", "verzekeraar", "register"]
},
{
"code": "ZRG-ZON-002",
"name": "Zorgplanning",
"description": "Functionaliteit voor het maken en beheren van afspraken, opnames, overplaatsingen, ontslag en verwijzing",
"keywords": ["planning", "afspraak", "agenda", "opname", "ontslag", "bed"]
},
{
"code": "ZRG-ZON-003",
"name": "Resource planning",
"description": "Functionaliteit voor het plannen van resources (personen, zorgverleners) en middelen",
"keywords": ["resource", "capaciteit", "rooster", "personeel", "middelen"]
},
{
"code": "ZRG-ZON-004",
"name": "Patiëntadministratie",
"description": "Functionaliteit voor beheer van demografie, contactpersonen en alle andere (niet medische) informatie nodig voor het ondersteunen van het consult en de behandeling",
"keywords": ["ZIS", "administratie", "demografie", "patient", "registratie", "NAW"]
},
{
"code": "ZRG-ZON-005",
"name": "Patiëntenlogistiek",
"description": "Functionaliteit voor de ondersteuning van het verplaatsen van mensen en middelen (bv transportlogistiek, route ondersteuning, track & tracing, aanmeldregistratie, wachtrijmanagement, oproep)",
"keywords": ["logistiek", "transport", "wachtrij", "aanmeldzuil", "tracking", "routing"]
},
{
"code": "ZRG-ZON-006",
"name": "Zorgfacturering",
"description": "Functionaliteit voor de vastlegging van de verrichting en factureren van het zorgproduct",
"keywords": ["facturatie", "DBC", "DOT", "declaratie", "verrichting", "tarief"]
}
]
},
{
"code": "OND",
"name": "Onderwijs",
"description": "Applicatiefuncties ter ondersteuning van medisch onderwijs",
"functions": [
{
"code": "OND-001",
"name": "Onderwijsportfolio",
"description": "Functionaliteit voor creatie en beheer van het onderwijsportfolio",
"keywords": ["portfolio", "EPA", "competentie", "voortgang", "student"]
},
{
"code": "OND-002",
"name": "Learning Content Management",
"description": "Functionaliteit creatie en beheer van onderwijscontent",
"keywords": ["LMS", "content", "cursus", "module", "e-learning"]
},
{
"code": "OND-003",
"name": "Educatie",
"description": "Functionaliteit voor het geven van educatie dmv digitale middelen",
"keywords": ["educatie", "training", "scholing", "e-learning", "webinar"]
},
{
"code": "OND-004",
"name": "Toetsing",
"description": "Functionaliteit voor het geven en beoordelen van toetsen",
"keywords": ["toets", "examen", "beoordeling", "assessment", "evaluatie"]
},
{
"code": "OND-005",
"name": "Student Informatie",
"description": "Functionaliteit voor het beheren van alle informatie van en over de student",
"keywords": ["SIS", "student", "opleiding", "registratie", "inschrijving"]
},
{
"code": "OND-006",
"name": "Onderwijs rooster & planning",
"description": "Functionaliteit voor het roosteren en plannen van het onderwijsprogramma",
"keywords": ["rooster", "planning", "stage", "coschap", "onderwijs"]
}
]
},
{
"code": "BED",
"name": "Bedrijfsondersteuning",
"description": "Applicatiefuncties ter ondersteuning van bedrijfsvoering",
"functions": [
{
"code": "BED-001",
"name": "Vastgoed",
"description": "Functionaliteit die beheer, bouw en exploitatie van gebouwen en de daaraan verbonden faciliteiten en goederenstromen ondersteunt",
"keywords": ["vastgoed", "gebouw", "facilitair", "onderhoud", "FMIS"]
},
{
"code": "BED-002",
"name": "Inkoop",
"description": "Functionaliteit die inkopen van producten en diensten alsook het beheren van leveranciers en contracten ondersteunt",
"keywords": ["inkoop", "procurement", "leverancier", "bestelling", "contract"]
},
{
"code": "BED-003",
"name": "Voorraadbeheer",
"description": "Beheren/beheersen van de in- en uitgaande goederenstroom (door middel van planningtools) inclusief supply chain",
"keywords": ["voorraad", "magazijn", "supply chain", "logistiek", "inventaris"]
},
{
"code": "BED-004",
"name": "Kennismanagement",
"description": "Functionaliteit die het creëeren en delen van gezamenlijke kennis ondersteunt",
"keywords": ["kennis", "wiki", "intranet", "SharePoint", "documentatie"]
},
{
"code": "BED-005",
"name": "Datamanagement",
"description": "Functionaliteit voor ondersteunen van datamanagement, inclusief reference & master datamangement, metadatamanagement, dataanalytics",
"keywords": ["data", "master data", "metadata", "analytics", "datawarehouse", "BI"]
},
{
"code": "BED-006",
"name": "Voorlichting",
"description": "Functionaliteit die het geven van voorlichting via verschillende kanalen ondersteunt",
"keywords": ["website", "CMS", "communicatie", "voorlichting", "publicatie"]
},
{
"code": "BED-007",
"name": "Hotelservice",
"description": "Functionaliteit die de hotelfunctie ondersteunt, hierbij inbegrepen zijn parkeren, catering, kassa",
"keywords": ["catering", "restaurant", "parkeren", "kassa", "hotel"]
},
{
"code": "BED-008",
"name": "Klachtenafhandeling",
"description": "Functionaliteit die de afhandeling van klachten ondersteunt",
"keywords": ["klacht", "melding", "incident", "feedback", "MIC", "MIM"]
},
{
"code": "BED-009",
"name": "Personeelbeheer",
"description": "Functionaliteit die het administreren en managen van medewerkers ondersteunt",
"keywords": ["HR", "HRM", "personeel", "medewerker", "werving", "talent"]
},
{
"code": "BED-010",
"name": "Tijdsregistratie",
"description": "Functionaliteit waarmee het registreren van de bestede tijd van individuen wordt ondersteund",
"keywords": ["tijd", "uren", "registratie", "klokken", "rooster"]
},
{
"code": "BED-011",
"name": "Financieel beheer",
"description": "Functionaliteit waarmee de financiële administratie en verwerking van financiële stromen wordt ondersteund",
"keywords": ["financieel", "boekhouding", "factuur", "budget", "ERP", "SAP"]
},
{
"code": "BED-012",
"name": "Salarisverwerking",
"description": "Functionaliteit waarmee het uitbetalen van salarissen aan medewerkers wordt ondersteund",
"keywords": ["salaris", "loon", "payroll", "verloning"]
},
{
"code": "BED-013",
"name": "Beheren medische technologie",
"description": "Functionaliteit die beheer, onderhoud en gebruik van diverse medische apparatuur ondersteunt",
"keywords": ["MT", "medische techniek", "apparatuur", "onderhoud", "kalibratie"]
},
{
"code": "BED-014",
"name": "Beveiliging",
"description": "Functionaliteit die ondersteunt bij het uitvoeren van de veiligheid, kwaliteit en milieu taken en verplichtingen",
"keywords": ["beveiliging", "VGM", "ARBO", "milieu", "veiligheid"]
},
{
"code": "BED-015",
"name": "Relatiebeheer",
"description": "Functionaliteit ter ondersteuning van relatiebeheer in brede zin",
"keywords": ["CRM", "relatie", "stakeholder", "contact", "netwerk"]
},
{
"code": "BED-016",
"name": "ICT-change en servicemanagement",
"description": "Functies voor het faciliteren van hulpvragen en oplossingen",
"keywords": ["ITSM", "servicedesk", "incident", "change", "TOPdesk", "ServiceNow"]
}
]
},
{
"code": "GEN-WRK",
"name": "Generieke ICT - Werkplek en samenwerken",
"description": "Generieke ICT-functies voor werkplek en samenwerking",
"functions": [
{
"code": "GEN-WRK-001",
"name": "Beheren werkplek",
"description": "Functionaliteit voor beheren hardware (PC, monitor, mobile device, printers, scanners, bedside, tv e.d.) en software op de werkplek of bed-site (LCM, CMDB, deployment, virtual desktop)",
"keywords": ["werkplek", "PC", "laptop", "VDI", "Citrix", "deployment", "SCCM", "Intune"]
},
{
"code": "GEN-WRK-002",
"name": "Printing & scanning",
"description": "Functionaliteit voor het afdrukken en scannen",
"keywords": ["print", "scan", "printer", "MFP", "document"]
},
{
"code": "GEN-WRK-003",
"name": "Kantoorautomatisering",
"description": "Functionaliteit voor standaard kantoorondersteuning (tekstverwerking, spreadsheet, e-mail en agenda)",
"keywords": ["Office", "Microsoft 365", "Word", "Excel", "Outlook", "email", "agenda"]
},
{
"code": "GEN-WRK-004",
"name": "Unified communications",
"description": "Functionaliteit voor de (geïntegreerde) communicatie tussen mensen via verschillende kanalen (spraak, instant messaging, video)",
"keywords": ["Teams", "telefonie", "video", "chat", "communicatie", "VoIP"]
},
{
"code": "GEN-WRK-005",
"name": "Document & Beeld beheer",
"description": "Functionaliteit voor het beheren van documenten en beelden",
"keywords": ["DMS", "document", "archief", "SharePoint", "OneDrive"]
},
{
"code": "GEN-WRK-006",
"name": "Content management",
"description": "Functionaliteit voor het verzamelen, managen en publiceren van (niet-patientgebonden) informatie in elke vorm of medium",
"keywords": ["CMS", "website", "intranet", "publicatie", "content"]
},
{
"code": "GEN-WRK-007",
"name": "Publieke ICT services",
"description": "Functionaliteit voor het aanbieden van bv radio en tv, internet, e-books, netflix",
"keywords": ["gastnetwerk", "wifi", "entertainment", "internet", "publiek"]
}
]
},
{
"code": "GEN-IAM",
"name": "Generieke ICT - Identiteit, toegang en beveiliging",
"description": "Generieke ICT-functies voor identity en access management",
"functions": [
{
"code": "GEN-IAM-001",
"name": "Identiteit & Authenticatie",
"description": "Functionaliteit voor het identificeren en authenticeren van individuen in systemen",
"keywords": ["IAM", "identiteit", "authenticatie", "SSO", "MFA", "Active Directory", "Entra"]
},
{
"code": "GEN-IAM-002",
"name": "Autorisatie management",
"description": "Functionaliteit voor beheren van rechten en toegang",
"keywords": ["autorisatie", "RBAC", "rechten", "toegang", "rollen"]
},
{
"code": "GEN-IAM-003",
"name": "Auditing & monitoring",
"description": "Functionaliteit voor audits en monitoring in het kader van rechtmatig gebruik en toegang",
"keywords": ["audit", "logging", "SIEM", "compliance", "NEN7513"]
},
{
"code": "GEN-IAM-004",
"name": "Certificate service",
"description": "Functionaliteit voor uitgifte en beheer van certificaten",
"keywords": ["certificaat", "PKI", "SSL", "TLS", "signing"]
},
{
"code": "GEN-IAM-005",
"name": "ICT Preventie en protectie",
"description": "Functionaliteit voor beheersen van kwetsbaarheden en penetraties",
"keywords": ["security", "antivirus", "EDR", "firewall", "vulnerability", "pentest"]
}
]
},
{
"code": "GEN-DC",
"name": "Generieke ICT - Datacenter",
"description": "Generieke ICT-functies voor datacenter en hosting",
"functions": [
{
"code": "GEN-DC-001",
"name": "Hosting servercapaciteit",
"description": "Functionaliteit voor het leveren van serverinfrastructuur (CPU power)",
"keywords": ["server", "hosting", "VM", "compute", "cloud", "Azure"]
},
{
"code": "GEN-DC-002",
"name": "Datacenter housing",
"description": "Functionaliteit voor beheren van het datacenter, bijvoorbeeld fysieke toegang, cooling",
"keywords": ["datacenter", "housing", "colocation", "rack", "cooling"]
},
{
"code": "GEN-DC-003",
"name": "Hosting data storage",
"description": "Functionaliteit voor data opslag",
"keywords": ["storage", "SAN", "NAS", "opslag", "disk"]
},
{
"code": "GEN-DC-004",
"name": "Data archiving",
"description": "Functionaliteit voor het archiveren van gegevens",
"keywords": ["archief", "archivering", "retentie", "backup", "cold storage"]
},
{
"code": "GEN-DC-005",
"name": "Backup & recovery",
"description": "Functionaliteit voor back-up en herstel",
"keywords": ["backup", "restore", "recovery", "DR", "disaster recovery"]
},
{
"code": "GEN-DC-006",
"name": "Database management",
"description": "Functionaliteit voor het beheren van databases",
"keywords": ["database", "SQL", "Oracle", "DBA", "DBMS"]
},
{
"code": "GEN-DC-007",
"name": "Provisioning & automation service",
"description": "Functionaliteit voor het distribueren en automatiseren van diensten/applicaties",
"keywords": ["automation", "provisioning", "deployment", "DevOps", "CI/CD"]
},
{
"code": "GEN-DC-008",
"name": "Monitoring & alerting",
"description": "Functionaliteit voor het monitoren en analyseren van het datacentrum",
"keywords": ["monitoring", "APM", "alerting", "Zabbix", "Splunk", "observability"]
},
{
"code": "GEN-DC-009",
"name": "Servermanagement",
"description": "Functionaliteit voor het beheren van servers",
"keywords": ["server", "beheer", "patching", "configuratie", "lifecycle"]
}
]
},
{
"code": "GEN-CON",
"name": "Generieke ICT - Connectiviteit",
"description": "Generieke ICT-functies voor netwerk en connectiviteit",
"functions": [
{
"code": "GEN-CON-001",
"name": "Netwerkmanagement",
"description": "Functionaliteit voor het beheren van het netwerk zoals bijv. acceptatie van hardware op netwerk/DC-LAN, Campus-LAN, WAN",
"keywords": ["netwerk", "LAN", "WAN", "switch", "router", "wifi"]
},
{
"code": "GEN-CON-002",
"name": "Locatiebepaling",
"description": "Functies voor het traceren en volgen van items of eigendom, nu of in het verleden. Bijvoorbeeld RFID-toepassingen",
"keywords": ["RFID", "RTLS", "tracking", "locatie", "asset tracking"]
},
{
"code": "GEN-CON-003",
"name": "DNS & IP Adress management",
"description": "Functionaliteit voor het beheren van DNS en IP adressen",
"keywords": ["DNS", "DHCP", "IP", "IPAM", "domain"]
},
{
"code": "GEN-CON-004",
"name": "Remote Access",
"description": "Functionaliteit voor toegang op afstand zoals inbelfaciliteiten",
"keywords": ["VPN", "remote", "thuiswerken", "toegang", "DirectAccess"]
},
{
"code": "GEN-CON-005",
"name": "Load Balancing",
"description": "Functionaliteit voor beheren van server en netwerkbelasting",
"keywords": ["load balancer", "F5", "HAProxy", "traffic", "availability"]
},
{
"code": "GEN-CON-006",
"name": "Gegevensuitwisseling",
"description": "Functionaliteit voor de ondersteuning van het gegevensuitwisseling (ESB, Message broker)",
"keywords": ["integratie", "ESB", "API", "HL7", "FHIR", "message broker", "MuleSoft"]
}
]
}
]
}

102
backend/src/index.ts Normal file
View File

@@ -0,0 +1,102 @@
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import { config, validateConfig } from './config/env.js';
import { logger } from './services/logger.js';
import { dataService } from './services/dataService.js';
import applicationsRouter from './routes/applications.js';
import classificationsRouter from './routes/classifications.js';
import referenceDataRouter from './routes/referenceData.js';
import dashboardRouter from './routes/dashboard.js';
import configurationRouter from './routes/configuration.js';
// Validate configuration
validateConfig();
const app = express();
// Security middleware
app.use(helmet());
app.use(cors({
origin: config.isDevelopment ? '*' : ['http://localhost:5173', 'http://localhost:3000'],
credentials: true,
}));
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 1000, // Limit each IP to 1000 requests per windowMs
message: 'Too many requests from this IP, please try again later.',
});
app.use(limiter);
// Body parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// Request logging
app.use((req, res, next) => {
logger.debug(`${req.method} ${req.path}`);
next();
});
// Health check
app.get('/health', async (req, res) => {
const jiraConnected = await dataService.testConnection();
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
dataSource: dataService.isUsingJiraAssets() ? 'jira-assets' : 'mock-data',
jiraConnected: dataService.isUsingJiraAssets() ? jiraConnected : null,
aiConfigured: !!config.anthropicApiKey,
});
});
// Config endpoint
app.get('/api/config', (req, res) => {
res.json({
jiraHost: config.jiraHost,
});
});
// API routes
app.use('/api/applications', applicationsRouter);
app.use('/api/classifications', classificationsRouter);
app.use('/api/reference-data', referenceDataRouter);
app.use('/api/dashboard', dashboardRouter);
app.use('/api/configuration', configurationRouter);
// Error handling
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.error('Unhandled error:', err);
res.status(500).json({
error: 'Internal server error',
message: config.isDevelopment ? err.message : undefined,
});
});
// 404 handler
app.use((req, res) => {
res.status(404).json({ error: 'Not found' });
});
// Start server
const PORT = config.port;
app.listen(PORT, () => {
logger.info(`Server running on http://localhost:${PORT}`);
logger.info(`Environment: ${config.nodeEnv}`);
logger.info(`AI Classification: ${config.anthropicApiKey ? 'Configured' : 'Not configured'}`);
logger.info(`Jira Assets: ${config.jiraPat ? 'Configured' : 'Using mock data'}`);
});
// Graceful shutdown
process.on('SIGTERM', () => {
logger.info('SIGTERM signal received: closing HTTP server');
process.exit(0);
});
process.on('SIGINT', () => {
logger.info('SIGINT signal received: closing HTTP server');
process.exit(0);
});

View File

@@ -0,0 +1,217 @@
import { Router, Request, Response } from 'express';
import { dataService } from '../services/dataService.js';
import { databaseService } from '../services/database.js';
import { logger } from '../services/logger.js';
import { calculateRequiredEffortApplicationManagement, calculateRequiredEffortApplicationManagementWithBreakdown } from '../services/effortCalculation.js';
import type { SearchFilters, ApplicationUpdateRequest, ReferenceValue, ClassificationResult, ApplicationDetails, ApplicationStatus } from '../types/index.js';
const router = Router();
// Search applications with filters
router.post('/search', async (req: Request, res: Response) => {
try {
const { filters, page = 1, pageSize = 25 } = req.body as {
filters: SearchFilters;
page?: number;
pageSize?: number;
};
const result = await dataService.searchApplications(filters, page, pageSize);
res.json(result);
} catch (error) {
logger.error('Failed to search applications', error);
res.status(500).json({ error: 'Failed to search applications' });
}
});
// Get team dashboard data
router.get('/team-dashboard', async (req: Request, res: Response) => {
try {
const excludedStatusesParam = req.query.excludedStatuses as string | undefined;
let excludedStatuses: ApplicationStatus[] = [];
if (excludedStatusesParam && excludedStatusesParam.trim().length > 0) {
// Parse comma-separated statuses
excludedStatuses = excludedStatusesParam
.split(',')
.map(s => s.trim())
.filter(s => s.length > 0) as ApplicationStatus[];
} else {
// Default to excluding 'Closed' and 'Deprecated' if not specified
excludedStatuses = ['Closed', 'Deprecated'];
}
const data = await dataService.getTeamDashboardData(excludedStatuses);
res.json(data);
} catch (error) {
logger.error('Failed to get team dashboard data', error);
res.status(500).json({ error: 'Failed to get team dashboard data' });
}
});
// Get application by ID
router.get('/:id', async (req: Request, res: Response) => {
try {
const { id } = req.params;
// Don't treat special routes as application IDs
if (id === 'team-dashboard' || id === 'calculate-effort' || id === 'search') {
res.status(404).json({ error: 'Route not found' });
return;
}
const application = await dataService.getApplicationById(id);
if (!application) {
res.status(404).json({ error: 'Application not found' });
return;
}
res.json(application);
} catch (error) {
logger.error('Failed to get application', error);
res.status(500).json({ error: 'Failed to get application' });
}
});
// Update application
router.put('/:id', async (req: Request, res: Response) => {
try {
const { id } = req.params;
const updates = req.body as {
applicationFunctions?: ReferenceValue[];
dynamicsFactor?: ReferenceValue;
complexityFactor?: ReferenceValue;
numberOfUsers?: ReferenceValue;
governanceModel?: ReferenceValue;
source?: 'AI_ACCEPTED' | 'AI_MODIFIED' | 'MANUAL';
};
const application = await dataService.getApplicationById(id);
if (!application) {
res.status(404).json({ error: 'Application not found' });
return;
}
// Build changes object for history
const changes: ClassificationResult['changes'] = {};
if (updates.applicationFunctions) {
changes.applicationFunctions = {
from: application.applicationFunctions,
to: updates.applicationFunctions,
};
}
if (updates.dynamicsFactor) {
changes.dynamicsFactor = {
from: application.dynamicsFactor,
to: updates.dynamicsFactor,
};
}
if (updates.complexityFactor) {
changes.complexityFactor = {
from: application.complexityFactor,
to: updates.complexityFactor,
};
}
if (updates.numberOfUsers) {
changes.numberOfUsers = {
from: application.numberOfUsers,
to: updates.numberOfUsers,
};
}
if (updates.governanceModel) {
changes.governanceModel = {
from: application.governanceModel,
to: updates.governanceModel,
};
}
const success = await dataService.updateApplication(id, updates);
if (success) {
// Save to classification history
const classificationResult: ClassificationResult = {
applicationId: id,
applicationName: application.name,
changes,
source: updates.source || 'MANUAL',
timestamp: new Date(),
};
databaseService.saveClassificationResult(classificationResult);
const updatedApp = await dataService.getApplicationById(id);
res.json(updatedApp);
} else {
res.status(500).json({ error: 'Failed to update application' });
}
} catch (error) {
logger.error('Failed to update application', error);
res.status(500).json({ error: 'Failed to update application' });
}
});
// Calculate FTE effort for an application (real-time calculation without saving)
router.post('/calculate-effort', async (req: Request, res: Response) => {
try {
const applicationData = req.body as Partial<ApplicationDetails>;
// Build a complete ApplicationDetails object with defaults
const application: ApplicationDetails = {
id: applicationData.id || '',
key: applicationData.key || '',
name: applicationData.name || '',
searchReference: applicationData.searchReference || null,
description: applicationData.description || null,
supplierProduct: applicationData.supplierProduct || null,
organisation: applicationData.organisation || null,
hostingType: applicationData.hostingType || null,
status: applicationData.status || null,
businessImportance: applicationData.businessImportance || null,
businessImpactAnalyse: applicationData.businessImpactAnalyse || null,
systemOwner: applicationData.systemOwner || null,
businessOwner: applicationData.businessOwner || null,
functionalApplicationManagement: applicationData.functionalApplicationManagement || null,
technicalApplicationManagement: applicationData.technicalApplicationManagement || null,
technicalApplicationManagementPrimary: applicationData.technicalApplicationManagementPrimary || null,
technicalApplicationManagementSecondary: applicationData.technicalApplicationManagementSecondary || null,
medischeTechniek: applicationData.medischeTechniek || false,
applicationFunctions: applicationData.applicationFunctions || [],
dynamicsFactor: applicationData.dynamicsFactor || null,
complexityFactor: applicationData.complexityFactor || null,
numberOfUsers: applicationData.numberOfUsers || null,
governanceModel: applicationData.governanceModel || null,
applicationCluster: applicationData.applicationCluster || null,
applicationType: applicationData.applicationType || null,
platform: applicationData.platform || null,
requiredEffortApplicationManagement: null,
overrideFTE: applicationData.overrideFTE || null,
applicationManagementHosting: applicationData.applicationManagementHosting || null,
applicationManagementTAM: applicationData.applicationManagementTAM || null,
technischeArchitectuur: applicationData.technischeArchitectuur || null,
};
const result = calculateRequiredEffortApplicationManagementWithBreakdown(application);
res.json({
requiredEffortApplicationManagement: result.finalEffort,
breakdown: result.breakdown,
});
} catch (error) {
logger.error('Failed to calculate effort', error);
res.status(500).json({ error: 'Failed to calculate effort' });
}
});
// Get application classification history
router.get('/:id/history', async (req: Request, res: Response) => {
try {
const { id } = req.params;
const history = databaseService.getClassificationsByApplicationId(id);
res.json(history);
} catch (error) {
logger.error('Failed to get classification history', error);
res.status(500).json({ error: 'Failed to get classification history' });
}
});
export default router;

View File

@@ -0,0 +1,203 @@
import { Router, Request, Response } from 'express';
import { aiService, AIProvider } from '../services/claude.js';
import { dataService } from '../services/dataService.js';
import { databaseService } from '../services/database.js';
import { logger } from '../services/logger.js';
import { config } from '../config/env.js';
const router = Router();
// Get AI classification for an application
router.post('/suggest/:id', async (req: Request, res: Response) => {
try {
const { id } = req.params;
// Get provider from query parameter or request body, default to config
const provider = (req.query.provider as AIProvider) || (req.body.provider as AIProvider) || config.defaultAIProvider;
if (!aiService.isConfigured(provider)) {
res.status(503).json({
error: 'AI classification not available',
message: `${provider === 'claude' ? 'Claude (Anthropic)' : 'OpenAI'} API is not configured. Please set ${provider === 'claude' ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY'}.`
});
return;
}
const application = await dataService.getApplicationById(id);
if (!application) {
res.status(404).json({ error: 'Application not found' });
return;
}
logger.info(`Generating AI classification for: ${application.name} using ${provider}`);
const suggestion = await aiService.classifyApplication(application, provider);
res.json(suggestion);
} catch (error) {
logger.error('Failed to generate AI classification', error);
res.status(500).json({ error: 'Failed to generate AI classification' });
}
});
// Get ZiRA taxonomy
router.get('/taxonomy', (req: Request, res: Response) => {
try {
const taxonomy = aiService.getTaxonomy();
res.json(taxonomy);
} catch (error) {
logger.error('Failed to get taxonomy', error);
res.status(500).json({ error: 'Failed to get taxonomy' });
}
});
// Get function by code
router.get('/function/:code', (req: Request, res: Response) => {
try {
const { code } = req.params;
const func = aiService.getFunctionByCode(code);
if (!func) {
res.status(404).json({ error: 'Function not found' });
return;
}
res.json(func);
} catch (error) {
logger.error('Failed to get function', error);
res.status(500).json({ error: 'Failed to get function' });
}
});
// Get classification history
router.get('/history', (req: Request, res: Response) => {
try {
const limit = parseInt(req.query.limit as string) || 50;
const history = databaseService.getClassificationHistory(limit);
res.json(history);
} catch (error) {
logger.error('Failed to get classification history', error);
res.status(500).json({ error: 'Failed to get classification history' });
}
});
// Get classification stats
router.get('/stats', (req: Request, res: Response) => {
try {
const dbStats = databaseService.getStats();
res.json(dbStats);
} catch (error) {
logger.error('Failed to get classification stats', error);
res.status(500).json({ error: 'Failed to get classification stats' });
}
});
// Check if AI is available - returns available providers
router.get('/ai-status', (req: Request, res: Response) => {
const availableProviders = aiService.getAvailableProviders();
res.json({
available: availableProviders.length > 0,
providers: availableProviders,
defaultProvider: config.defaultAIProvider,
claude: {
available: aiService.isProviderConfigured('claude'),
model: 'claude-sonnet-4-20250514',
},
openai: {
available: aiService.isProviderConfigured('openai'),
model: 'gpt-4o',
},
});
});
// Get the AI prompt for an application (for debugging/copy-paste)
router.get('/prompt/:id', async (req: Request, res: Response) => {
try {
const { id } = req.params;
const application = await dataService.getApplicationById(id);
if (!application) {
res.status(404).json({ error: 'Application not found' });
return;
}
const prompt = await aiService.getPromptForApplication(application);
res.json({ prompt });
} catch (error) {
logger.error('Failed to get AI prompt', error);
res.status(500).json({ error: 'Failed to get AI prompt' });
}
});
// Chat with AI about an application
router.post('/chat/:id', async (req: Request, res: Response) => {
try {
const { id } = req.params;
const { message, conversationId, provider: requestProvider } = req.body;
if (!message || typeof message !== 'string' || message.trim().length === 0) {
res.status(400).json({ error: 'Message is required' });
return;
}
const provider = (requestProvider as AIProvider) || config.defaultAIProvider;
if (!aiService.isConfigured(provider)) {
res.status(503).json({
error: 'AI chat not available',
message: `${provider === 'claude' ? 'Claude (Anthropic)' : 'OpenAI'} API is not configured.`
});
return;
}
const application = await dataService.getApplicationById(id);
if (!application) {
res.status(404).json({ error: 'Application not found' });
return;
}
logger.info(`Chat message for: ${application.name} using ${provider}`);
const response = await aiService.chat(application, message.trim(), conversationId, provider);
res.json(response);
} catch (error) {
logger.error('Failed to process chat message', error);
res.status(500).json({ error: 'Failed to process chat message' });
}
});
// Get conversation history
router.get('/chat/conversation/:conversationId', (req: Request, res: Response) => {
try {
const { conversationId } = req.params;
const messages = aiService.getConversationHistory(conversationId);
if (messages.length === 0) {
res.status(404).json({ error: 'Conversation not found' });
return;
}
res.json({ conversationId, messages });
} catch (error) {
logger.error('Failed to get conversation history', error);
res.status(500).json({ error: 'Failed to get conversation history' });
}
});
// Clear a conversation
router.delete('/chat/conversation/:conversationId', (req: Request, res: Response) => {
try {
const { conversationId } = req.params;
const deleted = aiService.clearConversation(conversationId);
if (!deleted) {
res.status(404).json({ error: 'Conversation not found' });
return;
}
res.json({ success: true });
} catch (error) {
logger.error('Failed to clear conversation', error);
res.status(500).json({ error: 'Failed to clear conversation' });
}
});
export default router;

View File

@@ -0,0 +1,121 @@
import { Router, Request, Response } from 'express';
import { readFile, writeFile } from 'fs/promises';
import { join } from 'path';
import { logger } from '../services/logger.js';
import { clearEffortCalculationConfigCache, getEffortCalculationConfigV25 } from '../services/effortCalculation.js';
import type { EffortCalculationConfig, EffortCalculationConfigV25 } from '../config/effortCalculation.js';
const router = Router();
// 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-v25.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' });
}
});
export default router;

View File

@@ -0,0 +1,79 @@
import { Router, Request, Response } from 'express';
import { dataService } from '../services/dataService.js';
import { databaseService } from '../services/database.js';
import { logger } from '../services/logger.js';
const router = Router();
// Simple in-memory cache for dashboard stats
interface CachedStats {
data: any;
timestamp: number;
}
let statsCache: CachedStats | null = null;
const CACHE_TTL_MS = 3 * 60 * 1000; // 3 minutes cache (longer since jiraAssets also caches)
// Get dashboard statistics
router.get('/stats', async (req: Request, res: Response) => {
try {
// Allow force refresh via query param
const forceRefresh = req.query.refresh === 'true';
// Check cache first (unless force refresh)
const now = Date.now();
if (!forceRefresh && statsCache && (now - statsCache.timestamp) < CACHE_TTL_MS) {
logger.debug('Returning cached dashboard stats');
return res.json(statsCache.data);
}
logger.info('Dashboard: Fetching fresh stats...');
// Default to true to include distributions, but allow disabling for performance
const includeDistributions = req.query.distributions !== 'false';
const stats = await dataService.getStats(includeDistributions);
const dbStats = databaseService.getStats();
const responseData = {
...stats,
classificationStats: dbStats,
};
// Update cache
statsCache = {
data: responseData,
timestamp: now,
};
logger.info('Dashboard: Stats fetched and cached successfully');
res.json(responseData);
} catch (error) {
logger.error('Failed to get dashboard stats', error);
// Return cached data if available (even if expired)
if (statsCache) {
logger.info('Dashboard: Returning stale cached data due to error');
return res.json({
...statsCache.data,
stale: true,
error: 'Using cached data due to API timeout',
});
}
res.status(500).json({ error: 'Failed to get dashboard stats' });
}
});
// Get recent classifications
router.get('/recent', (req: Request, res: Response) => {
try {
const limit = parseInt(req.query.limit as string) || 10;
const history = databaseService.getClassificationHistory(limit);
res.json(history);
} catch (error) {
logger.error('Failed to get recent classifications', error);
res.status(500).json({ error: 'Failed to get recent classifications' });
}
});
export default router;

View File

@@ -0,0 +1,203 @@
import { Router, Request, Response } from 'express';
import { dataService } from '../services/dataService.js';
import { logger } from '../services/logger.js';
const router = Router();
// Get all reference data
router.get('/', async (req: Request, res: Response) => {
try {
const [
dynamicsFactors,
complexityFactors,
numberOfUsers,
governanceModels,
organisations,
hostingTypes,
applicationFunctions,
applicationClusters,
applicationTypes,
businessImportance,
businessImpactAnalyses,
applicationManagementHosting,
applicationManagementTAM,
] = await Promise.all([
dataService.getDynamicsFactors(),
dataService.getComplexityFactors(),
dataService.getNumberOfUsers(),
dataService.getGovernanceModels(),
dataService.getOrganisations(),
dataService.getHostingTypes(),
dataService.getApplicationFunctions(),
dataService.getApplicationClusters(),
dataService.getApplicationTypes(),
dataService.getBusinessImportance(),
dataService.getBusinessImpactAnalyses(),
dataService.getApplicationManagementHosting(),
dataService.getApplicationManagementTAM(),
]);
res.json({
dynamicsFactors,
complexityFactors,
numberOfUsers,
governanceModels,
organisations,
hostingTypes,
applicationFunctions,
applicationClusters,
applicationTypes,
businessImportance,
businessImpactAnalyses,
applicationManagementHosting,
applicationManagementTAM,
});
} catch (error) {
logger.error('Failed to get reference data', error);
res.status(500).json({ error: 'Failed to get reference data' });
}
});
// Get dynamics factors
router.get('/dynamics-factors', async (req: Request, res: Response) => {
try {
const factors = await dataService.getDynamicsFactors();
res.json(factors);
} catch (error) {
logger.error('Failed to get dynamics factors', error);
res.status(500).json({ error: 'Failed to get dynamics factors' });
}
});
// Get complexity factors
router.get('/complexity-factors', async (req: Request, res: Response) => {
try {
const factors = await dataService.getComplexityFactors();
res.json(factors);
} catch (error) {
logger.error('Failed to get complexity factors', error);
res.status(500).json({ error: 'Failed to get complexity factors' });
}
});
// Get number of users options
router.get('/number-of-users', async (req: Request, res: Response) => {
try {
const options = await dataService.getNumberOfUsers();
res.json(options);
} catch (error) {
logger.error('Failed to get number of users', error);
res.status(500).json({ error: 'Failed to get number of users' });
}
});
// Get governance models
router.get('/governance-models', async (req: Request, res: Response) => {
try {
const models = await dataService.getGovernanceModels();
res.json(models);
} catch (error) {
logger.error('Failed to get governance models', error);
res.status(500).json({ error: 'Failed to get governance models' });
}
});
// Get organisations
router.get('/organisations', async (req: Request, res: Response) => {
try {
const orgs = await dataService.getOrganisations();
res.json(orgs);
} catch (error) {
logger.error('Failed to get organisations', error);
res.status(500).json({ error: 'Failed to get organisations' });
}
});
// Get hosting types
router.get('/hosting-types', async (req: Request, res: Response) => {
try {
const types = await dataService.getHostingTypes();
res.json(types);
} catch (error) {
logger.error('Failed to get hosting types', error);
res.status(500).json({ error: 'Failed to get hosting types' });
}
});
// Get application functions (from Jira Assets)
router.get('/application-functions', async (req: Request, res: Response) => {
try {
const functions = await dataService.getApplicationFunctions();
res.json(functions);
} catch (error) {
logger.error('Failed to get application functions', error);
res.status(500).json({ error: 'Failed to get application functions' });
}
});
// Get application clusters (from Jira Assets)
router.get('/application-clusters', async (req: Request, res: Response) => {
try {
const clusters = await dataService.getApplicationClusters();
res.json(clusters);
} catch (error) {
logger.error('Failed to get application clusters', error);
res.status(500).json({ error: 'Failed to get application clusters' });
}
});
// Get application types (from Jira Assets)
router.get('/application-types', async (req: Request, res: Response) => {
try {
const types = await dataService.getApplicationTypes();
res.json(types);
} catch (error) {
logger.error('Failed to get application types', error);
res.status(500).json({ error: 'Failed to get application types' });
}
});
router.get('/business-importance', async (req: Request, res: Response) => {
try {
const importance = await dataService.getBusinessImportance();
res.json(importance);
} catch (error) {
logger.error('Failed to get business importance', error);
res.status(500).json({ error: 'Failed to get business importance' });
}
});
// Get business impact analyses
router.get('/business-impact-analyses', async (req: Request, res: Response) => {
try {
const analyses = await dataService.getBusinessImpactAnalyses();
res.json(analyses);
} catch (error) {
logger.error('Failed to get business impact analyses', error);
res.status(500).json({ error: 'Failed to get business impact analyses' });
}
});
// Get application management hosting
router.get('/application-management-hosting', async (req: Request, res: Response) => {
try {
const hosting = await dataService.getApplicationManagementHosting();
res.json(hosting);
} catch (error) {
logger.error('Failed to get application management hosting', error);
res.status(500).json({ error: 'Failed to get application management hosting' });
}
});
// Get application management TAM
router.get('/application-management-tam', async (req: Request, res: Response) => {
try {
const tam = await dataService.getApplicationManagementTAM();
res.json(tam);
} catch (error) {
logger.error('Failed to get application management TAM', error);
res.status(500).json({ error: 'Failed to get application management TAM' });
}
});
export default router;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,207 @@
import { config } from '../config/env.js';
import { jiraAssetsService } from './jiraAssets.js';
import { mockDataService } from './mockData.js';
import { logger } from './logger.js';
import type {
ApplicationDetails,
ApplicationStatus,
ApplicationUpdateRequest,
ReferenceValue,
SearchFilters,
SearchResult,
TeamDashboardData,
} from '../types/index.js';
// Determine if we should use real Jira Assets or mock data
const useJiraAssets = !!(config.jiraPat && config.jiraSchemaId);
if (useJiraAssets) {
logger.info('Using Jira Assets API for data');
} else {
logger.info('Using mock data (Jira credentials not configured)');
}
export const dataService = {
async searchApplications(
filters: SearchFilters,
page: number = 1,
pageSize: number = 25
): Promise<SearchResult> {
if (useJiraAssets) {
return jiraAssetsService.searchApplications(filters, page, pageSize);
}
return mockDataService.searchApplications(filters, page, pageSize);
},
async getApplicationById(id: string): Promise<ApplicationDetails | null> {
if (useJiraAssets) {
return jiraAssetsService.getApplicationById(id);
}
return mockDataService.getApplicationById(id);
},
async updateApplication(
id: string,
updates: {
applicationFunctions?: ReferenceValue[];
dynamicsFactor?: ReferenceValue;
complexityFactor?: ReferenceValue;
numberOfUsers?: ReferenceValue;
governanceModel?: ReferenceValue;
applicationCluster?: ReferenceValue;
applicationType?: ReferenceValue;
hostingType?: ReferenceValue;
businessImpactAnalyse?: ReferenceValue;
overrideFTE?: number | null;
applicationManagementHosting?: string;
applicationManagementTAM?: string;
}
): Promise<boolean> {
logger.info(`dataService.updateApplication called for ${id}`);
logger.info(`Updates from frontend: ${JSON.stringify(updates)}`);
if (useJiraAssets) {
// Convert ReferenceValues to keys for Jira update
const jiraUpdates: ApplicationUpdateRequest = {
applicationFunctions: updates.applicationFunctions?.map((f) => f.key),
dynamicsFactor: updates.dynamicsFactor?.key,
complexityFactor: updates.complexityFactor?.key,
numberOfUsers: updates.numberOfUsers?.key,
governanceModel: updates.governanceModel?.key,
applicationCluster: updates.applicationCluster?.key,
applicationType: updates.applicationType?.key,
hostingType: updates.hostingType?.key,
businessImpactAnalyse: updates.businessImpactAnalyse?.key,
overrideFTE: updates.overrideFTE,
applicationManagementHosting: updates.applicationManagementHosting,
applicationManagementTAM: updates.applicationManagementTAM,
};
logger.info(`Converted to Jira format: ${JSON.stringify(jiraUpdates)}`);
return jiraAssetsService.updateApplication(id, jiraUpdates);
}
return mockDataService.updateApplication(id, updates);
},
async getDynamicsFactors(): Promise<ReferenceValue[]> {
if (useJiraAssets) {
return jiraAssetsService.getDynamicsFactors();
}
return mockDataService.getDynamicsFactors();
},
async getComplexityFactors(): Promise<ReferenceValue[]> {
if (useJiraAssets) {
return jiraAssetsService.getComplexityFactors();
}
return mockDataService.getComplexityFactors();
},
async getNumberOfUsers(): Promise<ReferenceValue[]> {
if (useJiraAssets) {
return jiraAssetsService.getNumberOfUsers();
}
return mockDataService.getNumberOfUsers();
},
async getGovernanceModels(): Promise<ReferenceValue[]> {
if (useJiraAssets) {
return jiraAssetsService.getGovernanceModels();
}
return mockDataService.getGovernanceModels();
},
async getOrganisations(): Promise<ReferenceValue[]> {
if (useJiraAssets) {
return jiraAssetsService.getOrganisations();
}
return mockDataService.getOrganisations();
},
async getHostingTypes(): Promise<ReferenceValue[]> {
if (useJiraAssets) {
return jiraAssetsService.getHostingTypes();
}
return mockDataService.getHostingTypes();
},
async getBusinessImpactAnalyses(): Promise<ReferenceValue[]> {
if (useJiraAssets) {
return jiraAssetsService.getBusinessImpactAnalyses();
}
return mockDataService.getBusinessImpactAnalyses();
},
async getApplicationManagementHosting(): Promise<ReferenceValue[]> {
if (useJiraAssets) {
return jiraAssetsService.getApplicationManagementHosting();
}
return mockDataService.getApplicationManagementHosting();
},
async getApplicationManagementTAM(): Promise<ReferenceValue[]> {
if (useJiraAssets) {
return jiraAssetsService.getApplicationManagementTAM();
}
return mockDataService.getApplicationManagementTAM();
},
async getStats(includeDistributions: boolean = true) {
if (useJiraAssets) {
return jiraAssetsService.getStats(includeDistributions);
}
return mockDataService.getStats();
},
async getApplicationFunctions(): Promise<ReferenceValue[]> {
if (useJiraAssets) {
return jiraAssetsService.getApplicationFunctions();
}
return mockDataService.getApplicationFunctions();
},
async getApplicationFunctionCategories(): Promise<ReferenceValue[]> {
if (useJiraAssets) {
return jiraAssetsService.getApplicationFunctionCategories();
}
return mockDataService.getApplicationFunctionCategories();
},
async getApplicationClusters(): Promise<ReferenceValue[]> {
if (useJiraAssets) {
return jiraAssetsService.getApplicationClusters();
}
return mockDataService.getApplicationClusters();
},
async getApplicationTypes(): Promise<ReferenceValue[]> {
if (useJiraAssets) {
return jiraAssetsService.getApplicationTypes();
}
return mockDataService.getApplicationTypes();
},
async getBusinessImportance(): Promise<ReferenceValue[]> {
if (useJiraAssets) {
return jiraAssetsService.getBusinessImportance();
}
return mockDataService.getBusinessImportance();
},
isUsingJiraAssets(): boolean {
return useJiraAssets;
},
async testConnection(): Promise<boolean> {
if (useJiraAssets) {
return jiraAssetsService.testConnection();
}
return true;
},
async getTeamDashboardData(excludedStatuses: ApplicationStatus[] = []): Promise<TeamDashboardData> {
if (useJiraAssets) {
return jiraAssetsService.getTeamDashboardData(excludedStatuses);
}
return mockDataService.getTeamDashboardData(excludedStatuses);
},
};

View File

@@ -0,0 +1,154 @@
import Database from 'better-sqlite3';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { logger } from './logger.js';
import type { ClassificationResult } from '../types/index.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const DB_PATH = join(__dirname, '../../data/classifications.db');
class DatabaseService {
private db: Database.Database;
constructor() {
this.db = new Database(DB_PATH);
this.initialize();
}
private initialize(): void {
// Create tables if they don't exist
this.db.exec(`
CREATE TABLE IF NOT EXISTS classification_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
application_id TEXT NOT NULL,
application_name TEXT NOT NULL,
changes TEXT NOT NULL,
source TEXT NOT NULL,
timestamp TEXT NOT NULL,
user_id TEXT
);
CREATE TABLE IF NOT EXISTS session_state (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_classification_app_id ON classification_history(application_id);
CREATE INDEX IF NOT EXISTS idx_classification_timestamp ON classification_history(timestamp);
`);
logger.info('Database initialized');
}
saveClassificationResult(result: ClassificationResult): void {
const stmt = this.db.prepare(`
INSERT INTO classification_history (application_id, application_name, changes, source, timestamp, user_id)
VALUES (?, ?, ?, ?, ?, ?)
`);
stmt.run(
result.applicationId,
result.applicationName,
JSON.stringify(result.changes),
result.source,
result.timestamp.toISOString(),
result.userId || null
);
}
getClassificationHistory(limit: number = 50): ClassificationResult[] {
const stmt = this.db.prepare(`
SELECT * FROM classification_history
ORDER BY timestamp DESC
LIMIT ?
`);
const rows = stmt.all(limit) as any[];
return rows.map((row) => ({
applicationId: row.application_id,
applicationName: row.application_name,
changes: JSON.parse(row.changes),
source: row.source,
timestamp: new Date(row.timestamp),
userId: row.user_id,
}));
}
getClassificationsByApplicationId(applicationId: string): ClassificationResult[] {
const stmt = this.db.prepare(`
SELECT * FROM classification_history
WHERE application_id = ?
ORDER BY timestamp DESC
`);
const rows = stmt.all(applicationId) as any[];
return rows.map((row) => ({
applicationId: row.application_id,
applicationName: row.application_name,
changes: JSON.parse(row.changes),
source: row.source,
timestamp: new Date(row.timestamp),
userId: row.user_id,
}));
}
saveSessionState(key: string, value: any): void {
const stmt = this.db.prepare(`
INSERT INTO session_state (key, value, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = ?
`);
const now = new Date().toISOString();
const valueStr = JSON.stringify(value);
stmt.run(key, valueStr, now, valueStr, now);
}
getSessionState<T>(key: string): T | null {
const stmt = this.db.prepare(`
SELECT value FROM session_state WHERE key = ?
`);
const row = stmt.get(key) as { value: string } | undefined;
if (row) {
return JSON.parse(row.value) as T;
}
return null;
}
clearSessionState(key: string): void {
const stmt = this.db.prepare(`
DELETE FROM session_state WHERE key = ?
`);
stmt.run(key);
}
getStats(): { totalClassifications: number; bySource: Record<string, number> } {
const totalStmt = this.db.prepare(`
SELECT COUNT(*) as count FROM classification_history
`);
const total = (totalStmt.get() as { count: number }).count;
const bySourceStmt = this.db.prepare(`
SELECT source, COUNT(*) as count FROM classification_history GROUP BY source
`);
const bySourceRows = bySourceStmt.all() as { source: string; count: number }[];
const bySource: Record<string, number> = {};
bySourceRows.forEach((row) => {
bySource[row.source] = row.count;
});
return { totalClassifications: total, bySource };
}
close(): void {
this.db.close();
}
}
export const databaseService = new DatabaseService();

View File

@@ -0,0 +1,577 @@
import {
EFFORT_CALCULATION_CONFIG_V25,
EffortCalculationConfigV25,
FTERange,
GovernanceModelConfig,
ApplicationTypeConfig,
BIALevelConfig,
HostingRule,
} from '../config/effortCalculation.js';
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { logger } from './logger.js';
import type { ApplicationDetails, EffortCalculationBreakdown } from '../types/index.js';
// Path to the configuration file (v25)
const CONFIG_FILE_PATH_V25 = join(__dirname, '../../data/effort-calculation-config-v25.json');
// Cache for loaded configuration
let cachedConfigV25: EffortCalculationConfigV25 | null = null;
// FTE to hours constants
const HOURS_PER_WEEK = 36;
const NET_WORK_WEEKS = 46;
const DECLARABLE_PERCENTAGE = 0.75;
/**
* Load effort calculation configuration v25 from file or use default (synchronous)
*/
function loadEffortCalculationConfigV25(): EffortCalculationConfigV25 {
if (cachedConfigV25) {
return cachedConfigV25;
}
try {
if (existsSync(CONFIG_FILE_PATH_V25)) {
const fileContent = readFileSync(CONFIG_FILE_PATH_V25, 'utf-8');
cachedConfigV25 = JSON.parse(fileContent) as EffortCalculationConfigV25;
logger.info('Loaded effort calculation configuration v25 from file');
return cachedConfigV25;
} else {
logger.info('Configuration file v25 not found, using default from code');
}
} catch (error) {
logger.warn('Failed to load configuration file v25, using default from code', error);
}
// Use default config
cachedConfigV25 = EFFORT_CALCULATION_CONFIG_V25;
return cachedConfigV25;
}
/**
* Clear the configuration cache (call after updating config)
*/
export function clearEffortCalculationConfigCache(): void {
cachedConfigV25 = null;
}
/**
* Get the current configuration
*/
export function getEffortCalculationConfigV25(): EffortCalculationConfigV25 {
return loadEffortCalculationConfigV25();
}
/**
* Extract BIA class letter from various formats
* Handles: "BIA-2024-0042 (Klasse E)", "E", "Klasse E", etc.
*/
function extractBIAClass(value: string | null): string | null {
if (!value) return null;
// Try to match "(Klasse X)" format
const klasseMatch = value.match(/\(Klasse\s+([A-F])\)/i);
if (klasseMatch) return klasseMatch[1].toUpperCase();
// Try to match "Klasse X" format
const klasseMatch2 = value.match(/Klasse\s+([A-F])/i);
if (klasseMatch2) return klasseMatch2[1].toUpperCase();
// Try single letter
const singleMatch = value.trim().match(/^([A-F])$/i);
if (singleMatch) return singleMatch[1].toUpperCase();
return null;
}
/**
* Extract regiemodel code from name
* Handles: "Regiemodel A", "Model A", "A", etc.
*/
function extractRegieModelCode(value: string | null): string | null {
if (!value) return null;
// Try to match "Regiemodel X" or "Model X" format
const modelMatch = value.match(/(?:Regiemodel|Model)\s+([A-E]\+?)/i);
if (modelMatch) return modelMatch[1].toUpperCase();
// Try single letter/code
const singleMatch = value.trim().match(/^([A-E]\+?)$/i);
if (singleMatch) return singleMatch[1].toUpperCase();
return null;
}
/**
* Calculate average FTE from min/max range
*/
function calculateAverageFTE(range: FTERange): number {
return (range.min + range.max) / 2;
}
/**
* Convert FTE to hours per year (declarable hours)
*/
function calculateHoursPerYear(fte: number): number {
return HOURS_PER_WEEK * NET_WORK_WEEKS * fte * DECLARABLE_PERCENTAGE;
}
/**
* Find matching hosting rule for a given hosting value
*/
function findMatchingHostingRule(
hosting: { [key: string]: HostingRule },
hostingValue: string | null
): { rule: HostingRule | null; ruleKey: string | null; usedDefault: boolean } {
if (!hostingValue) {
// No hosting value - look for _all or use first available
if (hosting['_all']) {
return { rule: hosting['_all'], ruleKey: '_all', usedDefault: true };
}
// Use first available as default
const keys = Object.keys(hosting);
if (keys.length > 0) {
return { rule: hosting[keys[0]], ruleKey: keys[0], usedDefault: true };
}
return { rule: null, ruleKey: null, usedDefault: true };
}
// Search for a rule that contains the hosting value
for (const [key, rule] of Object.entries(hosting)) {
if (rule.hostingValues.some(hv =>
hv.toLowerCase() === hostingValue.toLowerCase() ||
hostingValue.toLowerCase().includes(hv.toLowerCase()) ||
hv.toLowerCase().includes(hostingValue.toLowerCase())
)) {
return { rule, ruleKey: key, usedDefault: false };
}
}
// Fall back to _all if exists
if (hosting['_all']) {
return { rule: hosting['_all'], ruleKey: '_all', usedDefault: true };
}
// Use first available as default
const keys = Object.keys(hosting);
if (keys.length > 0) {
return { rule: hosting[keys[0]], ruleKey: keys[0], usedDefault: true };
}
return { rule: null, ruleKey: null, usedDefault: true };
}
/**
* Validate BIA against regiemodel constraints
*/
function validateBIAForRegieModel(
regieModelCode: string,
biaClass: string | null,
config: EffortCalculationConfigV25
): { isValid: boolean; warning: string | null } {
if (!biaClass) {
return { isValid: true, warning: null };
}
const allowedBia = config.validationRules.biaRegieModelConstraints[regieModelCode];
if (!allowedBia) {
return { isValid: true, warning: null };
}
if (!allowedBia.includes(biaClass)) {
const errorMessages: Record<string, string> = {
'A': `BIA ${biaClass} te laag voor Regiemodel A. Minimaal BIA D vereist.`,
'B': `BIA ${biaClass} niet toegestaan voor Regiemodel B. Toegestaan: C, D, E.`,
'B+': `BIA ${biaClass} niet toegestaan voor Regiemodel B+. Toegestaan: C, D, E.`,
'C': `BIA ${biaClass} te laag voor Regiemodel C. Minimaal BIA C vereist.`,
'D': `BIA ${biaClass} te hoog voor Regiemodel D. Maximaal BIA C toegestaan.`,
'E': `BIA ${biaClass} te hoog voor Regiemodel E. Maximaal BIA B toegestaan.`,
};
return {
isValid: false,
warning: errorMessages[regieModelCode] || `BIA ${biaClass} niet toegestaan voor Regiemodel ${regieModelCode}. Toegestaan: ${allowedBia.join(', ')}.`
};
}
return { isValid: true, warning: null };
}
/**
* Check for platform restrictions
*/
function checkPlatformRestrictions(
regieModelCode: string,
applicationType: string | null,
config: EffortCalculationConfigV25
): string | null {
if (!applicationType) return null;
const restriction = config.validationRules.platformRestrictions.find(
r => r.regiemodel === regieModelCode &&
r.applicationType.toLowerCase() === applicationType.toLowerCase()
);
return restriction ? restriction.warning : null;
}
/**
* Calculate Required Effort Application Management with full breakdown (v25)
*/
export function calculateRequiredEffortApplicationManagementV25(
application: ApplicationDetails
): {
finalEffort: number | null;
breakdown: EffortCalculationBreakdown;
} {
const config = loadEffortCalculationConfigV25();
// Initialize breakdown
const breakdown: EffortCalculationBreakdown = {
baseEffort: 0,
baseEffortMin: 0,
baseEffortMax: 0,
governanceModel: null,
governanceModelName: null,
applicationType: null,
businessImpactAnalyse: null,
applicationManagementHosting: null,
numberOfUsersFactor: { value: 1.0, name: null },
dynamicsFactor: { value: 1.0, name: null },
complexityFactor: { value: 1.0, name: null },
usedDefaults: [],
warnings: [],
errors: [],
requiresManualAssessment: false,
isFixedFte: false,
notRecommended: false,
hoursPerYear: 0,
hoursPerMonth: 0,
hoursPerWeek: 0,
};
try {
// Extract values from application
const governanceModelRaw = application.governanceModel?.name || null;
const regieModelCode = extractRegieModelCode(governanceModelRaw);
const applicationType = application.applicationType?.name || null;
const businessImpactAnalyseRaw = typeof application.businessImpactAnalyse === 'string'
? application.businessImpactAnalyse
: application.businessImpactAnalyse?.name || null;
const biaClass = extractBIAClass(businessImpactAnalyseRaw);
const applicationManagementHosting = typeof application.applicationManagementHosting === 'string'
? application.applicationManagementHosting
: application.applicationManagementHosting?.name || null;
// Store extracted values in breakdown
breakdown.governanceModel = regieModelCode;
breakdown.governanceModelName = governanceModelRaw;
breakdown.applicationType = applicationType;
breakdown.businessImpactAnalyse = biaClass;
breakdown.applicationManagementHosting = applicationManagementHosting;
logger.debug(`=== Effort Calculation v25 ===`);
logger.debug(`Regiemodel: ${regieModelCode} (${governanceModelRaw})`);
logger.debug(`Application Type: ${applicationType}`);
logger.debug(`BIA: ${biaClass} (${businessImpactAnalyseRaw})`);
logger.debug(`Hosting: ${applicationManagementHosting}`);
// Level 1: Find Regiemodel configuration
if (!regieModelCode || !config.regiemodellen[regieModelCode]) {
breakdown.errors.push(`Geen configuratie gevonden voor regiemodel: ${governanceModelRaw || 'niet ingesteld'}`);
breakdown.usedDefaults.push('regiemodel');
return { finalEffort: null, breakdown };
}
const regieModelConfig = config.regiemodellen[regieModelCode];
breakdown.governanceModelName = regieModelConfig.name;
// Validate BIA against regiemodel
const biaValidation = validateBIAForRegieModel(regieModelCode, biaClass, config);
if (!biaValidation.isValid && biaValidation.warning) {
breakdown.errors.push(biaValidation.warning);
}
// Level 2: Find Application Type configuration
let appTypeConfig: ApplicationTypeConfig | null = null;
let usedAppTypeDefault = false;
if (applicationType && regieModelConfig.applicationTypes[applicationType]) {
appTypeConfig = regieModelConfig.applicationTypes[applicationType];
} else {
// Use default from regiemodel
usedAppTypeDefault = true;
breakdown.usedDefaults.push('applicationType');
// Try to find a default application type or use regiemodel default
const appTypes = Object.keys(regieModelConfig.applicationTypes);
if (appTypes.includes('Applicatie')) {
appTypeConfig = regieModelConfig.applicationTypes['Applicatie'];
} else if (appTypes.length > 0) {
appTypeConfig = regieModelConfig.applicationTypes[appTypes[0]];
}
if (!appTypeConfig) {
// Use regiemodel default FTE
breakdown.baseEffortMin = regieModelConfig.defaultFte.min;
breakdown.baseEffortMax = regieModelConfig.defaultFte.max;
breakdown.baseEffort = calculateAverageFTE(regieModelConfig.defaultFte);
breakdown.warnings.push(`Geen specifieke configuratie voor applicatietype: ${applicationType || 'niet ingesteld'}. Default regiemodel waarde gebruikt.`);
}
}
// Check for special flags
if (appTypeConfig) {
if (appTypeConfig.requiresManualAssessment) {
breakdown.requiresManualAssessment = true;
breakdown.warnings.push('⚠️ Handmatige beoordeling vereist - zie Beheer Readiness Checklist sectie J');
}
if (appTypeConfig.fixedFte) {
breakdown.isFixedFte = true;
breakdown.warnings.push(` Vaste FTE waarde voor dit regiemodel (alleen CMDB + review)`);
}
if (appTypeConfig.notRecommended) {
breakdown.notRecommended = true;
const restriction = checkPlatformRestrictions(regieModelCode, applicationType, config);
if (restriction) {
breakdown.warnings.push(`⚠️ ${restriction}`);
}
}
}
// Level 3: Find BIA configuration
let biaConfig: BIALevelConfig | null = null;
let usedBiaDefault = false;
if (appTypeConfig) {
if (biaClass && appTypeConfig.biaLevels[biaClass]) {
biaConfig = appTypeConfig.biaLevels[biaClass];
} else if (appTypeConfig.biaLevels['_all']) {
biaConfig = appTypeConfig.biaLevels['_all'];
usedBiaDefault = true;
} else {
// Use application type default
usedBiaDefault = true;
breakdown.usedDefaults.push('businessImpact');
if (appTypeConfig.defaultFte) {
breakdown.baseEffortMin = appTypeConfig.defaultFte.min;
breakdown.baseEffortMax = appTypeConfig.defaultFte.max;
breakdown.baseEffort = calculateAverageFTE(appTypeConfig.defaultFte);
breakdown.warnings.push(`Geen specifieke configuratie voor BIA ${biaClass || 'niet ingesteld'}. Default applicatietype waarde gebruikt.`);
}
}
}
// Level 4: Find Hosting configuration
if (biaConfig) {
const hostingResult = findMatchingHostingRule(biaConfig.hosting, applicationManagementHosting);
if (hostingResult.rule) {
breakdown.baseEffortMin = hostingResult.rule.fte.min;
breakdown.baseEffortMax = hostingResult.rule.fte.max;
breakdown.baseEffort = calculateAverageFTE(hostingResult.rule.fte);
if (hostingResult.usedDefault) {
breakdown.usedDefaults.push('hosting');
breakdown.warnings.push(`Geen specifieke configuratie voor hosting: ${applicationManagementHosting || 'niet ingesteld'}. Default waarde gebruikt.`);
}
} else if (biaConfig.defaultFte) {
breakdown.baseEffortMin = biaConfig.defaultFte.min;
breakdown.baseEffortMax = biaConfig.defaultFte.max;
breakdown.baseEffort = calculateAverageFTE(biaConfig.defaultFte);
breakdown.usedDefaults.push('hosting');
}
}
// Get factors
breakdown.numberOfUsersFactor = {
value: application.numberOfUsers?.factor ?? 1.0,
name: application.numberOfUsers?.name || null,
};
breakdown.dynamicsFactor = {
value: application.dynamicsFactor?.factor ?? 1.0,
name: application.dynamicsFactor?.name || null,
};
breakdown.complexityFactor = {
value: application.complexityFactor?.factor ?? 1.0,
name: application.complexityFactor?.name || null,
};
// Calculate final effort
const finalEffort = breakdown.baseEffort *
breakdown.numberOfUsersFactor.value *
breakdown.dynamicsFactor.value *
breakdown.complexityFactor.value;
// Calculate hours
breakdown.hoursPerYear = calculateHoursPerYear(finalEffort);
breakdown.hoursPerMonth = breakdown.hoursPerYear / 12;
breakdown.hoursPerWeek = breakdown.hoursPerYear / NET_WORK_WEEKS;
logger.debug(`Base FTE: ${breakdown.baseEffort} (${breakdown.baseEffortMin} - ${breakdown.baseEffortMax})`);
logger.debug(`Final FTE: ${finalEffort}`);
logger.debug(`Hours/year: ${breakdown.hoursPerYear}`);
return { finalEffort, breakdown };
} catch (error) {
logger.error('Error calculating required effort application management v25', error);
breakdown.errors.push('Er is een fout opgetreden bij de berekening');
return { finalEffort: null, breakdown };
}
}
/**
* Calculate Required Effort Application Management based on application details
* Main entry point - uses v25 configuration
*/
export function calculateRequiredEffortApplicationManagement(
application: ApplicationDetails
): number | null {
const result = calculateRequiredEffortApplicationManagementV25(application);
return result.finalEffort;
}
/**
* Calculate Required Effort Application Management with breakdown
* Returns both the final value and the detailed breakdown
*/
export function calculateRequiredEffortApplicationManagementWithBreakdown(
application: ApplicationDetails
): {
baseEffort: number | null;
numberOfUsersFactor: number;
dynamicsFactor: number;
complexityFactor: number;
finalEffort: number | null;
breakdown?: EffortCalculationBreakdown;
} {
const result = calculateRequiredEffortApplicationManagementV25(application);
return {
baseEffort: result.breakdown.baseEffort || null,
numberOfUsersFactor: result.breakdown.numberOfUsersFactor.value,
dynamicsFactor: result.breakdown.dynamicsFactor.value,
complexityFactor: result.breakdown.complexityFactor.value,
finalEffort: result.finalEffort,
breakdown: result.breakdown,
};
}
/**
* Calculate Required Effort with min/max FTE values
* Returns the final effort plus the min and max based on configuration ranges
*/
export function calculateRequiredEffortWithMinMax(
application: ApplicationDetails
): {
finalEffort: number | null;
minFTE: number | null;
maxFTE: number | null;
} {
const result = calculateRequiredEffortApplicationManagementV25(application);
const breakdown = result.breakdown;
if (breakdown.baseEffortMin === 0 && breakdown.baseEffortMax === 0) {
return {
finalEffort: result.finalEffort,
minFTE: null,
maxFTE: null,
};
}
// Apply the same factors to min and max
const factorMultiplier =
breakdown.numberOfUsersFactor.value *
breakdown.dynamicsFactor.value *
breakdown.complexityFactor.value;
const minFTE = breakdown.baseEffortMin * factorMultiplier;
const maxFTE = breakdown.baseEffortMax * factorMultiplier;
return {
finalEffort: result.finalEffort,
minFTE: Math.round(minFTE * 100) / 100, // Round to 2 decimals
maxFTE: Math.round(maxFTE * 100) / 100,
};
}
/**
* Get the base effort for a given application (for real-time calculation without saving)
* This is a simplified version that returns just the base FTE
*/
export function calculateRequiredEffortApplicationManagementBase(
application: ApplicationDetails
): number | null {
const result = calculateRequiredEffortApplicationManagementV25(application);
return result.breakdown.baseEffort || null;
}
/**
* Get full breakdown including hours calculation
*/
export function getEffortCalculationBreakdown(
application: ApplicationDetails
): EffortCalculationBreakdown {
const result = calculateRequiredEffortApplicationManagementV25(application);
return result.breakdown;
}
/**
* Validate an application's configuration
* Returns warnings and errors without calculating FTE
*/
export function validateApplicationConfiguration(
application: ApplicationDetails
): {
isValid: boolean;
warnings: string[];
errors: string[];
} {
const result = calculateRequiredEffortApplicationManagementV25(application);
return {
isValid: result.breakdown.errors.length === 0,
warnings: result.breakdown.warnings,
errors: result.breakdown.errors,
};
}
/**
* Get all available regiemodellen from configuration
*/
export function getAvailableRegieModellen(): Array<{
code: string;
name: string;
description?: string;
allowedBia: string[];
}> {
const config = loadEffortCalculationConfigV25();
return Object.entries(config.regiemodellen).map(([code, model]) => ({
code,
name: model.name,
description: model.description,
allowedBia: model.allowedBia,
}));
}
/**
* Get all available application types for a regiemodel
*/
export function getApplicationTypesForRegieModel(regieModelCode: string): string[] {
const config = loadEffortCalculationConfigV25();
const regieModel = config.regiemodellen[regieModelCode];
if (!regieModel) return [];
return Object.keys(regieModel.applicationTypes);
}
/**
* Get allowed BIA levels for a regiemodel
*/
export function getAllowedBIAForRegieModel(regieModelCode: string): string[] {
const config = loadEffortCalculationConfigV25();
return config.validationRules.biaRegieModelConstraints[regieModelCode] || [];
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
import winston from 'winston';
import { config } from '../config/env.js';
const { combine, timestamp, printf, colorize, errors } = winston.format;
const logFormat = printf(({ level, message, timestamp, stack }) => {
return `${timestamp} [${level}]: ${stack || message}`;
});
export const logger = winston.createLogger({
level: config.isDevelopment ? 'debug' : 'info',
format: combine(
errors({ stack: true }),
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
logFormat
),
transports: [
new winston.transports.Console({
format: combine(
colorize(),
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
logFormat
),
}),
],
});
if (config.isProduction) {
logger.add(
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
})
);
logger.add(
new winston.transports.File({
filename: 'logs/combined.log',
})
);
}

View File

@@ -0,0 +1,859 @@
import { calculateRequiredEffortApplicationManagement } from './effortCalculation.js';
import type {
ApplicationDetails,
ApplicationListItem,
ReferenceValue,
SearchFilters,
SearchResult,
ClassificationResult,
TeamDashboardData,
ApplicationStatus,
} from '../types/index.js';
// Mock application data for development/demo
const mockApplications: ApplicationDetails[] = [
{
id: '1',
key: 'APP-001',
name: 'Epic Hyperspace',
searchReference: 'EPIC-HS',
description: 'Elektronisch Patiëntendossier module voor klinische documentatie en workflow. Ondersteunt de volledige patiëntenzorg van intake tot ontslag.',
supplierProduct: 'Epic Systems / Hyperspace',
organisation: 'Zorg',
hostingType: { objectId: '1', key: 'HOST-1', name: 'On-premises' },
status: 'In Production',
businessImportance: 'Kritiek',
businessImpactAnalyse: { objectId: '1', key: 'BIA-1', name: 'BIA-2024-0042 (Klasse E)' },
systemOwner: 'J. Janssen',
businessOwner: 'Dr. A. van der Berg',
functionalApplicationManagement: 'Team EPD',
technicalApplicationManagement: 'Team Zorgapplicaties',
technicalApplicationManagementPrimary: 'Jan Jansen',
technicalApplicationManagementSecondary: 'Piet Pietersen',
medischeTechniek: false,
applicationFunctions: [],
dynamicsFactor: { objectId: '3', key: 'DYN-3', name: '3 - Hoog' },
complexityFactor: { objectId: '4', key: 'CMP-4', name: '4 - Zeer hoog' },
numberOfUsers: null,
governanceModel: { objectId: 'A', key: 'GOV-A', name: 'Centraal Beheer' },
applicationCluster: null,
applicationType: null,
platform: null,
requiredEffortApplicationManagement: null,
},
{
id: '2',
key: 'APP-002',
name: 'SAP Finance',
searchReference: 'SAP-FIN',
description: 'Enterprise Resource Planning systeem voor financiële administratie, budgettering en controlling.',
supplierProduct: 'SAP SE / SAP S/4HANA',
organisation: 'Bedrijfsvoering',
hostingType: { objectId: '3', key: 'HOST-3', name: 'Cloud' },
status: 'In Production',
businessImportance: 'Kritiek',
businessImpactAnalyse: { objectId: '2', key: 'BIA-2', name: 'BIA-2024-0015 (Klasse D)' },
systemOwner: 'M. de Groot',
businessOwner: 'P. Bakker',
functionalApplicationManagement: 'Team ERP',
technicalApplicationManagement: 'Team Bedrijfsapplicaties',
medischeTechniek: false,
applicationFunctions: [],
dynamicsFactor: { objectId: '2', key: 'DYN-2', name: '2 - Gemiddeld' },
complexityFactor: { objectId: '3', key: 'CMP-3', name: '3 - Hoog' },
numberOfUsers: null,
governanceModel: null,
applicationCluster: null,
applicationType: null,
platform: null,
requiredEffortApplicationManagement: null,
},
{
id: '3',
key: 'APP-003',
name: 'Philips IntelliSpace PACS',
searchReference: 'PACS',
description: 'Picture Archiving and Communication System voor opslag en weergave van medische beelden inclusief radiologie, CT en MRI.',
supplierProduct: 'Philips Healthcare / IntelliSpace PACS',
organisation: 'Zorg',
hostingType: { objectId: '1', key: 'HOST-1', name: 'On-premises' },
status: 'In Production',
businessImportance: 'Hoog',
businessImpactAnalyse: { objectId: '3', key: 'BIA-3', name: 'BIA-2024-0028 (Klasse D)' },
systemOwner: 'R. Hermans',
businessOwner: 'Dr. K. Smit',
functionalApplicationManagement: 'Team Beeldvorming',
technicalApplicationManagement: 'Team Zorgapplicaties',
medischeTechniek: true,
applicationFunctions: [],
dynamicsFactor: null,
complexityFactor: null,
numberOfUsers: null,
governanceModel: { objectId: 'C', key: 'GOV-C', name: 'Uitbesteed met ICMT-Regie' },
applicationCluster: null,
applicationType: null,
platform: null,
requiredEffortApplicationManagement: null,
},
{
id: '4',
key: 'APP-004',
name: 'ChipSoft HiX',
searchReference: 'HIX',
description: 'Ziekenhuisinformatiesysteem en EPD voor patiëntregistratie, zorgplanning en klinische workflow.',
supplierProduct: 'ChipSoft / HiX',
organisation: 'Zorg',
hostingType: { objectId: '1', key: 'HOST-1', name: 'On-premises' },
status: 'In Production',
businessImportance: 'Kritiek',
businessImpactAnalyse: { objectId: '5', key: 'BIA-5', name: 'BIA-2024-0001 (Klasse F)' },
systemOwner: 'T. van Dijk',
businessOwner: 'Dr. L. Mulder',
functionalApplicationManagement: 'Team ZIS',
technicalApplicationManagement: 'Team Zorgapplicaties',
medischeTechniek: false,
applicationFunctions: [],
dynamicsFactor: { objectId: '4', key: 'DYN-4', name: '4 - Zeer hoog' },
complexityFactor: { objectId: '4', key: 'CMP-4', name: '4 - Zeer hoog' },
numberOfUsers: null,
governanceModel: { objectId: 'A', key: 'GOV-A', name: 'Centraal Beheer' },
applicationCluster: null,
applicationType: null,
platform: null,
requiredEffortApplicationManagement: null,
},
{
id: '5',
key: 'APP-005',
name: 'TOPdesk',
searchReference: 'TOPDESK',
description: 'IT Service Management platform voor incident, problem en change management.',
supplierProduct: 'TOPdesk / TOPdesk Enterprise',
organisation: 'ICMT',
hostingType: { objectId: '2', key: 'HOST-2', name: 'SaaS' },
status: 'In Production',
businessImportance: 'Hoog',
businessImpactAnalyse: { objectId: '6', key: 'BIA-6', name: 'BIA-2024-0055 (Klasse C)' },
systemOwner: 'B. Willems',
businessOwner: 'H. Claessen',
functionalApplicationManagement: 'Team Servicedesk',
technicalApplicationManagement: 'Team ICT Beheer',
medischeTechniek: false,
applicationFunctions: [],
dynamicsFactor: { objectId: '2', key: 'DYN-2', name: '2 - Gemiddeld' },
complexityFactor: { objectId: '2', key: 'CMP-2', name: '2 - Gemiddeld' },
numberOfUsers: null,
governanceModel: null,
applicationCluster: null,
applicationType: null,
platform: null,
requiredEffortApplicationManagement: null,
},
{
id: '6',
key: 'APP-006',
name: 'Microsoft 365',
searchReference: 'M365',
description: 'Kantoorautomatisering suite met Teams, Outlook, SharePoint, OneDrive en Office applicaties.',
supplierProduct: 'Microsoft / Microsoft 365 E5',
organisation: 'ICMT',
hostingType: { objectId: '2', key: 'HOST-2', name: 'SaaS' },
status: 'In Production',
businessImportance: 'Kritiek',
businessImpactAnalyse: { objectId: '1', key: 'BIA-1', name: 'BIA-2024-0042 (Klasse E)' },
systemOwner: 'S. Jansen',
businessOwner: 'N. Peters',
functionalApplicationManagement: 'Team Werkplek',
technicalApplicationManagement: 'Team Cloud',
medischeTechniek: false,
applicationFunctions: [],
dynamicsFactor: { objectId: '3', key: 'DYN-3', name: '3 - Hoog' },
complexityFactor: { objectId: '3', key: 'CMP-3', name: '3 - Hoog' },
numberOfUsers: { objectId: '7', key: 'USR-7', name: '> 15.000' },
governanceModel: { objectId: 'A', key: 'GOV-A', name: 'Centraal Beheer' },
applicationCluster: null,
applicationType: null,
platform: null,
requiredEffortApplicationManagement: null,
},
{
id: '7',
key: 'APP-007',
name: 'Carestream Vue PACS',
searchReference: 'VUE-PACS',
description: 'Enterprise imaging platform voor radiologie en cardiologie beeldvorming.',
supplierProduct: 'Carestream Health / Vue PACS',
organisation: 'Zorg',
hostingType: { objectId: '1', key: 'HOST-1', name: 'On-premises' },
status: 'End of life',
businessImportance: 'Gemiddeld',
businessImpactAnalyse: { objectId: '9', key: 'BIA-9', name: 'BIA-2022-0089 (Klasse C)' },
systemOwner: 'R. Hermans',
businessOwner: 'Dr. K. Smit',
functionalApplicationManagement: 'Team Beeldvorming',
technicalApplicationManagement: 'Team Zorgapplicaties',
medischeTechniek: true,
applicationFunctions: [],
dynamicsFactor: { objectId: '1', key: 'DYN-1', name: '1 - Stabiel' },
complexityFactor: { objectId: '2', key: 'CMP-2', name: '2 - Gemiddeld' },
numberOfUsers: null,
governanceModel: null,
applicationCluster: null,
applicationType: null,
platform: null,
requiredEffortApplicationManagement: null,
},
{
id: '8',
key: 'APP-008',
name: 'AFAS Profit',
searchReference: 'AFAS',
description: 'HR en salarisadministratie systeem voor personeelsbeheer, tijdregistratie en verloning.',
supplierProduct: 'AFAS Software / Profit',
organisation: 'Bedrijfsvoering',
hostingType: { objectId: '2', key: 'HOST-2', name: 'SaaS' },
status: 'In Production',
businessImportance: 'Hoog',
businessImpactAnalyse: { objectId: '7', key: 'BIA-7', name: 'BIA-2024-0022 (Klasse D)' },
systemOwner: 'E. Hendriks',
businessOwner: 'C. van Leeuwen',
functionalApplicationManagement: 'Team HR',
technicalApplicationManagement: 'Team Bedrijfsapplicaties',
medischeTechniek: false,
applicationFunctions: [],
dynamicsFactor: { objectId: '2', key: 'DYN-2', name: '2 - Gemiddeld' },
complexityFactor: { objectId: '2', key: 'CMP-2', name: '2 - Gemiddeld' },
numberOfUsers: { objectId: '6', key: 'USR-6', name: '10.000 - 15.000' },
governanceModel: { objectId: 'C', key: 'GOV-C', name: 'Uitbesteed met ICMT-Regie' },
applicationCluster: null,
applicationType: null,
platform: null,
requiredEffortApplicationManagement: null,
},
{
id: '9',
key: 'APP-009',
name: 'Zenya',
searchReference: 'ZENYA',
description: 'Kwaliteitsmanagementsysteem voor protocollen, procedures en incidentmeldingen.',
supplierProduct: 'Infoland / Zenya',
organisation: 'Kwaliteit',
hostingType: { objectId: '2', key: 'HOST-2', name: 'SaaS' },
status: 'In Production',
businessImportance: 'Hoog',
businessImpactAnalyse: { objectId: '8', key: 'BIA-8', name: 'BIA-2024-0067 (Klasse C)' },
systemOwner: 'F. Bos',
businessOwner: 'I. Dekker',
functionalApplicationManagement: 'Team Kwaliteit',
technicalApplicationManagement: 'Team Bedrijfsapplicaties',
medischeTechniek: false,
applicationFunctions: [],
dynamicsFactor: { objectId: '2', key: 'DYN-2', name: '2 - Gemiddeld' },
complexityFactor: { objectId: '1', key: 'CMP-1', name: '1 - Laag' },
numberOfUsers: { objectId: '4', key: 'USR-4', name: '2.000 - 5.000' },
governanceModel: { objectId: 'C', key: 'GOV-C', name: 'Uitbesteed met ICMT-Regie' },
applicationCluster: null,
applicationType: null,
platform: null,
requiredEffortApplicationManagement: null,
},
{
id: '10',
key: 'APP-010',
name: 'Castor EDC',
searchReference: 'CASTOR',
description: 'Electronic Data Capture platform voor klinisch wetenschappelijk onderzoek en trials.',
supplierProduct: 'Castor / Castor EDC',
organisation: 'Onderzoek',
hostingType: { objectId: '2', key: 'HOST-2', name: 'SaaS' },
status: 'In Production',
businessImportance: 'Gemiddeld',
businessImpactAnalyse: null, // BIA-2024-0078 (Klasse B) not in mock list
systemOwner: 'G. Vos',
businessOwner: 'Prof. Dr. W. Maas',
functionalApplicationManagement: 'Team Onderzoek',
technicalApplicationManagement: null,
medischeTechniek: false,
applicationFunctions: [],
dynamicsFactor: { objectId: '1', key: 'DYN-1', name: '1 - Stabiel' },
complexityFactor: { objectId: '1', key: 'CMP-1', name: '1 - Laag' },
numberOfUsers: { objectId: '1', key: 'USR-1', name: '< 100' },
governanceModel: { objectId: 'D', key: 'GOV-D', name: 'Uitbesteed met Business-Regie' },
applicationCluster: null,
applicationType: null,
platform: null,
requiredEffortApplicationManagement: null,
},
];
// Mock reference data
const mockDynamicsFactors: ReferenceValue[] = [
{ objectId: '1', key: 'DYN-1', name: '1 - Stabiel', summary: 'Weinig wijzigingen, < 2 releases/jaar', description: 'Weinig wijzigingen, < 2 releases/jaar', factor: 0.8 },
{ objectId: '2', key: 'DYN-2', name: '2 - Gemiddeld', summary: 'Regelmatige wijzigingen, 2-4 releases/jaar', description: 'Regelmatige wijzigingen, 2-4 releases/jaar', factor: 1.0 },
{ objectId: '3', key: 'DYN-3', name: '3 - Hoog', summary: 'Veel wijzigingen, > 4 releases/jaar', description: 'Veel wijzigingen, > 4 releases/jaar', factor: 1.2 },
{ objectId: '4', key: 'DYN-4', name: '4 - Zeer hoog', summary: 'Continu in beweging, grote transformaties', description: 'Continu in beweging, grote transformaties', factor: 1.5 },
];
const mockComplexityFactors: ReferenceValue[] = [
{ objectId: '1', key: 'CMP-1', name: '1 - Laag', summary: 'Standalone, weinig integraties', description: 'Standalone, weinig integraties', factor: 0.8 },
{ objectId: '2', key: 'CMP-2', name: '2 - Gemiddeld', summary: 'Enkele integraties, beperkt maatwerk', description: 'Enkele integraties, beperkt maatwerk', factor: 1.0 },
{ objectId: '3', key: 'CMP-3', name: '3 - Hoog', summary: 'Veel integraties, significant maatwerk', description: 'Veel integraties, significant maatwerk', factor: 1.3 },
{ objectId: '4', key: 'CMP-4', name: '4 - Zeer hoog', summary: 'Platform, uitgebreide governance', description: 'Platform, uitgebreide governance', factor: 1.6 },
];
const mockNumberOfUsers: ReferenceValue[] = [
{ objectId: '1', key: 'USR-1', name: '< 100', order: 1, factor: 0.5 },
{ objectId: '2', key: 'USR-2', name: '100 - 500', order: 2, factor: 0.7 },
{ objectId: '3', key: 'USR-3', name: '500 - 2.000', order: 3, factor: 1.0 },
{ objectId: '4', key: 'USR-4', name: '2.000 - 5.000', order: 4, factor: 1.2 },
{ objectId: '5', key: 'USR-5', name: '5.000 - 10.000', order: 5, factor: 1.4 },
{ objectId: '6', key: 'USR-6', name: '10.000 - 15.000', order: 6, factor: 1.6 },
{ objectId: '7', key: 'USR-7', name: '> 15.000', order: 7, factor: 2.0 },
];
const mockGovernanceModels: ReferenceValue[] = [
{ objectId: 'A', key: 'GOV-A', name: 'Centraal Beheer', summary: 'ICMT voert volledig beheer uit', description: 'ICMT voert volledig beheer uit' },
{ objectId: 'B', key: 'GOV-B', name: 'Federatief Beheer', summary: 'ICMT + business delen beheer', description: 'ICMT + business delen beheer' },
{ objectId: 'C', key: 'GOV-C', name: 'Uitbesteed met ICMT-Regie', summary: 'Leverancier beheert, ICMT regisseert', description: 'Leverancier beheert, ICMT regisseert' },
{ objectId: 'D', key: 'GOV-D', name: 'Uitbesteed met Business-Regie', summary: 'Leverancier beheert, business regisseert', description: 'Leverancier beheert, business regisseert' },
{ objectId: 'E', key: 'GOV-E', name: 'Volledig Decentraal Beheer', summary: 'Business voert volledig beheer uit', description: 'Business voert volledig beheer uit' },
];
const mockOrganisations: ReferenceValue[] = [
{ objectId: '1', key: 'ORG-1', name: 'Zorg' },
{ objectId: '2', key: 'ORG-2', name: 'Bedrijfsvoering' },
{ objectId: '3', key: 'ORG-3', name: 'ICMT' },
{ objectId: '4', key: 'ORG-4', name: 'Kwaliteit' },
{ objectId: '5', key: 'ORG-5', name: 'Onderzoek' },
{ objectId: '6', key: 'ORG-6', name: 'Onderwijs' },
];
const mockHostingTypes: ReferenceValue[] = [
{ objectId: '1', key: 'HOST-1', name: 'On-premises' },
{ objectId: '2', key: 'HOST-2', name: 'SaaS' },
{ objectId: '3', key: 'HOST-3', name: 'Cloud' },
{ objectId: '4', key: 'HOST-4', name: 'Hybrid' },
];
const mockBusinessImpactAnalyses: ReferenceValue[] = [
{ objectId: '1', key: 'BIA-1', name: 'BIA-2024-0042 (Klasse E)' },
{ objectId: '2', key: 'BIA-2', name: 'BIA-2024-0015 (Klasse D)' },
{ objectId: '3', key: 'BIA-3', name: 'BIA-2024-0028 (Klasse D)' },
{ objectId: '4', key: 'BIA-4', name: 'BIA-2024-0035 (Klasse C)' },
{ objectId: '5', key: 'BIA-5', name: 'BIA-2024-0001 (Klasse F)' },
{ objectId: '6', key: 'BIA-6', name: 'BIA-2024-0055 (Klasse C)' },
{ objectId: '7', key: 'BIA-7', name: 'BIA-2024-0022 (Klasse D)' },
{ objectId: '8', key: 'BIA-8', name: 'BIA-2024-0067 (Klasse C)' },
{ objectId: '9', key: 'BIA-9', name: 'BIA-2022-0089 (Klasse C)' },
];
const mockApplicationClusters: ReferenceValue[] = [
{ objectId: '1', key: 'CLUSTER-1', name: 'Zorgapplicaties' },
{ objectId: '2', key: 'CLUSTER-2', name: 'Bedrijfsvoering' },
{ objectId: '3', key: 'CLUSTER-3', name: 'Infrastructuur' },
];
const mockApplicationTypes: ReferenceValue[] = [
{ objectId: '1', key: 'TYPE-1', name: 'Applicatie' },
{ objectId: '2', key: 'TYPE-2', name: 'Platform' },
{ objectId: '3', key: 'TYPE-3', name: 'Workload' },
];
// Classification history
const mockClassificationHistory: ClassificationResult[] = [];
// Mock data service
export class MockDataService {
private applications: ApplicationDetails[] = [...mockApplications];
async searchApplications(
filters: SearchFilters,
page: number = 1,
pageSize: number = 25
): Promise<SearchResult> {
let filtered = [...this.applications];
// Apply search text filter
if (filters.searchText) {
const search = filters.searchText.toLowerCase();
filtered = filtered.filter(
(app) =>
app.name.toLowerCase().includes(search) ||
(app.description?.toLowerCase().includes(search) ?? false) ||
(app.supplierProduct?.toLowerCase().includes(search) ?? false) ||
(app.searchReference?.toLowerCase().includes(search) ?? false)
);
}
// Apply status filter
if (filters.statuses && filters.statuses.length > 0) {
filtered = filtered.filter((app) =>
app.status ? filters.statuses!.includes(app.status) : false
);
}
// Apply applicationFunction filter
if (filters.applicationFunction === 'empty') {
filtered = filtered.filter((app) => app.applicationFunctions.length === 0);
} else if (filters.applicationFunction === 'filled') {
filtered = filtered.filter((app) => app.applicationFunctions.length > 0);
}
// Apply governanceModel filter
if (filters.governanceModel === 'empty') {
filtered = filtered.filter((app) => !app.governanceModel);
} else if (filters.governanceModel === 'filled') {
filtered = filtered.filter((app) => !!app.governanceModel);
}
// Apply dynamicsFactor filter
if (filters.dynamicsFactor === 'empty') {
filtered = filtered.filter((app) => !app.dynamicsFactor);
} else if (filters.dynamicsFactor === 'filled') {
filtered = filtered.filter((app) => !!app.dynamicsFactor);
}
// Apply complexityFactor filter
if (filters.complexityFactor === 'empty') {
filtered = filtered.filter((app) => !app.complexityFactor);
} else if (filters.complexityFactor === 'filled') {
filtered = filtered.filter((app) => !!app.complexityFactor);
}
// Apply applicationCluster filter
if (filters.applicationCluster === 'empty') {
filtered = filtered.filter((app) => !app.applicationCluster);
} else if (filters.applicationCluster === 'filled') {
filtered = filtered.filter((app) => !!app.applicationCluster);
}
// Apply applicationType filter
if (filters.applicationType === 'empty') {
filtered = filtered.filter((app) => !app.applicationType);
} else if (filters.applicationType === 'filled') {
filtered = filtered.filter((app) => !!app.applicationType);
}
// Apply organisation filter
if (filters.organisation) {
filtered = filtered.filter((app) => app.organisation === filters.organisation);
}
// Apply hostingType filter
if (filters.hostingType) {
filtered = filtered.filter((app) => {
if (!app.hostingType) return false;
return app.hostingType.name === filters.hostingType || app.hostingType.key === filters.hostingType;
});
}
if (filters.businessImportance) {
filtered = filtered.filter((app) => app.businessImportance === filters.businessImportance);
}
const totalCount = filtered.length;
const totalPages = Math.ceil(totalCount / pageSize);
const startIndex = (page - 1) * pageSize;
const paginatedApps = filtered.slice(startIndex, startIndex + pageSize);
return {
applications: paginatedApps.map((app) => {
const effort = calculateRequiredEffortApplicationManagement(app);
return {
id: app.id,
key: app.key,
name: app.name,
status: app.status,
applicationFunctions: app.applicationFunctions,
governanceModel: app.governanceModel,
dynamicsFactor: app.dynamicsFactor,
complexityFactor: app.complexityFactor,
applicationCluster: app.applicationCluster,
applicationType: app.applicationType,
platform: app.platform,
requiredEffortApplicationManagement: effort,
};
}),
totalCount,
currentPage: page,
pageSize,
totalPages,
};
}
async getApplicationById(id: string): Promise<ApplicationDetails | null> {
const app = this.applications.find((app) => app.id === id);
if (!app) return null;
// Calculate required effort
const effort = calculateRequiredEffortApplicationManagement(app);
return {
...app,
requiredEffortApplicationManagement: effort,
};
}
async updateApplication(
id: string,
updates: {
applicationFunctions?: ReferenceValue[];
dynamicsFactor?: ReferenceValue;
complexityFactor?: ReferenceValue;
numberOfUsers?: ReferenceValue;
governanceModel?: ReferenceValue;
applicationCluster?: ReferenceValue;
applicationType?: ReferenceValue;
hostingType?: ReferenceValue;
businessImpactAnalyse?: ReferenceValue;
}
): Promise<boolean> {
const index = this.applications.findIndex((app) => app.id === id);
if (index === -1) return false;
const app = this.applications[index];
if (updates.applicationFunctions !== undefined) {
app.applicationFunctions = updates.applicationFunctions;
}
if (updates.dynamicsFactor !== undefined) {
app.dynamicsFactor = updates.dynamicsFactor;
}
if (updates.complexityFactor !== undefined) {
app.complexityFactor = updates.complexityFactor;
}
if (updates.numberOfUsers !== undefined) {
app.numberOfUsers = updates.numberOfUsers;
}
if (updates.governanceModel !== undefined) {
app.governanceModel = updates.governanceModel;
}
if (updates.applicationCluster !== undefined) {
app.applicationCluster = updates.applicationCluster;
}
if (updates.applicationType !== undefined) {
app.applicationType = updates.applicationType;
}
if (updates.hostingType !== undefined) {
app.hostingType = updates.hostingType;
}
if (updates.businessImpactAnalyse !== undefined) {
app.businessImpactAnalyse = updates.businessImpactAnalyse;
}
if (updates.applicationCluster !== undefined) {
app.applicationCluster = updates.applicationCluster;
}
if (updates.applicationType !== undefined) {
app.applicationType = updates.applicationType;
}
return true;
}
async getDynamicsFactors(): Promise<ReferenceValue[]> {
return mockDynamicsFactors;
}
async getComplexityFactors(): Promise<ReferenceValue[]> {
return mockComplexityFactors;
}
async getNumberOfUsers(): Promise<ReferenceValue[]> {
return mockNumberOfUsers;
}
async getGovernanceModels(): Promise<ReferenceValue[]> {
return mockGovernanceModels;
}
async getOrganisations(): Promise<ReferenceValue[]> {
return mockOrganisations;
}
async getHostingTypes(): Promise<ReferenceValue[]> {
return mockHostingTypes;
}
async getBusinessImpactAnalyses(): Promise<ReferenceValue[]> {
return mockBusinessImpactAnalyses;
}
async getApplicationManagementHosting(): Promise<ReferenceValue[]> {
// Mock Application Management - Hosting values (v25)
return [
{ objectId: '1', key: 'AMH-1', name: 'On-Premises' },
{ objectId: '2', key: 'AMH-2', name: 'Azure - Eigen beheer' },
{ objectId: '3', key: 'AMH-3', name: 'Azure - Delegated Management' },
{ objectId: '4', key: 'AMH-4', name: 'Extern (SaaS)' },
];
}
async getApplicationManagementTAM(): Promise<ReferenceValue[]> {
// Mock Application Management - TAM values
return [
{ objectId: '1', key: 'TAM-1', name: 'ICMT' },
{ objectId: '2', key: 'TAM-2', name: 'Business' },
{ objectId: '3', key: 'TAM-3', name: 'Leverancier' },
];
}
async getApplicationFunctions(): Promise<ReferenceValue[]> {
// Return empty for mock - in real implementation, this comes from Jira
return [];
}
async getApplicationClusters(): Promise<ReferenceValue[]> {
// Return empty for mock - in real implementation, this comes from Jira
return [];
}
async getApplicationTypes(): Promise<ReferenceValue[]> {
// Return empty for mock - in real implementation, this comes from Jira
return [];
}
async getBusinessImportance(): Promise<ReferenceValue[]> {
// Return empty for mock - in real implementation, this comes from Jira
return [];
}
async getApplicationFunctionCategories(): Promise<ReferenceValue[]> {
// Return empty for mock - in real implementation, this comes from Jira
return [];
}
async getStats() {
// Filter out applications with status "Closed" for KPIs
const activeApplications = this.applications.filter((a) => a.status !== 'Closed');
const total = activeApplications.length;
const classified = activeApplications.filter((a) => a.applicationFunctions.length > 0).length;
const unclassified = total - classified;
const byStatus: Record<string, number> = {};
const byGovernanceModel: Record<string, number> = {};
activeApplications.forEach((app) => {
if (app.status) {
byStatus[app.status] = (byStatus[app.status] || 0) + 1;
}
if (app.governanceModel) {
byGovernanceModel[app.governanceModel.name] =
(byGovernanceModel[app.governanceModel.name] || 0) + 1;
}
});
return {
totalApplications: total,
classifiedCount: classified,
unclassifiedCount: unclassified,
byStatus,
byDomain: {},
byGovernanceModel,
recentClassifications: mockClassificationHistory.slice(-10),
};
}
addClassificationResult(result: ClassificationResult): void {
mockClassificationHistory.push(result);
}
getClassificationHistory(): ClassificationResult[] {
return [...mockClassificationHistory];
}
async getTeamDashboardData(excludedStatuses: ApplicationStatus[] = []): Promise<TeamDashboardData> {
// Convert ApplicationDetails to ApplicationListItem for dashboard
let listItems: ApplicationListItem[] = this.applications.map(app => ({
id: app.id,
key: app.key,
name: app.name,
status: app.status,
applicationFunctions: app.applicationFunctions,
governanceModel: app.governanceModel,
dynamicsFactor: app.dynamicsFactor,
complexityFactor: app.complexityFactor,
applicationCluster: app.applicationCluster,
applicationType: app.applicationType,
platform: app.platform,
requiredEffortApplicationManagement: app.requiredEffortApplicationManagement,
}));
// Filter out excluded statuses
if (excludedStatuses.length > 0) {
listItems = listItems.filter(app => !app.status || !excludedStatuses.includes(app.status));
}
// Separate applications into Platforms, Workloads, and regular applications
const platforms: ApplicationListItem[] = [];
const workloads: ApplicationListItem[] = [];
const regularApplications: ApplicationListItem[] = [];
for (const app of listItems) {
const isPlatform = app.applicationType?.name === 'Platform';
const isWorkload = app.platform !== null;
if (isPlatform) {
platforms.push(app);
} else if (isWorkload) {
workloads.push(app);
} else {
regularApplications.push(app);
}
}
// Group workloads by their platform
const workloadsByPlatform = new Map<string, ApplicationListItem[]>();
for (const workload of workloads) {
const platformId = workload.platform!.objectId;
if (!workloadsByPlatform.has(platformId)) {
workloadsByPlatform.set(platformId, []);
}
workloadsByPlatform.get(platformId)!.push(workload);
}
// Build PlatformWithWorkloads structures
const platformsWithWorkloads: import('../types/index.js').PlatformWithWorkloads[] = [];
for (const platform of platforms) {
const platformWorkloads = workloadsByPlatform.get(platform.id) || [];
const platformEffort = platform.requiredEffortApplicationManagement || 0;
const workloadsEffort = platformWorkloads.reduce((sum, w) => sum + (w.requiredEffortApplicationManagement || 0), 0);
platformsWithWorkloads.push({
platform,
workloads: platformWorkloads,
platformEffort,
workloadsEffort,
totalEffort: platformEffort + workloadsEffort,
});
}
// Group all applications (regular + platforms + workloads) by cluster
const clusterMap = new Map<string, {
regular: ApplicationListItem[];
platforms: import('../types/index.js').PlatformWithWorkloads[];
}>();
const unassigned: {
regular: ApplicationListItem[];
platforms: import('../types/index.js').PlatformWithWorkloads[];
} = {
regular: [],
platforms: [],
};
// Group regular applications by cluster
for (const app of regularApplications) {
if (app.applicationCluster) {
const clusterId = app.applicationCluster.objectId;
if (!clusterMap.has(clusterId)) {
clusterMap.set(clusterId, { regular: [], platforms: [] });
}
clusterMap.get(clusterId)!.regular.push(app);
} else {
unassigned.regular.push(app);
}
}
// Group platforms by cluster
for (const platformWithWorkloads of platformsWithWorkloads) {
const platform = platformWithWorkloads.platform;
if (platform.applicationCluster) {
const clusterId = platform.applicationCluster.objectId;
if (!clusterMap.has(clusterId)) {
clusterMap.set(clusterId, { regular: [], platforms: [] });
}
clusterMap.get(clusterId)!.platforms.push(platformWithWorkloads);
} else {
unassigned.platforms.push(platformWithWorkloads);
}
}
// Get all clusters
const allClusters = mockApplicationClusters;
const clusters = allClusters.map(cluster => {
const clusterData = clusterMap.get(cluster.objectId) || { regular: [], platforms: [] };
const regularApps = clusterData.regular;
const platforms = clusterData.platforms;
// Calculate total effort: regular apps + platforms (including their workloads)
const regularEffort = regularApps.reduce((sum, app) =>
sum + (app.requiredEffortApplicationManagement || 0), 0
);
const platformsEffort = platforms.reduce((sum, p) => sum + p.totalEffort, 0);
const totalEffort = regularEffort + platformsEffort;
// Calculate total application count: regular apps + platforms + workloads
const platformsCount = platforms.length;
const workloadsCount = platforms.reduce((sum, p) => sum + p.workloads.length, 0);
const applicationCount = regularApps.length + platformsCount + workloadsCount;
// Calculate governance model distribution (including platforms and workloads)
const byGovernanceModel: Record<string, number> = {};
for (const app of regularApps) {
const govModel = app.governanceModel?.name || 'Niet ingesteld';
byGovernanceModel[govModel] = (byGovernanceModel[govModel] || 0) + 1;
}
for (const platformWithWorkloads of platforms) {
const platform = platformWithWorkloads.platform;
const govModel = platform.governanceModel?.name || 'Niet ingesteld';
byGovernanceModel[govModel] = (byGovernanceModel[govModel] || 0) + 1;
// Also count workloads
for (const workload of platformWithWorkloads.workloads) {
const workloadGovModel = workload.governanceModel?.name || 'Niet ingesteld';
byGovernanceModel[workloadGovModel] = (byGovernanceModel[workloadGovModel] || 0) + 1;
}
}
return {
cluster,
applications: regularApps,
platforms,
totalEffort,
minEffort: totalEffort * 0.8, // Mock: min is 80% of total
maxEffort: totalEffort * 1.2, // Mock: max is 120% of total
applicationCount,
byGovernanceModel,
};
});
// Calculate unassigned totals
const unassignedRegularEffort = unassigned.regular.reduce((sum, app) =>
sum + (app.requiredEffortApplicationManagement || 0), 0
);
const unassignedPlatformsEffort = unassigned.platforms.reduce((sum, p) => sum + p.totalEffort, 0);
const unassignedTotalEffort = unassignedRegularEffort + unassignedPlatformsEffort;
const unassignedPlatformsCount = unassigned.platforms.length;
const unassignedWorkloadsCount = unassigned.platforms.reduce((sum, p) => sum + p.workloads.length, 0);
const unassignedApplicationCount = unassigned.regular.length + unassignedPlatformsCount + unassignedWorkloadsCount;
// Calculate governance model distribution for unassigned
const unassignedByGovernanceModel: Record<string, number> = {};
for (const app of unassigned.regular) {
const govModel = app.governanceModel?.name || 'Niet ingesteld';
unassignedByGovernanceModel[govModel] = (unassignedByGovernanceModel[govModel] || 0) + 1;
}
for (const platformWithWorkloads of unassigned.platforms) {
const platform = platformWithWorkloads.platform;
const govModel = platform.governanceModel?.name || 'Niet ingesteld';
unassignedByGovernanceModel[govModel] = (unassignedByGovernanceModel[govModel] || 0) + 1;
for (const workload of platformWithWorkloads.workloads) {
const workloadGovModel = workload.governanceModel?.name || 'Niet ingesteld';
unassignedByGovernanceModel[workloadGovModel] = (unassignedByGovernanceModel[workloadGovModel] || 0) + 1;
}
}
return {
clusters,
unassigned: {
applications: unassigned.regular,
platforms: unassigned.platforms,
totalEffort: unassignedTotalEffort,
minEffort: unassignedTotalEffort * 0.8, // Mock: min is 80% of total
maxEffort: unassignedTotalEffort * 1.2, // Mock: max is 120% of total
applicationCount: unassignedApplicationCount,
byGovernanceModel: unassignedByGovernanceModel,
},
};
}
}
export const mockDataService = new MockDataService();

409
backend/src/types/index.ts Normal file
View File

@@ -0,0 +1,409 @@
// Application status types
export type ApplicationStatus =
| 'Status'
| 'Closed'
| 'Deprecated'
| 'End of life'
| 'End of support'
| 'Implementation'
| 'In Production'
| 'Proof of Concept'
| 'Shadow IT'
| 'Undefined';
// Reference value from Jira Assets
export interface ReferenceValue {
objectId: string;
key: string;
name: string;
description?: string;
summary?: string; // Summary attribute for Dynamics Factor, Complexity Factor, and Governance Model
category?: string; // Deprecated: kept for backward compatibility, use applicationFunctionCategory instead
applicationFunctionCategory?: ReferenceValue; // Reference to ApplicationFunctionCategory object
keywords?: string; // Keywords for ApplicationFunction
order?: number;
factor?: number; // Factor attribute for Dynamics Factor, Complexity Factor, and Number of Users
remarks?: string; // Remarks attribute for Governance Model
application?: string; // Application attribute for Governance Model
indicators?: string; // Indicators attribute for Business Impact Analyse
}
// Application list item (summary view)
export interface ApplicationListItem {
id: string;
key: string;
name: string;
status: ApplicationStatus | null;
applicationFunctions: ReferenceValue[]; // Multiple functions supported
governanceModel: ReferenceValue | null;
dynamicsFactor: ReferenceValue | null;
complexityFactor: ReferenceValue | null;
applicationCluster: ReferenceValue | null;
applicationType: ReferenceValue | null;
platform: ReferenceValue | null; // Reference to parent Platform Application Component
requiredEffortApplicationManagement: number | null; // Calculated field
minFTE?: number | null; // Minimum FTE from configuration range
maxFTE?: number | null; // Maximum FTE from configuration range
overrideFTE?: number | null; // Override FTE value (if set, overrides calculated value)
applicationManagementHosting?: ReferenceValue | null; // Application Management - Hosting
applicationManagementTAM?: ReferenceValue | null; // Application Management - TAM
}
// Full application details
export interface ApplicationDetails {
id: string;
key: string;
name: string;
searchReference: string | null;
description: string | null;
supplierProduct: string | null;
organisation: string | null;
hostingType: ReferenceValue | null;
status: ApplicationStatus | null;
businessImportance: string | null;
businessImpactAnalyse: ReferenceValue | null;
systemOwner: string | null;
businessOwner: string | null;
functionalApplicationManagement: string | null;
technicalApplicationManagement: string | null;
technicalApplicationManagementPrimary?: string | null; // Technical Application Management Primary
technicalApplicationManagementSecondary?: string | null; // Technical Application Management Secondary
medischeTechniek: boolean;
applicationFunctions: ReferenceValue[]; // Multiple functions supported
dynamicsFactor: ReferenceValue | null;
complexityFactor: ReferenceValue | null;
numberOfUsers: ReferenceValue | null;
governanceModel: ReferenceValue | null;
applicationCluster: ReferenceValue | null;
applicationType: ReferenceValue | null;
platform: ReferenceValue | null; // Reference to parent Platform Application Component
requiredEffortApplicationManagement: number | null; // Calculated field
overrideFTE?: number | null; // Override FTE value (if set, overrides calculated value)
applicationManagementHosting?: ReferenceValue | null; // Application Management - Hosting
applicationManagementTAM?: ReferenceValue | null; // Application Management - TAM
technischeArchitectuur?: string | null; // URL to Technical Architecture document (Attribute ID 572)
}
// Search filters
export interface SearchFilters {
searchText?: string;
statuses?: ApplicationStatus[];
applicationFunction?: 'all' | 'filled' | 'empty';
governanceModel?: 'all' | 'filled' | 'empty';
dynamicsFactor?: 'all' | 'filled' | 'empty';
complexityFactor?: 'all' | 'filled' | 'empty';
applicationCluster?: 'all' | 'filled' | 'empty';
applicationType?: 'all' | 'filled' | 'empty';
organisation?: string;
hostingType?: string;
businessImportance?: string;
}
// Paginated search result
export interface SearchResult {
applications: ApplicationListItem[];
totalCount: number;
currentPage: number;
pageSize: number;
totalPages: number;
}
// AI classification suggestion
export interface AISuggestion {
primaryFunction: {
code: string;
name: string;
reasoning: string;
};
secondaryFunctions: Array<{
code: string;
name: string;
reasoning: string;
}>;
managementClassification?: {
applicationType?: {
value: string;
reasoning: string;
};
dynamicsFactor?: {
value: string;
label: string;
reasoning: string;
};
complexityFactor?: {
value: string;
label: string;
reasoning: string;
};
hostingType?: {
value: string;
reasoning: string;
};
applicationManagementHosting?: {
value: string;
reasoning: string;
};
applicationManagementTAM?: {
value: string;
reasoning: string;
};
biaClassification?: {
value: string;
reasoning: string;
};
governanceModel?: {
value: string;
reasoning: string;
};
};
validationWarnings?: string[];
confidence: 'HOOG' | 'MIDDEN' | 'LAAG';
notes: string;
}
// Pending changes for an application
export interface PendingChanges {
applicationFunctions?: { from: ReferenceValue[]; to: ReferenceValue[] };
dynamicsFactor?: { from: ReferenceValue | null; to: ReferenceValue };
complexityFactor?: { from: ReferenceValue | null; to: ReferenceValue };
numberOfUsers?: { from: ReferenceValue | null; to: ReferenceValue };
governanceModel?: { from: ReferenceValue | null; to: ReferenceValue };
applicationCluster?: { from: ReferenceValue | null; to: ReferenceValue };
applicationType?: { from: ReferenceValue | null; to: ReferenceValue };
}
// Classification result for audit log
export interface ClassificationResult {
applicationId: string;
applicationName: string;
changes: PendingChanges;
source: 'AI_ACCEPTED' | 'AI_MODIFIED' | 'MANUAL';
timestamp: Date;
userId?: string;
}
// Reference options for dropdowns
export interface ReferenceOptions {
dynamicsFactors: ReferenceValue[];
complexityFactors: ReferenceValue[];
numberOfUsers: ReferenceValue[];
governanceModels: ReferenceValue[];
applicationFunctions: ReferenceValue[];
applicationClusters: ReferenceValue[];
applicationTypes: ReferenceValue[];
organisations: ReferenceValue[];
hostingTypes: ReferenceValue[];
businessImportance: ReferenceValue[];
applicationManagementHosting: ReferenceValue[];
applicationManagementTAM: ReferenceValue[];
}
// ZiRA domain structure
export interface ZiraDomain {
code: string;
name: string;
description: string;
functions: ZiraFunction[];
}
export interface ZiraFunction {
code: string;
name: string;
description: string;
keywords: string[];
}
export interface ZiraTaxonomy {
version: string;
source: string;
lastUpdated: string;
domains: ZiraDomain[];
}
// Dashboard statistics
export interface DashboardStats {
totalApplications: number;
classifiedCount: number;
unclassifiedCount: number;
byStatus: Record<string, number>;
byDomain: Record<string, number>;
byGovernanceModel: Record<string, number>;
recentClassifications: ClassificationResult[];
}
// Navigation state for detail screen
export interface NavigationState {
currentIndex: number;
totalInResults: number;
applicationIds: string[];
filters: SearchFilters;
}
// Effort calculation breakdown (v25)
export interface EffortCalculationBreakdown {
// Base FTE values
baseEffort: number; // Average of min/max
baseEffortMin: number;
baseEffortMax: number;
// Lookup path used
governanceModel: string | null;
governanceModelName: string | null;
applicationType: string | null;
businessImpactAnalyse: string | null;
applicationManagementHosting: string | null;
// Factors applied
numberOfUsersFactor: { value: number; name: string | null };
dynamicsFactor: { value: number; name: string | null };
complexityFactor: { value: number; name: string | null };
// Fallback information
usedDefaults: string[]; // Which levels used default values
// Validation warnings/errors
warnings: string[];
errors: string[];
// Special flags
requiresManualAssessment: boolean;
isFixedFte: boolean;
notRecommended: boolean;
// Hours calculation (based on final FTE)
hoursPerYear: number;
hoursPerMonth: number;
hoursPerWeek: number;
}
// Legacy type for backward compatibility
export interface EffortCalculationBreakdownLegacy {
baseEffort: number;
governanceModel: string | null;
applicationType: string | null;
businessImpactAnalyse: string | null;
hostingType: string | null;
numberOfUsersFactor: { value: number; name: string | null };
dynamicsFactor: { value: number; name: string | null };
complexityFactor: { value: number; name: string | null };
}
// Team dashboard types
export interface PlatformWithWorkloads {
platform: ApplicationListItem;
workloads: ApplicationListItem[];
platformEffort: number;
workloadsEffort: number;
totalEffort: number; // platformEffort + workloadsEffort
}
export interface TeamDashboardCluster {
cluster: ReferenceValue | null;
applications: ApplicationListItem[]; // Regular applications (non-Platform, non-Workload)
platforms: PlatformWithWorkloads[]; // Platforms with their workloads
totalEffort: number; // Sum of all applications + platforms + workloads
minEffort: number; // Sum of all minimum FTE values
maxEffort: number; // Sum of all maximum FTE values
applicationCount: number; // Count of all applications (including platforms and workloads)
byGovernanceModel: Record<string, number>; // Distribution per governance model
}
export interface TeamDashboardData {
clusters: TeamDashboardCluster[];
unassigned: {
applications: ApplicationListItem[]; // Regular applications (non-Platform, non-Workload)
platforms: PlatformWithWorkloads[]; // Platforms with their workloads
totalEffort: number; // Sum of all applications + platforms + workloads
minEffort: number; // Sum of all minimum FTE values
maxEffort: number; // Sum of all maximum FTE values
applicationCount: number; // Count of all applications (including platforms and workloads)
byGovernanceModel: Record<string, number>; // Distribution per governance model
};
}
// Jira Assets API types
export interface JiraAssetsObject {
id: number;
objectKey: string;
label: string;
objectType: {
id: number;
name: string;
};
attributes: JiraAssetsAttribute[];
}
export interface JiraAssetsAttribute {
objectTypeAttributeId: number;
objectTypeAttribute?: {
id: number;
name: string;
};
objectAttributeValues: Array<{
value?: string;
displayValue?: string;
referencedObject?: {
id: number;
objectKey: string;
label: string;
};
}>;
}
export interface JiraAssetsSearchResponse {
objectEntries: JiraAssetsObject[];
page: number;
pageSize: number;
totalCount: number;
totalFilterCount?: number; // Optional, may not be present in all API versions
}
export interface ApplicationUpdateRequest {
applicationFunctions?: string[];
dynamicsFactor?: string;
complexityFactor?: string;
numberOfUsers?: string;
governanceModel?: string;
applicationCluster?: string;
applicationType?: string;
hostingType?: string;
businessImpactAnalyse?: string;
overrideFTE?: number | null; // Override FTE value (null to clear)
applicationManagementHosting?: string; // Application Management - Hosting object key
applicationManagementTAM?: string; // Application Management - TAM object key
}
// Chat message for AI conversation
export interface ChatMessage {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: Date;
// For assistant messages, include the structured suggestion if available
suggestion?: AISuggestion;
}
// Chat conversation state
export interface ChatConversation {
id: string;
applicationId: string;
applicationName: string;
messages: ChatMessage[];
createdAt: Date;
updatedAt: Date;
}
// Chat request for follow-up
export interface ChatRequest {
conversationId?: string; // If continuing existing conversation
applicationId: string;
message: string;
provider?: 'claude' | 'openai';
}
// Chat response
export interface ChatResponse {
conversationId: string;
message: ChatMessage;
suggestion?: AISuggestion; // Updated suggestion if AI provided one
}