Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -61,6 +61,7 @@ export class CommitteeDashboardComponent {
public votingStatusFilter: WritableSignal<string | null>;
public committeesLoading: WritableSignal<boolean>;
public committees: Signal<Committee[]>;
public committeesCount: Signal<number>;
public categories: Signal<{ label: string; value: string | null }[]>;
public votingStatusOptions: Signal<{ label: string; value: string | null }[]>;
public filteredCommittees: Signal<Committee[]>;
Expand All @@ -71,7 +72,7 @@ export class CommitteeDashboardComponent {
private dialogRef: DynamicDialogRef | undefined;

// Statistics calculations
public totalCommittees: Signal<number> = computed(() => this.committees().length);
public totalCommittees: Signal<number> = computed(() => this.committeesCount());
public publicCommittees: Signal<number> = computed(() => this.committees().filter((c) => c.public).length);
public activeVoting: Signal<number> = computed(() => this.committees().filter((c) => c.enable_voting).length);

Expand All @@ -85,6 +86,7 @@ export class CommitteeDashboardComponent {
this.committeesLoading = signal<boolean>(true);
this.refresh = new BehaviorSubject<void>(undefined);
this.committees = this.initializeCommittees();
this.committeesCount = this.initializeCommitteesCount();
this.searchForm = this.initializeSearchForm();
this.categoryFilter = signal<string | null>(null);
this.votingStatusFilter = signal<string | null>(null);
Expand Down Expand Up @@ -259,6 +261,12 @@ export class CommitteeDashboardComponent {
);
}

private initializeCommitteesCount(): Signal<number> {
return toSignal(this.project() ? this.refresh.pipe(switchMap(() => this.committeeService.getCommitteesCountByProject(this.project()!.uid))) : of(0), {
initialValue: 0,
});
}

private initializeCategories(): Signal<{ label: string; value: string | null }[]> {
return computed(() => {
const committeesData = this.committees();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export class ProjectComponent {
// Signals to hold data
public allCommittees: Signal<Committee[]> = signal([]);
public allMeetings: Signal<Meeting[]> = signal([]);
public meetingsCount: Signal<number> = signal(0);
public committeesCount: Signal<number> = signal(0);
public committeesLoading: WritableSignal<boolean> = signal(true);
public meetingsLoading: WritableSignal<boolean> = signal(true);

Expand Down Expand Up @@ -86,6 +88,8 @@ export class ProjectComponent {
// Initialize data signals
this.allCommittees = this.initializeAllCommittees();
this.allMeetings = this.initializeAllMeetings();
this.meetingsCount = this.initializeMeetingsCount();
this.committeesCount = this.initializeCommitteesCount();
this.recentActivity = this.initializeRecentActivity();

// Initialize computed signals
Expand Down Expand Up @@ -170,6 +174,44 @@ export class ProjectComponent {
);
}

private initializeMeetingsCount(): Signal<number> {
return toSignal(
this.projectService.project$.pipe(
switchMap((project) => {
if (!project?.uid) {
return of(0);
}
return this.meetingService.getMeetingsCountByProject(project.uid).pipe(
catchError((error) => {
console.error('Error loading meetings count:', error);
return of(0);
})
);
})
),
{ initialValue: 0 }
);
}

private initializeCommitteesCount(): Signal<number> {
return toSignal(
this.projectService.project$.pipe(
switchMap((project) => {
if (!project?.uid) {
return of(0);
}
return this.committeeService.getCommitteesCountByProject(project.uid).pipe(
catchError((error) => {
console.error('Error loading committees count:', error);
return of(0);
})
);
})
),
{ initialValue: 0 }
);
}

private initializeRecentActivity(): Signal<RecentActivity[]> {
return toSignal(
this.projectService.project$.pipe(
Expand Down Expand Up @@ -277,20 +319,24 @@ export class ProjectComponent {
return computed(() => {
const committees = this.allCommittees();
const meetings = this.allMeetings();
const meetingsCount = this.meetingsCount();
const committeesCount = this.committeesCount();
const now = new Date();

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

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

return {
totalMembers,
totalCommittees: committees.length,
totalMeetings: meetings.length,
totalCommittees: committeesCount,
totalMeetings: meetingsCount,
upcomingMeetings: upcomingMeetingsCount,
publicMeetings,
privateMeetings,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ <h3 class="text-lg font-medium text-gray-900 mb-2">No Meetings Yet</h3>
<div class="flex flex-col gap-3">
<div class="flex justify-between items-center">
<span class="text-sm font-sans text-gray-600">Total Meetings</span>
<span class="text-base font-display font-semibold text-gray-900">{{ meetings().length }}</span>
<span class="text-base font-display font-semibold text-gray-900">{{ meetingsCount() }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm font-sans text-gray-600">Public</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export class MeetingDashboardComponent {
public committeeFilter: WritableSignal<string | null>;
public meetingsLoading: WritableSignal<boolean>;
public meetings: Signal<Meeting[]>;
public meetingsCount: Signal<number>;
public upcomingMeetings: Signal<(MeetingOccurrence & { meeting: Meeting })[]>;
public pastMeetingsLoading: WritableSignal<boolean>;
public pastMeetings: Signal<PastMeeting[]>;
Expand All @@ -79,6 +80,7 @@ export class MeetingDashboardComponent {
this.pastMeetingsLoading = signal<boolean>(true);
this.refresh = new BehaviorSubject<void>(undefined);
this.meetings = this.initializeMeetings();
this.meetingsCount = this.initializeMeetingsCount();
this.upcomingMeetings = this.initializeUpcomingMeetings();
this.pastMeetings = this.initializePastMeetings();
this.searchForm = this.initializeSearchForm();
Expand Down Expand Up @@ -181,6 +183,12 @@ export class MeetingDashboardComponent {
);
}

private initializeMeetingsCount(): Signal<number> {
return toSignal(this.project() ? this.refresh.pipe(switchMap(() => this.meetingService.getMeetingsCountByProject(this.project()!.uid))) : of(0), {
initialValue: 0,
});
}

private initializeUpcomingMeetings(): Signal<(MeetingOccurrence & { meeting: Meeting })[]> {
return computed(() => {
return this.meetings().flatMap((m) => m.occurrences.map((o) => ({ ...o, meeting: m })));
Expand Down
15 changes: 14 additions & 1 deletion apps/lfx-one/src/app/shared/services/committee.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { inject, Injectable, signal, WritableSignal } from '@angular/core';
import { Committee, CommitteeMember, CreateCommitteeMemberRequest } from '@lfx-one/shared/interfaces';
import { catchError, Observable, of, take, tap, throwError } from 'rxjs';
import { catchError, Observable, of, switchMap, take, tap, throwError } from 'rxjs';

@Injectable({
providedIn: 'root',
Expand All @@ -29,6 +29,19 @@ export class CommitteeService {
return this.getCommittees(params);
}

public getCommitteesCountByProject(projectId: string): Observable<number> {
const params = new HttpParams().set('tags', `project_uid:${projectId}`);
return this.http
.get<{ count: number }>('/api/committees/count', { params })
.pipe(
catchError((error) => {
console.error('Failed to load committees count:', error);
return of({ count: 0 });
})
)
.pipe(switchMap((response) => of(response.count)));
}

public getRecentCommitteesByProject(projectId: string): Observable<Committee[]> {
return this.getCommitteesByProject(projectId);
}
Expand Down
16 changes: 16 additions & 0 deletions apps/lfx-one/src/app/shared/services/meeting.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,22 @@ export class MeetingService {
return this.getMeetings(params);
}

public getMeetingsCountByProject(projectId: string): Observable<number> {
const params = new HttpParams().set('tags', `project_uid:${projectId}`);
return this.http
.get<{ count: number }>('/api/meetings/count', { params })
.pipe(
catchError((error) => {
console.error('Failed to load meetings count:', error);
return of({ count: 0 });
})
)
.pipe(
// Extract just the count number from the response
switchMap((response) => of(response.count))
);
}

public getRecentMeetingsByProject(projectId: string, limit: number = 3): Observable<Meeting[]> {
return this.getMeetingsByProject(projectId, limit, 'updated_at.desc');
}
Expand Down
22 changes: 22 additions & 0 deletions apps/lfx-one/src/server/controllers/committee.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,28 @@ export class CommitteeController {
}
}

/**
* GET /committees/count
*/
public async getCommitteesCount(req: Request, res: Response, next: NextFunction): Promise<void> {
const startTime = Logger.start(req, 'get_committees_count', {
query_params: Logger.sanitize(req.query as Record<string, any>),
});

try {
const count = await this.committeeService.getCommitteesCount(req, req.query);

Logger.success(req, 'get_committees_count', startTime, {
count,
});

res.json({ count });
} catch (error) {
Logger.error(req, 'get_committees_count', startTime, error);
next(error);
}
}

/**
* GET /committees/:id
*/
Expand Down
26 changes: 26 additions & 0 deletions apps/lfx-one/src/server/controllers/meeting.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,32 @@ export class MeetingController {
}
}

/**
* GET /meetings/count
*/
public async getMeetingsCount(req: Request, res: Response, next: NextFunction): Promise<void> {
const startTime = Logger.start(req, 'get_meetings_count', {
query_params: Logger.sanitize(req.query as Record<string, any>),
});

try {
// Get the meetings count
const count = await this.meetingService.getMeetingsCount(req, req.query as Record<string, any>);

// Log the success
Logger.success(req, 'get_meetings_count', startTime, {
count,
});

// Send the count to the client
res.json({ count });
} catch (error) {
// Log the error
Logger.error(req, 'get_meetings_count', startTime, error);
next(error);
}
}

/**
* GET /meetings/:uid
*/
Expand Down
10 changes: 4 additions & 6 deletions apps/lfx-one/src/server/controllers/project.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,10 @@ export class ProjectController {
const projects = await this.projectService.getProjects(req, req.query as Record<string, any>);

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

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

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

Expand Down
1 change: 1 addition & 0 deletions apps/lfx-one/src/server/routes/committees.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const committeeController = new CommitteeController();

// Committee CRUD routes - using new controller pattern
router.get('/', (req, res, next) => committeeController.getCommittees(req, res, next));
router.get('/count', (req, res, next) => committeeController.getCommitteesCount(req, res, next));
router.get('/:id', (req, res, next) => committeeController.getCommitteeById(req, res, next));
router.post('/', (req, res, next) => committeeController.createCommittee(req, res, next));
router.put('/:id', (req, res, next) => committeeController.updateCommittee(req, res, next));
Expand Down
3 changes: 3 additions & 0 deletions apps/lfx-one/src/server/routes/meetings.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ const meetingController = new MeetingController();
// GET /meetings - get all meetings
router.get('/', (req, res, next) => meetingController.getMeetings(req, res, next));

// GET /meetings/count - get meetings count
router.get('/count', (req, res, next) => meetingController.getMeetingsCount(req, res, next));

// GET /meetings/:uid - get a single meeting
router.get('/:uid', (req, res, next) => meetingController.getMeetingById(req, res, next));

Expand Down
15 changes: 15 additions & 0 deletions apps/lfx-one/src/server/services/committee.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
CommitteeSettingsData,
CommitteeUpdateData,
CreateCommitteeMemberRequest,
QueryServiceCountResponse,
QueryServiceResponse,
} from '@lfx-one/shared/interfaces';
import { Request } from 'express';
Expand Down Expand Up @@ -49,6 +50,20 @@ export class CommitteeService {
return await this.accessCheckService.addAccessToResources(req, committees, 'committee');
}

/**
* Fetches the count of committees based on query parameters
*/
public async getCommitteesCount(req: Request, query: Record<string, any> = {}): Promise<number> {
const params = {
...query,
type: 'committee',
};

const { count } = await this.microserviceProxy.proxyRequest<QueryServiceCountResponse>(req, 'LFX_V2_SERVICE', '/query/resources/count', 'GET', params);

return count;
}

/**
* Fetches a single committee by ID
*/
Expand Down
15 changes: 15 additions & 0 deletions apps/lfx-one/src/server/services/meeting.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
MeetingRegistrant,
PastMeetingParticipant,
QueryServiceResponse,
QueryServiceCountResponse,
UpdateMeetingRegistrantRequest,
UpdateMeetingRequest,
} from '@lfx-one/shared/interfaces';
Expand Down Expand Up @@ -63,6 +64,20 @@ export class MeetingService {
return meetings;
}

/**
* Fetches the count of meetings based on query parameters
*/
public async getMeetingsCount(req: Request, query: Record<string, any> = {}, meetingType: string = 'meeting'): Promise<number> {
const params = {
...query,
type: meetingType,
};

const { count } = await this.microserviceProxy.proxyRequest<QueryServiceCountResponse>(req, 'LFX_V2_SERVICE', '/query/resources/count', 'GET', params);

return count;
}

/**
* Fetches a single meeting by UID
*/
Expand Down
Loading