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:
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user