feat: add task status constants and labels for task management
This commit is contained in:
parent
c02cf4d7a3
commit
2df0ba517f
14 changed files with 121 additions and 172 deletions
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@
|
|||
@if (mobileMenuOpen){
|
||||
<app-task-menu
|
||||
[task]="task"
|
||||
[boardTaskStatus]="task.status"
|
||||
[currentTaskStatus]="task.status"
|
||||
(updateStatusEmitter)="onStatusUpdate($event)"
|
||||
></app-task-menu>
|
||||
}
|
||||
|
|
|
|||
15
src/app/constants/task-status.constants.ts
Normal file
15
src/app/constants/task-status.constants.ts
Normal 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',
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue