feat: add task status constants and labels for task management

This commit is contained in:
Chneemann 2025-04-16 19:36:26 +02:00
parent c02cf4d7a3
commit 2df0ba517f
14 changed files with 121 additions and 172 deletions

View file

@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common';
import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms'; import { FormsModule, NgForm } from '@angular/forms';
import { AssignedComponent } from './assigned/assigned.component'; import { AssignedComponent } from './assigned/assigned.component';
import { Subtask, Task } from '../../interfaces/task.interface'; import { Subtask, Task, TaskStatus } from '../../interfaces/task.interface';
import { OverlayService } from '../../services/overlay.service'; import { OverlayService } from '../../services/overlay.service';
import { FormBtnComponent } from '../../shared/components/buttons/form-btn/form-btn.component'; import { FormBtnComponent } from '../../shared/components/buttons/form-btn/form-btn.component';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
@ -97,7 +97,13 @@ export class AddTaskComponent implements OnInit, OnDestroy {
async loadExistingTaskData() { async loadExistingTaskData() {
if (this.overlayType === 'newTaskOverlay') { if (this.overlayType === 'newTaskOverlay') {
// OverlayData = Status "todo" // OverlayData = Status "todo"
this.taskData.status = this.overlayData; const statusCandidate = this.overlayData as TaskStatus;
if (Object.values(TaskStatus).includes(statusCandidate)) {
this.taskData.status = statusCandidate;
} else {
console.warn('Invalid status in overlayData:', this.overlayData);
}
} else if (this.overlayData) { } else if (this.overlayData) {
// OverlayData = Current TaskId // OverlayData = Current TaskId
const taskData = await firstValueFrom(this.getTaskData(this.overlayData)); const taskData = await firstValueFrom(this.getTaskData(this.overlayData));

View file

@ -12,7 +12,6 @@
(input)="searchTask(searchValue)" (input)="searchTask(searchValue)"
/> />
<span> <span>
<!-- Show clear icon when search input is active -->
@if (this.searchInput) { @if (this.searchInput) {
<img <img
src="./../../../assets/img/board/clear.svg" src="./../../../assets/img/board/clear.svg"
@ -20,18 +19,21 @@
alt="clear" alt="clear"
(click)="clearInput()" (click)="clearInput()"
/> />
<!-- Show search icon when no search input is active -->
} @else { } @else {
<img <img
src="./../../../assets/img/board/search.svg" src="./../../../assets/img/board/search.svg"
class="icon-search" class="icon-search"
alt="search" alt="search"
/>}</span />
> }
</span>
<span class="line"></span> <span class="line"></span>
</div> </div>
<button class="btn" type="submit" (click)="addNewTaskOverlay(TODO)"> <button
class="btn"
type="submit"
(click)="addNewTaskOverlay(STATUSES[0])"
>
<div class="btn-inside"> <div class="btn-inside">
<span>{{ "board.addTask" | translate }}</span> <span>{{ "board.addTask" | translate }}</span>
<img src="./../../../assets/img/board/add_white.svg" alt="check" /> <img src="./../../../assets/img/board/add_white.svg" alt="check" />
@ -39,133 +41,26 @@
</button> </button>
</div> </div>
</div> </div>
<div class="content"> <div class="content">
<div class="status"> <div class="status">
@for (status of STATUSES; track status) {
<div <div
class="column" class="column"
(dragover)="dragDropService.allowDrop($event, TODO)" (dragover)="dragDropService.allowDrop($event, status)"
(drop)="dragDropService.drop($event, TODO)" (drop)="dragDropService.drop($event, status)"
> >
<div class="headline"> <div class="headline">
<span>{{ "board.todo" | translate }}</span <span>{{ STATUS_LABELS[status] | translate }}</span>
><img <img
src="./../../../assets/img/board/plus.svg" src="./../../../assets/img/board/plus.svg"
alt="add" alt="add"
(click)="addNewTaskOverlay(TODO)" (click)="addNewTaskOverlay(status)"
/> />
</div> </div>
<div id="todo" class="details"> <div [id]="status" class="details">
<!-- Loop through 'todo' tasks and display them -->
@for (task of filteredTasks[TODO]; track task.id) {
<app-task
[task]="task"
(updateStatusEmitter)="onStatusUpdate($event)"
></app-task>
<!-- Show empty state if no 'todo' tasks exist -->
} @empty {
<!-- Loading spinner or empty task -->
@if (isLoading) {
<app-loading-spinner></app-loading-spinner>
} @else {
<app-task-empty>{{ "board.noTasks" | translate }}</app-task-empty>
} }
<!-- Highlight task if just moved to 'todo' -->
@if (taskMovedTo === TODO && taskMovedFrom !== TODO) {
<app-task-highlighted></app-task-highlighted>
}
</div>
</div>
<div
class="column"
(dragover)="dragDropService.allowDrop($event, IN_PROGRESS)"
(drop)="dragDropService.drop($event, IN_PROGRESS)"
>
<div class="headline">
<span>{{ "board.inProgress" | translate }}</span
><img
src="./../../../assets/img/board/plus.svg"
alt="add"
(click)="addNewTaskOverlay(IN_PROGRESS)"
/>
</div>
<div id="inprogress" class="details">
<!-- Loop through 'in progress' tasks and display them -->
@for (task of filteredTasks[IN_PROGRESS]; track task.id) {
<app-task
[task]="task"
(updateStatusEmitter)="onStatusUpdate($event)"
></app-task>
<!-- Show empty state if no 'in progress' tasks exist -->
} @empty {
<!-- Loading spinner or empty task -->
@if (isLoading) {
<app-loading-spinner></app-loading-spinner>
} @else {
<app-task-empty>{{ "board.noTasks" | translate }}</app-task-empty>
} }
<!-- Highlight task if just moved to 'in progress' -->
@if (taskMovedTo === IN_PROGRESS && taskMovedFrom !== IN_PROGRESS) {
<app-task-highlighted></app-task-highlighted>
}
</div>
</div>
<div
class="column"
(dragover)="dragDropService.allowDrop($event, AWAIT_FEEDBACK)"
(drop)="dragDropService.drop($event, AWAIT_FEEDBACK)"
>
<div class="headline">
<span>{{ "board.awaitFeedback" | translate }}</span
><img
src="./../../../assets/img/board/plus.svg"
alt="add"
(click)="addNewTaskOverlay(AWAIT_FEEDBACK)"
/>
</div>
<div id="awaitfeedback" class="details">
<!-- Loop through 'await feedback' tasks and display them -->
@for (task of filteredTasks[AWAIT_FEEDBACK]; track task.id) {
<app-task
[task]="task"
(updateStatusEmitter)="onStatusUpdate($event)"
></app-task>
<!-- Show empty state if no 'await feedback' tasks exist -->
} @empty {
<!-- Loading spinner or empty task -->
@if (isLoading) {
<app-loading-spinner></app-loading-spinner>
} @else {
<app-task-empty>{{ "board.noTasks" | translate }}</app-task-empty>
} }
<!-- Highlight task if just moved to 'await feedback' -->
@if (taskMovedTo === AWAIT_FEEDBACK && taskMovedFrom !==
AWAIT_FEEDBACK) {
<app-task-highlighted></app-task-highlighted>
}
</div>
</div>
<div
class="column"
(dragover)="dragDropService.allowDrop($event, DONE)"
(drop)="dragDropService.drop($event, DONE)"
>
<div class="headline">
<span>{{ "board.done" | translate }}</span
><img
src="./../../../assets/img/board/plus.svg"
alt="add"
(click)="addNewTaskOverlay(DONE)"
/>
</div>
<div id="done" class="details">
<!-- Loop through 'done' tasks and display them --> <!-- Loop through 'done' tasks and display them -->
@for (task of filteredTasks[DONE]; track task.id) { @for (task of filteredTasks[status]; track task.id) {
<app-task <app-task
[task]="task" [task]="task"
(updateStatusEmitter)="onStatusUpdate($event)" (updateStatusEmitter)="onStatusUpdate($event)"
@ -173,6 +68,7 @@
<!-- Show empty state if no 'done' tasks exist --> <!-- Show empty state if no 'done' tasks exist -->
} @empty { } @empty {
<!-- Loading spinner or empty task --> <!-- Loading spinner or empty task -->
@if (isLoading) { @if (isLoading) {
<app-loading-spinner></app-loading-spinner> <app-loading-spinner></app-loading-spinner>
@ -181,11 +77,12 @@
} } } }
<!-- Highlight task if just moved to 'done' --> <!-- Highlight task if just moved to 'done' -->
@if (taskMovedTo === DONE && taskMovedFrom !== DONE) { @if (taskMovedTo === status && taskMovedFrom !== status) {
<app-task-highlighted></app-task-highlighted> <app-task-highlighted></app-task-highlighted>
} }
</div> </div>
</div> </div>
}
</div> </div>
<div id="content-tasks"></div> <div id="content-tasks"></div>
</div> </div>

View file

@ -9,7 +9,11 @@ import { Router } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core'; 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, TaskMoveEvent } from '../../interfaces/task.interface'; import {
Task,
TaskMoveEvent,
TaskStatus,
} from '../../interfaces/task.interface';
import { TaskService } from '../../services/task.service'; import { TaskService } from '../../services/task.service';
import { LoadingSpinnerComponent } from '../../shared/components/loading-spinner/loading-spinner.component'; import { LoadingSpinnerComponent } from '../../shared/components/loading-spinner/loading-spinner.component';
import { debounceTime, finalize, Subject, take, takeUntil } from 'rxjs'; import { debounceTime, finalize, Subject, take, takeUntil } from 'rxjs';
@ -17,6 +21,7 @@ import { UpdateNotifierService } from '../../services/update-notifier.service';
import { ToastNotificationService } from '../../services/toast-notification.service'; import { ToastNotificationService } from '../../services/toast-notification.service';
import { ResizeService } from '../../services/resize.service'; import { ResizeService } from '../../services/resize.service';
import { HeadlineComponent } from '../../shared/components/headline/headline.component'; import { HeadlineComponent } from '../../shared/components/headline/headline.component';
import { STATUS_LABELS, STATUSES } from '../../constants/task-status.constants';
@Component({ @Component({
selector: 'app-board', selector: 'app-board',
@ -35,10 +40,8 @@ import { HeadlineComponent } from '../../shared/components/headline/headline.com
styleUrl: './board.component.scss', styleUrl: './board.component.scss',
}) })
export class BoardComponent implements OnInit, OnDestroy { export class BoardComponent implements OnInit, OnDestroy {
readonly TODO = 'todo'; readonly STATUSES = STATUSES;
readonly IN_PROGRESS = 'inprogress'; readonly STATUS_LABELS = STATUS_LABELS;
readonly AWAIT_FEEDBACK = 'awaitfeedback';
readonly DONE = 'done';
allTasks: Array<Task> = []; allTasks: Array<Task> = [];
filteredTasks: { [key: string]: Task[] } = {}; filteredTasks: { [key: string]: Task[] } = {};
@ -166,7 +169,7 @@ export class BoardComponent implements OnInit, OnDestroy {
* @param taskId The id of the task being moved. * @param taskId The id of the task being moved.
* @param status The status of the column where the task was dropped. * @param status The status of the column where the task was dropped.
*/ */
handleItemDropped(task: Task, status: string): void { handleItemDropped(task: Task, status: TaskStatus): void {
if (!task || task.status === status) return; if (!task || task.status === status) return;
this.apiService this.apiService
@ -186,7 +189,7 @@ export class BoardComponent implements OnInit, OnDestroy {
* @param taskId The ID of the task to be updated. * @param taskId The ID of the task to be updated.
* @param status The new status to assign to the task. * @param status The new status to assign to the task.
*/ */
updateTaskStatus(taskId: string, status: string) { updateTaskStatus(taskId: string, status: TaskStatus) {
const updatedTask = this.allTasks.find((task) => task.id === taskId); const updatedTask = this.allTasks.find((task) => task.id === taskId);
if (updatedTask) { if (updatedTask) {
const oldStatus = updatedTask.status; const oldStatus = updatedTask.status;

View file

@ -1,11 +1,11 @@
<section> <section>
@if (boardTaskStatus !== TODO) { <!-- Loop through statuses -->
<div class="link" (click)="moveTask(TODO)">To do</div> @for (status of STATUSES; track status) {
} @if (boardTaskStatus !== IN_PROGRESS) {
<div class="link" (click)="moveTask(IN_PROGRESS)">In progress</div> <!-- If the status is not the current task status, display a link to move the task to that status -->
} @if (boardTaskStatus !== AWAIT_FEEDBACK) { @if (status !== currentTaskStatus) {
<div class="link" (click)="moveTask(AWAIT_FEEDBACK)">Await feedback</div> <div class="link" (click)="moveTask(status)">
} @if (boardTaskStatus !== DONE) { {{ STATUS_LABELS[status] | translate }}
<div class="link" (click)="moveTask(DONE)">Done</div> </div>
} } }
</section> </section>

View file

@ -1,27 +1,31 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Task, TaskMoveEvent } from '../../../../interfaces/task.interface'; import {
Task,
TaskMoveEvent,
TaskStatus,
} from '../../../../interfaces/task.interface';
import {
STATUS_LABELS,
STATUSES,
} from '../../../../constants/task-status.constants';
import { TranslateModule } from '@ngx-translate/core';
@Component({ @Component({
selector: 'app-task-menu', selector: 'app-task-menu',
standalone: true, standalone: true,
imports: [], imports: [TranslateModule],
templateUrl: './task-menu.component.html', templateUrl: './task-menu.component.html',
styleUrl: './task-menu.component.scss', styleUrl: './task-menu.component.scss',
}) })
export class TaskMenuComponent { export class TaskMenuComponent {
@Input() task!: Task; @Input() task!: Task;
@Input() boardTaskStatus!: string; @Input() currentTaskStatus!: TaskStatus;
@Output() updateStatusEmitter = new EventEmitter<TaskMoveEvent>(); @Output() updateStatusEmitter = new EventEmitter<TaskMoveEvent>();
readonly TODO = 'todo'; readonly STATUSES = STATUSES;
readonly IN_PROGRESS = 'inprogress'; readonly STATUS_LABELS = STATUS_LABELS;
readonly AWAIT_FEEDBACK = 'awaitfeedback';
readonly DONE = 'done';
moveTask(moveTo: string) { moveTask(moveTo: TaskStatus) {
this.updateStatusEmitter.emit({ this.updateStatusEmitter.emit({ task: this.task, moveTo });
task: this.task,
moveTo: moveTo,
});
} }
} }

View file

@ -91,7 +91,7 @@
@if (mobileMenuOpen){ @if (mobileMenuOpen){
<app-task-menu <app-task-menu
[task]="task" [task]="task"
[boardTaskStatus]="task.status" [currentTaskStatus]="task.status"
(updateStatusEmitter)="onStatusUpdate($event)" (updateStatusEmitter)="onStatusUpdate($event)"
></app-task-menu> ></app-task-menu>
} }

View file

@ -0,0 +1,15 @@
import { TaskStatus } from '../interfaces/task.interface';
export const STATUSES: TaskStatus[] = [
TaskStatus.TODO,
TaskStatus.IN_PROGRESS,
TaskStatus.AWAIT_FEEDBACK,
TaskStatus.DONE,
];
export const STATUS_LABELS: Record<TaskStatus, string> = {
[TaskStatus.TODO]: 'taskStatus.todo',
[TaskStatus.IN_PROGRESS]: 'taskStatus.inProgress',
[TaskStatus.AWAIT_FEEDBACK]: 'taskStatus.awaitFeedback',
[TaskStatus.DONE]: 'taskStatus.done',
};

View file

@ -1,11 +1,22 @@
import { UserSummary } from './user.interface'; import { UserSummary } from './user.interface';
// Enums
export enum TaskStatus {
TODO = 'todo',
IN_PROGRESS = 'inProgress',
AWAIT_FEEDBACK = 'awaitFeedback',
DONE = 'done',
}
// Interfaces
export interface Task { export interface Task {
id?: string; id?: string;
title: string; title: string;
description: string; description: string;
category: string; category: string;
status: string; status: TaskStatus;
priority: string; priority: string;
subtasks: Subtask[]; subtasks: Subtask[];
assignees: Assignee[]; assignees: Assignee[];
@ -27,5 +38,5 @@ export interface Assignee {
export interface TaskMoveEvent { export interface TaskMoveEvent {
task: Task; task: Task;
moveTo: string; moveTo: TaskStatus;
} }

View file

@ -1,11 +1,11 @@
import { Injectable, EventEmitter } from '@angular/core'; import { Injectable, EventEmitter } from '@angular/core';
import { Task } from '../interfaces/task.interface'; import { Task, TaskStatus } from '../interfaces/task.interface';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class DragDropService { export class DragDropService {
itemDropped = new EventEmitter<{ task: Task; status: string }>(); itemDropped = new EventEmitter<{ task: Task; status: TaskStatus }>();
itemMovedFrom = new EventEmitter<{ status: string }>(); itemMovedFrom = new EventEmitter<{ status: string }>();
itemMovedTo = new EventEmitter<{ status: string }>(); itemMovedTo = new EventEmitter<{ status: string }>();
@ -37,7 +37,7 @@ export class DragDropService {
* - Prevents the default browser behavior. * - Prevents the default browser behavior.
* - Emits the `itemMovedTo` event with the status of the column. * - Emits the `itemMovedTo` event with the status of the column.
*/ */
allowDrop(event: DragEvent, status: string) { allowDrop(event: DragEvent, status: TaskStatus) {
event.preventDefault(); event.preventDefault();
this.itemMovedTo.emit({ status }); this.itemMovedTo.emit({ status });
} }
@ -53,7 +53,7 @@ export class DragDropService {
* - Sets the dragged task to null. * - Sets the dragged task to null.
* - Emits the `itemMovedTo` event with an empty status, to reset the highlight. * - Emits the `itemMovedTo` event with an empty status, to reset the highlight.
*/ */
drop(event: DragEvent, newStatus: string) { drop(event: DragEvent, newStatus: TaskStatus) {
event.preventDefault(); event.preventDefault();
if (this.draggedTask) { if (this.draggedTask) {

View file

@ -1,5 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { TaskState } from 'zone.js/lib/zone-impl';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@ -16,7 +17,7 @@ export class OverlayService {
* @param overlay The overlay type. * @param overlay The overlay type.
* @param data The overlay data. * @param data The overlay data.
*/ */
setOverlayData(overlay: string, data: any) { setOverlayData(overlay: string | TaskState, data: any) {
this.overlayDataSubject.next({ overlay, data }); this.overlayDataSubject.next({ overlay, data });
} }

View file

@ -1,18 +1,23 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { ApiService } from './api.service'; import { ApiService } from './api.service';
import { Task } from '../interfaces/task.interface'; import { Task, TaskStatus } from '../interfaces/task.interface';
import { catchError, map, switchMap } from 'rxjs/operators'; import { catchError, map, switchMap } from 'rxjs/operators';
import { UserSummary } from '../interfaces/user.interface'; import { UserSummary } from '../interfaces/user.interface';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class TaskService { export class TaskService {
private readonly statuses = ['todo', 'inprogress', 'awaitfeedback', 'done']; private readonly statuses: TaskStatus[] = [
TaskStatus.TODO,
TaskStatus.IN_PROGRESS,
TaskStatus.AWAIT_FEEDBACK,
TaskStatus.DONE,
];
private readonly priorities = ['low', 'medium', 'high']; private readonly priorities = ['low', 'medium', 'high'];
constructor(private apiService: ApiService) {} constructor(private apiService: ApiService) {}
getStatuses(): string[] { getStatuses(): TaskStatus[] {
return this.statuses; return this.statuses;
} }

View file

@ -103,6 +103,9 @@ export class OverlayComponent implements OnInit, OnDestroy {
* @param {boolean} close Indicates the close event. * @param {boolean} close Indicates the close event.
*/ */
onCloseOverlay() { onCloseOverlay() {
this.overlayType = null;
this.overlayData = null;
this.shouldShowOverlay = false;
this.overlayService.clearOverlayData(); this.overlayService.clearOverlayData();
} }

View file

@ -117,16 +117,18 @@
}, },
"board": { "board": {
"headline": "Tafel", "headline": "Tafel",
"todo": "Zu Erledigen",
"inProgress": "In Bearbeitung",
"awaitFeedback": "Warte auf Rückmeldung",
"done": "Erledigt",
"addTask": "Neue Aufgabe", "addTask": "Neue Aufgabe",
"findTask": "Aufgabe suchen", "findTask": "Aufgabe suchen",
"noTasks": "Keine Aufgaben", "noTasks": "Keine Aufgaben",
"dropHere": "Hier ablegen", "dropHere": "Hier ablegen",
"emptyTask": "Keine Aufgaben" "emptyTask": "Keine Aufgaben"
}, },
"taskStatus": {
"todo": "Zu Erledigen",
"inProgress": "In Bearbeitung",
"awaitFeedback": "Warte auf Rückmeldung",
"done": "Erledigt"
},
"addTask": { "addTask": {
"headline": "Aufgabe", "headline": "Aufgabe",
"title": "Titel", "title": "Titel",

View file

@ -117,16 +117,18 @@
}, },
"board": { "board": {
"headline": "Board", "headline": "Board",
"todo": "To-do",
"inProgress": "In progress",
"awaitFeedback": "Await feedback",
"done": "Done",
"addTask": "Add Task", "addTask": "Add Task",
"findTask": "Find Task", "findTask": "Find Task",
"noTasks": "No tasks", "noTasks": "No tasks",
"dropHere": "Drop here", "dropHere": "Drop here",
"emptyTask": "No tasks" "emptyTask": "No tasks"
}, },
"taskStatus": {
"todo": "To-do",
"inProgress": "In progress",
"awaitFeedback": "Await feedback",
"done": "Done"
},
"addTask": { "addTask": {
"headline": "Add Task", "headline": "Add Task",
"title": "Title", "title": "Title",