Files
ever-gauzy/apps/gauzy/src/app/pages/contacts/contacts.component.ts
T
Rahul R. e6b86f4c9d [Chore] Support Angular v19 (Nx v20.8.0) (#8610)
* chore(nx): migrate @nx/workspace to version 20.3.0

* chore(material): run migration-v19 for Angular Material update

* chore(packages): run explicit-standalone-flag migration for Angular v19

* chore(deps): downgrade TypeScript to 5.5.4 for Angular compatibility

* chore(deps): bump Angular ESLint dependencies to version 19

* chore(deps): bump @ng-select/ng-select to version 14.1.0

* chore(deps): bump @commitlint packages to latest versions

* fix(cspell): typo spelling :-)

* chore(packages): migrate initializers to new provider functions

* chore(deps): update the @angular/cli package version to ~19.0.0

* chore(deps): update packages to support Angular v19

* chore(packages): migrate initializers to new provider functions

* fix: correct Sentry.TraceService injection in AppInitializer

* Update yarn.lock

* Update yarn.lock

* fix: update yarn.lock

* fix(cspell): typo spelling :-)

* fix: update yarn.lock

* chore(deps): bump @angular/common to version 19.0.0

* chore(nx): migrate @nx/workspace to version 20.4.2

* chore(deps): update yarn.lock

* chore(nx): migrate @nx/workspace to version 20.4.4

* chore(deps): bump @angular/core from 19.1.5 to 19.1.6

* chore(deps): upgrade @nebular/auth and @nebular/theme to v15

- Upgrade @nebular/auth from ^14.0.2 to ^15.0.0
- Upgrade @nebular/theme from ^14.0.2 to ^15.0.0

* chore(deps): bump @angular-slider/ngx-slider from ^18.0.0 to ^19.0.0

* chore(deps): bump ngx-page-scroll and ngx-page-scroll-core to v14

- Bump ngx-page-scroll from ^13.0.0 to ^14.0.0
- Bump ngx-page-scroll-core from ^13.0.0 to ^14.0.0

* chore(deps): bump ngx-permissions from ^17.1.0 to ^19.0.0

* chore(deps): bump @ng-select/ng-select from ^14.1.0 to ^14.2.2

* chore(deps): bump @fortawesome/angular-fontawesome from 0.15.0 to 1.0.0

* fix: desktop build (make component standalone disabled)

* chore(deps): bump @angular/core from 18.2.12 to 19.1.6

* chore(nx): migrate @nx/workspace to version 20.8.0

* chore(deps): bump ng2-file-upload to version ^8.0.0

* chore(deps): bump @angular/core from 19.1.6 to 19.2.7

* chore(deps): bump @fullcalendar/core from ^6.1.15 to ^6.1.17

* chore(deps): bump @angular-eslint packages to v19.3.0

* fix: add `standalone: false` to angular components

* chore(deps): downgrade @angular-builders/custom-webpack from v19.0.1 to v18.0.0

* chore(deps): bump angular2-smart-table from ^3.5.0 to ^3.6.1

* refactor(styles): replace @import with @use for SCSS module

* refactor(styles): replace `@import` with `@use` for SCSS module

* refactor(styles): replace `@import` with `@use` for SCSS module

* refactor(styles): replace `@import` with `@use` for SCSS module

* refactor(styles): replace `@import` with `@use` for SCSS module

* refactor(styles): replace `@import` with `@use` for SCSS module

* refactor(styles): replace `@import` with `@use` for SCSS module

* refactor(styles): replace `@import` with `@use` for SCSS module

* refactor(styles): replace `@import` with `@use` for SCSS module

* refactor(styles): replace `@import` with `@use` for SCSS module

* refactor(styles): replace `@import` with `@use` for SCSS module

* chore(deps): bump @angular/core from 19.0.0 to 19.2.0

* chore(deps): updated dependencies for `@fullcalendar/angular`

* fix(cspell): typo spelling :-)

* Revert "chore(deps): updated dependencies for `@fullcalendar/angular`"

This reverts commit 4b34d72071.

* chore(deps): switch @angular/* to ^19.2.7

* fix: revert nx migrations.json

* Revert "chore(deps): switch @angular/* to ^19.2.7"

This reverts commit edd9105c51.

* fix: align @angular/cdk version with @angular/core version

* chore(deps): bump @angular-builders/custom-webpack from 18.0.0 to 19.0.1

* refactor(styles): replace `@import` with `@use` for SCSS module

* refactor(styles): replace `@import` with `@use` for SCSS module

* refactor(styles): replace `@import` with `@use` for SCSS module

* refactor(styles): replace `@import` with `@use` for SCSS module

* refactor(styles): replace `@import` with `@use` for SCSS module

* refactor(styles): replace `@import` with `@use` for SCSS module

* refactor(styles): replace `@import` with `@use` for SCSS module

* refactor(styles): replace `@import` with `@use` for SCSS module

* refactor(styles): replace `@import` with `@use` for SCSS module

* refactor(styles): replace `@import` with `@use` for SCSS module

* chore(deps): bump typescript from 5.5.4 to ^5.8.3

* fix(server-dashboard): remove redundant nullish coalescing from autoStart check

* fix: add suggestions from greptile-apps bot review

* fix: add suggestions from greptile-apps bot review
2025-04-26 15:47:55 +05:30

626 lines
17 KiB
TypeScript

import { Component, OnInit, ViewChild, Input, OnDestroy, ChangeDetectorRef, TemplateRef } from '@angular/core';
import { ActivatedRoute, Data, ParamMap, Router } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import {
IOrganizationContact,
IOrganizationContactCreateInput,
IOrganizationProject,
ComponentLayoutStyleEnum,
IOrganization,
IContact,
ICountry,
ContactType,
ContactOrganizationInviteStatus,
IContactCreateInput
} from '@gauzy/contracts';
import { NbDialogService } from '@nebular/theme';
import { TranslateService } from '@ngx-translate/core';
import { combineLatest, Subject, firstValueFrom } from 'rxjs';
import { debounceTime, filter, tap } from 'rxjs/operators';
import { Cell } from 'angular2-smart-table';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import {
CountryService,
OrganizationContactService,
OrganizationProjectsService,
ServerDataSource,
Store,
ToastrService
} from '@gauzy/ui-core/core';
import { API_PREFIX, ComponentEnum, distinctUntilChange } from '@gauzy/ui-core/common';
import { InviteContactComponent } from './invite-contact/invite-contact.component';
import {
ContactWithTagsComponent,
DeleteConfirmationComponent,
EmployeeWithLinksComponent,
IPaginationBase,
InputFilterComponent,
PaginationFilterBaseComponent,
ProjectComponent
} from '@gauzy/ui-core/shared';
@UntilDestroy({ checkProperties: true })
@Component({
selector: 'ngx-contacts-list',
templateUrl: './contacts.component.html',
styleUrls: ['./contacts.component.scss'],
standalone: false
})
export class ContactsComponent extends PaginationFilterBaseComponent implements OnInit, OnDestroy {
showAddCard: boolean;
organizationContacts: IOrganizationContact[] = [];
projectsWithoutOrganizationContacts: IOrganizationProject[] = [];
selectedOrganizationContact: IOrganizationContact;
viewComponentName: ComponentEnum;
dataLayoutStyle = ComponentLayoutStyleEnum.CARDS_GRID;
componentLayoutStyleEnum = ComponentLayoutStyleEnum;
contactOrganizationInviteStatus = ContactOrganizationInviteStatus;
settingsSmartTable: object;
countries: ICountry[] = [];
disableButton = true;
loading = false;
smartTableSource: ServerDataSource;
public contacts$: Subject<any> = this.subject$;
public organization: IOrganization;
public selectedEmployeeId: string;
private _refresh$: Subject<any> = new Subject();
/*
* Getter & Setter for contact type
*/
_contactType: string = ContactType.CUSTOMER;
get contactType(): string {
return this._contactType;
}
@Input() set contactType(value: string) {
this._contactType = value;
}
/*
* Actions Buttons directive
*/
@ViewChild('actionButtons', { static: true })
actionButtons: TemplateRef<any>;
constructor(
private readonly organizationContactService: OrganizationContactService,
private readonly organizationProjectsService: OrganizationProjectsService,
private readonly toastrService: ToastrService,
private readonly store: Store,
public readonly translateService: TranslateService,
private readonly dialogService: NbDialogService,
private readonly route: ActivatedRoute,
private readonly countryService: CountryService,
private readonly cd: ChangeDetectorRef,
private readonly _router: Router,
private readonly http: HttpClient
) {
super(translateService);
this.countryService.find$.next(true);
}
ngOnInit(): void {
this._applyTranslationOnSmartTable();
this.route.data
.pipe(
tap((params: Data) => (this.contactType = params.contactType)),
tap(() => this.setView()),
tap(() => this._loadSmartTableSettings()),
untilDestroyed(this)
)
.subscribe();
this.contacts$
.pipe(
debounceTime(100),
tap(() => this.clearItem()),
tap(async () => {
await this.loadProjectsWithoutOrganizationContacts();
await this.getContacts();
}),
untilDestroyed(this)
)
.subscribe();
this.pagination$
.pipe(
debounceTime(100),
distinctUntilChange(),
tap(() => this.contacts$.next(true)),
untilDestroyed(this)
)
.subscribe();
const storeOrganization$ = this.store.selectedOrganization$;
const storeEmployee$ = this.store.selectedEmployee$;
combineLatest([storeOrganization$, storeEmployee$])
.pipe(
debounceTime(300),
filter(([organization]) => !!organization),
distinctUntilChange(),
tap(([organization, employee]) => {
this.organization = organization;
this.selectedEmployeeId = employee ? employee.id : null;
}),
tap(() => this._refresh$.next(true)),
tap(() => this.contacts$.next(true)),
untilDestroyed(this)
)
.subscribe();
this.route.queryParamMap
.pipe(
filter((params: ParamMap) => !!params),
filter((params: ParamMap) => params.get('openAddDialog') === 'true'),
debounceTime(1000),
tap(() => this.add()),
untilDestroyed(this)
)
.subscribe();
this.route.queryParamMap
.pipe(
filter((params: ParamMap) => !!params),
filter((params: ParamMap) => !!params.get('id')),
tap(async (params: ParamMap) => await this._initEditMethod(params.get('id'))),
untilDestroyed(this)
)
.subscribe();
this.countryService.countries$
.pipe(
tap((countries: ICountry[]) => (this.countries = countries)),
untilDestroyed(this)
)
.subscribe();
this._refresh$
.pipe(
filter(() => this.dataLayoutStyle === ComponentLayoutStyleEnum.CARDS_GRID),
tap(() => this.refreshPagination()),
tap(() => (this.organizationContacts = [])),
untilDestroyed(this)
)
.subscribe();
}
private async _initEditMethod(id: string) {
if (id) {
this.loading = true;
const { tenantId } = this.store.user;
try {
const items = await this.organizationContactService.getById(id, tenantId, [
'projects',
'members',
'members.user',
'tags',
'contact'
]);
if (items) {
this.editOrganizationContact(items);
}
} catch (error) {
this.toastrService.danger(this.getTranslation('TOASTR.TITLE.ERROR'));
}
this.loading = false;
this.cd.detectChanges();
}
}
private _loadSmartTableSettings() {
const pagination: IPaginationBase = this.getPagination();
this.settingsSmartTable = {
actions: false,
selectedRowIndex: -1,
pager: {
display: false,
perPage: pagination ? pagination.itemsPerPage : 10
},
noDataMessage: this.getNoDataMessage(),
columns: {
name: {
title: this.getTranslation('ORGANIZATIONS_PAGE.NAME'),
width: '15%',
type: 'custom',
class: 'align-row',
renderComponent: ContactWithTagsComponent,
componentInitFunction: (instance: ContactWithTagsComponent, cell: Cell) => {
instance.rowData = cell.getRow().getData();
instance.value = cell.getValue();
},
filter: {
type: 'custom',
component: InputFilterComponent
},
filterFunction: (name: string) => {
this.setFilter({ field: 'name', search: name });
}
},
members: {
title: this.getTranslation('ORGANIZATIONS_PAGE.EDIT.TEAMS_PAGE.MEMBERS'),
type: 'custom',
isFilterable: false,
renderComponent: EmployeeWithLinksComponent,
componentInitFunction: (instance: EmployeeWithLinksComponent, cell: Cell) => {
instance.rowData = cell.getRow().getData();
instance.value = cell.getRawValue();
}
},
primaryPhone: {
title: this.getTranslation('CONTACTS_PAGE.PHONE'),
type: 'string',
filter: {
type: 'custom',
component: InputFilterComponent
},
filterFunction: (primaryPhone: string) => {
this.setFilter({ field: 'primaryPhone', search: primaryPhone });
}
},
primaryEmail: {
title: this.getTranslation('CONTACTS_PAGE.EMAIL'),
type: 'string',
filter: {
type: 'custom',
component: InputFilterComponent
},
filterFunction: (primaryEmail: string) => {
this.setFilter({
field: 'primaryEmail',
search: primaryEmail
});
}
},
projects: {
title: this.getTranslation('CONTACTS_PAGE.PROJECTS'),
type: 'custom',
isFilterable: false,
renderComponent: ProjectComponent,
componentInitFunction: (instance: ProjectComponent, cell: Cell) => {
instance.rowData = cell.getRow().getData();
instance.value = cell.getValue();
}
},
country: {
title: this.getTranslation('CONTACTS_PAGE.COUNTRY'),
type: 'string',
isFilterable: false,
valuePrepareFunction: (value: string) => this.getCountry(value)
},
city: {
title: this.getTranslation('CONTACTS_PAGE.CITY'),
type: 'string',
isFilterable: false
}
}
};
}
private getNoDataMessage() {
let noDataMessage: string;
switch (this.contactType) {
case ContactType.CLIENT:
noDataMessage = this.getTranslation('SM_TABLE.NO_DATA.CLIENT');
break;
case ContactType.LEAD:
noDataMessage = this.getTranslation('SM_TABLE.NO_DATA.LEAD');
break;
default:
noDataMessage = this.getTranslation('SM_TABLE.NO_DATA.CONTACT');
break;
}
return noDataMessage;
}
public onUpdateResult(params: any) {
if (params) this.invite(params);
}
selectContact({ isSelected, data }) {
this.disableButton = !isSelected;
this.selectedOrganizationContact = isSelected ? data : null;
}
async removeOrganizationContact(id?: string, name?: string) {
const result = await firstValueFrom(
this.dialogService.open(DeleteConfirmationComponent, {
context: {
recordType: 'Contact'
}
}).onClose
);
if (result) {
await this.organizationContactService.delete(
this.selectedOrganizationContact ? this.selectedOrganizationContact.id : id
);
this.toastrService.success('NOTES.ORGANIZATIONS.EDIT_ORGANIZATIONS_CONTACTS.REMOVE_CONTACT', {
name: this.selectedOrganizationContact ? this.selectedOrganizationContact.name : name
});
this._refresh$.next(true);
this.contacts$.next(true);
}
}
setView() {
switch (this.contactType) {
case ContactType.CLIENT:
this.viewComponentName = ComponentEnum.CLIENTS;
break;
case ContactType.LEAD:
this.viewComponentName = ComponentEnum.LEADS;
break;
default:
this.viewComponentName = ComponentEnum.CUSTOMERS;
break;
}
this.store
.componentLayout$(this.viewComponentName)
.pipe(
distinctUntilChange(),
tap((componentLayout) => (this.dataLayoutStyle = componentLayout)),
tap(() => this.refreshPagination()),
filter((componentLayout) => componentLayout === ComponentLayoutStyleEnum.CARDS_GRID),
tap(() => (this.organizationContacts = [])),
tap(() => this.contacts$.next(true)),
untilDestroyed(this)
)
.subscribe();
}
/**
* Manages adding or updating an organization contact and displays relevant notifications.
*
* @param organizationContact
* @returns
*/
public async addOrEditOrganizationContact(organizationContact: IOrganizationContactCreateInput) {
if (!this.organization) {
return;
}
const contact: IContactCreateInput = this.extractLocation(organizationContact.contact);
const request = {
...organizationContact,
contact
};
try {
if (organizationContact.name) {
const { name } = organizationContact;
if (organizationContact.id) {
await this.organizationContactService.update(organizationContact.id, request);
this.toastrService.success('NOTES.ORGANIZATIONS.EDIT_ORGANIZATIONS_CONTACTS.UPDATE_CONTACT', {
name
});
} else {
await this.organizationContactService.create(request);
this.toastrService.success('NOTES.ORGANIZATIONS.EDIT_ORGANIZATIONS_CONTACTS.ADD_CONTACT', { name });
}
this.showAddCard = !this.showAddCard;
this._refresh$.next(true);
this.contacts$.next(true);
} else {
this.toastrService.danger('NOTES.ORGANIZATIONS.EDIT_ORGANIZATIONS_CONTACTS.INVALID_CONTACT_DATA');
}
} catch (error) {
this.toastrService.danger(error);
}
}
/**
* A contact object with organization and tenant details from the current organization context.
*
* @param contact The contact object to be enriched.
* @returns An enriched contact object containing location details, organization, and tenant information.
*/
private extractLocation(contact: IContactCreateInput): IContactCreateInput | undefined {
if (!this.organization) {
return;
}
const { id: organizationId, tenantId } = this.organization;
return {
...contact,
organizationId,
organization: { id: organizationId },
tenantId,
tenant: { id: tenantId }
};
}
/*
* Register Smart Table Source Config
*/
setSmartTableSource() {
if (!this.organization) {
return;
}
const { tenantId } = this.store.user;
const { id: organizationId } = this.organization;
try {
this.smartTableSource = new ServerDataSource(this.http, {
endPoint: `${API_PREFIX}/organization-contact/pagination`,
relations: ['projects.members', 'members.user', 'tags', 'contact'],
join: {
alias: 'organization_contact',
leftJoin: {
members: 'organization_contact.members'
}
},
where: {
organizationId,
tenantId,
contactType: this.contactType,
...(this.selectedEmployeeId
? {
members: [this.selectedEmployeeId]
}
: {}),
...(this.filters.where ? this.filters.where : {})
},
resultMap: (contact: IOrganizationContact) => {
return Object.assign({}, contact, {
country: contact.contact ? contact.contact.country : '',
city: contact.contact ? contact.contact.city : '',
street: contact.contact ? contact.contact.address : '',
street2: contact.contact ? contact.contact.address2 : '',
postcode: contact.contact ? contact.contact.postcode : null,
fax: contact.contact ? contact.contact.fax : '',
website: contact.contact ? contact.contact.website : '',
fiscalInformation: contact.contact ? contact.contact.fiscalInformation : ''
});
},
finalize: () => {
this.setPagination({
...this.getPagination(),
totalItems: this.smartTableSource.count()
});
this.loading = false;
}
});
} catch (error) {
this.toastrService.danger(this.getTranslation('TOASTR.TITLE.ERROR'));
}
}
private async getContacts() {
if (!this.organization) {
return;
}
try {
this.setSmartTableSource();
const { activePage, itemsPerPage } = this.getPagination();
this.smartTableSource.setPaging(activePage, itemsPerPage, false);
await this._loadGridLayoutData();
} catch (error) {
this.toastrService.danger(error);
}
}
private async _loadGridLayoutData() {
if (this.dataLayoutStyle === ComponentLayoutStyleEnum.CARDS_GRID) {
this.organizationContacts.push(...(await this.smartTableSource.getElements()));
}
}
private async loadProjectsWithoutOrganizationContacts() {
if (!this.organization) {
return;
}
this.loading = true;
const { tenantId } = this.store.user;
const { id: organizationId } = this.organization;
try {
const { items } = await this.organizationProjectsService.getAll(['organizationContact'], {
organizationId,
tenantId,
organizationContactId: null
});
this.projectsWithoutOrganizationContacts = items;
} catch (error) {
this.toastrService.danger(this.getTranslation('TOASTR.TITLE.ERROR'));
}
this.cd.detectChanges();
}
cancel() {
this.loading = true;
this.selectedOrganizationContact = null;
this.showAddCard = !this.showAddCard;
}
async editOrganizationContact(organizationContact: IOrganizationContact) {
await this.loadProjectsWithoutOrganizationContacts();
this.selectedOrganizationContact = organizationContact;
this.showAddCard = true;
this.loading = false;
}
async add() {
this.selectedOrganizationContact = null;
this.showAddCard = true;
}
/**
* Redirect contact/client/customer to view page
*
* @returns
*/
navigateToContact(selectedItem?: IContact) {
if (selectedItem) {
this.selectContact({
isSelected: true,
data: selectedItem
});
}
if (!this.selectedOrganizationContact) {
return;
}
const { id } = this.selectedOrganizationContact;
this._router.navigate([`/pages/contacts/view`, id]);
}
async invite(selectedOrganizationContact?: IOrganizationContact) {
try {
const { id: organizationId } = this.organization;
const dialog = this.dialogService.open(InviteContactComponent, {
context: {
organizationId,
organizationContact: selectedOrganizationContact,
contactType: this.contactType,
selectedOrganization: this.organization
}
});
const result = await firstValueFrom(dialog.onClose);
if (result) {
this._refresh$.next(true);
this.contacts$.next(true);
this.toastrService.success('NOTES.ORGANIZATIONS.EDIT_ORGANIZATIONS_CONTACTS.INVITE_CONTACT', {
name: result.name
});
}
} catch (error) {
this.toastrService.danger('NOTES.ORGANIZATIONS.EDIT_ORGANIZATIONS_CONTACTS.INVITE_CONTACT_ERROR');
}
}
public _applyTranslationOnSmartTable() {
this.translateService.onLangChange
.pipe(
tap(() => this._loadSmartTableSettings()),
untilDestroyed(this)
)
.subscribe();
}
/*
* Clear selected item
*/
clearItem() {
this.selectContact({
isSelected: false,
data: null
});
}
/**
* Returns the country name based on the ISO code.
*
* @param isoCode - ISO code of the country.
* @returns The country name or null if not found.
*/
getCountry(isoCode: ICountry['isoCode']): string | null {
// Logic to find the country based on the ISO code
const country = this.countries.find((item) => item.isoCode === isoCode);
// Return the country name if found, otherwise return null
return country?.country ?? null;
}
ngOnDestroy(): void {}
}