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> {
|
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);
|
return jiraAssetsService.getTeamDashboardData(excludedStatuses);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user