Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -8,6 +8,7 @@ import { Router } from '@angular/router';
import { MeetingService } from '@app/shared/services/meeting.service';
import { ButtonComponent } from '@components/button/button.component';
import { DashboardMeetingCardComponent } from '@components/dashboard-meeting-card/dashboard-meeting-card.component';
import { getActiveOccurrences } from '@lfx-one/shared';
import { SkeletonModule } from 'primeng/skeleton';
import { finalize } from 'rxjs';

Expand Down Expand Up @@ -38,7 +39,10 @@ export class MyMeetingsComponent {
for (const meeting of this.allMeetings()) {
// Process occurrences if they exist
if (meeting.occurrences && meeting.occurrences.length > 0) {
for (const occurrence of meeting.occurrences) {
// Get only active (non-cancelled) occurrences
const activeOccurrences = getActiveOccurrences(meeting.occurrences);

for (const occurrence of activeOccurrences) {
const startTime = new Date(occurrence.start_time);
const startTimeMs = startTime.getTime();
const endTime = startTimeMs + occurrence.duration * 60 * 1000 + buffer;
Expand Down Expand Up @@ -89,7 +93,10 @@ export class MyMeetingsComponent {
for (const meeting of this.allMeetings()) {
// Process occurrences if they exist
if (meeting.occurrences && meeting.occurrences.length > 0) {
for (const occurrence of meeting.occurrences) {
// Get only active (non-cancelled) occurrences
const activeOccurrences = getActiveOccurrences(meeting.occurrences);

for (const occurrence of activeOccurrences) {
const startTime = new Date(occurrence.start_time);
const startTimeMs = startTime.getTime();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
<!-- SPDX-License-Identifier: MIT -->

<div class="meeting-cancel-occurrence-confirmation">
<!-- Occurrence Details -->
<div class="mb-6">
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 mb-3">Occurrence Details</h3>

<div class="space-y-2">
<div class="flex items-start">
<span class="font-medium text-gray-700 w-20 flex-shrink-0">Title:</span>
<span class="text-gray-900">{{ occurrence.title || meeting.title || 'Untitled Meeting' }}</span>
</div>

<div class="flex items-start">
<span class="font-medium text-gray-700 w-20 flex-shrink-0">Date:</span>
<span class="text-gray-900">{{ occurrence.start_time | meetingTime: occurrence.duration : 'date' }}</span>
</div>

<div class="flex items-start">
<span class="font-medium text-gray-700 w-20 flex-shrink-0">Time:</span>
<span class="text-gray-900">{{ occurrence.start_time | meetingTime: occurrence.duration : 'time' }}</span>
</div>

<div class="flex items-start">
<span class="font-medium text-gray-700 w-20 flex-shrink-0">Series:</span>
<span class="text-gray-900 flex items-center gap-2">
<i class="fa-light fa-repeat text-blue-500"></i>
Part of Recurring Series
</span>
</div>
</div>
</div>
</div>

<!-- Warning Message -->
<lfx-message severity="error" icon="fa-light fa-triangle-exclamation" styleClass="mb-6">
<ng-template #content>
<h4 class="font-medium mb-1">Warning</h4>
<p class="text-sm">This will permanently cancel this specific occurrence. Guests will be notified of the cancellation. This action cannot be undone.</p>
</ng-template>
</lfx-message>

<!-- Action Buttons -->
<div class="flex justify-end gap-3">
<lfx-button
label="Cancel"
severity="secondary"
[outlined]="true"
size="small"
type="button"
[disabled]="isCanceling()"
(click)="onCancel()"
data-testid="cancel-occurrence-cancel-button">
</lfx-button>

<lfx-button
[label]="isCanceling() ? 'Canceling...' : 'Cancel Occurrence'"
severity="danger"
size="small"
type="button"
[icon]="isCanceling() ? 'fa-light fa-circle-notch fa-spin' : 'fa-light fa-ban'"
[disabled]="isCanceling()"
(click)="onConfirm()"
data-testid="cancel-occurrence-confirm-button">
</lfx-button>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

import { Component, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
import { Meeting, MeetingOccurrence } from '@lfx-one/shared/interfaces';
import { MeetingService } from '@services/meeting.service';
import { ButtonComponent } from '@components/button/button.component';
import { MessageComponent } from '@components/message/message.component';
import { MeetingTimePipe } from '@pipes/meeting-time.pipe';
import { HttpErrorResponse } from '@angular/common/http';

@Component({
selector: 'lfx-meeting-cancel-occurrence-confirmation',
standalone: true,
imports: [CommonModule, ButtonComponent, MessageComponent, MeetingTimePipe],
templateUrl: './meeting-cancel-occurrence-confirmation.component.html',
})
export class MeetingCancelOccurrenceConfirmationComponent {
private readonly dialogRef = inject(DynamicDialogRef);
private readonly config = inject(DynamicDialogConfig);
private readonly meetingService = inject(MeetingService);

public readonly meeting: Meeting = this.config.data.meeting;
public readonly occurrence: MeetingOccurrence = this.config.data.occurrence;
public readonly isCanceling = signal(false);

public onCancel(): void {
this.dialogRef.close({ confirmed: false });
}

public onConfirm(): void {
this.isCanceling.set(true);

this.meetingService.cancelOccurrence(this.meeting.uid, this.occurrence.occurrence_id).subscribe({
next: () => {
this.isCanceling.set(false);
this.dialogRef.close({ confirmed: true });
},
error: (error: HttpErrorResponse) => {
this.isCanceling.set(false);
let errorMessage = 'Failed to cancel occurrence. Please try again.';

if (error.status === 404) {
errorMessage = 'Meeting occurrence not found.';
} else if (error.status === 403) {
errorMessage = 'You do not have permission to cancel this occurrence.';
} else if (error.status === 500) {
errorMessage = 'Server error occurred while canceling occurrence.';
} else if (error.status === 0) {
errorMessage = 'Network error. Please check your connection.';
}

this.dialogRef.close({ confirmed: false, error: errorMessage });
},
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
getCurrentOrNextOccurrence,
Meeting,
MeetingAttachment,
MeetingCancelOccurrenceResult,
MeetingOccurrence,
MeetingRegistrant,
PastMeeting,
Expand All @@ -37,8 +38,10 @@ import { DialogService } from 'primeng/dynamicdialog';
import { TooltipModule } from 'primeng/tooltip';
import { BehaviorSubject, catchError, filter, finalize, map, of, switchMap, take, tap } from 'rxjs';

import { MeetingCancelOccurrenceConfirmationComponent } from '../meeting-cancel-occurrence-confirmation/meeting-cancel-occurrence-confirmation.component';
import { MeetingCommitteeModalComponent } from '../meeting-committee-modal/meeting-committee-modal.component';
import { MeetingDeleteConfirmationComponent, MeetingDeleteResult } from '../meeting-delete-confirmation/meeting-delete-confirmation.component';
import { MeetingDeleteTypeSelectionComponent, MeetingDeleteTypeResult } from '../meeting-delete-type-selection/meeting-delete-type-selection.component';
import { RecordingModalComponent } from '../recording-modal/recording-modal.component';
import { RegistrantModalComponent } from '../registrant-modal/registrant-modal.component';
import { SummaryModalComponent } from '../summary-modal/summary-modal.component';
Expand Down Expand Up @@ -464,6 +467,85 @@ export class MeetingCardComponent implements OnInit {
const meeting = this.meeting();
if (!meeting) return;

// Check if meeting is recurring
const isRecurring = !!meeting.recurrence;

if (isRecurring) {
// For recurring meetings, first show the delete type selection modal
this.dialogService
.open(MeetingDeleteTypeSelectionComponent, {
header: 'Delete Recurring Meeting',
width: '500px',
modal: true,
closable: true,
dismissableMask: true,
data: {
meeting: meeting,
},
})
.onClose.pipe(take(1))
.subscribe((typeResult: MeetingDeleteTypeResult) => {
if (typeResult) {
if (typeResult.deleteType === 'occurrence') {
// User wants to cancel just this occurrence
this.showCancelOccurrenceModal(meeting);
} else {
// User wants to delete the entire series
this.showDeleteMeetingModal(meeting);
}
}
});
} else {
// For non-recurring meetings, show delete confirmation directly
this.showDeleteMeetingModal(meeting);
}
}

private showCancelOccurrenceModal(meeting: Meeting): void {
// Prefer the explicitly selected/current occurrence; fallback to next active
const occurrenceToCancel = this.occurrence() ?? getCurrentOrNextOccurrence(meeting);

if (!occurrenceToCancel) {
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'No upcoming occurrence found to cancel.',
});
return;
}

this.dialogService
.open(MeetingCancelOccurrenceConfirmationComponent, {
header: 'Cancel Occurrence',
width: '450px',
modal: true,
closable: true,
dismissableMask: true,
data: {
meeting: meeting,
occurrence: occurrenceToCancel,
},
})
.onClose.pipe(take(1))
.subscribe((result: MeetingCancelOccurrenceResult) => {
if (result?.confirmed) {
this.messageService.add({
severity: 'success',
summary: 'Success',
detail: 'Meeting occurrence canceled successfully',
});
this.meetingDeleted.emit();
} else if (result?.error) {
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: result.error,
});
}
});
}

private showDeleteMeetingModal(meeting: Meeting): void {
this.dialogService
.open(MeetingDeleteConfirmationComponent, {
header: 'Delete Meeting',
Expand All @@ -477,7 +559,7 @@ export class MeetingCardComponent implements OnInit {
})
.onClose.pipe(take(1))
.subscribe((result: MeetingDeleteResult) => {
if (result) {
if (result?.confirmed) {
this.meetingDeleted.emit();
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,13 @@ <h4 class="font-medium mb-1">Warning</h4>

<!-- Action Buttons -->
<div class="flex justify-end gap-3">
<lfx-button label="Cancel" severity="secondary" [outlined]="true" size="small" [disabled]="isDeleting()" (click)="onCancel()"> </lfx-button>
<lfx-button label="Cancel" severity="secondary" [outlined]="true" size="small" [disabled]="isDeleting()" type="button" (click)="onCancel()"> </lfx-button>

<lfx-button
[label]="isDeleting() ? 'Deleting...' : 'Delete Meeting'"
severity="danger"
size="small"
type="button"
[icon]="isDeleting() ? 'fa-light fa-circle-notch fa-spin' : 'fa-light fa-trash'"
[disabled]="isDeleting()"
(click)="onConfirm()">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
<!-- SPDX-License-Identifier: MIT -->

<div class="meeting-delete-type-selection">
<!-- Description -->
<div class="mb-6">
<p class="text-gray-700">This is a recurring meeting. Would you like to cancel just this occurrence or delete the entire meeting series?</p>
</div>

<!-- Selection Options -->
<div class="space-y-3 mb-6">
<div
class="border rounded-lg p-4 cursor-pointer transition-all"
[ngClass]="{
'border-blue-500 bg-blue-50': selectedType === 'occurrence',
'border-gray-200': selectedType !== 'occurrence',
}"
(click)="selectType('occurrence')"
data-testid="delete-type-occurrence-option">
<div class="flex items-start gap-3">
<div class="mt-1">
<i
class="fa-light text-lg"
[ngClass]="{
'fa-circle-dot text-blue-500': selectedType === 'occurrence',
'fa-circle text-gray-400': selectedType !== 'occurrence',
}"></i>
</div>
<div class="flex-1">
<h4 class="font-semibold text-gray-900 mb-1">Cancel This Occurrence</h4>
<p class="text-sm text-gray-600">Only this specific meeting instance will be canceled. The rest of the series will remain.</p>
</div>
</div>
</div>

<div
class="border rounded-lg p-4 cursor-pointer transition-all"
[ngClass]="{
'border-blue-500 bg-blue-50': selectedType === 'series',
'border-gray-200': selectedType !== 'series',
}"
(click)="selectType('series')"
data-testid="delete-type-series-option">
<div class="flex items-start gap-3">
<div class="mt-1">
<i
class="fa-light text-lg"
[ngClass]="{
'fa-circle-dot text-blue-500': selectedType === 'series',
'fa-circle text-gray-400': selectedType !== 'series',
}"></i>
</div>
<div class="flex-1">
<h4 class="font-semibold text-gray-900 mb-1">Delete Entire Series</h4>
<p class="text-sm text-gray-600">The entire recurring meeting series will be permanently deleted.</p>
</div>
</div>
</div>
</div>

<!-- Action Buttons -->
<div class="flex justify-end gap-3">
<lfx-button label="Cancel" severity="secondary" [outlined]="true" size="small" type="button" (click)="onCancel()" data-testid="delete-type-cancel-button">
</lfx-button>

<lfx-button
label="Continue"
severity="primary"
size="small"
type="button"
[disabled]="!selectedType"
(click)="onContinue()"
data-testid="delete-type-continue-button">
</lfx-button>
</div>
</div>
Loading