From b91cbc9671b8416694ef53c507dc8431f5009555 Mon Sep 17 00:00:00 2001 From: Alex ORLUC <alex.orluc@maarch.org> Date: Thu, 23 Jan 2020 11:12:03 +0100 Subject: [PATCH] FEAT #11882 TIME 1:10 front history batch --- .../administration-routing.module.ts | 2 + .../administration/administration.module.ts | 2 + ...istory-batch-administration.component.html | 161 ++++++++++ ...istory-batch-administration.component.scss | 63 ++++ .../history-batch-administration.component.ts | 294 ++++++++++++++++++ src/frontend/service/privileges.service.ts | 2 +- 6 files changed, 523 insertions(+), 1 deletion(-) create mode 100644 src/frontend/app/administration/history/batch/history-batch-administration.component.html create mode 100644 src/frontend/app/administration/history/batch/history-batch-administration.component.scss create mode 100644 src/frontend/app/administration/history/batch/history-batch-administration.component.ts diff --git a/src/frontend/app/administration/administration-routing.module.ts b/src/frontend/app/administration/administration-routing.module.ts index 735427b23cf..09528a92f8a 100755 --- a/src/frontend/app/administration/administration-routing.module.ts +++ b/src/frontend/app/administration/administration-routing.module.ts @@ -24,6 +24,7 @@ import { ReportsAdministrationComponent } from './report/reports-a import { NotificationsAdministrationComponent } from './notification/notifications-administration.component'; import { NotificationAdministrationComponent } from './notification/notification-administration.component'; import { HistoryAdministrationComponent } from './history/history-administration.component'; +import { HistoryBatchAdministrationComponent } from './history/batch/history-batch-administration.component'; import { UpdateStatusAdministrationComponent } from './updateStatus/update-status-administration.component'; import { ContactsGroupsAdministrationComponent } from './contact/group/contacts-groups-administration.component'; import { ContactsGroupAdministrationComponent } from './contact/group/contacts-group-administration.component'; @@ -81,6 +82,7 @@ import { ContactsPageAdministrationComponent } from './contact/page/contacts-pag { path: 'administration/notifications/new', canActivate: [AppGuard], component: NotificationAdministrationComponent }, { path: 'administration/notifications/:identifier', canActivate: [AppGuard], component: NotificationAdministrationComponent }, { path: 'administration/history', canActivate: [AppGuard], component: HistoryAdministrationComponent }, + { path: 'administration/history-batch', canActivate: [AppGuard], component: HistoryBatchAdministrationComponent }, { path: 'administration/update-status', canActivate: [AppGuard], component: UpdateStatusAdministrationComponent }, { path: 'administration/contacts', canActivate: [AppGuard], component: ContactsListAdministrationComponent }, { path: 'administration/contacts/list', redirectTo: 'administration/contacts', pathMatch: 'full' }, diff --git a/src/frontend/app/administration/administration.module.ts b/src/frontend/app/administration/administration.module.ts index f35bd496b96..20ded5a9545 100755 --- a/src/frontend/app/administration/administration.module.ts +++ b/src/frontend/app/administration/administration.module.ts @@ -27,6 +27,7 @@ import { PrioritiesAdministrationComponent } from './priority/priorit import { PriorityAdministrationComponent } from './priority/priority-administration.component'; import { ReportsAdministrationComponent } from './report/reports-administration.component'; import { HistoryAdministrationComponent } from './history/history-administration.component'; +import { HistoryBatchAdministrationComponent } from './history/batch/history-batch-administration.component'; import { UpdateStatusAdministrationComponent } from './updateStatus/update-status-administration.component'; import { NotificationsAdministrationComponent } from './notification/notifications-administration.component'; import { NotificationAdministrationComponent } from './notification/notification-administration.component'; @@ -79,6 +80,7 @@ import { ContactsPageAdministrationComponent } from './contact/page PriorityAdministrationComponent, ReportsAdministrationComponent, HistoryAdministrationComponent, + HistoryBatchAdministrationComponent, UpdateStatusAdministrationComponent, ContactsGroupsAdministrationComponent, ContactsGroupAdministrationComponent, diff --git a/src/frontend/app/administration/history/batch/history-batch-administration.component.html b/src/frontend/app/administration/history/batch/history-batch-administration.component.html new file mode 100644 index 00000000000..6a496d78dbb --- /dev/null +++ b/src/frontend/app/administration/history/batch/history-batch-administration.component.html @@ -0,0 +1,161 @@ +<mat-sidenav-container autosize class="maarch-container"> + <mat-sidenav #snav [mode]="appService.getViewMode() ? 'over' : 'side'" [fixedInViewport]="appService.getViewMode()" + [opened]="appService.getViewMode() ? false : true"> + <header-panel [snavLeft]="snav"></header-panel> + <menu-shortcut></menu-shortcut> + <menu-nav></menu-nav> + <mat-nav-list> + <a mat-list-item *ngFor="let menu of subMenus" [class.active]="menu.current" [routerLink]="menu.route"> + <mat-icon color="primary" mat-list-icon [class]="menu.icon"></mat-icon> + <p mat-line> + {{menu.label}} + </p> + </a> + </mat-nav-list> + <mat-divider></mat-divider> + </mat-sidenav> + <mat-sidenav-content> + <div class="bg-head"> + <div class="bg-head-title" [class.customContainerRight]="appService.getViewMode()"> + <div class="bg-head-title-label"> + <header-left [snavLeft]="snav"></header-left> + </div> + <div class="bg-head-title-tool"> + <header-right></header-right> + </div> + </div> + <div class="bg-head-content" [class.fullContainer]="appService.getViewMode()"> + <div style="display: grid;grid-template-columns: repeat(2, 1fr);grid-gap: 10px;width: 100%;"> + <mat-form-field (click)="startPicker.open()" style="cursor:pointer;" class="dateFilter"> + <mat-label style="color:white;">{{lang.since}} + </mat-label> + <input [(ngModel)]="startDateFilter" matInput [matDatepicker]="startPicker" + [placeholder]="lang.since" [max]="endDateFilter" readonly style="cursor:pointer;" + (dateChange)="filterStartDate()"> + <mat-datepicker-toggle matSuffix [for]="startPicker" *ngIf="!startDateFilter"> + </mat-datepicker-toggle> + <mat-datepicker [touchUi]="appService.getViewMode()" #startPicker> + </mat-datepicker> + <button mat-button color="warn" matSuffix mat-icon-button *ngIf="startDateFilter" + (click)="$event.stopPropagation();startDateFilter = '';filterStartDate()" + [title]="lang.eraseValue"> + <mat-icon color="warn" class="fa fa-calendar-times"> + </mat-icon> + </button> + </mat-form-field> + <mat-form-field (click)="endPicker.open()" style="cursor:pointer;" class="dateFilter"> + <mat-label style="color:white;">{{lang.until}} + </mat-label> + <input [(ngModel)]="endDateFilter" matInput [matDatepicker]="endPicker" + [placeholder]="lang.until" [min]="startDateFilter" readonly style="cursor:pointer;" + (dateChange)="filterEndDate()"> + <mat-datepicker-toggle matSuffix [for]="endPicker" *ngIf="!endDateFilter"> + </mat-datepicker-toggle> + <mat-datepicker [touchUi]="appService.getViewMode()" #endPicker> + </mat-datepicker> + <button mat-button color="warn" matSuffix mat-icon-button *ngIf="endDateFilter" + (click)="$event.stopPropagation();endDateFilter = '';filterEndDate()" + [title]="lang.eraseValue"> + <mat-icon color="warn" class="fa fa-calendar-times"> + </mat-icon> + </button> + </mat-form-field> + </div> + </div> + </div> + <div class="container" [class.fullContainer]="appService.getViewMode()"> + <div class="container-content"> + <div class="example-loading-shade" *ngIf="isLoadingResults"> + <mat-spinner *ngIf="isLoadingResults"></mat-spinner> + </div> + <div class="table-head"> + <div class="table-head-result"> + <mat-form-field floatLabel="never" style="font-size: 13px;"> + <input type="text" #autoCompleteInput [matAutocomplete]="auto" [placeholder]="lang.filterBy" + matInput [formControl]="searchHistory" (click)="$event.stopPropagation()" + maxlength="128"> + <mat-autocomplete #auto="matAutocomplete" (optionSelected)="addItemFilter($event.option)" + (opened)="initFilterListHistory()"> + <mat-option disabled *ngIf="loadingFilters"> + <div style="display: flex;justify-content: center;"> + <mat-spinner diameter="35"></mat-spinner> + </div> + </mat-option> + <ng-container *ngIf="filterList!==null && !loadingFilters"> + <ng-container *ngFor="let keyVal of filterList | keyvalue"> + <mat-optgroup *ngIf="(filteredList[keyVal.key] | async)?.length > 0" + [label]="lang[keyVal.key]" class="filterList"> + <mat-option [id]="keyVal.key" + [style.color]="!filter.used ? filterColor[keyVal.key] : ''" + *ngFor="let filter of filteredList[keyVal.key] | async | sortBy : 'label'" + [value]="filter" [disabled]="filter.used"> + {{filter.label}} + </mat-option> + </mat-optgroup> + </ng-container> + </ng-container> + + </mat-autocomplete> + </mat-form-field> + </div> + <div class="table-head-tool"> + <mat-paginator #paginatorHistoryList [length]="resultsLength" [hidePageSize]="true" + [pageSize]="10" class="paginatorResultList"></mat-paginator> + </div> + </div> + <div style="height:90%;overflow:auto;position:absolute;width:100%;"> + <div class="filterBadges"> + <ng-container *ngFor="let keyVal of filterUsed | keyvalue"> + <ng-container *ngIf="['startDate','endDate'].indexOf(keyVal.key) === -1"> + <span *ngFor="let filter of filterUsed[keyVal.key]; let i=index;" class="label" + [style.background]="filterColor[keyVal.key]" [title]="lang[keyVal.key]" + (click)="removeItemFilter(filter,keyVal.key,i)">{{filter.label}} + <i class="fa fa-times-circle"></i></span> + </ng-container> + </ng-container> + </div> + <mat-table id="history-list" #tableHistoryListSort="matSort" [dataSource]="data" matSort + matSortActive="event_date" matSortDirection="desc" style="width:100%;"> + <ng-container matColumnDef="event_date"> + <mat-header-cell *matHeaderCellDef mat-sort-header>{{lang.event}}</mat-header-cell> + <mat-cell mat-cell *matCellDef="let element" [title]="element.event_date | fullDate"> + {{element.event_date | timeAgo : 'full' | ucfirst}} </mat-cell> + </ng-container> + <ng-container matColumnDef="total_processed"> + <mat-header-cell *matHeaderCellDef mat-sort-header>{{lang.totalProcessed | ucfirst}} + </mat-header-cell> + <mat-cell *matCellDef="let element" [class.empty]="element.total_processed === 0"> + {{element.total_processed}} </mat-cell> + </ng-container> + <ng-container matColumnDef="total_errors"> + <mat-header-cell *matHeaderCellDef mat-sort-header>{{lang.totalErrors}} + </mat-header-cell> + <mat-cell *matCellDef="let element" [class.empty]="element.total_errors === 0" [class.error]="element.total_errors > 0"> + {{element.total_errors}} </mat-cell> + </ng-container> + <ng-container matColumnDef="info"> + <mat-header-cell *matHeaderCellDef mat-sort-header style="flex: 2;">{{lang.information}} + </mat-header-cell> + <mat-cell *matCellDef="let element" style="flex: 2;"> + {{element.info}} </mat-cell> + </ng-container> + <ng-container matColumnDef="module_name"> + <mat-header-cell *matHeaderCellDef mat-sort-header>{{lang.module}} + </mat-header-cell> + <mat-cell *matCellDef="let element"> + {{element.module_name}} </mat-cell> + </ng-container> + <mat-header-row *matHeaderRowDef="displayedColumnsHistory"></mat-header-row> + <mat-row *matRowDef="let row; columns: displayedColumnsHistory;"> + </mat-row> + </mat-table> + <div class="mat-paginator" + style="min-height:48px;min-height: 48px;display: flex;justify-content: end;align-items: center;padding-right: 20px;"> + {{resultsLength}} {{lang.elements}}</div> + </div> + <div class="table-head"> + </div> + </div> + </div> + </mat-sidenav-content> +</mat-sidenav-container> \ No newline at end of file diff --git a/src/frontend/app/administration/history/batch/history-batch-administration.component.scss b/src/frontend/app/administration/history/batch/history-batch-administration.component.scss new file mode 100644 index 00000000000..475b4db0990 --- /dev/null +++ b/src/frontend/app/administration/history/batch/history-batch-administration.component.scss @@ -0,0 +1,63 @@ +@import '../../../../css/vars.scss'; + +.active, +.active:hover, +.active:active, +.active:focus { + color: $primary; + border-left: solid 5px $primary; + background: rgba($primary, 0.14); +} + + +.paginatorResultList { + ::ng-deep.mat-paginator-range-label { + justify-content: flex-end; + display: flex; + } +} + +.filterList { + ::ng-deep.mat-optgroup-label { + color: $primary; + position: sticky; + top: 0px; + background: white !important; + z-index: 1; + } +} + +.label { + cursor: pointer; + margin: 5px; +} + +.bg-head-content { + ::ng-deep .mat-focused .mat-form-field-label { + /*change color of label*/ + color: white !important; + } + + ::ng-deep.mat-form-field-underline { + /*change color of underline*/ + background-color: white !important; + } + + ::ng-deep.mat-form-field-ripple { + /*change color of underline when focused*/ + background-color: white !important; + } + + .mat-icon,.mat-datepicker-toggle { + color:white; + } +} + +.empty { + opacity: 0.5; +} + +.error { + color: $warn; + font-weight: bold; +} \ No newline at end of file diff --git a/src/frontend/app/administration/history/batch/history-batch-administration.component.ts b/src/frontend/app/administration/history/batch/history-batch-administration.component.ts new file mode 100644 index 00000000000..193237f3cf2 --- /dev/null +++ b/src/frontend/app/administration/history/batch/history-batch-administration.component.ts @@ -0,0 +1,294 @@ +import { Component, OnInit, ViewChild, EventEmitter, 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 { MatSidenav } from '@angular/material/sidenav'; +import { AppService } from '../../../../service/app.service'; +import { Observable, merge, Subject, of as observableOf, of } from 'rxjs'; +import { MatPaginator, MatSort, MatDialog } from '@angular/material'; +import { takeUntil, startWith, switchMap, map, catchError, filter, exhaustMap, tap, debounceTime, distinctUntilChanged, finalize } from 'rxjs/operators'; +import { FormControl } from '@angular/forms'; +import { FunctionsService } from '../../../../service/functions.service'; +import { LatinisePipe } from 'ngx-pipes'; +import { PrivilegeService } from '../../../../service/privileges.service'; + +@Component({ + templateUrl: "history-batch-administration.component.html", + styleUrls: ['history-batch-administration.component.scss'], + providers: [AppService] +}) +export class HistoryBatchAdministrationComponent implements OnInit { + + @ViewChild('snav', { static: true }) public sidenavLeft: MatSidenav; + @ViewChild('snav2', { static: true }) public sidenavRight: MatSidenav; + + lang: any = LANG; + loading: boolean = false; + + filtersChange = new EventEmitter(); + + data: any; + + displayedColumnsHistory: string[] = ['event_date', 'total_processed', 'total_errors', 'info', 'module_name']; + + isLoadingResults = true; + routeUrl: string = '../../rest/batchHistory'; + resultListDatabase: HistoryListHttpDao | null; + resultsLength = 0; + + searchHistory = new FormControl(); + startDateFilter: any = ''; + endDateFilter: any = ''; + filterUrl: string = ''; + filterList: any = null; + filteredList: any = {}; + filterUsed: any = {}; + + filterColor = { + startDate: '#b5cfd8', + endDate: '#7393a7', + errors: '#7d5ba6' + }; + + loadingFilters: boolean = true; + + @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator; + @ViewChild('tableHistoryListSort', { static: true }) sort: MatSort; + @ViewChild('autoCompleteInput', { static: true }) autoCompleteInput: ElementRef; + + private destroy$ = new Subject<boolean>(); + + subMenus: any[] = []; + + constructor( + public http: HttpClient, + private notify: NotificationService, + private headerService: HeaderService, + public appService: AppService, + public dialog: MatDialog, + public functions: FunctionsService, + private latinisePipe: LatinisePipe, + private privilegeService: PrivilegeService) { } + + ngOnInit(): void { + if (this.privilegeService.hasCurrentUserPrivilege('view_history')) { + this.subMenus = [ + { + icon: 'fa fa-history', + route: '/administration/history', + label: this.lang.history, + current: false + }, + { + icon: 'fa fa-history', + route: '/administration/history-batch', + label: this.lang.historyBatch, + current: true + } + ]; + } else { + this.subMenus = [ + { + icon: 'fa fa-history', + route: '/administration/history-batch', + label: this.lang.historyBatch, + current: true + } + ]; + } + this.loading = true; + this.initHistoryList(); + } + + initHistoryList() { + this.resultListDatabase = new HistoryListHttpDao(this.http); + this.paginator.pageIndex = 0; + this.sort.active = 'event_date'; + this.sort.direction = 'desc'; + this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); + + // When list is refresh (sort, page, filters) + merge(this.sort.sortChange, this.paginator.page, this.filtersChange) + .pipe( + takeUntil(this.destroy$), + startWith({}), + switchMap(() => { + this.isLoadingResults = true; + return this.resultListDatabase!.getRepoIssues( + this.sort.active, this.sort.direction, this.paginator.pageIndex, this.routeUrl, this.filterUrl); + }), + map(data => { + this.isLoadingResults = false; + data = this.processPostData(data); + this.resultsLength = data.count; + this.headerService.setHeader(this.lang.administration + ' ' + this.lang.historyBatch.toLowerCase(), '', ''); + return data.history; + }), + catchError((err: any) => { + this.notify.handleErrors(err); + this.isLoadingResults = false; + return observableOf([]); + }) + ).subscribe(data => this.data = data); + } + + processPostData(data: any) { + data.history = data.history.map((item: any) => { + return { + ...item, + total_errors : item.total_errors === null ? 0 : item.total_errors + } + }) + return data; + } + + + refreshDao() { + this.paginator.pageIndex = 0; + this.filtersChange.emit(); + } + + initFilterListHistory() { + + if (this.filterList === null) { + this.filterList = {}; + this.loadingFilters = true; + + this.http.get("../../rest/history/availableFilters").pipe( + map((data: any) => { + data = {}; + data.modules = [ + { + id : 'retrieveMailsFromSignatoryBook', + label : 'retrieveMailsFromSignatoryBook' + } + ]; + + data.totalErrors = [ + { + id : 'errorElement', + label : 'Éléments en erreur' + } + ]; + + return data; + }), + tap((data: any) => { + Object.keys(data).forEach((filterType: any) => { + if (this.functions.empty(this.filterList[filterType])) { + this.filterList[filterType] = []; + this.filteredList[filterType] = []; + } + data[filterType].forEach((element: any) => { + this.filterList[filterType].push(element); + }); + + this.filteredList[filterType] = this.searchHistory.valueChanges + .pipe( + startWith(''), + map(element => element ? this.filter(element, filterType) : this.filterList[filterType].slice()) + ); + }); + + }), + finalize(() => this.loadingFilters = false), + catchError((err: any) => { + this.notify.handleSoftErrors(err); + return of(false); + }) + ).subscribe(); + + } + } + + + filterStartDate() { + if (this.functions.empty(this.filterUsed['startDate'])) { + this.filterUsed['startDate'] = []; + } + this.filterUsed['startDate'][0] = { + id: this.functions.empty(this.startDateFilter) ? '' : this.functions.formatDateObjectToDateString(this.startDateFilter), + label: this.functions.empty(this.startDateFilter) ? '' : this.functions.formatDateObjectToDateString(this.startDateFilter) + }; + this.generateUrlFilter(); + this.refreshDao(); + } + + filterEndDate() { + if (this.functions.empty(this.filterUsed['endDate'])) { + this.filterUsed['endDate'] = []; + } + this.filterUsed['endDate'][0] = { + id: this.functions.empty(this.endDateFilter) ? '' : this.functions.formatDateObjectToDateString(this.endDateFilter, true), + label: this.functions.empty(this.endDateFilter) ? '' : this.functions.formatDateObjectToDateString(this.endDateFilter) + }; + this.generateUrlFilter(); + this.refreshDao(); + } + + addItemFilter(elem: any) { + elem.value.used = true; + if (this.functions.empty(this.filterUsed[elem.id])) { + this.filterUsed[elem.id] = []; + } + this.filterUsed[elem.id].push(elem.value); + this.generateUrlFilter(); + this.searchHistory.reset(); + this.autoCompleteInput.nativeElement.blur(); + this.refreshDao(); + } + + removeItemFilter(elem: any, type: string, index: number) { + elem.used = false; + this.filterUsed[type].splice(index, 1); + this.generateUrlFilter(); + this.refreshDao(); + } + + + generateUrlFilter() { + this.filterUrl = ''; + let arrTmpUrl: any[] = []; + Object.keys(this.filterUsed).forEach((type: any) => { + this.filterUsed[type].forEach((filter: any) => { + if (!this.functions.empty(filter.id)) { + if (['startDate', 'endDate'].indexOf(type) > -1) { + arrTmpUrl.push(`${type}=${filter.id}`); + } else { + arrTmpUrl.push(`${type}[]=${filter.id}`); + } + } + }); + }); + if (arrTmpUrl.length > 0) { + this.filterUrl = '&' + arrTmpUrl.join('&'); + } + } + + private filter(value: string, type: string): any[] { + if (typeof value === 'string') { + const filterValue = this.latinisePipe.transform(value.toLowerCase()); + return this.filterList[type].filter((elem: any) => this.latinisePipe.transform(elem.label.toLowerCase()).includes(filterValue)); + } else { + return this.filterList[type]; + } + } +} + +export interface HistoryList { + history: any[]; + count: number; +} +export class HistoryListHttpDao { + + constructor(private http: HttpClient) { } + + getRepoIssues(sort: string, order: string, page: number, href: string, search: string): Observable<HistoryList> { + + let offset = page * 10; + const requestUrl = `${href}?limit=10&offset=${offset}&order=${order}&orderBy=${sort}${search}`; + + return this.http.get<HistoryList>(requestUrl); + } +} \ No newline at end of file diff --git a/src/frontend/service/privileges.service.ts b/src/frontend/service/privileges.service.ts index 68b7ca039f5..6e4a7311808 100644 --- a/src/frontend/service/privileges.service.ts +++ b/src/frontend/service/privileges.service.ts @@ -271,7 +271,7 @@ export class PrivilegeService { "id": "view_history_batch", "label": this.lang.historyBatch, "comment": this.lang.historyBatchAdmin, - "route": "/administration/history", + "route": "/administration/history-batch", "unit": "supervision", "style": "fa fa-history", "angular" : true, -- GitLab