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 { FormsModule, NgForm } from '@angular/forms';
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 { FormBtnComponent } from '../../shared/components/buttons/form-btn/form-btn.component';
import { ActivatedRoute, Router } from '@angular/router';
@ -97,7 +97,13 @@ export class AddTaskComponent implements OnInit, OnDestroy {
async loadExistingTaskData() {
if (this.overlayType === 'newTaskOverlay') {
// 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) {
// OverlayData = Current TaskId
const taskData = await firstValueFrom(this.getTaskData(this.overlayData));

View file

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

View file

@ -9,7 +9,11 @@ import { Router } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { TaskHighlightedComponent } from './task/task-highlighted/task-highlighted.component';
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 { LoadingSpinnerComponent } from '../../shared/components/loading-spinner/loading-spinner.component';
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 { ResizeService } from '../../services/resize.service';
import { HeadlineComponent } from '../../shared/components/headline/headline.component';
import { STATUS_LABELS, STATUSES } from '../../constants/task-status.constants';
@Component({
selector: 'app-board',
@ -35,10 +40,8 @@ import { HeadlineComponent } from '../../shared/components/headline/headline.com
styleUrl: './board.component.scss',
})
export class BoardComponent implements OnInit, OnDestroy {
readonly TODO = 'todo';
readonly IN_PROGRESS = 'inprogress';
readonly AWAIT_FEEDBACK = 'awaitfeedback';
readonly DONE = 'done';
readonly STATUSES = STATUSES;
readonly STATUS_LABELS = STATUS_LABELS;
allTasks: Array<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 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;
this.apiService
@ -186,7 +189,7 @@ export class BoardComponent implements OnInit, OnDestroy {
* @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) {
updateTaskStatus(taskId: string, status: TaskStatus) {
const updatedTask = this.allTasks.find((task) => task.id === taskId);
if (updatedTask) {
const oldStatus = updatedTask.status;

View file

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

View file

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

View file

@ -91,7 +91,7 @@
@if (mobileMenuOpen){
<app-task-menu
[task]="task"
[boardTaskStatus]="task.status"
[currentTaskStatus]="task.status"
(updateStatusEmitter)="onStatusUpdate($event)"
></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';
// Enums
export enum TaskStatus {
TODO = 'todo',
IN_PROGRESS = 'inProgress',
AWAIT_FEEDBACK = 'awaitFeedback',
DONE = 'done',
}
// Interfaces
export interface Task {
id?: string;
title: string;
description: string;
category: string;
status: string;
status: TaskStatus;
priority: string;
subtasks: Subtask[];
assignees: Assignee[];
@ -27,5 +38,5 @@ export interface Assignee {
export interface TaskMoveEvent {
task: Task;
moveTo: string;
moveTo: TaskStatus;
}

View file

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

View file

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

View file

@ -1,18 +1,23 @@
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
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 { UserSummary } from '../interfaces/user.interface';
@Injectable({ providedIn: 'root' })
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'];
constructor(private apiService: ApiService) {}
getStatuses(): string[] {
getStatuses(): TaskStatus[] {
return this.statuses;
}

View file

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

View file

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

View file

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