finished filters
This commit is contained in:
		
							
								
								
									
										2
									
								
								src/Staticfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/Staticfile
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
pushstate: enabled
 | 
			
		||||
force_https: true
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
<rb-full-header>
 | 
			
		||||
  <nav *rbMainNavItems>
 | 
			
		||||
    <a routerLink="/home" routerLinkActive="active" rbLoadingLink>Home</a>
 | 
			
		||||
    <a routerLink="/samples" routerLinkActive="active" rbLoadingLink *ngIf="loginService.canActivate()">Samples</a>
 | 
			
		||||
    <a routerLink="/samples" routerLinkActive="active" rbLoadingLink *ngIf="loginService.isLoggedIn">Samples</a>
 | 
			
		||||
  </nav>
 | 
			
		||||
 | 
			
		||||
  <nav *rbActionNavItems>
 | 
			
		||||
@@ -18,7 +18,7 @@
 | 
			
		||||
    </div>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
 | 
			
		||||
  <div *rbSubBrandHeader>Digital Fingerprint of Plastics</div>
 | 
			
		||||
  <div *rbSubBrandHeader><span class="dev-label" *ngIf="devMode">DEVELOPMENT</span>Digital Fingerprint of Plastics</div>
 | 
			
		||||
</rb-full-header>
 | 
			
		||||
 | 
			
		||||
<div class="container">
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,5 @@
 | 
			
		||||
.dev-label {
 | 
			
		||||
  color: #F00;
 | 
			
		||||
  font-size: 32px;
 | 
			
		||||
  margin-right: 40px;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { Component } from '@angular/core';
 | 
			
		||||
import { Component, isDevMode} from '@angular/core';
 | 
			
		||||
import {LoginService} from './services/login.service';
 | 
			
		||||
 | 
			
		||||
// TODO: add multiple samples at once
 | 
			
		||||
@@ -6,6 +6,7 @@ import {LoginService} from './services/login.service';
 | 
			
		||||
// TODO: validation: VZ, Humidity: min/max value, DPT: filename
 | 
			
		||||
// TODO: filter by not completely filled/no measurements
 | 
			
		||||
// TODO: account
 | 
			
		||||
// TODO: admin user handling, template pages, validation of samples
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-root',
 | 
			
		||||
@@ -18,4 +19,8 @@ export class AppComponent {
 | 
			
		||||
    public loginService: LoginService
 | 
			
		||||
  ) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get devMode() {
 | 
			
		||||
    return isDevMode();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ import { SampleComponent } from './sample/sample.component';
 | 
			
		||||
import { ValidateDirective } from './validate.directive';
 | 
			
		||||
import {CommonModule} from '@angular/common';
 | 
			
		||||
import { ErrorComponent } from './error/error.component';
 | 
			
		||||
import { ObjectPipe } from './object.pipe';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations: [
 | 
			
		||||
@@ -24,7 +25,8 @@ import { ErrorComponent } from './error/error.component';
 | 
			
		||||
    SamplesComponent,
 | 
			
		||||
    SampleComponent,
 | 
			
		||||
    ValidateDirective,
 | 
			
		||||
    ErrorComponent
 | 
			
		||||
    ErrorComponent,
 | 
			
		||||
    ObjectPipe
 | 
			
		||||
  ],
 | 
			
		||||
  imports: [
 | 
			
		||||
    LocalStorageModule.forRoot({
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import {Component, OnInit, ViewChild} from '@angular/core';
 | 
			
		||||
import {ValidationService} from '../services/validation.service';
 | 
			
		||||
import {LoginService} from '../services/login.service';
 | 
			
		||||
import {Router} from '@angular/router';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
@@ -19,6 +20,7 @@ export class LoginComponent implements OnInit {
 | 
			
		||||
  constructor(
 | 
			
		||||
    private validate: ValidationService,
 | 
			
		||||
    private loginService: LoginService,
 | 
			
		||||
    private router: Router
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
@@ -28,6 +30,7 @@ export class LoginComponent implements OnInit {
 | 
			
		||||
    this.loginService.login(this.username, this.password).then(ok => {
 | 
			
		||||
      if (ok) {
 | 
			
		||||
        this.message = 'Login successful';  // TODO: think about following action
 | 
			
		||||
        this.router.navigate(['/samples']);
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        this.message = 'Wrong credentials!';
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								src/app/object.pipe.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/app/object.pipe.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
import { ObjectPipe } from './object.pipe';
 | 
			
		||||
 | 
			
		||||
describe('ObjectPipe', () => {
 | 
			
		||||
  it('create an instance', () => {
 | 
			
		||||
    const pipe = new ObjectPipe();
 | 
			
		||||
    expect(pipe).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										12
									
								
								src/app/object.pipe.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/app/object.pipe.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
import { Pipe, PipeTransform } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
@Pipe({
 | 
			
		||||
  name: 'object'
 | 
			
		||||
})
 | 
			
		||||
export class ObjectPipe implements PipeTransform {
 | 
			
		||||
 | 
			
		||||
  transform(value: object): string {
 | 
			
		||||
    return value ? Object.entries(value).map(e => e.join(': ')).join(', ') : '';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -24,7 +24,6 @@ import {MeasurementModel} from '../models/measurement.model';
 | 
			
		||||
// TODO: multiple samples for base data, extend multiple measurements, conditions
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-sample',
 | 
			
		||||
  templateUrl: './sample.component.html',
 | 
			
		||||
 
 | 
			
		||||
@@ -8,17 +8,18 @@
 | 
			
		||||
<rb-accordion>
 | 
			
		||||
  <rb-accordion-title [open]="true"><span class="rb-ic rb-ic-filter"></span>  Filter</rb-accordion-title>
 | 
			
		||||
  <rb-accordion-body>
 | 
			
		||||
    <form (change)="loadSamples({event: $event})">
 | 
			
		||||
    <form class="filters">
 | 
			
		||||
      <div class="status-selection">
 | 
			
		||||
        <label class="label">Status</label>
 | 
			
		||||
        <rb-form-checkbox name="status-validated" [(ngModel)]="filters.status.validated" [disabled]="!filters.status.new">
 | 
			
		||||
        <rb-form-checkbox name="status-validated" [(ngModel)]="filters.status.validated" [disabled]="!filters.status.new" (ngModelChange)="loadSamples({firstPage: true})">
 | 
			
		||||
          validated
 | 
			
		||||
        </rb-form-checkbox>
 | 
			
		||||
        <rb-form-checkbox name="status-new" [(ngModel)]="filters.status.new" [disabled]="!filters.status.validated">
 | 
			
		||||
        <rb-form-checkbox name="status-new" [(ngModel)]="filters.status.new" [disabled]="!filters.status.validated" (ngModelChange)="loadSamples({firstPage: true})">
 | 
			
		||||
          new
 | 
			
		||||
        </rb-form-checkbox>
 | 
			
		||||
      </div>
 | 
			
		||||
      <rb-form-select name="pageSizeSelection" label="page size" [(ngModel)]="filters.pageSize" class="page-size-selection" #pageSizeSelection>
 | 
			
		||||
      <rb-form-select name="pageSizeSelection" label="page size" [(ngModel)]="filters.pageSize" class="selection" (ngModelChange)="loadSamples({firstPage: true})" #pageSizeSelection>
 | 
			
		||||
        <option value="3">3</option>
 | 
			
		||||
        <option value="10">10</option>
 | 
			
		||||
        <option value="25">25</option>
 | 
			
		||||
        <option value="50">50</option>
 | 
			
		||||
@@ -27,38 +28,92 @@
 | 
			
		||||
        <option value="500">500</option>
 | 
			
		||||
      </rb-form-select>
 | 
			
		||||
 | 
			
		||||
      <rb-form-multi-select name="fieldSelect" idField="id" [items]="keys" [(ngModel)]="activeKeys" label="Fields" class="selection" (ngModelChange)="loadSamples()">
 | 
			
		||||
        <span *rbFormMultiSelectOption="let item" class="load-first-page">{{item.label}}</span>
 | 
			
		||||
      </rb-form-multi-select>
 | 
			
		||||
 | 
			
		||||
      <div class="fieldfilters">
 | 
			
		||||
        <div *ngFor="let filter of filters.filters">
 | 
			
		||||
          <rb-form-checkbox [name]="'filteractive-' + filter.field" [(ngModel)]="filter.active" (ngModelChange)="loadSamples({firstPage: true})"></rb-form-checkbox>
 | 
			
		||||
          <rb-form-select [name]="'filtermode-' + filter.field" class="filtermode" [(ngModel)]="filter.mode" (ngModelChange)="updateFilterFields(filter.field)">
 | 
			
		||||
            <option value="eq">=</option>
 | 
			
		||||
            <option value="ne">≠</option>
 | 
			
		||||
            <option value="lt"><</option>
 | 
			
		||||
            <option value="lte">≤</option>
 | 
			
		||||
            <option value="gt">></option>
 | 
			
		||||
            <option value="gte">≥</option>
 | 
			
		||||
            <option value="in">∈</option>
 | 
			
		||||
            <option value="nin">∉</option>
 | 
			
		||||
          </rb-form-select>
 | 
			
		||||
          <div>
 | 
			
		||||
            <ng-container *ngFor="let ignore of [].constructor(filter.values.length); index as i">
 | 
			
		||||
              <rb-form-date-input *ngIf="filter.field === 'added'; else noDate" [name]="'filter-' + filter.field + i" [label]="filter.label" [(ngModel)]="filter.values[i]" (ngModelChange)="updateFilterFields(filter.field)"></rb-form-date-input>
 | 
			
		||||
              <ng-template #noDate>
 | 
			
		||||
                <rb-form-input *ngIf="!filter.autocomplete.length" [name]="'filter-' + filter.field + i" [label]="filter.label" [(ngModel)]="filter.values[i]" (ngModelChange)="updateFilterFields(filter.field)"></rb-form-input>
 | 
			
		||||
                <rb-form-input *ngIf="filter.autocomplete.length" [name]="'filter-' + filter.field + i" [label]="filter.label" [rbFormInputAutocomplete]="autocomplete.bind(this, filter.autocomplete)" [rbDebounceTime]="0" (keydown)="preventDefault($event, 'Enter')" [(ngModel)]="filter.values[i]" (ngModelChange)="updateFilterFields(filter.field)" ngModel></rb-form-input>
 | 
			
		||||
              </ng-template>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
          </div>
 | 
			
		||||
<!--          <rb-form-date-input *ngIf="filter.field === 'added'; else noDate" [name]="'filter-' + filter.field" [label]="filter.label" [(ngModel)]="filter.values[0]" (ngModelChange)="loadSamples({firstPage: true})"></rb-form-date-input>-->
 | 
			
		||||
<!--          <ng-template #noDate>-->
 | 
			
		||||
<!--            <rb-form-input *ngIf="!filter.autocomplete.length" [name]="'filter-' + filter.field" [label]="filter.label" [(ngModel)]="filter.values[0]" (ngModelChange)="loadSamples({firstPage: true})"></rb-form-input>-->
 | 
			
		||||
<!--            <rb-form-input *ngIf="filter.autocomplete.length" [name]="'filter-' + filter.field" [label]="filter.label" [rbFormInputAutocomplete]="autocomplete.bind(this, filter.autocomplete)" [rbDebounceTime]="0" (keydown)="preventDefault($event, 'Enter')" [(ngModel)]="filter.values[0]" (ngModelChange)="loadSamples({firstPage: true})" ngModel></rb-form-input>-->
 | 
			
		||||
<!--          </ng-template>-->
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
<!--      <button class="rb-btn rb-secondary" (click)="loadSamples()">Apply filters</button>-->
 | 
			
		||||
    </form>
 | 
			
		||||
  </rb-accordion-body>
 | 
			
		||||
</rb-accordion>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<ng-container *ngTemplateOutlet="paging"></ng-container>
 | 
			
		||||
 | 
			
		||||
<div class="download">
 | 
			
		||||
  <button class="rb-btn rb-secondary" type="button" [rbModal]="linkModal" ><span class="rb-ic rb-ic-download"></span>  JSON download link
 | 
			
		||||
  </button>
 | 
			
		||||
  <ng-template #linkModal let-close="close">
 | 
			
		||||
    URL for JSON download:
 | 
			
		||||
    <textarea class="linkmodal" #linkarea [value]="sampleUrl({export: true, host: true})" (keydown)="preventDefault($event)"></textarea>
 | 
			
		||||
    <rb-form-checkbox name="download-csv" [(ngModel)]="downloadCsv">
 | 
			
		||||
      add spectra
 | 
			
		||||
    </rb-form-checkbox>
 | 
			
		||||
    <button class="rb-btn rb-secondary" type="button" (click)="clipboard()"><span class="rb-ic rb-ic-clipboard"></span>  Copy to clipboard</button>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
  <a [href]="csvUrl" download="samples.csv">
 | 
			
		||||
    <button class="rb-btn rb-secondary" type="button" (mousedown)="csvUrl = sampleUrl({csv: true, export: true})"><span class="rb-ic rb-ic-download"></span>  Download result as CSV</button>
 | 
			
		||||
  </a>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<rb-table>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <th *ngFor="let key of activeKeys">
 | 
			
		||||
    <th *ngFor="let key of activeKeysArray()">
 | 
			
		||||
      <div class="sort-header">
 | 
			
		||||
        <span>{{key.name}}</span>
 | 
			
		||||
        <span class="rb-ic rb-ic-up" [ngClass]="{'sort-active-desc': filters.sort === key.key + '-' + 'desc'}" (click)="setSort(key.key + '-' + 'desc')"></span>
 | 
			
		||||
        <span class="rb-ic rb-ic-down" [ngClass]="{'sort-active-asc': filters.sort === key.key + '-' + 'asc'}" (click)="setSort(key.key + '-' + 'asc')"></span>
 | 
			
		||||
        <span>{{key.label}}</span>
 | 
			
		||||
<!--        <span class="rb-ic rb-ic-up" [ngClass]="{'sort-active-desc': filters.sort === key.id + '-' + 'desc'}" (click)="setSort(key.id + '-' + 'desc')"></span>-->
 | 
			
		||||
<!--        <span class="rb-ic rb-ic-down" [ngClass]="{'sort-active-asc': filters.sort === key.id + '-' + 'asc'}" (click)="setSort(key.id + '-' + 'asc')"></span>-->
 | 
			
		||||
        <span class="rb-ic rb-ic-up sort-arr-up" (click)="setSort(key.id + '-' + 'desc')"><span *ngIf="filters.sort === key.id + '-' + 'desc'"></span></span>
 | 
			
		||||
        <span class="rb-ic rb-ic-down sort-arr-down" (click)="setSort(key.id + '-' + 'asc')"><span *ngIf="filters.sort === key.id + '-' + 'asc'"></span></span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </th>
 | 
			
		||||
    <th></th>
 | 
			
		||||
  </tr>
 | 
			
		||||
 | 
			
		||||
  <tr *ngFor="let sample of samples">
 | 
			
		||||
    <td>{{sample.number}}</td>
 | 
			
		||||
<!--    <td>{{sample.material_number}}</td>-->
 | 
			
		||||
    <td>{{materials[sample.material_id].name}}</td>
 | 
			
		||||
    <td>{{materials[sample.material_id].supplier}}</td>
 | 
			
		||||
<!--    <td>{{materials[sample.material_id].group}}</td>-->
 | 
			
		||||
<!--    <td>{{materials[sample.material_id].glass_fiber}}</td>-->
 | 
			
		||||
<!--    <td>{{materials[sample.material_id].carbon_fiber}}</td>-->
 | 
			
		||||
<!--    <td>{{materials[sample.material_id].mineral}}</td>-->
 | 
			
		||||
    <td>{{sample.type}}</td>
 | 
			
		||||
    <td>{{sample.color}}</td>
 | 
			
		||||
    <td>{{sample.batch}}</td>
 | 
			
		||||
    <td>{{sample.added | date}}</td>
 | 
			
		||||
    <td *ngIf="activeKeys['number']">{{sample.number}}</td>
 | 
			
		||||
    <td *ngIf="activeKeys['material.number']">{{sample.material_number}}</td>
 | 
			
		||||
    <td *ngIf="activeKeys['material.name']">{{materials[sample.material_id].name}}</td>
 | 
			
		||||
    <td *ngIf="activeKeys['material.supplier']">{{materials[sample.material_id].supplier}}</td>
 | 
			
		||||
    <td *ngIf="activeKeys['material.group']">{{materials[sample.material_id].group}}</td>
 | 
			
		||||
    <td *ngIf="activeKeys['material.glass_fiber']">{{materials[sample.material_id].glass_fiber}}</td>
 | 
			
		||||
    <td *ngIf="activeKeys['material.carbon_fiber']">{{materials[sample.material_id].carbon_fiber}}</td>
 | 
			
		||||
    <td *ngIf="activeKeys['material.mineral']">{{materials[sample.material_id].mineral}}</td>
 | 
			
		||||
    <td *ngIf="activeKeys['type']">{{sample.type}}</td>
 | 
			
		||||
    <td *ngIf="activeKeys['color']">{{sample.color}}</td>
 | 
			
		||||
    <td *ngIf="activeKeys['batch']">{{sample.batch}}</td>
 | 
			
		||||
    <td *ngIf="activeKeys['added']">{{sample.added | date:'dd/MM/yy'}}</td>
 | 
			
		||||
    <td *ngFor="let key of activeMeasurementKeys()">{{sample[key[1]] ? sample[key[1]][key[2]] : ''}}</td>
 | 
			
		||||
    <td><a [routerLink]="'/samples/edit/' + sample._id"><span class="rb-ic rb-ic-edit"></span></a></td>
 | 
			
		||||
  </tr>
 | 
			
		||||
</rb-table>
 | 
			
		||||
@@ -72,7 +127,7 @@
 | 
			
		||||
    </button>
 | 
			
		||||
    <rb-form-input label="page" (change)="loadPage({toPage: $event.target.value - page})" [ngModel]="page"></rb-form-input>
 | 
			
		||||
    <span>
 | 
			
		||||
      of {{pages()}}
 | 
			
		||||
      of {{pages()}} ({{totalSamples}} samples)
 | 
			
		||||
    </span>
 | 
			
		||||
    <button class="rb-btn rb-link" type="button" (click)="loadPage(1)" [disabled]="page >= pages()">
 | 
			
		||||
      <span class="rb-ic  rb-ic-forward-right"></span>
 | 
			
		||||
 
 | 
			
		||||
@@ -23,10 +23,6 @@
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
form {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.status-selection {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  margin-bottom: 10px;
 | 
			
		||||
@@ -46,8 +42,8 @@ form {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.page-size-selection {
 | 
			
		||||
  max-width: 125px;
 | 
			
		||||
.selection {
 | 
			
		||||
  max-width: 230px;
 | 
			
		||||
  float: left;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -102,3 +98,74 @@ form {
 | 
			
		||||
  background: linear-gradient(to top, #FFF 17%, $color-bosch-light-blue-w50 17%);;
 | 
			
		||||
  border-radius: 8px 8px 0 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.filters:after {
 | 
			
		||||
  content:"";
 | 
			
		||||
  clear:both;
 | 
			
		||||
  display:block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.download {
 | 
			
		||||
  margin-top: 5px;
 | 
			
		||||
  float: right;
 | 
			
		||||
 | 
			
		||||
  & > rb-form-checkbox {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  button {
 | 
			
		||||
    margin-right: 10px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sort-arr-up {
 | 
			
		||||
  position: relative;
 | 
			
		||||
 | 
			
		||||
  & > span {
 | 
			
		||||
    width: 0;
 | 
			
		||||
    height: 0;
 | 
			
		||||
    border-left: 6.3px solid transparent;
 | 
			
		||||
    border-right: 6.3px solid transparent;
 | 
			
		||||
    border-bottom: 6.3px solid #000;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 5px;
 | 
			
		||||
    display: block;
 | 
			
		||||
    left: 2px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sort-arr-down {
 | 
			
		||||
  position: relative;
 | 
			
		||||
 | 
			
		||||
  & > span {
 | 
			
		||||
    width: 0;
 | 
			
		||||
    height: 0;
 | 
			
		||||
    border-left: 6.3px solid transparent;
 | 
			
		||||
    border-right: 6.3px solid transparent;
 | 
			
		||||
    border-top: 6.3px solid #000;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 5px;
 | 
			
		||||
    display: block;
 | 
			
		||||
    left: 2px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fieldfilters {
 | 
			
		||||
  clear: both;
 | 
			
		||||
 | 
			
		||||
  & > div {
 | 
			
		||||
    display: grid;
 | 
			
		||||
    grid-template-columns: auto auto 1fr;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.filtermode {
 | 
			
		||||
  max-width: 100px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
textarea.linkmodal {
 | 
			
		||||
  display: block;
 | 
			
		||||
  min-width: 600px;
 | 
			
		||||
  min-height: 200px;
 | 
			
		||||
  border: none;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
import {Component, OnInit, ViewChild} from '@angular/core';
 | 
			
		||||
import {Component, ElementRef, isDevMode, OnInit, ViewChild} from '@angular/core';
 | 
			
		||||
import {ApiService} from '../services/api.service';
 | 
			
		||||
import {AutocompleteService} from '../services/autocomplete.service';
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -15,36 +17,77 @@ interface LoadSamplesOptions {
 | 
			
		||||
  styleUrls: ['./samples.component.scss']
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// TODO: always show first page on sort change
 | 
			
		||||
 | 
			
		||||
export class SamplesComponent implements OnInit {  // TODO: implement paging
 | 
			
		||||
 | 
			
		||||
  @ViewChild('pageSizeSelection') pageSizeSelection: HTMLElement;
 | 
			
		||||
export class SamplesComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  @ViewChild('pageSizeSelection') pageSizeSelection: ElementRef<HTMLElement>;
 | 
			
		||||
  @ViewChild('linkarea') linkarea: ElementRef<HTMLTextAreaElement>;
 | 
			
		||||
 | 
			
		||||
  customFields = [''];
 | 
			
		||||
  downloadCsv = false;
 | 
			
		||||
  materials = {};
 | 
			
		||||
  samples = [];
 | 
			
		||||
  totalSamples = 0;  // total number of samples
 | 
			
		||||
  filters = {status: {new: true, validated: true}, pageSize: 25, toPage: 0, sort: 'added-asc'};
 | 
			
		||||
  csvUrl = '';  // store url separate so it only has to be generated when clicking the download button
 | 
			
		||||
  filters = {
 | 
			
		||||
    status: {new: true, validated: true},
 | 
			
		||||
    pageSize: 25,
 | 
			
		||||
    toPage: 0,
 | 
			
		||||
    sort: 'added-asc',
 | 
			
		||||
    filters: [
 | 
			
		||||
      {field: 'number', label: 'Number', active: false, autocomplete: [], mode: 'eq', values: ['']},
 | 
			
		||||
      {field: 'material.number', label: 'Material number', active: false, autocomplete: [], mode: 'eq', values: ['']},
 | 
			
		||||
      {field: 'material.name', label: 'Material name', active: false, autocomplete: [], mode: 'eq', values: ['']},
 | 
			
		||||
      {field: 'material.supplier', label: 'Supplier', active: false, autocomplete: [], mode: 'eq', values: ['']},
 | 
			
		||||
      {field: 'material.group', label: 'Material', active: false, autocomplete: [], mode: 'eq', values: ['']},
 | 
			
		||||
      {field: 'material.glass_fiber', label: 'GF', active: false, autocomplete: [], mode: 'eq', values: ['']},
 | 
			
		||||
      {field: 'material.carbon_fiber', label: 'CF', active: false, autocomplete: [], mode: 'eq', values: ['']},
 | 
			
		||||
      {field: 'material.mineral', label: 'M', active: false, autocomplete: [], mode: 'eq', values: ['']},
 | 
			
		||||
      {field: 'type', label: 'Type', active: false, autocomplete: [], mode: 'eq', values: ['']},
 | 
			
		||||
      {field: 'color', label: 'Color', active: false, autocomplete: [], mode: 'eq', values: ['']},
 | 
			
		||||
      {field: 'batch', label: 'Batch', active: false, autocomplete: [], mode: 'eq', values: ['']},
 | 
			
		||||
      {field: 'added', label: 'Added', active: false, autocomplete: [], mode: 'eq', values: [new Date()]}
 | 
			
		||||
    ]
 | 
			
		||||
  };
 | 
			
		||||
  page = 1;
 | 
			
		||||
  loadSamplesQueue = [];  // arguments of queued up loadSamples() calls
 | 
			
		||||
  activeKeys = [
 | 
			
		||||
    {name: 'Number', key: 'number'},
 | 
			
		||||
    // {name: 'Material number', key: ''},
 | 
			
		||||
    {name: 'Material name', key: ''},
 | 
			
		||||
    {name: 'Supplier', key: ''},
 | 
			
		||||
    // {name: 'Material', key: ''},
 | 
			
		||||
    // {name: 'GF', key: ''},
 | 
			
		||||
    // {name: 'CF', key: ''},
 | 
			
		||||
    // {name: 'M', key: ''},
 | 
			
		||||
    {name: 'Type', key: 'type'},
 | 
			
		||||
    {name: 'Color', key: 'color'},
 | 
			
		||||
    {name: 'Batch', key: 'batch'},
 | 
			
		||||
    {name: 'Added', key: 'added'},
 | 
			
		||||
  apiKey = '';
 | 
			
		||||
  activeKeys =  {
 | 
			
		||||
    number: true,
 | 
			
		||||
    'material.number': true,
 | 
			
		||||
    'material.name': true,
 | 
			
		||||
    'material.supplier': true,
 | 
			
		||||
    'material.group': false,
 | 
			
		||||
    'material.glass_fiber': false,
 | 
			
		||||
    'material.carbon_fiber': false,
 | 
			
		||||
    'material.mineral': false,
 | 
			
		||||
    type: true,
 | 
			
		||||
    color: true,
 | 
			
		||||
    batch: true,
 | 
			
		||||
    added: true,
 | 
			
		||||
  };
 | 
			
		||||
  keys = [
 | 
			
		||||
    {id: 'number', label: 'Number'},
 | 
			
		||||
    {id: 'material.number', label: 'Material number'},
 | 
			
		||||
    {id: 'material.name', label: 'Material name'},
 | 
			
		||||
    {id: 'material.supplier', label: 'Supplier'},
 | 
			
		||||
    {id: 'material.group', label: 'Material'},
 | 
			
		||||
    {id: 'material.glass_fiber', label: 'GF'},
 | 
			
		||||
    {id: 'material.carbon_fiber', label: 'CF'},
 | 
			
		||||
    {id: 'material.mineral', label: 'M'},
 | 
			
		||||
    {id: 'type', label: 'Type'},
 | 
			
		||||
    {id: 'color', label: 'Color'},
 | 
			
		||||
    {id: 'batch', label: 'Batch'},
 | 
			
		||||
    {id: 'added', label: 'Added'}
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private api: ApiService
 | 
			
		||||
  ) { }
 | 
			
		||||
    private api: ApiService,
 | 
			
		||||
    public autocomplete: AutocompleteService
 | 
			
		||||
  ) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.api.get('/materials?status=all', (mData: any) => {
 | 
			
		||||
@@ -52,10 +95,30 @@ export class SamplesComponent implements OnInit {  // TODO: implement paging
 | 
			
		||||
      mData.forEach(material => {
 | 
			
		||||
        this.materials[material._id] = material;
 | 
			
		||||
      });
 | 
			
		||||
      this.filters.filters.find(e => e.field === 'material.name').autocomplete = mData.map(e => e.name);
 | 
			
		||||
      this.filters.filters.find(e => e.field === 'color').autocomplete = [...new Set(mData.reduce((s, e) => {s.push(...e.numbers.map(el => el.color)); return s; }, []))];
 | 
			
		||||
      this.loadSamples();
 | 
			
		||||
    });
 | 
			
		||||
    this.api.get('/samples/count', (data: {count: number}) => {
 | 
			
		||||
      this.totalSamples = data.count;
 | 
			
		||||
    this.api.get('/user/key', (data: {key: string}) => {
 | 
			
		||||
      this.apiKey = data.key;
 | 
			
		||||
    });
 | 
			
		||||
    this.api.get<string[]>('/material/suppliers', (data: any) => {
 | 
			
		||||
      this.filters.filters.find(e => e.field === 'material.supplier').autocomplete = data;
 | 
			
		||||
    });
 | 
			
		||||
    this.api.get<string[]>('/material/groups', (data: any) => {
 | 
			
		||||
      this.filters.filters.find(e => e.field === 'material.group').autocomplete = data;
 | 
			
		||||
    });
 | 
			
		||||
    this.api.get('/template/measurements', (data: {name: string, parameters: {name: string, range: object}[]}[]) => {
 | 
			
		||||
      const measurementKeys = [];
 | 
			
		||||
      data.forEach(item => {
 | 
			
		||||
        item.parameters.forEach(parameter => {
 | 
			
		||||
          this.activeKeys[`measurements.${item.name}.${encodeURIComponent(parameter.name)}`] = false;
 | 
			
		||||
          measurementKeys.push({id: `measurements.${item.name}.${encodeURIComponent(parameter.name)}`, label: `${this.ucFirst(item.name)} ${this.ucFirst(parameter.name)}`});
 | 
			
		||||
          this.filters.filters.push({field: `measurements.${item.name}.${parameter.name}`, label: `${this.ucFirst(item.name)} ${this.ucFirst(parameter.name)}`, active: false, autocomplete: [], mode: 'eq', values: ['']});
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
      console.log(this.filters.filters);
 | 
			
		||||
      this.keys = [...this.keys, ...measurementKeys];
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -67,26 +130,13 @@ export class SamplesComponent implements OnInit {  // TODO: implement paging
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private sampleLoader(options: LoadSamplesOptions) {  // actual loading of the sample, do not call directly
 | 
			
		||||
    const query: string[] = [];
 | 
			
		||||
    query.push('status=' + (this.filters.status.new && this.filters.status.validated ? 'all' : (this.filters.status.new ? 'new' : 'validated')));
 | 
			
		||||
    if (this.samples[0]) {  // do not include from-id when page size was changed
 | 
			
		||||
      if (!options.firstPage && (!options.event || ((options.event.target as HTMLElement).id.indexOf(this.pageSizeSelection.id) < 0))) {
 | 
			
		||||
        query.push('from-id=' + this.samples[0]._id);
 | 
			
		||||
    this.api.get(this.sampleUrl({paging: true, pagingOptions: options}), (sData, ignore, headers) => {
 | 
			
		||||
      if (!options.toPage) {
 | 
			
		||||
        this.totalSamples = headers['x-total-items'];
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        this.page = 1;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (options.toPage) {
 | 
			
		||||
      query.push('to-page=' + options.toPage);
 | 
			
		||||
    }
 | 
			
		||||
    query.push('page-size=' + this.filters.pageSize);
 | 
			
		||||
    query.push('sort=' + this.filters.sort);
 | 
			
		||||
 | 
			
		||||
    this.api.get('/samples?' + query.join('&'), sData => {
 | 
			
		||||
      this.samples = sData as any;
 | 
			
		||||
      this.samples.forEach(sample => {
 | 
			
		||||
        sample.material_number = this.materials[sample.material_id].numbers.find(e => sample.color === e.color).number;
 | 
			
		||||
        sample.material_number = sample.color === '' ? '' : this.materials[sample.material_id].numbers.find(e => sample.color === e.color).number;
 | 
			
		||||
      });
 | 
			
		||||
      this.loadSamplesQueue.shift();
 | 
			
		||||
      if (this.loadSamplesQueue.length > 0) {  // execute next queue item
 | 
			
		||||
@@ -95,6 +145,65 @@ export class SamplesComponent implements OnInit {  // TODO: implement paging
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  sampleUrl(options: {paging?: boolean, pagingOptions?: {firstPage?: boolean, toPage?: number, event?: Event}, csv?: boolean, export?: boolean, host?: boolean}) {  // return url to fetch samples
 | 
			
		||||
    const additionalTableKeys = ['material_id', '_id', 'color'];  // keys which should always be added if export = false
 | 
			
		||||
    const query: string[] = [];
 | 
			
		||||
    query.push('status=' + (this.filters.status.new && this.filters.status.validated ? 'all' : (this.filters.status.new ? 'new' : 'validated')));
 | 
			
		||||
    if (options.paging) {
 | 
			
		||||
      if (this.samples[0]) {  // do not include from-id when page size was changed
 | 
			
		||||
        if (!options.pagingOptions.firstPage) {
 | 
			
		||||
          query.push('from-id=' + this.samples[0]._id);
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
          this.page = 1;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (options.pagingOptions.toPage) {
 | 
			
		||||
        query.push('to-page=' + options.pagingOptions.toPage);
 | 
			
		||||
      }
 | 
			
		||||
      query.push('page-size=' + this.filters.pageSize);
 | 
			
		||||
    }
 | 
			
		||||
    query.push('sort=' + this.filters.sort);
 | 
			
		||||
    if (options.csv) {
 | 
			
		||||
      query.push('csv=true');
 | 
			
		||||
    }
 | 
			
		||||
    if (options.export) {
 | 
			
		||||
      query.push('key=' + this.apiKey);
 | 
			
		||||
    }
 | 
			
		||||
    Object.keys(this.activeKeys).forEach(key => {
 | 
			
		||||
      if (this.activeKeys[key] && (options.export || (!options.export && key.indexOf('material') < 0))) {  // do not load material properties for table
 | 
			
		||||
        query.push('fields[]=' + key);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    console.log(this.filters.filters);
 | 
			
		||||
 | 
			
		||||
    query.push(..._.cloneDeep(this.filters.filters)
 | 
			
		||||
      .map(e => {
 | 
			
		||||
        e.values = e.values.filter(el => el !== '');
 | 
			
		||||
        if (e.field === 'added') {
 | 
			
		||||
          console.log(e.values);
 | 
			
		||||
          e.values = e.values.map(el => new Date(new Date(el).getTime() - new Date(el).getTimezoneOffset() * 60000).toISOString());
 | 
			
		||||
        }
 | 
			
		||||
        return e;
 | 
			
		||||
      })
 | 
			
		||||
      .filter(e => e.active && e.values[0] !== '')
 | 
			
		||||
      .map(e => 'filters[]=' + encodeURIComponent(JSON.stringify(_.pick(e, ['mode', 'field', 'values']))))
 | 
			
		||||
    );
 | 
			
		||||
    console.log(this.filters);
 | 
			
		||||
    if (!options.export) {
 | 
			
		||||
      additionalTableKeys.forEach(key => {
 | 
			
		||||
      if (query.indexOf('fields[]=' + key) < 0) {  // add key if not already added
 | 
			
		||||
        query.push('fields[]=' + key);
 | 
			
		||||
      }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    else if (this.downloadCsv) {
 | 
			
		||||
      query.push('fields[]=measurements.spectrum.dpt');
 | 
			
		||||
    }
 | 
			
		||||
    console.log('/samples?' + query.join('&'));
 | 
			
		||||
    return (options.host && isDevMode() ? window.location.host : '') + (options.export ? this.api.hostName : '') + '/samples?' + query.join('&');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  loadPage(delta) {
 | 
			
		||||
    if (!/[0-9]+/.test(delta) || (this.page <= 1 && delta < 0)) {  // invalid delta
 | 
			
		||||
      return;
 | 
			
		||||
@@ -103,6 +212,24 @@ export class SamplesComponent implements OnInit {  // TODO: implement paging
 | 
			
		||||
    this.loadSamples({toPage: delta});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateFilterFields(field) {
 | 
			
		||||
    const filter = this.filters.filters.find(e => e.field === field);
 | 
			
		||||
    if (filter.mode === 'in' || filter.mode === 'nin') {
 | 
			
		||||
      if (filter.values[filter.values.length - 1] === '' && filter.values[filter.values.length - 2] === '') {
 | 
			
		||||
        filter.values.pop();
 | 
			
		||||
      }
 | 
			
		||||
      else if (filter.values[filter.values.length - 1] !== '') {
 | 
			
		||||
        filter.values.push((filter.field === 'added' ? new Date() : '') as string & Date);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      filter.values = [filter.values[0] as string & Date];
 | 
			
		||||
    }
 | 
			
		||||
    if (filter.active) {
 | 
			
		||||
      this.loadSamples({firstPage: true});
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pages() {
 | 
			
		||||
    return Math.ceil(this.totalSamples / this.filters.pageSize);
 | 
			
		||||
  }
 | 
			
		||||
@@ -111,4 +238,28 @@ export class SamplesComponent implements OnInit {  // TODO: implement paging
 | 
			
		||||
    this.filters.sort = string;
 | 
			
		||||
    this.loadSamples({firstPage: true});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  activeKeysArray() {  // array with all activeKeys names
 | 
			
		||||
    return Object.keys(this.activeKeys).filter(e => this.activeKeys[e] === true).map(e => this.keys.find(el => el.id === e));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  activeMeasurementKeys() {
 | 
			
		||||
    return Object.keys(this.activeKeys).filter(e => e.indexOf('measurements.') >= 0 && this.activeKeys[e]).map(e => e.split('.').map(el => decodeURIComponent(el)));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  preventDefault(event, key = 'all') {
 | 
			
		||||
    if (key === 'all' || event.key === key) {
 | 
			
		||||
      event.preventDefault();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  clipboard() {
 | 
			
		||||
    this.linkarea.nativeElement.select();
 | 
			
		||||
    this.linkarea.nativeElement.setSelectionRange(0, 99999);
 | 
			
		||||
    document.execCommand('copy');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ucFirst(string) {
 | 
			
		||||
    return string[0].toUpperCase() + string.slice(1);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { Injectable, isDevMode } from '@angular/core';
 | 
			
		||||
import {HttpClient, HttpHeaders} from '@angular/common/http';
 | 
			
		||||
import {LocalStorageService} from 'angular-2-local-storage';
 | 
			
		||||
import {Observable} from 'rxjs';
 | 
			
		||||
@@ -11,7 +11,7 @@ import {ModalService} from '@inst-iot/bosch-angular-ui-components';
 | 
			
		||||
})
 | 
			
		||||
export class ApiService {
 | 
			
		||||
 | 
			
		||||
  private host = '/api';
 | 
			
		||||
  private host = isDevMode() ? '/api' : 'https://definma-api.apps.de1.bosch-iot-cloud.com';
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private http: HttpClient,
 | 
			
		||||
@@ -19,25 +19,29 @@ export class ApiService {
 | 
			
		||||
    private modalService: ModalService
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  get<T>(url, f: (data?: T, err?) => void = () => {}) {
 | 
			
		||||
    this.requestErrorHandler<T>(this.http.get(this.host + url, this.authOptions()), f);
 | 
			
		||||
  get hostName() {
 | 
			
		||||
    return this.host;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  post<T>(url, data = null, f: (data?: T, err?) => void = () => {}) {
 | 
			
		||||
    this.requestErrorHandler<T>(this.http.post(this.host + url, data, this.authOptions()), f);
 | 
			
		||||
  get<T>(url, f: (data?: T, err?, headers?) => void = () => {}) {
 | 
			
		||||
    this.requestErrorHandler<T>(this.http.get(this.host + url, this.options()), f);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  put<T>(url, data = null, f: (data?: T, err?) => void = () => {}) {
 | 
			
		||||
    this.requestErrorHandler<T>(this.http.put(this.host + url, data, this.authOptions()), f);
 | 
			
		||||
  post<T>(url, data = null, f: (data?: T, err?, headers?) => void = () => {}) {
 | 
			
		||||
    this.requestErrorHandler<T>(this.http.post(this.host + url, data, this.options()), f);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  delete<T>(url, f: (data?: T, err?) => void = () => {}) {
 | 
			
		||||
    this.requestErrorHandler<T>(this.http.delete(this.host + url, this.authOptions()), f);
 | 
			
		||||
  put<T>(url, data = null, f: (data?: T, err?, headers?) => void = () => {}) {
 | 
			
		||||
    this.requestErrorHandler<T>(this.http.put(this.host + url, data, this.options()), f);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private requestErrorHandler<T>(observable: Observable<any>, f: (data?: T, err?) => void) {
 | 
			
		||||
  delete<T>(url, f: (data?: T, err?, headers?) => void = () => {}) {
 | 
			
		||||
    this.requestErrorHandler<T>(this.http.delete(this.host + url, this.options()), f);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private requestErrorHandler<T>(observable: Observable<any>, f: (data?: T, err?, headers?) => void) {
 | 
			
		||||
    observable.subscribe(data => {
 | 
			
		||||
      f(data, undefined);
 | 
			
		||||
      f(data.body, undefined, data.headers.keys().reduce((s, e) => {s[e.toLowerCase()] = data.headers.get(e); return s; }, {}));
 | 
			
		||||
    }, err => {
 | 
			
		||||
      if (f.length === 2) {
 | 
			
		||||
        f(undefined, err);
 | 
			
		||||
@@ -49,13 +53,17 @@ export class ApiService {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private authOptions() {
 | 
			
		||||
  private options(): {headers: HttpHeaders, observe: 'body'} {
 | 
			
		||||
    return {headers: this.authOptions(), observe: 'response' as 'body'};
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private authOptions(): HttpHeaders {
 | 
			
		||||
    const auth = this.storage.get('basicAuth');
 | 
			
		||||
    if (auth) {
 | 
			
		||||
      return {headers: new HttpHeaders({Authorization: 'Basic ' + auth})};
 | 
			
		||||
      return new HttpHeaders({Authorization: 'Basic ' + auth});
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      return {};
 | 
			
		||||
      return new HttpHeaders();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -56,4 +56,8 @@ export class LoginService implements CanActivate {
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get isLoggedIn() {
 | 
			
		||||
    return this.loggedIn;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user