Skip to content

Commit bb81511

Browse files
authored
feat(meetings): add meeting rsvp functionality with dynamic count calculation (#135)
* feat(meetings): add rsvp functionality with dynamic count calculation Add comprehensive RSVP support for meetings with the following features: - RSVP buttons (Yes/Maybe/No) in meeting cards with visual state - Scope selection modal for recurring meetings (this/all/following) - Dynamic RSVP count calculation respecting scope rules and recency - Backend endpoints for creating and fetching RSVPs - RSVP calculator utility handling occurrence-specific logic - Token-based user identification (backend derives user from bearer token) The RSVP system correctly handles: - Non-recurring meetings with simple scope - Recurring meetings with occurrence-specific RSVPs - Precedence by most recent applicable RSVP per user - Scope types: 'this' (single), 'all' (entire series), 'following' (future) Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Andres Tobon <andrest2455@gmail.com> * refactor(meetings): improve rsvp implementation with signal-based approach - Convert RSVP loading from effects to signal-based reactive streams - Add occurrence_id field to MeetingRsvp interface for proper typing - Improve RSVP matching logic with case-insensitive comparison - Add RSVP refresh coordination across occurrence cards - Display meeting context in RSVP scope modal - Change RsvpScope from 'this' to 'single' for clarity - Fix nested ternary expression for better readability - Remove debug console.log statements - Add username validation in getUserMeetingRsvp 🤖 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Andres Tobon <andrest2455@gmail.com> * fix(meetings): use occurrence-scoped skip for rsvp refresh - Change skip logic from meetingUid to composite key (meetingUid:occurrenceId) - Fix RSVP refresh to only skip the specific occurrence that was clicked - Update event payload to include occurrenceId - Fix rsvpFetchTrigger to return structured data instead of string concatenation - Prevent UUID corruption from hyphen-based string splitting This ensures all occurrence cards properly update their RSVP status when scope is "all" or "following", except the specific occurrence that was just clicked. 🤖 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Andres Tobon <andrest2455@gmail.com> * refactor(meetings): consolidate rsvp interfaces and constants to shared package - Move RsvpScopeOption interface to shared package for reusability - Move RsvpCounts interface to shared package interfaces - Add RSVP_SCOPE_OPTIONS constant to shared package constants - Update RSVP scope modal to use shared constants and interfaces - Update dashboard meeting card to use ngClass for RSVP buttons - Add pipe(take(1)) to dialog close subscription to prevent memory leaks - Remove unnecessary DialogService provider from dashboard meeting card - Simplify skip refresh key logic in my-meetings component - Fix max-len linting error in meeting-registrants component - Update registrant card to use ngClass for icon styling - Improve type safety with proper RsvpResponse type annotation Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Andres Tobon <andrest2455@gmail.com> * feat(meetings): add include_rsvp query parameter to optimize registrant fetching - Add optional include_rsvp query parameter to GET /meetings/:uid/registrants endpoint - Backend service fetches and attaches RSVP data when include_rsvp=true - Add optional rsvp field to MeetingRegistrant interface - Optimize meeting-registrants component to use single API call with include_rsvp=true - Remove forkJoin pattern and complex RSVP matching logic from frontend - Reduce code complexity by ~70 lines in meeting-registrants component - Improve performance by reducing network requests from 2 to 1 - Update frontend service to accept includeRsvp parameter (defaults to false) - Add comprehensive error handling and logging for RSVP fetching - Maintain backward compatibility with existing API consumers Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Andres Tobon <andrest2455@gmail.com> * refactor(dashboards): remove RSVP submission UI from dashboard Remove the ability to submit RSVPs directly from the dashboard meeting cards. This functionality will be refactored and reimplemented in a future iteration. Changes: - Remove RSVP buttons (Yes/Maybe/No) from dashboard meeting cards - Delete RSVP scope modal component - Remove RSVP submission handlers and refresh logic - Clean up unused inputs (refreshTrigger, skipRefreshKey) - Remove isRecurringMeeting computed signal - Add update:current_user_metadata scope for future user profile updates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Andres Tobon <andrest2455@gmail.com> * refactor(dashboards): remove RSVP submission UI from dashboard Remove the ability to submit RSVPs directly from the dashboard meeting cards. This functionality will be refactored and reimplemented in a future iteration. Changes: - Remove RSVP buttons (Yes/Maybe/No) from dashboard meeting cards - Delete RSVP scope modal component - Remove RSVP submission handlers and refresh logic - Clean up unused inputs (refreshTrigger, skipRefreshKey) - Remove isRecurringMeeting computed signal - Add update:current_user_metadata scope for future user profile updates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Andres Tobon <andrest2455@gmail.com> * Remove some more unused variables Signed-off-by: Andres Tobon <andrest2455@gmail.com> --------- Signed-off-by: Andres Tobon <andrest2455@gmail.com>
1 parent 2a98ab7 commit bb81511

File tree

8 files changed

+541
-17
lines changed

8 files changed

+541
-17
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ export class MeetingRegistrantsComponent implements OnInit {
300300
if (!uid) return;
301301

302302
this.meetingService
303-
.getMeetingRegistrants(uid)
303+
.getMeetingRegistrants(uid, false)
304304
.pipe(
305305
take(1),
306306
catchError((error) => {

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

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import {
88
BatchRegistrantOperationResponse,
99
CreateMeetingRegistrantRequest,
1010
CreateMeetingRequest,
11+
CreateMeetingRsvpRequest,
1112
GenerateAgendaRequest,
1213
GenerateAgendaResponse,
1314
Meeting,
1415
MeetingAttachment,
1516
MeetingJoinURL,
1617
MeetingRegistrant,
1718
MeetingRegistrantWithState,
19+
MeetingRsvp,
1820
PastMeeting,
1921
PastMeetingParticipant,
2022
PastMeetingRecording,
@@ -304,8 +306,9 @@ export class MeetingService {
304306
);
305307
}
306308

307-
public getMeetingRegistrants(meetingUid: string): Observable<MeetingRegistrant[]> {
308-
return this.http.get<MeetingRegistrant[]>(`/api/meetings/${meetingUid}/registrants`).pipe(
309+
public getMeetingRegistrants(meetingUid: string, includeRsvp: boolean = false): Observable<MeetingRegistrant[]> {
310+
const params = new HttpParams().set('include_rsvp', includeRsvp.toString());
311+
return this.http.get<MeetingRegistrant[]>(`/api/meetings/${meetingUid}/registrants`, { params }).pipe(
309312
catchError((error) => {
310313
console.error(`Failed to load registrants for meeting ${meetingUid}:`, error);
311314
return of([]);
@@ -438,6 +441,34 @@ export class MeetingService {
438441
);
439442
}
440443

444+
public createMeetingRsvp(meetingUid: string, request: CreateMeetingRsvpRequest): Observable<MeetingRsvp> {
445+
return this.http.post<MeetingRsvp>(`/api/meetings/${meetingUid}/rsvp`, request).pipe(
446+
take(1),
447+
catchError((error) => {
448+
console.error(`Failed to create RSVP for meeting ${meetingUid}:`, error);
449+
return throwError(() => error);
450+
})
451+
);
452+
}
453+
454+
public getUserMeetingRsvp(meetingUid: string): Observable<MeetingRsvp | null> {
455+
return this.http.get<MeetingRsvp | null>(`/api/meetings/${meetingUid}/rsvp`).pipe(
456+
catchError((error) => {
457+
console.error(`Failed to get RSVP for meeting ${meetingUid}:`, error);
458+
return of(null);
459+
})
460+
);
461+
}
462+
463+
public getMeetingRsvps(meetingUid: string): Observable<MeetingRsvp[]> {
464+
return this.http.get<MeetingRsvp[]>(`/api/meetings/${meetingUid}/rsvps`).pipe(
465+
catchError((error) => {
466+
console.error(`Failed to get RSVPs for meeting ${meetingUid}:`, error);
467+
return of([]);
468+
})
469+
);
470+
}
471+
441472
private readFileAsBase64(file: File): Promise<string> {
442473
return new Promise((resolve, reject) => {
443474
const reader = new FileReader();

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

Lines changed: 139 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
BatchRegistrantOperationResponse,
66
CreateMeetingRegistrantRequest,
77
CreateMeetingRequest,
8+
CreateMeetingRsvpRequest,
89
UpdateMeetingRegistrantRequest,
910
UpdateMeetingRequest,
1011
} from '@lfx-one/shared/interfaces';
@@ -289,8 +290,12 @@ export class MeetingController {
289290
*/
290291
public async getMeetingRegistrants(req: Request, res: Response, next: NextFunction): Promise<void> {
291292
const { uid } = req.params;
293+
const { include_rsvp } = req.query;
294+
const includeRsvp = include_rsvp === 'true';
295+
292296
const startTime = Logger.start(req, 'get_meeting_registrants', {
293297
meeting_uid: uid,
298+
include_rsvp: includeRsvp,
294299
});
295300

296301
try {
@@ -306,11 +311,12 @@ export class MeetingController {
306311
}
307312

308313
// Get the meeting registrants
309-
const registrants = await this.meetingService.getMeetingRegistrants(req, uid);
314+
const registrants = await this.meetingService.getMeetingRegistrants(req, uid, includeRsvp);
310315

311316
Logger.success(req, 'get_meeting_registrants', startTime, {
312317
meeting_uid: uid,
313318
registrant_count: registrants.length,
319+
include_rsvp: includeRsvp,
314320
});
315321

316322
// Send the registrants data to the client
@@ -319,6 +325,7 @@ export class MeetingController {
319325
// Log the error
320326
Logger.error(req, 'get_meeting_registrants', startTime, error, {
321327
meeting_uid: uid,
328+
include_rsvp: includeRsvp,
322329
});
323330

324331
// Send the error to the next middleware
@@ -723,6 +730,137 @@ export class MeetingController {
723730
}
724731
}
725732

733+
/**
734+
* POST /meetings/:uid/rsvp
735+
*/
736+
public async createMeetingRsvp(req: Request, res: Response, next: NextFunction): Promise<void> {
737+
const { uid } = req.params;
738+
const rsvpData: CreateMeetingRsvpRequest = req.body;
739+
740+
const startTime = Logger.start(req, 'create_meeting_rsvp', {
741+
meeting_uid: uid,
742+
registrant_id: rsvpData.registrant_id,
743+
response: rsvpData.response,
744+
scope: rsvpData.scope,
745+
});
746+
747+
try {
748+
// Validate meeting UID
749+
if (
750+
!validateUidParameter(uid, req, next, {
751+
operation: 'create_meeting_rsvp',
752+
})
753+
) {
754+
return;
755+
}
756+
757+
// Validate RSVP data
758+
if (!rsvpData.response || !rsvpData.scope) {
759+
throw ServiceValidationError.fromFieldErrors(
760+
{
761+
response: !rsvpData.response ? 'Response is required' : [],
762+
scope: !rsvpData.scope ? 'Scope is required' : [],
763+
},
764+
'RSVP data validation failed',
765+
{
766+
operation: 'create_meeting_rsvp',
767+
service: 'meeting_controller',
768+
}
769+
);
770+
}
771+
772+
// Create the RSVP
773+
const rsvp = await this.meetingService.createMeetingRsvp(req, uid, rsvpData);
774+
775+
// Log success
776+
Logger.success(req, 'create_meeting_rsvp', startTime, {
777+
rsvp_id: rsvp.id,
778+
});
779+
780+
// Send response
781+
res.json(rsvp);
782+
} catch (error) {
783+
// Log error
784+
Logger.error(req, 'create_meeting_rsvp', startTime, error);
785+
next(error);
786+
}
787+
}
788+
789+
/**
790+
* GET /meetings/:uid/rsvp
791+
*/
792+
public async getUserMeetingRsvp(req: Request, res: Response, next: NextFunction): Promise<void> {
793+
const { uid } = req.params;
794+
795+
const startTime = Logger.start(req, 'get_user_meeting_rsvp', {
796+
meeting_uid: uid,
797+
});
798+
799+
try {
800+
// Validate meeting UID
801+
if (
802+
!validateUidParameter(uid, req, next, {
803+
operation: 'get_user_meeting_rsvp',
804+
})
805+
) {
806+
return;
807+
}
808+
809+
// Get the user's RSVP
810+
const rsvp = await this.meetingService.getUserMeetingRsvp(req, uid);
811+
812+
// Log success
813+
Logger.success(req, 'get_user_meeting_rsvp', startTime, {
814+
found: !!rsvp,
815+
rsvp_id: rsvp?.id,
816+
});
817+
818+
// Send response
819+
res.json(rsvp);
820+
} catch (error) {
821+
// Log error
822+
Logger.error(req, 'get_user_meeting_rsvp', startTime, error);
823+
next(error);
824+
}
825+
}
826+
827+
/**
828+
* GET /meetings/:uid/rsvps
829+
*/
830+
public async getMeetingRsvps(req: Request, res: Response, next: NextFunction): Promise<void> {
831+
const { uid } = req.params;
832+
833+
const startTime = Logger.start(req, 'get_meeting_rsvps', {
834+
meeting_uid: uid,
835+
});
836+
837+
try {
838+
// Validate meeting UID
839+
if (
840+
!validateUidParameter(uid, req, next, {
841+
operation: 'get_meeting_rsvps',
842+
})
843+
) {
844+
return;
845+
}
846+
847+
// Get all RSVPs for the meeting
848+
const rsvps = await this.meetingService.getMeetingRsvps(req, uid);
849+
850+
// Log success
851+
Logger.success(req, 'get_meeting_rsvps', startTime, {
852+
count: rsvps.length,
853+
});
854+
855+
// Send response
856+
res.json(rsvps);
857+
} catch (error) {
858+
// Log error
859+
Logger.error(req, 'get_meeting_rsvps', startTime, error);
860+
next(error);
861+
}
862+
}
863+
726864
/**
727865
* Private helper to process registrant operations with fail-fast for 403 errors
728866
*/
@@ -779,9 +917,6 @@ export class MeetingController {
779917
}
780918
}
781919

782-
/**
783-
* Private helper to create batch response from results
784-
*/
785920
private createBatchResponse<T, I>(
786921
results: PromiseSettledResult<T>[],
787922
inputData: I[],

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ router.delete('/:uid/registrants', (req, res, next) => meetingController.deleteM
4848
// POST /meetings/:uid/registrants/:registrantId/resend - resend invitation to specific registrant
4949
router.post('/:uid/registrants/:registrantId/resend', (req, res, next) => meetingController.resendMeetingInvitation(req, res, next));
5050

51+
// RSVP routes
52+
router.post('/:uid/rsvp', (req, res, next) => meetingController.createMeetingRsvp(req, res, next));
53+
router.get('/:uid/rsvp', (req, res, next) => meetingController.getUserMeetingRsvp(req, res, next));
54+
router.get('/:uid/rsvps', (req, res, next) => meetingController.getMeetingRsvps(req, res, next));
55+
5156
router.post('/:uid/attachments/upload', async (req: Request, res: Response, next: NextFunction) => {
5257
const startTime = Date.now();
5358
const meetingId = req.params['uid'];

0 commit comments

Comments
 (0)