From 69edc4e19797215a81ce7d6e1700f61208bb48fa Mon Sep 17 00:00:00 2001 From: Borgia Leiva Date: Wed, 11 Feb 2026 10:50:13 +0100 Subject: [PATCH] Added task editing page --- src/app/app.routes.ts | 5 + .../task-item/task-item.component.css | 20 ++++ .../task-item/task-item.component.html | 1 + .../task-item/task-item.component.ts | 17 ++- src/app/models/tasks.models.ts | 6 + .../project-details.component.html | 6 +- .../pages/task-edit/task-edit.component.css | 99 ++++++++++++++++ .../pages/task-edit/task-edit.component.html | 60 ++++++++++ .../pages/task-edit/task-edit.component.ts | 110 ++++++++++++++++++ 9 files changed, 322 insertions(+), 2 deletions(-) create mode 100644 src/app/pages/task-edit/task-edit.component.css create mode 100644 src/app/pages/task-edit/task-edit.component.html create mode 100644 src/app/pages/task-edit/task-edit.component.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 068aaf2..907aba0 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -54,6 +54,11 @@ export const routes: Routes = [ loadComponent: () => import('./pages/task-create/task-create.component').then((m) => m.TaskCreateComponent), }, + { + path: 'projects/:id/tasks/:taskId/edit', + loadComponent: () => + import('./pages/task-edit/task-edit.component').then((m) => m.TaskEditComponent), + }, ], }, ]; diff --git a/src/app/components/task-item/task-item.component.css b/src/app/components/task-item/task-item.component.css index dfa390b..1eaf5fe 100644 --- a/src/app/components/task-item/task-item.component.css +++ b/src/app/components/task-item/task-item.component.css @@ -56,6 +56,26 @@ border-color: #fca5a5; } +.btn-edit { + height: 32px; + padding: 0 12px; + border-radius: 8px; + border: 1px solid #cbd5f0; + background: #ffffff; + color: #334155; + font-weight: 600; + font-size: 12px; + cursor: pointer; + transition: + background-color 0.2s ease, + border-color 0.2s ease; +} + +.btn-edit:hover { + background-color: #f1f5f9; + border-color: #94a3b8; +} + .task-info h4 { margin: 0 0 6px 0; color: #111827; diff --git a/src/app/components/task-item/task-item.component.html b/src/app/components/task-item/task-item.component.html index b21d479..d6972e4 100644 --- a/src/app/components/task-item/task-item.component.html +++ b/src/app/components/task-item/task-item.component.html @@ -15,6 +15,7 @@ } + diff --git a/src/app/components/task-item/task-item.component.ts b/src/app/components/task-item/task-item.component.ts index c4d9ab9..7cc27c1 100644 --- a/src/app/components/task-item/task-item.component.ts +++ b/src/app/components/task-item/task-item.component.ts @@ -1,4 +1,5 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component, EventEmitter, Input, Output, inject } from '@angular/core'; +import { Router } from '@angular/router'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { Task } from '../../models/tasks.models'; @@ -11,6 +12,7 @@ import { Task } from '../../models/tasks.models'; styleUrl: './task-item.component.css', }) export class TaskItemComponent { + private router = inject(Router); private _task!: Task; statusValue: Task['status'] = 'pending'; @@ -24,6 +26,9 @@ export class TaskItemComponent { return this._task; } + @Input() + projectId?: number; + @Output() statusChange = new EventEmitter<{ task: Task; status: Task['status']; @@ -59,4 +64,14 @@ export class TaskItemComponent { this.remove.emit(this._task); } } + + onEdit() { + if (!this._task || this.projectId == null) { + return; + } + + this.router.navigate(['/projects', this.projectId, 'tasks', this._task.id, 'edit'], { + state: { task: this._task }, + }); + } } diff --git a/src/app/models/tasks.models.ts b/src/app/models/tasks.models.ts index a063e92..c52fd60 100644 --- a/src/app/models/tasks.models.ts +++ b/src/app/models/tasks.models.ts @@ -10,3 +10,9 @@ export interface CreateTaskRequest { description?: string; status: Task['status']; } + +export interface UpdateTaskRequest { + title: string; + description?: string; + status: Task['status']; +} diff --git a/src/app/pages/project-details/project-details.component.html b/src/app/pages/project-details/project-details.component.html index c1097f2..a7c95c6 100644 --- a/src/app/pages/project-details/project-details.component.html +++ b/src/app/pages/project-details/project-details.component.html @@ -28,7 +28,11 @@ @if ((project()?.tasks?.length ?? 0) > 0) {
@for (task of project()?.tasks ?? []; track task.id) { - + }
} @else { diff --git a/src/app/pages/task-edit/task-edit.component.css b/src/app/pages/task-edit/task-edit.component.css new file mode 100644 index 0000000..7dcd03b --- /dev/null +++ b/src/app/pages/task-edit/task-edit.component.css @@ -0,0 +1,99 @@ +.create-task { + min-height: 100vh; + background-color: #f5f7fa; + padding: 40px; + display: flex; + justify-content: center; + align-items: flex-start; +} + +.card { + width: 100%; + max-width: 640px; + height: fit-content; + background: white; + border-radius: 12px; + padding: 32px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.card h1 { + margin: 0 0 24px 0; + font-size: 28px; + color: #2c3e50; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: 600; + color: #374151; +} + +.form-group input, +.form-group textarea, +.form-group select { + width: 100%; + padding: 12px 14px; + border: 1px solid #e5e7eb; + border-radius: 8px; + font-size: 14px; + box-sizing: border-box; + background-color: #ffffff; +} + +.form-group textarea { + resize: vertical; +} + +.actions { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 12px; +} + +.btn-primary { + padding: 10px 16px; + background-color: #10b981; + color: #fff; + border: none; + border-radius: 8px; + font-weight: 600; + cursor: pointer; +} + +.btn-primary:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.btn-secondary { + padding: 10px 16px; + background: none; + border: 1px solid #cbd5f0; + color: #334155; + border-radius: 8px; + font-weight: 600; + cursor: pointer; +} + +.error { + margin-bottom: 16px; + padding: 12px; + border-radius: 8px; + background-color: #fee2e2; + color: #b91c1c; +} + +.loading { + margin-bottom: 16px; + padding: 12px; + border-radius: 8px; + background-color: #e2e8f0; + color: #334155; +} diff --git a/src/app/pages/task-edit/task-edit.component.html b/src/app/pages/task-edit/task-edit.component.html new file mode 100644 index 0000000..f8d13a0 --- /dev/null +++ b/src/app/pages/task-edit/task-edit.component.html @@ -0,0 +1,60 @@ +
+
+

Edit task

+ + @if (errorMessage()) { +
{{ errorMessage() }}
+ } + + @if (isLoading()) { +
Loading task...
+ } @else { +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ } +
+
diff --git a/src/app/pages/task-edit/task-edit.component.ts b/src/app/pages/task-edit/task-edit.component.ts new file mode 100644 index 0000000..2bcf382 --- /dev/null +++ b/src/app/pages/task-edit/task-edit.component.ts @@ -0,0 +1,110 @@ +import { Component, inject, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ApiService } from '../../services/api.service'; +import { Task, UpdateTaskRequest } from '../../models/tasks.models'; + +@Component({ + selector: 'app-task-edit', + standalone: true, + imports: [CommonModule, FormsModule], + templateUrl: './task-edit.component.html', + styleUrls: ['./task-edit.component.css'], +}) +export class TaskEditComponent { + private apiService = inject(ApiService); + private route = inject(ActivatedRoute); + private router = inject(Router); + + title = ''; + description = ''; + status: Task['status'] = 'pending'; + isLoading = signal(true); + isSaving = signal(false); + errorMessage = signal(''); + private projectId: number | null = null; + private taskId: number | null = null; + + constructor() { + const navState = this.router.getCurrentNavigation()?.extras.state ?? (history.state as any); + const initialTask = navState?.task as Task | undefined; + + const projectParam = this.route.snapshot.paramMap.get('id'); + const taskParam = this.route.snapshot.paramMap.get('taskId'); + const projectId = projectParam ? Number(projectParam) : Number.NaN; + const taskId = taskParam ? Number(taskParam) : Number.NaN; + + if (!Number.isFinite(projectId) || !Number.isFinite(taskId)) { + this.isLoading.set(false); + this.errorMessage.set('Invalid project or task id.'); + return; + } + + this.projectId = projectId; + this.taskId = taskId; + + if (initialTask && initialTask.id === taskId) { + this.title = initialTask.title ?? ''; + this.description = initialTask.description ?? ''; + this.status = initialTask.status ?? 'pending'; + this.isLoading.set(false); + return; + } + + this.apiService.get(`/projects/${projectId}/tasks/${taskId}`).subscribe({ + next: (task) => { + this.title = task.title ?? ''; + this.description = task.description ?? ''; + this.status = task.status ?? 'pending'; + this.isLoading.set(false); + }, + error: (error) => { + this.isLoading.set(false); + this.errorMessage.set(error?.error?.message || 'Failed to load task.'); + }, + }); + } + + onSubmit() { + if (!this.title.trim()) { + this.errorMessage.set('Task title is required.'); + return; + } + + if (this.projectId == null || this.taskId == null) { + this.errorMessage.set('Invalid project or task id.'); + return; + } + + const payload: UpdateTaskRequest = { + title: this.title.trim(), + description: this.description.trim() ? this.description.trim() : undefined, + status: this.status, + }; + + this.isSaving.set(true); + this.errorMessage.set(''); + + this.apiService + .put(`/projects/${this.projectId}/tasks/${this.taskId}`, payload) + .subscribe({ + next: () => { + this.isSaving.set(false); + this.router.navigate(['/projects', this.projectId]); + }, + error: (error) => { + this.isSaving.set(false); + this.errorMessage.set(error?.error?.message || 'Failed to update task.'); + }, + }); + } + + onCancel() { + if (this.projectId != null) { + this.router.navigate(['/projects', this.projectId]); + } else { + this.router.navigate(['/']); + } + } +}