Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<div class="mb-6 flex items-center gap-4" data-testid="organization-selector">
<label for="organization-select" class="text-sm font-semibold text-gray-700">Organization:</label>
<lfx-select
[form]="accountForm"
[form]="form"
control="selectedAccountId"
[options]="availableAccounts()"
optionLabel="accountName"
Expand All @@ -17,8 +17,7 @@
[showClear]="false"
styleClass="min-w-[300px]"
inputId="organization-select"
data-testid="organization-select"
(onChange)="handleAccountChange($event)" />
data-testid="organization-select" />
</div>

<!-- Dashboard Sections -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
// SPDX-License-Identifier: MIT

import { Component, computed, inject, Signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { Account } from '@lfx-one/shared/interfaces';

import { SelectComponent } from '../../../shared/components/select/select.component';
import { AccountContextService } from '../../../shared/services/account-context.service';
import { FoundationHealthComponent } from '../components/foundation-health/foundation-health.component';
Expand All @@ -20,20 +22,21 @@ import { PendingActionsComponent } from '../components/pending-actions/pending-a
export class BoardMemberDashboardComponent {
private readonly accountContextService = inject(AccountContextService);

protected readonly accountForm = new FormGroup({
public readonly form = new FormGroup({
selectedAccountId: new FormControl<string>(this.accountContextService.selectedAccount().accountId),
});

protected readonly availableAccounts: Signal<Account[]> = computed(() => this.accountContextService.availableAccounts);
public readonly availableAccounts: Signal<Account[]> = computed(() => this.accountContextService.availableAccounts);

/**
* Handle account selection change
*/
protected handleAccountChange(event: any): void {
const selectedAccountId = event.value as string;
const selectedAccount = this.accountContextService.availableAccounts.find((acc) => acc.accountId === selectedAccountId);
if (selectedAccount) {
this.accountContextService.setAccount(selectedAccount);
}
public constructor() {
this.form
.get('selectedAccountId')
?.valueChanges.pipe(takeUntilDestroyed())
.subscribe((value) => {
const selectedAccount = this.accountContextService.availableAccounts.find((acc) => acc.accountId === value);
if (selectedAccount) {
this.accountContextService.setAccount(selectedAccount as Account);
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ <h3 class="text-sm font-medium">{{ metric.title }}</h3>
}
}
<div class="space-y-0.5">
<div class="text-xl font-medium">{{ metric.value | number }}</div>
<div class="text-xl font-medium">{{ metric.value }}</div>
@if (metric.subtitle) {
<div class="text-xs text-gray-500">{{ metric.subtitle }}</div>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

import { CommonModule } from '@angular/common';
import { CommonModule, CurrencyPipe } from '@angular/common';
import { Component, computed, inject } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { AccountContextService } from '@app/shared/services/account-context.service';
Expand All @@ -13,6 +13,7 @@ import {
ImpactMetric,
MembershipTierResponse,
OrganizationContributorsResponse,
OrganizationEventSponsorshipsResponse,
OrganizationInvolvementMetricWithChart,
OrganizationMaintainersResponse,
PrimaryInvolvementMetric,
Expand All @@ -24,12 +25,14 @@ import { map, switchMap } from 'rxjs';
selector: 'lfx-organization-involvement',
standalone: true,
imports: [CommonModule, ChartComponent],
providers: [CurrencyPipe],
templateUrl: './organization-involvement.component.html',
styleUrl: './organization-involvement.component.scss',
})
export class OrganizationInvolvementComponent {
private readonly analyticsService = inject(AnalyticsService);
private readonly accountContextService = inject(AccountContextService);
private readonly currencyPipe = inject(CurrencyPipe);

private readonly selectedAccountId$ = toObservable(this.accountContextService.selectedAccount).pipe(map((account) => account.accountId));

Expand Down Expand Up @@ -91,6 +94,63 @@ export class OrganizationInvolvementComponent {
}
);

private readonly projectsParticipatingData = toSignal(
this.selectedAccountId$.pipe(switchMap((accountId) => this.analyticsService.getOrganizationProjectsParticipating(accountId))),
{
initialValue: {
projectsParticipating: 0,
accountId: '',
segmentId: '',
},
}
);

private readonly totalCommitsData = toSignal(
this.selectedAccountId$.pipe(switchMap((accountId) => this.analyticsService.getOrganizationTotalCommits(accountId))),
{
initialValue: {
totalCommits: 0,
accountId: '',
segmentId: '',
},
}
);

private readonly certifiedEmployeesData = toSignal(
this.selectedAccountId$.pipe(switchMap((accountId) => this.analyticsService.getOrganizationCertifiedEmployees(accountId))),
{
initialValue: {
certifications: 0,
certifiedEmployees: 0,
accountId: '',
},
}
);

private readonly boardMeetingAttendanceData = toSignal(
this.selectedAccountId$.pipe(switchMap((accountId) => this.analyticsService.getOrganizationBoardMeetingAttendance(accountId))),
{
initialValue: {
totalMeetings: 0,
attendedMeetings: 0,
notAttendedMeetings: 0,
attendancePercentage: 0,
accountId: '',
},
}
);

private readonly eventSponsorshipsData = toSignal(
this.selectedAccountId$.pipe(switchMap((accountId) => this.analyticsService.getOrganizationEventSponsorships(accountId))),
{
initialValue: {
currencySummaries: [],
totalEvents: 0,
accountId: '',
},
}
);

protected readonly isLoading = computed<boolean>(() => {
const maintainersData = this.organizationMaintainersData();
const contributorsData = this.organizationContributorsData();
Expand Down Expand Up @@ -128,14 +188,30 @@ export class OrganizationInvolvementComponent {

protected readonly contributionsMetrics = computed<ContributionMetric[]>((): ContributionMetric[] => {
const techCommitteeData = this.technicalCommitteeData();
const commitsData = this.totalCommitsData();
const boardMeetingData = this.boardMeetingAttendanceData();

return CONTRIBUTIONS_METRICS.map((metric) => {
if (metric.title === 'TOC/TSC/TAG Participation') {
const hasData = techCommitteeData.totalRepresentatives > 0;
return {
...metric,
descriptiveValue: hasData ? `${techCommitteeData.totalRepresentatives} representatives` : metric.descriptiveValue,
isConnected: hasData,
descriptiveValue: `${techCommitteeData.totalRepresentatives} representatives`,
isConnected: true,
};
}
if (metric.title === 'Total Commits') {
return {
...metric,
descriptiveValue: `${commitsData.totalCommits.toLocaleString()} commits`,
isConnected: true,
};
}
if (metric.title === 'Board Meetings Participation') {
const percentage = boardMeetingData.attendancePercentage.toFixed(0);
return {
...metric,
descriptiveValue: `${percentage}% attendance`,
isConnected: true,
};
}
return {
Expand All @@ -147,22 +223,36 @@ export class OrganizationInvolvementComponent {

protected readonly impactMetrics = computed<ImpactMetric[]>((): ImpactMetric[] => {
const eventData = this.eventAttendanceData();
const projectsData = this.projectsParticipatingData();
const certifiedData = this.certifiedEmployeesData();

return IMPACT_METRICS.map((metric) => {
if (metric.title === 'Event Attendees') {
const hasData = eventData.totalAttendees > 0;
return {
...metric,
descriptiveValue: hasData ? `${eventData.totalAttendees} employees` : metric.descriptiveValue,
isConnected: hasData,
descriptiveValue: `${eventData.totalAttendees} employees`,
isConnected: true,
};
}
if (metric.title === 'Event Speakers') {
const hasData = eventData.totalSpeakers > 0;
return {
...metric,
descriptiveValue: hasData ? `${eventData.totalSpeakers} speakers` : metric.descriptiveValue,
isConnected: hasData,
descriptiveValue: `${eventData.totalSpeakers} speakers`,
isConnected: true,
};
}
if (metric.title === 'Projects Participating') {
return {
...metric,
descriptiveValue: `${projectsData.projectsParticipating} projects`,
isConnected: true,
};
}
if (metric.title === 'Certified Employees') {
return {
...metric,
descriptiveValue: `${certifiedData.certifications.toLocaleString()} certifications (${certifiedData.certifiedEmployees} employees)`,
isConnected: true,
};
}
return {
Expand All @@ -176,8 +266,12 @@ export class OrganizationInvolvementComponent {
const maintainersData = this.organizationMaintainersData();
const contributorsData = this.organizationContributorsData();
const tierData = this.membershipTierData();
const sponsorshipsData = this.eventSponsorshipsData();

return PRIMARY_INVOLVEMENT_METRICS.map((metric) => {
if (metric.title === 'Event Sponsorship') {
return this.transformEventSponsorship(sponsorshipsData, metric);
}
if (metric.title === 'Active Contributors') {
return this.transformActiveContributors(contributorsData, metric);
}
Expand All @@ -192,10 +286,6 @@ export class OrganizationInvolvementComponent {
});

private transformActiveContributors(data: OrganizationContributorsResponse, metric: PrimaryInvolvementMetric): OrganizationInvolvementMetricWithChart {
if (data.contributors === 0) {
return { ...this.transformDefaultMetric(metric), isConnected: false };
}

return {
title: metric.title,
value: data.contributors.toString(),
Expand All @@ -220,10 +310,6 @@ export class OrganizationInvolvementComponent {
}

private transformMaintainers(data: OrganizationMaintainersResponse, metric: PrimaryInvolvementMetric): OrganizationInvolvementMetricWithChart {
if (data.maintainers === 0) {
return { ...this.transformDefaultMetric(metric), isConnected: false };
}

return {
title: metric.title,
value: data.maintainers.toString(),
Expand Down Expand Up @@ -251,15 +337,15 @@ export class OrganizationInvolvementComponent {
if (!data.tier) {
return {
title: metric.title,
value: metric.value,
subtitle: metric.subtitle,
value: 'No Membership',
subtitle: 'Not a member',
icon: metric.icon ?? '',
tier: metric.tier,
tierSince: metric.tierSince,
annualFee: metric.annualFee,
nextDue: metric.nextDue,
tier: '',
tierSince: '',
annualFee: '$0',
nextDue: '',
isMembershipTier: metric.isMembershipTier,
isConnected: false,
isConnected: true,
};
}

Expand All @@ -283,6 +369,38 @@ export class OrganizationInvolvementComponent {
};
}

private transformEventSponsorship(data: OrganizationEventSponsorshipsResponse, metric: PrimaryInvolvementMetric): OrganizationInvolvementMetricWithChart {
// Filter out summaries with null/empty currency codes and transform remaining valid entries
const formattedAmounts = data.currencySummaries
.filter((summary) => summary.currencyCode && summary.currencyCode.trim() !== '')
.map((summary) => this.currencyPipe.transform(summary.amount, summary.currencyCode, 'symbol', '1.0-0'))
.filter((formatted) => formatted !== null);

const displayValue = formattedAmounts.length > 0 ? formattedAmounts.join(' + ') : '$0';

return {
title: metric.title,
value: displayValue,
subtitle: `${data.totalEvents} events sponsored this year`,
icon: metric.icon ?? '',
isConnected: true,
chartData: {
labels: Array.from({ length: metric.sparklineData?.length ?? 0 }, (_, i) => `Point ${i + 1}`),
datasets: [
{
data: metric.sparklineData ?? [],
borderColor: metric.sparklineColor ?? '',
backgroundColor: hexToRgba(metric.sparklineColor ?? '', 0.1),
fill: true,
tension: 0.4,
borderWidth: 2,
pointRadius: 0,
},
],
},
};
}

private transformDefaultMetric(metric: PrimaryInvolvementMetric): OrganizationInvolvementMetricWithChart {
return {
title: metric.title,
Expand Down
Loading