2023-05-30 09:26:50 +03:00

741 lines
22 KiB
TypeScript

import { Overlay, OverlayRef } from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal';
import { TextFieldModule } from '@angular/cdk/text-field';
import { DatePipe, NgClass, NgFor, NgIf } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit, Renderer2, TemplateRef, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatOptionModule, MatRippleModule } from '@angular/material/core';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatDrawerToggleResult } from '@angular/material/sidenav';
import { MatTooltipModule } from '@angular/material/tooltip';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { FuseFindByKeyPipe } from '@fuse/pipes/find-by-key/find-by-key.pipe';
import { FuseConfirmationService } from '@fuse/services/confirmation';
import { ContactsService } from 'app/modules/admin/apps/contacts/contacts.service';
import { Contact, Country, Tag } from 'app/modules/admin/apps/contacts/contacts.types';
import { ContactsListComponent } from 'app/modules/admin/apps/contacts/list/list.component';
import { debounceTime, Subject, takeUntil } from 'rxjs';
@Component({
selector : 'contacts-details',
templateUrl : './details.component.html',
encapsulation : ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone : true,
imports : [NgIf, MatButtonModule, MatTooltipModule, RouterLink, MatIconModule, NgFor, FormsModule, ReactiveFormsModule, MatRippleModule, MatFormFieldModule, MatInputModule, MatCheckboxModule, NgClass, MatSelectModule, MatOptionModule, MatDatepickerModule, TextFieldModule, FuseFindByKeyPipe, DatePipe],
})
export class ContactsDetailsComponent implements OnInit, OnDestroy
{
@ViewChild('avatarFileInput') private _avatarFileInput: ElementRef;
@ViewChild('tagsPanel') private _tagsPanel: TemplateRef<any>;
@ViewChild('tagsPanelOrigin') private _tagsPanelOrigin: ElementRef;
editMode: boolean = false;
tags: Tag[];
tagsEditMode: boolean = false;
filteredTags: Tag[];
contact: Contact;
contactForm: UntypedFormGroup;
contacts: Contact[];
countries: Country[];
private _tagsPanelOverlayRef: OverlayRef;
private _unsubscribeAll: Subject<any> = new Subject<any>();
/**
* Constructor
*/
constructor(
private _activatedRoute: ActivatedRoute,
private _changeDetectorRef: ChangeDetectorRef,
private _contactsListComponent: ContactsListComponent,
private _contactsService: ContactsService,
private _formBuilder: UntypedFormBuilder,
private _fuseConfirmationService: FuseConfirmationService,
private _renderer2: Renderer2,
private _router: Router,
private _overlay: Overlay,
private _viewContainerRef: ViewContainerRef,
)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Open the drawer
this._contactsListComponent.matDrawer.open();
// Create the contact form
this.contactForm = this._formBuilder.group({
id : [''],
avatar : [null],
name : ['', [Validators.required]],
emails : this._formBuilder.array([]),
phoneNumbers: this._formBuilder.array([]),
title : [''],
company : [''],
birthday : [null],
address : [null],
notes : [null],
tags : [[]],
});
// Get the contacts
this._contactsService.contacts$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((contacts: Contact[]) =>
{
this.contacts = contacts;
// Mark for check
this._changeDetectorRef.markForCheck();
});
// Get the contact
this._contactsService.contact$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((contact: Contact) =>
{
// Open the drawer in case it is closed
this._contactsListComponent.matDrawer.open();
// Get the contact
this.contact = contact;
// Clear the emails and phoneNumbers form arrays
(this.contactForm.get('emails') as UntypedFormArray).clear();
(this.contactForm.get('phoneNumbers') as UntypedFormArray).clear();
// Patch values to the form
this.contactForm.patchValue(contact);
// Setup the emails form array
const emailFormGroups = [];
if ( contact.emails.length > 0 )
{
// Iterate through them
contact.emails.forEach((email) =>
{
// Create an email form group
emailFormGroups.push(
this._formBuilder.group({
email: [email.email],
label: [email.label],
}),
);
});
}
else
{
// Create an email form group
emailFormGroups.push(
this._formBuilder.group({
email: [''],
label: [''],
}),
);
}
// Add the email form groups to the emails form array
emailFormGroups.forEach((emailFormGroup) =>
{
(this.contactForm.get('emails') as UntypedFormArray).push(emailFormGroup);
});
// Setup the phone numbers form array
const phoneNumbersFormGroups = [];
if ( contact.phoneNumbers.length > 0 )
{
// Iterate through them
contact.phoneNumbers.forEach((phoneNumber) =>
{
// Create an email form group
phoneNumbersFormGroups.push(
this._formBuilder.group({
country : [phoneNumber.country],
phoneNumber: [phoneNumber.phoneNumber],
label : [phoneNumber.label],
}),
);
});
}
else
{
// Create a phone number form group
phoneNumbersFormGroups.push(
this._formBuilder.group({
country : ['us'],
phoneNumber: [''],
label : [''],
}),
);
}
// Add the phone numbers form groups to the phone numbers form array
phoneNumbersFormGroups.forEach((phoneNumbersFormGroup) =>
{
(this.contactForm.get('phoneNumbers') as UntypedFormArray).push(phoneNumbersFormGroup);
});
// Toggle the edit mode off
this.toggleEditMode(false);
// Mark for check
this._changeDetectorRef.markForCheck();
});
// Get the country telephone codes
this._contactsService.countries$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((codes: Country[]) =>
{
this.countries = codes;
// Mark for check
this._changeDetectorRef.markForCheck();
});
// Get the tags
this._contactsService.tags$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((tags: Tag[]) =>
{
this.tags = tags;
this.filteredTags = tags;
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next(null);
this._unsubscribeAll.complete();
// Dispose the overlays if they are still on the DOM
if ( this._tagsPanelOverlayRef )
{
this._tagsPanelOverlayRef.dispose();
}
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Close the drawer
*/
closeDrawer(): Promise<MatDrawerToggleResult>
{
return this._contactsListComponent.matDrawer.close();
}
/**
* Toggle edit mode
*
* @param editMode
*/
toggleEditMode(editMode: boolean | null = null): void
{
if ( editMode === null )
{
this.editMode = !this.editMode;
}
else
{
this.editMode = editMode;
}
// Mark for check
this._changeDetectorRef.markForCheck();
}
/**
* Update the contact
*/
updateContact(): void
{
// Get the contact object
const contact = this.contactForm.getRawValue();
// Go through the contact object and clear empty values
contact.emails = contact.emails.filter(email => email.email);
contact.phoneNumbers = contact.phoneNumbers.filter(phoneNumber => phoneNumber.phoneNumber);
// Update the contact on the server
this._contactsService.updateContact(contact.id, contact).subscribe(() =>
{
// Toggle the edit mode off
this.toggleEditMode(false);
});
}
/**
* Delete the contact
*/
deleteContact(): void
{
// Open the confirmation dialog
const confirmation = this._fuseConfirmationService.open({
title : 'Delete contact',
message: 'Are you sure you want to delete this contact? This action cannot be undone!',
actions: {
confirm: {
label: 'Delete',
},
},
});
// Subscribe to the confirmation dialog closed action
confirmation.afterClosed().subscribe((result) =>
{
// If the confirm button pressed...
if ( result === 'confirmed' )
{
// Get the current contact's id
const id = this.contact.id;
// Get the next/previous contact's id
const currentContactIndex = this.contacts.findIndex(item => item.id === id);
const nextContactIndex = currentContactIndex + ((currentContactIndex === (this.contacts.length - 1)) ? -1 : 1);
const nextContactId = (this.contacts.length === 1 && this.contacts[0].id === id) ? null : this.contacts[nextContactIndex].id;
// Delete the contact
this._contactsService.deleteContact(id)
.subscribe((isDeleted) =>
{
// Return if the contact wasn't deleted...
if ( !isDeleted )
{
return;
}
// Navigate to the next contact if available
if ( nextContactId )
{
this._router.navigate(['../', nextContactId], {relativeTo: this._activatedRoute});
}
// Otherwise, navigate to the parent
else
{
this._router.navigate(['../'], {relativeTo: this._activatedRoute});
}
// Toggle the edit mode off
this.toggleEditMode(false);
});
// Mark for check
this._changeDetectorRef.markForCheck();
}
});
}
/**
* Upload avatar
*
* @param fileList
*/
uploadAvatar(fileList: FileList): void
{
// Return if canceled
if ( !fileList.length )
{
return;
}
const allowedTypes = ['image/jpeg', 'image/png'];
const file = fileList[0];
// Return if the file is not allowed
if ( !allowedTypes.includes(file.type) )
{
return;
}
// Upload the avatar
this._contactsService.uploadAvatar(this.contact.id, file).subscribe();
}
/**
* Remove the avatar
*/
removeAvatar(): void
{
// Get the form control for 'avatar'
const avatarFormControl = this.contactForm.get('avatar');
// Set the avatar as null
avatarFormControl.setValue(null);
// Set the file input value as null
this._avatarFileInput.nativeElement.value = null;
// Update the contact
this.contact.avatar = null;
}
/**
* Open tags panel
*/
openTagsPanel(): void
{
// Create the overlay
this._tagsPanelOverlayRef = this._overlay.create({
backdropClass : '',
hasBackdrop : true,
scrollStrategy : this._overlay.scrollStrategies.block(),
positionStrategy: this._overlay.position()
.flexibleConnectedTo(this._tagsPanelOrigin.nativeElement)
.withFlexibleDimensions(true)
.withViewportMargin(64)
.withLockedPosition(true)
.withPositions([
{
originX : 'start',
originY : 'bottom',
overlayX: 'start',
overlayY: 'top',
},
]),
});
// Subscribe to the attachments observable
this._tagsPanelOverlayRef.attachments().subscribe(() =>
{
// Add a class to the origin
this._renderer2.addClass(this._tagsPanelOrigin.nativeElement, 'panel-opened');
// Focus to the search input once the overlay has been attached
this._tagsPanelOverlayRef.overlayElement.querySelector('input').focus();
});
// Create a portal from the template
const templatePortal = new TemplatePortal(this._tagsPanel, this._viewContainerRef);
// Attach the portal to the overlay
this._tagsPanelOverlayRef.attach(templatePortal);
// Subscribe to the backdrop click
this._tagsPanelOverlayRef.backdropClick().subscribe(() =>
{
// Remove the class from the origin
this._renderer2.removeClass(this._tagsPanelOrigin.nativeElement, 'panel-opened');
// If overlay exists and attached...
if ( this._tagsPanelOverlayRef && this._tagsPanelOverlayRef.hasAttached() )
{
// Detach it
this._tagsPanelOverlayRef.detach();
// Reset the tag filter
this.filteredTags = this.tags;
// Toggle the edit mode off
this.tagsEditMode = false;
}
// If template portal exists and attached...
if ( templatePortal && templatePortal.isAttached )
{
// Detach it
templatePortal.detach();
}
});
}
/**
* Toggle the tags edit mode
*/
toggleTagsEditMode(): void
{
this.tagsEditMode = !this.tagsEditMode;
}
/**
* Filter tags
*
* @param event
*/
filterTags(event): void
{
// Get the value
const value = event.target.value.toLowerCase();
// Filter the tags
this.filteredTags = this.tags.filter(tag => tag.title.toLowerCase().includes(value));
}
/**
* Filter tags input key down event
*
* @param event
*/
filterTagsInputKeyDown(event): void
{
// Return if the pressed key is not 'Enter'
if ( event.key !== 'Enter' )
{
return;
}
// If there is no tag available...
if ( this.filteredTags.length === 0 )
{
// Create the tag
this.createTag(event.target.value);
// Clear the input
event.target.value = '';
// Return
return;
}
// If there is a tag...
const tag = this.filteredTags[0];
const isTagApplied = this.contact.tags.find(id => id === tag.id);
// If the found tag is already applied to the contact...
if ( isTagApplied )
{
// Remove the tag from the contact
this.removeTagFromContact(tag);
}
else
{
// Otherwise add the tag to the contact
this.addTagToContact(tag);
}
}
/**
* Create a new tag
*
* @param title
*/
createTag(title: string): void
{
const tag = {
title,
};
// Create tag on the server
this._contactsService.createTag(tag)
.subscribe((response) =>
{
// Add the tag to the contact
this.addTagToContact(response);
});
}
/**
* Update the tag title
*
* @param tag
* @param event
*/
updateTagTitle(tag: Tag, event): void
{
// Update the title on the tag
tag.title = event.target.value;
// Update the tag on the server
this._contactsService.updateTag(tag.id, tag)
.pipe(debounceTime(300))
.subscribe();
// Mark for check
this._changeDetectorRef.markForCheck();
}
/**
* Delete the tag
*
* @param tag
*/
deleteTag(tag: Tag): void
{
// Delete the tag from the server
this._contactsService.deleteTag(tag.id).subscribe();
// Mark for check
this._changeDetectorRef.markForCheck();
}
/**
* Add tag to the contact
*
* @param tag
*/
addTagToContact(tag: Tag): void
{
// Add the tag
this.contact.tags.unshift(tag.id);
// Update the contact form
this.contactForm.get('tags').patchValue(this.contact.tags);
// Mark for check
this._changeDetectorRef.markForCheck();
}
/**
* Remove tag from the contact
*
* @param tag
*/
removeTagFromContact(tag: Tag): void
{
// Remove the tag
this.contact.tags.splice(this.contact.tags.findIndex(item => item === tag.id), 1);
// Update the contact form
this.contactForm.get('tags').patchValue(this.contact.tags);
// Mark for check
this._changeDetectorRef.markForCheck();
}
/**
* Toggle contact tag
*
* @param tag
*/
toggleContactTag(tag: Tag): void
{
if ( this.contact.tags.includes(tag.id) )
{
this.removeTagFromContact(tag);
}
else
{
this.addTagToContact(tag);
}
}
/**
* Should the create tag button be visible
*
* @param inputValue
*/
shouldShowCreateTagButton(inputValue: string): boolean
{
return !!!(inputValue === '' || this.tags.findIndex(tag => tag.title.toLowerCase() === inputValue.toLowerCase()) > -1);
}
/**
* Add the email field
*/
addEmailField(): void
{
// Create an empty email form group
const emailFormGroup = this._formBuilder.group({
email: [''],
label: [''],
});
// Add the email form group to the emails form array
(this.contactForm.get('emails') as UntypedFormArray).push(emailFormGroup);
// Mark for check
this._changeDetectorRef.markForCheck();
}
/**
* Remove the email field
*
* @param index
*/
removeEmailField(index: number): void
{
// Get form array for emails
const emailsFormArray = this.contactForm.get('emails') as UntypedFormArray;
// Remove the email field
emailsFormArray.removeAt(index);
// Mark for check
this._changeDetectorRef.markForCheck();
}
/**
* Add an empty phone number field
*/
addPhoneNumberField(): void
{
// Create an empty phone number form group
const phoneNumberFormGroup = this._formBuilder.group({
country : ['us'],
phoneNumber: [''],
label : [''],
});
// Add the phone number form group to the phoneNumbers form array
(this.contactForm.get('phoneNumbers') as UntypedFormArray).push(phoneNumberFormGroup);
// Mark for check
this._changeDetectorRef.markForCheck();
}
/**
* Remove the phone number field
*
* @param index
*/
removePhoneNumberField(index: number): void
{
// Get form array for phone numbers
const phoneNumbersFormArray = this.contactForm.get('phoneNumbers') as UntypedFormArray;
// Remove the phone number field
phoneNumbersFormArray.removeAt(index);
// Mark for check
this._changeDetectorRef.markForCheck();
}
/**
* Get country info by iso code
*
* @param iso
*/
getCountryByIso(iso: string): Country
{
return this.countries.find(country => country.iso === iso);
}
/**
* Track by function for ngFor loops
*
* @param index
* @param item
*/
trackByFn(index: number, item: any): any
{
return item.id || index;
}
}