Skip to content

Commit 678036e

Browse files
committed
feat(project-details): add guarded project deletion with confirm dialog and next-project fallback
1 parent d3b1223 commit 678036e

2 files changed

Lines changed: 99 additions & 1 deletion

File tree

src/app/features/projects/components/project-details/project-details.component.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<div class="project-details-page">
2+
<p-confirmDialog [baseZIndex]="1300" [appendTo]="'body'"></p-confirmDialog>
3+
24
<p-card class="project-details-header-card">
35
<div class="project-details-header">
46
<div class="project-details-header__title">
@@ -39,6 +41,16 @@ <h2>Project Details</h2>
3941
<button pButton type="button" icon="pi pi-refresh" class="p-button-outlined" (click)="refresh()" [loading]="isLoadingProjects || isLoadingDetails"></button>
4042
<button pButton type="button" label="Open Kanban" icon="pi pi-th-large" class="p-button-outlined" (click)="openKanban()" [disabled]="!selectedProjectId"></button>
4143
<button pButton type="button" label="Open Tasks" icon="pi pi-list" class="p-button-outlined" (click)="openTasks()"></button>
44+
<button
45+
*ngIf="canDeleteSelectedProject()"
46+
pButton
47+
type="button"
48+
label="Delete Project"
49+
icon="pi pi-trash"
50+
class="p-button-outlined p-button-danger"
51+
[disabled]="isProjectDeletePending || !selectedProjectId"
52+
(click)="deleteSelectedProject()">
53+
</button>
4254
</div>
4355
</div>
4456
</p-card>

src/app/features/projects/components/project-details/project-details.component.ts

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { Component, OnDestroy, OnInit, inject } from '@angular/core';
22
import { ActivatedRoute, Router } from '@angular/router';
3-
import { Message } from 'primeng/api';
3+
import { ConfirmationService, Message, MessageService } from 'primeng/api';
44
import { Subject, forkJoin, of, takeUntil } from 'rxjs';
55
import { catchError } from 'rxjs/operators';
66
import { ProjectsApiClient } from '../../../../core/api/clients/projects-api.client';
77
import { TaskItemsApiClient } from '../../../../core/api/clients/task-items-api.client';
88
import { ProjectDto, ProjectMemberDto } from '../../../../core/api/models/project.model';
99
import { TaskItemDto } from '../../../../core/api/models/task-item.model';
1010
import { TaskStatus } from '../../../../core/api/models/task-status.enum';
11+
import { AppRole } from '../../../../core/auth/models/app-role.model';
1112
import { AuthService } from '../../../../core/auth/services/auth.service';
1213
import { APP_ENVIRONMENT } from '../../../../core/config/app-environment.token';
1314
import { AppPreferencesService } from '../../../../core/preferences/app-preferences.service';
@@ -27,6 +28,8 @@ export class ProjectDetailsComponent implements OnInit, OnDestroy {
2728
private readonly projectsApiClient = inject(ProjectsApiClient);
2829
private readonly taskItemsApiClient = inject(TaskItemsApiClient);
2930
private readonly authService = inject(AuthService);
31+
private readonly confirmationService = inject(ConfirmationService);
32+
private readonly messageService = inject(MessageService);
3033
private readonly appEnvironment = inject(APP_ENVIRONMENT);
3134
private readonly preferencesService = inject(AppPreferencesService);
3235
private readonly router = inject(Router);
@@ -44,6 +47,7 @@ export class ProjectDetailsComponent implements OnInit, OnDestroy {
4447
isPreviewMode = false;
4548
previewDetail: string | null = null;
4649
errors: Message[] = [];
50+
isProjectDeletePending = false;
4751

4852
ngOnInit(): void {
4953
this.loadProjects();
@@ -130,6 +134,36 @@ export class ProjectDetailsComponent implements OnInit, OnDestroy {
130134
void this.router.navigate(['/tasks']);
131135
}
132136

137+
canDeleteSelectedProject(): boolean {
138+
const project = this.selectedProjectDetails ?? this.selectedProject;
139+
if (!project) {
140+
return false;
141+
}
142+
143+
if (this.authService.hasRole(AppRole.Administrator)) {
144+
return true;
145+
}
146+
147+
return this.authService.hasRole(AppRole.ProjectManager) && project.ownerUserId === this.authService.currentUserId();
148+
}
149+
150+
deleteSelectedProject(): void {
151+
const project = this.selectedProjectDetails ?? this.selectedProject;
152+
if (!project || this.isProjectDeletePending || !this.canDeleteSelectedProject()) {
153+
return;
154+
}
155+
156+
this.confirmationService.confirm({
157+
header: 'Delete Project',
158+
message: `Delete project "${project.name}"? This will remove related project data.`,
159+
icon: 'pi pi-exclamation-triangle',
160+
acceptLabel: 'Delete',
161+
rejectLabel: 'Cancel',
162+
acceptButtonStyleClass: 'p-button-danger',
163+
accept: () => this.executeProjectDeletion(project)
164+
});
165+
}
166+
133167
getStatusName(status: TaskStatus): string {
134168
switch (status) {
135169
case TaskStatus.Todo:
@@ -290,6 +324,58 @@ export class ProjectDetailsComponent implements OnInit, OnDestroy {
290324
});
291325
}
292326

327+
private executeProjectDeletion(project: ProjectDto): void {
328+
const previousProjects = [...this.projects];
329+
const previousSelectedProjectId = this.selectedProjectId;
330+
const previousSelectedDetails = this.selectedProjectDetails;
331+
const previousMembers = [...this.projectMembers];
332+
const previousRecentTasks = [...this.recentTasks];
333+
334+
this.isProjectDeletePending = true;
335+
this.projects = this.projects.filter((entry) => entry.id !== project.id);
336+
this.selectedProjectDetails = null;
337+
this.projectMembers = [];
338+
this.recentTasks = [];
339+
340+
const nextProjectId = this.projects[0]?.id ?? null;
341+
this.selectedProjectId = nextProjectId;
342+
if (nextProjectId) {
343+
this.updateProjectQueryParam(nextProjectId);
344+
this.preferencesService.setLastSelectedProject(ProjectDetailsComponent.PROJECT_SELECTION_CONTEXT, nextProjectId);
345+
}
346+
347+
if (this.isPreviewMode) {
348+
this.isProjectDeletePending = false;
349+
if (nextProjectId) {
350+
this.loadPreviewProjectData(nextProjectId);
351+
}
352+
this.messageService.add({ severity: 'success', summary: 'Deleted', detail: 'Project deleted in preview mode.' });
353+
return;
354+
}
355+
356+
this.projectsApiClient
357+
.delete(project.id)
358+
.pipe(takeUntil(this.destroy$))
359+
.subscribe({
360+
next: () => {
361+
this.isProjectDeletePending = false;
362+
if (nextProjectId) {
363+
this.loadProjectDetails(nextProjectId);
364+
}
365+
this.messageService.add({ severity: 'success', summary: 'Deleted', detail: 'Project deleted successfully.' });
366+
},
367+
error: () => {
368+
this.isProjectDeletePending = false;
369+
this.projects = previousProjects;
370+
this.selectedProjectId = previousSelectedProjectId;
371+
this.selectedProjectDetails = previousSelectedDetails;
372+
this.projectMembers = previousMembers;
373+
this.recentTasks = previousRecentTasks;
374+
this.messageService.add({ severity: 'error', summary: 'Delete Failed', detail: 'Could not delete project.' });
375+
}
376+
});
377+
}
378+
293379
private loadPreviewProjects(detail: string): void {
294380
this.isPreviewMode = true;
295381
this.previewDetail = detail;

0 commit comments

Comments
 (0)