diff --git a/apps/lfx-pcc/src/app/shared/services/meeting.service.ts b/apps/lfx-pcc/src/app/shared/services/meeting.service.ts index 93ba5890..4bc48364 100644 --- a/apps/lfx-pcc/src/app/shared/services/meeting.service.ts +++ b/apps/lfx-pcc/src/app/shared/services/meeting.service.ts @@ -38,6 +38,15 @@ export class MeetingService { ); } + public getPastMeetings(params?: HttpParams): Observable { + return this.http.get('/api/past-meetings', { params }).pipe( + catchError((error) => { + console.error('Failed to load past meetings:', error); + return of([]); + }) + ); + } + public getMeetingsByProject(projectId: string, limit?: number, orderBy?: string): Observable { let params = new HttpParams().set('tags', `project_uid:${projectId}`); @@ -70,12 +79,11 @@ export class MeetingService { public getPastMeetingsByProject(projectId: string, limit: number = 3): Observable { let params = new HttpParams().set('tags', `project_uid:${projectId}`); - // TODO: Add filter for past meetings if (limit) { params = params.set('limit', limit.toString()); } - return this.getMeetings(params); + return this.getPastMeetings(params); } public getMeeting(id: string): Observable { @@ -88,6 +96,16 @@ export class MeetingService { ); } + public getPastMeeting(id: string): Observable { + return this.http.get(`/api/past-meetings/${id}`).pipe( + catchError((error) => { + console.error(`Failed to load past meeting ${id}:`, error); + return throwError(() => error); + }), + tap((meeting) => this.meeting.set(meeting)) + ); + } + public getPublicMeeting(id: string): Observable<{ meeting: Meeting; project: Project }> { return this.http.get<{ meeting: Meeting; project: Project }>(`/public/api/meetings/${id}`).pipe( catchError((error) => { diff --git a/apps/lfx-pcc/src/server/controllers/past-meeting.controller.ts b/apps/lfx-pcc/src/server/controllers/past-meeting.controller.ts new file mode 100644 index 00000000..4e54a9bd --- /dev/null +++ b/apps/lfx-pcc/src/server/controllers/past-meeting.controller.ts @@ -0,0 +1,128 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +import { NextFunction, Request, Response } from 'express'; + +import { Logger } from '../helpers/logger'; +import { validateUidParameter } from '../helpers/validation.helper'; +import { MeetingService } from '../services/meeting.service'; + +/** + * Controller for handling past meeting HTTP requests + */ +export class PastMeetingController { + private meetingService: MeetingService = new MeetingService(); + + /** + * GET /past-meetings + */ + public async getPastMeetings(req: Request, res: Response, next: NextFunction): Promise { + const startTime = Logger.start(req, 'get_past_meetings', { + query_params: Logger.sanitize(req.query as Record), + }); + + try { + // Get the past meetings using meetingType 'past_meeting' + const meetings = await this.meetingService.getMeetings(req, req.query as Record, 'past_meeting'); + + // TODO: Remove this once we have a way to get the registrants count + // Process each meeting individually to add registrant counts + await Promise.all( + meetings.map(async (meeting) => { + const counts = await this.addRegistrantCounts(req, meeting.uid); + meeting.individual_registrants_count = counts.individual_registrants_count; + meeting.committee_members_count = counts.committee_members_count; + }) + ); + + // Log the success + Logger.success(req, 'get_past_meetings', startTime, { + meeting_count: meetings.length, + }); + + // Send the meetings data to the client + res.json(meetings); + } catch (error) { + // Log the error + Logger.error(req, 'get_past_meetings', startTime, error); + next(error); + } + } + + /** + * GET /past-meetings/:uid + */ + public async getPastMeetingById(req: Request, res: Response, next: NextFunction): Promise { + const { uid } = req.params; + const startTime = Logger.start(req, 'get_past_meeting_by_id', { + meeting_uid: uid, + }); + + try { + // Check if the meeting UID is provided + if ( + !validateUidParameter(uid, req, next, { + operation: 'get_past_meeting_by_id', + service: 'past_meeting_controller', + logStartTime: startTime, + }) + ) { + return; + } + + // Get the past meeting by ID using meetingType 'past_meeting' + const meeting = await this.meetingService.getMeetingById(req, uid, 'past_meeting'); + + // Log the success + Logger.success(req, 'get_past_meeting_by_id', startTime, { + meeting_uid: uid, + project_uid: meeting.project_uid, + title: meeting.title, + }); + + // TODO: Remove this once we have a way to get the registrants count + const counts = await this.addRegistrantCounts(req, meeting.uid); + meeting.individual_registrants_count = counts.individual_registrants_count; + meeting.committee_members_count = counts.committee_members_count; + + // Send the meeting data to the client + res.json(meeting); + } catch (error) { + // Log the error + Logger.error(req, 'get_past_meeting_by_id', startTime, error, { + meeting_uid: uid, + }); + + // Send the error to the next middleware + next(error); + } + } + + /** + * Helper method to add registrant counts to a meeting + * @param req - Express request object + * @param meetingUid - UID of the meeting + * @returns Promise with registrant counts or defaults to 0 on error + */ + private async addRegistrantCounts(req: Request, meetingUid: string): Promise<{ individual_registrants_count: number; committee_members_count: number }> { + try { + const registrants = await this.meetingService.getMeetingRegistrants(req, meetingUid); + const committeeMembers = registrants.filter((r) => r.type === 'committee').length ?? 0; + + return { + individual_registrants_count: registrants.length - committeeMembers, + committee_members_count: committeeMembers, + }; + } catch (error) { + // Log error but don't fail - default to 0 counts + Logger.error(req, 'add_registrant_counts', Date.now(), error, { + meeting_uid: meetingUid, + }); + + return { + individual_registrants_count: 0, + committee_members_count: 0, + }; + } + } +} diff --git a/apps/lfx-pcc/src/server/routes/past-meetings.route.ts b/apps/lfx-pcc/src/server/routes/past-meetings.route.ts new file mode 100644 index 00000000..2e65f235 --- /dev/null +++ b/apps/lfx-pcc/src/server/routes/past-meetings.route.ts @@ -0,0 +1,17 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +import express from 'express'; + +import { PastMeetingController } from '../controllers/past-meeting.controller'; + +const router = express.Router(); +const pastMeetingController = new PastMeetingController(); + +// Past meeting routes +router.get('/', (req, res, next) => pastMeetingController.getPastMeetings(req, res, next)); + +// Get past meeting by UID +router.get('/:uid', (req, res, next) => pastMeetingController.getPastMeetingById(req, res, next)); + +export default router; diff --git a/apps/lfx-pcc/src/server/server.ts b/apps/lfx-pcc/src/server/server.ts index 2f33bbb4..a2dcde9a 100644 --- a/apps/lfx-pcc/src/server/server.ts +++ b/apps/lfx-pcc/src/server/server.ts @@ -20,6 +20,7 @@ import { apiErrorHandler } from './middleware/error-handler.middleware'; import { protectedRoutesMiddleware } from './middleware/protected-routes.middleware'; import committeesRouter from './routes/committees.route'; import meetingsRouter from './routes/meetings.route'; +import pastMeetingsRouter from './routes/past-meetings.route'; import permissionsRouter from './routes/permissions.route'; import projectsRouter from './routes/projects.route'; import publicMeetingsRouter from './routes/public-meetings.route'; @@ -202,6 +203,7 @@ app.use('/api/projects', projectsRouter); app.use('/api/projects', permissionsRouter); app.use('/api/committees', committeesRouter); app.use('/api/meetings', meetingsRouter); +app.use('/api/past-meetings', pastMeetingsRouter); // Add API error handler middleware app.use('/api/*', apiErrorHandler); diff --git a/apps/lfx-pcc/src/server/services/meeting.service.ts b/apps/lfx-pcc/src/server/services/meeting.service.ts index b8e88e30..7424c98e 100644 --- a/apps/lfx-pcc/src/server/services/meeting.service.ts +++ b/apps/lfx-pcc/src/server/services/meeting.service.ts @@ -38,10 +38,10 @@ export class MeetingService { /** * Fetches all meetings based on query parameters */ - public async getMeetings(req: Request, query: Record = {}): Promise { + public async getMeetings(req: Request, query: Record = {}, meetingType: string = 'meeting'): Promise { const params = { ...query, - type: 'meeting', + type: meetingType, }; const { resources } = await this.microserviceProxy.proxyRequest>(req, 'LFX_V2_SERVICE', '/query/resources', 'GET', params); @@ -61,9 +61,9 @@ export class MeetingService { /** * Fetches a single meeting by UID */ - public async getMeetingById(req: Request, meetingUid: string): Promise { + public async getMeetingById(req: Request, meetingUid: string, meetingType: string = 'meeting'): Promise { const params = { - type: 'meeting', + type: meetingType, tags: `meeting_uid:${meetingUid}`, };