feat: migrate TaskComponent & TaskOverlayComponent to Django Rest and implement TaskService

This commit is contained in:
Chneemann 2025-03-22 20:14:43 +01:00
parent df7629256a
commit 18903e3c55
10 changed files with 252 additions and 132 deletions

View file

@ -59,6 +59,7 @@ export class AddTaskComponent implements OnInit {
subtasksDone: [], subtasksDone: [],
assigned: [], assigned: [],
assignees: [], assignees: [],
userData: [],
creator: this.firebaseService.getCurrentUserId(), creator: this.firebaseService.getCurrentUserId(),
date: this.currentDate, date: this.currentDate,
}; };

View file

@ -11,7 +11,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { TaskHighlightedComponent } from './task/task-highlighted/task-highlighted.component'; import { TaskHighlightedComponent } from './task/task-highlighted/task-highlighted.component';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
import { Task } from '../../interfaces/task.interface'; import { Task } from '../../interfaces/task.interface';
import { forkJoin } from 'rxjs'; import { TaskService } from '../../services/task.service';
@Component({ @Component({
selector: 'app-board', selector: 'app-board',
@ -37,6 +37,7 @@ export class BoardComponent {
public dragDropService: DragDropService, public dragDropService: DragDropService,
public overlayService: OverlayService, public overlayService: OverlayService,
private sharedService: SharedService, private sharedService: SharedService,
private taskService: TaskService,
private apiService: ApiService, private apiService: ApiService,
private router: Router private router: Router
) {} ) {}
@ -62,22 +63,15 @@ export class BoardComponent {
* Retrieves all tasks from the API and initializes the `allTasks` and `filteredTasks` properties. * Retrieves all tasks from the API and initializes the `allTasks` and `filteredTasks` properties.
*/ */
loadTasks(): void { loadTasks(): void {
const statuses = [ this.taskService.loadAllTasks().subscribe({
this.TODO, next: (result) => {
this.IN_PROGRESS, this.allTasks = result.allTasks;
this.AWAIT_FEEDBACK, this.filteredTasks = result.filteredTasks;
this.DONE, },
];
const requests = statuses.map((status) =>
this.apiService.getTasksByStatus(status)
);
forkJoin(requests).subscribe((results) => { error: (err) => {
this.allTasks = results.flat(); console.error('Error loading the tasks:', err);
this.filteredTasks = statuses.reduce((acc, status, index) => { },
acc[status] = results[index];
return acc;
}, {} as { [key: string]: Task[] });
}); });
} }

View file

@ -27,7 +27,7 @@
<div class="description"> <div class="description">
{{ task.description }} {{ task.description }}
</div> </div>
@if(task.subtasksTitle.length > 0) { @if(task.subtasks.length > 0) {
<div class="subtask"> <div class="subtask">
<div class="subtask-line"> <div class="subtask-line">
<span <span
@ -36,28 +36,31 @@
></span> ></span>
</div> </div>
<div class="subtask-text"> <div class="subtask-text">
{{ completedSubtasks() }} / {{ task.subtasksTitle.length }} Subtasks {{ completedSubtasks() }} / {{ task.subtasks.length }} Subtasks
</div> </div>
</div> </div>
} }
<div class="footer"> <div class="footer">
<div class="footer-badge"> <div class="footer-badge">
@for (user of task.userData; track user) { @if (task.creator ===
user.id) {
<span <span
class="footer-badged" class="footer-badged"
(mousemove)="openDialog(task.creator, $event)" (mousemove)="openDialog(task.creator, $event)"
(mouseleave)="closeDialog()" (mouseleave)="closeDialog()"
[style.background-color]="userBadgedColor(task.creator)" [style.background-color]="user.color"
>{{ userBadged(task.creator) }}</span >{{ user.initials }}</span
> >
@for (assigned of task.assigned; track assigned) { } @for (assigned of task.assignees; track assigned.id) { @if
(assigned.user === user.id) {
<span <span
class="footer-badged" class="footer-badged"
(mousemove)="openDialog(assigned, $event)" (mousemove)="openDialog(user.id, $event)"
(mouseleave)="closeDialog()" (mouseleave)="closeDialog()"
[style.background-color]="userBadgedColor(assigned)" [style.background-color]="user.color"
>{{ userBadged(assigned) }}</span >{{ user.initials }}</span
> >
} } } }
</div> </div>
<div class="footer-priority prio-{{ task.priority }}"></div> <div class="footer-priority prio-{{ task.priority }}"></div>
</div> </div>
@ -70,14 +73,15 @@
></app-task-menu> ></app-task-menu>
} @if (AssignedDialogId != '') { } @if (AssignedDialogId != '') {
<div class="dialog" [style.left.px]="dialogX" [style.top.px]="dialogY"> <div class="dialog" [style.left.px]="dialogX" [style.top.px]="dialogY">
@for (user of task.userData; track user) { @if (user.id ===
AssignedDialogId) {
<p> <p>
{{ firebaseService.getUserDetails(AssignedDialogId, "firstName") }} {{ user.firstName }}
@if (AssignedDialogId === firebaseService.getCurrentUserId()) {
<span>(du)</span> }
</p> </p>
<p> <p>
{{ firebaseService.getUserDetails(AssignedDialogId, "lastName") }} {{ user.lastName }}
</p> </p>
}}
</div> </div>
} } } }
</section> </section>

View file

@ -22,6 +22,9 @@ export class TaskComponent {
dialogX: number = 0; dialogX: number = 0;
dialogY: number = 0; dialogY: number = 0;
creator: any = '';
assignees: any[] = [];
categoryColors = new Map<string, string>([ categoryColors = new Map<string, string>([
['User Story', '#0038ff'], ['User Story', '#0038ff'],
['Technical Task', '#20d7c2'], ['Technical Task', '#20d7c2'],
@ -129,8 +132,7 @@ export class TaskComponent {
* @returns the number of completed subtasks * @returns the number of completed subtasks
*/ */
completedSubtasks(): number { completedSubtasks(): number {
return this.task.subtasksDone.filter((subtask: boolean) => subtask === true) return this.task.subtasks.filter((subtask) => subtask.done).length;
.length;
} }
/** /**
@ -138,12 +140,11 @@ export class TaskComponent {
* @returns the percentage of completed subtasks as a number * @returns the percentage of completed subtasks as a number
*/ */
completedSubtasksPercent(): number { completedSubtasksPercent(): number {
const subtasks = this.task.subtasksDone; const completedSubtasksCount = this.task.subtasks.filter(
const completedSubtasksCount = subtasks.filter( (subtask) => subtask.done
(subtask: boolean) => subtask === true
).length; ).length;
return (completedSubtasksCount / subtasks.length) * 100; return (completedSubtasksCount / this.task.subtasks.length) * 100;
} }
// Assigned // Assigned

View file

@ -1,3 +1,5 @@
import { UserSummary } from './user.interface';
export interface Task { export interface Task {
id?: string; id?: string;
title: string; title: string;
@ -9,7 +11,8 @@ export interface Task {
subtasksTitle: string[]; subtasksTitle: string[];
subtasksDone: boolean[]; subtasksDone: boolean[];
assigned: string[]; assigned: string[];
assignees: string[]; assignees: Assignee[];
userData: UserSummary[];
creator: string; creator: string;
date: string; date: string;
} }
@ -19,3 +22,9 @@ export interface Subtask {
title: string; title: string;
done: boolean; done: boolean;
} }
export interface Assignee {
id: string;
user: string;
task: string;
}

View file

@ -10,3 +10,11 @@ export interface User {
status: boolean; status: boolean;
lastLogin: number; lastLogin: number;
} }
export interface UserSummary {
id?: string;
firstName: string;
lastName: string;
initials: string;
color: string;
}

View file

@ -1,8 +1,9 @@
import { Injectable } from '@angular/core'; import { Injectable, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { apiConfig } from '../environments/config'; import { apiConfig } from '../environments/config';
import { Task } from '../interfaces/task.interface'; import { Task } from '../interfaces/task.interface';
import { User } from '../interfaces/user.interface';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@ -14,6 +15,10 @@ export class ApiService {
// ------------- TASKS ------------- // // ------------- TASKS ------------- //
getTaskById(taskId: string): Observable<Task> {
return this.http.get<Task>(`${this.apiUrl}/api/tasks/${taskId}/`);
}
getTasksByStatus(status: string): Observable<Task[]> { getTasksByStatus(status: string): Observable<Task[]> {
return this.http.get<Task[]>(`${this.apiUrl}/api/tasks/`, { return this.http.get<Task[]>(`${this.apiUrl}/api/tasks/`, {
params: { status }, params: { status },
@ -26,4 +31,12 @@ export class ApiService {
{ status: status } { status: status }
); );
} }
// ------------- USERS ------------- //
getUsersByIds(userIds: string[]): Observable<User[]> {
return this.http.get<User[]>(`${this.apiUrl}/api/users/`, {
params: { ids: userIds.join(',') },
});
}
} }

View file

@ -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<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[]>);
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[]>);
return {
allTasks: tasksWithUsers,
filteredTasks: updatedFilteredTasks,
};
})
);
})
);
}
private fetchTasksByStatus(status: string): Observable<Task[]> {
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<Task[]> {
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<Task | null> {
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<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,
};
return acc;
}, {} as Record<string, UserSummary>);
}
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);
}
}

View file

@ -3,16 +3,14 @@
overlay: !overlayMobile, overlay: !overlayMobile,
'overlay-mobile': overlayMobile, 'overlay-mobile': overlayMobile,
}" }"
*ngIf="overlayData !== '' && getTaskData(overlayData).length > 0" *ngIf="overlayData !== '' && task != null"
> >
<div class="header"> <div class="header">
<div <div
class="category" class="category"
[style.background-color]=" [style.background-color]="categoryColors.get(task.category)"
categoryColors.get(getTaskData(overlayData)[0].category)
"
> >
{{ getTaskData(overlayData)[0].category }} {{ task.category }}
</div> </div>
@if (overlayMobile) { @if (overlayMobile) {
<app-btn-back></app-btn-back> <app-btn-back></app-btn-back>
@ -26,126 +24,104 @@
'content-mobile': overlayMobile, 'content-mobile': overlayMobile,
}" }"
> >
<div class="headline">{{ getTaskData(overlayData)[0].title }}</div> <div class="headline">{{ task.title }}</div>
<div class="description"> <div class="description">
{{ getTaskData(overlayData)[0].description }} {{ task.description }}
</div> </div>
<div class="date"> <div class="date">
<p>{{ "addTask.date" | translate }}:</p> <p>{{ "addTask.date" | translate }}:</p>
{{ timeConverter(getTaskData(overlayData)[0].date) }} {{ timeConverter(task.date) }}
</div> </div>
<div class="priority"> <div class="priority">
<p>{{ "addTask.priority" | translate }}:</p> <p>{{ "addTask.priority" | translate }}:</p>
{{ capitalizeFirstLetter(getTaskData(overlayData)[0].priority) }} {{ capitalizeFirstLetter(task.priority) }}
<div <div class="priority-bg priority-{{ task.priority }}"></div>
class="priority-bg priority-{{ getTaskData(overlayData)[0].priority }}"
></div>
</div> </div>
<div class="creator"> <div class="creator">
<p>{{ "addTask.creator" | translate }}:</p> <p>{{ "addTask.creator" | translate }}:</p>
@for (user of task.userData; track user) { @if (task.creator === user.id)
{
<div class="users"> <div class="users">
<div <div
class="circle" class="circle"
[ngStyle]="{ [ngStyle]="{
'background-color': firebaseService.getUserDetails( 'background-color': user.color
getTaskData(overlayData)[0].creator,
'color'
)
}" }"
> >
<div class="initials"> <div class="initials">
{{ {{ user.initials }}
firebaseService.getUserDetails(
getTaskData(overlayData)[0].creator,
"initials"
)
}}
</div> </div>
</div> </div>
<div class="details"> <div class="details">
<div class="name"> <div class="name">
<p> <p>
{{ {{ user.firstName }}
firebaseService.getUserDetails(
getTaskData(overlayData)[0].creator,
"firstName"
)
}}
</p> </p>
<span>&nbsp;</span> <span>&nbsp;</span>
<p class="last-name"> <p class="last-name">
{{ {{ user.lastName }}
firebaseService.getUserDetails(
getTaskData(overlayData)[0].creator,
"lastName"
)
}}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
} }
</div> </div>
@if (getTaskData(overlayData)[0].assigned.length > 0) { @if (task.assignees.length > 0) {
<div class="assigned"> <div class="assigned">
<p>{{ "addTask.assigned" | translate }}:</p> <p>{{ "addTask.assigned" | translate }}:</p>
@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) {
<div class="users"> <div class="users">
<div <div
class="circle" class="circle"
[ngStyle]="{ [ngStyle]="{
'background-color': firebaseService.getUserDetails(user, 'color') 'background-color': user.color
}" }"
> >
<div class="initials"> <div class="initials">
{{ firebaseService.getUserDetails(user, "initials") }} {{ user.initials }}
</div> </div>
</div> </div>
<div class="details"> <div class="details">
<div class="name"> <div class="name">
<p> <p>
{{ firebaseService.getUserDetails(user, "firstName") }} {{ user.firstName }}
</p> </p>
<span>&nbsp;</span> <span>&nbsp;</span>
<p class="last-name"> <p class="last-name">
{{ firebaseService.getUserDetails(user, "lastName") }} {{ user.lastName }}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
} } } }
</div> </div>
} @if (getTaskData(overlayData)[0].subtasksTitle.length > 0) { } @if (task.subtasks.length > 0) {
<div class="subtasks"> <div class="subtasks">
<p>{{ "addTask.subtask" | translate }}:</p> <p>{{ "addTask.subtask" | translate }}:</p>
@for (subtask of getTaskData(overlayData)[0].subtasksTitle; track subtask; @for (subtask of task.subtasks; track subtask; let i = $index) {
let i = $index) {
<div <div
class="single-subtask" class="single-subtask"
(click)=" (click)="
toggleSubtaskStatus( toggleSubtaskStatus(overlayData, i, task.subtasksDone, subtask.done)
overlayData,
i,
getTaskData(overlayData)[0].subtasksDone,
getSubTaskStatus(overlayData, i)
)
" "
> >
<img <img
[src]=" [src]="
getSubTaskStatus(overlayData, i) subtask.done
? './../../../../../assets/img/board/check-btn-checked.svg' ? './../../../../../assets/img/board/check-btn-checked.svg'
: './../../../../../assets/img/board/check-btn-unchecked.svg' : './../../../../../assets/img/board/check-btn-unchecked.svg'
" "
alt="" alt=""
/> />
<p>{{ subtask }}</p> <p>{{ subtask.title }}</p>
</div> </div>
} }
</div> </div>
} }
</div> </div>
@if(getTaskData(overlayData)[0].creator === @if(task.creator === firebaseService.getCurrentUserId()) {
firebaseService.getCurrentUserId()) {
<div class="btns"> <div class="btns">
<div class="btn btn-delete" (click)="deleteTask(overlayData)"> <div class="btn btn-delete" (click)="deleteTask(overlayData)">
<img src="./../../../../../assets/img/contact/delete.svg" alt="" /> <img src="./../../../../../assets/img/contact/delete.svg" alt="" />

View file

@ -6,6 +6,8 @@ import { OverlayService } from '../../../../services/overlay.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { BtnBackComponent } from '../../buttons/btn-back/btn-back.component'; import { BtnBackComponent } from '../../buttons/btn-back/btn-back.component';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { Task } from '../../../../interfaces/task.interface';
import { TaskService } from '../../../../services/task.service';
@Component({ @Component({
selector: 'app-task-overlay', selector: 'app-task-overlay',
@ -18,11 +20,13 @@ export class TaskOverlayComponent implements OnInit {
@Input() overlayData: string = ''; @Input() overlayData: string = '';
@Output() closeDialogEmitter = new EventEmitter<string>(); @Output() closeDialogEmitter = new EventEmitter<string>();
task: Task | null = null;
overlayMobile: boolean = false; overlayMobile: boolean = false;
constructor( constructor(
public firebaseService: FirebaseService, public firebaseService: FirebaseService,
private overlayService: OverlayService, private overlayService: OverlayService,
private taskService: TaskService,
private router: Router, private router: Router,
private route: ActivatedRoute private route: ActivatedRoute
) {} ) {}
@ -40,16 +44,30 @@ export class TaskOverlayComponent implements OnInit {
* Also sets `overlayMobile` to `true` if parameters are successfully retrieved. * Also sets `overlayMobile` to `true` if parameters are successfully retrieved.
*/ */
ngOnInit() { ngOnInit() {
if (this.overlayData == '') { this.setOverlayDataFromRoute();
if (this.route.params.subscribe()) { this.loadTask(this.overlayData);
this.route.params.subscribe((params) => { }
this.overlayData = params['id'];
this.overlayMobile = true; 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. * Close the task overlay.
* *
@ -89,40 +107,6 @@ export class TaskOverlayComponent implements OnInit {
this.closeDialog(); 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. * Toggles the status of a single subtask of a task.
* *