Skip to content

Commit 56c1599

Browse files
authored
feat(api): add count endpoints for meetings and committees in dashboards (#106)
* perf(api): optimize meeting and committee count queries Replace inefficient full resource fetching with dedicated count endpoints for project metrics. This improves performance by avoiding unnecessary data transfer when only counts are needed. - Add getMeetingsCount() method to meeting service - Add getCommitteesCount() method to committee service - Add QueryServiceCountResponse interface for count API responses - Update project controller to use count methods instead of fetching arrays - Remove TODO comments as optimization is now implemented 🤖 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Andres Tobon <andrest2455@gmail.com> * feat(api): add count endpoints for meetings and committees in dashboards Replace inefficient array fetching with dedicated count endpoints across multiple dashboard components to improve performance and reduce bandwidth usage. Frontend changes: - Add getMeetingsCountByProject() and getCommitteesCountByProject() methods to services - Update project dashboard to use count endpoints for total statistics - Update meetings dashboard to use count endpoint for total meetings display - Update committees dashboard to use count endpoint for total committees display Backend changes: - Add /api/meetings/count and /api/committees/count endpoints - Add getMeetingsCount() and getCommitteesCount() controller methods - Utilize existing optimized count service methods Performance improvements: - Reduced bandwidth by fetching counts instead of full resource arrays - Better scalability as performance no longer degrades with resource growth - Faster loading times for dashboard statistics 🤖 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Andres Tobon <andrest2455@gmail.com> * refactor(services): replace switchMap with map for synchronous property extraction Replace unnecessary switchMap + of pattern with map for extracting count property from HTTP responses in meeting and committee services. Changes: - meetingService.getMeetingsCountByProject(): use map instead of switchMap + of - committeeService.getCommitteesCountByProject(): use map instead of switchMap + of This follows RxJS best practices where map should be used for synchronous transformations and switchMap only when flattening observables. 🤖 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Andres Tobon <andrest2455@gmail.com> * refactor(types): improve type safety and signal optimization in count services - Use QueryServiceCountResponse interface for better type safety in count endpoints - Remove unnecessary computed() wrapper for totalCommittees signal - Add proper interface imports for count response types This ensures consistent typing across count endpoints and eliminates redundant signal wrapping where direct signal assignment is sufficient. 🤖 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Andres Tobon <andrest2455@gmail.com> --------- Signed-off-by: Andres Tobon <andrest2455@gmail.com>
1 parent 660aef8 commit 56c1599

File tree

14 files changed

+196
-13
lines changed

14 files changed

+196
-13
lines changed

apps/lfx-one/src/app/modules/project/committees/committee-dashboard/committee-dashboard.component.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export class CommitteeDashboardComponent {
7171
private dialogRef: DynamicDialogRef | undefined;
7272

7373
// Statistics calculations
74-
public totalCommittees: Signal<number> = computed(() => this.committees().length);
74+
public totalCommittees: Signal<number>;
7575
public publicCommittees: Signal<number> = computed(() => this.committees().filter((c) => c.public).length);
7676
public activeVoting: Signal<number> = computed(() => this.committees().filter((c) => c.enable_voting).length);
7777

@@ -85,6 +85,7 @@ export class CommitteeDashboardComponent {
8585
this.committeesLoading = signal<boolean>(true);
8686
this.refresh = new BehaviorSubject<void>(undefined);
8787
this.committees = this.initializeCommittees();
88+
this.totalCommittees = this.initializeCommitteesCount();
8889
this.searchForm = this.initializeSearchForm();
8990
this.categoryFilter = signal<string | null>(null);
9091
this.votingStatusFilter = signal<string | null>(null);
@@ -259,6 +260,12 @@ export class CommitteeDashboardComponent {
259260
);
260261
}
261262

263+
private initializeCommitteesCount(): Signal<number> {
264+
return toSignal(this.project() ? this.refresh.pipe(switchMap(() => this.committeeService.getCommitteesCountByProject(this.project()!.uid))) : of(0), {
265+
initialValue: 0,
266+
});
267+
}
268+
262269
private initializeCategories(): Signal<{ label: string; value: string | null }[]> {
263270
return computed(() => {
264271
const committeesData = this.committees();

apps/lfx-one/src/app/modules/project/dashboard/project-dashboard/project.component.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ export class ProjectComponent {
4141
// Signals to hold data
4242
public allCommittees: Signal<Committee[]> = signal([]);
4343
public allMeetings: Signal<Meeting[]> = signal([]);
44+
public meetingsCount: Signal<number> = signal(0);
45+
public committeesCount: Signal<number> = signal(0);
4446
public committeesLoading: WritableSignal<boolean> = signal(true);
4547
public meetingsLoading: WritableSignal<boolean> = signal(true);
4648

@@ -86,6 +88,8 @@ export class ProjectComponent {
8688
// Initialize data signals
8789
this.allCommittees = this.initializeAllCommittees();
8890
this.allMeetings = this.initializeAllMeetings();
91+
this.meetingsCount = this.initializeMeetingsCount();
92+
this.committeesCount = this.initializeCommitteesCount();
8993
this.recentActivity = this.initializeRecentActivity();
9094

9195
// Initialize computed signals
@@ -170,6 +174,44 @@ export class ProjectComponent {
170174
);
171175
}
172176

177+
private initializeMeetingsCount(): Signal<number> {
178+
return toSignal(
179+
this.projectService.project$.pipe(
180+
switchMap((project) => {
181+
if (!project?.uid) {
182+
return of(0);
183+
}
184+
return this.meetingService.getMeetingsCountByProject(project.uid).pipe(
185+
catchError((error) => {
186+
console.error('Error loading meetings count:', error);
187+
return of(0);
188+
})
189+
);
190+
})
191+
),
192+
{ initialValue: 0 }
193+
);
194+
}
195+
196+
private initializeCommitteesCount(): Signal<number> {
197+
return toSignal(
198+
this.projectService.project$.pipe(
199+
switchMap((project) => {
200+
if (!project?.uid) {
201+
return of(0);
202+
}
203+
return this.committeeService.getCommitteesCountByProject(project.uid).pipe(
204+
catchError((error) => {
205+
console.error('Error loading committees count:', error);
206+
return of(0);
207+
})
208+
);
209+
})
210+
),
211+
{ initialValue: 0 }
212+
);
213+
}
214+
173215
private initializeRecentActivity(): Signal<RecentActivity[]> {
174216
return toSignal(
175217
this.projectService.project$.pipe(
@@ -277,20 +319,24 @@ export class ProjectComponent {
277319
return computed(() => {
278320
const committees = this.allCommittees();
279321
const meetings = this.allMeetings();
322+
const meetingsCount = this.meetingsCount();
323+
const committeesCount = this.committeesCount();
280324
const now = new Date();
281325

282326
// Calculate total members from all committees
327+
// TODO: Get this from the query service once committee members can be filted by project
283328
const totalMembers = committees.reduce((sum, committee) => sum + (committee.total_members || 0), 0);
284329

285330
// Calculate meeting statistics
331+
// TODO: Get this from the query service once the query service supports getting counts by a field date range
286332
const upcomingMeetingsCount = meetings.filter((m) => m.start_time && new Date(m.start_time) >= now).length;
287333
const publicMeetings = meetings.filter((m) => m.visibility === 'public').length;
288334
const privateMeetings = meetings.filter((m) => m.visibility === 'private').length;
289335

290336
return {
291337
totalMembers,
292-
totalCommittees: committees.length,
293-
totalMeetings: meetings.length,
338+
totalCommittees: committeesCount,
339+
totalMeetings: meetingsCount,
294340
upcomingMeetings: upcomingMeetingsCount,
295341
publicMeetings,
296342
privateMeetings,

apps/lfx-one/src/app/modules/project/meetings/meeting-dashboard/meeting-dashboard.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ <h3 class="text-lg font-medium text-gray-900 mb-2">No Meetings Yet</h3>
142142
<div class="flex flex-col gap-3">
143143
<div class="flex justify-between items-center">
144144
<span class="text-sm font-sans text-gray-600">Total Meetings</span>
145-
<span class="text-base font-display font-semibold text-gray-900">{{ meetings().length }}</span>
145+
<span class="text-base font-display font-semibold text-gray-900">{{ meetingsCount() }}</span>
146146
</div>
147147
<div class="flex justify-between items-center">
148148
<span class="text-sm font-sans text-gray-600">Public</span>

apps/lfx-one/src/app/modules/project/meetings/meeting-dashboard/meeting-dashboard.component.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export class MeetingDashboardComponent {
5555
public committeeFilter: WritableSignal<string | null>;
5656
public meetingsLoading: WritableSignal<boolean>;
5757
public meetings: Signal<Meeting[]>;
58+
public meetingsCount: Signal<number>;
5859
public upcomingMeetings: Signal<(MeetingOccurrence & { meeting: Meeting })[]>;
5960
public pastMeetingsLoading: WritableSignal<boolean>;
6061
public pastMeetings: Signal<PastMeeting[]>;
@@ -79,6 +80,7 @@ export class MeetingDashboardComponent {
7980
this.pastMeetingsLoading = signal<boolean>(true);
8081
this.refresh = new BehaviorSubject<void>(undefined);
8182
this.meetings = this.initializeMeetings();
83+
this.meetingsCount = this.initializeMeetingsCount();
8284
this.upcomingMeetings = this.initializeUpcomingMeetings();
8385
this.pastMeetings = this.initializePastMeetings();
8486
this.searchForm = this.initializeSearchForm();
@@ -181,6 +183,12 @@ export class MeetingDashboardComponent {
181183
);
182184
}
183185

186+
private initializeMeetingsCount(): Signal<number> {
187+
return toSignal(this.project() ? this.refresh.pipe(switchMap(() => this.meetingService.getMeetingsCountByProject(this.project()!.uid))) : of(0), {
188+
initialValue: 0,
189+
});
190+
}
191+
184192
private initializeUpcomingMeetings(): Signal<(MeetingOccurrence & { meeting: Meeting })[]> {
185193
return computed(() => {
186194
return this.meetings().flatMap((m) => m.occurrences.map((o) => ({ ...o, meeting: m })));

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
import { HttpClient, HttpParams } from '@angular/common/http';
55
import { inject, Injectable, signal, WritableSignal } from '@angular/core';
6-
import { Committee, CommitteeMember, CreateCommitteeMemberRequest } from '@lfx-one/shared/interfaces';
7-
import { catchError, Observable, of, take, tap, throwError } from 'rxjs';
6+
import { Committee, CommitteeMember, CreateCommitteeMemberRequest, QueryServiceCountResponse } from '@lfx-one/shared/interfaces';
7+
import { catchError, Observable, of, map, take, tap, throwError } from 'rxjs';
88

99
@Injectable({
1010
providedIn: 'root',
@@ -29,6 +29,19 @@ export class CommitteeService {
2929
return this.getCommittees(params);
3030
}
3131

32+
public getCommitteesCountByProject(projectId: string): Observable<number> {
33+
const params = new HttpParams().set('tags', `project_uid:${projectId}`);
34+
return this.http
35+
.get<QueryServiceCountResponse>('/api/committees/count', { params })
36+
.pipe(
37+
catchError((error) => {
38+
console.error('Failed to load committees count:', error);
39+
return of({ count: 0 });
40+
})
41+
)
42+
.pipe(map((response) => response.count));
43+
}
44+
3245
public getRecentCommitteesByProject(projectId: string): Observable<Committee[]> {
3346
return this.getCommitteesByProject(projectId);
3447
}

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ import {
1818
PastMeeting,
1919
PastMeetingParticipant,
2020
Project,
21+
QueryServiceCountResponse,
2122
UpdateMeetingRegistrantRequest,
2223
UpdateMeetingRequest,
2324
UploadFileResponse,
2425
} from '@lfx-one/shared/interfaces';
25-
import { catchError, defer, Observable, of, switchMap, take, tap, throwError } from 'rxjs';
26+
import { catchError, defer, Observable, of, map, switchMap, take, tap, throwError } from 'rxjs';
2627

2728
@Injectable({
2829
providedIn: 'root',
@@ -64,6 +65,22 @@ export class MeetingService {
6465
return this.getMeetings(params);
6566
}
6667

68+
public getMeetingsCountByProject(projectId: string): Observable<number> {
69+
const params = new HttpParams().set('tags', `project_uid:${projectId}`);
70+
return this.http
71+
.get<QueryServiceCountResponse>('/api/meetings/count', { params })
72+
.pipe(
73+
catchError((error) => {
74+
console.error('Failed to load meetings count:', error);
75+
return of({ count: 0 });
76+
})
77+
)
78+
.pipe(
79+
// Extract just the count number from the response
80+
map((response) => response.count)
81+
);
82+
}
83+
6784
public getRecentMeetingsByProject(projectId: string, limit: number = 3): Observable<Meeting[]> {
6885
return this.getMeetingsByProject(projectId, limit, 'updated_at.desc');
6986
}

apps/lfx-one/src/server/controllers/committee.controller.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,28 @@ export class CommitteeController {
3636
}
3737
}
3838

39+
/**
40+
* GET /committees/count
41+
*/
42+
public async getCommitteesCount(req: Request, res: Response, next: NextFunction): Promise<void> {
43+
const startTime = Logger.start(req, 'get_committees_count', {
44+
query_params: Logger.sanitize(req.query as Record<string, any>),
45+
});
46+
47+
try {
48+
const count = await this.committeeService.getCommitteesCount(req, req.query);
49+
50+
Logger.success(req, 'get_committees_count', startTime, {
51+
count,
52+
});
53+
54+
res.json({ count });
55+
} catch (error) {
56+
Logger.error(req, 'get_committees_count', startTime, error);
57+
next(error);
58+
}
59+
}
60+
3961
/**
4062
* GET /committees/:id
4163
*/

apps/lfx-one/src/server/controllers/meeting.controller.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,32 @@ export class MeetingController {
6666
}
6767
}
6868

69+
/**
70+
* GET /meetings/count
71+
*/
72+
public async getMeetingsCount(req: Request, res: Response, next: NextFunction): Promise<void> {
73+
const startTime = Logger.start(req, 'get_meetings_count', {
74+
query_params: Logger.sanitize(req.query as Record<string, any>),
75+
});
76+
77+
try {
78+
// Get the meetings count
79+
const count = await this.meetingService.getMeetingsCount(req, req.query as Record<string, any>);
80+
81+
// Log the success
82+
Logger.success(req, 'get_meetings_count', startTime, {
83+
count,
84+
});
85+
86+
// Send the count to the client
87+
res.json({ count });
88+
} catch (error) {
89+
// Log the error
90+
Logger.error(req, 'get_meetings_count', startTime, error);
91+
next(error);
92+
}
93+
}
94+
6995
/**
7096
* GET /meetings/:uid
7197
*/

apps/lfx-one/src/server/controllers/project.controller.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,10 @@ export class ProjectController {
3131
const projects = await this.projectService.getProjects(req, req.query as Record<string, any>);
3232

3333
// Add metrics to all projects
34-
// TODO: Remove this once we have a way to get the metrics from the microservice
3534
await Promise.all(
3635
projects.map(async (project) => {
37-
project.meetings_count = (await this.meetingService.getMeetings(req, { tags: `project_uid:${project.uid}` }).catch(() => [])).length;
38-
project.committees_count = (await this.committeeService.getCommittees(req, { tags: `project_uid:${project.uid}` }).catch(() => [])).length;
36+
project.meetings_count = Number(await this.meetingService.getMeetingsCount(req, { tags: `project_uid:${project.uid}` }).catch(() => 0));
37+
project.committees_count = Number(await this.committeeService.getCommitteesCount(req, { tags: `project_uid:${project.uid}` }).catch(() => 0));
3938
})
4039
);
4140

@@ -83,11 +82,10 @@ export class ProjectController {
8382
const results = await this.projectService.searchProjects(req, q);
8483

8584
// Add metrics to all projects
86-
// TODO: Remove this once we have a way to get the metrics from the microservice
8785
await Promise.all(
8886
results.map(async (project) => {
89-
project.meetings_count = (await this.meetingService.getMeetings(req, { tags: `project_uid:${project.uid}` }).catch(() => [])).length;
90-
project.committees_count = (await this.committeeService.getCommittees(req, { tags: `project_uid:${project.uid}` }).catch(() => [])).length;
87+
project.meetings_count = Number(await this.meetingService.getMeetingsCount(req, { tags: `project_uid:${project.uid}` }).catch(() => 0));
88+
project.committees_count = Number(await this.committeeService.getCommitteesCount(req, { tags: `project_uid:${project.uid}` }).catch(() => 0));
9189
})
9290
);
9391

apps/lfx-one/src/server/routes/committees.route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const committeeController = new CommitteeController();
1111

1212
// Committee CRUD routes - using new controller pattern
1313
router.get('/', (req, res, next) => committeeController.getCommittees(req, res, next));
14+
router.get('/count', (req, res, next) => committeeController.getCommitteesCount(req, res, next));
1415
router.get('/:id', (req, res, next) => committeeController.getCommitteeById(req, res, next));
1516
router.post('/', (req, res, next) => committeeController.createCommittee(req, res, next));
1617
router.put('/:id', (req, res, next) => committeeController.updateCommittee(req, res, next));

0 commit comments

Comments
 (0)