Skip to content

Commit 4a6907b

Browse files
committed
feat: add create task page
1 parent 9d7c454 commit 4a6907b

6 files changed

Lines changed: 235 additions & 1 deletion

File tree

src/app/app.routes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import { ProjectKanbanComponent } from './features/projects/components/project-k
66
import { TaskItemListComponent } from './features/task-item/components/task-item-list/task-item-list.component';
77
import { UserTaskItemsComponent } from './features/task-item/components/user-task-items/user-task-items.component';
88
import { ProjectCreateComponent } from './features/projects/components/project-create/project-create.component';
9+
import { TaskItemCreateComponent } from './features/task-item/components/task-item-create/task-item-create.component';
910

1011
export const routes: Routes = [
1112
{ path: '', component: DashboardComponent },
1213
{ path: 'projects', component: ProjectListComponent },
1314
{ path: 'projects/kanban', component: ProjectKanbanComponent },
1415
{ path: 'projects/create', component: ProjectCreateComponent },
1516
{ path: 'tasks', component: TaskItemListComponent },
17+
{ path: 'tasks/create', component: TaskItemCreateComponent },
1618
{ path: 'tasks/my-tasks', component: UserTaskItemsComponent },
1719
{ path: 'login', component: HomeLoginComponent },
1820
];
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<p-card header="Create New Task">
2+
<form [formGroup]="taskForm" (ngSubmit)="onSubmit()">
3+
4+
<div class="field mb-4">
5+
<label for="title" class="block text-surface-900 dark:text-surface-0 font-medium mb-2">Task Title</label>
6+
<input id="title" type="text" pInputText formControlName="title" class="w-full" [ngClass]="{'ng-invalid ng-dirty': isInvalid('title')}" />
7+
<small *ngIf="isInvalid('title')" class="p-error block mt-1">
8+
Task title is required.
9+
</small>
10+
</div>
11+
12+
<div class="field mb-4">
13+
<label for="projectId" class="block text-surface-900 dark:text-surface-0 font-medium mb-2">Project</label>
14+
<p-dropdown
15+
id="projectId"
16+
formControlName="projectId"
17+
[options]="projectOptions"
18+
placeholder="Select a Project"
19+
optionLabel="name"
20+
optionValue="value"
21+
styleClass="w-full"
22+
[filter]="projectOptions.length > 10"
23+
[showClear]="true"
24+
[ngClass]="{'ng-invalid ng-dirty': isInvalid('projectId')}">
25+
</p-dropdown>
26+
<small *ngIf="isInvalid('projectId')" class="p-error block mt-1">
27+
Project selection is required.
28+
</small>
29+
</div>
30+
31+
<div class="field mb-4">
32+
<label for="status" class="block text-surface-900 dark:text-surface-0 font-medium mb-2">Status</label>
33+
<p-dropdown
34+
id="status"
35+
formControlName="status"
36+
[options]="statusOptions"
37+
placeholder="Select Status"
38+
optionLabel="name"
39+
optionValue="value"
40+
styleClass="w-full"
41+
[ngClass]="{'ng-invalid ng-dirty': isInvalid('status')}">
42+
</p-dropdown>
43+
<small *ngIf="isInvalid('status')" class="p-error block mt-1">
44+
Status is required.
45+
</small>
46+
</div>
47+
48+
<div class="field mb-4">
49+
<label for="dueDate" class="block text-surface-900 dark:text-surface-0 font-medium mb-2">Due Date (Optional)</label>
50+
<p-calendar
51+
id="dueDate"
52+
formControlName="dueDate"
53+
styleClass="w-full"
54+
[showIcon]="true"
55+
dateFormat="yy-mm-dd">
56+
</p-calendar>
57+
</div>
58+
59+
<div class="field mb-4">
60+
<label for="description" class="block text-surface-900 dark:text-surface-0 font-medium mb-2">Description</label>
61+
<textarea id="description" pInputTextarea formControlName="description" class="w-full" [rows]="5"></textarea>
62+
</div>
63+
64+
<div class="flex justify-content-end gap-2">
65+
<p-button
66+
label="Cancel"
67+
icon="pi pi-times"
68+
styleClass="p-button-secondary"
69+
(click)="cancel()"
70+
[disabled]="isLoading">
71+
</p-button>
72+
<p-button
73+
type="submit"
74+
label="Create Task"
75+
icon="pi pi-check"
76+
[loading]="isLoading"
77+
[disabled]="!taskForm.valid || isLoading">
78+
</p-button>
79+
</div>
80+
81+
</form>
82+
</p-card>

src/app/features/task-item/components/task-item-create/task-item-create.component.scss

Whitespace-only changes.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { TaskItemCreateComponent } from './task-item-create.component';
4+
5+
describe('TaskItemCreateComponent', () => {
6+
let component: TaskItemCreateComponent;
7+
let fixture: ComponentFixture<TaskItemCreateComponent>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
imports: [TaskItemCreateComponent]
12+
})
13+
.compileComponents();
14+
15+
fixture = TestBed.createComponent(TaskItemCreateComponent);
16+
component = fixture.componentInstance;
17+
fixture.detectChanges();
18+
});
19+
20+
it('should create', () => {
21+
expect(component).toBeTruthy();
22+
});
23+
});
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { Component } from '@angular/core';
2+
import { SharedModule } from '../../../../shared/shared.module';
3+
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
4+
import { Router } from '@angular/router';
5+
import { SelectItem, MessageService } from 'primeng/api';
6+
import { Subject, takeUntil } from 'rxjs';
7+
import { ProjectDto, ProjectService } from '../../../projects/services/project.service';
8+
import { TaskStatus, TaskItemService } from '../../services/task-item.service';
9+
10+
@Component({
11+
selector: 'app-task-item-create',
12+
standalone: true,
13+
imports: [SharedModule, ReactiveFormsModule],
14+
templateUrl: './task-item-create.component.html',
15+
styleUrl: './task-item-create.component.scss'
16+
})
17+
export class TaskItemCreateComponent {
18+
taskForm!: FormGroup;
19+
isLoading = false;
20+
21+
projectOptions: SelectItem<string>[] = [];
22+
statusOptions: SelectItem<TaskStatus>[] = [];
23+
24+
private destroy$ = new Subject<void>();
25+
26+
constructor(
27+
private fb: FormBuilder,
28+
private taskItemService: TaskItemService,
29+
private projectService: ProjectService,
30+
private router: Router,
31+
private messageService: MessageService
32+
) { }
33+
34+
ngOnInit(): void {
35+
this.initializeForm();
36+
this.loadDropdownData();
37+
this.prepareStatusOptions();
38+
}
39+
40+
ngOnDestroy(): void {
41+
this.destroy$.next();
42+
this.destroy$.complete();
43+
}
44+
45+
initializeForm(): void {
46+
this.taskForm = this.fb.group({
47+
title: ['', Validators.required],
48+
projectId: ['', Validators.required],
49+
status: [TaskStatus.Todo, Validators.required],
50+
dueDate: [null],
51+
description: ['']
52+
});
53+
}
54+
55+
loadDropdownData(): void {
56+
this.isLoading = true;
57+
this.projectService.getUserProjects()
58+
.pipe(takeUntil(this.destroy$))
59+
.subscribe({
60+
next: (projects) => {
61+
this.projectOptions = projects.map(proj => ({
62+
label: proj.name,
63+
title: proj.name,
64+
value: proj.id
65+
}));
66+
this.isLoading = false;
67+
},
68+
error: (err) => {
69+
console.error("Error loading projects for dropdown", err);
70+
this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Could not load projects. Please try again.' });
71+
this.isLoading = false;
72+
this.projectOptions = [];
73+
}
74+
});
75+
}
76+
77+
prepareStatusOptions(): void {
78+
this.statusOptions = Object.keys(TaskStatus)
79+
.filter(key => !isNaN(Number(TaskStatus[key as keyof typeof TaskStatus])))
80+
.map(key => ({
81+
name: key,
82+
value: TaskStatus[key as keyof typeof TaskStatus] as TaskStatus
83+
}));
84+
}
85+
86+
isInvalid(controlName: string): boolean {
87+
const control = this.taskForm.get(controlName);
88+
return !!control && control.invalid && (control.dirty || control.touched);
89+
}
90+
91+
onSubmit(): void {
92+
if (this.taskForm.invalid) {
93+
this.taskForm.markAllAsTouched();
94+
this.messageService.add({ severity: 'warn', summary: 'Validation Error', detail: 'Please fill in all required fields.' });
95+
return;
96+
}
97+
98+
this.isLoading = true;
99+
const taskData = this.taskForm.value;
100+
101+
const selectedProject = this.projectOptions.find(p => p.value === taskData.projectId);
102+
if (selectedProject) {
103+
taskData.projectName = selectedProject.label;
104+
}
105+
106+
this.taskItemService.createTask(taskData)
107+
.pipe(takeUntil(this.destroy$))
108+
.subscribe({
109+
next: (createdTask) => {
110+
this.isLoading = false;
111+
this.messageService.add({ severity: 'success', summary: 'Success', detail: `Task "${createdTask.title}" created successfully!` });
112+
this.router.navigate(['/tasks']);
113+
},
114+
error: (err) => {
115+
this.isLoading = false;
116+
console.error('Error creating task:', err);
117+
this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Could not create task. Please try again.' });
118+
}
119+
});
120+
}
121+
122+
cancel(): void {
123+
this.router.navigate(['/tasks']);
124+
}
125+
}

src/app/shared/shared.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { ProgressSpinnerModule } from 'primeng/progressspinner';
2525
import { RippleModule } from 'primeng/ripple';
2626
import { SkeletonModule } from 'primeng/skeleton';
2727
import { ToastModule } from 'primeng/toast';
28+
import { CalendarModule } from 'primeng/calendar';
2829

2930
@NgModule({
3031
exports: [
@@ -58,7 +59,8 @@ import { ToastModule } from 'primeng/toast';
5859
ProgressSpinnerModule,
5960
RippleModule,
6061
SkeletonModule,
61-
ToastModule
62+
ToastModule,
63+
CalendarModule
6264
],
6365
})
6466
export class SharedModule {}

0 commit comments

Comments
 (0)