Load team dashboard data from database cache instead of API

- Replace API-based getTeamDashboardData with database-backed implementation
- Load all ApplicationComponents from normalized cache store
- Reuse existing grouping and KPI calculation logic
- Significantly faster as it avoids hundreds of API calls
- Falls back to API if database query fails
This commit is contained in:
2026-01-21 13:36:00 +01:00
parent e1ad0d9aa7
commit 3c11402e6b

View File

@@ -1283,8 +1283,342 @@ export const dataService = {
},
async getTeamDashboardData(excludedStatuses: ApplicationStatus[] = []): Promise<TeamDashboardData> {
// Always get from Jira Assets API (has proper Team/Subteam field parsing)
// Load from database cache instead of API for better performance
logger.info(`Loading team dashboard data from database cache (excluding: ${excludedStatuses.join(', ')})`);
try {
// Get all ApplicationComponents from database cache
const allApplications = await cmdbService.getObjects<ApplicationComponent>('ApplicationComponent');
logger.info(`Loaded ${allApplications.length} applications from database cache`);
// Convert to ApplicationListItem
const applicationListItems = await Promise.all(
allApplications.map(app => toApplicationListItem(app))
);
// Filter out excluded statuses
const filteredApplications = excludedStatuses.length > 0
? applicationListItems.filter(app => !app.status || !excludedStatuses.includes(app.status))
: applicationListItems;
logger.info(`After status filter: ${filteredApplications.length} applications (excluded: ${excludedStatuses.join(', ')})`);
// Separate into Platforms, Workloads, and regular applications
const platforms: ApplicationListItem[] = [];
const workloads: ApplicationListItem[] = [];
const regularApplications: ApplicationListItem[] = [];
for (const app of filteredApplications) {
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);
}
}
logger.info(`Identified ${platforms.length} platforms, ${workloads.length} workloads, ${regularApplications.length} regular applications`);
// 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);
}
// Helper functions for FTE calculations
const getEffectiveFTE = (app: ApplicationListItem): number => {
return app.overrideFTE !== null && app.overrideFTE !== undefined
? app.overrideFTE
: (app.requiredEffortApplicationManagement || 0);
};
const getMinFTE = (app: ApplicationListItem): number => {
if (app.overrideFTE !== null && app.overrideFTE !== undefined) {
return app.overrideFTE;
}
return app.minFTE ?? app.requiredEffortApplicationManagement ?? 0;
};
const getMaxFTE = (app: ApplicationListItem): number => {
if (app.overrideFTE !== null && app.overrideFTE !== undefined) {
return app.overrideFTE;
}
return app.maxFTE ?? app.requiredEffortApplicationManagement ?? 0;
};
// Build PlatformWithWorkloads structures
const platformsWithWorkloads: PlatformWithWorkloads[] = [];
for (const platform of platforms) {
const platformWorkloads = workloadsByPlatform.get(platform.id) || [];
const platformEffort = getEffectiveFTE(platform);
const workloadsEffort = platformWorkloads.reduce((sum, w) => sum + getEffectiveFTE(w), 0);
platformsWithWorkloads.push({
platform,
workloads: platformWorkloads,
platformEffort,
workloadsEffort,
totalEffort: platformEffort + workloadsEffort,
});
}
// Helper function to calculate subteam KPIs
const calculateSubteamKPIs = (
regularApps: ApplicationListItem[],
platformsList: PlatformWithWorkloads[]
) => {
const regularEffort = regularApps.reduce((sum, app) => sum + getEffectiveFTE(app), 0);
const platformsEffort = platformsList.reduce((sum, p) => sum + p.totalEffort, 0);
const totalEffort = regularEffort + platformsEffort;
const regularMinEffort = regularApps.reduce((sum, app) => sum + getMinFTE(app), 0);
const regularMaxEffort = regularApps.reduce((sum, app) => sum + getMaxFTE(app), 0);
const platformsMinEffort = platformsList.reduce((sum, p) => {
const platformMin = getMinFTE(p.platform);
const workloadsMin = p.workloads.reduce((s, w) => s + getMinFTE(w), 0);
return sum + platformMin + workloadsMin;
}, 0);
const platformsMaxEffort = platformsList.reduce((sum, p) => {
const platformMax = getMaxFTE(p.platform);
const workloadsMax = p.workloads.reduce((s, w) => s + getMaxFTE(w), 0);
return sum + platformMax + workloadsMax;
}, 0);
const minEffort = regularMinEffort + platformsMinEffort;
const maxEffort = regularMaxEffort + platformsMaxEffort;
const platformsCount = platformsList.length;
const workloadsCount = platformsList.reduce((sum, p) => sum + p.workloads.length, 0);
const applicationCount = regularApps.length + platformsCount + workloadsCount;
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 pwl of platformsList) {
const govModel = pwl.platform.governanceModel?.name || 'Niet ingesteld';
byGovernanceModel[govModel] = (byGovernanceModel[govModel] || 0) + 1;
for (const workload of pwl.workloads) {
const workloadGovModel = workload.governanceModel?.name || 'Niet ingesteld';
byGovernanceModel[workloadGovModel] = (byGovernanceModel[workloadGovModel] || 0) + 1;
}
}
return { totalEffort, minEffort, maxEffort, applicationCount, byGovernanceModel };
};
// HIERARCHICAL GROUPING: Team -> Subteam -> Applications
type SubteamData = {
subteam: ReferenceValue | null;
regular: ApplicationListItem[];
platforms: PlatformWithWorkloads[];
};
type TeamData = {
team: ReferenceValue | null;
subteams: Map<string, SubteamData>;
};
// Load Subteam -> Team mapping (still from API but cached, so should be fast)
const subteamToTeamMap = await jiraAssetsService.getSubteamToTeamMapping();
logger.info(`Loaded ${subteamToTeamMap.size} subteam->team mappings`);
const teamMap = new Map<string, TeamData>();
const unassignedData: SubteamData = {
subteam: null,
regular: [],
platforms: [],
};
// Helper to get Team from Subteam
const getTeamForSubteam = (subteam: ReferenceValue | null): ReferenceValue | null => {
if (!subteam) return null;
return subteamToTeamMap.get(subteam.objectId) || null;
};
// Group regular applications by Team -> Subteam
for (const app of regularApplications) {
const subteam = app.applicationSubteam;
const team = getTeamForSubteam(subteam);
if (team) {
const teamId = team.objectId;
if (!teamMap.has(teamId)) {
teamMap.set(teamId, { team, subteams: new Map() });
}
const teamData = teamMap.get(teamId)!;
const subteamId = subteam?.objectId || 'no-subteam';
if (!teamData.subteams.has(subteamId)) {
teamData.subteams.set(subteamId, {
subteam: subteam || null,
regular: [],
platforms: [],
});
}
teamData.subteams.get(subteamId)!.regular.push(app);
} else if (subteam) {
// Has subteam but no team - put under a virtual "Geen Team" team
const noTeamId = 'no-team';
if (!teamMap.has(noTeamId)) {
teamMap.set(noTeamId, {
team: { objectId: noTeamId, key: 'NO-TEAM', name: 'Geen Team' },
subteams: new Map()
});
}
const teamData = teamMap.get(noTeamId)!;
const subteamId = subteam.objectId;
if (!teamData.subteams.has(subteamId)) {
teamData.subteams.set(subteamId, {
subteam: subteam,
regular: [],
platforms: [],
});
}
teamData.subteams.get(subteamId)!.regular.push(app);
} else {
// No subteam assigned - goes to unassigned
unassignedData.regular.push(app);
}
}
// Group platforms by Team -> Subteam
for (const pwl of platformsWithWorkloads) {
const platform = pwl.platform;
const subteam = platform.applicationSubteam;
const team = getTeamForSubteam(subteam);
if (team) {
const teamId = team.objectId;
if (!teamMap.has(teamId)) {
teamMap.set(teamId, { team, subteams: new Map() });
}
const teamData = teamMap.get(teamId)!;
const subteamId = subteam?.objectId || 'no-subteam';
if (!teamData.subteams.has(subteamId)) {
teamData.subteams.set(subteamId, {
subteam: subteam || null,
regular: [],
platforms: [],
});
}
teamData.subteams.get(subteamId)!.platforms.push(pwl);
} else if (subteam) {
// Has subteam but no team - put under "Geen Team"
const noTeamId = 'no-team';
if (!teamMap.has(noTeamId)) {
teamMap.set(noTeamId, {
team: { objectId: noTeamId, key: 'NO-TEAM', name: 'Geen Team' },
subteams: new Map()
});
}
const teamData = teamMap.get(noTeamId)!;
const subteamId = subteam.objectId;
if (!teamData.subteams.has(subteamId)) {
teamData.subteams.set(subteamId, {
subteam: subteam,
regular: [],
platforms: [],
});
}
teamData.subteams.get(subteamId)!.platforms.push(pwl);
} else {
// No subteam assigned - goes to unassigned
unassignedData.platforms.push(pwl);
}
}
logger.info(`Team dashboard: grouped ${regularApplications.length} regular apps and ${platformsWithWorkloads.length} platforms into ${teamMap.size} teams`);
// Build the hierarchical result structure
const teams: TeamDashboardTeam[] = [];
// Process teams in alphabetical order
const sortedTeamIds = Array.from(teamMap.keys()).sort((a, b) => {
const teamA = teamMap.get(a)!.team?.name || '';
const teamB = teamMap.get(b)!.team?.name || '';
return teamA.localeCompare(teamB, 'nl', { sensitivity: 'base' });
});
for (const teamId of sortedTeamIds) {
const teamData = teamMap.get(teamId)!;
const fullTeam = teamData.team;
const subteams: TeamDashboardSubteam[] = [];
// Sort subteams alphabetically (with "no-subteam" at the end)
const sortedSubteamEntries = Array.from(teamData.subteams.entries()).sort((a, b) => {
if (a[0] === 'no-subteam') return 1;
if (b[0] === 'no-subteam') return -1;
const nameA = a[1].subteam?.name || '';
const nameB = b[1].subteam?.name || '';
return nameA.localeCompare(nameB, 'nl', { sensitivity: 'base' });
});
for (const [subteamId, subteamData] of sortedSubteamEntries) {
const kpis = calculateSubteamKPIs(subteamData.regular, subteamData.platforms);
subteams.push({
subteam: subteamData.subteam,
applications: subteamData.regular,
platforms: subteamData.platforms,
...kpis,
});
}
// Aggregate team KPIs from all subteams
const teamTotalEffort = subteams.reduce((sum, s) => sum + s.totalEffort, 0);
const teamMinEffort = subteams.reduce((sum, s) => sum + s.minEffort, 0);
const teamMaxEffort = subteams.reduce((sum, s) => sum + s.maxEffort, 0);
const teamApplicationCount = subteams.reduce((sum, s) => sum + s.applicationCount, 0);
const teamByGovernanceModel: Record<string, number> = {};
for (const subteam of subteams) {
for (const [model, count] of Object.entries(subteam.byGovernanceModel)) {
teamByGovernanceModel[model] = (teamByGovernanceModel[model] || 0) + count;
}
}
teams.push({
team: fullTeam,
subteams,
totalEffort: teamTotalEffort,
minEffort: teamMinEffort,
maxEffort: teamMaxEffort,
applicationCount: teamApplicationCount,
byGovernanceModel: teamByGovernanceModel,
});
}
// Calculate unassigned KPIs
const unassignedKPIs = calculateSubteamKPIs(unassignedData.regular, unassignedData.platforms);
const result: TeamDashboardData = {
teams,
unassigned: {
subteam: null,
applications: unassignedData.regular,
platforms: unassignedData.platforms,
...unassignedKPIs,
},
};
logger.info(`Team dashboard data loaded from database: ${teams.length} teams, ${unassignedData.regular.length + unassignedData.platforms.length} unassigned apps`);
return result;
} catch (error) {
logger.error('Failed to get team dashboard data from database', error);
// Fallback to API if database fails
logger.warn('Falling back to API for team dashboard data');
return jiraAssetsService.getTeamDashboardData(excludedStatuses);
}
},
/**