@@ -33,7 +33,7 @@ import {
3333} from '@lfx-one/shared/interfaces' ;
3434import {
3535 combineDateTime ,
36- formatTo12Hour ,
36+ formatTo12HourInTimezone ,
3737 generateRecurrenceObject ,
3838 getDefaultStartDateTime ,
3939 getUserTimezone ,
@@ -42,6 +42,7 @@ import {
4242import { editModeDateTimeValidator , futureDateTimeValidator } from '@lfx-one/shared/validators' ;
4343import { MeetingService } from '@services/meeting.service' ;
4444import { ProjectContextService } from '@services/project-context.service' ;
45+ import { toZonedTime } from 'date-fns-tz' ;
4546import { ConfirmationService , MessageService } from 'primeng/api' ;
4647import { ConfirmDialogModule } from 'primeng/confirmdialog' ;
4748import { 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
0 commit comments