diff --git a/src/app/components/add-task/add-task.component.ts b/src/app/components/add-task/add-task.component.ts index 0016305..ddc3af7 100644 --- a/src/app/components/add-task/add-task.component.ts +++ b/src/app/components/add-task/add-task.component.ts @@ -54,9 +54,11 @@ export class AddTaskComponent implements OnInit { category: '', status: 'todo', priority: 'medium', + subtasks: [], subtasksTitle: [], subtasksDone: [], assigned: [], + assignees: [], creator: this.firebaseService.getCurrentUserId(), date: this.currentDate, }; diff --git a/src/app/components/board/board.component.html b/src/app/components/board/board.component.html index 9e22cc2..e959dbb 100644 --- a/src/app/components/board/board.component.html +++ b/src/app/components/board/board.component.html @@ -9,9 +9,10 @@ type="text" placeholder="{{ 'board.findTask' | translate }}" [(ngModel)]="searchValue" - (input)="searchTask()" + (input)="searchTask(searchValue)" /> + @if (this.searchInput) { clear + + } @else {
{{ "board.todo" | translate }}add
- @for(task of getTaskStatus("todo"); track task) { + + @for (task of filteredTasks[TODO]; track task.id) { + + } @empty { {{ "board.noTasks" | translate }} - } @if(taskMovedTo === "todo" && taskMovedFrom !== "todo") { + } + + + @if (taskMovedTo === TODO && taskMovedFrom !== TODO) { }
{{ "board.inProgress" | translate }}add
- @for(task of getTaskStatus("inprogress"); track task) { + + @for (task of filteredTasks[IN_PROGRESS]; track task.id) { + + } @empty { {{ "board.noTasks" | translate }} - } @if(taskMovedTo === "inprogress" && taskMovedFrom !== "inprogress") - { + } + + + @if (taskMovedTo === IN_PROGRESS && taskMovedFrom !== IN_PROGRESS) { }
{{ "board.awaitFeedback" | translate }}add
- @for(task of getTaskStatus("awaitfeedback"); track task) { - } @empty { + + @for (task of filteredTasks[AWAIT_FEEDBACK]; track task.id) { + + + + } @empty { {{ "board.noTasks" | translate }} - } @if(taskMovedTo === "awaitfeedback" && taskMovedFrom !== - "awaitfeedback") { + } + + + @if (taskMovedTo === AWAIT_FEEDBACK && taskMovedFrom !== + AWAIT_FEEDBACK) { }
{{ "board.done" | translate }}add
- @for(task of getTaskStatus("done"); track task) { + + @for (task of filteredTasks[DONE]; track task.id) { + + } @empty { {{ "board.noTasks" | translate }} - } @if(taskMovedTo === "done" && taskMovedFrom !== "done") { + } + + + @if (taskMovedTo === DONE && taskMovedFrom !== DONE) { }
diff --git a/src/app/components/board/board.component.ts b/src/app/components/board/board.component.ts index 09c9f68..cbbbf5e 100644 --- a/src/app/components/board/board.component.ts +++ b/src/app/components/board/board.component.ts @@ -4,12 +4,14 @@ import { CommonModule } from '@angular/common'; import { TaskComponent } from './task/task.component'; import { TaskEmptyComponent } from './task/task-empty/task-empty.component'; import { FormsModule } from '@angular/forms'; -import { FirebaseService } from '../../services/firebase.service'; import { OverlayService } from '../../services/overlay.service'; import { Router } from '@angular/router'; import { SharedService } from '../../services/shared.service'; 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'; @Component({ selector: 'app-board', @@ -26,47 +28,84 @@ import { TaskHighlightedComponent } from './task/task-highlighted/task-highlight styleUrl: './board.component.scss', }) export class BoardComponent { + readonly TODO = 'todo'; + readonly IN_PROGRESS = 'inprogress'; + readonly AWAIT_FEEDBACK = 'awaitfeedback'; + readonly DONE = 'done'; + constructor( public dragDropService: DragDropService, public overlayService: OverlayService, - private firebaseService: FirebaseService, private sharedService: SharedService, + private apiService: ApiService, private router: Router ) {} + + allTasks: Task[] = []; + filteredTasks: { [key: string]: Task[] } = {}; + searchValue: string = ''; searchInput: boolean = false; taskMovedTo: string = ''; taskMovedFrom: string = ''; - taskDropped: boolean = false; /** - * OnInit lifecycle hook. Subscribes to the following events: - * - * - itemDropped: updates the taskMovedTo and taskMovedFrom properties - * - itemMovedTo: updates the taskMovedTo property - * - itemMovedFrom: updates the taskMovedFrom property - * - * These properties are used to conditionally render a highlighted task - * component when a task is moved. - * - * @returns void + * 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.subscribeToDragDropEvents(); + } + + /** + * 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) + ); + + 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[] }); + }); + } + + /** + * Subscribes to events from the DragDropService and handles them. + * @remarks + * Handles the following events: + * - `itemDropped`: Calls `handleItemDropped` with the task id and status. + * - `itemMovedTo`: Sets `taskMovedTo` to the status. + * - `itemMovedFrom`: Sets `taskMovedFrom` to the status. + */ + private subscribeToDragDropEvents(): void { this.dragDropService.itemDropped.subscribe(({ id, status }) => { this.handleItemDropped(id, status); }); + this.dragDropService.itemMovedTo.subscribe(({ status }) => { this.taskMovedTo = status; }); + this.dragDropService.itemMovedFrom.subscribe(({ status }) => { this.taskMovedFrom = status; }); } /** - * Adds a new task by opening a new route if the user is in page view mode, - * or by setting the overlay data to open a new task overlay in overlay mode. - * @param status The status of the task to be added. + * Opens an overlay or navigates to the add-task page based on the view mode. + * @param status The status to be used for the new task. */ addNewTaskOverlay(status: string) { this.sharedService.isPageViewMedia @@ -75,92 +114,67 @@ export class BoardComponent { } /** - * Retrieves tasks based on their status. - * If a search input is active, it filters the tasks from the filteredTasks list. - * Otherwise, it filters from the complete list of tasks. - * - * @param status The status of the tasks to be retrieved. - * @returns A list of tasks matching the given status. + * Handles the event when an item is dropped on a column. + * @param taskId The id of the task being moved. + * @param status The status of the column where the task was dropped. */ - getTaskStatus(status: string) { - if (this.updateSearchInput()) { - return this.firebaseService - .getFiltertTasks() - .filter((task) => task.status === status); - } else { - return this.firebaseService - .getAllTasks() - .filter((task) => task.status === status); + handleItemDropped(taskId: string, status: string): void { + this.apiService.updateTaskStatus(taskId, status).subscribe({ + next: () => { + this.updateTaskStatus(taskId, status); + }, + error: (error) => console.error('Error updating task:', error), + }); + } + + /** + * Updates the status of a task in the local task lists. + * @param taskId The ID of the task to be updated. + * @param status The new status to assign to the task. + */ + updateTaskStatus(taskId: string, status: string) { + const updatedTask = this.allTasks.find((task) => task.id === taskId); + if (updatedTask) { + const oldStatus = updatedTask.status; + updatedTask.status = status; + + this.filteredTasks[oldStatus] = this.filteredTasks[oldStatus].filter( + (task) => task !== updatedTask + ); + this.filteredTasks[status] = [ + ...(this.filteredTasks[status] || []), + updatedTask, + ]; } } /** - * Updates the status of a task in both the complete and filtered lists - * of tasks when it is dropped. - * - * @param id The id of the task to be updated. - * @param status The new status of the task. - */ - handleItemDropped(id: string, status: string): void { - const index = this.firebaseService.allTasks.findIndex( - (task) => task.id === id - ); - const filteredIndex = this.firebaseService.filteredTasks.findIndex( - (task) => task.id === id - ); - if (index !== -1) { - this.firebaseService.allTasks[index].status = status; - if (filteredIndex !== -1) { - this.firebaseService.filteredTasks[filteredIndex].status = status; - } - this.firebaseService.updateTask(id, index); - } - } - - /** - * Clears the search input by resetting the search input flag and the search value, and - * then calling searchTask() to update the list of tasks. + * Clears the search input by resetting the search input flag and the search value. */ clearInput() { this.searchInput = false; this.searchValue = ''; - this.searchTask(); + this.searchTask(this.searchValue); } /** - * Updates the search input flag and the search value by stripping any XSS - * characters from the search value, and then setting the search input flag - * to true if the search value is not empty, and false otherwise. - * - * @returns The updated search input flag. + * Filters the tasks by the given search term. + * @param searchTerm The search term to filter the tasks by. + * @returns The filtered tasks, with the same structure as the original tasks. */ - updateSearchInput() { - this.searchValue = this.sharedService.replaceXSSChars(this.searchValue); - if (this.searchValue) { - this.searchInput = this.searchValue.toLowerCase().length > 0; - } else { - this.searchInput = false; - } - return this.searchInput; - } + searchTask(searchTerm: string): void { + searchTerm = searchTerm.toLowerCase(); - /** - * Filters the list of tasks based on the current search value. - * - * It first calls updateSearchInput() to update the search input flag and the search value. - * Then it sets the filteredTasks property of the FirebaseService to the result of calling - * filter() on the list of all tasks, where the filter condition is that any of the - * task's title, description, or category includes the search value (case-insensitive). - */ - searchTask(): void { - this.updateSearchInput(); - this.firebaseService.filteredTasks = this.firebaseService - .getAllTasks() - .filter( - (task) => - task.title.toLowerCase().includes(this.searchValue) || - task.description.toLowerCase().includes(this.searchValue) || - task.category.toLowerCase().includes(this.searchValue) - ); + this.filteredTasks = Object.fromEntries( + Object.entries(this.filteredTasks).map(([status]) => [ + status, + this.allTasks.filter( + (task) => + task.status === status && + (task.title.toLowerCase().includes(searchTerm) || + task.description.toLowerCase().includes(searchTerm)) + ), + ]) + ); } } diff --git a/src/app/interfaces/task.interface.ts b/src/app/interfaces/task.interface.ts index ecd396e..e14fd28 100644 --- a/src/app/interfaces/task.interface.ts +++ b/src/app/interfaces/task.interface.ts @@ -5,9 +5,17 @@ export interface Task { category: string; status: string; priority: string; + subtasks: Subtask[]; subtasksTitle: string[]; subtasksDone: boolean[]; assigned: string[]; + assignees: string[]; creator: string; date: string; } + +export interface Subtask { + id: number; + title: string; + done: boolean; +} diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts new file mode 100644 index 0000000..28dd9db --- /dev/null +++ b/src/app/services/api.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { apiConfig } from '../environments/config'; +import { Task } from '../interfaces/task.interface'; + +@Injectable({ + providedIn: 'root', +}) +export class ApiService { + private apiUrl = apiConfig.apiUrl; + + constructor(private http: HttpClient) {} + + // ------------- TASKS ------------- // + + getTasksByStatus(status: string): Observable { + return this.http.get(`${this.apiUrl}/api/tasks/`, { + params: { status }, + }); + } + + updateTaskStatus(taskId: string, status: string): Observable { + return this.http.put( + `${this.apiUrl}/api/tasks/${taskId}/update_status/`, + { status: status } + ); + } +}