diff --git a/src/app/components/add-task/add-task.component.ts b/src/app/components/add-task/add-task.component.ts index ddc3af7..615dbc9 100644 --- a/src/app/components/add-task/add-task.component.ts +++ b/src/app/components/add-task/add-task.component.ts @@ -59,6 +59,7 @@ export class AddTaskComponent implements OnInit { subtasksDone: [], assigned: [], assignees: [], + userData: [], creator: this.firebaseService.getCurrentUserId(), date: this.currentDate, }; diff --git a/src/app/components/board/board.component.ts b/src/app/components/board/board.component.ts index cbbbf5e..cc2bd21 100644 --- a/src/app/components/board/board.component.ts +++ b/src/app/components/board/board.component.ts @@ -11,7 +11,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { TaskHighlightedComponent } from './task/task-highlighted/task-highlighted.component'; import { ApiService } from '../../services/api.service'; import { Task } from '../../interfaces/task.interface'; -import { forkJoin } from 'rxjs'; +import { TaskService } from '../../services/task.service'; @Component({ selector: 'app-board', @@ -37,6 +37,7 @@ export class BoardComponent { public dragDropService: DragDropService, public overlayService: OverlayService, private sharedService: SharedService, + private taskService: TaskService, private apiService: ApiService, private router: Router ) {} @@ -62,22 +63,15 @@ export class BoardComponent { * Retrieves all tasks from the API and initializes the `allTasks` and `filteredTasks` properties. */ loadTasks(): void { - const statuses = [ - this.TODO, - this.IN_PROGRESS, - this.AWAIT_FEEDBACK, - this.DONE, - ]; - const requests = statuses.map((status) => - this.apiService.getTasksByStatus(status) - ); + this.taskService.loadAllTasks().subscribe({ + next: (result) => { + this.allTasks = result.allTasks; + this.filteredTasks = result.filteredTasks; + }, - forkJoin(requests).subscribe((results) => { - this.allTasks = results.flat(); - this.filteredTasks = statuses.reduce((acc, status, index) => { - acc[status] = results[index]; - return acc; - }, {} as { [key: string]: Task[] }); + error: (err) => { + console.error('Error loading the tasks:', err); + }, }); } diff --git a/src/app/components/board/task/task.component.html b/src/app/components/board/task/task.component.html index 1847982..c27b810 100644 --- a/src/app/components/board/task/task.component.html +++ b/src/app/components/board/task/task.component.html @@ -27,7 +27,7 @@
{{ task.description }}
- @if(task.subtasksTitle.length > 0) { + @if(task.subtasks.length > 0) {
- {{ completedSubtasks() }} / {{ task.subtasksTitle.length }} Subtasks + {{ completedSubtasks() }} / {{ task.subtasks.length }} Subtasks
} @@ -70,14 +73,15 @@ > } @if (AssignedDialogId != '') {
+ @for (user of task.userData; track user) { @if (user.id === + AssignedDialogId) {

- {{ firebaseService.getUserDetails(AssignedDialogId, "firstName") }} - @if (AssignedDialogId === firebaseService.getCurrentUserId()) { - (du) } + {{ user.firstName }}

- {{ firebaseService.getUserDetails(AssignedDialogId, "lastName") }} + {{ user.lastName }}

+ }}
} } diff --git a/src/app/components/board/task/task.component.ts b/src/app/components/board/task/task.component.ts index 1198625..95ba6c5 100644 --- a/src/app/components/board/task/task.component.ts +++ b/src/app/components/board/task/task.component.ts @@ -22,6 +22,9 @@ export class TaskComponent { dialogX: number = 0; dialogY: number = 0; + creator: any = ''; + assignees: any[] = []; + categoryColors = new Map([ ['User Story', '#0038ff'], ['Technical Task', '#20d7c2'], @@ -129,8 +132,7 @@ export class TaskComponent { * @returns the number of completed subtasks */ completedSubtasks(): number { - return this.task.subtasksDone.filter((subtask: boolean) => subtask === true) - .length; + return this.task.subtasks.filter((subtask) => subtask.done).length; } /** @@ -138,12 +140,11 @@ export class TaskComponent { * @returns the percentage of completed subtasks as a number */ completedSubtasksPercent(): number { - const subtasks = this.task.subtasksDone; - const completedSubtasksCount = subtasks.filter( - (subtask: boolean) => subtask === true + const completedSubtasksCount = this.task.subtasks.filter( + (subtask) => subtask.done ).length; - return (completedSubtasksCount / subtasks.length) * 100; + return (completedSubtasksCount / this.task.subtasks.length) * 100; } // Assigned diff --git a/src/app/interfaces/task.interface.ts b/src/app/interfaces/task.interface.ts index e14fd28..0776d21 100644 --- a/src/app/interfaces/task.interface.ts +++ b/src/app/interfaces/task.interface.ts @@ -1,3 +1,5 @@ +import { UserSummary } from './user.interface'; + export interface Task { id?: string; title: string; @@ -9,7 +11,8 @@ export interface Task { subtasksTitle: string[]; subtasksDone: boolean[]; assigned: string[]; - assignees: string[]; + assignees: Assignee[]; + userData: UserSummary[]; creator: string; date: string; } @@ -19,3 +22,9 @@ export interface Subtask { title: string; done: boolean; } + +export interface Assignee { + id: string; + user: string; + task: string; +} diff --git a/src/app/interfaces/user.interface.ts b/src/app/interfaces/user.interface.ts index c631ff3..8d6e809 100644 --- a/src/app/interfaces/user.interface.ts +++ b/src/app/interfaces/user.interface.ts @@ -10,3 +10,11 @@ export interface User { status: boolean; lastLogin: number; } + +export interface UserSummary { + id?: string; + firstName: string; + lastName: string; + initials: string; + color: string; +} diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts index 28dd9db..2d96b12 100644 --- a/src/app/services/api.service.ts +++ b/src/app/services/api.service.ts @@ -1,8 +1,9 @@ -import { Injectable } from '@angular/core'; +import { Injectable, signal } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { apiConfig } from '../environments/config'; import { Task } from '../interfaces/task.interface'; +import { User } from '../interfaces/user.interface'; @Injectable({ providedIn: 'root', @@ -14,6 +15,10 @@ export class ApiService { // ------------- TASKS ------------- // + getTaskById(taskId: string): Observable { + return this.http.get(`${this.apiUrl}/api/tasks/${taskId}/`); + } + getTasksByStatus(status: string): Observable { return this.http.get(`${this.apiUrl}/api/tasks/`, { params: { status }, @@ -26,4 +31,12 @@ export class ApiService { { status: status } ); } + + // ------------- USERS ------------- // + + getUsersByIds(userIds: string[]): Observable { + return this.http.get(`${this.apiUrl}/api/users/`, { + params: { ids: userIds.join(',') }, + }); + } } diff --git a/src/app/services/task.service.ts b/src/app/services/task.service.ts new file mode 100644 index 0000000..2107d59 --- /dev/null +++ b/src/app/services/task.service.ts @@ -0,0 +1,130 @@ +import { Injectable } from '@angular/core'; +import { Observable, forkJoin, of } from 'rxjs'; +import { ApiService } from './api.service'; +import { Task } from '../interfaces/task.interface'; +import { catchError, map, switchMap } from 'rxjs/operators'; +import { UserSummary } from '../interfaces/user.interface'; + +@Injectable({ providedIn: 'root' }) +export class TaskService { + private readonly statuses = ['todo', 'inprogress', 'awaitfeedback', 'done']; + + constructor(private apiService: ApiService) {} + + loadAllTasks(): Observable<{ + allTasks: Task[]; + filteredTasks: Record; + }> { + return forkJoin( + this.statuses.map((status) => this.fetchTasksByStatus(status)) + ).pipe( + switchMap((results) => { + const allTasks = results.flat(); + const filteredTasks = this.statuses.reduce((acc, status, index) => { + acc[status] = results[index] || []; + return acc; + }, {} as Record); + + return this.loadUsersForTasks(allTasks).pipe( + map((tasksWithUsers) => { + const updatedFilteredTasks = this.statuses.reduce((acc, status) => { + acc[status] = tasksWithUsers.filter( + (task) => task.status === status + ); + return acc; + }, {} as Record); + + return { + allTasks: tasksWithUsers, + filteredTasks: updatedFilteredTasks, + }; + }) + ); + }) + ); + } + + private fetchTasksByStatus(status: string): Observable { + return this.apiService.getTasksByStatus(status).pipe( + catchError((error) => { + console.error( + `Fehler beim Abrufen von Tasks für Status ${status}:`, + error + ); + return of([]); + }) + ); + } + + private loadUsersForTasks(tasks: Task[]): Observable { + const userIds = new Set( + tasks.flatMap((task) => [ + task.creator, + ...task.assignees.map((a) => a.user), + ]) + ); + + return this.apiService.getUsersByIds([...userIds]).pipe( + catchError((error) => { + console.error('Fehler beim Abrufen der Benutzer:', error); + return of([]); + }), + map((users) => { + const userMap = this.createUserMap(users); + return tasks.map((task) => ({ + ...task, + userData: [ + ...task.assignees.map((a) => userMap[a.user] || null), + userMap[task.creator] || null, + ].filter(Boolean), + })); + }) + ); + } + + loadSingleTask(taskId: string): Observable { + return this.apiService.getTaskById(taskId).pipe( + switchMap((task) => { + if (!task) return of(null); + const userIds = [task.creator, ...task.assignees.map((a) => a.user)]; + + return this.apiService.getUsersByIds(userIds).pipe( + map((users) => ({ + ...task, + userData: this.mapTaskUsers(task, users), + })), + catchError((error) => { + console.error('Fehler beim Abrufen der Benutzerdaten:', error); + return of(task); + }) + ); + }), + catchError((error) => { + console.error('Fehler beim Laden des Tasks:', error); + return of(null); + }) + ); + } + + private createUserMap(users: UserSummary[]): Record { + return users.reduce((acc, user) => { + if (user?.id) + acc[user.id] = { + id: user.id, + firstName: user.firstName, + lastName: user.lastName, + initials: user.initials, + color: user.color, + }; + return acc; + }, {} as Record); + } + + private mapTaskUsers(task: Task, users: UserSummary[]): UserSummary[] { + const userMap = this.createUserMap(users); + return [ + ...task.assignees.map((a) => userMap[a.user] || null), + userMap[task.creator] || null, + ].filter(Boolean); + } +} diff --git a/src/app/shared/components/overlay/task-overlay/task-overlay.component.html b/src/app/shared/components/overlay/task-overlay/task-overlay.component.html index bb0062e..424d9b4 100644 --- a/src/app/shared/components/overlay/task-overlay/task-overlay.component.html +++ b/src/app/shared/components/overlay/task-overlay/task-overlay.component.html @@ -3,16 +3,14 @@ overlay: !overlayMobile, 'overlay-mobile': overlayMobile, }" - *ngIf="overlayData !== '' && getTaskData(overlayData).length > 0" + *ngIf="overlayData !== '' && task != null" >
- {{ getTaskData(overlayData)[0].category }} + {{ task.category }}
@if (overlayMobile) { @@ -26,126 +24,104 @@ 'content-mobile': overlayMobile, }" > -
{{ getTaskData(overlayData)[0].title }}
+
{{ task.title }}
- {{ getTaskData(overlayData)[0].description }} + {{ task.description }}

{{ "addTask.date" | translate }}:

- {{ timeConverter(getTaskData(overlayData)[0].date) }} + {{ timeConverter(task.date) }}

{{ "addTask.priority" | translate }}:

- {{ capitalizeFirstLetter(getTaskData(overlayData)[0].priority) }} -
+ {{ capitalizeFirstLetter(task.priority) }} +
+

{{ "addTask.creator" | translate }}:

+ @for (user of task.userData; track user) { @if (task.creator === user.id) + {
- {{ - firebaseService.getUserDetails( - getTaskData(overlayData)[0].creator, - "initials" - ) - }} + {{ user.initials }}

- {{ - firebaseService.getUserDetails( - getTaskData(overlayData)[0].creator, - "firstName" - ) - }} + {{ user.firstName }}

 

- {{ - firebaseService.getUserDetails( - getTaskData(overlayData)[0].creator, - "lastName" - ) - }} + {{ user.lastName }}

+ } }
- @if (getTaskData(overlayData)[0].assigned.length > 0) { + @if (task.assignees.length > 0) {

{{ "addTask.assigned" | translate }}:

- @for (user of getTaskData(overlayData)[0].assigned; track user) { + @for (assigned of task.assignees; track assigned.id) { @for (user of + task.userData; track user) { @if (assigned.user === user.id) {
- {{ firebaseService.getUserDetails(user, "initials") }} + {{ user.initials }}

- {{ firebaseService.getUserDetails(user, "firstName") }} + {{ user.firstName }}

 

- {{ firebaseService.getUserDetails(user, "lastName") }} + {{ user.lastName }}

- } + } } }
- } @if (getTaskData(overlayData)[0].subtasksTitle.length > 0) { + } @if (task.subtasks.length > 0) {

{{ "addTask.subtask" | translate }}:

- @for (subtask of getTaskData(overlayData)[0].subtasksTitle; track subtask; - let i = $index) { + @for (subtask of task.subtasks; track subtask; let i = $index) {
-

{{ subtask }}

+

{{ subtask.title }}

}
}
- @if(getTaskData(overlayData)[0].creator === - firebaseService.getCurrentUserId()) { + @if(task.creator === firebaseService.getCurrentUserId()) {
diff --git a/src/app/shared/components/overlay/task-overlay/task-overlay.component.ts b/src/app/shared/components/overlay/task-overlay/task-overlay.component.ts index 684aaea..0726f84 100644 --- a/src/app/shared/components/overlay/task-overlay/task-overlay.component.ts +++ b/src/app/shared/components/overlay/task-overlay/task-overlay.component.ts @@ -6,6 +6,8 @@ import { OverlayService } from '../../../../services/overlay.service'; import { ActivatedRoute, Router } from '@angular/router'; import { BtnBackComponent } from '../../buttons/btn-back/btn-back.component'; import { TranslateModule } from '@ngx-translate/core'; +import { Task } from '../../../../interfaces/task.interface'; +import { TaskService } from '../../../../services/task.service'; @Component({ selector: 'app-task-overlay', @@ -18,11 +20,13 @@ export class TaskOverlayComponent implements OnInit { @Input() overlayData: string = ''; @Output() closeDialogEmitter = new EventEmitter(); + task: Task | null = null; overlayMobile: boolean = false; constructor( public firebaseService: FirebaseService, private overlayService: OverlayService, + private taskService: TaskService, private router: Router, private route: ActivatedRoute ) {} @@ -40,16 +44,30 @@ export class TaskOverlayComponent implements OnInit { * Also sets `overlayMobile` to `true` if parameters are successfully retrieved. */ ngOnInit() { - if (this.overlayData == '') { - if (this.route.params.subscribe()) { - this.route.params.subscribe((params) => { - this.overlayData = params['id']; - this.overlayMobile = true; - }); - } + this.setOverlayDataFromRoute(); + this.loadTask(this.overlayData); + } + + setOverlayDataFromRoute() { + if (this.overlayData === '') { + this.route.params.subscribe((params) => { + this.overlayData = params['id']; + this.overlayMobile = true; + }); } } + loadTask(taskId: string) { + this.taskService.loadSingleTask(taskId).subscribe({ + next: (task) => { + this.task = task; + }, + error: (err) => { + console.error('Error loading the task:', err); + }, + }); + } + /** * Close the task overlay. * @@ -89,40 +107,6 @@ export class TaskOverlayComponent implements OnInit { this.closeDialog(); } - /** - * Gets the task data from the Firebase Realtime Database for the given task id. - * - * This function takes a task id and returns an array of tasks with the given id. - * The function uses the Firebase service to get all tasks from the Realtime Database - * and then filters the result to return only the task with the given id. - * - * @param taskId The id of the task to be retrieved. - * @returns {Task[]} An array of tasks with the given id. - */ - getTaskData(taskId: string) { - return this.firebaseService - .getAllTasks() - .filter((task) => task.id === taskId); - } - - /** - * Retrieves the status of a single subtask of a task. - * - * This function takes the task id and the index of the subtask - * and returns the status of the subtask at the given index. - * - * @param taskId The task id of the task containing the subtask. - * @param index The index of the subtask. - * @returns {boolean} The status of the subtask at the given index. - */ - getSubTaskStatus(taskId: string, index: number) { - const subtask = this.firebaseService - .getAllTasks() - .filter((task) => task.id === taskId); - - return subtask[0].subtasksDone[index]; - } - /** * Toggles the status of a single subtask of a task. *