+
+
+
Committee Management
+
+ Manage committees, their settings, and organizational structure. Sub-committees are indicated with indentation.
+
+
+
+
+
-
-
-
+
+
+
+
+
Total Committees
+
+
{{ totalCommittees() }}
+
+
+ +2 this week
+
+
- @if (committees().length > 0) {
-
-
-
-
-
- |
- Name
- |
-
- Type
- |
-
-
- Members
-
- |
-
-
- Voting
-
- |
-
-
- Public
-
- |
-
-
- Last Updated
-
- |
-
-
- Actions
-
- |
-
-
-
-
-
-
- |
-
- {{ committee.name }}
-
- |
-
- {{ committee.category || 'Uncategorized' }}
- |
-
- {{ committee.total_members || 0 }}
- |
-
- {{ committee.enable_voting ? 'Yes' : 'No' }}
- |
-
-
-
- |
-
- {{ formatDate(committee.updated_at) }}
- |
-
-
- |
-
-
-
-
-
-
-
-
-
- No committees found
- Try adjusting your search or filters
-
- |
-
-
-
-
-
-
+
+
+
Public Committees
+
+
{{ publicCommittees() }}
+
+ {{ totalCommittees() > 0 ? ((publicCommittees() / totalCommittees()) * 100 | number: '1.0-0') : 0 }}% of total
+
+
+
+
-
-
- } @else {
-
-
-
-
No Committees Yet
-
This project doesn't have any committees yet.
-
+
+
+
Active Voting
+
+
{{ activeVoting() }}
+
+ Voting enabled
- }
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+ @if (committees().length > 0) {
+
+
+
+
+
+
+
+
+ } @else {
+
+
+
+
No Committees Yet
+
This project doesn't have any committees yet.
+
+
+
+ }
+
}
diff --git a/apps/lfx-pcc/src/app/modules/project/committees/committee-dashboard/committee-dashboard.component.ts b/apps/lfx-pcc/src/app/modules/project/committees/committee-dashboard/committee-dashboard.component.ts
index 06a9391b..e419ee9c 100644
--- a/apps/lfx-pcc/src/app/modules/project/committees/committee-dashboard/committee-dashboard.component.ts
+++ b/apps/lfx-pcc/src/app/modules/project/committees/committee-dashboard/committee-dashboard.component.ts
@@ -5,13 +5,11 @@ import { CommonModule } from '@angular/common';
import { Component, computed, inject, signal, Signal, WritableSignal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
-import { Router, RouterLink } from '@angular/router';
+import { Router } from '@angular/router';
import { ButtonComponent } from '@components/button/button.component';
import { CardComponent } from '@components/card/card.component';
import { InputTextComponent } from '@components/input-text/input-text.component';
-import { MenuComponent } from '@components/menu/menu.component';
import { SelectComponent } from '@components/select/select.component';
-import { TableComponent } from '@components/table/table.component';
import { Committee } from '@lfx-pcc/shared/interfaces';
import { CommitteeService } from '@services/committee.service';
import { ProjectService } from '@services/project.service';
@@ -23,6 +21,7 @@ import { BehaviorSubject, of } from 'rxjs';
import { debounceTime, distinctUntilChanged, startWith, switchMap, tap } from 'rxjs/operators';
import { CommitteeFormComponent } from '../components/committee-form/committee-form.component';
+import { CommitteeTableComponent } from '../components/committee-table/committee-table.component';
import { UpcomingCommitteeMeetingComponent } from '../components/upcoming-committee-meeting/upcoming-committee-meeting.component';
@Component({
@@ -30,10 +29,8 @@ import { UpcomingCommitteeMeetingComponent } from '../components/upcoming-commit
imports: [
CommonModule,
ReactiveFormsModule,
- RouterLink,
CardComponent,
- MenuComponent,
- TableComponent,
+ CommitteeTableComponent,
InputTextComponent,
SelectComponent,
ButtonComponent,
@@ -62,9 +59,11 @@ export class CommitteeDashboardComponent {
public rows: number;
public searchForm: FormGroup;
public categoryFilter: WritableSignal
;
+ public votingStatusFilter: WritableSignal;
public committeesLoading: WritableSignal;
public committees: Signal;
public categories: Signal<{ label: string; value: string | null }[]>;
+ public votingStatusOptions: Signal<{ label: string; value: string | null }[]>;
public filteredCommittees: Signal;
public totalRecords: Signal;
public menuItems: MenuItem[];
@@ -73,6 +72,11 @@ export class CommitteeDashboardComponent {
private searchTerm: Signal;
private dialogRef: DynamicDialogRef | undefined;
+ // Statistics calculations
+ public totalCommittees: Signal = computed(() => this.committees().length);
+ public publicCommittees: Signal = computed(() => this.committees().filter((c) => c.public_enabled).length);
+ public activeVoting: Signal = computed(() => this.committees().filter((c) => c.enable_voting).length);
+
public constructor() {
// Initialize all class variables
this.project = this.projectService.project;
@@ -85,8 +89,10 @@ export class CommitteeDashboardComponent {
this.committees = this.initializeCommittees();
this.searchForm = this.initializeSearchForm();
this.categoryFilter = signal(null);
+ this.votingStatusFilter = signal(null);
this.searchTerm = this.initializeSearchTerm();
this.categories = this.initializeCategories();
+ this.votingStatusOptions = this.initializeVotingStatusOptions();
this.filteredCommittees = this.initializeFilteredCommittees();
this.totalRecords = this.initializeTotalRecords();
this.menuItems = this.initializeMenuItems();
@@ -104,26 +110,16 @@ export class CommitteeDashboardComponent {
this.first.set(0);
}
- public onSearch(): void {
- // Reset to first page when searching
+ public onVotingStatusChange(value: string | null): void {
+ // Update the voting status filter signal
+ this.votingStatusFilter.set(value);
+ // Reset to first page when changing filter
this.first.set(0);
}
- public formatDate(dateString: string): string {
- if (!dateString) return '-';
- const date = new Date(dateString);
- return date.toLocaleDateString('en-US', {
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- });
- }
-
- // Toggle action menu for a committee
- public toggleActionMenu(event: Event, committee: Committee, menuComponent: MenuComponent): void {
- event.stopPropagation();
- this.selectedCommittee.set(committee);
- menuComponent.toggle(event);
+ public onSearch(): void {
+ // Reset to first page when searching
+ this.first.set(0);
}
// Dialog methods for create/edit
@@ -145,6 +141,22 @@ export class CommitteeDashboardComponent {
});
}
+ // Committee table event handlers
+ public onEditCommittee(committee: Committee): void {
+ this.selectedCommittee.set(committee);
+ this.editCommittee();
+ }
+
+ public onViewCommittee(committee: Committee): void {
+ this.selectedCommittee.set(committee);
+ this.viewCommittee();
+ }
+
+ public onDeleteCommittee(committee: Committee): void {
+ this.selectedCommittee.set(committee);
+ this.deleteCommittee();
+ }
+
// Action handlers (use selectedCommittee)
private viewCommittee(): void {
const committee = this.selectedCommittee();
@@ -218,6 +230,7 @@ export class CommitteeDashboardComponent {
return new FormGroup({
search: new FormControl(''),
category: new FormControl(null),
+ votingStatus: new FormControl(null),
});
}
@@ -240,8 +253,41 @@ export class CommitteeDashboardComponent {
private initializeCategories(): Signal<{ label: string; value: string | null }[]> {
return computed(() => {
const committeesData = this.committees();
- const uniqueCategories = [...new Set(committeesData.map((c) => c.category).filter(Boolean))];
- return [{ label: 'All Categories', value: null }, ...uniqueCategories.map((cat) => ({ label: cat, value: cat }))];
+
+ // Count committees by category
+ const categoryCounts = new Map();
+ committeesData.forEach((committee) => {
+ if (committee.category) {
+ categoryCounts.set(committee.category, (categoryCounts.get(committee.category) || 0) + 1);
+ }
+ });
+
+ // Get unique categories and sort them
+ const uniqueCategories = Array.from(categoryCounts.keys()).sort((a, b) => a.localeCompare(b));
+
+ // Create options with counts
+ const categoryOptions = uniqueCategories.map((cat) => ({
+ label: `${cat} (${categoryCounts.get(cat)})`,
+ value: cat,
+ }));
+
+ return [{ label: 'All Committee Types', value: null }, ...categoryOptions];
+ });
+ }
+
+ private initializeVotingStatusOptions(): Signal<{ label: string; value: string | null }[]> {
+ return computed(() => {
+ const committeesData = this.committees();
+
+ // Count committees by voting status
+ const votingEnabledCount = committeesData.filter((c) => c.enable_voting === true).length;
+ const votingDisabledCount = committeesData.filter((c) => c.enable_voting === false).length;
+
+ return [
+ { label: 'All Voting Status', value: null },
+ { label: `Voting Enabled (${votingEnabledCount})`, value: 'enabled' },
+ { label: `Voting Disabled (${votingDisabledCount})`, value: 'disabled' },
+ ];
});
}
@@ -266,6 +312,16 @@ export class CommitteeDashboardComponent {
filtered = filtered.filter((committee) => committee.category === category);
}
+ // Apply voting status filter
+ const votingStatus = this.votingStatusFilter();
+ if (votingStatus) {
+ if (votingStatus === 'enabled') {
+ filtered = filtered.filter((committee) => committee.enable_voting === true);
+ } else if (votingStatus === 'disabled') {
+ filtered = filtered.filter((committee) => committee.enable_voting === false);
+ }
+ }
+
return filtered;
});
}
diff --git a/apps/lfx-pcc/src/app/modules/project/committees/committee-view/committee-view.component.html b/apps/lfx-pcc/src/app/modules/project/committees/committee-view/committee-view.component.html
index b17c1ed0..9255b139 100644
--- a/apps/lfx-pcc/src/app/modules/project/committees/committee-view/committee-view.component.html
+++ b/apps/lfx-pcc/src/app/modules/project/committees/committee-view/committee-view.component.html
@@ -68,39 +68,6 @@ {{ committee()?.name }}
[membersLoading]="membersLoading()"
(refresh)="refreshMembers()">
}
-
-
- @if (committee()?.subcommittees && (committee()?.subcommittees)!.length > 0) {
-
-
-
-
- | Name |
- Category |
- Members |
- Voting Reps |
- Actions |
-
-
-
-
-
- | {{ subcommittee.name }} |
-
-
- {{ subcommittee.category }}
-
- |
- {{ subcommittee.total_members }} |
- {{ subcommittee.total_voting_reps }} |
-
-
- |
-
-
-
-
- }
@@ -132,11 +99,6 @@
Description
{{ committee()?.total_voting_reps || 0 }}
-
- Subcommittees
- {{ committee()?.subcommittees?.length || 0 }}
-
-
@if (committee()?.committee_website) {
Website
diff --git a/apps/lfx-pcc/src/app/modules/project/committees/committee-view/committee-view.component.ts b/apps/lfx-pcc/src/app/modules/project/committees/committee-view/committee-view.component.ts
index 10141094..1756a19f 100644
--- a/apps/lfx-pcc/src/app/modules/project/committees/committee-view/committee-view.component.ts
+++ b/apps/lfx-pcc/src/app/modules/project/committees/committee-view/committee-view.component.ts
@@ -2,13 +2,12 @@
// SPDX-License-Identifier: MIT
import { CommonModule } from '@angular/common';
-import { Component, computed, inject, Injector, signal, Signal, WritableSignal } from '@angular/core';
+import { Component, computed, inject, signal, Signal, WritableSignal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute, Router } from '@angular/router';
import { ButtonComponent } from '@components/button/button.component';
import { CardComponent } from '@components/card/card.component';
import { MenuComponent } from '@components/menu/menu.component';
-import { TableComponent } from '@components/table/table.component';
import { Committee, CommitteeMember } from '@lfx-pcc/shared/interfaces';
import { CommitteeService } from '@services/committee.service';
import { ProjectService } from '@services/project.service';
@@ -27,7 +26,6 @@ import { UpcomingCommitteeMeetingComponent } from '../components/upcoming-commit
CommonModule,
CardComponent,
MenuComponent,
- TableComponent,
ButtonComponent,
CommitteeMembersComponent,
UpcomingCommitteeMeetingComponent,
@@ -46,7 +44,6 @@ export class CommitteeViewComponent {
private readonly committeeService = inject(CommitteeService);
private readonly confirmationService = inject(ConfirmationService);
private readonly dialogService = inject(DialogService);
- private readonly injector = inject(Injector);
// Class variables with types
private dialogRef: DynamicDialogRef | undefined;
diff --git a/apps/lfx-pcc/src/app/modules/project/committees/components/committee-form/committee-form.component.html b/apps/lfx-pcc/src/app/modules/project/committees/components/committee-form/committee-form.component.html
index 171d1098..257ed9f1 100644
--- a/apps/lfx-pcc/src/app/modules/project/committees/components/committee-form/committee-form.component.html
+++ b/apps/lfx-pcc/src/app/modules/project/committees/components/committee-form/committee-form.component.html
@@ -36,6 +36,21 @@
Basic Information
Category is required
+
+
+
+
+
Select a parent committee to create a subcommittee. Leave empty for top-level committees.
+
+
diff --git a/apps/lfx-pcc/src/app/modules/project/committees/components/committee-form/committee-form.component.ts b/apps/lfx-pcc/src/app/modules/project/committees/components/committee-form/committee-form.component.ts
index 03e31246..e61d2e06 100644
--- a/apps/lfx-pcc/src/app/modules/project/committees/components/committee-form/committee-form.component.ts
+++ b/apps/lfx-pcc/src/app/modules/project/committees/components/committee-form/committee-form.component.ts
@@ -2,7 +2,8 @@
// SPDX-License-Identifier: MIT
import { CommonModule } from '@angular/common';
-import { Component, computed, inject, signal } from '@angular/core';
+import { Component, computed, inject, signal, Signal } from '@angular/core';
+import { toSignal } from '@angular/core/rxjs-interop';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { ButtonComponent } from '@components/button/button.component';
@@ -11,10 +12,12 @@ import { SelectComponent } from '@components/select/select.component';
import { TextareaComponent } from '@components/textarea/textarea.component';
import { ToggleComponent } from '@components/toggle/toggle.component';
import { COMMITTEE_CATEGORIES } from '@lfx-pcc/shared/constants';
+import { Committee } from '@lfx-pcc/shared/interfaces';
import { CommitteeService } from '@services/committee.service';
import { ProjectService } from '@services/project.service';
import { MessageService } from 'primeng/api';
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
+import { map } from 'rxjs';
@Component({
selector: 'lfx-committee-form',
@@ -45,6 +48,9 @@ export class CommitteeFormComponent {
// Committee category options
public categoryOptions = COMMITTEE_CATEGORIES;
+ // Parent committee options
+ public parentCommitteeOptions: Signal<{ label: string; value: string | null }[]> = this.initializeParentCommitteeOptions();
+
public constructor() {
// Initialize form with data when component is created
this.initializeForm();
@@ -170,11 +176,42 @@ export class CommitteeFormComponent {
}
}
+ private initializeParentCommitteeOptions(): Signal<{ label: string; value: string | null }[]> {
+ const projectId = this.config.data?.projectId || this.projectService.project()?.uid;
+ if (!projectId) {
+ return signal([{ label: 'No Parent Committee', value: null }]);
+ }
+
+ return toSignal(
+ this.committeeService.getCommitteesByProject(projectId).pipe(
+ map((committees: Committee[]) => {
+ // Filter to only top-level committees (no parent_uid)
+ const topLevelCommittees = committees.filter((committee) => !committee.parent_uid);
+
+ // If editing, exclude the current committee
+ const currentCommitteeId = this.committee()?.id;
+ const availableCommittees = currentCommitteeId ? topLevelCommittees.filter((committee) => committee.id !== currentCommitteeId) : topLevelCommittees;
+
+ // Transform to dropdown options
+ const options = availableCommittees.map((committee) => ({
+ label: committee.name,
+ value: committee.id,
+ }));
+
+ // Add "No Parent Committee" option at the beginning
+ return [{ label: 'No Parent Committee', value: null }, ...options];
+ })
+ ),
+ { initialValue: [{ label: 'No Parent Committee', value: null }] }
+ );
+ }
+
private createCommitteeFormGroup(committee?: any): FormGroup {
return new FormGroup({
name: new FormControl(committee?.name || '', [Validators.required]),
category: new FormControl(committee?.category || '', [Validators.required]),
description: new FormControl(committee?.description || ''),
+ parent_uid: new FormControl(committee?.parent_uid || null),
business_email_required: new FormControl(committee?.business_email_required || false),
enable_voting: new FormControl(committee?.enable_voting || false),
is_audit_enabled: new FormControl(committee?.is_audit_enabled || false),
diff --git a/apps/lfx-pcc/src/app/modules/project/committees/components/committee-table/committee-table.component.html b/apps/lfx-pcc/src/app/modules/project/committees/components/committee-table/committee-table.component.html
new file mode 100644
index 00000000..a4968de5
--- /dev/null
+++ b/apps/lfx-pcc/src/app/modules/project/committees/components/committee-table/committee-table.component.html
@@ -0,0 +1,163 @@
+
+
+
+
+
+
+
+
+ |
+ Committee Name
+ |
+
+ Description
+ |
+
+ Type
+ |
+
+ Visibility
+ |
+
+ Voting
+ |
+
+ Members
+ |
+
+ Last Updated
+ |
+
+ Actions
+ |
+
+
+
+
+
+
+
+
+
+ @if (committee.level === 1) {
+
+ }
+
+
+ |
+
+
+
+
+ {{ committee.description || '-' }}
+
+ |
+
+
+
+ {{ committee.category }}
+ |
+
+
+
+ @if (committee.public_enabled) {
+
+
+ Public
+
+ } @else {
+
+
+ Private
+
+ }
+ |
+
+
+
+ @if (committee.enable_voting) {
+
+
+ Enabled
+
+ } @else {
+
+
+ Disabled
+
+ }
+ |
+
+
+
+ {{ committee.total_members || 0 }}
+ |
+
+
+
+ {{ committee.updated_at ? (committee.updated_at | date: 'MMM d, y') : '-' }}
+ |
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+ No committees found
+ Try adjusting your search or filters
+
+ |
+
+
+
+
diff --git a/apps/lfx-pcc/src/app/modules/project/committees/components/committee-table/committee-table.component.ts b/apps/lfx-pcc/src/app/modules/project/committees/components/committee-table/committee-table.component.ts
new file mode 100644
index 00000000..ea2b2340
--- /dev/null
+++ b/apps/lfx-pcc/src/app/modules/project/committees/components/committee-table/committee-table.component.ts
@@ -0,0 +1,108 @@
+// Copyright The Linux Foundation and each contributor to LFX.
+// SPDX-License-Identifier: MIT
+
+import { CommonModule } from '@angular/common';
+import { Component, computed, inject, input, output, Signal, signal, WritableSignal } from '@angular/core';
+import { RouterLink } from '@angular/router';
+import { ButtonComponent } from '@components/button/button.component';
+import { MenuComponent } from '@components/menu/menu.component';
+import { TableComponent } from '@components/table/table.component';
+import { Committee } from '@lfx-pcc/shared/interfaces';
+import { CommitteeTypeColorPipe } from '@pipes/committee-type-colors.pipe';
+import { ProjectService } from '@services/project.service';
+import { MenuItem } from 'primeng/api';
+
+@Component({
+ selector: 'lfx-committee-table',
+ standalone: true,
+ imports: [CommonModule, RouterLink, ButtonComponent, MenuComponent, TableComponent, CommitteeTypeColorPipe],
+ templateUrl: './committee-table.component.html',
+})
+export class CommitteeTableComponent {
+ private readonly projectService = inject(ProjectService);
+
+ public readonly committees = input.required
();
+ public readonly editCommittee = output();
+ public readonly deleteCommittee = output();
+ public readonly viewCommittee = output();
+
+ public readonly project = this.projectService.project;
+
+ // Selected committee for menu actions
+ public selectedCommittee: WritableSignal = signal(null);
+
+ // Menu items for committee actions
+ public committeeActionMenuItems: MenuItem[] = this.initializeCommitteeActionMenuItems();
+
+ // Organize committees with hierarchy for display
+ public readonly organizedCommittees: Signal<(Committee & { level?: number })[]> = computed(() => {
+ const allCommittees = this.committees();
+ const result: (Committee & { level?: number })[] = [];
+
+ // First, add all parent committees (those without parent_uid)
+ const parentCommittees = allCommittees.filter((c) => !c.parent_uid);
+
+ parentCommittees.forEach((parent) => {
+ result.push(parent);
+
+ // Then add any subcommittees for this parent
+ const subcommittees = allCommittees.filter((c) => c.parent_uid === parent.id);
+ subcommittees.forEach((sub) => {
+ result.push({ ...sub, level: 1 });
+ });
+ });
+
+ // Add any orphaned committees (shouldn't happen, but just in case)
+ const orphaned = allCommittees.filter((c) => c.parent_uid && !allCommittees.find((p) => p.id === c.parent_uid));
+ result.push(...orphaned);
+
+ return result;
+ });
+
+ public onEdit(committee: Committee): void {
+ this.editCommittee.emit(committee);
+ }
+
+ public onDelete(committee: Committee): void {
+ this.deleteCommittee.emit(committee);
+ }
+
+ public onView(committee: Committee): void {
+ this.viewCommittee.emit(committee);
+ }
+
+ public toggleCommitteeActionMenu(event: Event, committee: Committee, menuComponent: MenuComponent): void {
+ event.stopPropagation();
+ this.selectedCommittee.set(committee);
+ menuComponent.toggle(event);
+ }
+
+ private initializeCommitteeActionMenuItems(): MenuItem[] {
+ return [
+ {
+ label: 'View',
+ icon: 'fa-light fa-eye',
+ command: () => {
+ const committee = this.selectedCommittee();
+ if (committee) {
+ this.onView(committee);
+ }
+ },
+ },
+ {
+ separator: true,
+ },
+ {
+ label: 'Delete',
+ icon: 'fa-light fa-trash',
+ styleClass: 'text-red-500',
+ command: () => {
+ const committee = this.selectedCommittee();
+ if (committee) {
+ this.onDelete(committee);
+ }
+ },
+ },
+ ];
+ }
+}
diff --git a/apps/lfx-pcc/src/app/modules/project/committees/components/upcoming-committee-meeting/upcoming-committee-meeting.component.html b/apps/lfx-pcc/src/app/modules/project/committees/components/upcoming-committee-meeting/upcoming-committee-meeting.component.html
index 88072952..86c491d4 100644
--- a/apps/lfx-pcc/src/app/modules/project/committees/components/upcoming-committee-meeting/upcoming-committee-meeting.component.html
+++ b/apps/lfx-pcc/src/app/modules/project/committees/components/upcoming-committee-meeting/upcoming-committee-meeting.component.html
@@ -26,20 +26,10 @@
}
}
-
-
-
-
-
} @else {
-
+
No upcoming meetings
No committee meetings scheduled
diff --git a/apps/lfx-pcc/src/app/modules/project/committees/components/upcoming-committee-meeting/upcoming-committee-meeting.component.ts b/apps/lfx-pcc/src/app/modules/project/committees/components/upcoming-committee-meeting/upcoming-committee-meeting.component.ts
index 5ba335a5..48cf143c 100644
--- a/apps/lfx-pcc/src/app/modules/project/committees/components/upcoming-committee-meeting/upcoming-committee-meeting.component.ts
+++ b/apps/lfx-pcc/src/app/modules/project/committees/components/upcoming-committee-meeting/upcoming-committee-meeting.component.ts
@@ -6,7 +6,6 @@ import { HttpParams } from '@angular/common/http';
import { Component, computed, inject, Injector, input, OnInit, runInInjectionContext, Signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { RouterLink } from '@angular/router';
-import { ButtonComponent } from '@components/button/button.component';
import { Meeting } from '@lfx-pcc/shared/interfaces';
import { MeetingTimePipe } from '@pipes/meeting-time.pipe';
import { MeetingService } from '@services/meeting.service';
@@ -17,7 +16,7 @@ import { map, of } from 'rxjs';
@Component({
selector: 'lfx-upcoming-committee-meeting',
standalone: true,
- imports: [CommonModule, RouterLink, ButtonComponent, MeetingTimePipe, TooltipModule],
+ imports: [CommonModule, RouterLink, MeetingTimePipe, TooltipModule],
templateUrl: './upcoming-committee-meeting.component.html',
styleUrl: './upcoming-committee-meeting.component.scss',
})
diff --git a/apps/lfx-pcc/src/app/shared/pipes/committee-type-colors.pipe.ts b/apps/lfx-pcc/src/app/shared/pipes/committee-type-colors.pipe.ts
new file mode 100644
index 00000000..363e8c31
--- /dev/null
+++ b/apps/lfx-pcc/src/app/shared/pipes/committee-type-colors.pipe.ts
@@ -0,0 +1,15 @@
+// Copyright The Linux Foundation and each contributor to LFX.
+// SPDX-License-Identifier: MIT
+
+import { Pipe, PipeTransform } from '@angular/core';
+import { getCommitteeTypeColor } from '@lfx-pcc/shared/constants';
+
+@Pipe({
+ name: 'committeeTypeColor',
+ standalone: true,
+})
+export class CommitteeTypeColorPipe implements PipeTransform {
+ public transform(category: string): string {
+ return getCommitteeTypeColor(category);
+ }
+}
diff --git a/docs/architecture/frontend/angular-patterns.md b/docs/architecture/frontend/angular-patterns.md
index 9fde418d..702b4e96 100644
--- a/docs/architecture/frontend/angular-patterns.md
+++ b/docs/architecture/frontend/angular-patterns.md
@@ -161,6 +161,56 @@ Modern Angular template syntax used throughout:
{{ signalValue() }}
```
+## 🔄 Data Transformation Best Practices
+
+### Use Pipes Instead of Methods
+
+For data transformation in templates, always use Angular pipes instead of component methods:
+
+```typescript
+// ❌ BAD: Using methods in templates
+@Component({
+ template: `
{{ formatDate(item.created_at) }}`,
+})
+export class BadComponent {
+ formatDate(date: string): string {
+ return new Date(date).toLocaleDateString();
+ }
+}
+
+// ✅ GOOD: Using pipes in templates
+@Component({
+ template: `
{{ item.created_at | date: 'MMM d, y' }}`,
+})
+export class GoodComponent {
+ // No method needed - use Angular's built-in date pipe
+}
+```
+
+### Benefits of Using Pipes
+
+1. **Performance**: Pipes are pure by default and cached - they only re-execute when inputs change
+2. **Reusability**: Pipes can be shared across components
+3. **Testability**: Easier to unit test pipes in isolation
+4. **Separation of Concerns**: Keeps components focused on logic, not formatting
+5. **Change Detection**: Methods are called on every change detection cycle, pipes only when needed
+
+### Common Built-in Pipes
+
+```html
+
+{{ dateValue | date: 'MMM d, y' }} {{ dateValue | date: 'MMM d, yyyy @ h:mm a' }}
+
+
+{{ numberValue | number: '1.2-2' }} {{ price | currency: 'USD' }}
+
+
+{{ text | uppercase }} {{ text | lowercase }} {{ text | titlecase }}
+
+
+{{ items | slice: 0:10 }} {{ object | json }}
+```
+
## 🎨 Component Input/Output Patterns
### Input Signals
diff --git a/packages/shared/src/constants/committees.ts b/packages/shared/src/constants/committees.ts
index 06f57295..e934e854 100644
--- a/packages/shared/src/constants/committees.ts
+++ b/packages/shared/src/constants/committees.ts
@@ -44,3 +44,54 @@ export const VOTING_STATUSES = [
{ label: 'Voting Rep', value: 'Voting Rep' },
{ label: 'Emeritus', value: 'Emeritus' },
];
+
+/**
+ * Committee type color mappings for consistent styling across the application
+ * Colors match corresponding meeting types for consistency
+ */
+export const COMMITTEE_TYPE_COLORS = {
+ // Board and governance
+ Board: 'text-red-500', // Matches meeting type
+ 'Government Advisory Council': 'text-red-600', // Similar to board governance
+
+ // Technical committees
+ 'Technical Steering Committee': 'text-purple-500', // Matches "Technical" meeting type
+ 'Technical Oversight Committee/Technical Advisory Committee': 'text-purple-500', // Matches "Technical" meeting type
+ 'Technical Mailing List': 'text-purple-400', // Technical related
+ Maintainers: 'text-blue-500', // Matches meeting type
+ Committers: 'text-blue-600', // Similar to maintainers
+
+ // Legal and compliance
+ 'Legal Committee': 'text-amber-500', // Matches meeting type
+ 'Code of Conduct': 'text-amber-600', // Legal/compliance related
+ 'Product Security': 'text-amber-700', // Security/compliance
+
+ // Marketing and outreach
+ 'Marketing Oversight Committee/Marketing Advisory Committee': 'text-green-500', // Matches marketing meeting type
+ 'Marketing Committee/Sub Committee': 'text-green-600', // Marketing related
+ 'Marketing Mailing List': 'text-green-400', // Marketing related
+ Ambassador: 'text-green-700', // Outreach/marketing
+
+ // Finance
+ 'Finance Committee': 'text-emerald-500', // Financial management
+
+ // Working groups and special interest
+ 'Working Group': 'text-orange-700', // Distinct color for working groups
+ 'Special Interest Group': 'text-amber-600', // Special interest groups
+ 'Expert Group': 'text-amber-700', // Similar to special interest
+
+ // Other/miscellaneous
+ Other: 'text-gray-600', // General other category
+} as const;
+
+/**
+ * Default color scheme for unknown committee types
+ */
+export const DEFAULT_COMMITTEE_TYPE_COLOR = 'text-gray-500';
+
+/**
+ * Get color class for a committee type
+ */
+export function getCommitteeTypeColor(type: string): string {
+ return COMMITTEE_TYPE_COLORS[type as keyof typeof COMMITTEE_TYPE_COLORS] || DEFAULT_COMMITTEE_TYPE_COLOR;
+}
diff --git a/packages/shared/src/interfaces/committee.interface.ts b/packages/shared/src/interfaces/committee.interface.ts
index f6bccbcb..9b14f684 100644
--- a/packages/shared/src/interfaces/committee.interface.ts
+++ b/packages/shared/src/interfaces/committee.interface.ts
@@ -5,7 +5,7 @@ export interface Committee {
id: string;
name: string;
category: string;
- committee_id: string;
+ parent_uid?: string | null;
description?: string;
business_email_required: boolean;
enable_voting: boolean;
@@ -22,33 +22,6 @@ export interface Committee {
system_mod_stamp: string;
total_members: number;
total_voting_reps: number;
- subcommittees: Subcommittee[] | null;
- project_uid: string;
- joinable?: boolean;
-}
-
-export interface Subcommittee {
- id: string;
- name: string;
- category: string;
- committee_id: string;
- committee?: CommitteeSummary;
- description?: string;
- business_email_required: boolean;
- enable_voting: boolean;
- is_audit_enabled: boolean;
- public_enabled: boolean;
- public_name?: string;
- sso_group_enabled: boolean;
- sso_group_name?: string;
- created_by?: string;
- created_at: string;
- updated_at?: string;
- last_updated_by?: string;
- system_mod_stamp: string;
- total_members: number;
- total_voting_reps: number;
- subcommittees: Subcommittee[] | null;
project_uid: string;
joinable?: boolean;
}