diff --git a/src/frontend/app/administration/administration.module.ts b/src/frontend/app/administration/administration.module.ts index aeacf866e6f77235fba83fa75ad6ae437a1fde63..4a76714578f744f47acda5f5b386110662507d59 100755 --- a/src/frontend/app/administration/administration.module.ts +++ b/src/frontend/app/administration/administration.module.ts @@ -21,6 +21,7 @@ import { BasketAdministrationComponent, BasketAdministrationSettingsModalCompone import { BasketsAdministrationComponent } from './basket/baskets-administration.component'; import { ContactDuplicateComponent } from './contact/contact-duplicate/contact-duplicate.component'; import { ContactExportComponent } from './contact/list/export/contact-export.component'; +import { ContactImportComponent } from './contact/list/import/contact-import.component'; import { ContactsCustomFieldsAdministrationComponent } from './contact/customField/contacts-custom-fields-administration.component'; import { ContactsGroupAdministrationComponent } from './contact/group/contacts-group-administration.component'; import { ContactsGroupsAdministrationComponent } from './contact/group/contacts-groups-administration.component'; @@ -99,6 +100,7 @@ import { RegisteredMailListComponent } from './registered-mail/registered-mail-l BasketsAdministrationComponent, ContactDuplicateComponent, ContactExportComponent, + ContactImportComponent, ContactsCustomFieldsAdministrationComponent, ContactsGroupAdministrationComponent, ContactsGroupsAdministrationComponent, @@ -164,6 +166,7 @@ import { RegisteredMailListComponent } from './registered-mail/registered-mail-l BasketAdministrationGroupListModalComponent, BasketAdministrationSettingsModalComponent, ContactExportComponent, + ContactImportComponent, ContactsListAdministrationRedirectModalComponent, DoctypesAdministrationRedirectModalComponent, EntitiesAdministrationRedirectModalComponent, diff --git a/src/frontend/app/administration/contact/list/contacts-list-administration.component.html b/src/frontend/app/administration/contact/list/contacts-list-administration.component.html index 3eab432026791e590e90bae0e629a2237735c8aa..3be7f487efb5d14d948fd2ea4285aba9aeb36a14 100755 --- a/src/frontend/app/administration/contact/list/contacts-list-administration.component.html +++ b/src/frontend/app/administration/contact/list/contacts-list-administration.component.html @@ -14,6 +14,12 @@ {{'lang.exportContacts' | translate}} </p> </a> + <a mat-list-item (click)="openContactImportModal()"> + <mat-icon color="primary" mat-list-icon class="fas fa-file-import"></mat-icon> + <p mat-line> + {{'lang.importContacts' | translate}} + </p> + </a> </mat-nav-list> <mat-divider></mat-divider> <mat-nav-list> diff --git a/src/frontend/app/administration/contact/list/contacts-list-administration.component.ts b/src/frontend/app/administration/contact/list/contacts-list-administration.component.ts index b19e7de83e956f6f0ac19131a23f61b78b88e585..e75a60957ec5fdd2ca7371f0302f0e43418180cd 100644 --- a/src/frontend/app/administration/contact/list/contacts-list-administration.component.ts +++ b/src/frontend/app/administration/contact/list/contacts-list-administration.component.ts @@ -15,6 +15,7 @@ import { FormControl } from '@angular/forms'; import { FunctionsService } from '../../../../service/functions.service'; import { ContactExportComponent } from './export/contact-export.component'; import { AdministrationService } from '../../../../app/administration/administration.service'; +import { ContactImportComponent } from './import/contact-import.component'; @Component({ selector: 'contact-list', @@ -229,6 +230,26 @@ export class ContactsListAdministrationComponent implements OnInit { this.dialog.open(ContactExportComponent, { panelClass: 'maarch-modal', width: '800px', autoFocus: false }); } + openContactImportModal() { + const dialogRef = this.dialog.open(ContactImportComponent, { + disableClose: true, + width: '99vw', + maxWidth: '99vw', + panelClass: 'maarch-full-height-modal' + }); + + dialogRef.afterClosed().pipe( + filter((data: any) => data === 'success'), + tap(() => { + this.refreshDao(); + }), + catchError((err: any) => { + this.notify.handleSoftErrors(err); + return of(false); + }) + ).subscribe(); + } + refreshDao() { this.filtersChange.emit(); } diff --git a/src/frontend/app/administration/contact/list/import/contact-import.component.html b/src/frontend/app/administration/contact/list/import/contact-import.component.html new file mode 100644 index 0000000000000000000000000000000000000000..e225e162354a03012fe27ea6ed785ddc88574235 --- /dev/null +++ b/src/frontend/app/administration/contact/list/import/contact-import.component.html @@ -0,0 +1,91 @@ +<div class="mat-dialog-content-container"> + <h1 mat-dialog-title>{{'lang.contactsImport' | translate}}</h1> + <div mat-dialog-content> + <mat-toolbar class="import-tool" [class.hide]="csvData.length === 0 || loading"> + <span style="flex:1;"> + <mat-slide-toggle color="primary" [checked]="hasHeader" (change)="toggleHeader()">En-tête csv + </mat-slide-toggle> + </span> + <span style="flex:1;text-align: center;"><i class="fa fa-users" + color="primary"></i> {{'lang.contactsOfFile' | translate}} : <b color="primary">{{countAll}}</b> + </span> + <span style="flex:1;text-align: center;"><i class="fa fa-user-plus" + color="primary"></i> {{'lang.additions' | translate}} : <b + color="primary">{{countAdd}}</b></span> + <span style="flex:1;text-align: right;"><i class="fa fa-user-edit" + color="primary"></i> {{'lang.modifications' | translate}} : <b + color="primary">{{countUp}}</b></span> + </mat-toolbar> + <ng-container *ngIf="loading; else elseTemplate"> + <div class="loader"> + <mat-spinner></mat-spinner> + </div> + </ng-container> + <ng-template #elseTemplate> + <input type="file" name="files[]" #uploadFile id="uploadFile" (change)="uploadCsv($event)" accept=".csv" + style="display: none;"> + <div *ngIf="csvData.length === 0" appUploadFileDragDrop (click)="uploadFile.click()" + (onFileDropped)="dndUploadFile($event)" class="dndFile"> + <div> + {{'lang.dndFileCsvDesc' | translate}} + <mat-form-field appearance="outline" style="font-size:14px;" (click)="$event.stopPropagation()"> + <mat-label>{{'lang.delimiter' | translate}}</mat-label> + <mat-select [(ngModel)]="currentDelimiter" (click)="$event.stopPropagation()"> + <mat-option *ngFor="let delimiter of delimiters" [value]="delimiter"> + {{delimiter === '\t' ? 'TAB' : delimiter}} + </mat-option> + </mat-select> + </mat-form-field> + </div> + </div> + <div class="row" style="margin: 0px;"> + <div class="col-md-12"> + <mat-paginator #paginator [length]="100" [hidePageSize]="true" [pageSize]="10"> + </mat-paginator> + </div> + </div> + <div style="width:100%;box-shadow: inset 0px 0px 5px 0px rgba(0,0,0,0.75);padding:10px;"> + <div style="overflow: auto;"> + <mat-table *ngIf="csvData.length > 0" #table [dataSource]="dataSource" style="width:5000px;"> + <ng-container *ngFor="let column of contactColumns;let i=index;"> + <ng-container [matColumnDef]="column"> + <mat-header-cell *matHeaderCellDef> + <i class="fas fa-database" color="primary" + [title]="'lang.dbColumn' | translate"></i> <b color="primary" + [title]="'lang.dbColumn' | translate">{{column}}</b> + <i class="fas fa-arrows-alt-h"></i> + <i class="fas fa-file-csv" [title]="'lang.csvColumn' | translate"></i> + <mat-form-field [title]="'lang.csvColumn' | translate" + (click)="$event.stopPropagation()" style="width: 80px !important;"> + <mat-select [(ngModel)]="associatedColmuns[column]" + (selectionChange)="changeColumn(column, $event.value)"> + <mat-option value=""></mat-option> + <mat-option *ngFor="let col of csvColumns" [value]="col"> + {{col}} + </mat-option> + </mat-select> + </mat-form-field> + </mat-header-cell> + <mat-cell *matCellDef="let element"> + {{element[column]}} + </mat-cell> + </ng-container> + </ng-container> + <mat-header-row *matHeaderRowDef="contactColumns"></mat-header-row> + <mat-row *matRowDef="let row; columns: contactColumns;"></mat-row> + </mat-table> + </div> + </div> + <div class="alert-message alert-message-info" [innerHTML]="'lang.infoImportcontacts' | translate" + style="min-width: 100%"> + </div> + </ng-template> + </div> + <span class="divider-modal"></span> + <div mat-dialog-actions class="actions"> + <button mat-raised-button mat-button *ngIf="csvData.length > 0" color="primary" [disabled]="loading" + (click)="onSubmit()">{{'lang.validate' | translate}}</button> + <button mat-raised-button mat-button [disabled]="loading" + [mat-dialog-close]="">{{'lang.cancel' | translate }}</button> + </div> +</div> \ No newline at end of file diff --git a/src/frontend/app/administration/contact/list/import/contact-import.component.scss b/src/frontend/app/administration/contact/list/import/contact-import.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..a5f90325d237737ed4446eb7f3c98013bc28d3b3 --- /dev/null +++ b/src/frontend/app/administration/contact/list/import/contact-import.component.scss @@ -0,0 +1,27 @@ +@import '../../../../../css/vars.scss'; + +.loader { + display: flex; + height: 100%; + align-items: center; + justify-content: center; +} + +.hide { + display: none; +} + +.import-tool { + font-size: 14px; +} + +.dndFile { + height: 100%; + display: flex; + align-items: center; + margin: 0px; + justify-content: center; + font-size: 30px; + opacity: 0.5 !important; + cursor: pointer; +} \ No newline at end of file diff --git a/src/frontend/app/administration/contact/list/import/contact-import.component.ts b/src/frontend/app/administration/contact/list/import/contact-import.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..6d0f243d463239dddf0c3c2092d2cf8c6f21fdf3 --- /dev/null +++ b/src/frontend/app/administration/contact/list/import/contact-import.component.ts @@ -0,0 +1,254 @@ +import { Component, OnInit, Inject, ViewChild } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { NotificationService } from '../../../../../service/notification/notification.service'; +import { MAT_DIALOG_DATA, MatDialogRef, MatDialog } from '@angular/material/dialog'; +import { TranslateService } from '@ngx-translate/core'; +import { MatTableDataSource } from '@angular/material/table'; +import { FunctionsService } from '../../../../../service/functions.service'; +import { ConfirmComponent } from '../../../../../plugins/modal/confirm.component'; +import { filter, exhaustMap, tap, catchError } from 'rxjs/operators'; +import { of } from 'rxjs/internal/observable/of'; +import { AlertComponent } from '../../../../../plugins/modal/alert.component'; +import { LocalStorageService } from '../../../../../service/local-storage.service'; +import { HeaderService } from '../../../../../service/header.service'; +import { MatPaginator } from '@angular/material/paginator'; + +@Component({ + templateUrl: 'contact-import.component.html', + styleUrls: ['contact-import.component.scss'] +}) +export class ContactImportComponent implements OnInit { + + loading: boolean = false; + contactColumns: string[] = [ + 'id', + 'company', + 'civility', + 'firstname', + 'lastname', + 'function', + 'department', + 'email', + 'addressAdditional1', + 'addressNumber', + 'addressStreet', + 'addressAdditional2', + 'addressPostcode', + 'addressTown', + 'addressCountry', + 'communicationMeans', + 'externalId_m2m', + ]; + + csvColumns: string[] = [ + + ]; + + delimiters = [';', ',', '\t']; + currentDelimiter = ';'; + + associatedColmuns: any = {}; + dataSource = new MatTableDataSource(null); + hasHeader: boolean = true; + csvData: any[] = []; + contactData: any[] = []; + countAll: number = 0; + countAdd: number = 0; + countUp: number = 0; + + @ViewChild(MatPaginator, { static: false }) paginator: MatPaginator; + + constructor( + public translate: TranslateService, + public http: HttpClient, + private notify: NotificationService, + private functionsService: FunctionsService, + private localStorage: LocalStorageService, + private headerService: HeaderService, + public dialog: MatDialog, + public dialogRef: MatDialogRef<ContactImportComponent>, + @Inject(MAT_DIALOG_DATA) public data: any, + ) { + } + + ngOnInit(): void { + this.setConfiguration(); + } + + changeColumn(coldb: string, colCsv: string) { + this.contactData = []; + for (let index = this.hasHeader ? 1 : 0; index < this.csvData.length; index++) { + const data = this.csvData[index]; + + const objContact = {}; + + this.contactColumns.forEach(key => { + objContact[key] = coldb === key ? data[this.csvColumns.filter(col => col === colCsv)[0]] : data[this.associatedColmuns[key]]; + }); + + this.contactData.push(objContact); + } + + this.countAdd = this.csvData.filter((data: any, index: number) => index > 0 && this.functionsService.empty(data[this.associatedColmuns['id']])).length; + this.countUp = this.csvData.filter((data: any, index: number) => index > 0 && !this.functionsService.empty(data[this.associatedColmuns['id']])).length; + + setTimeout(() => { + this.dataSource = new MatTableDataSource(this.contactData); + this.dataSource.paginator = this.paginator; + }, 0); + } + + uploadCsv(fileInput: any) { + if (fileInput.target.files && fileInput.target.files[0] && fileInput.target.files[0].type === 'text/csv') { + this.loading = true; + + let rawCsv = []; + const reader = new FileReader(); + + reader.readAsText(fileInput.target.files[0]); + + reader.onload = (value: any) => { + rawCsv = value.target.result.split('\n'); + rawCsv = rawCsv.filter(data => data !== ''); + + if (rawCsv[0].split(this.currentDelimiter).map(s => s.replace(/"/gi, '').trim()).length >= this.contactColumns.length - 1) { + let dataCol = []; + let objData = {}; + this.setCsvColumns(rawCsv[0].split(this.currentDelimiter).map(s => s.replace(/"/gi, '').trim())); + + this.countAll = this.hasHeader ? rawCsv.length - 1 : rawCsv.length; + + for (let index = 0; index < rawCsv.length; index++) { + objData = {}; + dataCol = rawCsv[index].split(this.currentDelimiter).map(s => s.replace(/"/gi, '').trim()); + + dataCol.forEach((element: any, index2: number) => { + objData[this.csvColumns[index2]] = element; + }); + this.csvData.push(objData); + } + this.initData(); + this.countAdd = this.csvData.filter((data: any, index: number) => index > 0 && this.functionsService.empty(data[this.associatedColmuns['id']])).length; + this.countUp = this.csvData.filter((data: any, index: number) => index > 0 && !this.functionsService.empty(data[this.associatedColmuns['id']])).length; + this.localStorage.save(`importContactFields_${this.headerService.user.id}`, this.currentDelimiter); + } else { + this.notify.error(this.translate.instant('lang.mustAtLeastMinValues')); + } + this.loading = false; + }; + } else { + this.dialog.open(AlertComponent, { panelClass: 'maarch-modal', autoFocus: false, disableClose: true, data: { title: this.translate.instant('lang.notAllowedExtension') + ' !', msg: this.translate.instant('lang.file') + ' : <b>' + fileInput.target.files[0].name + '</b>, ' + this.translate.instant('lang.type') + ' : <b>' + fileInput.target.files[0].type + '</b><br/><br/><u>' + this.translate.instant('lang.allowedExtensions') + '</u> : <br/>' + 'text/csv' } }); + } + } + + setCsvColumns(headerData: string[] = null) { + if (headerData.filter(col => this.functionsService.empty(col)).length > 0) { + this.csvColumns = Object.keys(headerData).map((val, index) => `${index}`); + } else { + this.csvColumns = headerData; + } + } + + toggleHeader() { + this.hasHeader = !this.hasHeader; + this.countAll = this.hasHeader ? this.csvData.length - 1 : this.csvData.length; + if (this.hasHeader) { + this.countAdd = this.csvData.filter((data: any, index: number) => index > 0 && this.functionsService.empty(data[this.associatedColmuns['id']])).length; + this.countUp = this.csvData.filter((data: any, index: number) => index > 0 && !this.functionsService.empty(data[this.associatedColmuns['id']])).length; + } else { + this.countAdd = this.csvData.filter((data: any, index: number) => this.functionsService.empty(data[this.associatedColmuns['id']])).length; + this.countUp = this.csvData.filter((data: any, index: number) => !this.functionsService.empty(data[this.associatedColmuns['id']])).length; + } + this.initData(); + } + + initData() { + this.contactData = []; + for (let index = this.hasHeader ? 1 : 0; index < this.csvData.length; index++) { + const data = this.csvData[index]; + const objContact = {}; + + this.contactColumns.forEach((key, indexCol) => { + this.associatedColmuns[key] = this.csvColumns[indexCol]; + objContact[key] = data[this.csvColumns[indexCol]]; + }); + this.contactData.push(objContact); + + } + setTimeout(() => { + this.dataSource = new MatTableDataSource(this.contactData); + this.dataSource.paginator = this.paginator; + }, 0); + } + + dndUploadFile(event: any) { + const fileInput = { + target: { + files: [ + event[0] + ] + } + }; + this.uploadCsv(fileInput); + } + + onSubmit() { + const dataToSend: any[] = []; + let confirmText = ''; + this.translate.get('lang.confirmImportUsers', { 0: this.countAll }).subscribe((res: string) => { + confirmText = `${res} ?<br/><br/>`; + confirmText += `<ul><li><b>${this.countAdd}</b> ${this.translate.instant('lang.additions')}</li><li><b>${this.countUp}</b> ${this.translate.instant('lang.modifications')}</li></ul>`; + }); + let dialogRef = this.dialog.open(ConfirmComponent, { panelClass: 'maarch-modal', autoFocus: false, disableClose: true, data: { title: this.translate.instant('lang.import'), msg: confirmText } }); + dialogRef.afterClosed().pipe( + filter((data: string) => data === 'ok'), + tap(() => { + this.loading = true; + this.csvData.forEach((element: any, index: number) => { + if ((this.hasHeader && index > 0) || !this.hasHeader) { + const objContact = {}; + this.contactColumns.forEach((key) => { + objContact[key] = element[this.associatedColmuns[key]]; + }); + dataToSend.push(objContact); + } + }); + }), + exhaustMap(() => this.http.put(`../rest/users/import`, { users: dataToSend })), + tap((data: any) => { + let textModal = ''; + if (data.warnings.count > 0) { + textModal = `<br/>${data.warnings.count} ${this.translate.instant('lang.withWarnings')} : <ul>`; + data.errors.details.forEach(element => { + textModal += `<li> ${this.translate.instant('element.lang')} (${this.translate.instant('lang.line')} : ${this.hasHeader ? element.index + 2 : element.index + 1})</li>`; + }); + textModal += '</ul>'; + } + + if (data.errors.count > 0) { + textModal += `<br/>${data.errors.count} ${this.translate.instant('lang.withErrors')} : <ul>`; + data.errors.details.forEach(element => { + textModal += `<li> ${this.translate.instant('element.lang')} (${this.translate.instant('lang.line')} : ${this.hasHeader ? element.index + 2 : element.index + 1})</li>`; + }); + textModal += '</ul>'; + } + dialogRef = this.dialog.open(AlertComponent, { panelClass: 'maarch-modal', autoFocus: false, disableClose: true, data: { title: this.translate.instant('lang.import'), msg: '<b>' + data.success + '</b> / <b>' + this.countAll + '</b> ' + this.translate.instant('lang.importedUsers') + '.' + textModal } }); + }), + exhaustMap(() => dialogRef.afterClosed()), + tap(() => { + this.dialogRef.close('success'); + }), + catchError((err: any) => { + this.loading = false; + this.notify.handleSoftErrors(err); + return of(false); + }) + ).subscribe(); + } + + setConfiguration() { + if (this.localStorage.get(`importContactFields_${this.headerService.user.id}`) !== null) { + this.currentDelimiter = this.localStorage.get(`importContactFields_${this.headerService.user.id}`); + } + } +}