Skip to content

Commit a08bc3a

Browse files
asithadeclaude
andauthored
fix(meetings): use direct endpoint calls and fix past meeting display (#101)
* fix(meetings): use direct endpoint calls in getMeetingById (LFXV2-561) - Replace query service calls with direct endpoint calls to /{meetingType}/{uid} - Update meeting type parameters from 'meeting'/'past_meeting' to 'meetings'/'past_meetings' - Simplify error handling by checking meeting.uid instead of resources array - Remove redundant multiple meeting warnings since direct endpoint calls return single objects - Maintain backward compatibility with access control and committee data fetching Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <asithade@gmail.com> * fix(meetings): add PastMeeting type and fix time display - Create MeetingSession and PastMeeting interfaces for proper typing - Fix past meeting time display by handling scheduled_start_time field - Add shared getCurrentOrNextOccurrence utility to follow DRY principle - Update services and components to use PastMeeting type instead of Meeting - Remove 'as any' casts for better type safety - Implement proper occurrence priority logic in meeting cards Related JIRA: - LFXV2-564: Add PastMeeting interface and improve type safety - LFXV2-565: Fix past meeting time display in meeting cards - LFXV2-566: Create shared utility for meeting occurrence logic 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Asitha de Silva <asithade@gmail.com> --------- Signed-off-by: Asitha de Silva <asithade@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 956af0d commit a08bc3a

File tree

10 files changed

+154
-72
lines changed

10 files changed

+154
-72
lines changed

apps/lfx-one/src/app/modules/meeting/meeting-join/meeting-join.component.ts

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { ExpandableTextComponent } from '@components/expandable-text/expandable-
1515
import { InputTextComponent } from '@components/input-text/input-text.component';
1616
import { MessageComponent } from '@components/message/message.component';
1717
import { environment } from '@environments/environment';
18-
import { extractUrlsWithDomains, Meeting, MeetingOccurrence, Project, User } from '@lfx-one/shared';
18+
import { extractUrlsWithDomains, getCurrentOrNextOccurrence, Meeting, MeetingOccurrence, Project, User } from '@lfx-one/shared';
1919
import { MeetingTimePipe } from '@pipes/meeting-time.pipe';
2020
import { MeetingService } from '@services/meeting.service';
2121
import { UserService } from '@services/user.service';
@@ -149,32 +149,7 @@ export class MeetingJoinComponent {
149149
private initializeCurrentOccurrence(): Signal<MeetingOccurrence | null> {
150150
return computed(() => {
151151
const meeting = this.meeting();
152-
if (!meeting?.occurrences || meeting.occurrences.length === 0) {
153-
return null;
154-
}
155-
156-
const now = new Date();
157-
const earlyJoinMinutes = meeting.early_join_time_minutes || 10;
158-
159-
// Find the first occurrence that is currently joinable (within the join window)
160-
const joinableOccurrence = meeting.occurrences.find((occurrence) => {
161-
const startTime = new Date(occurrence.start_time);
162-
const earliestJoinTime = new Date(startTime.getTime() - earlyJoinMinutes * 60000);
163-
const latestJoinTime = new Date(startTime.getTime() + occurrence.duration * 60000 + 40 * 60000); // 40 minutes after end
164-
165-
return now >= earliestJoinTime && now <= latestJoinTime;
166-
});
167-
168-
if (joinableOccurrence) {
169-
return joinableOccurrence;
170-
}
171-
172-
// If no joinable occurrence, find the next future occurrence
173-
const futureOccurrences = meeting.occurrences
174-
.filter((occurrence) => new Date(occurrence.start_time) > now)
175-
.sort((a, b) => new Date(a.start_time).getTime() - new Date(b.start_time).getTime());
176-
177-
return futureOccurrences.length > 0 ? futureOccurrences[0] : null;
152+
return getCurrentOrNextOccurrence(meeting);
178153
});
179154
}
180155

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,10 @@ <h3 class="text-xl font-display font-semibold text-gray-900 mb-1 leading-tight"
9393
</h3>
9494
}
9595
</div>
96-
@if (occurrence()?.start_time || meeting().start_time) {
96+
@if (meetingStartTime()) {
9797
<div class="flex items-center gap-1 text-xs text-gray-600 bg-gray-100 px-2 py-1 rounded flex-shrink-0" data-testid="meeting-datetime">
9898
<i class="fa-light fa-calendar-days text-gray-400"></i>
99-
<span
100-
>{{ occurrence()?.start_time || meeting().start_time | meetingTime: meeting().duration : 'date' }} •
101-
{{ occurrence()?.start_time || meeting().start_time | meetingTime: meeting().duration : 'time' }}</span
102-
>
99+
<span>{{ meetingStartTime() | meetingTime: meeting().duration : 'date' }} • {{ meetingStartTime() | meetingTime: meeting().duration : 'time' }}</span>
103100
</div>
104101
}
105102
</div>

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

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,16 @@ import { ButtonComponent } from '@components/button/button.component';
1515
import { ExpandableTextComponent } from '@components/expandable-text/expandable-text.component';
1616
import { MenuComponent } from '@components/menu/menu.component';
1717
import { environment } from '@environments/environment';
18-
import { extractUrlsWithDomains, Meeting, MeetingAttachment, MeetingOccurrence, MeetingRegistrant, PastMeetingParticipant } from '@lfx-one/shared';
18+
import {
19+
extractUrlsWithDomains,
20+
getCurrentOrNextOccurrence,
21+
Meeting,
22+
MeetingAttachment,
23+
MeetingOccurrence,
24+
MeetingRegistrant,
25+
PastMeeting,
26+
PastMeetingParticipant,
27+
} from '@lfx-one/shared';
1928
import { MeetingTimePipe } from '@pipes/meeting-time.pipe';
2029
import { MeetingService } from '@services/meeting.service';
2130
import { ProjectService } from '@services/project.service';
@@ -61,15 +70,15 @@ export class MeetingCardComponent implements OnInit {
6170
private readonly injector = inject(Injector);
6271
private readonly clipboard = inject(Clipboard);
6372

64-
public readonly meetingInput = input.required<Meeting>();
73+
public readonly meetingInput = input.required<Meeting | PastMeeting>();
6574
public readonly occurrenceInput = input<MeetingOccurrence | null>(null);
6675
public readonly pastMeeting = input<boolean>(false);
6776
public readonly loading = input<boolean>(false);
6877
public readonly showBorder = input<boolean>(false);
6978
public readonly meetingRegistrantCount: Signal<number> = this.initMeetingRegistrantCount();
7079
public readonly registrantResponseBreakdown: Signal<string> = this.initRegistrantResponseBreakdown();
7180
public showRegistrants: WritableSignal<boolean> = signal(false);
72-
public meeting: WritableSignal<Meeting> = signal({} as Meeting);
81+
public meeting: WritableSignal<Meeting | PastMeeting> = signal({} as Meeting | PastMeeting);
7382
public occurrence: WritableSignal<MeetingOccurrence | null> = signal(null);
7483
public registrantsLoading: WritableSignal<boolean> = signal(true);
7584
private refresh$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
@@ -91,6 +100,8 @@ export class MeetingCardComponent implements OnInit {
91100
public readonly attendedCount: Signal<number> = this.initAttendedCount();
92101
public readonly notAttendedCount: Signal<number> = this.initNotAttendedCount();
93102
public readonly participantCount: Signal<number> = this.initParticipantCount();
103+
public readonly currentOccurrence: Signal<MeetingOccurrence | null> = this.initCurrentOccurrence();
104+
public readonly meetingStartTime: Signal<string | null> = this.initMeetingStartTime();
94105

95106
public readonly meetingDeleted = output<void>();
96107
public readonly project = this.projectService.project;
@@ -103,8 +114,16 @@ export class MeetingCardComponent implements OnInit {
103114
public constructor() {
104115
effect(() => {
105116
this.meeting.set(this.meetingInput());
117+
// Priority: explicit occurrenceInput > current occurrence for upcoming > null for past without input
106118
if (this.occurrenceInput()) {
119+
// If explicitly passed an occurrence, always use it
107120
this.occurrence.set(this.occurrenceInput()!);
121+
} else if (!this.pastMeeting()) {
122+
// For upcoming meetings without explicit occurrence, use current occurrence
123+
this.occurrence.set(this.currentOccurrence());
124+
} else {
125+
// For past meetings without occurrence input, set to null
126+
this.occurrence.set(null);
108127
}
109128
});
110129
}
@@ -604,4 +623,43 @@ export class MeetingCardComponent implements OnInit {
604623
return this.meeting()?.participant_count || 0;
605624
});
606625
}
626+
627+
private initCurrentOccurrence(): Signal<MeetingOccurrence | null> {
628+
return computed(() => {
629+
const meeting = this.meeting();
630+
return getCurrentOrNextOccurrence(meeting);
631+
});
632+
}
633+
634+
private initMeetingStartTime(): Signal<string | null> {
635+
return computed(() => {
636+
const meeting = this.meeting();
637+
638+
if (!this.pastMeeting()) {
639+
// For upcoming meetings, use current occurrence (next upcoming occurrence) or meeting start_time
640+
const currentOccurrence = this.occurrence();
641+
if (currentOccurrence?.start_time) {
642+
return currentOccurrence.start_time;
643+
}
644+
if (meeting?.start_time) {
645+
return meeting.start_time;
646+
}
647+
} else {
648+
// For past meetings, use occurrence input or fallback to scheduled_start_time/start_time
649+
const occurrence = this.occurrence();
650+
if (occurrence?.start_time) {
651+
return occurrence.start_time;
652+
}
653+
if (meeting?.start_time) {
654+
return meeting.start_time;
655+
}
656+
// Handle past meetings that use scheduled_start_time (type-safe check)
657+
if ('scheduled_start_time' in meeting && meeting.scheduled_start_time) {
658+
return meeting.scheduled_start_time;
659+
}
660+
}
661+
662+
return null;
663+
});
664+
}
607665
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { InputTextComponent } from '@components/input-text/input-text.component'
1212
import { MenuComponent } from '@components/menu/menu.component';
1313
import { SelectButtonComponent } from '@components/select-button/select-button.component';
1414
import { SelectComponent } from '@components/select/select.component';
15-
import { CalendarEvent, Meeting, MeetingOccurrence } from '@lfx-one/shared/interfaces';
15+
import { CalendarEvent, Meeting, MeetingOccurrence, PastMeeting } from '@lfx-one/shared/interfaces';
1616
import { MeetingService } from '@services/meeting.service';
1717
import { ProjectService } from '@services/project.service';
1818
import { AnimateOnScrollModule } from 'primeng/animateonscroll';
@@ -57,11 +57,11 @@ export class MeetingDashboardComponent {
5757
public meetings: Signal<Meeting[]>;
5858
public upcomingMeetings: Signal<(MeetingOccurrence & { meeting: Meeting })[]>;
5959
public pastMeetingsLoading: WritableSignal<boolean>;
60-
public pastMeetings: Signal<Meeting[]>;
60+
public pastMeetings: Signal<PastMeeting[]>;
6161
public meetingListView: WritableSignal<'upcoming' | 'past'>;
6262
public visibilityOptions: Signal<{ label: string; value: string | null }[]>;
6363
public committeeOptions: Signal<{ label: string; value: string | null }[]>;
64-
public filteredMeetings: Signal<Meeting[]>;
64+
public filteredMeetings: Signal<Meeting[] | PastMeeting[]>;
6565
public publicMeetingsCount: Signal<number>;
6666
public privateMeetingsCount: Signal<number>;
6767
public menuItems: MenuItem[];
@@ -187,7 +187,7 @@ export class MeetingDashboardComponent {
187187
});
188188
}
189189

190-
private initializePastMeetings(): Signal<Meeting[]> {
190+
private initializePastMeetings(): Signal<PastMeeting[]> {
191191
return toSignal(
192192
this.project()
193193
? this.refresh.pipe(

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
MeetingJoinURL,
1616
MeetingRegistrant,
1717
MeetingRegistrantWithState,
18+
PastMeeting,
1819
PastMeetingParticipant,
1920
Project,
2021
UpdateMeetingRegistrantRequest,
@@ -40,8 +41,8 @@ export class MeetingService {
4041
);
4142
}
4243

43-
public getPastMeetings(params?: HttpParams): Observable<Meeting[]> {
44-
return this.http.get<Meeting[]>('/api/past-meetings', { params }).pipe(
44+
public getPastMeetings(params?: HttpParams): Observable<PastMeeting[]> {
45+
return this.http.get<PastMeeting[]>('/api/past-meetings', { params }).pipe(
4546
catchError((error) => {
4647
console.error('Failed to load past meetings:', error);
4748
return of([]);
@@ -78,7 +79,7 @@ export class MeetingService {
7879
return this.getMeetings(params);
7980
}
8081

81-
public getPastMeetingsByProject(projectId: string, limit: number = 3): Observable<Meeting[]> {
82+
public getPastMeetingsByProject(projectId: string, limit: number = 3): Observable<PastMeeting[]> {
8283
let params = new HttpParams().set('tags', `project_uid:${projectId}`);
8384

8485
if (limit) {
@@ -98,8 +99,8 @@ export class MeetingService {
9899
);
99100
}
100101

101-
public getPastMeeting(id: string): Observable<Meeting> {
102-
return this.http.get<Meeting>(`/api/past-meetings/${id}`).pipe(
102+
public getPastMeeting(id: string): Observable<PastMeeting> {
103+
return this.http.get<PastMeeting>(`/api/past-meetings/${id}`).pipe(
103104
catchError((error) => {
104105
console.error(`Failed to load past meeting ${id}:`, error);
105106
return throwError(() => error);

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

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

44
import { NextFunction, Request, Response } from 'express';
55

6+
import { PastMeeting } from '@lfx-one/shared/interfaces';
67
import { Logger } from '../helpers/logger';
78
import { validateUidParameter } from '../helpers/validation.helper';
89
import { MeetingService } from '../services/meeting.service';
@@ -23,7 +24,7 @@ export class PastMeetingController {
2324

2425
try {
2526
// Get the past meetings using meetingType 'past_meeting'
26-
const meetings = await this.meetingService.getMeetings(req, req.query as Record<string, any>, 'past_meeting');
27+
const meetings = (await this.meetingService.getMeetings(req, req.query as Record<string, any>, 'past_meeting')) as PastMeeting[];
2728

2829
// TODO: Remove this once we have a way to get the registrants count
2930
// Process each meeting individually to add registrant and participant counts
@@ -73,7 +74,7 @@ export class PastMeetingController {
7374
}
7475

7576
// Get the past meeting by ID using meetingType 'past_meeting'
76-
const meeting = await this.meetingService.getMeetingById(req, uid, 'past_meeting');
77+
const meeting = (await this.meetingService.getMeetingById(req, uid, 'past_meetings')) as PastMeeting;
7778

7879
// Log the success
7980
Logger.success(req, 'get_past_meeting_by_id', startTime, {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ export class PublicMeetingController {
203203
*/
204204
private async fetchMeetingWithM2M(req: Request, id: string) {
205205
await this.setupM2MToken(req);
206-
return await this.meetingService.getMeetingById(req, id, 'meeting', false);
206+
return await this.meetingService.getMeetingById(req, id, 'meetings', false);
207207
}
208208

209209
/**

apps/lfx-one/src/server/services/meeting.service.ts

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -66,44 +66,28 @@ export class MeetingService {
6666
/**
6767
* Fetches a single meeting by UID
6868
*/
69-
public async getMeetingById(req: Request, meetingUid: string, meetingType: string = 'meeting', access: boolean = true): Promise<Meeting> {
70-
const params = {
71-
type: meetingType,
72-
tags: `meeting_uid:${meetingUid}`,
73-
};
69+
public async getMeetingById(req: Request, meetingUid: string, meetingType: string = 'meetings', access: boolean = true): Promise<Meeting> {
70+
let meeting = await this.microserviceProxy.proxyRequest<Meeting>(req, 'LFX_V2_SERVICE', `/${meetingType}/${meetingUid}`, 'GET');
7471

75-
const { resources } = await this.microserviceProxy.proxyRequest<QueryServiceResponse<Meeting>>(req, 'LFX_V2_SERVICE', '/query/resources', 'GET', params);
76-
77-
if (!resources || resources.length === 0) {
72+
if (!meeting || !meeting.uid) {
7873
throw new ResourceNotFoundError('Meeting', meetingUid, {
7974
operation: 'get_meeting_by_id',
8075
service: 'meeting_service',
8176
path: `/meetings/${meetingUid}`,
8277
});
8378
}
8479

85-
if (resources.length > 1) {
86-
req.log.warn(
87-
{
88-
meeting_uid: meetingUid,
89-
result_count: resources.length,
90-
},
91-
'Multiple meetings found for single UID lookup'
92-
);
93-
}
94-
95-
let meeting = resources.map((resource) => resource.data);
96-
97-
if (meeting[0].committees && meeting[0].committees.length > 0) {
98-
meeting = await this.getMeetingCommittees(req, meeting);
80+
if (meeting.committees && meeting.committees.length > 0) {
81+
const meetingWithCommittees = await this.getMeetingCommittees(req, [meeting]);
82+
meeting = meetingWithCommittees[0];
9983
}
10084

10185
if (access) {
10286
// Add writer access field to the meeting
103-
return await this.accessCheckService.addAccessToResource(req, meeting[0], 'meeting', 'organizer');
87+
return await this.accessCheckService.addAccessToResource(req, meeting, 'meeting', 'organizer');
10488
}
10589

106-
return meeting[0];
90+
return meeting;
10791
}
10892

10993
/**

packages/shared/src/interfaces/meeting.interface.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,38 @@ export interface RecurrenceSummary {
480480
fullSummary: string;
481481
}
482482

483+
/**
484+
* Meeting session information
485+
* @description Individual session within a meeting (typically for past meetings)
486+
*/
487+
export interface MeetingSession {
488+
/** Session unique identifier */
489+
uid: string;
490+
/** Session start time */
491+
start_time: string;
492+
/** Session end time */
493+
end_time: string;
494+
}
495+
496+
/**
497+
* Past meeting interface
498+
* @description Extended meeting interface with additional fields specific to past meetings
499+
*/
500+
export interface PastMeeting extends Meeting {
501+
/** Scheduled start time for past meetings */
502+
scheduled_start_time: string;
503+
/** Scheduled end time for past meetings */
504+
scheduled_end_time: string;
505+
/** Original meeting UID (different from uid which is the past meeting occurrence UID) */
506+
meeting_uid: string;
507+
/** The specific occurrence ID for recurring meetings */
508+
occurrence_id: string;
509+
/** Platform-specific meeting ID (e.g., Zoom meeting ID) */
510+
platform_meeting_id: string;
511+
/** Array of session objects with start/end times */
512+
sessions: MeetingSession[];
513+
}
514+
483515
/**
484516
* Past meeting participant information
485517
* @description Individual participant who was invited/attended a past meeting

0 commit comments

Comments
 (0)