refactor: extracted assigned logic from AddTaskComponent to AssignedComponent and migrated to Django Rest
This commit is contained in:
parent
3293def254
commit
ff5abfa41a
10 changed files with 500 additions and 415 deletions
|
|
@ -5,14 +5,14 @@
|
|||
}"
|
||||
>
|
||||
<div
|
||||
class="headline"
|
||||
class="header"
|
||||
[ngStyle]="{ display: overlayData !== '' ? 'none' : 'block' }"
|
||||
>
|
||||
<div class="title">{{ "addTask.headline" | translate }}</div>
|
||||
</div>
|
||||
|
||||
<form
|
||||
(ngSubmit)="onSubmit(taskForm, overlayData)"
|
||||
(ngSubmit)="onSubmit(taskForm)"
|
||||
(keydown.enter)="handleEnterKey($event)"
|
||||
#taskForm="ngForm"
|
||||
onsubmit="return false"
|
||||
|
|
@ -26,7 +26,9 @@
|
|||
>
|
||||
<div class="left-side">
|
||||
<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
|
||||
type="text"
|
||||
id="title"
|
||||
|
|
@ -49,10 +51,10 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
<div class="headline">
|
||||
{{ "addTask.description" | translate
|
||||
}}<span class="red-dot">*</span>
|
||||
</p>
|
||||
</div>
|
||||
<textarea
|
||||
id="description"
|
||||
rows="5"
|
||||
|
|
@ -75,62 +77,11 @@
|
|||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="assigned">
|
||||
<p>{{ "addTask.assigned" | translate }}</p>
|
||||
<input
|
||||
#searchField
|
||||
id="search-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"
|
||||
[assignedList]="taskData.assigned"
|
||||
(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>
|
||||
}
|
||||
></app-assigned>
|
||||
</div>
|
||||
<div class="middle-spacer"><div class="line"></div></div>
|
||||
<div class="right-side">
|
||||
|
|
@ -140,7 +91,9 @@
|
|||
'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
|
||||
type="date"
|
||||
id="date"
|
||||
|
|
@ -161,7 +114,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="priority">
|
||||
<p>{{ "addTask.priority" | translate }}</p>
|
||||
<div class="headline">{{ "addTask.priority" | translate }}</div>
|
||||
<div class="btns">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -217,9 +170,9 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="category">
|
||||
<p>
|
||||
<div class="headline">
|
||||
{{ "addTask.category" | translate }}<span class="red-dot">*</span>
|
||||
</p>
|
||||
</div>
|
||||
<select
|
||||
id="category"
|
||||
name="category"
|
||||
|
|
@ -246,7 +199,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="subtask">
|
||||
<p>{{ "addTask.subtask" | translate }}</p>
|
||||
<div class="headline">{{ "addTask.subtask" | translate }}</div>
|
||||
<input
|
||||
type="text"
|
||||
id="subtask"
|
||||
|
|
|
|||
|
|
@ -8,21 +8,6 @@ section {
|
|||
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 {
|
||||
height: calc(700px - 148px);
|
||||
padding: 0 6px;
|
||||
|
|
@ -36,6 +21,12 @@ section {
|
|||
background-color: var(--very-light-gray);
|
||||
}
|
||||
|
||||
.headline {
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
|
|
@ -65,12 +56,6 @@ select:valid {
|
|||
border: 1px solid black;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.red-dot {
|
||||
color: var(--light-red);
|
||||
}
|
||||
|
|
@ -91,7 +76,7 @@ p {
|
|||
|
||||
/*------------- HEADER -------------*/
|
||||
|
||||
.headline {
|
||||
.header {
|
||||
margin-bottom: 48px;
|
||||
.title {
|
||||
font-size: 61px;
|
||||
|
|
@ -105,55 +90,6 @@ p {
|
|||
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 {
|
||||
position: relative;
|
||||
.add {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { CommonModule } from '@angular/common';
|
|||
import { Component, HostListener, Input, OnInit } from '@angular/core';
|
||||
import { FormsModule, NgForm } from '@angular/forms';
|
||||
import { AssignedComponent } from './assigned/assigned.component';
|
||||
import { User } from '../../interfaces/user.interface';
|
||||
import { FirebaseService } from '../../services/firebase.service';
|
||||
import { Task } from '../../interfaces/task.interface';
|
||||
import { OverlayService } from '../../services/overlay.service';
|
||||
|
|
@ -36,14 +35,7 @@ export class AddTaskComponent implements OnInit {
|
|||
|
||||
currentDate: string = new Date().toISOString().split('T')[0];
|
||||
dateInPast: boolean = false;
|
||||
isAssignedOpen: boolean = false;
|
||||
subtaskValue: string = '';
|
||||
searchValue: string = '';
|
||||
searchInput: boolean = false;
|
||||
filteredUsers: User[] = [];
|
||||
AssignedDialogId: string = '';
|
||||
dialogX: number = 0;
|
||||
dialogY: number = 0;
|
||||
|
||||
constructor(
|
||||
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.
|
||||
* @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.
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
|
@ -373,7 +296,7 @@ export class AddTaskComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
onSubmit(ngForm: NgForm, overlayData: string) {
|
||||
onSubmit(ngForm: NgForm) {
|
||||
if (ngForm.submitted && ngForm.form.valid) {
|
||||
const { id, ...taskWithoutId } = this.taskData;
|
||||
|
||||
|
|
@ -412,28 +335,9 @@ export class AddTaskComponent implements OnInit {
|
|||
*/
|
||||
deleteTask(overlayData: string) {
|
||||
this.apiService.deleteTaskById(overlayData);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
replaceXSSChars(input: string) {
|
||||
return input.replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>, </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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,47 +1,65 @@
|
|||
<section>
|
||||
@for (user of displayAssigned(); track user; let index = $index) {
|
||||
<div
|
||||
class="content"
|
||||
(click)="addAssignedToTask(user.id)"
|
||||
[ngClass]="{ selected: assigned.includes(user.id) }"
|
||||
*ngIf="user.id"
|
||||
<div class="assigned">
|
||||
<div class="headline">{{ "addTask.assigned" | translate }}</div>
|
||||
<input
|
||||
#searchField
|
||||
id="search-assigned"
|
||||
class="search-assigned"
|
||||
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
|
||||
class="circle"
|
||||
(mousemove)="openTooltipUser(user.id, $event)"
|
||||
(mouseleave)="closeTooltipUser()"
|
||||
[ngStyle]="{
|
||||
'background-color': firebaseService.getUserDetails(user.id, 'color')
|
||||
'background-color': user.color
|
||||
}"
|
||||
>
|
||||
<div class="initials">
|
||||
{{ firebaseService.getUserDetails(user.id, "initials") }}
|
||||
{{ user.initials }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="details">
|
||||
<div class="name">
|
||||
} } } @if (tooltipUserId) {
|
||||
<div
|
||||
class="tooltip-user"
|
||||
[style.left.px]="dialogX"
|
||||
[style.top.px]="dialogY"
|
||||
>
|
||||
@for (user of users; track user) { @if (user.id === tooltipUserId) {
|
||||
<p>
|
||||
{{ firebaseService.getUserDetails(user.id, "firstName") }}
|
||||
{{ user.firstName }}
|
||||
</p>
|
||||
<span>, </span>
|
||||
<p class="last-name">
|
||||
{{ firebaseService.getUserDetails(user.id, "lastName") }}
|
||||
<p>
|
||||
{{ 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>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,84 +1,101 @@
|
|||
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;
|
||||
}
|
||||
.assigned {
|
||||
position: relative;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
.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 {
|
||||
background-color: var(--light-gray);
|
||||
filter: invert(60%) sepia(58%) saturate(1629%) hue-rotate(165deg)
|
||||
brightness(92%) contrast(92%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.circle {
|
||||
.assigned-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 6px 6px 12px 6px;
|
||||
height: 40px;
|
||||
|
||||
.circle {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 35px;
|
||||
min-width: 35px;
|
||||
height: 35px;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
.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);
|
||||
p {
|
||||
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);
|
||||
}
|
||||
.headline {
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
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;
|
||||
}
|
||||
|
||||
input.ng-invalid.ng-touched,
|
||||
textarea.ng-invalid.ng-touched,
|
||||
select.ng-invalid.ng-touched {
|
||||
border: 1px dashed red;
|
||||
}
|
||||
|
||||
input:valid,
|
||||
textarea:valid,
|
||||
select:valid {
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,101 +1,109 @@
|
|||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { FirebaseService } from '../../../services/firebase.service';
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
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 { ApiService } from '../../../services/api.service';
|
||||
import { map, catchError, of } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-assigned',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
imports: [CommonModule, FormsModule, TranslateModule, AssignedListComponent],
|
||||
templateUrl: './assigned.component.html',
|
||||
styleUrl: './assigned.component.scss',
|
||||
})
|
||||
export class AssignedComponent {
|
||||
@Input() filteredUsers: User[] = [];
|
||||
@Input() searchInput: boolean = false;
|
||||
@Input() taskCreator: string = '';
|
||||
@Input() assignedList: 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) {
|
||||
this.loadTaskAssignedData();
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.loadUsers();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
loadUsers(): void {
|
||||
this.apiService
|
||||
.getUsers()
|
||||
.pipe(
|
||||
map((users) => users.filter((user) => user.id !== this.taskCreator)),
|
||||
catchError(() => of([]))
|
||||
)
|
||||
.subscribe((filteredUsers) => {
|
||||
this.users = filteredUsers;
|
||||
this.filteredUsers = filteredUsers;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
searchTask(): void {
|
||||
this.searchValue = this.replaceXSSChars(this.searchValue) || '';
|
||||
this.searchInput = this.searchValue.trim().length > 0;
|
||||
|
||||
/**
|
||||
* 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));
|
||||
const searchValue = this.searchValue.toLowerCase();
|
||||
this.filteredUsers = this.users.filter(
|
||||
(user) =>
|
||||
user.firstName.toLowerCase().includes(searchValue) ||
|
||||
user.lastName.toLowerCase().includes(searchValue) ||
|
||||
user.initials.toLowerCase().includes(searchValue)
|
||||
);
|
||||
if (this.filteredUsers.length === 0) {
|
||||
this.filteredUsers = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
replaceXSSChars(input: string) {
|
||||
return input.replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of users that should be displayed in the assigned component.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @return The list of users to be displayed in the assigned component.
|
||||
*/
|
||||
displayAssigned() {
|
||||
if (this.searchInput) {
|
||||
return this.filteredUsers;
|
||||
} else {
|
||||
return this.firebaseService.getFilteredUsers(this.taskCreator);
|
||||
openTooltipUser(userId: any, event: MouseEvent) {
|
||||
this.tooltipUserId = userId;
|
||||
this.updateTooltipUserPosition(event);
|
||||
}
|
||||
|
||||
updateTooltipUserPosition(event: MouseEvent) {
|
||||
this.dialogX = event.clientX + 25;
|
||||
this.dialogY = event.clientY + 10;
|
||||
}
|
||||
|
||||
closeTooltipUser() {
|
||||
this.tooltipUserId = null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue