|
| 1 | +import type { TeamMetrics, ActivityStatus } from './types'; |
| 2 | + |
| 3 | +/** |
| 4 | + * Member data for aggregation |
| 5 | + */ |
| 6 | +interface MemberData { |
| 7 | + github_handle: string; |
| 8 | + name: string; |
| 9 | + workspace_count: number; |
| 10 | + last_active: string; |
| 11 | + activity_status: ActivityStatus; |
| 12 | +} |
| 13 | + |
| 14 | +/** |
| 15 | + * Extract company name from team name |
| 16 | + * |
| 17 | + * @param teamName - Full team name (e.g., "scotiabank-2-tangerine", "bell-1") |
| 18 | + * @returns Company name (e.g., "scotiabank", "bell") |
| 19 | + * |
| 20 | + * @example |
| 21 | + * extractCompanyName("scotiabank-2-tangerine") // "scotiabank" |
| 22 | + * extractCompanyName("bell-1") // "bell" |
| 23 | + * extractCompanyName("hitachi-rail-1") // "hitachi-rail" |
| 24 | + * extractCompanyName("facilitators") // "facilitators" |
| 25 | + */ |
| 26 | +export function extractCompanyName(teamName: string): string { |
| 27 | + // Special cases: teams without numeric suffix |
| 28 | + if (teamName === 'facilitators' || teamName === 'Unassigned') { |
| 29 | + return teamName; |
| 30 | + } |
| 31 | + |
| 32 | + // Extract prefix before first "-{digit}" pattern |
| 33 | + // Handles: "scotiabank-2-tangerine", "bell-1", "hitachi-rail-1" |
| 34 | + const match = teamName.match(/^([a-z-]+?)-(\d+)/); |
| 35 | + return match ? match[1] : teamName; |
| 36 | +} |
| 37 | + |
| 38 | +/** |
| 39 | + * Aggregate teams by company |
| 40 | + * Groups individual teams (e.g., "bell-1", "bell-2") into company-level aggregates (e.g., "bell") |
| 41 | + * Follows pattern from lib/metrics.ts:aggregateByTeam() |
| 42 | + * |
| 43 | + * @param teams - Array of individual team metrics with workspaces_for_template count |
| 44 | + * @returns Array of company-aggregated metrics |
| 45 | + * |
| 46 | + * Aggregation rules: |
| 47 | + * - Sum numeric metrics (workspaces, hours, active_days) |
| 48 | + * - Deduplicate members by github_handle, keeping most recent activity |
| 49 | + * - Sum workspace_count across team instances for each member |
| 50 | + * - Merge template distributions |
| 51 | + * - Calculate active_days as union of all dates (not sum) |
| 52 | + */ |
| 53 | +export function aggregateByCompany( |
| 54 | + teams: (TeamMetrics & { workspaces_for_template: number })[] |
| 55 | +): (TeamMetrics & { workspaces_for_template: number })[] { |
| 56 | + // Group teams by company using Map |
| 57 | + const companyMap = new Map<string, (TeamMetrics & { workspaces_for_template: number })[]>(); |
| 58 | + |
| 59 | + teams.forEach(team => { |
| 60 | + const company = extractCompanyName(team.team_name); |
| 61 | + const companyTeams = companyMap.get(company) || []; |
| 62 | + companyTeams.push(team); |
| 63 | + companyMap.set(company, companyTeams); |
| 64 | + }); |
| 65 | + |
| 66 | + // Aggregate each company |
| 67 | + return Array.from(companyMap.entries()).map(([companyName, companyTeams]) => { |
| 68 | + // Sum numeric metrics |
| 69 | + const total_workspaces = companyTeams.reduce((sum, t) => sum + t.total_workspaces, 0); |
| 70 | + const total_workspace_hours = companyTeams.reduce((sum, t) => sum + t.total_workspace_hours, 0); |
| 71 | + const total_active_hours = companyTeams.reduce((sum, t) => sum + t.total_active_hours, 0); |
| 72 | + const workspaces_for_template = companyTeams.reduce((sum, t) => sum + t.workspaces_for_template, 0); |
| 73 | + const avg_workspace_hours = total_workspaces > 0 ? total_workspace_hours / total_workspaces : 0; |
| 74 | + |
| 75 | + // Deduplicate members by github_handle, keep most recent activity |
| 76 | + const memberMap = new Map<string, MemberData>(); |
| 77 | + companyTeams.forEach(team => { |
| 78 | + team.members.forEach(member => { |
| 79 | + const existing = memberMap.get(member.github_handle); |
| 80 | + if (!existing || new Date(member.last_active) > new Date(existing.last_active)) { |
| 81 | + // Keep most recent, but sum workspace_count |
| 82 | + memberMap.set(member.github_handle, { |
| 83 | + ...member, |
| 84 | + workspace_count: (existing?.workspace_count || 0) + member.workspace_count |
| 85 | + }); |
| 86 | + } |
| 87 | + }); |
| 88 | + }); |
| 89 | + |
| 90 | + const members = Array.from(memberMap.values()) |
| 91 | + .sort((a, b) => new Date(b.last_active).getTime() - new Date(a.last_active).getTime()); |
| 92 | + |
| 93 | + // Count unique active users |
| 94 | + const unique_active_users = members.filter(m => m.activity_status === 'active').length; |
| 95 | + |
| 96 | + // Merge template distributions |
| 97 | + const template_distribution: Record<string, number> = {}; |
| 98 | + companyTeams.forEach(team => { |
| 99 | + Object.entries(team.template_distribution).forEach(([template, count]) => { |
| 100 | + template_distribution[template] = (template_distribution[template] || 0) + count; |
| 101 | + }); |
| 102 | + }); |
| 103 | + |
| 104 | + // Calculate active days as union of all dates |
| 105 | + const activeDates = new Set<string>(); |
| 106 | + companyTeams.forEach(team => { |
| 107 | + team.members.forEach(member => { |
| 108 | + const date = new Date(member.last_active).toISOString().split('T')[0]; |
| 109 | + activeDates.add(date); |
| 110 | + }); |
| 111 | + }); |
| 112 | + |
| 113 | + return { |
| 114 | + team_name: companyName, |
| 115 | + total_workspaces, |
| 116 | + unique_active_users, |
| 117 | + total_workspace_hours: Math.round(total_workspace_hours), |
| 118 | + total_active_hours: Math.round(total_active_hours), |
| 119 | + avg_workspace_hours: Math.round(avg_workspace_hours * 10) / 10, |
| 120 | + active_days: activeDates.size, |
| 121 | + workspaces_for_template, |
| 122 | + template_distribution, |
| 123 | + members |
| 124 | + }; |
| 125 | + }); |
| 126 | +} |
0 commit comments