import { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { getTeamDashboardData, getReferenceData } from '../services/api'; import type { TeamDashboardData, TeamDashboardCluster, ApplicationStatus, ReferenceValue } from '../types'; const ALL_STATUSES: ApplicationStatus[] = [ 'In Production', 'Implementation', 'Proof of Concept', 'End of support', 'End of life', 'Deprecated', 'Shadow IT', 'Closed', 'Undefined', ]; type SortOption = 'alphabetical' | 'fte-descending'; // Color scheme for governance models - matches exact names from Jira Assets const GOVERNANCE_MODEL_COLORS: Record = { 'Regiemodel A': { bg: '#20556B', text: '#FFFFFF', letter: 'A' }, 'Regiemodel B': { bg: '#286B86', text: '#FFFFFF', letter: 'B' }, 'Regiemodel B+': { bg: '#286B86', text: '#FFFFFF', letter: 'B+' }, 'Regiemodel C': { bg: '#81CBF2', text: '#20556B', letter: 'C' }, 'Regiemodel D': { bg: '#F5A733', text: '#FFFFFF', letter: 'D' }, 'Regiemodel E': { bg: '#E95053', text: '#FFFFFF', letter: 'E' }, 'Niet ingesteld': { bg: '#EEEEEE', text: '#AAAAAA', letter: '?' }, }; // Get governance model colors and letter - with fallback for unknown models const getGovernanceModelStyle = (governanceModelName: string | null | undefined) => { const name = governanceModelName || 'Niet ingesteld'; // First try exact match if (GOVERNANCE_MODEL_COLORS[name]) { return GOVERNANCE_MODEL_COLORS[name]; } // Try to match by pattern (e.g., "Regiemodel X" -> letter X) const match = name.match(/Regiemodel\s+(.+)/i); if (match) { const letter = match[1]; // Return a color based on the letter if (letter === 'A') return { bg: '#20556B', text: '#FFFFFF', letter: 'A' }; if (letter === 'B') return { bg: '#286B86', text: '#FFFFFF', letter: 'B' }; if (letter === 'B+') return { bg: '#286B86', text: '#FFFFFF', letter: 'B+' }; if (letter === 'C') return { bg: '#81CBF2', text: '#20556B', letter: 'C' }; if (letter === 'D') return { bg: '#F5A733', text: '#FFFFFF', letter: 'D' }; if (letter === 'E') return { bg: '#E95053', text: '#FFFFFF', letter: 'E' }; return { bg: '#6B7280', text: '#FFFFFF', letter }; } return { bg: '#6B7280', text: '#FFFFFF', letter: '?' }; }; export default function TeamDashboard() { const [data, setData] = useState(null); const [initialLoading, setInitialLoading] = useState(true); // Only for first load const [dataLoading, setDataLoading] = useState(false); // For filter changes const [error, setError] = useState(null); const [expandedClusters, setExpandedClusters] = useState>(new Set()); // Start with all clusters collapsed const [expandedPlatforms, setExpandedPlatforms] = useState>(new Set()); // Track expanded platforms // Status filter: excludedStatuses contains statuses that are NOT shown const [excludedStatuses, setExcludedStatuses] = useState(['Closed', 'Deprecated']); // Default: exclude Closed and Deprecated const [sortOption, setSortOption] = useState('fte-descending'); const [statusDropdownOpen, setStatusDropdownOpen] = useState(false); const [governanceModels, setGovernanceModels] = useState([]); const [hoveredGovModel, setHoveredGovModel] = useState(null); // Fetch governance models on mount useEffect(() => { async function fetchGovernanceModels() { try { const refData = await getReferenceData(); setGovernanceModels(refData.governanceModels); } catch (err) { console.error('Failed to fetch governance models:', err); } } fetchGovernanceModels(); }, []); useEffect(() => { async function fetchData() { try { // Only show full page loading on initial load const isInitialLoad = data === null; if (isInitialLoad) { setInitialLoading(true); } else { setDataLoading(true); } const dashboardData = await getTeamDashboardData(excludedStatuses); setData(dashboardData); // Keep clusters collapsed by default (expandedClusters remains empty) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load team dashboard'); } finally { setInitialLoading(false); setDataLoading(false); } } fetchData(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [excludedStatuses]); // Close status dropdown when pressing Escape useEffect(() => { const handleEscape = (event: KeyboardEvent) => { if (event.key === 'Escape' && statusDropdownOpen) { setStatusDropdownOpen(false); } }; if (statusDropdownOpen) { document.addEventListener('keydown', handleEscape); } return () => { document.removeEventListener('keydown', handleEscape); }; }, [statusDropdownOpen]); const toggleCluster = (clusterId: string, event?: React.MouseEvent) => { // Prevent scroll jump by storing and restoring scroll position const scrollY = window.scrollY; setExpandedClusters(prev => { const newSet = new Set(prev); if (newSet.has(clusterId)) { newSet.delete(clusterId); } else { newSet.add(clusterId); } return newSet; }); // Use requestAnimationFrame to restore scroll position after state update requestAnimationFrame(() => { window.scrollTo(0, scrollY); }); }; const togglePlatform = (platformId: string, e?: React.MouseEvent) => { if (e) { e.preventDefault(); e.stopPropagation(); } setExpandedPlatforms(prev => { const newSet = new Set(prev); if (newSet.has(platformId)) { newSet.delete(platformId); } else { newSet.add(platformId); } return newSet; }); }; const toggleStatus = (status: ApplicationStatus) => { setExcludedStatuses(prev => { if (prev.includes(status)) { // Remove from excluded (show it) return prev.filter(s => s !== status); } else { // Add to excluded (hide it) return [...prev, status]; } }); }; // Only show full page loading on initial load if (initialLoading) { return (
); } const hasNoApplications = data ? ( data.clusters.length === 0 && data.unassigned.applications.length === 0 && data.unassigned.platforms.length === 0 ) : true; const ClusterBlock = ({ clusterData, isUnassigned = false }: { clusterData: TeamDashboardCluster; isUnassigned?: boolean }) => { const clusterId = clusterData.cluster?.objectId || 'unassigned'; const isExpanded = expandedClusters.has(clusterId); const clusterName = isUnassigned ? 'Nog niet toegekend' : (clusterData.cluster?.name || 'Onbekend'); // Helper function to get effective FTE for an application const getEffectiveFTE = (app: { overrideFTE?: number | null; requiredEffortApplicationManagement?: number | null }) => app.overrideFTE !== null && app.overrideFTE !== undefined ? app.overrideFTE : (app.requiredEffortApplicationManagement || 0); // Use pre-calculated min/max from backend (sum of all min/max FTE values) const minFTE = clusterData.minEffort ?? 0; const maxFTE = clusterData.maxEffort ?? 0; // Calculate application type distribution const byApplicationType: Record = {}; clusterData.applications.forEach(app => { const appType = app.applicationType?.name || 'Niet ingesteld'; byApplicationType[appType] = (byApplicationType[appType] || 0) + 1; }); clusterData.platforms.forEach(platformWithWorkloads => { const platformType = platformWithWorkloads.platform.applicationType?.name || 'Niet ingesteld'; byApplicationType[platformType] = (byApplicationType[platformType] || 0) + 1; platformWithWorkloads.workloads.forEach(workload => { const workloadType = workload.applicationType?.name || 'Niet ingesteld'; byApplicationType[workloadType] = (byApplicationType[workloadType] || 0) + 1; }); }); // Sort applications based on selected sort option const sortedApplications = [...clusterData.applications].sort((a, b) => { if (sortOption === 'alphabetical') { return a.name.localeCompare(b.name, 'nl', { sensitivity: 'base' }); } else { // Sort by FTE descending (use override if present, otherwise calculated) const aFTE = getEffectiveFTE(a); const bFTE = getEffectiveFTE(b); return bFTE - aFTE; } }); // Sort platforms based on selected sort option const sortedPlatforms = [...clusterData.platforms].sort((a, b) => { if (sortOption === 'alphabetical') { return a.platform.name.localeCompare(b.platform.name, 'nl', { sensitivity: 'base' }); } else { // Sort by total FTE descending return b.totalEffort - a.totalEffort; } }); return (
{isExpanded && (
{clusterData.applications.length === 0 && clusterData.platforms.length === 0 ? (

Geen applicaties in dit cluster

) : (
{/* Platforms with Workloads - shown first */} {sortedPlatforms.map((platformWithWorkloads) => { const platformId = platformWithWorkloads.platform.id; const isPlatformExpanded = expandedPlatforms.has(platformId); const hasWorkloads = platformWithWorkloads.workloads.length > 0; const platformGovStyle = getGovernanceModelStyle(platformWithWorkloads.platform.governanceModel?.name); const platform = platformWithWorkloads.platform; const platformMinFTE = platform.overrideFTE !== null && platform.overrideFTE !== undefined ? platform.overrideFTE : (platform.minFTE ?? platform.requiredEffortApplicationManagement ?? 0); const platformMaxFTE = platform.overrideFTE !== null && platform.overrideFTE !== undefined ? platform.overrideFTE : (platform.maxFTE ?? platform.requiredEffortApplicationManagement ?? 0); // Calculate total min/max including workloads const totalMinFTE = platformMinFTE + platformWithWorkloads.workloads.reduce((sum, w) => { return sum + (w.overrideFTE ?? w.minFTE ?? w.requiredEffortApplicationManagement ?? 0); }, 0); const totalMaxFTE = platformMaxFTE + platformWithWorkloads.workloads.reduce((sum, w) => { return sum + (w.overrideFTE ?? w.maxFTE ?? w.requiredEffortApplicationManagement ?? 0); }, 0); return (
{/* Governance Model indicator */}
{platformGovStyle.letter}
{/* Platform header */}
{hasWorkloads && ( )}
{platformWithWorkloads.platform.name}
Platform {platform.applicationManagementHosting?.name && ( {platform.applicationManagementHosting.name} )}
{platformWithWorkloads.platform.key}
{(() => { const platformHasOverride = platform.overrideFTE !== null && platform.overrideFTE !== undefined; const platformCalculated = platform.requiredEffortApplicationManagement || 0; const workloadsCalculated = platformWithWorkloads.workloads.reduce((sum, w) => sum + (w.requiredEffortApplicationManagement || 0), 0 ); const totalCalculated = platformCalculated + workloadsCalculated; const hasAnyOverride = platformHasOverride || platformWithWorkloads.workloads.some(w => w.overrideFTE !== null && w.overrideFTE !== undefined ); return ( <>
{platformWithWorkloads.totalEffort.toFixed(2)} FTE
{totalMinFTE.toFixed(2)} - {totalMaxFTE.toFixed(2)}
{hasAnyOverride && (
(berekend: {totalCalculated.toFixed(2)})
)}
Platform: {platformWithWorkloads.platformEffort.toFixed(2)} FTE {platformHasOverride && platformCalculated !== null && ( (berekend: {platformCalculated.toFixed(2)}) )} {hasWorkloads && ( <> + Workloads: {platformWithWorkloads.workloadsEffort.toFixed(2)} FTE )}
); })()}
{/* Workloads list */} {hasWorkloads && isPlatformExpanded && (
Workloads ({platformWithWorkloads.workloads.length})
{[...platformWithWorkloads.workloads] .sort((a, b) => { if (sortOption === 'alphabetical') { return a.name.localeCompare(b.name, 'nl', { sensitivity: 'base' }); } else { // Sort by FTE descending (use override if present, otherwise calculated) const wlEffectiveFTE = (wl: typeof a) => wl.overrideFTE !== null && wl.overrideFTE !== undefined ? wl.overrideFTE : (wl.requiredEffortApplicationManagement || 0); const aFTE = wlEffectiveFTE(a); const bFTE = wlEffectiveFTE(b); return bFTE - aFTE; } }) .map((workload) => { const workloadGovStyle = getGovernanceModelStyle(workload.governanceModel?.name); const workloadType = workload.applicationType?.name || 'Workload'; const workloadHosting = workload.applicationManagementHosting?.name; const workloadEffectiveFTE = workload.overrideFTE !== null && workload.overrideFTE !== undefined ? workload.overrideFTE : workload.requiredEffortApplicationManagement; const workloadMinFTE = workload.overrideFTE ?? workload.minFTE ?? workload.requiredEffortApplicationManagement ?? 0; const workloadMaxFTE = workload.overrideFTE ?? workload.maxFTE ?? workload.requiredEffortApplicationManagement ?? 0; return (
{/* Governance Model indicator for workload */}
{workloadGovStyle.letter}
{workload.name} {workloadType} {workloadHosting && ( {workloadHosting} )}
{workload.key}
{workloadEffectiveFTE !== null && workloadEffectiveFTE !== undefined ? (
{workloadEffectiveFTE.toFixed(2)} FTE
{workloadMinFTE.toFixed(2)} - {workloadMaxFTE.toFixed(2)}
) : (
Niet berekend
)}
); })}
)}
); })} {/* Regular applications - shown after platforms */} {sortedApplications.map((app) => { const govStyle = getGovernanceModelStyle(app.governanceModel?.name); const appType = app.applicationType?.name || 'Niet ingesteld'; const appHosting = app.applicationManagementHosting?.name; const effectiveFTE = app.overrideFTE !== null && app.overrideFTE !== undefined ? app.overrideFTE : app.requiredEffortApplicationManagement; const appMinFTE = app.overrideFTE !== null && app.overrideFTE !== undefined ? app.overrideFTE : (app.minFTE ?? app.requiredEffortApplicationManagement ?? 0); const appMaxFTE = app.overrideFTE !== null && app.overrideFTE !== undefined ? app.overrideFTE : (app.maxFTE ?? app.requiredEffortApplicationManagement ?? 0); return ( {/* Governance Model indicator */}
{govStyle.letter}
{app.name} {appType} {appHosting && ( {appHosting} )}
{app.key}
{effectiveFTE !== null && effectiveFTE !== undefined ? (
{effectiveFTE.toFixed(2)} FTE
{appMinFTE.toFixed(2)} - {appMaxFTE.toFixed(2)}
) : (
Niet berekend
)}
); })}
)}
)}
); }; return (

Team-indeling

Overzicht van applicaties gegroepeerd per Application Cluster

{/* Compact Filter Bar */}
{/* Sort Option */}
{/* Status Filter Dropdown */}
{statusDropdownOpen && ( <>
setStatusDropdownOpen(false)} />
Selecteer statussen
{ALL_STATUSES.map((status) => { const isExcluded = excludedStatuses.includes(status); return ( ); })}

Uitgevinkte statussen worden verborgen

)}
{/* Error message */} {error && (
{error}
)} {/* Loading indicator for data updates */} {dataLoading && (
Resultaten bijwerken...
)} {/* Clusters */} {!dataLoading && data && data.clusters.length > 0 && (
{data.clusters.map((clusterData) => ( ))}
)} {/* Unassigned applications */} {!dataLoading && data && (data.unassigned.applications.length > 0 || data.unassigned.platforms.length > 0) && (

Deze applicaties zijn nog niet toegekend aan een cluster.

)} {!dataLoading && data && hasNoApplications && (

Geen applicaties gevonden

)}
); }