diff --git a/src/frontend/app/app-common.module.ts b/src/frontend/app/app-common.module.ts index e146cac93b0a75acb6a953a4383e33aebce4f479..3fcb0be54a81f5b544a173dff8492e649f6eb471 100755 --- a/src/frontend/app/app-common.module.ts +++ b/src/frontend/app/app-common.module.ts @@ -59,6 +59,7 @@ import { PluginSelectSearchComponent } from '../plugins/s import { FolderInputComponent } from '../app/folder/indexing/folder-input.component'; import { TagInputComponent } from '../app/tag/indexing/tag-input.component'; import { DragDropDirective } from '../app/viewer/upload-file-dnd.directive'; +import { ContactAutocompleteComponent } from './contact/autocomplete/contact-autocomplete.component'; import { DiffusionsListComponent } from './diffusions/diffusions-list.component'; @@ -118,7 +119,8 @@ export class MyHammerConfig extends HammerGestureConfig { DiffusionsListComponent, DocumentViewerComponent, DragDropDirective, - EcplOnlyofficeViewerComponent + EcplOnlyofficeViewerComponent, + ContactAutocompleteComponent ], exports: [ CommonModule, @@ -158,7 +160,8 @@ export class MyHammerConfig extends HammerGestureConfig { DiffusionsListComponent, DocumentViewerComponent, DragDropDirective, - EcplOnlyofficeViewerComponent + EcplOnlyofficeViewerComponent, + ContactAutocompleteComponent ], providers: [ HeaderService, diff --git a/src/frontend/app/contact/autocomplete/contact-autocomplete.component.html b/src/frontend/app/contact/autocomplete/contact-autocomplete.component.html new file mode 100644 index 0000000000000000000000000000000000000000..257770974d29766614c45c3e48f1319513c08247 --- /dev/null +++ b/src/frontend/app/contact/autocomplete/contact-autocomplete.component.html @@ -0,0 +1,115 @@ +<form> + <input type="hidden" [formControl]="controlAutocomplete"> + <mat-form-field floatLabel="never" class="input-form" *ngIf="!controlAutocomplete.disabled"> + <mat-icon color="primary" class="fa fa-search" matPrefix style="padding-left: 20px;font-size: 15px;"></mat-icon> + <input type="text" #autoCompleteInput [placeholder]="lang.searchContact" matInput [formControl]="myControl" + [matAutocomplete]="auto" (click)="$event.stopPropagation()" (focus)="resetAutocomplete()" maxlength="128"> + <button [disabled]="!canAdd" type="button" matSuffix mat-icon-button (click)="addItem()"> + <mat-icon class="fa fa-plus" [title]="lang.add" + [style.visibility]="canAdd && myControl.value !== null && myControl.value.length > 0 ? 'visible' : 'hidden'"> + </mat-icon> + </button> + <mat-autocomplete #auto="matAutocomplete" (optionSelected)="selectOpt($event)"> + <ng-container *ngIf="options.length > 0 && !loading"> + <mat-option *ngFor="let option of filteredOptions | async" [value]="option"> + <mat-card> + <mat-card-header class="contact-header"> + <div mat-card-avatar class="contact-header-image fa" + [class.fa-building]="option.type === 'contact'" + [class.fa-sitemap]="option.type ==='entity'" [class.fa-user]="option.type ==='user'" + [title]="lang['contact_'+option.type]"> + </div> + <mat-card-title [title]="option.firstname + ' ' + option.lastname">{{option.firstname}} + {{option.lastname}}</mat-card-title> + <mat-card-subtitle [title]="option.function" *ngIf="!empty(option.function)"> + {{option.function}} + </mat-card-subtitle> + <i class="contact-filling fa fa-circle" + *ngIf="option.type === 'contact' && option.fillingRate !== undefined" + [style.color]="option.fillingRate.color" + [title]="lang.contactsFillingRate + ' : ' + option.fillingRate.rate + '%'"></i> + </mat-card-header> + <mat-card-content> + <mat-list> + <mat-list-item class="contact-item" *ngIf="!empty(option.company)"> + <mat-icon mat-list-icon class="contact-group far fa-building" + [title]="lang.contactsParameters_company"></mat-icon> + <p mat-line class="contact-content" [title]="option.company"> {{option.company}} + </p> + </mat-list-item> + <mat-list-item class="contact-item" *ngIf="!empty(option.department)"> + <mat-icon mat-list-icon class="contact-group fa fa-sitemap" + [title]="lang.contactsParameters_department"></mat-icon> + <p mat-line class="contact-content" [title]="option.department"> + {{option.department}} </p> + <p mat-line class="contact-content" *ngIf="!empty(option.occupancy)" + [title]="option.occupancy"> ({{option.occupancy}}) </p> + </mat-list-item> + <mat-list-item class="contact-item" *ngIf="!empty(option.email)"> + <mat-icon mat-list-icon class="contact-group far fa-envelope" [title]="lang.email"> + </mat-icon> + <p mat-line class="contact-content" [title]="option.email"> {{option.email}} </p> + </mat-list-item> + <mat-list-item class="contact-item" *ngIf="!empty(option.phone)"> + <mat-icon mat-list-icon class="contact-group fas fa-phone" + [title]="lang.phoneNumber"> + </mat-icon> + <p mat-line class="contact-content" [title]="option.phone"> {{option.phone}} </p> + </mat-list-item> + <mat-list-item class="contact-address" [title]="lang.address" + *ngIf="!empty(option.number) || !empty(option.street) || !empty(option.complement) || !empty(option.postalCode) || !empty(option.town) || !empty(option.country)"> + <mat-icon mat-list-icon class="contact-group fas fa-map-marker-alt"></mat-icon> + <p mat-line class="contact-content" + *ngIf="!empty(option.number) || !empty(option.street)" [title]="option.street"> + {{option.number}} {{option.street}} </p> + <p mat-line class="contact-content" *ngIf="!empty(option.complement)" + [title]="option.complement"> ({{option.complement}}) </p> + <p mat-line class="contact-content" + *ngIf="!empty(option.postalCode) || !empty(option.town)" + [title]="option.postalCode + ' ' + option.town"> {{option.postalCode}} + {{option.town}} </p> + <p mat-line class="contact-content" *ngIf="!empty(option.country)" + [title]="option.country"> {{option.country}} </p> + </mat-list-item> + </mat-list> + </mat-card-content> + </mat-card> + </mat-option> + </ng-container> + <mat-option class="autoCompleteInfoResult smallInputInfo" *ngIf="options.length === 0 && !loading" disabled + [innerHTML]="listInfo"> + </mat-option> + <mat-option *ngIf="loading" disabled> + <mat-spinner diameter="20"></mat-spinner> + </mat-option> + </mat-autocomplete> + </mat-form-field> + <div class="itemList"> + <mat-chip-list *ngIf="controlAutocomplete.value.length > 0" class="mat-chip-list-stacked itemChip" + color="default"> + <ng-container *ngIf="!loadingValues"> + <mat-chip *ngFor="let item of controlAutocomplete.value;let i=index" class="listAutocomplete" + color="default" [removable]="!controlAutocomplete.disabled" (removed)="removeItem(i)"> + <span style="display: flex;flex: 1;align-items: center;"> + <i class="fa" [class.fa-building]="this.valuesToDisplay[item.id].type === 'contact'" + [class.fa-sitemap]="this.valuesToDisplay[item.id].type ==='entity'" + [class.fa-user]="this.valuesToDisplay[item.id].type ==='user'" + [title]="lang['contact_'+this.valuesToDisplay[item.id].type]" + style="padding-right:5px;"></i> + <ng-container *ngIf="!empty(this.valuesToDisplay[item.id].firstname)"> + {{this.valuesToDisplay[item.id].firstname}} + </ng-container> + + {{this.valuesToDisplay[item.id].lastname}} <ng-container + *ngIf="!empty(this.valuesToDisplay[item.id].company)"> + ({{this.valuesToDisplay[item.id].company}})</ng-container> + </span> + <mat-icon matChipRemove class="fa fa-times" *ngIf="!controlAutocomplete.disabled"></mat-icon> + </mat-chip> + </ng-container> + </mat-chip-list> + <div class="noResult" *ngIf="controlAutocomplete.value.length === 0"> + {{lang.noSelectedContact}} + </div> + </div> +</form> \ No newline at end of file diff --git a/src/frontend/app/contact/autocomplete/contact-autocomplete.component.scss b/src/frontend/app/contact/autocomplete/contact-autocomplete.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..6d822ba97d6c3ef7df99198d5c1e0fb0798fa676 --- /dev/null +++ b/src/frontend/app/contact/autocomplete/contact-autocomplete.component.scss @@ -0,0 +1,149 @@ +@import '../../../css/vars.scss'; + +.smallInputInfo { + font-size: 9px; + white-space: normal; + line-height: 13px; + display: table-cell; + vertical-align: middle; + text-align: center; +} + +.mat-chip.mat-standard-chip { + color: white; + background-color: lighten($primary,10%); +} + +.noResult { + text-align: center; + font-style: italic; + opacity: 0.5; +} + +.listAutocomplete { + height: auto; +} + +.itemChip { + display: block; + width: 95%; + + ::ng-deep.mat-chip-list-wrapper { + margin: 0px; + } +} + +.mat-card { + padding: 0px; + margin: 5px; +} + +.mat-card:hover { + box-shadow: inset 0px 0px 5px 0px rgba(0,0,0,0.12); +} + +.itemList { + padding-top: 10px; + overflow-x: hidden; + max-height: 165px; +} + +.mat-list { + padding: 0px; +} + +.mat-option:not(.smallInputInfo) { + white-space: initial; + line-height: initial; + height: auto; + padding: 0px; +} + +.mat-option:hover { + background: initial; +} + +.contact-card { + width: 100%; + margin-top: 10px; + margin-bottom: 10px; + padding: 0px; + box-shadow: none; + border: solid 1px rgba(0, 0, 0, 0.12); +} + +.mat-card-header { + background: #F9F9F9; + padding: 10px; + padding-bottom: 5px; +} + +.contact-header { + ::ng-deep.mat-card-header-text { + overflow: hidden; + text-overflow: ellipsis; + } +} + + + +.mat-card-subtitle { + white-space: pre; + text-overflow: ellipsis; + overflow: hidden; +} + +.contact-item { + height: 20px !important; +} + +.mat-card-title { + font-size: 13px; +} + +.mat-card-subtitle { + font-size: 10px; + margin-bottom: 0px; +} + + +.contact-header-image { + background: $primary; + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + color: white; + height: 24px; + width: 24px; +} + +.contact-group { + color: $primary; + font-size: 10px !important; + display: flex; + align-items: center; + justify-content: center; + padding: 0px !important; + margin: 0; +} +.mat-list-item-content { + padding: 0px; +} + +.contact-content { + font-size: 10px !important; + color: #337ab7; +} + +.contact-address { + ::ng-deep.mat-list-item-content { + padding-top: 0px !important; + padding-bottom: 0px !important; + } +} + +.contact-filling { + position: absolute; + right: 8px; +} \ No newline at end of file diff --git a/src/frontend/app/contact/autocomplete/contact-autocomplete.component.ts b/src/frontend/app/contact/autocomplete/contact-autocomplete.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..e198a6a78d608dbf1d0ce5a9a71bad389ecd2b22 --- /dev/null +++ b/src/frontend/app/contact/autocomplete/contact-autocomplete.component.ts @@ -0,0 +1,208 @@ +import { Component, OnInit, Input, ViewChild, ElementRef } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { LANG } from '../../translate.component'; +import { NotificationService } from '../../notification.service'; +import { HeaderService } from '../../../service/header.service'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { AppService } from '../../../service/app.service'; +import { SortPipe } from '../../../plugins/sorting.pipe'; +import { FormControl } from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { debounceTime, filter, distinctUntilChanged, tap, switchMap, exhaustMap, catchError } from 'rxjs/operators'; +import { LatinisePipe } from 'ngx-pipes'; +import { PrivilegeService } from '../../../service/privileges.service'; + +@Component({ + selector: 'app-contact-autocomplete', + templateUrl: "contact-autocomplete.component.html", + styleUrls: [ + 'contact-autocomplete.component.scss', + '../../indexation/indexing-form/indexing-form.component.scss' + ], + providers: [NotificationService, AppService, SortPipe] +}) + +export class ContactAutocompleteComponent implements OnInit { + + lang: any = LANG; + + loading: boolean = false; + loadingValues: boolean = true; + + key: string = 'id'; + + canAdd: boolean = false; + + listInfo: string; + myControl = new FormControl(); + filteredOptions: Observable<string[]>; + options: any; + valuesToDisplay: any = {}; + dialogRef: MatDialogRef<any>; + newIds: number[] = []; + + /** + * FormControl used when autocomplete is used in form and must be catched in a form control. + */ + @Input('control') controlAutocomplete: FormControl; + + @ViewChild('autoCompleteInput', { static: true }) autoCompleteInput: ElementRef; + + constructor( + public http: HttpClient, + private notify: NotificationService, + public dialog: MatDialog, + private headerService: HeaderService, + public appService: AppService, + private latinisePipe: LatinisePipe, + private privilegeService: PrivilegeService + ) { + + } + + ngOnInit() { + console.log(this.controlAutocomplete); + this.controlAutocomplete.setValue(this.controlAutocomplete.value === null || this.controlAutocomplete.value === '' ? [] : this.controlAutocomplete.value); + this.canAdd = this.privilegeService.hasCurrentUserPrivilege('manage_tags_application'); + this.initFormValue(); + this.initAutocompleteRoute(); + } + + initAutocompleteRoute() { + this.listInfo = this.lang.autocompleteInfo; + this.options = []; + this.myControl.valueChanges + .pipe( + //tap((value) => this.canAdd = value.length === 0 ? false : true), + debounceTime(300), + filter(value => value.length > 2), + distinctUntilChanged(), + tap(() => this.loading = true), + switchMap((data: any) => this.getDatas(data)), + tap((data: any) => { + if (data.length === 0) { + this.listInfo = this.lang.noAvailableValue; + } else { + this.listInfo = ''; + } + this.options = data; + this.filteredOptions = of(this.options); + this.loading = false; + }) + ).subscribe(); + } + + getDatas(data: string) { + return this.http.get('../../rest/autocomplete/all', { params: { "search": data } }); + } + + selectOpt(ev: any) { + this.setFormValue(ev.option.value); + this.myControl.setValue(''); + + } + + initFormValue() { + this.controlAutocomplete.value.forEach((contact: any) => { + this.http.get('../../rest/contacts/' + contact.id).pipe( + tap((data: any) => { + this.valuesToDisplay[data.id] = { + type: 'contact', + firstname: data.firstname, + lastname: data.lastname, + company: data.company + }; + this.loadingValues = false; + }) + ).subscribe(); + }); + } + + setFormValue(item: any) { + if (this.controlAutocomplete.value.map((contact: any) => contact.id).indexOf(item['id']) === -1) { + let arrvalue = []; + if (this.controlAutocomplete.value !== null) { + arrvalue = this.controlAutocomplete.value; + } + arrvalue.push( + { + type: item['type'], + id: item['id'] + + }); + this.valuesToDisplay[item['id']] = item; + this.controlAutocomplete.setValue(arrvalue); + this.loadingValues = false; + } + } + + resetAutocomplete() { + this.options = []; + this.listInfo = this.lang.autocompleteInfo; + } + + private _filter(value: string): string[] { + if (typeof value === 'string') { + const filterValue = this.latinisePipe.transform(value.toLowerCase()); + return this.options.filter((option: any) => this.latinisePipe.transform(option[this.key].toLowerCase()).includes(filterValue)); + } else { + return this.options; + } + } + + unsetValue() { + this.controlAutocomplete.setValue(''); + this.myControl.setValue(''); + this.myControl.enable(); + } + + removeItem(index: number) { + + if (this.newIds.indexOf(this.controlAutocomplete.value[index]) === -1) { + let arrValue = this.controlAutocomplete.value; + this.controlAutocomplete.value.splice(index, 1); + this.controlAutocomplete.setValue(arrValue); + } else { + this.http.delete('../../rest/tags/' + this.controlAutocomplete.value[index]).pipe( + tap((data: any) => { + let arrValue = this.controlAutocomplete.value; + this.controlAutocomplete.value.splice(index, 1); + this.controlAutocomplete.setValue(arrValue); + }), + catchError((err: any) => { + this.notify.handleErrors(err); + return of(false); + }) + ).subscribe(); + } + } + + addItem() { + const newElem = {}; + + newElem[this.key] = this.myControl.value; + + this.http.post('../../rest/tags', { label: newElem[this.key] }).pipe( + tap((data: any) => { + for (var key in data) { + newElem['id'] = data[key]; + this.newIds.push(data[key]); + } + this.setFormValue(newElem); + this.myControl.setValue(''); + }), + catchError((err: any) => { + this.notify.handleErrors(err); + return of(false); + }) + ).subscribe(); + } + + empty(value: any) { + if (value !== null && value !== '' && value !== undefined) { + return false; + } else { + return true; + } + } +} \ No newline at end of file diff --git a/src/frontend/app/indexation/indexing-form/indexing-form.component.html b/src/frontend/app/indexation/indexing-form/indexing-form.component.html index 4b08bb72b1511c67d25ba81c8189defc5f2dc39c..f3e33915f77e0f4b7c66cf05794daf94ca87d71a 100644 --- a/src/frontend/app/indexation/indexing-form/indexing-form.component.html +++ b/src/frontend/app/indexation/indexing-form/indexing-form.component.html @@ -142,6 +142,11 @@ [manageDatas]="!adminMode ? field.values[1] : undefined" style="width:100%;"> </plugin-autocomplete> </ng-container>--> + + <ng-container *ngIf="field.identifier === 'senders'"> + <app-contact-autocomplete [control]="arrFormControl[field.identifier]" style="width:100%;"> + </app-contact-autocomplete> + </ng-container> <ng-container *ngIf="field.identifier === 'folders'"> <app-folder-input [control]="arrFormControl[field.identifier]" style="width:100%;"> </app-folder-input> diff --git a/src/frontend/app/indexation/indexing-form/indexing-form.component.ts b/src/frontend/app/indexation/indexing-form/indexing-form.component.ts index ba29355f7be6c598b83ccee03f245a01eca3ba5b..95780b68e42aad7b11dd3de7cf0d1829a7cb34e0 100644 --- a/src/frontend/app/indexation/indexing-form/indexing-form.component.ts +++ b/src/frontend/app/indexation/indexing-form/indexing-form.component.ts @@ -74,7 +74,7 @@ export class IndexingFormComponent implements OnInit { availableFields: any[] = [ { - identifier: 'getRecipients', + identifier: 'recipients', label: this.lang.getRecipients, type: 'autocomplete', default_value: null, diff --git a/src/frontend/app/tag/indexing/tag-input.component.html b/src/frontend/app/tag/indexing/tag-input.component.html index 0f80b2880a6ba35d33373f6a1b0b74dbd44b7a6c..ec41f158c6ee582924d17e9841f95513ca856ff8 100644 --- a/src/frontend/app/tag/indexing/tag-input.component.html +++ b/src/frontend/app/tag/indexing/tag-input.component.html @@ -28,7 +28,7 @@ <span style="display: flex;flex: 1;align-items: center;" [title]="this.valuesToDisplay[item]"> <i class="fa fa-tag" style="padding-right:5px;"></i> {{this.valuesToDisplay[item]}} </span> - <mat-icon matChipRemove class="fa fa-times"></mat-icon> + <mat-icon matChipRemove class="fa fa-times" *ngIf="!controlAutocomplete.disabled"></mat-icon> </mat-chip> </mat-chip-list> <div class="noResult" *ngIf="controlAutocomplete.value.length === 0"> diff --git a/src/frontend/lang/lang-en.ts b/src/frontend/lang/lang-en.ts index 3483611cba1061b4e0d937d109944489793be99b..89e2d8c82f173e95dcedaf782b30da379253ff90 100755 --- a/src/frontend/lang/lang-en.ts +++ b/src/frontend/lang/lang-en.ts @@ -143,7 +143,7 @@ export const LANG_EN = { "checkSendmail" : "Check mail server status", "chooseAnotherEntityUser" : "Choose another person from the entity", "chooseBasket" : "Choose a basket", - "chooseCategoryAssociation" : "Choose one or some associatedd categories", + "chooseCategoryAssociation" : "Choose one or some associated categories", "chooseColor" : "Choose a color", "chooseContactType" : "Choose a contact type", "chooseDefaultAction" : "Choose main action", @@ -1327,4 +1327,6 @@ export const LANG_EN = { "siretGeneratedButAnnuaryUnreachable": "SIRET number generated but annuary is unreachable", "entityDeletedButAnnuaryUnreachable": "Entity deleted but annuary is unreachable", "langISO": "en-US", + "noSelectedContact": "No associated contact", + "searchContact": "Search a contact", }; diff --git a/src/frontend/lang/lang-fr.ts b/src/frontend/lang/lang-fr.ts index 4ceaddb50ef01e8aca63dcedce4e80bfd6680702..1eeda4fe956541720726a5e853eb6ef871230ca0 100755 --- a/src/frontend/lang/lang-fr.ts +++ b/src/frontend/lang/lang-fr.ts @@ -1365,4 +1365,6 @@ export const LANG_FR = { "siretGeneratedButAnnuaryUnreachable": "Numéro SIRET généré mais l'annuaire est injoignable", "entityDeletedButAnnuaryUnreachable": "Entité supprimée mais l'annuaire est injoignable, l'entité est possiblement toujours présente dedans", "langISO": "fr-FR", + "noSelectedContact": "Aucun contact associé", + "searchContact": "Recherchez un contact", }; diff --git a/src/frontend/lang/lang-nl.ts b/src/frontend/lang/lang-nl.ts index 2a79d0b0027aded4cf6b52ec67a367fceed8c19e..586b6efea56c62bc9ce20bc3681e9b350faaf630 100755 --- a/src/frontend/lang/lang-nl.ts +++ b/src/frontend/lang/lang-nl.ts @@ -1352,4 +1352,6 @@ export const LANG_NL = { "siretGeneratedButAnnuaryUnreachable": "SIRET number generated but annuary is unreachable",//_TO_TRANSLATE "entityDeletedButAnnuaryUnreachable": "Entity deleted but annuary is unreachable",//_TO_TRANSLATE "langISO": "nl-FR", + "noSelectedContact": "No associated contact", //_TO_TRANSLATE + "searchContact": "Search a contact", //_TO_TRANSLATE };