refactor: improve task database communication and enhance data flow in components

This commit is contained in:
Chneemann 2025-03-29 15:45:24 +01:00
parent 6e0429255b
commit 72ca03086d
12 changed files with 189 additions and 172 deletions

View file

@ -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": []
}
}

View file

@ -324,7 +324,7 @@
? 'block'
: 'none'
}"
(click)="deleteTaskData(overlayData)"
(click)="deleteTask(overlayData)"
></app-form-btn>
<app-form-btn
[class]="'btn-submit'"

View file

@ -10,6 +10,10 @@ import { FormBtnComponent } from '../../shared/components/buttons/form-btn/form-
import { ActivatedRoute, Router } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { SharedService } from '../../services/shared.service';
import { AuthService } from '../../services/auth.service';
import { firstValueFrom, map } from 'rxjs';
import { TaskService } from '../../services/task.service';
import { ApiService } from '../../services/api.service';
@Component({
selector: 'app-add-task',
@ -44,6 +48,9 @@ export class AddTaskComponent implements OnInit {
public firebaseService: FirebaseService,
private overlayService: OverlayService,
private sharedService: SharedService,
private taskService: TaskService,
private apiService: ApiService,
private authService: AuthService,
private route: ActivatedRoute,
private router: Router
) {}
@ -52,15 +59,15 @@ export class AddTaskComponent implements OnInit {
title: '',
description: '',
category: '',
status: 'todo',
priority: 'medium',
status: this.taskService.getStatuses()[0],
priority: this.taskService.getPriorities()[0],
subtasks: [],
subtasksTitle: [],
subtasksDone: [],
assigned: [],
assignees: [],
userData: [],
creator: this.firebaseService.getCurrentUserId(),
creator: '',
date: this.currentDate,
};
@ -72,21 +79,30 @@ export class AddTaskComponent implements OnInit {
* - Loads any existing task data from local storage.
*/
ngOnInit() {
this.setCurrentUserId();
this.loadEditTaskData();
this.routeParams();
this.loadLocalStorageData();
}
setCurrentUserId() {
this.authService
.getCurrentUserId()
.pipe(map((userId) => 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.apiService.saveNewTask(taskWithoutId).subscribe({
next: (response) => {
this.removeTaskData(ngForm);
this.closeOverlay();
} else {
if (this.getTaskData(overlayData).length > 0) {
const { id, ...taskWithoutId } = this.taskData;
this.firebaseService.replaceTaskData(overlayData, taskWithoutId);
this.closeOverlay();
}
}
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'])

View file

@ -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<void>();
readonly TODO = 'todo';
readonly IN_PROGRESS = 'inprogress';
readonly AWAIT_FEEDBACK = 'awaitfeedback';
@ -53,35 +56,39 @@ 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;
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);
this.isLoading = false;
},
});
}
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[] });
}
/**
* Subscribes to events from the DragDropService and handles them.
* @remarks
@ -178,4 +185,9 @@ export class BoardComponent {
])
);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}

View file

@ -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,23 +30,24 @@ 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;
this.taskService
.getTasks()
.pipe(finalize(() => (this.isLoading = false)))
.subscribe({
next: (response) => {
this.allTasks = response;
},
error: (err) => {
console.error('Error loading the tasks:', err);
this.isLoading = false;
},
});
}

View file

@ -36,20 +36,28 @@ export class ApiService {
// ------------- TASKS ------------- //
getTasks(): Observable<Task[]> {
return this.request<Task[]>('GET', `/api/tasks/`);
}
getTaskById(taskId: string): Observable<Task> {
return this.request<Task>('GET', `/api/tasks/${taskId}/`);
}
getTasksByStatus(status: string): Observable<Task[]> {
return this.request<Task[]>('GET', '/api/tasks/', undefined, { status });
}
updateTaskStatus(taskId: string, status: string): Observable<Task> {
return this.request<Task>('PUT', `/api/tasks/${taskId}/update_status/`, {
return this.request<Task>('PATCH', `/api/tasks/${taskId}/update_status/`, {
status,
});
}
saveNewTask(task: Task): Observable<Task> {
return this.request<Task>('POST', '/api/tasks/', task);
}
deleteTaskById(taskId: string): Observable<Task> {
return this.request<Task>('DELETE', `/api/tasks/${taskId}/`);
}
// ------------- USERS ------------- //
getUserById(userId: string): Observable<User> {

View file

@ -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({

View file

@ -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<string, Task[]>;
}> {
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<string, Task[]>);
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<string, Task[]>);
getPriorities(): string[] {
return this.priorities;
}
return {
allTasks: tasksWithUsers,
filteredTasks: updatedFilteredTasks,
};
})
);
})
getTasks(): Observable<Task[]> {
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<Task[]> {
return this.apiService.getTasksByStatus(status).pipe(
getTaskById(taskId: string): Observable<Task | null> {
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<Task[]> {
return this.apiService.getTasks().pipe(
catchError((error) => {
console.error('Error when retrieving the tasks:', error);
return of([]);
})
);
}
private loadUsersForTasks(tasks: Task[]): Observable<Task[]> {
private attachUsersToTask(task: Task): Observable<Task> {
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<Task[]> {
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<Task | null> {
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<string, UserSummary> {
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<string, UserSummary>);
}
private mapTaskUsers(task: Task, users: UserSummary[]): UserSummary[] {
const userMap = this.createUserMap(users);
private mapTaskUsers(
task: Task,
userMap: Record<string, UserSummary>
): UserSummary[] {
return [
...task.assignees.map((a) => userMap[a.userId] || null),
userMap[task.creator] || null,

View file

@ -5,7 +5,7 @@
}"
>
@if (overlayType === 'newTaskOverlay' || (overlayData !== '' &&
getTaskData(overlayData).length > 0)) {
getTaskData(overlayData) !== undefined)) {
<div class="header">
<app-btn-close (click)="closeDialog()"></app-btn-close>
</div>

View file

@ -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);
}
/**

View file

@ -121,7 +121,7 @@
</div>
}
</div>
@if(task.creator === firebaseService.getCurrentUserId()) {
@if(task.creator === currentUserId) {
<div class="btns">
<div class="btn btn-delete" (click)="deleteTask(overlayData)">
<img src="./../../../../../assets/img/contact/delete.svg" alt="" />

View file

@ -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();
}