/** * EffortDisplay - Shared component for displaying FTE effort calculations * * Used in: * - ApplicationInfo.tsx (detail page) * - GovernanceModelHelper.tsx (governance helper page) */ import type { EffortCalculationBreakdown } from '../types'; export interface EffortDisplayProps { /** The effective FTE value (after override if applicable) */ effectiveFte: number | null; /** The calculated FTE value (before override) */ calculatedFte?: number | null; /** Override FTE value if set */ overrideFte?: number | null; /** Full breakdown from effort calculation */ breakdown?: EffortCalculationBreakdown | null; /** Whether this is a preview (unsaved changes) */ isPreview?: boolean; /** Show full details (expanded view) */ showDetails?: boolean; /** Show override input field */ showOverrideInput?: boolean; /** Callback when override value changes */ onOverrideChange?: (value: number | null) => void; } // Display-only constants (for showing the formula, NOT for calculation) // The actual calculation happens in backend/src/services/effortCalculation.ts const HOURS_PER_WEEK_DISPLAY = 36; const WORK_WEEKS_PER_YEAR_DISPLAY = 46; const DECLARABLE_PERCENTAGE_DISPLAY = 0.75; export function EffortDisplay({ effectiveFte, calculatedFte, overrideFte, breakdown, isPreview = false, showDetails = true, showOverrideInput = false, onOverrideChange, }: EffortDisplayProps) { const hasOverride = overrideFte !== null && overrideFte !== undefined; const hasBreakdown = breakdown !== null && breakdown !== undefined; // Extract breakdown values const baseEffort = breakdown?.baseEffort ?? null; const baseEffortMin = breakdown?.baseEffortMin ?? null; const baseEffortMax = breakdown?.baseEffortMax ?? null; const numberOfUsersFactor = breakdown?.numberOfUsersFactor ?? { value: 1.0, name: null }; const dynamicsFactor = breakdown?.dynamicsFactor ?? { value: 1.0, name: null }; const complexityFactor = breakdown?.complexityFactor ?? { value: 1.0, name: null }; // Calculate final min/max FTE by applying factors to base min/max const factorMultiplier = numberOfUsersFactor.value * dynamicsFactor.value * complexityFactor.value; const finalMinFTE = baseEffortMin !== null ? baseEffortMin * factorMultiplier : null; const finalMaxFTE = baseEffortMax !== null ? baseEffortMax * factorMultiplier : null; const governanceModelName = breakdown?.governanceModelName ?? breakdown?.governanceModel ?? null; const applicationTypeName = breakdown?.applicationType ?? null; const businessImpactAnalyse = breakdown?.businessImpactAnalyse ?? null; const applicationManagementHosting = breakdown?.applicationManagementHosting ?? null; const warnings = breakdown?.warnings ?? []; const errors = breakdown?.errors ?? []; const usedDefaults = breakdown?.usedDefaults ?? []; const requiresManualAssessment = breakdown?.requiresManualAssessment ?? false; const isFixedFte = breakdown?.isFixedFte ?? false; // Use hours from backend breakdown (calculated in effortCalculation.ts) // Only fall back to local calculation if breakdown is not available const declarableHoursPerYear = breakdown?.hoursPerYear ?? (effectiveFte !== null ? HOURS_PER_WEEK_DISPLAY * WORK_WEEKS_PER_YEAR_DISPLAY * effectiveFte * DECLARABLE_PERCENTAGE_DISPLAY : 0); const hoursPerMonth = breakdown?.hoursPerMonth ?? declarableHoursPerYear / 12; const hoursPerWeekCalculated = breakdown?.hoursPerWeek ?? declarableHoursPerYear / WORK_WEEKS_PER_YEAR_DISPLAY; const minutesPerWeek = hoursPerWeekCalculated * 60; // For display of netto hours (before declarable percentage) const netHoursPerYear = effectiveFte !== null ? HOURS_PER_WEEK_DISPLAY * WORK_WEEKS_PER_YEAR_DISPLAY * effectiveFte : 0; // No effort calculated if (effectiveFte === null || effectiveFte === undefined) { if (errors.length > 0) { return (
{errors.map((error, i) => (
{error}
))}
Niet berekend - configuratie onvolledig
); } return Niet berekend; } return (
{/* Errors */} {errors.length > 0 && (
{errors.map((error, i) => (
{error}
))}
)} {/* Warnings */} {warnings.length > 0 && (
{warnings.map((warning, i) => (
{warning.startsWith('⚠️') || warning.startsWith('ℹ️') ? '' : 'ℹ️'} {warning}
))}
)} {/* Main FTE display */}
{effectiveFte.toFixed(2)} FTE {finalMinFTE !== null && finalMaxFTE !== null && finalMinFTE !== finalMaxFTE && ( (bandbreedte: {finalMinFTE.toFixed(2)} - {finalMaxFTE.toFixed(2)}) )} {hasOverride && ( (Override) )} {isPreview && !hasOverride && ( (voorvertoning) )} {isFixedFte && ( (vast) )} {requiresManualAssessment && ( (handmatige beoordeling) )}
{/* Show calculated value if override is active */} {hasOverride && calculatedFte !== null && calculatedFte !== undefined && (
Berekende waarde: {calculatedFte.toFixed(2)} FTE
)} {/* Override input */} {showOverrideInput && onOverrideChange && (
{ const value = e.target.value; if (value === '') { onOverrideChange(null); } else { const numValue = parseFloat(value); if (!isNaN(numValue)) { onOverrideChange(numValue); } } }} className="w-24 px-2 py-1 border border-gray-300 rounded text-sm" placeholder="Leeg" />
)} {showDetails && baseEffort !== null && (
{/* Base FTE with range */}
Basis FTE: {baseEffort.toFixed(2)} FTE {baseEffortMin !== null && baseEffortMax !== null && baseEffortMin !== baseEffortMax && ( (range: {baseEffortMin.toFixed(2)} - {baseEffortMax.toFixed(2)}) )}
{/* Lookup path */}
ICT Governance Model: {governanceModelName || 'Niet ingesteld'} {usedDefaults.includes('regiemodel') && (default)}
Application Type: {applicationTypeName || 'Niet ingesteld'} {usedDefaults.includes('applicationType') && (default)}
Business Impact Analyse: {businessImpactAnalyse || 'Niet ingesteld'} {usedDefaults.includes('businessImpact') && (default)}
Hosting: {applicationManagementHosting || 'Niet ingesteld'} {usedDefaults.includes('hosting') && (default)}
{/* Factors */}
Factoren:
Number of Users: × {numberOfUsersFactor.value.toFixed(2)} {numberOfUsersFactor.name && ` (${numberOfUsersFactor.name})`}
Dynamics Factor: × {dynamicsFactor.value.toFixed(2)} {dynamicsFactor.name && ` (${dynamicsFactor.name})`}
Complexity Factor: × {complexityFactor.value.toFixed(2)} {complexityFactor.name && ` (${complexityFactor.name})`}
{/* Hours breakdown */}
Uren per jaar (écht inzetbaar):
{declarableHoursPerYear.toFixed(1)} uur per jaar
≈ {hoursPerMonth.toFixed(1)} uur per maand
≈ {hoursPerWeekCalculated.toFixed(2)} uur per week
≈ {minutesPerWeek.toFixed(0)} minuten per week
Berekening: {HOURS_PER_WEEK_DISPLAY} uur/week × {WORK_WEEKS_PER_YEAR_DISPLAY} weken × {effectiveFte.toFixed(2)} FTE × {DECLARABLE_PERCENTAGE_DISPLAY * 100}% = {declarableHoursPerYear.toFixed(1)} uur/jaar
(Netto: {netHoursPerYear.toFixed(1)} uur/jaar, waarvan {DECLARABLE_PERCENTAGE_DISPLAY * 100}% declarabel)
)}
); } export default EffortDisplay;