refactor: extracted assigned logic from AddTaskComponent to AssignedComponent and migrated to Django Rest

This commit is contained in:
Chneemann 2025-04-01 18:23:18 +02:00
parent 3293def254
commit ff5abfa41a
10 changed files with 500 additions and 415 deletions

View file

@ -5,14 +5,14 @@
}" }"
> >
<div <div
class="headline" class="header"
[ngStyle]="{ display: overlayData !== '' ? 'none' : 'block' }" [ngStyle]="{ display: overlayData !== '' ? 'none' : 'block' }"
> >
<div class="title">{{ "addTask.headline" | translate }}</div> <div class="title">{{ "addTask.headline" | translate }}</div>
</div> </div>
<form <form
(ngSubmit)="onSubmit(taskForm, overlayData)" (ngSubmit)="onSubmit(taskForm)"
(keydown.enter)="handleEnterKey($event)" (keydown.enter)="handleEnterKey($event)"
#taskForm="ngForm" #taskForm="ngForm"
onsubmit="return false" onsubmit="return false"
@ -26,7 +26,9 @@
> >
<div class="left-side"> <div class="left-side">
<div class="title"> <div class="title">
<p>{{ "addTask.title" | translate }}<span class="red-dot">*</span></p> <div class="headline">
{{ "addTask.title" | translate }}<span class="red-dot">*</span>
</div>
<input <input
type="text" type="text"
id="title" id="title"
@ -49,10 +51,10 @@
</div> </div>
</div> </div>
<div class="description"> <div class="description">
<p> <div class="headline">
{{ "addTask.description" | translate {{ "addTask.description" | translate
}}<span class="red-dot">*</span> }}<span class="red-dot">*</span>
</p> </div>
<textarea <textarea
id="description" id="description"
rows="5" rows="5"
@ -75,62 +77,11 @@
} }
</div> </div>
</div> </div>
<div class="assigned"> <app-assigned
<p>{{ "addTask.assigned" | translate }}</p> [taskCreator]="taskData.creator"
<input [assignedList]="taskData.assigned"
#searchField (assignedChange)="receiveAssigned($event)"
id="search-assigned" ></app-assigned>
class="search-assigned"
name="search"
placeholder="{{ 'addTask.assignedFormField' | translate }}"
type="text"
[(ngModel)]="searchValue"
(input)="searchTask(taskData.creator)"
autocomplete="off"
(click)="toggleAssignedMenu()"
/>
@if (isAssignedOpen) {
<app-assigned
[filteredUsers]="filteredUsers"
[taskCreator]="taskData.creator"
[searchInput]="searchInput"
(assignedChange)="receiveAssigned($event)"
>
></app-assigned
>
<img class="close" src="./../../../assets/img/close.svg" alt="" />
} @else {
<img
class="open"
src="./../../../assets/img/add-task/arrow-down.svg"
alt=""
/>
}
</div>
<div class="assigned-badge">
@for (user of taskData.assigned; track user ) {
<div
class="circle"
(mousemove)="openDialog(user, $event)"
(mouseleave)="closeDialog()"
[ngStyle]="{
'background-color': firebaseService.getUserDetails(user, 'color')
}"
>
<div class="initials">
{{ firebaseService.getUserDetails(user, "initials") }}
</div>
</div>
}
</div>
@if (AssignedDialogId != '') {
<div class="dialog" [style.left.px]="dialogX" [style.top.px]="dialogY">
<p>
{{ firebaseService.getUserDetails(AssignedDialogId, "firstName") }}
{{ firebaseService.getUserDetails(AssignedDialogId, "lastName") }}
</p>
</div>
}
</div> </div>
<div class="middle-spacer"><div class="line"></div></div> <div class="middle-spacer"><div class="line"></div></div>
<div class="right-side"> <div class="right-side">
@ -140,7 +91,9 @@
'margin-top': overlayMobile ? '28px' : '' 'margin-top': overlayMobile ? '28px' : ''
}" }"
> >
<p>{{ "addTask.date" | translate }}<span class="red-dot">*</span></p> <div class="headline">
{{ "addTask.date" | translate }}<span class="red-dot">*</span>
</div>
<input <input
type="date" type="date"
id="date" id="date"
@ -161,7 +114,7 @@
</div> </div>
</div> </div>
<div class="priority"> <div class="priority">
<p>{{ "addTask.priority" | translate }}</p> <div class="headline">{{ "addTask.priority" | translate }}</div>
<div class="btns"> <div class="btns">
<button <button
type="button" type="button"
@ -217,9 +170,9 @@
</div> </div>
</div> </div>
<div class="category"> <div class="category">
<p> <div class="headline">
{{ "addTask.category" | translate }}<span class="red-dot">*</span> {{ "addTask.category" | translate }}<span class="red-dot">*</span>
</p> </div>
<select <select
id="category" id="category"
name="category" name="category"
@ -246,7 +199,7 @@
</div> </div>
</div> </div>
<div class="subtask"> <div class="subtask">
<p>{{ "addTask.subtask" | translate }}</p> <div class="headline">{{ "addTask.subtask" | translate }}</div>
<input <input
type="text" type="text"
id="subtask" id="subtask"

View file

@ -8,21 +8,6 @@ section {
width: 100%; width: 100%;
} }
.dialog {
position: fixed;
width: min-content;
height: fit-content;
padding: 10px 15px;
border-radius: 0px 20px 20px 20px;
border: 1px solid var(--black);
z-index: 2;
background-color: var(--white);
p {
font-size: 18px;
font-weight: 400;
}
}
.edit-task-overlay-content { .edit-task-overlay-content {
height: calc(700px - 148px); height: calc(700px - 148px);
padding: 0 6px; padding: 0 6px;
@ -36,6 +21,12 @@ section {
background-color: var(--very-light-gray); background-color: var(--very-light-gray);
} }
.headline {
font-size: 20px;
font-weight: 400;
padding-bottom: 6px;
}
input, input,
textarea, textarea,
select { select {
@ -65,12 +56,6 @@ select:valid {
border: 1px solid black; border: 1px solid black;
} }
p {
font-size: 20px;
font-weight: 400;
padding-bottom: 6px;
}
.red-dot { .red-dot {
color: var(--light-red); color: var(--light-red);
} }
@ -91,7 +76,7 @@ p {
/*------------- HEADER -------------*/ /*------------- HEADER -------------*/
.headline { .header {
margin-bottom: 48px; margin-bottom: 48px;
.title { .title {
font-size: 61px; font-size: 61px;
@ -105,55 +90,6 @@ p {
width: 45%; width: 45%;
} }
.assigned {
position: relative;
.open {
position: absolute;
top: 55px;
right: 20px;
width: 14px;
height: 7px;
}
.close {
position: absolute;
top: 50px;
right: 20px;
width: 16px;
height: 16px;
cursor: pointer;
&:hover {
filter: invert(60%) sepia(58%) saturate(1629%) hue-rotate(165deg)
brightness(92%) contrast(92%);
}
}
}
.assigned-badge {
display: flex;
align-items: center;
margin: 6px;
height: 40px;
}
.circle {
display: flex;
justify-content: center;
align-items: center;
width: 30px;
min-width: 30px;
height: 30px;
border-radius: 100%;
border: 2px solid var(--white);
margin-right: 6px;
cursor: default;
.initials {
font-size: 12px;
font-weight: 400;
color: var(--white);
text-shadow: 1px 1px 2px var(--black);
}
}
.subtask { .subtask {
position: relative; position: relative;
.add { .add {

View file

@ -2,7 +2,6 @@ import { CommonModule } from '@angular/common';
import { Component, HostListener, Input, OnInit } from '@angular/core'; import { Component, HostListener, Input, 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 { User } from '../../interfaces/user.interface';
import { FirebaseService } from '../../services/firebase.service'; import { FirebaseService } from '../../services/firebase.service';
import { Task } from '../../interfaces/task.interface'; import { Task } from '../../interfaces/task.interface';
import { OverlayService } from '../../services/overlay.service'; import { OverlayService } from '../../services/overlay.service';
@ -36,14 +35,7 @@ export class AddTaskComponent implements OnInit {
currentDate: string = new Date().toISOString().split('T')[0]; currentDate: string = new Date().toISOString().split('T')[0];
dateInPast: boolean = false; dateInPast: boolean = false;
isAssignedOpen: boolean = false;
subtaskValue: string = ''; subtaskValue: string = '';
searchValue: string = '';
searchInput: boolean = false;
filteredUsers: User[] = [];
AssignedDialogId: string = '';
dialogX: number = 0;
dialogY: number = 0;
constructor( constructor(
public firebaseService: FirebaseService, public firebaseService: FirebaseService,
@ -142,32 +134,6 @@ export class AddTaskComponent implements OnInit {
} }
} }
/**
* Opens the user dialog for the given user id at the position of the mouse click
* @param userId the id of the user
* @param event the MouseEvent that triggered the dialog
*/
openDialog(userId: any, event: MouseEvent) {
this.AssignedDialogId = userId;
this.updateDialogPosition(event);
}
/**
* Updates the position of the user dialog based on the MouseEvent
* @param event the MouseEvent that triggered the dialog
*/
updateDialogPosition(event: MouseEvent) {
this.dialogX = event.clientX + 25;
this.dialogY = event.clientY + 10;
}
/**
* Closes the assigned user dialog
*/
closeDialog() {
this.AssignedDialogId = '';
}
/** /**
* Gets the task data from the Firebase service for the given task id. * Gets the task data from the Firebase service for the given task id.
* @param taskId the id of the task * @param taskId the id of the task
@ -200,40 +166,6 @@ export class AddTaskComponent implements OnInit {
} }
} }
/**
* Updates the filteredUsers array with users that match the current search value
* from the getFilteredUsers list.
* @param taskCreator the id of the task creator
*/
searchTask(taskCreator: string): void {
this.updateSearchInput();
this.filteredUsers = this.firebaseService
.getFilteredUsers(taskCreator)
.filter(
(user) =>
user.firstName.toLowerCase().includes(this.searchValue) ||
user.lastName.toLowerCase().includes(this.searchValue) ||
user.initials.toLowerCase().includes(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.
*/
updateSearchInput() {
this.searchValue = this.replaceXSSChars(this.searchValue);
if (this.searchValue) {
this.searchInput = this.searchValue.toLowerCase().length > 0;
} else {
this.searchInput = false;
}
return this.searchInput;
}
/** /**
* Updates the subtask value by removing any potential XSS characters. * Updates the subtask value by removing any potential XSS characters.
* This ensures that the subtask value is sanitized before further processing or storage. * This ensures that the subtask value is sanitized before further processing or storage.
@ -251,15 +183,6 @@ export class AddTaskComponent implements OnInit {
this.taskData.assigned = assigned; this.taskData.assigned = assigned;
} }
/**
* Toggle the assigned menu for the current task.
* This function toggles the boolean flag isAssignedOpen, which determines whether the assigned menu is open or not.
* @returns {void}
*/
toggleAssignedMenu() {
this.isAssignedOpen = !this.isAssignedOpen;
}
/** /**
* Checks if the selected date is in the past. * Checks if the selected date is in the past.
* *
@ -373,7 +296,7 @@ export class AddTaskComponent implements OnInit {
} }
} }
onSubmit(ngForm: NgForm, overlayData: string) { onSubmit(ngForm: NgForm) {
if (ngForm.submitted && ngForm.form.valid) { if (ngForm.submitted && ngForm.form.valid) {
const { id, ...taskWithoutId } = this.taskData; const { id, ...taskWithoutId } = this.taskData;
@ -412,28 +335,9 @@ export class AddTaskComponent implements OnInit {
*/ */
deleteTask(overlayData: string) { deleteTask(overlayData: string) {
this.apiService.deleteTaskById(overlayData); this.apiService.deleteTaskById(overlayData);
this.closeDialog();
} }
replaceXSSChars(input: string) { replaceXSSChars(input: string) {
return input.replace(/</g, '&lt;').replace(/>/g, '&gt;'); return input.replace(/</g, '&lt;').replace(/>/g, '&gt;');
} }
@HostListener('document:click', ['$event'])
/**
* Closes the assigned dropdown menu if the user clicks anywhere outside of
* the assigned dropdown menu.
* @param event the MouseEvent
* @returns {void}
*/
checkOpenNavbar(event: MouseEvent) {
const targetElement = event.target as HTMLElement;
if (
!targetElement.closest('.search-assigned') &&
!targetElement.closest('app-assigned') &&
!targetElement.closest('.checkbox-img')
) {
this.isAssignedOpen = false;
}
}
} }

View file

@ -0,0 +1,49 @@
<section>
@if (filteredUsers.length === 0) {
<div class="content"><p class="no-users">No users found</p></div>
} @else { @for (user of filteredUsers; track user.id) {
<div
class="content"
(click)="addAssignedToTask(user.id)"
[ngClass]="{ selected: assigned.includes(user.id) }"
*ngIf="user.id"
>
<div
class="circle"
[ngStyle]="{
'background-color': user.color
}"
>
<div class="initials">
{{ user.initials }}
</div>
</div>
<div class="details">
<div class="name">
<p>
{{ user.firstName }}
</p>
<span>,&nbsp;</span>
<p class="last-name">
{{ user.lastName }}
</p>
</div>
</div>
<div class="checkbox">
@if (assigned.includes(user.id)) {
<img
class="checkbox-img"
src="./../../../../assets/img/add-task/checkbox-checked.svg"
alt=""
/>
} @else {
<img
class="checkbox-img"
src="./../../../../assets/img/add-task/checkbox-empty.svg"
alt=""
/>
}
</div>
</div>
} }
</section>

View file

@ -0,0 +1,89 @@
section {
position: absolute;
top: 75px;
left: 0;
right: 0;
width: 100%;
height: auto;
max-height: 125px;
padding-top: 12px;
border-radius: 0px 0px 20px 20px;
border: 1px solid var(--light-gray);
border-top: 0px;
background-color: var(--white);
overflow-y: auto;
}
.content {
display: flex;
align-items: center;
padding: 6px 12px;
cursor: pointer;
&:hover {
background-color: var(--light-gray);
}
}
.circle {
display: flex;
justify-content: center;
align-items: center;
width: 35px;
min-width: 35px;
height: 35px;
border-radius: 100%;
border: 2px solid var(--white);
.initials {
font-size: 12px;
font-weight: 400;
color: var(--white);
text-shadow: 1px 1px 2px var(--black);
}
}
.no-users {
font-size: 18px;
font-weight: 400;
}
.details {
margin-left: 24px;
width: 80%;
overflow: hidden;
white-space: nowrap;
display: inline-block;
}
.name {
display: flex;
align-items: center;
max-width: 100%;
overflow: hidden;
white-space: nowrap;
font-size: 18px;
font-weight: 400;
}
.last-name {
overflow: hidden;
text-overflow: ellipsis;
}
.checkbox {
img {
width: 18px;
height: 18px;
}
}
.selected {
background-color: var(--dark-blue);
color: var(--white);
img {
filter: brightness(0) saturate(100%) invert(100%) sepia(0%) saturate(7500%)
hue-rotate(346deg) brightness(99%) contrast(103%);
}
&:hover {
background-color: var(--very-dark-blue);
}
}

View file

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AssignedListComponent } from './assigned-list.component';
describe('AssignedListComponent', () => {
let component: AssignedListComponent;
let fixture: ComponentFixture<AssignedListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AssignedListComponent]
})
.compileComponents();
fixture = TestBed.createComponent(AssignedListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,88 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FirebaseService } from '../../../../services/firebase.service';
import { CommonModule } from '@angular/common';
import { User } from '../../../../interfaces/user.interface';
@Component({
selector: 'app-assigned-list',
standalone: true,
imports: [CommonModule],
templateUrl: './assigned-list.component.html',
styleUrl: './assigned-list.component.scss',
})
export class AssignedListComponent {
@Input() filteredUsers: User[] = [];
@Input() searchInput: boolean = false;
@Input() taskCreator: string = '';
@Output() assignedChange = new EventEmitter<string[]>();
users: User[] = [];
assigned: string[] = [];
currentUser: User | null = null;
constructor(public firebaseService: FirebaseService) {}
ngOnInit() {
this.loadTaskAssignedData();
}
/**
* Updates the assignedChange event emitter with the current list of assigned users.
* This should be called whenever the assigned list is updated.
*/
updateAssigned() {
this.assignedChange.emit(this.assigned);
}
/**
* Toggles the assignment of a user to the task.
*
* If the user is not currently assigned, they are added to the assigned list.
* If the user is already assigned, they are removed from the list.
* Updates the local storage with the current list of assigned users and emits the
* assignedChange event to notify other components of the update.
*
* @param userId The ID of the user to be added or removed from the assigned list.
*/
addAssignedToTask(userId: string) {
if (!this.assigned.includes(userId)) {
this.assigned.push(userId);
} else {
this.assigned.splice(this.assigned.indexOf(userId), 1);
}
this.saveTaskData();
this.updateAssigned();
}
/**
* Saves the current list of assigned users to local storage.
*
* Retrieves the current task data from local storage, updates the assigned
* list with the current list of assigned users, and saves the updated task
* data back to local storage.
*/
saveTaskData() {
let taskDataString = localStorage.getItem('taskData');
if (taskDataString !== null) {
let taskData = JSON.parse(taskDataString);
taskData.assigned = this.assigned;
localStorage.setItem('taskData', JSON.stringify(taskData));
}
}
/**
* Loads the list of assigned users from local storage.
*
* Retrieves the task data from local storage, checks if the assigned list exists,
* and if so, assigns the list to the local assigned variable.
*/
loadTaskAssignedData() {
const taskDataString = localStorage.getItem('taskData');
if (taskDataString !== null) {
const taskData = JSON.parse(taskDataString);
if (taskData.hasOwnProperty('assigned')) {
this.assigned = taskData.assigned;
}
}
}
}

View file

@ -1,47 +1,65 @@
<section> <div class="assigned">
@for (user of displayAssigned(); track user; let index = $index) { <div class="headline">{{ "addTask.assigned" | translate }}</div>
<div <input
class="content" #searchField
(click)="addAssignedToTask(user.id)" id="search-assigned"
[ngClass]="{ selected: assigned.includes(user.id) }" class="search-assigned"
*ngIf="user.id" name="search"
placeholder="{{ 'addTask.assignedFormField' | translate }}"
type="text"
[(ngModel)]="searchValue"
(input)="searchTask()"
autocomplete="off"
(click)="toggleAssignedMenu()"
/>
@if (showAssignedList) {
<app-assigned-list
[filteredUsers]="filteredUsers"
[taskCreator]="taskCreator"
[searchInput]="searchInput"
(assignedChange)="receiveAssigned($event)"
> >
></app-assigned-list
>
<img class="close" src="./../../../assets/img/close.svg" alt="" />
} @else {
<img
class="open"
src="./../../../assets/img/add-task/arrow-down.svg"
alt=""
/>
}
<div class="assigned-badge">
@for (user of users; track user; let index = $index) { @for (users of
assignedList; track users ) { @if (user.id === users) {
<div <div
class="circle" class="circle"
(mousemove)="openTooltipUser(user.id, $event)"
(mouseleave)="closeTooltipUser()"
[ngStyle]="{ [ngStyle]="{
'background-color': firebaseService.getUserDetails(user.id, 'color') 'background-color': user.color
}" }"
> >
<div class="initials"> <div class="initials">
{{ firebaseService.getUserDetails(user.id, "initials") }} {{ user.initials }}
</div> </div>
</div> </div>
<div class="details"> } } } @if (tooltipUserId) {
<div class="name"> <div
<p> class="tooltip-user"
{{ firebaseService.getUserDetails(user.id, "firstName") }} [style.left.px]="dialogX"
</p> [style.top.px]="dialogY"
<span>,&nbsp;</span> >
<p class="last-name"> @for (user of users; track user) { @if (user.id === tooltipUserId) {
{{ firebaseService.getUserDetails(user.id, "lastName") }} <p>
</p> {{ user.firstName }}
</div> </p>
</div> <p>
<div class="checkbox"> {{ user.lastName }}
@if (assigned.includes(user.id)) { </p>
<img }}
class="checkbox-img"
src="./../../../../assets/img/add-task/checkbox-checked.svg"
alt=""
/>
} @else {
<img
class="checkbox-img"
src="./../../../../assets/img/add-task/checkbox-empty.svg"
alt=""
/>
}
</div> </div>
}
</div> </div>
} </div>
</section>

View file

@ -1,84 +1,101 @@
section { .assigned {
position: absolute; position: relative;
top: 75px;
left: 0; .open {
right: 0; position: absolute;
width: 100%; top: 55px;
height: auto; right: 20px;
max-height: 125px; width: 14px;
padding-top: 12px; height: 7px;
border-radius: 0px 0px 20px 20px; }
border: 1px solid var(--light-gray);
border-top: 0px; .close {
position: absolute;
top: 50px;
right: 20px;
width: 16px;
height: 16px;
cursor: pointer;
&:hover {
filter: invert(60%) sepia(58%) saturate(1629%) hue-rotate(165deg)
brightness(92%) contrast(92%);
}
}
}
.assigned-badge {
display: flex;
align-items: center;
margin: 6px 6px 12px 6px;
height: 40px;
.circle {
display: flex;
justify-content: center;
align-items: center;
width: 30px;
min-width: 30px;
height: 30px;
border-radius: 100%;
border: 2px solid var(--white);
margin-right: 6px;
cursor: default;
.initials {
font-size: 12px;
font-weight: 400;
color: var(--white);
text-shadow: 1px 1px 2px var(--black);
}
}
}
.tooltip-user {
position: fixed;
width: min-content;
height: fit-content;
padding: 10px 15px;
border-radius: 0px 20px 20px 20px;
border: 1px solid var(--black);
z-index: 2;
background-color: var(--white); background-color: var(--white);
overflow-y: auto; p {
} font-size: 18px;
.content {
display: flex;
align-items: center;
padding: 6px 12px;
cursor: pointer;
&:hover {
background-color: var(--light-gray);
}
}
.circle {
display: flex;
justify-content: center;
align-items: center;
width: 35px;
min-width: 35px;
height: 35px;
border-radius: 100%;
border: 2px solid var(--white);
.initials {
font-size: 12px;
font-weight: 400; font-weight: 400;
color: var(--white);
text-shadow: 1px 1px 2px var(--black);
} }
} }
.details { .headline {
margin-left: 24px; font-size: 20px;
width: 80%;
overflow: hidden;
white-space: nowrap;
display: inline-block;
}
.name {
display: flex;
align-items: center;
max-width: 100%;
overflow: hidden;
white-space: nowrap;
font-size: 18px;
font-weight: 400; font-weight: 400;
padding-bottom: 6px;
} }
.last-name { input,
overflow: hidden; textarea,
text-overflow: ellipsis; select {
font-size: 20px;
font-weight: 400;
border-radius: 10px;
border: 1px solid var(--light-gray);
padding: 12px 21px;
width: calc(100% - 42px);
color: var(--black);
background-color: var(--white);
resize: none;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
} }
.checkbox { input.ng-invalid.ng-touched,
img { textarea.ng-invalid.ng-touched,
width: 18px; select.ng-invalid.ng-touched {
height: 18px; border: 1px dashed red;
}
} }
.selected { input:valid,
background-color: var(--dark-blue); textarea:valid,
color: var(--white); select:valid {
img { border: 1px solid black;
filter: brightness(0) saturate(100%) invert(100%) sepia(0%) saturate(7500%)
hue-rotate(346deg) brightness(99%) contrast(103%);
}
&:hover {
background-color: var(--very-dark-blue);
}
} }

View file

@ -1,101 +1,109 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'; import {
import { FirebaseService } from '../../../services/firebase.service'; Component,
EventEmitter,
HostListener,
Input,
Output,
} from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { AssignedListComponent } from './assigned-list/assigned-list.component';
import { TranslateModule } from '@ngx-translate/core';
import { FormsModule } from '@angular/forms';
import { User } from '../../../interfaces/user.interface'; import { User } from '../../../interfaces/user.interface';
import { ApiService } from '../../../services/api.service';
import { map, catchError, of } from 'rxjs';
@Component({ @Component({
selector: 'app-assigned', selector: 'app-assigned',
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule, FormsModule, TranslateModule, AssignedListComponent],
templateUrl: './assigned.component.html', templateUrl: './assigned.component.html',
styleUrl: './assigned.component.scss', styleUrl: './assigned.component.scss',
}) })
export class AssignedComponent { export class AssignedComponent {
@Input() filteredUsers: User[] = [];
@Input() searchInput: boolean = false;
@Input() taskCreator: string = ''; @Input() taskCreator: string = '';
@Input() assignedList: string[] = [];
@Output() assignedChange = new EventEmitter<string[]>(); @Output() assignedChange = new EventEmitter<string[]>();
assigned: string[] = []; tooltipUserId: string | null = null;
filteredUsers: User[] = [];
users: User[] = [];
searchValue: string = '';
searchInput: boolean = false;
showAssignedList: boolean = false;
dialogX: number = 0;
dialogY: number = 0;
constructor(public firebaseService: FirebaseService) { constructor(private apiService: ApiService) {}
this.loadTaskAssignedData();
ngOnInit() {
this.loadUsers();
} }
/** loadUsers(): void {
* Updates the assignedChange event emitter with the current list of assigned users. this.apiService
* This should be called whenever the assigned list is updated. .getUsers()
*/ .pipe(
updateAssigned() { map((users) => users.filter((user) => user.id !== this.taskCreator)),
this.assignedChange.emit(this.assigned); catchError(() => of([]))
)
.subscribe((filteredUsers) => {
this.users = filteredUsers;
this.filteredUsers = filteredUsers;
});
} }
/** searchTask(): void {
* Toggles the assignment of a user to the task. this.searchValue = this.replaceXSSChars(this.searchValue) || '';
* this.searchInput = this.searchValue.trim().length > 0;
* If the user is not currently assigned, they are added to the assigned list.
* If the user is already assigned, they are removed from the list.
* Updates the local storage with the current list of assigned users and emits the
* assignedChange event to notify other components of the update.
*
* @param userId The ID of the user to be added or removed from the assigned list.
*/
addAssignedToTask(userId: string) {
if (!this.assigned.includes(userId)) {
this.assigned.push(userId);
} else {
this.assigned.splice(this.assigned.indexOf(userId), 1);
}
this.saveTaskData();
this.updateAssigned();
}
/** const searchValue = this.searchValue.toLowerCase();
* Saves the current list of assigned users to local storage. this.filteredUsers = this.users.filter(
* (user) =>
* Retrieves the current task data from local storage, updates the assigned user.firstName.toLowerCase().includes(searchValue) ||
* list with the current list of assigned users, and saves the updated task user.lastName.toLowerCase().includes(searchValue) ||
* data back to local storage. user.initials.toLowerCase().includes(searchValue)
*/ );
saveTaskData() { if (this.filteredUsers.length === 0) {
let taskDataString = localStorage.getItem('taskData'); this.filteredUsers = [];
if (taskDataString !== null) {
let taskData = JSON.parse(taskDataString);
taskData.assigned = this.assigned;
localStorage.setItem('taskData', JSON.stringify(taskData));
} }
} }
/** replaceXSSChars(input: string) {
* Loads the list of assigned users from local storage. return input.replace(/</g, '&lt;').replace(/>/g, '&gt;');
*
* Retrieves the task data from local storage, checks if the assigned list exists,
* and if so, assigns the list to the local assigned variable.
*/
loadTaskAssignedData() {
const taskDataString = localStorage.getItem('taskData');
if (taskDataString !== null) {
const taskData = JSON.parse(taskDataString);
if (taskData.hasOwnProperty('assigned')) {
this.assigned = taskData.assigned;
}
}
} }
/** openTooltipUser(userId: any, event: MouseEvent) {
* Returns the list of users that should be displayed in the assigned component. this.tooltipUserId = userId;
* this.updateTooltipUserPosition(event);
* If the search input is active (i.e., the user is typing in the search box), }
* the list of filtered users is returned. Otherwise, the list of users that are
* not the task creator is returned. updateTooltipUserPosition(event: MouseEvent) {
* this.dialogX = event.clientX + 25;
* @return The list of users to be displayed in the assigned component. this.dialogY = event.clientY + 10;
*/ }
displayAssigned() {
if (this.searchInput) { closeTooltipUser() {
return this.filteredUsers; this.tooltipUserId = null;
} else { }
return this.firebaseService.getFilteredUsers(this.taskCreator);
receiveAssigned(assigned: string[]) {
this.assignedChange.emit(assigned);
}
toggleAssignedMenu() {
this.showAssignedList = !this.showAssignedList;
}
@HostListener('document:click', ['$event'])
checkOpenNavbar(event: MouseEvent) {
const targetElement = event.target as HTMLElement;
if (
!targetElement.closest('.search-assigned') &&
!targetElement.closest('app-assigned-list') &&
!targetElement.closest('.checkbox-img')
) {
this.showAssignedList = false;
} }
} }
} }