From 72ca03086d9ce7ff229868f9917f7e648d6e5859 Mon Sep 17 00:00:00 2001 From: Chneemann Date: Sat, 29 Mar 2025 15:45:24 +0100 Subject: [PATCH] refactor: improve task database communication and enhance data flow in components --- angular.json | 4 +- .../add-task/add-task.component.html | 2 +- .../components/add-task/add-task.component.ts | 85 +++++------ src/app/components/board/board.component.ts | 54 ++++--- .../components/summary/summary.component.ts | 26 ++-- src/app/services/api.service.ts | 18 ++- src/app/services/auth.service.ts | 1 - src/app/services/task.service.ts | 132 ++++++++---------- .../task-edit-overlay.component.html | 2 +- .../task-edit-overlay.component.ts | 6 +- .../task-overlay/task-overlay.component.html | 2 +- .../task-overlay/task-overlay.component.ts | 29 +++- 12 files changed, 189 insertions(+), 172 deletions(-) diff --git a/angular.json b/angular.json index 3c168f2..9c05626 100644 --- a/angular.json +++ b/angular.json @@ -24,7 +24,7 @@ "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": ["src/favicon.ico", "src/assets"], - "styles": ["src/styles.css", "node_modules/ngx-toastr/toastr.css"], + "styles": ["src/styles.scss", "node_modules/ngx-toastr/toastr.css"], "scripts": [] }, "configurations": { @@ -76,7 +76,7 @@ "tsConfig": "tsconfig.spec.json", "inlineStyleLanguage": "scss", "assets": ["src/favicon.ico", "src/assets"], - "styles": ["src/styles.css", "node_modules/ngx-toastr/toastr.css"], + "styles": ["src/styles.scss", "node_modules/ngx-toastr/toastr.css"], "scripts": [] } } diff --git a/src/app/components/add-task/add-task.component.html b/src/app/components/add-task/add-task.component.html index 91dec35..4e677a6 100644 --- a/src/app/components/add-task/add-task.component.html +++ b/src/app/components/add-task/add-task.component.html @@ -324,7 +324,7 @@ ? 'block' : 'none' }" - (click)="deleteTaskData(overlayData)" + (click)="deleteTask(overlayData)" > userId ?? '')) + .subscribe((userId) => { + this.taskData.creator = userId; + }); + } + /** * Loads task data for editing if applicable, or sets the task status if this is a new task overlay. * @param {string} overlayData The task id or status of the task to be loaded. * @param {string} overlayType The type of overlay to be opened. * @returns {void} */ - loadEditTaskData() { - const excludedValues = ['', 'todo', 'inprogress', 'awaitfeedback', 'done']; - if (!excludedValues.includes(this.overlayData)) { - const taskData = this.getTaskData(this.overlayData)[0]; + async loadEditTaskData() { + if (this.overlayData) { + const taskData = await firstValueFrom(this.getTaskData(this.overlayData)); Object.assign(this.taskData, taskData); } else if (this.overlayType === 'newTaskOverlay') { this.taskData.status = this.overlayData; @@ -156,9 +172,7 @@ export class AddTaskComponent implements OnInit { * @returns {Task[]} an array of tasks with the given id */ getTaskData(taskId: string) { - return this.firebaseService - .getAllTasks() - .filter((task) => task.id === taskId); + return this.apiService.getTaskById(taskId); } /** @@ -357,43 +371,20 @@ export class AddTaskComponent implements OnInit { } } - /** - * Submits the task form and saves the task data to the Firebase Realtime Database. - * - * If the form is valid and the overlay data is one of the allowed values, this - * function adds a new task to the 'tasks' node in the Firebase Realtime Database. - * If the overlay data is not one of the allowed values, this function updates the - * task with the given overlay data in the 'tasks' node. - * - * After submitting the form, this function resets the form and closes the overlay. - * It then navigates to the '/board' route. - * @param ngForm the form to submit - * @param overlayData the overlay data corresponding to the task to be submitted - * @returns {void} - */ onSubmit(ngForm: NgForm, overlayData: string) { - const allowedValues = [ - '', - 'none', - 'todo', - 'inprogress', - 'awaitfeedback', - 'done', - ]; if (ngForm.submitted && ngForm.form.valid) { - if (allowedValues.includes(overlayData)) { - const { id, ...taskWithoutId } = this.taskData; - this.firebaseService.addNewTask(taskWithoutId); - this.removeTaskData(ngForm); - this.closeOverlay(); - } else { - if (this.getTaskData(overlayData).length > 0) { - const { id, ...taskWithoutId } = this.taskData; - this.firebaseService.replaceTaskData(overlayData, taskWithoutId); + const { id, ...taskWithoutId } = this.taskData; + + this.apiService.saveNewTask(taskWithoutId).subscribe({ + next: (response) => { + this.removeTaskData(ngForm); this.closeOverlay(); - } - } - this.router.navigate(['/board']); + this.router.navigate(['/board']); + }, + error: (error) => { + console.error('Fehler beim Speichern:', error); + }, + }); } } @@ -419,9 +410,9 @@ export class AddTaskComponent implements OnInit { * @param overlayData the overlay data of the task to be deleted * @returns {void} */ - deleteTaskData(overlayData: string) { - this.firebaseService.deleteTask(overlayData); - this.closeOverlay(); + deleteTask(overlayData: string) { + this.apiService.deleteTaskById(overlayData); + this.closeDialog(); } @HostListener('document:click', ['$event']) diff --git a/src/app/components/board/board.component.ts b/src/app/components/board/board.component.ts index 90f019d..724d1b0 100644 --- a/src/app/components/board/board.component.ts +++ b/src/app/components/board/board.component.ts @@ -13,6 +13,7 @@ import { ApiService } from '../../services/api.service'; import { Task } from '../../interfaces/task.interface'; import { TaskService } from '../../services/task.service'; import { LoadingSpinnerComponent } from '../../shared/components/loading-spinner/loading-spinner.component'; +import { finalize, Subject, takeUntil } from 'rxjs'; @Component({ selector: 'app-board', @@ -30,6 +31,8 @@ import { LoadingSpinnerComponent } from '../../shared/components/loading-spinner styleUrl: './board.component.scss', }) export class BoardComponent { + private destroy$ = new Subject(); + readonly TODO = 'todo'; readonly IN_PROGRESS = 'inprogress'; readonly AWAIT_FEEDBACK = 'awaitfeedback'; @@ -53,33 +56,37 @@ export class BoardComponent { taskMovedFrom: string = ''; isLoading = false; - /** - * Is called when the component is initialized. - * Calls the `loadTasks` method to load tasks and subscribes to drag-and-drop events via `subscribeToDragDropEvents`. - */ ngOnInit() { - this.loadTasks(); + this.loadAllTasks(); this.subscribeToDragDropEvents(); } - /** - * Retrieves all tasks from the API and initializes the `allTasks` and `filteredTasks` properties. - */ - - loadTasks(): void { + loadAllTasks(): void { this.isLoading = true; - this.taskService.loadAllTasks().subscribe({ - next: (result) => { - this.allTasks = result.allTasks; - this.filteredTasks = result.filteredTasks; - this.isLoading = false; - }, - error: (err) => { - console.error('Error loading the tasks:', err); - this.isLoading = false; - }, - }); + this.taskService + .getTasksWithUsers() + .pipe(finalize(() => (this.isLoading = false))) + .subscribe({ + next: (response) => { + this.allTasks = response.allTasks; + this.filteredTasks = this.groupTasksByStatus(response.allTasks); + }, + error: (err) => { + console.error('Error loading the tasks:', err); + }, + }); + } + + groupTasksByStatus(tasks: Task[]): { [key: string]: Task[] } { + return tasks.reduce((acc, task) => { + const status = task.status; + if (!acc[status]) { + acc[status] = []; + } + acc[status].push(task); + return acc; + }, {} as { [key: string]: Task[] }); } /** @@ -178,4 +185,9 @@ export class BoardComponent { ]) ); } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } } diff --git a/src/app/components/summary/summary.component.ts b/src/app/components/summary/summary.component.ts index 35c4b41..03afc56 100644 --- a/src/app/components/summary/summary.component.ts +++ b/src/app/components/summary/summary.component.ts @@ -5,6 +5,7 @@ import { FirebaseService } from '../../services/firebase.service'; import { Task } from '../../interfaces/task.interface'; import { TaskService } from '../../services/task.service'; import { LoadingSpinnerComponent } from '../../shared/components/loading-spinner/loading-spinner.component'; +import { finalize } from 'rxjs'; @Component({ selector: 'app-summary', @@ -29,25 +30,26 @@ export class SummaryComponent { * This method loads all tasks from the TaskService. */ ngOnInit() { - this.loadTasks(); + this.loadAllTasks(); } /** * Loads all tasks from the TaskService. */ - loadTasks(): void { + loadAllTasks(): void { this.isLoading = true; - this.taskService.loadAllTasks().subscribe({ - next: (result) => { - this.allTasks = result.allTasks; - this.isLoading = false; - }, - error: (err) => { - console.error('Error loading the tasks:', err); - this.isLoading = false; - }, - }); + this.taskService + .getTasks() + .pipe(finalize(() => (this.isLoading = false))) + .subscribe({ + next: (response) => { + this.allTasks = response; + }, + error: (err) => { + console.error('Error loading the tasks:', err); + }, + }); } /** diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts index b941895..3cfee09 100644 --- a/src/app/services/api.service.ts +++ b/src/app/services/api.service.ts @@ -36,20 +36,28 @@ export class ApiService { // ------------- TASKS ------------- // + getTasks(): Observable { + return this.request('GET', `/api/tasks/`); + } + getTaskById(taskId: string): Observable { return this.request('GET', `/api/tasks/${taskId}/`); } - getTasksByStatus(status: string): Observable { - return this.request('GET', '/api/tasks/', undefined, { status }); - } - updateTaskStatus(taskId: string, status: string): Observable { - return this.request('PUT', `/api/tasks/${taskId}/update_status/`, { + return this.request('PATCH', `/api/tasks/${taskId}/update_status/`, { status, }); } + saveNewTask(task: Task): Observable { + return this.request('POST', '/api/tasks/', task); + } + + deleteTaskById(taskId: string): Observable { + return this.request('DELETE', `/api/tasks/${taskId}/`); + } + // ------------- USERS ------------- // getUserById(userId: string): Observable { diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts index 5d964ed..a34fd1b 100755 --- a/src/app/services/auth.service.ts +++ b/src/app/services/auth.service.ts @@ -10,7 +10,6 @@ import { import { ApiService } from './api.service'; import { TokenService } from './token.service'; import { ErrorHandlingService } from './error-handling.service'; -import { ToastrService } from 'ngx-toastr'; import { Router } from '@angular/router'; @Injectable({ diff --git a/src/app/services/task.service.ts b/src/app/services/task.service.ts index 70a2dc8..41b3eeb 100644 --- a/src/app/services/task.service.ts +++ b/src/app/services/task.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { Observable, forkJoin, of } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { ApiService } from './api.service'; import { Task } from '../interfaces/task.interface'; import { catchError, map, switchMap } from 'rxjs/operators'; @@ -8,55 +8,67 @@ import { UserSummary } from '../interfaces/user.interface'; @Injectable({ providedIn: 'root' }) export class TaskService { private readonly statuses = ['todo', 'inprogress', 'awaitfeedback', 'done']; + private readonly priorities = ['low', 'medium', 'high']; 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); + getStatuses(): string[] { + return this.statuses; + } - 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); + getPriorities(): string[] { + return this.priorities; + } - return { - allTasks: tasksWithUsers, - filteredTasks: updatedFilteredTasks, - }; - }) - ); - }) + getTasks(): Observable { + return this.fetchTasks(); + } + + getTasksWithUsers(): Observable<{ allTasks: Task[] }> { + return this.fetchTasks().pipe( + switchMap((tasks) => + this.attachUsersToTasks(tasks).pipe( + map((tasksWithUsers) => ({ allTasks: tasksWithUsers })) + ) + ) ); } - private fetchTasksByStatus(status: string): Observable { - return this.apiService.getTasksByStatus(status).pipe( + getTaskById(taskId: string): Observable { + return this.apiService.getTaskById(taskId).pipe( catchError((error) => { - console.error( - `Fehler beim Abrufen von Tasks für Status ${status}:`, - error - ); + console.error('Error loading the task:', error); + return of(null); + }), + switchMap((task) => (task ? this.attachUsersToTask(task) : of(null))) + ); + } + + private fetchTasks(): Observable { + return this.apiService.getTasks().pipe( + catchError((error) => { + console.error('Error when retrieving the tasks:', error); return of([]); }) ); } - private loadUsersForTasks(tasks: Task[]): Observable { + private attachUsersToTask(task: Task): Observable { + const userIds = [task.creator, ...task.assignees.map((a) => a.userId)]; + + return this.apiService.getUsersByIds(userIds).pipe( + catchError((error) => { + console.error('Error when retrieving user data:', error); + return of([]); + }), + map((users) => ({ + ...task, + userData: this.mapTaskUsers(task, this.createUserMap(users)), + })) + ); + } + + private attachUsersToTasks(tasks: Task[]): Observable { const userIds = new Set( tasks.flatMap((task) => [ task.creator, @@ -66,62 +78,32 @@ export class TaskService { return this.apiService.getUsersByIds([...userIds]).pipe( catchError((error) => { - console.error('Fehler beim Abrufen der Benutzer:', error); + console.error('Error when retrieving users:', error); return of([]); }), map((users) => { const userMap = this.createUserMap(users); return tasks.map((task) => ({ ...task, - userData: [ - ...task.assignees.map((a) => userMap[a.userId] || null), - userMap[task.creator] || null, - ].filter(Boolean), + userData: this.mapTaskUsers(task, userMap), })); }) ); } - 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.userId)]; - - 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, - }; + if (user?.id) { + acc[user.id] = user; + } return acc; }, {} as Record); } - private mapTaskUsers(task: Task, users: UserSummary[]): UserSummary[] { - const userMap = this.createUserMap(users); + private mapTaskUsers( + task: Task, + userMap: Record + ): UserSummary[] { return [ ...task.assignees.map((a) => userMap[a.userId] || null), userMap[task.creator] || null, diff --git a/src/app/shared/components/overlay/task-edit-overlay/task-edit-overlay.component.html b/src/app/shared/components/overlay/task-edit-overlay/task-edit-overlay.component.html index 3dedb0d..95234e1 100644 --- a/src/app/shared/components/overlay/task-edit-overlay/task-edit-overlay.component.html +++ b/src/app/shared/components/overlay/task-edit-overlay/task-edit-overlay.component.html @@ -5,7 +5,7 @@ }" > @if (overlayType === 'newTaskOverlay' || (overlayData !== '' && - getTaskData(overlayData).length > 0)) { + getTaskData(overlayData) !== undefined)) {
diff --git a/src/app/shared/components/overlay/task-edit-overlay/task-edit-overlay.component.ts b/src/app/shared/components/overlay/task-edit-overlay/task-edit-overlay.component.ts index ba8e764..822e3dc 100644 --- a/src/app/shared/components/overlay/task-edit-overlay/task-edit-overlay.component.ts +++ b/src/app/shared/components/overlay/task-edit-overlay/task-edit-overlay.component.ts @@ -7,6 +7,7 @@ import { AssignedComponent } from '../../../../components/add-task/assigned/assi import { AddTaskComponent } from '../../../../components/add-task/add-task.component'; import { ActivatedRoute, Router } from '@angular/router'; import { BtnBackComponent } from '../../buttons/btn-back/btn-back.component'; +import { ApiService } from '../../../../services/api.service'; @Component({ selector: 'app-task-edit-overlay', @@ -24,6 +25,7 @@ export class TaskEditOverlayComponent { constructor( public firebaseService: FirebaseService, + private apiService: ApiService, private route: ActivatedRoute, private router: Router ) {} @@ -53,9 +55,7 @@ export class TaskEditOverlayComponent { * @returns {Task[]} an array of tasks with the given id */ getTaskData(taskId: string) { - return this.firebaseService - .getAllTasks() - .filter((task) => task.id === taskId); + return this.apiService.getTaskById(taskId); } /** 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 5edf324..8c8f83f 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 @@ -121,7 +121,7 @@ } - @if(task.creator === firebaseService.getCurrentUserId()) { + @if(task.creator === currentUserId) {
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 0726f84..cf58333 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 @@ -8,6 +8,9 @@ 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'; +import { AuthService } from '../../../../services/auth.service'; +import { map } from 'rxjs'; +import { ApiService } from '../../../../services/api.service'; @Component({ selector: 'app-task-overlay', @@ -22,11 +25,14 @@ export class TaskOverlayComponent implements OnInit { task: Task | null = null; overlayMobile: boolean = false; + currentUserId: string = ''; constructor( public firebaseService: FirebaseService, private overlayService: OverlayService, private taskService: TaskService, + private authService: AuthService, + private apiService: ApiService, private router: Router, private route: ActivatedRoute ) {} @@ -44,10 +50,20 @@ export class TaskOverlayComponent implements OnInit { * Also sets `overlayMobile` to `true` if parameters are successfully retrieved. */ ngOnInit() { + this.setCurrentUserId(); this.setOverlayDataFromRoute(); this.loadTask(this.overlayData); } + setCurrentUserId() { + this.authService + .getCurrentUserId() + .pipe(map((userId) => userId ?? '')) + .subscribe((userId) => { + this.currentUserId = userId; + }); + } + setOverlayDataFromRoute() { if (this.overlayData === '') { this.route.params.subscribe((params) => { @@ -58,7 +74,7 @@ export class TaskOverlayComponent implements OnInit { } loadTask(taskId: string) { - this.taskService.loadSingleTask(taskId).subscribe({ + this.taskService.getTaskById(taskId).subscribe({ next: (task) => { this.task = task; }, @@ -102,8 +118,15 @@ export class TaskOverlayComponent implements OnInit { * @param overlayData the overlay data of the task to be deleted * @returns {void} */ - deleteTask(overlayData: string) { - this.firebaseService.deleteTask(overlayData); + deleteTask(taskId: string) { + this.apiService.deleteTaskById(taskId).subscribe({ + next: (task) => { + console.log('Task deleted successfully:', task); + }, + error: (err) => { + console.error('Error deleting task', err); + }, + }); this.closeDialog(); }