diff --git a/apps/lfx-pcc/src/app/modules/project/committees/committee-dashboard/committee-dashboard.component.html b/apps/lfx-pcc/src/app/modules/project/committees/committee-dashboard/committee-dashboard.component.html index 92b5e8fb..485c3f51 100644 --- a/apps/lfx-pcc/src/app/modules/project/committees/committee-dashboard/committee-dashboard.component.html +++ b/apps/lfx-pcc/src/app/modules/project/committees/committee-dashboard/committee-dashboard.component.html @@ -12,164 +12,148 @@ } @if (committees() && !committeesLoading()) { -
-
- - -
- -
- -
+
+
+
+

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
- } +
+
+ + +
+
Upcoming Meeting
+ +
-
-
- - - -
- - - + + + +
+ +
+ +
+ + +
+ +
+ + +
+
-
+ + @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.name }} + + @if (committee.level === 1) { +
Sub-committee
+ } +
+
+ + + + +
+ {{ 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; }