Skip to content

Commit 5977c97

Browse files
authored
fix(meetings): correct timezone display when editing meetings (#201)
Convert UTC meeting times to meeting's timezone for proper display in the date/time pickers. Previously, times were shown in the user's local timezone, causing confusion when editing meetings set in other zones. - Add formatTo12HourInTimezone utility for timezone-aware formatting - Use toZonedTime to convert UTC dates to meeting timezone - Add X-Sync header to ensure synchronous meeting creation - Refactor attachment operations to process in parallel with updates - Extract processAttachmentOperations and navigateAfterMeetingSave - Add showSubmitAllOperationToast for unified messaging LFXV2-889 Signed-off-by: Asitha de Silva <[email protected]>
1 parent a21c693 commit 5977c97

File tree

3 files changed

+215
-92
lines changed

3 files changed

+215
-92
lines changed

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

Lines changed: 194 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {
3333
} from '@lfx-one/shared/interfaces';
3434
import {
3535
combineDateTime,
36-
formatTo12Hour,
36+
formatTo12HourInTimezone,
3737
generateRecurrenceObject,
3838
getDefaultStartDateTime,
3939
getUserTimezone,
@@ -42,6 +42,7 @@ import {
4242
import { editModeDateTimeValidator, futureDateTimeValidator } from '@lfx-one/shared/validators';
4343
import { MeetingService } from '@services/meeting.service';
4444
import { ProjectContextService } from '@services/project-context.service';
45+
import { toZonedTime } from 'date-fns-tz';
4546
import { ConfirmationService, MessageService } from 'primeng/api';
4647
import { ConfirmDialogModule } from 'primeng/confirmdialog';
4748
import { StepperModule } from 'primeng/stepper';
@@ -264,7 +265,7 @@ export class MeetingManageComponent {
264265
}
265266

266267
public onSubmitAll(): void {
267-
// Edit mode only - save meeting and registrants together using forkJoin
268+
// Edit mode only - save meeting, attachments, and registrants together using forkJoin
268269
if (!this.isEditMode()) {
269270
return;
270271
}
@@ -284,37 +285,69 @@ export class MeetingManageComponent {
284285

285286
// Prepare meeting data
286287
const meetingData = this.prepareMeetingData();
287-
const updateMeeting$ = this.meetingService.updateMeeting(this.meetingId()!, meetingData as UpdateMeetingRequest, 'single');
288+
const meetingId = this.meetingId()!;
289+
const updateMeeting$ = this.meetingService.updateMeeting(meetingId, meetingData as UpdateMeetingRequest, 'single');
288290

289291
// Prepare registrant operations
290292
const registrantOperations = this.buildRegistrantOperations();
291293
const registrants$ = registrantOperations.length > 0 ? concat(...registrantOperations).pipe(toArray()) : of([]);
292294

293-
// Execute both operations in parallel
295+
// Prepare attachment operations
296+
const attachments$ = this.processAttachmentOperations(meetingId);
297+
298+
// Execute all operations in parallel
294299
forkJoin({
295300
meeting: updateMeeting$,
296301
registrants: registrants$,
302+
attachments: attachments$,
297303
})
298304
.pipe(finalize(() => this.submitting.set(false)))
299305
.subscribe({
300-
next: (result: { meeting: Meeting; registrants: { type: string; success: number; failed: number }[] }) => {
306+
next: (result: {
307+
meeting: Meeting;
308+
registrants: { type: string; success: number; failed: number }[];
309+
attachments: {
310+
deletions: { successes: number; failures: string[] };
311+
uploads: { successes: MeetingAttachment[]; failures: { fileName: string; error: any }[] };
312+
links: { successes: MeetingAttachment[]; failures: { linkName: string; error: any }[] };
313+
} | null;
314+
}) => {
301315
const registrantResults = result.registrants;
316+
const attachmentResults = result.attachments;
302317

303318
// Calculate registrant operation results
304-
const totalSuccess = registrantResults.reduce((sum: number, r: { type: string; success: number; failed: number }) => sum + r.success, 0);
305-
const totalFailed = registrantResults.reduce((sum: number, r: { type: string; success: number; failed: number }) => sum + r.failed, 0);
319+
const totalRegistrantSuccess = registrantResults.reduce((sum: number, r: { type: string; success: number; failed: number }) => sum + r.success, 0);
320+
const totalRegistrantFailed = registrantResults.reduce((sum: number, r: { type: string; success: number; failed: number }) => sum + r.failed, 0);
321+
322+
// Calculate attachment operation results
323+
let totalAttachmentSuccess = 0;
324+
let totalAttachmentFailed = 0;
325+
if (attachmentResults) {
326+
totalAttachmentSuccess =
327+
attachmentResults.deletions.successes + attachmentResults.uploads.successes.length + attachmentResults.links.successes.length;
328+
totalAttachmentFailed =
329+
attachmentResults.deletions.failures.length + attachmentResults.uploads.failures.length + attachmentResults.links.failures.length;
330+
331+
// Clear pending deletions when operations complete without failures
332+
if (attachmentResults.deletions.failures.length === 0 && this.pendingAttachmentDeletions().length > 0) {
333+
this.pendingAttachmentDeletions.set([]);
334+
}
306335

307-
// Show success message
308-
if (totalSuccess > 0 || totalFailed > 0) {
309-
this.showRegistrantOperationToast(totalSuccess, totalFailed, totalSuccess + totalFailed);
310-
} else {
311-
this.messageService.add({
312-
severity: 'success',
313-
summary: 'Success',
314-
detail: 'Meeting updated successfully',
336+
// Log individual attachment failures for debugging
337+
attachmentResults.uploads.failures.forEach((failure) => {
338+
console.error(`Failed to upload attachment ${failure.fileName}:`, failure.error);
339+
});
340+
attachmentResults.links.failures.forEach((failure) => {
341+
console.error(`Failed to add link ${failure.linkName}:`, failure.error);
342+
});
343+
attachmentResults.deletions.failures.forEach((attachmentId) => {
344+
console.error(`Failed to delete attachment ${attachmentId}`);
315345
});
316346
}
317347

348+
// Show appropriate success message
349+
this.showSubmitAllOperationToast(totalRegistrantSuccess, totalRegistrantFailed, totalAttachmentSuccess, totalAttachmentFailed);
350+
318351
// Navigate back to meetings list
319352
this.router.navigate(['/meetings']);
320353
},
@@ -445,79 +478,59 @@ export class MeetingManageComponent {
445478
private handleMeetingSuccess(meeting: Meeting): void {
446479
this.meetingId.set(meeting.uid);
447480

448-
// If we're in create mode and not on the last step, continue to next step
449-
if (!this.isEditMode() && this.currentStep() < this.totalSteps) {
481+
// If we're in create mode and before the resources step (step 4), just continue to next step
482+
// We need to process attachments starting from step 4 (Resources & Summary) onwards
483+
if (!this.isEditMode() && this.currentStep() < this.totalSteps - 1) {
450484
this.nextStep();
451485
this.submitting.set(false);
452486
return;
453487
}
454488

455-
const hasPendingDeletions = this.pendingAttachmentDeletions().length > 0;
456-
const hasPendingUploads = this.pendingAttachments.length > 0;
457-
const importantLinksArray = this.form().get('important_links') as FormArray;
458-
const hasPendingLinks = importantLinksArray.length > 0;
459-
460-
// If we have pending deletions, uploads, or links, process them
461-
if (hasPendingDeletions || hasPendingUploads || hasPendingLinks) {
462-
// Process deletions, then uploads, then links
463-
this.deletePendingAttachments(meeting.uid)
464-
.pipe(
465-
switchMap((deletionResult) =>
466-
this.savePendingAttachments(meeting.uid).pipe(
467-
switchMap((uploadResult) =>
468-
this.saveLinkAttachments(meeting.uid).pipe(
469-
switchMap((linkResult) =>
470-
of({
471-
deletions: deletionResult,
472-
uploads: uploadResult,
473-
links: linkResult,
474-
})
475-
)
476-
)
477-
)
478-
)
479-
),
480-
take(1)
481-
)
482-
.subscribe({
483-
next: (result) => {
484-
// Process attachment operations after meeting save
485-
this.handleAttachmentOperationsResults(result);
486-
},
487-
error: (attachmentError: any) => {
488-
console.error('Error processing attachments:', attachmentError);
489-
const warningMessage = this.isEditMode()
490-
? 'Meeting updated but some attachment operations failed. You can manage them later.'
491-
: 'Meeting created but some attachment operations failed. You can manage them later.';
492-
this.messageService.add({
493-
severity: 'warn',
494-
summary: this.isEditMode() ? 'Meeting Updated' : 'Meeting Created',
495-
detail: warningMessage,
496-
});
489+
// Process attachment operations using extracted method
490+
this.processAttachmentOperations(meeting.uid).subscribe({
491+
next: (result) => {
492+
if (result) {
493+
// Process attachment operations after meeting save
494+
this.handleAttachmentOperationsResults(result);
495+
} else {
496+
// No attachment operations to process
497+
this.messageService.add({
498+
severity: 'success',
499+
summary: 'Success',
500+
detail: `Meeting ${this.isEditMode() ? 'updated' : 'created'} successfully`,
501+
});
497502

498-
// For edit mode, navigate to step 5 to manage guests
499-
if (this.isEditMode()) {
500-
this.router.navigate([], { queryParams: { step: '5' } });
501-
this.submitting.set(false);
502-
} else {
503-
this.router.navigate(['/meetings']);
504-
}
505-
},
503+
this.navigateAfterMeetingSave();
504+
}
505+
},
506+
error: (attachmentError: any) => {
507+
console.error('Error processing attachments:', attachmentError);
508+
const warningMessage = this.isEditMode()
509+
? 'Meeting updated but some attachment operations failed. You can manage them later.'
510+
: 'Meeting created but some attachment operations failed. You can manage them later.';
511+
this.messageService.add({
512+
severity: 'warn',
513+
summary: this.isEditMode() ? 'Meeting Updated' : 'Meeting Created',
514+
detail: warningMessage,
506515
});
507-
} else {
508-
this.messageService.add({
509-
severity: 'success',
510-
summary: 'Success',
511-
detail: `Meeting ${this.isEditMode() ? 'updated' : 'created'} successfully`,
512-
});
513516

514-
// For edit mode, navigate to step 5 to manage guests
515-
if (this.isEditMode()) {
516-
this.router.navigate([], { queryParams: { step: '5' } });
517-
this.submitting.set(false);
518-
} else {
519-
this.router.navigate(['/meetings']);
520-
}
517+
this.navigateAfterMeetingSave();
518+
},
519+
});
520+
}
521+
522+
private navigateAfterMeetingSave(): void {
523+
this.submitting.set(false);
524+
525+
if (this.isEditMode()) {
526+
// In edit mode, navigate to step 5 to manage guests
527+
this.router.navigate([], { queryParams: { step: '5' } });
528+
} else if (this.currentStep() < this.totalSteps) {
529+
// In create mode and not on the last step, continue to next step
530+
this.nextStep();
531+
} else {
532+
// In create mode on the last step, navigate to meetings list
533+
this.router.navigate(['/meetings']);
521534
}
522535
}
523536

@@ -618,13 +631,7 @@ export class MeetingManageComponent {
618631
this.pendingAttachmentDeletions.set([]);
619632
}
620633

621-
// For edit mode, navigate to step 5 to manage guests
622-
if (this.isEditMode()) {
623-
this.router.navigate([], { queryParams: { step: '5' } });
624-
this.submitting.set(false);
625-
} else {
626-
this.router.navigate(['/meetings']);
627-
}
634+
this.navigateAfterMeetingSave();
628635
}
629636

630637
private initializeMeeting() {
@@ -666,11 +673,17 @@ export class MeetingManageComponent {
666673
let startTime = '';
667674

668675
if (meeting.start_time) {
669-
const date = new Date(meeting.start_time);
670-
startDate = date;
676+
const utcDate = new Date(meeting.start_time);
677+
const meetingTimezone = meeting.timezone || getUserTimezone();
671678

672-
// Convert to 12-hour format for display
673-
startTime = formatTo12Hour(date);
679+
// Convert UTC date to the meeting's timezone for proper display
680+
// This ensures the date picker and time picker show the correct values
681+
// in the meeting's timezone, not the user's local timezone
682+
const zonedDate = toZonedTime(utcDate, meetingTimezone);
683+
startDate = zonedDate;
684+
685+
// Convert to 12-hour format in the meeting's timezone for display
686+
startTime = formatTo12HourInTimezone(utcDate, meetingTimezone);
674687
}
675688

676689
// Map recurrence object back to form value
@@ -926,6 +939,42 @@ export class MeetingManageComponent {
926939
}
927940
}
928941

942+
private processAttachmentOperations(meetingId: string): Observable<{
943+
deletions: { successes: number; failures: string[] };
944+
uploads: { successes: MeetingAttachment[]; failures: { fileName: string; error: any }[] };
945+
links: { successes: MeetingAttachment[]; failures: { linkName: string; error: any }[] };
946+
} | null> {
947+
const hasPendingDeletions = this.pendingAttachmentDeletions().length > 0;
948+
const hasPendingUploads = this.pendingAttachments.length > 0;
949+
const importantLinksArray = this.form().get('important_links') as FormArray;
950+
const hasPendingLinks = importantLinksArray.length > 0;
951+
952+
// If no pending operations, return null
953+
if (!hasPendingDeletions && !hasPendingUploads && !hasPendingLinks) {
954+
return of(null);
955+
}
956+
957+
// Process deletions, then uploads, then links
958+
return this.deletePendingAttachments(meetingId).pipe(
959+
switchMap((deletionResult) =>
960+
this.savePendingAttachments(meetingId).pipe(
961+
switchMap((uploadResult) =>
962+
this.saveLinkAttachments(meetingId).pipe(
963+
switchMap((linkResult) =>
964+
of({
965+
deletions: deletionResult,
966+
uploads: uploadResult,
967+
links: linkResult,
968+
})
969+
)
970+
)
971+
)
972+
)
973+
),
974+
take(1)
975+
);
976+
}
977+
929978
private deletePendingAttachments(meetingId: string): Observable<{ successes: number; failures: string[] }> {
930979
const attachmentIdsToDelete = this.pendingAttachmentDeletions();
931980

@@ -1098,6 +1147,60 @@ export class MeetingManageComponent {
10981147
}
10991148
}
11001149

1150+
private showSubmitAllOperationToast(registrantSuccess: number, registrantFailed: number, attachmentSuccess: number, attachmentFailed: number): void {
1151+
const totalSuccess = registrantSuccess + attachmentSuccess;
1152+
const totalFailed = registrantFailed + attachmentFailed;
1153+
const hasOperations = totalSuccess > 0 || totalFailed > 0;
1154+
1155+
if (!hasOperations) {
1156+
// No additional operations, just meeting update
1157+
this.messageService.add({
1158+
severity: 'success',
1159+
summary: 'Success',
1160+
detail: 'Meeting updated successfully',
1161+
});
1162+
return;
1163+
}
1164+
1165+
if (totalFailed === 0) {
1166+
// All successful
1167+
const parts = [];
1168+
if (registrantSuccess > 0) {
1169+
parts.push(`${registrantSuccess} guest(s)`);
1170+
}
1171+
if (attachmentSuccess > 0) {
1172+
parts.push(`${attachmentSuccess} attachment(s)`);
1173+
}
1174+
this.messageService.add({
1175+
severity: 'success',
1176+
summary: 'Success',
1177+
detail: `Meeting updated successfully with ${parts.join(' and ')}`,
1178+
});
1179+
} else if (totalSuccess > 0 && totalFailed > 0) {
1180+
// Partial success
1181+
const successParts = [];
1182+
const failureParts = [];
1183+
1184+
if (registrantSuccess > 0) successParts.push(`${registrantSuccess} guest(s)`);
1185+
if (attachmentSuccess > 0) successParts.push(`${attachmentSuccess} attachment(s)`);
1186+
if (registrantFailed > 0) failureParts.push(`${registrantFailed} guest(s)`);
1187+
if (attachmentFailed > 0) failureParts.push(`${attachmentFailed} attachment(s)`);
1188+
1189+
this.messageService.add({
1190+
severity: 'warn',
1191+
summary: 'Partial Success',
1192+
detail: `Meeting updated. ${successParts.join(' and ')} succeeded, ${failureParts.join(' and ')} failed`,
1193+
});
1194+
} else {
1195+
// All additional operations failed
1196+
this.messageService.add({
1197+
severity: 'warn',
1198+
summary: 'Meeting Updated',
1199+
detail: 'Meeting updated but some operations failed. You can manage them later.',
1200+
});
1201+
}
1202+
}
1203+
11011204
private needsCustomRecurrence(recurrence: any): boolean {
11021205
if (!recurrence) return false;
11031206

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,9 @@ export class MeetingService {
174174
const sanitizedPayload = Logger.sanitize({ createPayload });
175175
req.log.debug(sanitizedPayload, 'Creating meeting payload');
176176

177-
const newMeeting = await this.microserviceProxy.proxyRequest<Meeting>(req, 'LFX_V2_SERVICE', '/meetings', 'POST', undefined, createPayload);
177+
const newMeeting = await this.microserviceProxy.proxyRequest<Meeting>(req, 'LFX_V2_SERVICE', '/meetings', 'POST', undefined, createPayload, {
178+
['X-Sync']: 'true',
179+
});
178180

179181
req.log.info(
180182
{

0 commit comments

Comments
 (0)