Skip to content

Commit 3cb5034

Browse files
committed
feat(my-tasks): add project scope dropdown with persisted selection and filtered task sections
1 parent 865e0d3 commit 3cb5034

3 files changed

Lines changed: 108 additions & 5 deletions

File tree

src/app/features/task-item/components/user-task-items/user-task-items.component.html

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ <h2>My Tasks</h2>
1111
<i class="pi pi-user"></i>
1212
{{ currentUserDisplayName }}
1313
</span>
14+
<span class="meta-pill">
15+
<i class="pi pi-folder"></i>
16+
{{ selectedProjectName }}
17+
</span>
1418
<span class="meta-pill" *ngIf="isPreviewMode">
1519
<i class="pi pi-eye"></i>
1620
Preview mode
@@ -19,7 +23,23 @@ <h2>My Tasks</h2>
1923
</div>
2024
</div>
2125

22-
<button pButton type="button" label="Refresh" icon="pi pi-refresh" class="p-button-outlined" (click)="refresh()" [loading]="isLoading"></button>
26+
<div class="my-tasks-header__controls">
27+
<p-dropdown
28+
inputId="myTasksProjectPicker"
29+
[options]="projectFilterOptions"
30+
optionLabel="label"
31+
optionValue="value"
32+
[filter]="true"
33+
filterBy="label"
34+
[showClear]="false"
35+
[ngModel]="selectedProjectId"
36+
[disabled]="isLoading || projectFilterOptions.length <= 1"
37+
placeholder="Project scope"
38+
(onChange)="onProjectFilterChange($event.value)">
39+
</p-dropdown>
40+
41+
<button pButton type="button" label="Refresh" icon="pi pi-refresh" class="p-button-outlined" (click)="refresh()" [loading]="isLoading"></button>
42+
</div>
2343
</div>
2444
</p-card>
2545

src/app/features/task-item/components/user-task-items/user-task-items.component.scss

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@
1212
gap: 1rem;
1313
}
1414

15+
.my-tasks-header__controls {
16+
display: inline-flex;
17+
align-items: center;
18+
gap: 0.65rem;
19+
}
20+
21+
.my-tasks-header__controls :where(.p-dropdown) {
22+
min-width: 15rem;
23+
}
24+
1525
.my-tasks-header__title h2 {
1626
margin: 0;
1727
}
@@ -368,6 +378,15 @@
368378
}
369379

370380
@media (max-width: 992px) {
381+
.my-tasks-header__controls {
382+
width: 100%;
383+
}
384+
385+
.my-tasks-header__controls :where(.p-dropdown) {
386+
flex: 1;
387+
min-width: 0;
388+
}
389+
371390
.my-tasks-kpis {
372391
grid-template-columns: repeat(2, minmax(0, 1fr));
373392
}

src/app/features/task-item/components/user-task-items/user-task-items.component.ts

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ interface TaskEditForm {
2323
dueDate: Date | null;
2424
}
2525

26+
interface ProjectFilterOption {
27+
label: string;
28+
value: string | null;
29+
}
30+
2631
@Component({
2732
selector: 'app-user-task-items',
2833
standalone: true,
@@ -31,6 +36,7 @@ interface TaskEditForm {
3136
styleUrl: './user-task-items.component.scss'
3237
})
3338
export class UserTaskItemsComponent implements OnInit, OnDestroy {
39+
private static readonly PROJECT_SELECTION_CONTEXT = 'my-tasks';
3440
private readonly taskItemsApiClient = inject(TaskItemsApiClient);
3541
private readonly authService = inject(AuthService);
3642
private readonly appEnvironment = inject(APP_ENVIRONMENT);
@@ -48,6 +54,7 @@ export class UserTaskItemsComponent implements OnInit, OnDestroy {
4854
];
4955

5056
tasks: TaskItemDto[] = [];
57+
selectedProjectId: string | null = null;
5158
isLoading = false;
5259
isPreviewMode = false;
5360
previewDetail: string | null = null;
@@ -74,19 +81,19 @@ export class UserTaskItemsComponent implements OnInit, OnDestroy {
7481
}
7582

7683
get totalTasks(): number {
77-
return this.tasks.length;
84+
return this.filteredTasks.length;
7885
}
7986

8087
get todoTasks(): TaskItemDto[] {
81-
return this.tasks.filter((task) => task.status === TaskStatus.Todo);
88+
return this.filteredTasks.filter((task) => task.status === TaskStatus.Todo);
8289
}
8390

8491
get inProgressTasks(): TaskItemDto[] {
85-
return this.tasks.filter((task) => task.status === TaskStatus.InProgress);
92+
return this.filteredTasks.filter((task) => task.status === TaskStatus.InProgress);
8693
}
8794

8895
get doneTasks(): TaskItemDto[] {
89-
return this.tasks.filter((task) => task.status === TaskStatus.Done);
96+
return this.filteredTasks.filter((task) => task.status === TaskStatus.Done);
9097
}
9198

9299
get overdueTasksCount(): number {
@@ -122,10 +129,49 @@ export class UserTaskItemsComponent implements OnInit, OnDestroy {
122129
return 'Current User';
123130
}
124131

132+
get projectFilterOptions(): ProjectFilterOption[] {
133+
const uniqueProjects = new Map<string, string>();
134+
for (const task of this.tasks) {
135+
if (!task.projectId) {
136+
continue;
137+
}
138+
139+
if (!uniqueProjects.has(task.projectId)) {
140+
uniqueProjects.set(task.projectId, task.projectName || 'Unnamed project');
141+
}
142+
}
143+
144+
const projectOptions = [...uniqueProjects.entries()]
145+
.map(([value, label]) => ({ label, value }))
146+
.sort((left, right) => left.label.localeCompare(right.label));
147+
148+
return [{ label: 'All Projects', value: null }, ...projectOptions];
149+
}
150+
151+
get selectedProjectName(): string {
152+
return this.projectFilterOptions.find((option) => option.value === this.selectedProjectId)?.label ?? 'All Projects';
153+
}
154+
155+
private get filteredTasks(): TaskItemDto[] {
156+
if (!this.selectedProjectId) {
157+
return this.tasks;
158+
}
159+
160+
return this.tasks.filter((task) => task.projectId === this.selectedProjectId);
161+
}
162+
125163
refresh(): void {
126164
this.loadTasks();
127165
}
128166

167+
onProjectFilterChange(projectId: string | null): void {
168+
this.selectedProjectId = projectId;
169+
170+
if (projectId) {
171+
this.preferencesService.setLastSelectedProject(UserTaskItemsComponent.PROJECT_SELECTION_CONTEXT, projectId);
172+
}
173+
}
174+
129175
taskTrackBy(_: number, task: TaskItemDto): string {
130176
return task.id;
131177
}
@@ -398,6 +444,7 @@ export class UserTaskItemsComponent implements OnInit, OnDestroy {
398444
.subscribe({
399445
next: (tasks) => {
400446
this.tasks = this.sortTasks(tasks);
447+
this.ensureSelectedProjectIsValid();
401448
this.isLoading = false;
402449
},
403450
error: () => {
@@ -475,6 +522,7 @@ export class UserTaskItemsComponent implements OnInit, OnDestroy {
475522
}
476523
]);
477524

525+
this.ensureSelectedProjectIsValid();
478526
this.isLoading = false;
479527
}
480528

@@ -531,5 +579,21 @@ export class UserTaskItemsComponent implements OnInit, OnDestroy {
531579
const nextTasks = [...this.tasks];
532580
nextTasks[index] = updatedTask;
533581
this.tasks = this.sortTasks(nextTasks);
582+
this.ensureSelectedProjectIsValid();
583+
}
584+
585+
private ensureSelectedProjectIsValid(): void {
586+
const availableProjectIds = new Set(this.projectFilterOptions.map((option) => option.value).filter((value): value is string => !!value));
587+
if (this.selectedProjectId && availableProjectIds.has(this.selectedProjectId)) {
588+
return;
589+
}
590+
591+
const rememberedProjectId = this.preferencesService.getLastSelectedProject(UserTaskItemsComponent.PROJECT_SELECTION_CONTEXT);
592+
if (rememberedProjectId && availableProjectIds.has(rememberedProjectId)) {
593+
this.selectedProjectId = rememberedProjectId;
594+
return;
595+
}
596+
597+
this.selectedProjectId = null;
534598
}
535599
}

0 commit comments

Comments
 (0)