Skip to content

Commit 87de0f2

Browse files
authored
feat(dashboards): integrate snowflake analytics for board member metrics (#140)
* feat(dashboards): integrate snowflake analytics for board member metrics - Add 5 new API endpoints for organization-level analytics data - Integrate real Snowflake data into organization involvement metrics - Refactor board member dashboard to use reactive form pattern - Add support for projects participating, total commits, certified employees, board meeting attendance, and event sponsorships metrics - Update organization involvement component to display live analytics data - Add multi-currency formatting for event sponsorships - Create TypeScript interfaces in shared package for type safety LFXV2-701 Signed-off-by: Asitha de Silva <asithade@gmail.com> * refactor(dashboards): extract defaults and fix currency crash - Extract hardcoded accountId, projectId, and segmentId to shared constants - Create ANALYTICS_DEFAULTS constant in shared package for temporary values - Replace 16 inline hardcoded values in analytics controller with constants - Fix InvalidPipeArgument crash when Snowflake returns null/empty currency codes - Add filtering in transformEventSponsorship to handle imperfect data gracefully - Dashboard now resilient to legacy/draft sponsorship records with missing currency LFXV2-701 Signed-off-by: Asitha de Silva <asithade@gmail.com> * refactor(analytics): extract service layer from controller Refactored analytics.controller.ts to follow the established Controller → Service → Data Source pattern used by other controllers in the codebase. Changes: - Moved 4 user analytics methods to UserService (active weeks streak, pull requests, code commits, projects) - Moved 10 organization analytics methods to OrganizationService (maintainers, membership tier, contributors, events, technical committee, certifications, board meetings, sponsorships) - Removed inline SQL queries and business logic from analytics.controller.ts - Controller now only handles HTTP concerns (validation, logging, response) - Reduced controller complexity from 930 lines to ~400 lines - Removed all unused interface imports from controller LFXV2-701 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Asitha de Silva <asithade@gmail.com> * refactor(analytics): consolidate endpoints and remove unused code Consolidate 15 individual analytics endpoints into 4 optimized endpoints: - getOrganizationContributionsOverview (maintainers + contributors + technical committee) - getOrganizationSegmentOverview (projects participating + total commits) - getBoardMemberDashboard (membership tier + certified employees + board attendance) - getOrganizationEventsOverview (event attendance + event sponsorships) Backend changes: - Inline getEventAttendance and getEventSponsorships into getEventsOverview - Remove 10 legacy endpoint handlers from analytics controller - Rename private service methods for consistency (remove "Consolidated" suffix) - Clean up JSDoc comments and simplify method documentation Frontend changes: - Remove 10 unused HTTP methods from analytics service - Update organization-involvement component to use consolidated endpoints Shared package changes: - Remove 21 unused interfaces from analytics-data.interface.ts (~450 lines) - Clean up legacy interfaces no longer used after consolidation This refactor reduces codebase by ~602 lines while improving API efficiency through consolidated database queries and parallel execution patterns. Generated with [Claude Code](https://claude.ai/code) Refs: LFXV2-701 Signed-off-by: Asitha de Silva <asithade@gmail.com> * refactor(dashboards): fix partial data 404s and remove annual fee LFXV2-701 - Refactor SQL queries to use CTE base with LEFT JOINs instead of anchoring on specific tables - This fixes 404 errors when organizations have partial data (e.g., contributors but no maintainers) - Remove MEMBERSHIP_PRICE/annual fee field from backend, shared interfaces, and frontend - Update membership tier card layout to show three clear rows: Tier, Member Since, and Renewal Date Signed-off-by: Asitha de Silva <asithade@gmail.com> --------- Signed-off-by: Asitha de Silva <asithade@gmail.com>
1 parent 41a38b5 commit 87de0f2

16 files changed

Lines changed: 1173 additions & 812 deletions

apps/lfx-one/src/app/modules/dashboards/board-member/board-member-dashboard.component.html

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<div class="mb-6 flex items-center gap-4" data-testid="organization-selector">
77
<label for="organization-select" class="text-sm font-semibold text-gray-700">Organization:</label>
88
<lfx-select
9-
[form]="accountForm"
9+
[form]="form"
1010
control="selectedAccountId"
1111
[options]="availableAccounts()"
1212
optionLabel="accountName"
@@ -17,8 +17,7 @@
1717
[showClear]="false"
1818
styleClass="min-w-[300px]"
1919
inputId="organization-select"
20-
data-testid="organization-select"
21-
(onChange)="handleAccountChange($event)" />
20+
data-testid="organization-select" />
2221
</div>
2322

2423
<!-- Dashboard Sections -->

apps/lfx-one/src/app/modules/dashboards/board-member/board-member-dashboard.component.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
// SPDX-License-Identifier: MIT
33

44
import { Component, computed, inject, Signal } from '@angular/core';
5+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
56
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
67
import { Account } from '@lfx-one/shared/interfaces';
8+
79
import { SelectComponent } from '../../../shared/components/select/select.component';
810
import { AccountContextService } from '../../../shared/services/account-context.service';
911
import { FoundationHealthComponent } from '../components/foundation-health/foundation-health.component';
@@ -20,20 +22,21 @@ import { PendingActionsComponent } from '../components/pending-actions/pending-a
2022
export class BoardMemberDashboardComponent {
2123
private readonly accountContextService = inject(AccountContextService);
2224

23-
protected readonly accountForm = new FormGroup({
25+
public readonly form = new FormGroup({
2426
selectedAccountId: new FormControl<string>(this.accountContextService.selectedAccount().accountId),
2527
});
2628

27-
protected readonly availableAccounts: Signal<Account[]> = computed(() => this.accountContextService.availableAccounts);
29+
public readonly availableAccounts: Signal<Account[]> = computed(() => this.accountContextService.availableAccounts);
2830

29-
/**
30-
* Handle account selection change
31-
*/
32-
protected handleAccountChange(event: any): void {
33-
const selectedAccountId = event.value as string;
34-
const selectedAccount = this.accountContextService.availableAccounts.find((acc) => acc.accountId === selectedAccountId);
35-
if (selectedAccount) {
36-
this.accountContextService.setAccount(selectedAccount);
37-
}
31+
public constructor() {
32+
this.form
33+
.get('selectedAccountId')
34+
?.valueChanges.pipe(takeUntilDestroyed())
35+
.subscribe((value) => {
36+
const selectedAccount = this.accountContextService.availableAccounts.find((acc) => acc.accountId === value);
37+
if (selectedAccount) {
38+
this.accountContextService.setAccount(selectedAccount as Account);
39+
}
40+
});
3841
}
3942
}

apps/lfx-one/src/app/modules/dashboards/components/organization-involvement/organization-involvement.component.html

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,16 @@ <h3 class="text-sm font-medium">{{ metric.title }}</h3>
4040
<div class="space-y-2 pt-1">
4141
<div class="flex items-center justify-between">
4242
<span class="text-sm text-gray-500">Tier</span>
43-
<div class="flex items-center gap-2">
44-
<span class="px-2 py-0.5 text-xs font-medium rounded bg-gradient-to-r from-gray-400 to-gray-300 text-white border border-gray-300">
45-
{{ metric.tier }}
46-
</span>
47-
<span class="text-xs text-gray-500">since {{ metric.tierSince }}</span>
48-
</div>
43+
<span class="px-2 py-0.5 text-xs font-medium rounded bg-gradient-to-r from-gray-400 to-gray-300 text-white border border-gray-300">
44+
{{ metric.tier }}
45+
</span>
4946
</div>
5047
<div class="flex items-center justify-between">
51-
<span class="text-sm text-gray-500">Annual Fee</span>
52-
<span class="text-sm font-medium">{{ metric.annualFee }}</span>
48+
<span class="text-sm text-gray-500">Member Since</span>
49+
<span class="text-sm font-medium">{{ metric.tierSince }}</span>
5350
</div>
5451
<div class="flex items-center justify-between">
55-
<span class="text-sm text-gray-500">Next Due</span>
52+
<span class="text-sm text-gray-500">Renewal Date</span>
5653
<span class="text-sm font-medium">{{ metric.nextDue }}</span>
5754
</div>
5855
</div>
@@ -87,7 +84,7 @@ <h3 class="text-sm font-medium">{{ metric.title }}</h3>
8784
}
8885
}
8986
<div class="space-y-0.5">
90-
<div class="text-xl font-medium">{{ metric.value | number }}</div>
87+
<div class="text-xl font-medium">{{ metric.value }}</div>
9188
@if (metric.subtitle) {
9289
<div class="text-xs text-gray-500">{{ metric.subtitle }}</div>
9390
}

0 commit comments

Comments
 (0)