Skip to content

Commit e84447e

Browse files
committed
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>
1 parent 8d2a4b5 commit e84447e

7 files changed

Lines changed: 85 additions & 81 deletions

File tree

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

Lines changed: 6 additions & 9 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>

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

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
// SPDX-License-Identifier: MIT
33

44
import { CommonModule, CurrencyPipe } from '@angular/common';
5-
import { Component, computed, inject } from '@angular/core';
5+
import { Component, computed, inject, signal } from '@angular/core';
66
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
77
import { AccountContextService } from '@app/shared/services/account-context.service';
88
import { AnalyticsService } from '@app/shared/services/analytics.service';
99
import { ChartComponent } from '@components/chart/chart.component';
1010
import { CONTRIBUTIONS_METRICS, IMPACT_METRICS, PRIMARY_INVOLVEMENT_METRICS } from '@lfx-one/shared/constants';
1111
import { ContributionMetric, ImpactMetric, OrganizationInvolvementMetricWithChart, PrimaryInvolvementMetric } from '@lfx-one/shared/interfaces';
1212
import { hexToRgba } from '@lfx-one/shared/utils';
13-
import { map, switchMap } from 'rxjs';
13+
import { finalize, map, switchMap } from 'rxjs';
1414

1515
@Component({
1616
selector: 'lfx-organization-involvement',
@@ -25,11 +25,22 @@ export class OrganizationInvolvementComponent {
2525
private readonly accountContextService = inject(AccountContextService);
2626
private readonly currencyPipe = inject(CurrencyPipe);
2727

28+
// Loading state signals for each API call
29+
private readonly contributionsLoading = signal(true);
30+
private readonly dashboardLoading = signal(true);
31+
private readonly segmentLoading = signal(true);
32+
private readonly eventsLoading = signal(true);
33+
2834
private readonly selectedAccountId$ = toObservable(this.accountContextService.selectedAccount).pipe(map((account) => account.accountId));
2935

3036
// Consolidated API call for contributions overview (maintainers + contributors + technical committee)
3137
private readonly contributionsOverviewData = toSignal(
32-
this.selectedAccountId$.pipe(switchMap((accountId) => this.analyticsService.getOrganizationContributionsOverview(accountId))),
38+
this.selectedAccountId$.pipe(
39+
switchMap((accountId) => {
40+
this.contributionsLoading.set(true);
41+
return this.analyticsService.getOrganizationContributionsOverview(accountId).pipe(finalize(() => this.contributionsLoading.set(false)));
42+
})
43+
),
3344
{
3445
initialValue: {
3546
maintainers: {
@@ -52,14 +63,18 @@ export class OrganizationInvolvementComponent {
5263

5364
// Consolidated API call for board member dashboard (membership tier + certified employees + board meeting attendance)
5465
private readonly boardMemberDashboardData = toSignal(
55-
this.selectedAccountId$.pipe(switchMap((accountId) => this.analyticsService.getBoardMemberDashboard(accountId))),
66+
this.selectedAccountId$.pipe(
67+
switchMap((accountId) => {
68+
this.dashboardLoading.set(true);
69+
return this.analyticsService.getBoardMemberDashboard(accountId).pipe(finalize(() => this.dashboardLoading.set(false)));
70+
})
71+
),
5672
{
5773
initialValue: {
5874
membershipTier: {
5975
tier: '',
6076
membershipStartDate: '',
6177
membershipEndDate: '',
62-
membershipPrice: 0,
6378
membershipStatus: '',
6479
},
6580
certifiedEmployees: {
@@ -80,7 +95,12 @@ export class OrganizationInvolvementComponent {
8095

8196
// Consolidated API call for segment overview (projects participating + total commits)
8297
private readonly segmentOverviewData = toSignal(
83-
this.selectedAccountId$.pipe(switchMap((accountId) => this.analyticsService.getOrganizationSegmentOverview(accountId))),
98+
this.selectedAccountId$.pipe(
99+
switchMap((accountId) => {
100+
this.segmentLoading.set(true);
101+
return this.analyticsService.getOrganizationSegmentOverview(accountId).pipe(finalize(() => this.segmentLoading.set(false)));
102+
})
103+
),
84104
{
85105
initialValue: {
86106
projectsParticipating: 0,
@@ -93,7 +113,12 @@ export class OrganizationInvolvementComponent {
93113

94114
// Consolidated API call for events overview (event attendance + event sponsorships)
95115
private readonly eventsOverviewData = toSignal(
96-
this.selectedAccountId$.pipe(switchMap((accountId) => this.analyticsService.getOrganizationEventsOverview(accountId))),
116+
this.selectedAccountId$.pipe(
117+
switchMap((accountId) => {
118+
this.eventsLoading.set(true);
119+
return this.analyticsService.getOrganizationEventsOverview(accountId).pipe(finalize(() => this.eventsLoading.set(false)));
120+
})
121+
),
97122
{
98123
initialValue: {
99124
eventAttendance: {
@@ -113,9 +138,7 @@ export class OrganizationInvolvementComponent {
113138
);
114139

115140
protected readonly isLoading = computed<boolean>(() => {
116-
const contributionsData = this.contributionsOverviewData();
117-
const dashboardData = this.boardMemberDashboardData();
118-
return contributionsData.maintainers.maintainers === 0 && contributionsData.contributors.contributors === 0 && dashboardData.membershipTier.tier === '';
141+
return this.contributionsLoading() || this.dashboardLoading() || this.segmentLoading() || this.eventsLoading();
119142
});
120143

121144
protected readonly sparklineChartOptions = {
@@ -302,7 +325,6 @@ export class OrganizationInvolvementComponent {
302325
tier: string;
303326
membershipStartDate: string;
304327
membershipEndDate: string;
305-
membershipPrice: number;
306328
membershipStatus: string;
307329
},
308330
metric: PrimaryInvolvementMetric
@@ -315,7 +337,6 @@ export class OrganizationInvolvementComponent {
315337
icon: metric.icon ?? '',
316338
tier: '',
317339
tierSince: '',
318-
annualFee: '$0',
319340
nextDue: '',
320341
isMembershipTier: metric.isMembershipTier,
321342
isConnected: true,
@@ -326,16 +347,14 @@ export class OrganizationInvolvementComponent {
326347
const endDate = new Date(data.membershipEndDate);
327348
const tierSince = startDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
328349
const nextDue = endDate.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
329-
const annualFee = `$${data.membershipPrice.toLocaleString()}`;
330350

331351
return {
332352
title: metric.title,
333353
value: data.tier,
334-
subtitle: `since ${tierSince}`,
354+
subtitle: `Active membership`,
335355
icon: metric.icon ?? '',
336356
tier: data.tier,
337357
tierSince,
338-
annualFee,
339358
nextDue,
340359
isMembershipTier: metric.isMembershipTier,
341360
isConnected: true,
@@ -386,8 +405,8 @@ export class OrganizationInvolvementComponent {
386405
private transformDefaultMetric(metric: PrimaryInvolvementMetric): OrganizationInvolvementMetricWithChart {
387406
return {
388407
title: metric.title,
389-
value: metric.value,
390-
subtitle: metric.subtitle,
408+
value: metric.value ?? 'N/A',
409+
subtitle: metric.subtitle ?? 'No data available',
391410
icon: metric.icon ?? '',
392411
isConnected: false,
393412
chartData: {

apps/lfx-one/src/app/shared/services/analytics.service.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,6 @@ export class AnalyticsService {
169169
tier: '',
170170
membershipStartDate: '',
171171
membershipEndDate: '',
172-
membershipPrice: 0,
173172
membershipStatus: '',
174173
},
175174
certifiedEmployees: {

apps/lfx-one/src/server/services/organization.service.ts

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ export class OrganizationService {
9494
tier,
9595
membershipStartDate: data.CURRENT_MEMBERSHIP_START_DATE || '',
9696
membershipEndDate: data.CURRENT_MEMBERSHIP_END_DATE || '',
97-
membershipPrice: data.MEMBERSHIP_PRICE || 0,
9897
membershipStatus: data.MEMBERSHIP_STATUS || '',
9998
},
10099
certifiedEmployees: {
@@ -209,18 +208,21 @@ export class OrganizationService {
209208
*/
210209
private async getContributionsData(accountId: string): Promise<OrganizationContributionsConsolidatedRow> {
211210
const query = `
211+
WITH base AS (SELECT ? AS ACCOUNT_ID)
212212
SELECT
213213
m.MAINTAINERS,
214214
m.PROJECTS AS MAINTAINER_PROJECTS,
215215
c.CONTRIBUTORS,
216216
c.PROJECTS AS CONTRIBUTOR_PROJECTS,
217217
tc.TOTAL_REPRESENTATIVES,
218218
tc.TOTAL_PROJECTS AS TOTAL_TC_PROJECTS,
219-
COALESCE(m.ACCOUNT_ID, c.ACCOUNT_ID, tc.ACCOUNT_ID) AS ACCOUNT_ID,
219+
COALESCE(m.ACCOUNT_ID, c.ACCOUNT_ID, tc.ACCOUNT_ID, base.ACCOUNT_ID) AS ACCOUNT_ID,
220220
COALESCE(m.ACCOUNT_NAME, c.ACCOUNT_NAME) AS ACCOUNT_NAME
221-
FROM ANALYTICS_DEV.DEV_JEVANS_PLATINUM_LFX_ONE.MEMBER_DASHBOARD_MAINTAINERS m
221+
FROM base
222+
LEFT JOIN ANALYTICS_DEV.DEV_JEVANS_PLATINUM_LFX_ONE.MEMBER_DASHBOARD_MAINTAINERS m
223+
ON base.ACCOUNT_ID = m.ACCOUNT_ID
222224
LEFT JOIN ANALYTICS_DEV.DEV_JEVANS_PLATINUM_LFX_ONE.MEMBER_DASHBOARD_CONTRIBUTORS c
223-
ON m.ACCOUNT_ID = c.ACCOUNT_ID
225+
ON base.ACCOUNT_ID = c.ACCOUNT_ID
224226
LEFT JOIN (
225227
SELECT
226228
SUM(COUNT) AS TOTAL_REPRESENTATIVES,
@@ -230,8 +232,7 @@ export class OrganizationService {
230232
WHERE ACCOUNT_ID = ?
231233
GROUP BY ACCOUNT_ID
232234
) tc
233-
ON m.ACCOUNT_ID = tc.ACCOUNT_ID
234-
WHERE m.ACCOUNT_ID = ?
235+
ON base.ACCOUNT_ID = tc.ACCOUNT_ID
235236
LIMIT 1
236237
`;
237238

@@ -254,16 +255,17 @@ export class OrganizationService {
254255
*/
255256
private async getSegmentData(accountId: string, segmentId: string): Promise<SegmentContributionsConsolidatedRow> {
256257
const query = `
258+
WITH base AS (SELECT ? AS ACCOUNT_ID, ? AS SEGMENT_ID)
257259
SELECT
258260
pp.PROJECTS_PARTICIPATING,
259261
tc.TOTAL_COMMITS,
260-
COALESCE(pp.ACCOUNT_ID, tc.ACCOUNT_ID) AS ACCOUNT_ID,
261-
COALESCE(pp.SEGMENT_ID, tc.SEGMENT_ID) AS SEGMENT_ID
262-
FROM ANALYTICS_DEV.DEV_JEVANS_PLATINUM_LFX_ONE.MEMBER_DASHBOARD_PROJECTS_PARTICIPATING pp
262+
COALESCE(pp.ACCOUNT_ID, tc.ACCOUNT_ID, base.ACCOUNT_ID) AS ACCOUNT_ID,
263+
COALESCE(pp.SEGMENT_ID, tc.SEGMENT_ID, base.SEGMENT_ID) AS SEGMENT_ID
264+
FROM base
265+
LEFT JOIN ANALYTICS_DEV.DEV_JEVANS_PLATINUM_LFX_ONE.MEMBER_DASHBOARD_PROJECTS_PARTICIPATING pp
266+
ON base.ACCOUNT_ID = pp.ACCOUNT_ID AND base.SEGMENT_ID = pp.SEGMENT_ID
263267
LEFT JOIN ANALYTICS_DEV.DEV_JEVANS_PLATINUM_LFX_ONE.MEMBER_DASHBOARD_TOTAL_COMMITS tc
264-
ON pp.ACCOUNT_ID = tc.ACCOUNT_ID AND pp.SEGMENT_ID = tc.SEGMENT_ID
265-
WHERE pp.ACCOUNT_ID = ?
266-
AND pp.SEGMENT_ID = ?
268+
ON base.ACCOUNT_ID = tc.ACCOUNT_ID AND base.SEGMENT_ID = tc.SEGMENT_ID
267269
LIMIT 1
268270
`;
269271

@@ -286,11 +288,11 @@ export class OrganizationService {
286288
*/
287289
private async getDashboardData(accountId: string, projectId: string): Promise<BoardMemberDashboardConsolidatedRow> {
288290
const query = `
291+
WITH base AS (SELECT ? AS ACCOUNT_ID, ? AS PROJECT_ID)
289292
SELECT
290293
mt.MEMBERSHIP_TIER,
291294
mt.CURRENT_MEMBERSHIP_START_DATE,
292295
mt.CURRENT_MEMBERSHIP_END_DATE,
293-
mt.MEMBERSHIP_PRICE,
294296
mt.MEMBERSHIP_STATUS,
295297
ce.CERTIFICATIONS,
296298
ce.CERTIFIED_EMPLOYEES,
@@ -301,15 +303,15 @@ export class OrganizationService {
301303
WHEN bma.TOTAL_MEETINGS > 0 THEN (bma.ATTENDED_MEETINGS::FLOAT / bma.TOTAL_MEETINGS::FLOAT) * 100
302304
ELSE 0
303305
END AS ATTENDANCE_PERCENTAGE,
304-
COALESCE(mt.ACCOUNT_ID, ce.ACCOUNT_ID, bma.ACCOUNT_ID) AS ACCOUNT_ID,
305-
COALESCE(mt.PROJECT_ID, ce.PROJECT_ID, bma.PROJECT_ID) AS PROJECT_ID
306-
FROM ANALYTICS_DEV.DEV_JEVANS_PLATINUM_LFX_ONE.MEMBER_DASHBOARD_MEMBERSHIP_TIER mt
306+
COALESCE(mt.ACCOUNT_ID, ce.ACCOUNT_ID, bma.ACCOUNT_ID, base.ACCOUNT_ID) AS ACCOUNT_ID,
307+
COALESCE(mt.PROJECT_ID, ce.PROJECT_ID, bma.PROJECT_ID, base.PROJECT_ID) AS PROJECT_ID
308+
FROM base
309+
LEFT JOIN ANALYTICS_DEV.DEV_JEVANS_PLATINUM_LFX_ONE.MEMBER_DASHBOARD_MEMBERSHIP_TIER mt
310+
ON base.ACCOUNT_ID = mt.ACCOUNT_ID AND base.PROJECT_ID = mt.PROJECT_ID
307311
LEFT JOIN ANALYTICS_DEV.DEV_JEVANS_PLATINUM_LFX_ONE.MEMBER_DASHBOARD_CERTIFIED_EMPLOYEES ce
308-
ON mt.ACCOUNT_ID = ce.ACCOUNT_ID AND mt.PROJECT_ID = ce.PROJECT_ID
312+
ON base.ACCOUNT_ID = ce.ACCOUNT_ID AND base.PROJECT_ID = ce.PROJECT_ID
309313
LEFT JOIN ANALYTICS_DEV.DEV_JEVANS_PLATINUM_LFX_ONE.MEMBER_DASHBOARD_BOARD_MEETING_ATTENDANCE bma
310-
ON mt.ACCOUNT_ID = bma.ACCOUNT_ID AND mt.PROJECT_ID = bma.PROJECT_ID
311-
WHERE mt.ACCOUNT_ID = ?
312-
AND mt.PROJECT_ID = ?
314+
ON base.ACCOUNT_ID = bma.ACCOUNT_ID AND base.PROJECT_ID = bma.PROJECT_ID
313315
LIMIT 1
314316
`;
315317

packages/shared/src/constants/organization-involvement.constants.ts

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,48 +14,40 @@ const generateTrendData = (months: number, baseValue: number): number[] => {
1414
};
1515

1616
/**
17-
* Primary metrics for board member organization involvement
18-
* Matches the React implementation for CNCF Overview page
17+
* Primary metrics configuration for board member organization involvement
18+
* NOTE: This contains only UI configuration (icons, chart styling). All data values come from live API.
19+
* This serves as a configuration template matched by title to determine visual presentation.
1920
*/
2021
export const PRIMARY_INVOLVEMENT_METRICS: PrimaryInvolvementMetric[] = [
2122
{
22-
// NOTE: All membership tier values are placeholders - replaced with real Snowflake data in component
2323
title: 'Membership Tier',
24-
value: 'Silver',
25-
subtitle: 'since January 2024',
26-
tier: 'Silver',
27-
tierSince: 'January 2024',
28-
annualFee: '$0',
29-
nextDue: 'January 1, 2025',
24+
icon: 'fa-light fa-badge-check',
3025
isMembershipTier: true,
26+
// All membership data (tier, dates, fees) comes from getBoardMemberDashboard API
3127
},
3228
{
3329
title: 'Event Sponsorship',
34-
value: '$250K',
35-
subtitle: '8 events sponsored this year',
3630
icon: 'fa-light fa-dollar-sign',
37-
sparklineData: generateTrendData(12, 250000),
38-
sparklineColor: '#0094FF',
31+
sparklineData: generateTrendData(12, 60), // TODO: Replace with API trend data
32+
sparklineColor: '#0094FF', // TODO: Replace with API color
3933
chartType: 'line' as const,
34+
// All event data (amounts, counts) comes from getOrganizationEventsOverview API
4035
},
4136
{
4237
title: 'Active Contributors',
43-
value: '847',
44-
subtitle: 'Contributors from our organization',
4538
icon: 'fa-light fa-users',
46-
sparklineData: [645, 678, 702, 725, 748, 771, 794, 812, 829, 835, 842, 847],
47-
sparklineColor: '#0094FF',
39+
sparklineData: generateTrendData(12, 60), // TODO: Replace with API trend data
40+
sparklineColor: '#0094FF', // TODO: Replace with API color
4841
chartType: 'line' as const,
42+
// All contributor data comes from getOrganizationContributionsOverview API
4943
},
5044
{
51-
// NOTE: Value and subtitle are placeholder - replaced with real Snowflake data in component
5245
title: 'Maintainers',
53-
value: '60',
54-
subtitle: 'Across multiple projects',
5546
icon: 'fa-light fa-user-check',
56-
sparklineData: generateTrendData(12, 60),
57-
sparklineColor: '#0094FF',
47+
sparklineData: generateTrendData(12, 60), // TODO: Replace with API trend data
48+
sparklineColor: '#0094FF', // TODO: Replace with API color
5849
chartType: 'line' as const,
50+
// All maintainer data comes from getOrganizationContributionsOverview API
5951
},
6052
];
6153

packages/shared/src/interfaces/analytics-data.interface.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,6 @@ export interface BoardMemberDashboardConsolidatedRow {
207207
MEMBERSHIP_TIER: string | null;
208208
CURRENT_MEMBERSHIP_START_DATE: string | null;
209209
CURRENT_MEMBERSHIP_END_DATE: string | null;
210-
MEMBERSHIP_PRICE: number | null;
211210
MEMBERSHIP_STATUS: string | null;
212211

213212
// Certified Employees fields
@@ -343,7 +342,6 @@ export interface BoardMemberDashboardResponse {
343342
tier: string;
344343
membershipStartDate: string;
345344
membershipEndDate: string;
346-
membershipPrice: number;
347345
membershipStatus: string;
348346
};
349347

0 commit comments

Comments
 (0)