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(['/']);
+ }
+ }
+}