From c5b21f166d09337699b3837cbbfbf53d8121f9f9 Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Tue, 10 Feb 2026 11:49:40 +0100 Subject: [PATCH] Created project managment page --- .../project-details.component.css | 239 ++++++++++++++++++ .../project-details.component.html | 86 +++++++ .../project-details.component.ts | 108 ++++++++ 3 files changed, 433 insertions(+) create mode 100644 src/app/pages/project-details/project-details.component.css create mode 100644 src/app/pages/project-details/project-details.component.html create mode 100644 src/app/pages/project-details/project-details.component.ts diff --git a/src/app/pages/project-details/project-details.component.css b/src/app/pages/project-details/project-details.component.css new file mode 100644 index 0000000..c67b5fa --- /dev/null +++ b/src/app/pages/project-details/project-details.component.css @@ -0,0 +1,239 @@ +.details-container { + min-height: 100vh; + background-color: #f5f7fa; +} + +.content { + padding: 40px; + max-width: 1200px; + margin: 0 auto; +} + +.header { + display: flex; + align-items: flex-start; + gap: 16px; + flex-wrap: wrap; + margin-bottom: 24px; +} + +.btn-back { + padding: 8px 16px; + border-radius: 8px; + border: none; + background-color: #e2e8f0; + color: #1f2933; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.btn-back:hover { + background-color: #cbd5e1; +} + +.title h2 { + margin: 0 0 6px 0; + font-size: 26px; + color: #1f2933; +} + +.title p { + margin: 0; + color: #6b7280; +} + +.project-card, +.collaborators-card, +.tasks-card, +.danger-zone, +.loading, +.error-state, +.empty-state { + background: #ffffff; + padding: 24px; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} + +.project-card { + margin-bottom: 20px; +} + +.collaborators-card { + margin-bottom: 20px; +} + +.danger-zone { + margin-top: 20px; + border: 1px solid #fee2e2; + background: #fff5f5; +} + +.danger-header h3 { + margin: 0 0 6px 0; + color: #b91c1c; +} + +.danger-header p { + margin: 0; + color: #7f1d1d; +} + +.danger-actions { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 16px; +} + +.danger-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 16px; + border-radius: 10px; + background: #ffffff; + border: 1px solid #fecaca; +} + +.danger-item h4 { + margin: 0 0 4px 0; + color: #7f1d1d; + font-size: 16px; +} + +.danger-item p { + margin: 0; + color: #991b1b; + font-size: 13px; +} + +.btn-danger, +.btn-danger-outline { + padding: 8px 14px; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + transition: + background-color 0.2s ease, + color 0.2s ease, + border-color 0.2s ease; + white-space: nowrap; +} + +.btn-danger { + border: none; + background-color: #dc2626; + color: #ffffff; +} + +.btn-danger:hover { + background-color: #b91c1c; +} + +.btn-danger-outline { + border: 1px solid #fca5a5; + background-color: #ffffff; + color: #b91c1c; +} + +.btn-danger-outline:hover { + border-color: #f87171; + background-color: #fee2e2; +} + +.project-card h3 { + margin: 0 0 12px 0; + font-size: 22px; + color: #111827; +} + +.description { + margin: 0; + color: #4b5563; +} + +.tasks-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 16px; +} + +.collaborators-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 16px; +} + +.collaborators-count { + font-size: 14px; + color: #6b7280; +} + +.collaborators-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; +} + +.btn-add-task { + width: 100%; + padding: 8px 14px; + border-radius: 8px; + border: none; + background-color: #10b981; + color: #ffffff; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s ease; + margin-top: 16px; +} + +.btn-add-collaborator { + width: 100%; + padding: 8px 14px; + border-radius: 8px; + border: none; + background-color: #10b981; + color: #ffffff; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s ease; + margin-top: 16px; +} + +.btn-add-task:hover { + background-color: #059669; +} + +.btn-add-collaborator:hover { + background-color: #059669; +} + +.tasks-count { + font-size: 14px; + color: #6b7280; +} + +.tasks-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; +} + +.loading p, +.error-state p, +.empty-state p { + margin: 0; + color: #4b5563; +} + +.error-state p { + color: #b91c1c; +} diff --git a/src/app/pages/project-details/project-details.component.html b/src/app/pages/project-details/project-details.component.html new file mode 100644 index 0000000..7440746 --- /dev/null +++ b/src/app/pages/project-details/project-details.component.html @@ -0,0 +1,86 @@ +
+
+ @if (isLoading()) { +
+

Loading project...

+
+ } @else if (errorMessage()) { +
+

{{ errorMessage() }}

+
+ } @else { +
+

{{ project()?.name }}

+

Project description:

+

+ {{ project()?.description || 'No description available.' }} +

+
+ +
+
+

Collaborators

+ {{ project()?.users?.length ?? 0 }} total +
+ + @if ((project()?.users?.length ?? 0) > 0) { +
+ @for (user of project()?.users ?? []; track user.id) { + + } +
+ } @else { +
+

No collaborators yet.

+
+ } + + +
+ +
+
+

Tasks

+ {{ project()?.tasks?.length ?? 0 }} total +
+ + @if ((project()?.tasks?.length ?? 0) > 0) { +
+ @for (task of project()?.tasks ?? []; track task.id) { + + } +
+ } @else { +
+

No tasks yet.

+
+ } + + +
+ +
+
+

Danger zone

+

Actions that affect this project permanently.

+
+
+
+
+

Edit project details

+

Update the project name or description.

+
+ +
+
+
+

Delete project

+

This will remove the project and all associated data.

+
+ +
+
+
+ } +
+
diff --git a/src/app/pages/project-details/project-details.component.ts b/src/app/pages/project-details/project-details.component.ts new file mode 100644 index 0000000..81d65d7 --- /dev/null +++ b/src/app/pages/project-details/project-details.component.ts @@ -0,0 +1,108 @@ +import { Component, inject, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ApiService } from '../../services/api.service'; +import { ProjectFull } from '../../models/projects.models'; +import { CollaboratorItemComponent } from '../../components/collaborator-item/collaborator-item.component'; +import { TaskItemComponent } from '../../components/task-item/task-item.component'; +import { User } from '../../models/auth.models'; + +@Component({ + selector: 'app-project-details', + standalone: true, + imports: [CommonModule, CollaboratorItemComponent, TaskItemComponent], + templateUrl: './project-details.component.html', + styleUrl: './project-details.component.css', +}) +export class ProjectDetailsComponent { + private apiService = inject(ApiService); + private route = inject(ActivatedRoute); + private router = inject(Router); + private projectId: number | null = null; + + project = signal(null); + isLoading = signal(true); + errorMessage = signal(''); + + constructor() { + const navState = this.router.getCurrentNavigation()?.extras.state ?? (history.state as any); + const initialProject = navState?.project as ProjectFull | undefined; + + if (initialProject) { + this.project.set(initialProject); + this.isLoading.set(false); + } + + const idParam = this.route.snapshot.paramMap.get('id'); + const projectId = idParam ? Number(idParam) : Number.NaN; + + if (!Number.isFinite(projectId)) { + this.isLoading.set(false); + this.errorMessage.set('Invalid project id.'); + return; + } + + this.projectId = projectId; + + this.apiService.get(`/projects/${projectId}`).subscribe({ + next: (project) => { + this.project.set(project); + this.isLoading.set(false); + }, + error: (error) => { + this.isLoading.set(false); + if (!initialProject) { + this.errorMessage.set(error?.error?.message || 'Failed to load project details.'); + } + }, + }); + } + + onBack() { + this.router.navigate(['/']); + } + + onRemoveCollaborator(user: User) { + const current = this.project(); + if (!current || this.projectId == null) { + return; + } + + const targetId = this.getUserId(user); + if (!targetId) { + return; + } + + const previousUsers = current.users ?? []; + const updatedUsers = (current.users ?? []).filter( + (collaborator) => this.getUserId(collaborator) !== targetId, + ); + + this.project.set({ + ...current, + users: updatedUsers, + }); + + this.apiService.delete(`/projects/${this.projectId}/users/${targetId}`).subscribe({ + error: (error) => { + this.project.set({ + ...current, + users: previousUsers, + }); + this.errorMessage.set(error?.error?.message || 'Failed to remove collaborator.'); + }, + }); + } + + private getUserId(user: User): string { + return String(user.id ?? ''); + } + + onAddTask() { + if (this.projectId == null) { + return; + } + + this.router.navigate(['/projects', this.projectId, 'tasks', 'new']); + } +}