Merge pull request #25 in ~VLE2FE/definma-ui from development to master
* commit '602dfb51daf7c417d14797b6319e5528ba9aba54': added batch edit added help component, improved prediction with model templates
This commit is contained in:
		| @@ -11,12 +11,14 @@ import {UsersComponent} from './users/users.component'; | ||||
| import {ChangelogComponent} from './changelog/changelog.component'; | ||||
| import {DocumentationDatabaseComponent} from './documentation-database/documentation-database.component'; | ||||
| import {PredictionComponent} from './prediction/prediction.component'; | ||||
| import {ModelTemplatesComponent} from './model-templates/model-templates.component'; | ||||
|  | ||||
|  | ||||
| const routes: Routes = [ | ||||
|   {path: '', component: HomeComponent}, | ||||
|   {path: 'home', component: HomeComponent}, | ||||
|   {path: 'prediction', component: PredictionComponent}, | ||||
|   {path: 'models', component: ModelTemplatesComponent}, | ||||
|   {path: 'samples', component: SamplesComponent, canActivate: [LoginService]}, | ||||
|   {path: 'samples/new', component: SampleComponent, canActivate: [LoginService]}, | ||||
|   {path: 'samples/edit/:id', component: SampleComponent, canActivate: [LoginService]}, | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| <rb-full-header id="top"> | ||||
|   <nav *rbMainNavItems> | ||||
|     <a routerLink="/home" routerLinkActive="active" rbLoadingLink>Home</a> | ||||
|     <a routerLink="/prediction" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.admin">Prediction</a> | ||||
|     <a routerLink="/prediction" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.dev">Prediction</a> | ||||
|     <a routerLink="/models" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.dev">Models</a> | ||||
|     <a routerLink="/samples" routerLinkActive="active" rbLoadingLink *ngIf="login.isLoggedIn">Samples</a> | ||||
|     <a routerLink="/templates" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.dev"> | ||||
|       Templates | ||||
| @@ -18,6 +19,10 @@ | ||||
|     </nav> | ||||
|   </ng-container> | ||||
|  | ||||
|   <nav *rbMetaNavItems> | ||||
|     <span class="rb-ic rb-ic-question-frame clickable space-above" (click)="help()"></span> | ||||
|   </nav> | ||||
|  | ||||
|   <ng-container *ngIf="login.isLoggedIn"> | ||||
|     <nav *rbActionNavItems> | ||||
|       <a href="javascript:"  [rbPopover]="userPopover" [anchor]="popoverAnchor"> | ||||
| @@ -55,3 +60,12 @@ | ||||
|     <rb-icon-button icon="up" mode="primary" iconOnly (click)="toTheTop()"></rb-icon-button> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| <rb-footer-nav> | ||||
|   <span class="copyright"> | ||||
|     CR/APS1 and CR/ANA1 2020 | ||||
|   </span> | ||||
|   <nav> | ||||
|     <a [href]="'mailto:' + d.contact">Contact</a> | ||||
|   </nav> | ||||
| </rb-footer-nav> | ||||
|   | ||||
| @@ -16,6 +16,7 @@ | ||||
|  | ||||
| .to-the-top-container { | ||||
|   position: relative; | ||||
|   overflow: hidden; | ||||
| } | ||||
|  | ||||
| .to-the-top { | ||||
|   | ||||
| @@ -1,6 +1,9 @@ | ||||
| import {Component, isDevMode, OnInit} from '@angular/core'; | ||||
| import {LoginService} from './services/login.service'; | ||||
| import {NavigationStart, Router} from '@angular/router'; | ||||
| import {ModalService} from '@inst-iot/bosch-angular-ui-components'; | ||||
| import {HelpComponent} from './help/help.component'; | ||||
| import {DataService} from './services/data.service'; | ||||
|  | ||||
|  | ||||
| // TODO: get rid of chart.js (+moment.js) | ||||
| @@ -19,7 +22,9 @@ export class AppComponent implements OnInit{ | ||||
|   constructor( | ||||
|     public login: LoginService, | ||||
|     public router: Router, | ||||
|     private window: Window | ||||
|     private window: Window, | ||||
|     private modal: ModalService, | ||||
|     public d: DataService | ||||
|   ) { | ||||
|     this.devMode = isDevMode(); | ||||
|     this.router.events.subscribe(event => { | ||||
| @@ -30,7 +35,11 @@ export class AppComponent implements OnInit{ | ||||
|   } | ||||
|  | ||||
|   ngOnInit() { | ||||
|     this.login.login(); | ||||
|     this.login.login().then(res => { | ||||
|       if (!res) { | ||||
|         this.router.navigate(['/']); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   logout() { | ||||
| @@ -38,6 +47,10 @@ export class AppComponent implements OnInit{ | ||||
|     this.router.navigate(['/']); | ||||
|   } | ||||
|  | ||||
|   help() { | ||||
|     this.modal.openComponent(HelpComponent); | ||||
|   } | ||||
|  | ||||
|   bugReportContent() { | ||||
|     return `mailto:lukas.veit@de.bosch.com?subject=Bug report&body=Thanks for sending the report! Your bug will be (hopefully) fixed soon. | ||||
| %0D%0A%0D%0A--- REPORT DATA --- | ||||
|   | ||||
| @@ -30,6 +30,8 @@ import { DocumentationDatabaseComponent } from './documentation-database/documen | ||||
| import { PredictionComponent } from './prediction/prediction.component'; | ||||
| import { ServiceWorkerModule } from '@angular/service-worker'; | ||||
| import { environment } from '../environments/environment'; | ||||
| import { HelpComponent } from './help/help.component'; | ||||
| import { ModelTemplatesComponent } from './model-templates/model-templates.component'; | ||||
|  | ||||
| @NgModule({ | ||||
|   declarations: [ | ||||
| @@ -50,7 +52,9 @@ import { environment } from '../environments/environment'; | ||||
|     UsersComponent, | ||||
|     ChangelogComponent, | ||||
|     DocumentationDatabaseComponent, | ||||
|     PredictionComponent | ||||
|     PredictionComponent, | ||||
|     HelpComponent, | ||||
|     ModelTemplatesComponent | ||||
|   ], | ||||
|   imports: [ | ||||
|     LocalStorageModule.forRoot({ | ||||
|   | ||||
							
								
								
									
										14
									
								
								src/app/help/help.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/app/help/help.component.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| <h3>Help</h3> | ||||
|  | ||||
| <ng-container [ngSwitch]="route"> | ||||
|  <p *ngSwitchCase="'/'">Please log in for further access. If you do not have an account yet, please contact | ||||
|    <a [href]="'mailto:' + d.contact">{{d.contact}}</a>. | ||||
|  </p> | ||||
|  <p *ngSwitchCase="'/home'">Please log in for further access. If you do not have an account yet, please contact | ||||
|    <a [href]="'mailto:' + d.contact">{{d.contact}}</a>. | ||||
|  </p> | ||||
|  <p *ngSwitchDefault> | ||||
|    Sadly, currently there is no help available for this page. Please contact | ||||
|    <a [href]="'mailto:' + d.contact">{{d.contact}}</a> for further questions. | ||||
|  </p> | ||||
| </ng-container> | ||||
							
								
								
									
										0
									
								
								src/app/help/help.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/app/help/help.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										25
									
								
								src/app/help/help.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/app/help/help.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| import { async, ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { HelpComponent } from './help.component'; | ||||
|  | ||||
| describe('HelpComponent', () => { | ||||
|   let component: HelpComponent; | ||||
|   let fixture: ComponentFixture<HelpComponent>; | ||||
|  | ||||
|   beforeEach(async(() => { | ||||
|     TestBed.configureTestingModule({ | ||||
|       declarations: [ HelpComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   })); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(HelpComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										24
									
								
								src/app/help/help.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/app/help/help.component.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import {Router} from '@angular/router'; | ||||
| import {DataService} from '../services/data.service'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-help', | ||||
|   templateUrl: './help.component.html', | ||||
|   styleUrls: ['./help.component.scss'] | ||||
| }) | ||||
| export class HelpComponent implements OnInit { | ||||
|  | ||||
|   route = ''; | ||||
|  | ||||
|   constructor( | ||||
|     private router: Router, | ||||
|     public d: DataService | ||||
|   ) { } | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|     this.route = this.router.url.replace(/\/[0-9a-f]{24}/, '');  // remove ids | ||||
|     console.log(this.route); | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										65
									
								
								src/app/model-templates/model-templates.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/app/model-templates/model-templates.component.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| <h2>Models</h2> | ||||
|  | ||||
| <rb-icon-button icon="add" mode="primary" (click)="newModel = !newModel" class="space-below">New model</rb-icon-button> | ||||
|  | ||||
| <form *ngIf="newModel" #modelForm="ngForm"> | ||||
|   <rb-form-input name="group" label="group" appValidate="string" required [(ngModel)]="modelGroup" #groupInput="ngModel" | ||||
|                  [rbFormInputAutocomplete]="autocomplete.bind(this, groups)" | ||||
|                  [rbDebounceTime]="0" [rbInitialOpen]="true"> | ||||
|     <ng-template rbFormValidationMessage="failure">{{groupInput.errors.failure}}</ng-template> | ||||
|     <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> | ||||
|   </rb-form-input> | ||||
|   <rb-form-input name="name" label="name" appValidate="string" required [(ngModel)]="model.name" #nameInput="ngModel"> | ||||
|     <ng-template rbFormValidationMessage="failure">{{nameInput.errors.failure}}</ng-template> | ||||
|     <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> | ||||
|   </rb-form-input> | ||||
|   <rb-form-input name="label" label="label" appValidate="string" [(ngModel)]="model.label" #labelInput="ngModel"> | ||||
|     <ng-template rbFormValidationMessage="failure">{{labelInput.errors.failure}}</ng-template> | ||||
|   </rb-form-input> | ||||
|   <rb-form-input name="url" label="URL" appValidate="url" required [(ngModel)]="model.url" #urlInput="ngModel"> | ||||
|     <ng-template rbFormValidationMessage="failure">{{urlInput.errors.failure}}</ng-template> | ||||
|     <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> | ||||
|   </rb-form-input> | ||||
|  | ||||
|   <rb-icon-button icon="save" mode="primary" type="submit" [disabled]="!modelForm.form.valid" (click)="saveModel()"> | ||||
|     Save model | ||||
|   </rb-icon-button> | ||||
| </form> | ||||
|  | ||||
| <rb-table class="space-above"> | ||||
|   <tr> | ||||
|     <th>Name</th> | ||||
|     <th>Label</th> | ||||
|     <th>URL</th> | ||||
|     <th></th> | ||||
|     <th></th> | ||||
|   </tr> | ||||
|  | ||||
|   <ng-container *ngFor="let group of d.arr.modelGroups"> | ||||
|     <tr><th>{{group.group}}</th><th></th><th></th><th></th><th></th></tr> | ||||
|     <tr *ngFor="let modelItem of group.models"> | ||||
|       <td>{{modelItem.name}}</td> | ||||
|       <td>{{modelItem.label}}</td> | ||||
|       <td>{{modelItem.url}}</td> | ||||
|       <td> | ||||
|         <span class="rb-ic rb-ic-edit clickable" | ||||
|               (click)="modelGroup = group.group; | ||||
|                        oldModelGroup = group.group; | ||||
|                        oldModelName = modelItem.name; | ||||
|                        model = modelItem; | ||||
|                        newModel = true;"> | ||||
|         </span> | ||||
|       </td> | ||||
|       <td> | ||||
|         <span class="rb-ic rb-ic-delete clickable" | ||||
|               (click)="deleteModel(group.group, modelItem.name, modalDeleteConfirm)"></span> | ||||
|       </td> | ||||
|     </tr> | ||||
|   </ng-container> | ||||
| </rb-table> | ||||
|  | ||||
| <ng-template #modalDeleteConfirm> | ||||
|   <rb-alert alertTitle="Are you sure?" type="danger" okBtnLabel="Delete model" cancelBtnLabel="Cancel"> | ||||
|     Do you really want to delete this model? | ||||
|   </rb-alert> | ||||
| </ng-template> | ||||
							
								
								
									
										25
									
								
								src/app/model-templates/model-templates.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/app/model-templates/model-templates.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| import { async, ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { ModelTemplatesComponent } from './model-templates.component'; | ||||
|  | ||||
| describe('ModelTemplatesComponent', () => { | ||||
|   let component: ModelTemplatesComponent; | ||||
|   let fixture: ComponentFixture<ModelTemplatesComponent>; | ||||
|  | ||||
|   beforeEach(async(() => { | ||||
|     TestBed.configureTestingModule({ | ||||
|       declarations: [ ModelTemplatesComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   })); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(ModelTemplatesComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										74
									
								
								src/app/model-templates/model-templates.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/app/model-templates/model-templates.component.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import {DataService} from '../services/data.service'; | ||||
| import {ModelItemModel} from '../models/model-item.model'; | ||||
| import {ApiService} from '../services/api.service'; | ||||
| import {AutocompleteService} from '../services/autocomplete.service'; | ||||
| import {ModalService} from '@inst-iot/bosch-angular-ui-components'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-model-templates', | ||||
|   templateUrl: './model-templates.component.html', | ||||
|   styleUrls: ['./model-templates.component.scss'] | ||||
| }) | ||||
| export class ModelTemplatesComponent implements OnInit { | ||||
|  | ||||
|   newModel = false; | ||||
|   modelGroup = ''; | ||||
|   oldModelGroup = ''; | ||||
|   oldModelName = ''; | ||||
|   model = new ModelItemModel().models[0]; | ||||
|   groups = []; | ||||
|  | ||||
|   constructor( | ||||
|     private api: ApiService, | ||||
|     public autocomplete: AutocompleteService, | ||||
|     public d: DataService, | ||||
|     private modal: ModalService | ||||
|   ) { } | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|     this.loadGroups(); | ||||
|   } | ||||
|  | ||||
|   loadGroups() { | ||||
|     delete this.d.arr.modelGroups; | ||||
|     this.d.load('modelGroups', () => { | ||||
|       this.groups = this.d.arr.modelGroups.map(e => e.group); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   saveModel() { | ||||
|     if (this.modelGroup !== this.oldModelGroup) {  // group was changed, delete model in old group | ||||
|       this.deleteModel(this.oldModelGroup, this.oldModelName); | ||||
|     } | ||||
|     this.api.post('/model/' + this.modelGroup, this.model, () => { | ||||
|       this.newModel = false; | ||||
|       this.loadGroups(); | ||||
|       this.modelGroup = ''; | ||||
|       this.oldModelGroup = ''; | ||||
|       this.oldModelName = ''; | ||||
|       this.model = new ModelItemModel().models[0]; | ||||
|       this.groups = this.d.arr.modelGroups.map(e => e.group); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   deleteModel(group, name, modal = null) { | ||||
|     new Promise(resolve => { | ||||
|       if (modal) { | ||||
|         this.modal.open(modal).then(result => { | ||||
|           resolve(result); | ||||
|         }); | ||||
|       } | ||||
|       else { | ||||
|         resolve(true); | ||||
|       } | ||||
|     }).then(res => { | ||||
|       if (res) { | ||||
|         this.api.delete(`/model/${group}/${name}`, () => { | ||||
|           this.loadGroups(); | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -8,6 +8,7 @@ export class MeasurementModel extends BaseModel { | ||||
|   sample_id: IdModel = null; | ||||
|   measurement_template: IdModel; | ||||
|   values: {[prop: string]: any} = {}; | ||||
|   status = ''; | ||||
|  | ||||
|   constructor(measurementTemplate: IdModel = null) { | ||||
|     super(); | ||||
|   | ||||
							
								
								
									
										7
									
								
								src/app/models/model-item.model.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/app/models/model-item.model.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| import { ModelItemModel } from './model-item.model'; | ||||
|  | ||||
| describe('ModelItemModel', () => { | ||||
|   it('should create an instance', () => { | ||||
|     expect(new ModelItemModel()).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										10
									
								
								src/app/models/model-item.model.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/app/models/model-item.model.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import {BaseModel} from './base.model'; | ||||
|  | ||||
| export class ModelItemModel extends BaseModel { | ||||
|   group = ''; | ||||
|   models = [{ | ||||
|     name: '', | ||||
|     url: '', | ||||
|     label: '' | ||||
|   }]; | ||||
| } | ||||
| @@ -17,7 +17,7 @@ export class SampleModel extends BaseModel { | ||||
|   measurements: MeasurementModel[] = []; | ||||
|   note_id: IdModel = null; | ||||
|   user_id: IdModel = null; | ||||
|   validate = false; | ||||
|   selected = false; | ||||
|   notes: { | ||||
|     comment: string, | ||||
|     sample_references: {sample_id: IdModel, relation: string}[], | ||||
| @@ -42,7 +42,16 @@ export class SampleModel extends BaseModel { | ||||
|   } | ||||
|  | ||||
|   sendFormat() { | ||||
|     return pick(this.conditionTemplateCheck(), ['color', 'type', 'batch', 'condition', 'material_id', 'notes']); | ||||
|     const tmp = pick(this.conditionTemplateCheck(), ['color', 'type', 'batch', 'condition', 'material_id', 'notes']); | ||||
|     Object.keys(tmp).forEach(key => { | ||||
|       if (tmp[key] === undefined) { | ||||
|         delete tmp[key]; | ||||
|       } | ||||
|     }); | ||||
|     if (this.material && this.material.name === undefined) { | ||||
|       delete tmp.material_id; | ||||
|     } | ||||
|     return tmp; | ||||
|   } | ||||
|  | ||||
|   private conditionTemplateCheck() { | ||||
|   | ||||
| @@ -7,10 +7,4 @@ export class TemplateModel extends BaseModel { | ||||
|   version = 0; | ||||
|   first_id: IdModel = null; | ||||
|   parameters: {name: string, range: {[prop: string]: any}, rangeString?: string}[] = []; | ||||
|  | ||||
|   deserialize(input: any): this { | ||||
|     Object.assign(this, input); | ||||
|  | ||||
|     return this; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,12 +1,57 @@ | ||||
| <h2>Prediction</h2> | ||||
|  | ||||
| <h4 *ngIf="result !== '' || loading" [@inOut]> | ||||
|   Result: {{result}}<rb-loading-spinner *ngIf="loading"></rb-loading-spinner> | ||||
| </h4> | ||||
| <rb-tab-panel (tabChanged)="groupChange($event)"> | ||||
|   <ng-container *ngFor="let group of d.arr.modelGroups; index as i"> | ||||
|     <div *rbTabPanelItem="group.group; id: i"></div> | ||||
|   </ng-container> | ||||
| </rb-tab-panel> | ||||
|  | ||||
| <rb-form-file name="spectrum-upload" label="spectrum file" maxSize="10000000" class="space-below" | ||||
|               (ngModelChange)="fileToArray($event)" placeholder="Select file or drag and drop" dragDrop ngModel> | ||||
| </rb-form-file> | ||||
| <rb-form-select label="Model" (change)="result = undefined" [(ngModel)]="activeModelIndex"> | ||||
|   <option *ngFor="let model of activeGroup.models; index as i" [value]="i">{{model.name}}</option> | ||||
| </rb-form-select> | ||||
|  | ||||
| <div *ngIf="result" class="result" [@inOut]> | ||||
|   <ng-container *ngIf="multipleSamples; else singleSampleResult"> | ||||
|     <h4 *ngFor="let prediction of result.predictions; index as i"> | ||||
|       {{spectrumNames[i]}}: {{prediction}} {{activeGroup.models[activeModelIndex].label}} | ||||
|     </h4> | ||||
|   </ng-container> | ||||
|   <ng-template #singleSampleResult> | ||||
|     <h4> | ||||
|       Average result: {{result.meanPrediction}} {{activeGroup.models[activeModelIndex].label}}, | ||||
|       standard deviation: {{result.std}} | ||||
|     </h4> | ||||
|     <a href="javascript:" class="rb-details-toggle" rbDetailsToggle #triggerDetails="rbDetailsToggle">Details</a> | ||||
|     <div *ngIf="triggerDetails.open" class="space-below"> | ||||
|       <p *ngFor="let prediction of result.predictions; index as i"> | ||||
|         {{spectrumNames[i]}}: {{prediction}} {{activeGroup.models[activeModelIndex].label}} | ||||
|       </p> | ||||
|     </div> | ||||
|   </ng-template> | ||||
| </div> | ||||
|  | ||||
| <div class="file-input space-below"> | ||||
|   <rb-form-file name="spectrum-upload" label="spectrum file" maxSize="10000000" class="space-below" multiple | ||||
|                 (ngModelChange)="fileToArray($event)" placeholder="Select file or drag and drop" dragDrop ngModel> | ||||
|   </rb-form-file> | ||||
|  | ||||
|   <rb-loading-spinner *ngIf="loading; else predictButton"></rb-loading-spinner> | ||||
|   <ng-template #predictButton> | ||||
|     <rb-icon-button icon="forward-right" mode="primary" *ngIf="spectrumNames.length; else placeholder" | ||||
|                     (click)="loadPrediction()"> | ||||
|       Predict | ||||
|     </rb-icon-button> | ||||
|     <ng-template #placeholder><div></div></ng-template> | ||||
|   </ng-template> | ||||
|  | ||||
|   <div> | ||||
|     Prediction of: | ||||
|     <rb-form-radio name="multiple-samples" label="Single sample" [(ngModel)]="multipleSamples" [value]="false"> | ||||
|     </rb-form-radio> | ||||
|     <rb-form-radio name="multiple-samples" label="Multiple samples" [(ngModel)]="multipleSamples" [value]="true"> | ||||
|     </rb-form-radio> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| <div class="dpt-chart"> | ||||
|   <canvas baseChart | ||||
|   | ||||
| @@ -2,3 +2,17 @@ | ||||
|   max-width: 800px; | ||||
|   margin: 0 auto; | ||||
| } | ||||
|  | ||||
| .file-input { | ||||
|   display: grid; | ||||
|   grid-template-columns: 1fr auto; | ||||
|   grid-column-gap: 1rem; | ||||
| } | ||||
|  | ||||
| .result { | ||||
|   margin: 30px 0; | ||||
|  | ||||
|   h4 { | ||||
|     margin-bottom: 1rem; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -2,6 +2,15 @@ import { Component, OnInit } from '@angular/core'; | ||||
| import {ChartOptions} from 'chart.js'; | ||||
| import {ApiService} from '../services/api.service'; | ||||
| import {animate, style, transition, trigger} from '@angular/animations'; | ||||
| import cloneDeep from 'lodash/cloneDeep'; | ||||
| import {DataService} from '../services/data.service'; | ||||
| import {ModelItemModel} from '../models/model-item.model'; | ||||
|  | ||||
| interface PredictionResult { | ||||
|   meanPrediction: string; | ||||
|   std: string; | ||||
|   predictions: string[]; | ||||
| } | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-prediction', | ||||
| @@ -24,11 +33,16 @@ import {animate, style, transition, trigger} from '@angular/animations'; | ||||
| }) | ||||
| export class PredictionComponent implements OnInit { | ||||
|  | ||||
|   readonly predictionUrl = 'https://definma-model-test.apps.de1.bosch-iot-cloud.com/predict'; | ||||
|   result = ''; | ||||
|   result: PredictionResult; | ||||
|   loading = false; | ||||
|   activeGroup: ModelItemModel = new ModelItemModel(); | ||||
|   activeModelIndex = 0; | ||||
|   multipleSamples = false;  // if true, spectra belong to different samples, otherwise multiple spectra from the same sample are given | ||||
|   spectrumNames: string[] = []; | ||||
|   spectrum: string[][] = [[]]; | ||||
|   chart = [{ | ||||
|   flattenedSpectra = []; | ||||
|   chart = []; | ||||
|   readonly chartInit = { | ||||
|     data: [], | ||||
|     label: 'Spectrum', | ||||
|     showLine: true, | ||||
| @@ -36,7 +50,7 @@ export class PredictionComponent implements OnInit { | ||||
|     pointRadius: 0, | ||||
|     borderColor: '#00a8b0', | ||||
|     borderWidth: 2 | ||||
|   }]; | ||||
|   }; | ||||
|   readonly chartOptions: ChartOptions = { | ||||
|     scales: { | ||||
|       xAxes: [{ticks: {min: 400, max: 4000, stepSize: 400, reverse: true}}], | ||||
| @@ -50,25 +64,53 @@ export class PredictionComponent implements OnInit { | ||||
|   }; | ||||
|  | ||||
|   constructor( | ||||
|     private api: ApiService | ||||
|   ) { } | ||||
|     private api: ApiService, | ||||
|     public d: DataService | ||||
|   ) { | ||||
|     this.chart[0] = cloneDeep(this.chartInit); | ||||
|   } | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|     this.d.load('modelGroups', () => { | ||||
|       this.activeGroup = this.d.arr.modelGroups[0]; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   fileToArray(files) { | ||||
|     const fileReader = new FileReader(); | ||||
|     fileReader.onload = () => { | ||||
|       this.spectrum = fileReader.result.toString().split('\r\n').map(e => e.split(',')); | ||||
|       this.loading = true; | ||||
|       this.api.post<{result: string}>(this.predictionUrl, this.spectrum, data => { | ||||
|         this.result = data.result; | ||||
|         this.loading = false; | ||||
|       }); | ||||
|       this.chart[0].data = this.spectrum.map(e => ({x: parseFloat(e[0]), y: parseFloat(e[1])})); | ||||
|       console.log(this.chart); | ||||
|     }; | ||||
|     fileReader.readAsText(files[0]); | ||||
|     this.loading = true; | ||||
|     this.flattenedSpectra = []; | ||||
|     this.chart = []; | ||||
|     let load = files.length; | ||||
|     this.spectrumNames = files.map(e => e.name); | ||||
|     for (const i in files) { | ||||
|       if (files.hasOwnProperty(i)) { | ||||
|         const fileReader = new FileReader(); | ||||
|         fileReader.onload = () => { | ||||
|           this.spectrum = fileReader.result.toString().split('\r\n').map(e => e.split(',').map(el => parseFloat(el))) as any; | ||||
|           this.flattenedSpectra[i] = {labels: this.spectrum.map(e => e[0]), values: this.spectrum.map(e => e[1])}; | ||||
|           this.chart[i] = cloneDeep(this.chartInit); | ||||
|           this.chart[i].data = this.spectrum.map(e => ({x: parseFloat(e[0]), y: parseFloat(e[1])})); | ||||
|           load --; | ||||
|           if (load <= 0) { | ||||
|             this.loadPrediction(); | ||||
|           } | ||||
|         }; | ||||
|         fileReader.readAsText(files[i]); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   loadPrediction() { | ||||
|     this.loading = true; | ||||
|     console.log(this.activeModelIndex); | ||||
|     this.api.post<PredictionResult>(this.activeGroup.models[this.activeModelIndex].url, this.flattenedSpectra, data => { | ||||
|       this.result = data; | ||||
|       this.loading = false; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   groupChange(index) { | ||||
|     this.activeGroup = this.d.arr.modelGroups[index]; | ||||
|     this.result = undefined; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -129,13 +129,38 @@ export class RbArrayInputComponent implements ControlValueAccessor, OnInit, Afte | ||||
|   } | ||||
|  | ||||
|   writeValue(obj: any) {  // add empty value on init | ||||
|     this.values = obj ? obj : []; | ||||
|     if (this.values.length === 0 || this.values[0] !== '') { | ||||
|       // add empty last field if pushTemplate is specified | ||||
|     if (obj) { | ||||
|       if (this.pushTemplate !== null) { | ||||
|         this.values.push(cloneDeep(this.pushTemplate)); | ||||
|         this.values = [...obj.filter(e => e[this.pushPath] !== ''), cloneDeep(this.pushTemplate)]; | ||||
|       } | ||||
|       else { | ||||
|         this.values = obj; | ||||
|       } | ||||
|     } | ||||
|     else { | ||||
|       if (this.pushTemplate !== null) { | ||||
|         this.values = [cloneDeep(this.pushTemplate)]; | ||||
|       } | ||||
|       else { | ||||
|         this.values = ['']; | ||||
|       } | ||||
|     } | ||||
|     // this.values = obj ? obj : []; | ||||
|     // console.log('-----'); | ||||
|     // console.log(obj); | ||||
|     // console.log(this.pushPath); | ||||
|     // if (this.values && this.values.length) { | ||||
|     //   this.values = obj.filter(e => this.pushPath ? e[this.pushPath] !== '' : e !== ''); | ||||
|     // } | ||||
|     // console.log(this.values); | ||||
|     // // console.log(obj.filter(e => this.pushPath ? e[this.pushPath] !== '' : e !== '')); | ||||
|     // // this.values = obj ? obj.filter(e => this.pushPath ? e[this.pushPath] !== '' : e !== '') : []; | ||||
|     // if (this.values.length === 0 || this.values[0] !== '') { | ||||
|     //   // add empty last field if pushTemplate is specified | ||||
|     //   if (this.pushTemplate !== null) { | ||||
|     //     this.values.push(cloneDeep(this.pushTemplate)); | ||||
|     //   } | ||||
|     // } | ||||
|   } | ||||
|  | ||||
|   registerOnChange(fn: any) { | ||||
|   | ||||
| @@ -1,263 +1,324 @@ | ||||
| <h2>{{new ? 'Add new sample' : 'Edit sample ' + sample.number}}</h2> | ||||
| <h2>{{mode === 'new' ? 'Add new sample' : 'Edit sample '}}</h2> | ||||
| <!--TODO: title--> | ||||
|  | ||||
| <rb-loading-spinner *ngIf="loading"></rb-loading-spinner> | ||||
| <rb-loading-spinner *ngIf="loading; else content"></rb-loading-spinner> | ||||
|  | ||||
| <form #sampleForm="ngForm" *ngIf="(!generatedSamples.length || editSampleBase) && !loading"> | ||||
|   <div class="sample"> | ||||
|     <div> | ||||
|       <rb-form-input name="materialname" label="material name" [rbDebounceTime]="0" [rbInitialOpen]="true" | ||||
|                      [rbFormInputAutocomplete]="autocomplete.bind(this, materialNames)" appValidate="stringOf" | ||||
|                      (keydown)="preventDefault($event)" (ngModelChange)="findMaterial($event)" ngModel | ||||
|                      [appValidateArgs]="[materialNames]" required [(ngModel)]="material.name" [autofocus]="true" | ||||
|                      title="trade name of the material, eg. Ultradur B4300 G6"> | ||||
|         <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> | ||||
|         <ng-template rbFormValidationMessage="failure">Unknown material, add properties for new material</ng-template> | ||||
|       </rb-form-input> | ||||
|       <rb-icon-button class="set-new-material space-below" icon="add" mode="secondary" | ||||
|                       (click)="setNewMaterial(!newMaterial)">New material</rb-icon-button> | ||||
|     </div> | ||||
| <ng-template #content> | ||||
|  | ||||
|     <div class="material shaded-container" *ngIf="newMaterial" [@inOut]> | ||||
|       <h4>Material properties</h4> | ||||
|       <rb-form-input name="supplier" label="supplier" | ||||
|                      [rbFormInputAutocomplete]="autocomplete.bind(this, d.arr.materialSuppliers)" | ||||
|                      [rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required | ||||
|                      [(ngModel)]="material.supplier" #supplierInput="ngModel" | ||||
|                      (focusout)="checkTypo($event, 'materialSuppliers', 'supplier', modalWarning)" | ||||
|                      title="material supplier, eg. BASF"> | ||||
|         <ng-template rbFormValidationMessage="failure">{{supplierInput.errors.failure}}</ng-template> | ||||
|       </rb-form-input> | ||||
|       <rb-form-input name="group" label="group" | ||||
|                      [rbFormInputAutocomplete]="autocomplete.bind(this, d.arr.materialGroups)" | ||||
|                      [rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required | ||||
|                      [(ngModel)]="material.group" #groupInput="ngModel" | ||||
|                      (focusout)="checkTypo($event, 'materialGroups', 'group', modalWarning)" | ||||
|                      title="chemical material type, eg. PA66"> | ||||
|         <ng-template rbFormValidationMessage="failure">{{groupInput.errors.failure}}</ng-template> | ||||
|       </rb-form-input> | ||||
|       <ng-template #modalWarning> | ||||
|         <rb-alert alertTitle="Warning" type="warning" okBtnLabel="Use suggestion" cancelBtnLabel="Keep value"> | ||||
|           The specified {{modalText.list}} could not be found in the list. <br> | ||||
|           Did you mean {{modalText.suggestion}}? | ||||
|         </rb-alert> | ||||
|       </ng-template> | ||||
|       <rb-array-input [(ngModel)]="material.numbers" name="materialNumbers" [pushTemplate]="''"> | ||||
|         <rb-form-input *rbArrayInputItem="let item" [rbArrayInputListener]="'materialNumber'" [index]="item.i" | ||||
|                        label="material number" appValidate="string" [name]="'materialNumber-' + item.i" | ||||
|                        [ngModel]="item.value" title="Bosch material part number, eg. 5515753021"></rb-form-input> | ||||
|       </rb-array-input> | ||||
|       <rb-form-select name="propertiesSelect" label="Type" title="=overall material group specific properties" | ||||
|                       [(ngModel)]="material.properties.material_template"> | ||||
|         <option *ngFor="let m of d.latest.materialTemplates" [value]="m._id">{{m.name}}</option> | ||||
|       </rb-form-select> | ||||
|       <rb-form-input *ngFor="let parameter of d.id.materialTemplates[material.properties.material_template].parameters; | ||||
|                      index as i" [name]="'materialParameter' + i" | ||||
|                      [label]="parameter.name" appValidate="string" required | ||||
|                      [(ngModel)]="material.properties[parameter.name]" #parameterInput="ngModel"> | ||||
|         <ng-template rbFormValidationMessage="failure">{{parameterInput.errors.failure}}</ng-template> | ||||
|         <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> | ||||
|       </rb-form-input> | ||||
|     </div> | ||||
| <!--BASE--> | ||||
|  | ||||
|     <div> | ||||
|       <rb-form-select name="type" label="type" required [(ngModel)]="sample.type" ngModel | ||||
|                       title="material status of the sample"> | ||||
|         <option value="as-delivered/raw">as-delivered/raw</option> | ||||
|         <option value="processed">processed</option> | ||||
|         <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> | ||||
|       </rb-form-select> | ||||
|       <rb-form-input name="color" label="color" appValidate="string" [(ngModel)]="sample.color" | ||||
|                      #colorInput="ngModel" title="sample color, eg. black"> | ||||
|         <ng-template rbFormValidationMessage="failure">{{colorInput.errors.failure}}</ng-template> | ||||
|         <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> | ||||
|       </rb-form-input> | ||||
|       <rb-form-input name="batch" label="batch" appValidate="string" [(ngModel)]="sample.batch" #batchInput="ngModel" | ||||
|                      title="batch number the sample was from, eg. 2264486614"> | ||||
|         <ng-template rbFormValidationMessage="failure">{{batchInput.errors.failure}}</ng-template> | ||||
|       </rb-form-input> | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
|   <div class="notes"> | ||||
|     <rb-form-input name="comment" label="comment" appValidate="stringLength" [appValidateArgs]="[512]" | ||||
|                    [(ngModel)]="sample.notes.comment" #commentInput="ngModel" | ||||
|                    title="general remarks that cannot be expressed in additional properties, eg. stabilized"> | ||||
|       <ng-template rbFormValidationMessage="failure">{{commentInput.errors.failure}}</ng-template> | ||||
|     </rb-form-input> | ||||
|     <h5>Sample references</h5> | ||||
|     <div *ngFor="let reference of sampleReferences; index as i" class="two-col" [@inOut]> | ||||
|   <form #sampleForm="ngForm" *ngIf="view.base"> | ||||
|     <div class="sample"> | ||||
|       <div> | ||||
|         <rb-form-input [name]="'sr-id' + i" label="sample number" [rbFormInputAutocomplete]="sampleReferenceListBind()" | ||||
|                        [rbDebounceTime]="300" appValidate="stringOf" | ||||
|                        [appValidateArgs]="[sampleReferenceAutocomplete[i]]" | ||||
|                        (ngModelChange)="checkSampleReference($event, i)" [ngModel]="reference[0]" ngModel | ||||
|                        title="sample number of the referenced sample, eg. An31"> | ||||
|           <ng-template rbFormValidationMessage="failure">Unknown sample number</ng-template> | ||||
|         <rb-form-input name="materialname" label="material name" [rbDebounceTime]="0" [rbInitialOpen]="true" | ||||
|                        [rbFormInputAutocomplete]="autocomplete.bind(this, materialNames)" appValidate="stringOf" | ||||
|                        (keydown)="preventDefault($event)" (ngModelChange)="findMaterial($event)" ngModel | ||||
|                        [appValidateArgs]="[materialNames]" required [(ngModel)]="material.name" [autofocus]="true" | ||||
|                        *ngIf="baseSample.material !== undefined || mode === 'new'" | ||||
|                        title="trade name of the material, eg. Ultradur B4300 G6"> | ||||
|           <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> | ||||
|           <ng-template rbFormValidationMessage="failure">Unknown material, add properties for new material</ng-template> | ||||
|         </rb-form-input> | ||||
|         <rb-icon-button class="set-new-material space-below" icon="add" mode="secondary" | ||||
|                         (click)="setNewMaterial(!newMaterial)" *ngIf="baseSample.material !== undefined"> | ||||
|           New material | ||||
|         </rb-icon-button> | ||||
|       </div> | ||||
|       <rb-form-input [name]="'sr-relation' + i" label="relation" appValidate="string" [required]="reference[0] !== ''" | ||||
|                      [(ngModel)]="reference[1]" title="description how the samples are connected, eg. belongs to"> | ||||
|         <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> | ||||
|       </rb-form-input> | ||||
|     </div> | ||||
|     <h5>Additional properties</h5> | ||||
|     <rb-array-input [(ngModel)]="customFields" name="customFields" [pushTemplate]="['', '']" pushPath="0" | ||||
|                     class="two-col" [@inOut]> | ||||
|       <ng-container *rbArrayInputItem="let item"> | ||||
|         <div> | ||||
|           <rb-form-input [name]="'cf-key' + item.i" label="key" [rbArrayInputListener]="'cf-key'" [index]="item.i" | ||||
|                          [rbFormInputAutocomplete]="autocomplete.bind(this, availableCustomFields)" [rbDebounceTime]="0" | ||||
|                          [rbInitialOpen]="true" appValidate="unique" [appValidateArgs]="[uniqueCfValues(item.i)]" | ||||
|                          [ngModel]="item.value[0]" #keyInput="ngModel" title="name of additional property, eg. vwz"> | ||||
|             <ng-template rbFormValidationMessage="failure">{{keyInput.errors.failure}}</ng-template> | ||||
|           </rb-form-input> | ||||
|         </div> | ||||
|         <rb-form-input [name]="'cf-value' + item.i" label="value" appValidate="string" [required]="item.value[0] !== ''" | ||||
|                        [ngModel]="item.value[1]" title="value of additional property, eg. 0 min"> | ||||
|  | ||||
|       <div class="material shaded-container" *ngIf="newMaterial" [@inOut]> | ||||
|         <h4>Material properties</h4> | ||||
|         <rb-form-input name="supplier" label="supplier" | ||||
|                        [rbFormInputAutocomplete]="autocomplete.bind(this, d.arr.materialSuppliers)" | ||||
|                        [rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required | ||||
|                        [(ngModel)]="material.supplier" #supplierInput="ngModel" | ||||
|                        (focusout)="checkTypo($event, 'materialSuppliers', 'supplier', modalWarning)" | ||||
|                        title="material supplier, eg. BASF"> | ||||
|           <ng-template rbFormValidationMessage="failure">{{supplierInput.errors.failure}}</ng-template> | ||||
|         </rb-form-input> | ||||
|         <rb-form-input name="group" label="group" | ||||
|                        [rbFormInputAutocomplete]="autocomplete.bind(this, d.arr.materialGroups)" | ||||
|                        [rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required | ||||
|                        [(ngModel)]="material.group" #groupInput="ngModel" | ||||
|                        (focusout)="checkTypo($event, 'materialGroups', 'group', modalWarning)" | ||||
|                        title="chemical material type, eg. PA66"> | ||||
|           <ng-template rbFormValidationMessage="failure">{{groupInput.errors.failure}}</ng-template> | ||||
|         </rb-form-input> | ||||
|         <ng-template #modalWarning> | ||||
|           <rb-alert alertTitle="Warning" type="warning" okBtnLabel="Use suggestion" cancelBtnLabel="Keep value"> | ||||
|             The specified {{modalText.list}} could not be found in the list. <br> | ||||
|             Did you mean {{modalText.suggestion}}? | ||||
|           </rb-alert> | ||||
|         </ng-template> | ||||
|         <rb-array-input [(ngModel)]="material.numbers" name="materialNumbers" [pushTemplate]="''"> | ||||
|           <rb-form-input *rbArrayInputItem="let item" [rbArrayInputListener]="'materialNumber'" [index]="item.i" | ||||
|                          label="material number" appValidate="string" [name]="'materialNumber-' + item.i" | ||||
|                          [ngModel]="item.value" title="Bosch material part number, eg. 5515753021"></rb-form-input> | ||||
|         </rb-array-input> | ||||
|         <rb-form-select name="propertiesSelect" label="Type" title="=overall material group specific properties" | ||||
|                         [(ngModel)]="material.properties.material_template"> | ||||
|           <option *ngFor="let m of d.latest.materialTemplates" [value]="m._id">{{m.name}}</option> | ||||
|         </rb-form-select> | ||||
|         <rb-form-input *ngFor="let parameter of | ||||
|                          d.id.materialTemplates[material.properties.material_template].parameters; | ||||
|                        index as i" [name]="'materialParameter' + i" | ||||
|                        [label]="parameter.name" appValidate="string" required | ||||
|                        [(ngModel)]="material.properties[parameter.name]" #parameterInput="ngModel"> | ||||
|           <ng-template rbFormValidationMessage="failure">{{parameterInput.errors.failure}}</ng-template> | ||||
|           <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> | ||||
|         </rb-form-input> | ||||
|       </ng-container> | ||||
|     </rb-array-input> | ||||
|   </div> | ||||
|       </div> | ||||
|  | ||||
|   <div *ngIf="editSampleBase; else generateSamples" class="space-below"> | ||||
|     <button class="rb-btn rb-primary" type="submit" (click)="saveSample()" [disabled]="!sampleForm.form.valid"> | ||||
|       Save sample | ||||
|     </button> | ||||
|   </div> | ||||
|   <ng-template #generateSamples> | ||||
|     <rb-form-input type="number" name="sample-count" label="number of samples" pattern="^\d+?$" required | ||||
|                    rbNumberConverter rbMin="1" [(ngModel)]="sampleCount" class="sample-count" | ||||
|                    title="number of samples to create with this base information"> | ||||
|       <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> | ||||
|       <ng-template rbFormValidationMessage="rbMin">Must be at least 1</ng-template> | ||||
|     </rb-form-input> | ||||
|     <button class="rb-btn rb-primary space-below" type="submit" (click)="saveSample()" | ||||
|             [disabled]="!sampleForm.form.valid"> | ||||
|       Generate sample{{sampleCount > 1 ? 's' : ''}} | ||||
|     </button> | ||||
|   </ng-template> | ||||
| </form> | ||||
|       <div> | ||||
|         <rb-form-select name="type" label="type" required [(ngModel)]="baseSample.type" ngModel | ||||
|                         *ngIf="baseSample.type !== undefined" | ||||
|                         title="material status of the sample"> | ||||
|           <option value="as-delivered/raw">as-delivered/raw</option> | ||||
|           <option value="processed">processed</option> | ||||
|           <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> | ||||
|         </rb-form-select> | ||||
|         <rb-form-input name="color" label="color" appValidate="string" [(ngModel)]="baseSample.color" | ||||
|                        *ngIf="baseSample.color !== undefined" #colorInput="ngModel" title="sample color, eg. black"> | ||||
|           <ng-template rbFormValidationMessage="failure">{{colorInput.errors.failure}}</ng-template> | ||||
|           <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> | ||||
|         </rb-form-input> | ||||
|         <rb-form-input name="batch" label="batch" appValidate="string" [(ngModel)]="baseSample.batch" | ||||
|                        #batchInput="ngModel" *ngIf="baseSample.batch !== undefined" | ||||
|                        title="batch number the sample was from, eg. 2264486614"> | ||||
|           <ng-template rbFormValidationMessage="failure">{{batchInput.errors.failure}}</ng-template> | ||||
|         </rb-form-input> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="notes" *ngIf="baseSample.notes !== undefined"> | ||||
|       <rb-form-input name="comment" label="comment" appValidate="stringLength" [appValidateArgs]="[512]" | ||||
|                      [(ngModel)]="baseSample.notes.comment" #commentInput="ngModel" | ||||
|                      title="general remarks that cannot be expressed in additional properties, eg. stabilized"> | ||||
|         <ng-template rbFormValidationMessage="failure">{{commentInput.errors.failure}}</ng-template> | ||||
|       </rb-form-input> | ||||
|       <h5>Sample references</h5> | ||||
|       <div *ngFor="let reference of sampleReferences; index as i" class="two-col" [@inOut]> | ||||
|         <div> | ||||
|           <rb-form-input [name]="'sr-id' + i" label="sample number" | ||||
|                          [rbFormInputAutocomplete]="sampleReferenceListBind()" | ||||
|                          [rbDebounceTime]="300" appValidate="stringOf" | ||||
|                          [appValidateArgs]="[sampleReferenceAutocomplete[i]]" | ||||
|                          (ngModelChange)="checkSampleReference($event, i)" [ngModel]="reference[0]" ngModel | ||||
|                          title="sample number of the referenced sample, eg. An31"> | ||||
|             <ng-template rbFormValidationMessage="failure">Unknown sample number</ng-template> | ||||
|           </rb-form-input> | ||||
|         </div> | ||||
|         <rb-form-input [name]="'sr-relation' + i" label="relation" appValidate="string" [required]="reference[0] !== ''" | ||||
|                        [(ngModel)]="reference[1]" title="description how the samples are connected, eg. belongs to"> | ||||
|           <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> | ||||
|         </rb-form-input> | ||||
|       </div> | ||||
|       <h5>Additional properties</h5> | ||||
|       <rb-array-input [(ngModel)]="customFields" name="customFields" [pushTemplate]="['', '']" pushPath="0" | ||||
|                       class="two-col" [@inOut]> | ||||
|         <ng-container *rbArrayInputItem="let item"> | ||||
|           <div> | ||||
|             <rb-form-input [name]="'cf-key' + item.i" label="key" [rbArrayInputListener]="'cf-key'" [index]="item.i" | ||||
|                            [rbFormInputAutocomplete]="autocomplete.bind(this, availableCustomFields)" | ||||
|                            [rbDebounceTime]="0" | ||||
|                            [rbInitialOpen]="true" appValidate="unique" [appValidateArgs]="[uniqueCfValues(item.i)]" | ||||
|                            [(ngModel)]="item.value[0]" #keyInput="ngModel" title="name of additional property, eg. vwz"> | ||||
|               <ng-template rbFormValidationMessage="failure">{{keyInput.errors.failure}}</ng-template> | ||||
|             </rb-form-input> | ||||
|           </div> | ||||
|           <rb-form-input [name]="'cf-value' + item.i" label="value" appValidate="string" | ||||
|                          [required]="item.value[0] !== ''" | ||||
|                          [(ngModel)]="item.value[1]" title="value of additional property, eg. 0 min"> | ||||
|             <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> | ||||
|           </rb-form-input> | ||||
|         </ng-container> | ||||
|       </rb-array-input> | ||||
|     </div> | ||||
|  | ||||
| <div *ngIf="generatedSamples.length && !loading"> | ||||
|   <div *ngIf="!editSampleBase"> | ||||
|     <h3 *ngIf="new">Successfully added samples:</h3> | ||||
|     <span *ngIf="!new" class="rb-ic rb-ic-edit clickable" (click)="checkFormAfterInit = editSampleBase = true"></span> | ||||
|     <div *ngIf="samples.length; else generateSamples" class="space-below"> | ||||
|       <rb-icon-button icon="save" mode="primary" type="submit" (click)="saveSample()" | ||||
|                       [disabled]="!sampleForm.form.valid"> | ||||
|         Save sample | ||||
|       </rb-icon-button> | ||||
|     </div> | ||||
|     <ng-template #generateSamples> | ||||
|       <rb-form-input type="number" name="sample-count" label="number of samples" pattern="^\d+?$" required | ||||
|                      rbNumberConverter rbMin="1" [(ngModel)]="sampleCount" class="sample-count" | ||||
|                      title="number of samples to create with this base information"> | ||||
|         <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> | ||||
|         <ng-template rbFormValidationMessage="rbMin">Must be at least 1</ng-template> | ||||
|       </rb-form-input> | ||||
|       <button class="rb-btn rb-primary space-below" type="submit" (click)="saveSample()" | ||||
|               [disabled]="!sampleForm.form.valid"> | ||||
|         Generate sample{{sampleCount > 1 ? 's' : ''}} | ||||
|       </button> | ||||
|     </ng-template> | ||||
|   </form> | ||||
|  | ||||
| <!--BASE SUMMARY--> | ||||
|  | ||||
|   <div *ngIf="view.baseSum"> | ||||
|     <h3 *ngIf="mode === 'new'">Successfully added samples:</h3> | ||||
|     <span class="rb-ic rb-ic-edit clickable" (click)="checkFormAfterInit = view.base = true; view.baseSum = false"> | ||||
|     </span> | ||||
|     <rb-table id="response-data"> | ||||
|       <tr><td>Material</td><td>{{generatedSamples[0].material.name}}</td></tr> | ||||
|       <tr><td>Type</td><td>{{generatedSamples[0].type}}</td></tr> | ||||
|       <tr><td>color</td><td>{{generatedSamples[0].color}}</td></tr> | ||||
|       <tr><td>Batch</td><td>{{generatedSamples[0].batch}}</td></tr> | ||||
|       <tr><td>Material</td><td>{{material.name}}</td></tr> | ||||
|       <tr><td>Type</td><td>{{baseSample.type}}</td></tr> | ||||
|       <tr><td>color</td><td>{{baseSample.color}}</td></tr> | ||||
|       <tr><td>Batch</td><td>{{baseSample.batch}}</td></tr> | ||||
|       <tr><td>Comment</td><td>{{baseSample.notes.comment}}</td></tr> | ||||
|       <tr *ngFor="let reference of sampleReferences.slice(0, -1)"> | ||||
|         <td>Sample reference</td><td>{{reference[0]}} - {{reference[1]}}</td> | ||||
|       </tr> | ||||
|       <tr *ngFor="let field of customFields"><td>{{field[0]}}</td><td>{{field[1]}}</td></tr> | ||||
|     </rb-table> | ||||
|   </div> | ||||
|  | ||||
|   <form #cmForm="ngForm"> | ||||
|     <div *ngFor="let gSample of generatedSamples; index as gIndex"> | ||||
|       <h4 *ngIf="new">{{gSample.number}}</h4> | ||||
|       <div class="conditions shaded-container space-below"> | ||||
|         <h5> | ||||
|           Condition | ||||
|           <button class="rb-btn rb-secondary condition-set" type="button" (click)="toggleCondition(gSample)"> | ||||
|             {{gSample.condition.condition_template ? 'Do not set condition' : 'Set condition'}} | ||||
|           </button> | ||||
|         </h5> | ||||
|         <div *ngIf="gSample.condition.condition_template" [@inOut]> | ||||
|           <rb-form-select name="conditionSelect" label="Condition" | ||||
|                           [(ngModel)]="gSample.condition.condition_template"> | ||||
|             <option *ngFor="let c of d.latest.conditionTemplates" [value]="c._id">{{c.name}}</option> | ||||
|           </rb-form-select> | ||||
| <!--CM--> | ||||
|  | ||||
|           <ng-container *ngFor="let parameter of | ||||
|                         d.id.conditionTemplates[gSample.condition.condition_template].parameters; index as i" | ||||
|                         [ngSwitch]="(parameter.range.values ? 1 : 0)"> | ||||
|             <rb-form-select *ngSwitchCase="1" | ||||
|                             [name]="'conditionParameter-' + gIndex + '-' + i" | ||||
|                             [label]="parameter.name" [(ngModel)]="gSample.condition[parameter.name]" ngModel> | ||||
|               <option *ngFor="let value of parameter.range.values" [value]="value">{{value}}</option> | ||||
|               <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> | ||||
|   <div *ngIf="view.cm"> | ||||
|     <form #cmForm="ngForm" [ngClass]="{'cm-form': samples.length > 1}"> | ||||
|       <rb-tab-panel (tabChanged)="cmSampleIndex = $event" class="space-below" *ngIf="samples.length > 1"> | ||||
|         <ng-container *ngFor="let sample of samples; index as i"> | ||||
|           <div *rbTabPanelItem="sample.number; id: i"></div> | ||||
|         </ng-container> | ||||
|       </rb-tab-panel> | ||||
|       <div *ngFor="let sample of samples; index as gIndex" | ||||
|            [ngStyle]="{display: gIndex == cmSampleIndex || gIndex == cmSampleIndex  + 1 ? 'initial' : 'none'}"> | ||||
|         <h4 *ngIf="samples.length > 1">{{sample.number}}</h4> | ||||
|         <div class="conditions shaded-container space-below"> | ||||
|           <h5> | ||||
|             Condition | ||||
|             <button class="rb-btn rb-secondary condition-set" type="button" (click)="toggleCondition(sample)"> | ||||
|               {{sample.condition.condition_template ? 'Do not set condition' : 'Set condition'}} | ||||
|             </button> | ||||
|           </h5> | ||||
|           <div *ngIf="sample.condition.condition_template" [@inOut]> | ||||
|             <rb-form-select name="conditionSelect" label="Condition" | ||||
|                             [(ngModel)]="sample.condition.condition_template"> | ||||
|               <option *ngFor="let c of d.latest.conditionTemplates" [value]="c._id">{{c.name}}</option> | ||||
|             </rb-form-select> | ||||
|             <rb-form-input *ngSwitchDefault | ||||
|                            [name]="'conditionParameter-' + gIndex + '-' + i" | ||||
|                            [label]="parameter.name" appValidate="string" required | ||||
|                            [(ngModel)]="gSample.condition[parameter.name]" #parameterInput="ngModel"> | ||||
|               <ng-template rbFormValidationMessage="failure">{{parameterInput.errors.failure}}</ng-template> | ||||
|               <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> | ||||
|             </rb-form-input> | ||||
|           </ng-container> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       <div class="measurements shaded-container space-below"> | ||||
|         <h5>Measurements</h5> | ||||
|         <div *ngFor="let measurement of gSample.measurements; index as mIndex" [@inOut]> | ||||
|           <rb-form-select [name]="'measurementTemplateSelect-' + gIndex + '-' + mIndex" label="Template" | ||||
|                           [(ngModel)]="measurement.measurement_template" | ||||
|                           (ngModelChange)="clearMeasurement(gIndex, mIndex)"> | ||||
|             <option *ngFor="let m of d.latest.measurementTemplates" [value]="m._id">{{m.name}}</option> | ||||
|           </rb-form-select> | ||||
|  | ||||
|           <div *ngFor="let parameter of d.id.measurementTemplates[measurement.measurement_template].parameters; | ||||
|                index as pIndex"> | ||||
|             <ng-container [ngSwitch]="(parameter.range.type ? 1 : 0) + (parameter.range.values ? 2 : 0)"> | ||||
|               <rb-form-file *ngSwitchCase="1" | ||||
|                             [name]="'measurementParameter-' + gIndex + '-' + mIndex + '-' + pIndex" | ||||
|                             [label]="parameter.name" maxSize="10000000" multiple | ||||
|                             [required]="measurement.values[parameter.name] && | ||||
|                             !measurement.values[parameter.name].length" | ||||
|                             (ngModelChange)="fileToArray($event, gIndex, mIndex, parameter.name)" | ||||
|                             placeholder="Select file or drag and drop" dragDrop ngModel> | ||||
|                 <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> | ||||
|               </rb-form-file> | ||||
|               <rb-form-select *ngSwitchCase="2" | ||||
|                               [name]="'measurementParameter-' + gIndex + '-' + mIndex + '-' + pIndex" | ||||
|                               [label]="parameter.name" [(ngModel)]="measurement.values[parameter.name]" ngModel> | ||||
|                 <option *ngFor="let device of d.d.user.devices" [value]="device">{{device}}</option> | ||||
|             <ng-container *ngFor="let parameter of | ||||
|                           d.id.conditionTemplates[sample.condition.condition_template].parameters; index as i" | ||||
|                           [ngSwitch]="(parameter.range.values ? 1 : 0)"> | ||||
|               <rb-form-select *ngSwitchCase="1" | ||||
|                               [name]="'conditionParameter-' + gIndex + '-' + i" | ||||
|                               [label]="parameter.name" [(ngModel)]="sample.condition[parameter.name]" ngModel> | ||||
|                 <option *ngFor="let value of parameter.range.values" [value]="value">{{value}}</option> | ||||
|                 <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> | ||||
|               </rb-form-select> | ||||
|               <rb-form-input *ngSwitchDefault | ||||
|                              [name]="'measurementParameter-' + gIndex + '-' + mIndex + '-' + pIndex" | ||||
|                              [label]="parameter.name" appValidate="string" | ||||
|                              [(ngModel)]="measurement.values[parameter.name]" #parameterInput="ngModel"> | ||||
|                              [name]="'conditionParameter-' + gIndex + '-' + i" | ||||
|                              [label]="parameter.name" appValidate="string" required | ||||
|                              [(ngModel)]="sample.condition[parameter.name]" #parameterInput="ngModel"> | ||||
|                 <ng-template rbFormValidationMessage="failure">{{parameterInput.errors.failure}}</ng-template> | ||||
|                 <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> | ||||
|               </rb-form-input> | ||||
|             </ng-container> | ||||
|             <canvas baseChart *ngIf="parameter.range.type && charts[gIndex][mIndex][0].data.length > 0" | ||||
|                     class="dpt-chart" | ||||
|                     [@inOut] | ||||
|                     [datasets]="charts[gIndex][mIndex]" | ||||
|                     [labels]="[]" | ||||
|                     [options]="chartOptions" | ||||
|                     [legend]="false" | ||||
|                     chartType="scatter"> | ||||
|             </canvas> | ||||
|           </div> | ||||
|           <rb-icon-button icon="delete" mode="danger" (click)="removeMeasurement(gIndex, mIndex)"> | ||||
|             Delete measurement | ||||
|           </rb-icon-button> | ||||
|         </div> | ||||
|  | ||||
|           | ||||
|         <div class="measurements shaded-container space-below"> | ||||
|           <h5> | ||||
|             Measurements | ||||
|             <rb-icon-button icon="undo" mode="secondary" *ngIf="measurementRestoreData.length" | ||||
|                             class="restore-measurements" (click)="restoreMeasurements()"> | ||||
|               Restore measurements | ||||
|             </rb-icon-button> | ||||
|           </h5> | ||||
|           <div *ngFor="let measurement of sample.measurements; index as mIndex" [@inOut]  class="space-below"> | ||||
|             <rb-form-select [name]="'measurementTemplateSelect-' + gIndex + '-' + mIndex" label="Template" | ||||
|                             [(ngModel)]="measurement.measurement_template" | ||||
|                             (ngModelChange)="clearMeasurement(gIndex, mIndex)"> | ||||
|               <option *ngFor="let m of d.latest.measurementTemplates" [value]="m._id">{{m.name}}</option> | ||||
|             </rb-form-select> | ||||
|  | ||||
|         <div> | ||||
|           <rb-icon-button icon="add" mode="secondary" (click)="addMeasurement(gIndex)"> | ||||
|             New measurement | ||||
|           </rb-icon-button> | ||||
|             <div *ngFor="let parameter of d.id.measurementTemplates[measurement.measurement_template].parameters; | ||||
|                  index as pIndex"> | ||||
|               <ng-container [ngSwitch]="(parameter.range.type ? 1 : 0) + (parameter.range.values ? 2 : 0)"> | ||||
|                 <rb-form-file *ngSwitchCase="1" | ||||
|                               [name]="'measurementParameter-' + gIndex + '-' + mIndex + '-' + pIndex" | ||||
|                               [label]="parameter.name" maxSize="10000000" multiple | ||||
|                               [required]="measurement.values[parameter.name] && | ||||
|                               !measurement.values[parameter.name].length" | ||||
|                               (ngModelChange)="fileToArray($event, gIndex, mIndex, parameter.name)" | ||||
|                               placeholder="Select file or drag and drop" dragDrop ngModel> | ||||
|                   <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> | ||||
|                 </rb-form-file> | ||||
|                 <rb-form-select *ngSwitchCase="2" | ||||
|                                 [name]="'measurementParameter-' + gIndex + '-' + mIndex + '-' + pIndex" | ||||
|                                 [label]="parameter.name" [(ngModel)]="measurement.values[parameter.name]" ngModel> | ||||
|                   <option *ngFor="let device of d.d.user.devices" [value]="device">{{device}}</option> | ||||
|                   <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> | ||||
|                 </rb-form-select> | ||||
|                 <rb-form-input *ngSwitchDefault | ||||
|                                [name]="'measurementParameter-' + gIndex + '-' + mIndex + '-' + pIndex" | ||||
|                                [label]="parameter.name" appValidate="string" | ||||
|                                [(ngModel)]="measurement.values[parameter.name]" #parameterInput="ngModel"> | ||||
|                   <ng-template rbFormValidationMessage="failure">{{parameterInput.errors.failure}}</ng-template> | ||||
|                   <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template> | ||||
|                 </rb-form-input> | ||||
|               </ng-container> | ||||
|               <canvas baseChart *ngIf="parameter.range.type && charts[gIndex][mIndex][0].data.length > 0" | ||||
|                       class="dpt-chart" | ||||
|                       [@inOut] | ||||
|                       [datasets]="charts[gIndex][mIndex]" | ||||
|                       [labels]="[]" | ||||
|                       [options]="chartOptions" | ||||
|                       [legend]="false" | ||||
|                       chartType="scatter"> | ||||
|               </canvas> | ||||
|             </div> | ||||
|             <rb-icon-button icon="delete" mode="danger" (click)="removeMeasurement(gIndex, mIndex)"> | ||||
|               Delete measurement | ||||
|             </rb-icon-button> | ||||
|           </div> | ||||
|           <div> | ||||
|             <rb-icon-button icon="add" mode="secondary" (click)="addMeasurement(gIndex)"> | ||||
|               New measurement | ||||
|             </rb-icon-button> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <rb-icon-button icon="save" mode="primary" type="submit" (click)="cmSave()" [disabled]="!cmForm.form.valid"> | ||||
|       Save sample{{generatedSamples.length > 1 ? 's' : ''}} | ||||
|       <div class="buttons"> | ||||
|         <rb-icon-button icon="summary" mode="primary" type="submit" (click)="view.cm = false; view.cmSum = true" | ||||
|                         [disabled]="!cmForm.form.valid"> | ||||
|           Summary | ||||
|         </rb-icon-button> | ||||
|         <rb-icon-button class="delete-sample" icon="delete" mode="danger" *ngIf="mode !== 'new'" | ||||
|                         (click)="deleteConfirm(modalDeleteConfirm)"> | ||||
|           Delete sample | ||||
|         </rb-icon-button> | ||||
|         <ng-template #modalDeleteConfirm> | ||||
|           <rb-alert alertTitle="Are you sure?" type="danger" okBtnLabel="Delete sample" cancelBtnLabel="Cancel"> | ||||
|             Do you really want to delete this sample? | ||||
|           </rb-alert> | ||||
|         </ng-template> | ||||
|       </div> | ||||
|     </form> | ||||
|   </div> | ||||
|  | ||||
| <!--CM SUMMARY--> | ||||
|  | ||||
|   <div *ngIf="view.cmSum"> | ||||
|     <span class="rb-ic rb-ic-edit clickable" (click)="checkFormAfterInit = view.cm = true; view.cmSum = false"> | ||||
|     </span> | ||||
|     <rb-table> | ||||
|       <ng-container *ngFor="let sample of samples; index as gIndex"> | ||||
|         <tr><th>{{sample.number}}</th><th></th></tr> | ||||
|         <tr *ngFor="let parameter of (d.id.conditionTemplates[sample.condition.condition_template] || {parameters: []}) | ||||
|             .parameters"> | ||||
|           <td>{{parameter.name}}</td><td>{{sample.condition[parameter.name]}}</td> | ||||
|         </tr> | ||||
|         <ng-container *ngFor="let measurement of sample.measurements"> | ||||
|           <tr *ngFor="let parameter of d.id.measurementTemplates[measurement.measurement_template].parameters"> | ||||
|             <td>{{parameter.name}}</td><td>{{measurement.values[parameter.name]}}</td> | ||||
|           </tr> | ||||
|         </ng-container> | ||||
|       </ng-container> | ||||
|     </rb-table> | ||||
|     <rb-icon-button icon="save" mode="primary" type="submit" (click)="cmSave()"> | ||||
|       Save sample{{samples.length > 1 ? 's' : ''}} | ||||
|     </rb-icon-button> | ||||
|     <rb-icon-button class="delete-sample" icon="delete" mode="danger" *ngIf="!new" | ||||
|                     (click)="deleteConfirm(modalDeleteConfirm)"> | ||||
|       Delete sample | ||||
|     </rb-icon-button> | ||||
|     <ng-template #modalDeleteConfirm> | ||||
|       <rb-alert alertTitle="Are you sure?" type="danger" okBtnLabel="Delete sample" cancelBtnLabel="Cancel"> | ||||
|         Do you really want to delete {{sample.number}}? | ||||
|       </rb-alert> | ||||
|     </ng-template> | ||||
|   </form> | ||||
| </div> | ||||
|   </div> | ||||
| </ng-template> | ||||
|   | ||||
| @@ -33,3 +33,17 @@ td:first-child { | ||||
| .delete-sample { | ||||
|   float: right; | ||||
| } | ||||
|  | ||||
| .cm-form { | ||||
|   display: grid; | ||||
|   grid-template-columns: 1fr 1fr; | ||||
|   grid-column-gap: 1rem; | ||||
|  | ||||
|   rb-tab-panel, div.buttons { | ||||
|     grid-column: span 2; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .restore-measurements { | ||||
|   float: right; | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import cloneDeep from 'lodash/cloneDeep'; | ||||
| import merge from 'lodash/merge'; | ||||
| import omit from 'lodash/omit'; | ||||
| import isEqual from 'lodash/isEqual'; | ||||
| import strCompare from 'str-compare'; | ||||
| import { | ||||
|   AfterContentChecked, | ||||
| @@ -21,6 +22,7 @@ import {animate, style, transition, trigger} from '@angular/animations'; | ||||
| import {Observable} from 'rxjs'; | ||||
| import {ModalService} from '@inst-iot/bosch-angular-ui-components'; | ||||
| import {DataService} from '../services/data.service'; | ||||
| import {LoginService} from '../services/login.service'; | ||||
|  | ||||
| // TODO: additional property value not validated on edit | ||||
|  | ||||
| @@ -48,9 +50,9 @@ export class SampleComponent implements OnInit, AfterContentChecked { | ||||
|   @ViewChild('sampleForm') sampleForm: NgForm; | ||||
|   @ViewChild('cmForm') cmForm: NgForm; | ||||
|  | ||||
|   sample = new SampleModel();            // base sample which is saved | ||||
|   baseSample = new SampleModel();            // base sample which is saved | ||||
|   sampleCount = 1;                       // number of samples to be generated | ||||
|   generatedSamples: SampleModel[] = [];  // gets filled with response data after saving the sample | ||||
|   samples: SampleModel[] = [];  // gets filled with response data after saving the sample | ||||
|  | ||||
|   sampleReferences: [string, string, string][] = [['', '', '']]; | ||||
|   sampleReferenceFinds: {_id: string, number: string}[] = [];    // raw sample reference data from db | ||||
| @@ -66,11 +68,20 @@ export class SampleComponent implements OnInit, AfterContentChecked { | ||||
|   material = new MaterialModel();        // object of current selected material | ||||
|   defaultDevice = '';                    // default device for spectra | ||||
|  | ||||
|   new;                                   // true if new sample should be created | ||||
|   editSampleBase = false;                // set true to edit sample base values even when generatedSamples .length > 0 | ||||
|   // component mode, either new for generating new samples, editOne or editMulti, editing one or multiple samples | ||||
|   mode = 'new'; | ||||
|   view = {                               // active views | ||||
|     base: false,                         // base sample | ||||
|     baseSum: false,                      // base sample summary | ||||
|     cm: false,                           // conditions and measurements | ||||
|     cmSum: false                         // conditions and measurements summary | ||||
|   }; | ||||
|   loading = 0;                           // number of currently loading instances | ||||
|   checkFormAfterInit = false; | ||||
|   modalText = {list: '', suggestion: ''}; | ||||
|   cmSampleIndex = '0'; | ||||
|   measurementDeleteList = [];            // buffer with measurements to delete, if the user confirms and saves the cm changes | ||||
|   measurementRestoreData: MeasurementModel[] = [];  // deleted measurements if user is allowed and measurements are available | ||||
|  | ||||
|   charts = [[]];                         // chart data for spectra | ||||
|   readonly chartInit = [{ | ||||
| @@ -101,11 +112,12 @@ export class SampleComponent implements OnInit, AfterContentChecked { | ||||
|     private validation: ValidationService, | ||||
|     public autocomplete: AutocompleteService, | ||||
|     private modal: ModalService, | ||||
|     public d: DataService | ||||
|     public d: DataService, | ||||
|     private login: LoginService | ||||
|   ) { } | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|     this.new = this.router.url === '/samples/new'; | ||||
|     this.mode = this.router.url === '/samples/new' ? 'new' : ''; | ||||
|     this.loading = 7; | ||||
|     this.d.load('materials', () => { | ||||
|       this.materialNames = this.d.arr.materials.map(e => e.name); | ||||
| @@ -144,49 +156,88 @@ export class SampleComponent implements OnInit, AfterContentChecked { | ||||
|       this.availableCustomFields = this.d.arr.sampleNotesFields.map(e => e.name); | ||||
|       this.loading--; | ||||
|     }); | ||||
|     if (!this.new) { | ||||
|       this.loading++; | ||||
|       this.api.get<SampleModel>('/sample/' + this.route.snapshot.paramMap.get('id'), sData => { | ||||
|         this.sample.deserialize(sData); | ||||
|         this.generatedSamples[0] = this.sample; | ||||
|         this.charts = [[]]; | ||||
|         let spectrumCounter = 0;  // generate charts for spectrum measurements | ||||
|         this.generatedSamples[0].measurements.forEach((measurement, i) => { | ||||
|           this.charts[0].push(cloneDeep(this.chartInit)); | ||||
|           if (measurement.values.dpt) { | ||||
|             setTimeout(() => { | ||||
|               this.generateChart(measurement.values.dpt, 0, i); | ||||
|             }, spectrumCounter * 20);  // generate charts one after another to avoid freezing the UI | ||||
|             spectrumCounter ++; | ||||
|           } | ||||
|         }); | ||||
|         this.material = new MaterialModel().deserialize(sData.material);  // read material | ||||
|         this.customFields = this.sample.notes.custom_fields && this.sample.notes.custom_fields !== {} ?  // read custom fields | ||||
|           Object.keys(this.sample.notes.custom_fields).map(e => [e, this.sample.notes.custom_fields[e]]) : []; | ||||
|         if (this.sample.notes.sample_references.length) {  // read sample references | ||||
|           this.sampleReferences = []; | ||||
|           this.sampleReferenceAutocomplete = []; | ||||
|           let loadCounter = this.sample.notes.sample_references.length;  // count down instances still loading | ||||
|           this.sample.notes.sample_references.forEach(reference => { | ||||
|             this.api.get<SampleModel>('/sample/' + reference.sample_id, srData => {  // get sample numbers for ids | ||||
|               this.sampleReferences.push([srData.number, reference.relation, reference.sample_id]); | ||||
|               this.sampleReferenceAutocomplete.push([srData.number]); | ||||
|               if (!--loadCounter) {  // insert empty template when all instances were loaded | ||||
|                 this.sampleReferences.push(['', '', '']); | ||||
|                 this.sampleReferenceAutocomplete.push([]); | ||||
|               } | ||||
|             }); | ||||
|     if (this.mode !== 'new') { | ||||
|       const sampleIds = this.route.snapshot.paramMap.get('id').split(','); | ||||
|       if (sampleIds.length === 1) { | ||||
|         this.mode = 'editOne'; | ||||
|         this.view.baseSum = true; | ||||
|         this.view.cm = true; | ||||
|         if (this.login.isLevel.dev) {  // load measurement restore data | ||||
|           this.api.get<MeasurementModel[]>('/measurement/sample/' + sampleIds[0], (data, ignore) => { | ||||
|             if (data) { | ||||
|               this.measurementRestoreData = data.filter(e => e.status === 'deleted').map(e => new MeasurementModel().deserialize(e)); | ||||
|             } | ||||
|             console.log(this.measurementRestoreData); | ||||
|           }); | ||||
|         } | ||||
|         this.loading--; | ||||
|         this.checkFormAfterInit = true; | ||||
|       } | ||||
|       else { | ||||
|         this.mode = 'editMulti'; | ||||
|         this.view.base = true; | ||||
|       } | ||||
|       this.loading += sampleIds.length; | ||||
|       this.samples = []; | ||||
|       sampleIds.forEach((sampleId, i) => { | ||||
|         this.api.get<SampleModel>('/sample/' + sampleId, sData => { | ||||
|           this.samples.push(new SampleModel().deserialize(sData)); | ||||
|           if (i === 0) { | ||||
|             this.baseSample.deserialize(sData); | ||||
|             this.material = new MaterialModel().deserialize(sData.material);  // read material | ||||
|             this.customFields = this.baseSample.notes.custom_fields && this.baseSample.notes.custom_fields !== {} ?  // read custom fields | ||||
|               Object.keys(this.baseSample.notes.custom_fields).map(e => [e, this.baseSample.notes.custom_fields[e]]) : []; | ||||
|             if (this.baseSample.notes.sample_references.length) {  // read sample references | ||||
|               this.sampleReferences = []; | ||||
|               this.sampleReferenceAutocomplete = []; | ||||
|               let loadCounter = this.baseSample.notes.sample_references.length;  // count down instances still loading | ||||
|               this.baseSample.notes.sample_references.forEach(reference => { | ||||
|                 this.api.get<SampleModel>('/sample/' + reference.sample_id, srData => {  // get sample numbers for ids | ||||
|                   this.sampleReferences.push([srData.number, reference.relation, reference.sample_id]); | ||||
|                   this.sampleReferenceAutocomplete.push([srData.number]); | ||||
|                   if (!--loadCounter) {  // insert empty template when all instances were loaded | ||||
|                     this.sampleReferences.push(['', '', '']); | ||||
|                     this.sampleReferenceAutocomplete.push([]); | ||||
|                   } | ||||
|                 }); | ||||
|               }); | ||||
|             } | ||||
|             if (this.mode === 'editOne') { | ||||
|               this.charts = [[]]; | ||||
|               let spectrumCounter = 0;  // generate charts for spectrum measurements | ||||
|               this.samples[i].measurements.forEach((measurement, j) => { | ||||
|                 this.charts[i].push(cloneDeep(this.chartInit)); | ||||
|                 if (measurement.values.dpt) { | ||||
|                   setTimeout(() => { | ||||
|                     this.generateChart(measurement.values.dpt, 0, j); | ||||
|                   }, spectrumCounter * 20);  // generate charts one after another to avoid freezing the UI | ||||
|                   spectrumCounter ++; | ||||
|                 } | ||||
|               }); | ||||
|             } | ||||
|             this.checkFormAfterInit = true; | ||||
|           } | ||||
|           else { | ||||
|             ['type', 'color', 'batch', 'notes'].forEach((key) => { | ||||
|               console.log(isEqual(sData[key], this.baseSample[key])); | ||||
|               if (!isEqual(sData[key], this.baseSample[key])) { | ||||
|                 this.baseSample[key] = undefined; | ||||
|               } | ||||
|             }); | ||||
|             if (!isEqual(sData.material.name, this.baseSample.material.name)) { | ||||
|               this.baseSample.material.name = undefined; | ||||
|             } | ||||
|           } | ||||
|           this.loading--; | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
|     else { | ||||
|       this.view.base = true; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   ngAfterContentChecked() { | ||||
|     if (this.generatedSamples.length) {  // conditions are displayed | ||||
|       this.generatedSamples.forEach((gSample, gIndex) => { | ||||
|     if (this.samples.length) {  // conditions are displayed | ||||
|       this.samples.forEach((gSample, gIndex) => { | ||||
|         if (this.d.id.conditionTemplates[gSample.condition.condition_template]) { | ||||
|           this.d.id.conditionTemplates[gSample.condition.condition_template].parameters.forEach((parameter, pIndex) => { | ||||
|             this.attachValidator(this.cmForm, `conditionParameter-${gIndex}-${pIndex}`, parameter.range, true); | ||||
| @@ -207,7 +258,7 @@ export class SampleComponent implements OnInit, AfterContentChecked { | ||||
|     } | ||||
|  | ||||
|     if (this.checkFormAfterInit) { | ||||
|       if (this.editSampleBase) {  // validate sampleForm | ||||
|       if (this.view.base) {  // validate sampleForm | ||||
|         if (this.sampleForm !== undefined && this.sampleForm.form.get('cf-key0')) { | ||||
|           this.checkFormAfterInit = false; | ||||
|           Object.keys(this.sampleForm.form.controls).forEach(field => { | ||||
| @@ -218,20 +269,18 @@ export class SampleComponent implements OnInit, AfterContentChecked { | ||||
|       else {  // validate cmForm | ||||
|         // check that all fields are ready for validation | ||||
|         let formReady: boolean = this.cmForm !== undefined;  // forms exist | ||||
|         if (this.generatedSamples[0].condition.condition_template) {  // if condition is set, last condition field exists | ||||
|         if (this.samples[0].condition.condition_template) {  // if condition is set, last condition field exists | ||||
|           formReady = formReady && this.cmForm.form.get('conditionParameter-0-' + | ||||
|             (this.d.id.conditionTemplates[this.generatedSamples[0].condition.condition_template].parameters.length - 1)) as any; | ||||
|             (this.d.id.conditionTemplates[this.samples[0].condition.condition_template].parameters.length - 1)) as any; | ||||
|         } | ||||
|         if (this.generatedSamples[0].measurements.length) {  // if there are measurements, last measurement field exists | ||||
|           formReady = formReady && this.cmForm.form.get('measurementParameter-0-' + (this.generatedSamples[0].measurements.length - 1) + | ||||
|             '-' + (this.d.id.measurementTemplates[this.generatedSamples[0].measurements[this.generatedSamples[0].measurements.length - 1] | ||||
|         if (this.samples[0].measurements.length) {  // if there are measurements, last measurement field exists | ||||
|           formReady = formReady && this.cmForm.form.get('measurementParameter-0-' + (this.samples[0].measurements.length - 1) + | ||||
|             '-' + (this.d.id.measurementTemplates[this.samples[0].measurements[this.samples[0].measurements.length - 1] | ||||
|               .measurement_template].parameters.length - 1)) as any; | ||||
|         } | ||||
|         if (formReady) {  // fields are ready, do validation | ||||
|           this.checkFormAfterInit = false; | ||||
|           Object.keys(this.cmForm.form.controls).forEach(field => { | ||||
|             console.log(field); | ||||
|             console.log(this.cmForm.form.get(field).valid); | ||||
|             this.cmForm.form.get(field).updateValueAndValidity(); | ||||
|           }); | ||||
|         } | ||||
| @@ -264,17 +313,16 @@ export class SampleComponent implements OnInit, AfterContentChecked { | ||||
|  | ||||
|   // save base sample | ||||
|   saveSample() { | ||||
|     if (this.new) { | ||||
|     if (this.samples.length === 0) { | ||||
|       this.loading = this.sampleCount;  // set up loading spinner | ||||
|     } | ||||
|     new Promise<void>(resolve => { | ||||
|       if (this.newMaterial) {  // save material first if new one exists | ||||
|         console.log(this.material); | ||||
|         this.material.numbers = this.material.numbers.filter(e => e !== ''); | ||||
|         this.api.post<MaterialModel>('/material/new', this.material.sendFormat(), data => { | ||||
|           this.d.arr.materials.push(data);  // add material to data | ||||
|           this.material = data; | ||||
|           this.sample.material_id = data._id;  // add new material id to sample data | ||||
|           this.baseSample.material_id = data._id;  // add new material id to sample data | ||||
|           resolve(); | ||||
|         }); | ||||
|       } | ||||
| @@ -282,29 +330,36 @@ export class SampleComponent implements OnInit, AfterContentChecked { | ||||
|         resolve(); | ||||
|       } | ||||
|     }).then(() => {  // save sample | ||||
|       this.sample.notes.custom_fields = {}; | ||||
|       this.baseSample.notes.custom_fields = {}; | ||||
|       this.customFields.forEach(element => { | ||||
|         if (element[0] !== '') { | ||||
|           this.sample.notes.custom_fields[element[0]] = element[1]; | ||||
|           this.baseSample.notes.custom_fields[element[0]] = element[1]; | ||||
|         } | ||||
|       }); | ||||
|       this.sample.notes.sample_references = this.sampleReferences | ||||
|       this.baseSample.notes.sample_references = this.sampleReferences | ||||
|         .filter(e => e[0] && e[1] && e[2]) | ||||
|         .map(e => ({sample_id: e[2], relation: e[1]})); | ||||
|       if (this.new) { | ||||
|       if (this.samples.length === 0) {  // only save new sample for the first time in mode new, otherwise save changes | ||||
|         for (let i = 0; i < this.sampleCount; i ++) { | ||||
|           this.api.post<SampleModel>('/sample/new', this.sample.sendFormat(), data => { | ||||
|             this.generatedSamples[i] = new SampleModel().deserialize(data); | ||||
|             this.generatedSamples[i].material = this.d.arr.materials.find(e => e._id === this.generatedSamples[i].material_id); | ||||
|           this.api.post<SampleModel>('/sample/new', this.baseSample.sendFormat(), data => { | ||||
|             this.samples[i] = new SampleModel().deserialize(data); | ||||
|             this.samples[i].material = this.d.arr.materials.find(e => e._id === this.samples[i].material_id); | ||||
|             this.loading --; | ||||
|           }); | ||||
|         } | ||||
|         this.view.base = false; | ||||
|         this.view.baseSum = true; | ||||
|         this.view.cm = true; | ||||
|       } | ||||
|       else { | ||||
|         this.api.put<SampleModel>('/sample/' + this.sample._id, this.sample.sendFormat(), data => { | ||||
|           merge(this.generatedSamples[0], omit(data, ['condition'])); | ||||
|           this.generatedSamples[0].material = this.d.arr.materials.find(e => e._id === this.generatedSamples[0].material_id); | ||||
|           this.editSampleBase = false; | ||||
|         this.samples.forEach((sample, i) => { | ||||
|           console.log(sample._id); | ||||
|           this.api.put<SampleModel>('/sample/' + sample._id, this.baseSample.sendFormat(), data => { | ||||
|             merge(this.samples[i], omit(data, ['condition'])); | ||||
|             this.samples[i].material = this.d.arr.materials.find(e => e._id === this.samples[0].material_id); | ||||
|             this.view.base = false; | ||||
|             this.view.baseSum = true; | ||||
|           }); | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
| @@ -312,7 +367,7 @@ export class SampleComponent implements OnInit, AfterContentChecked { | ||||
|  | ||||
|   // save conditions and measurements | ||||
|   cmSave() {  // save measurements and conditions | ||||
|     this.generatedSamples.forEach(sample => { | ||||
|     this.samples.forEach(sample => { | ||||
|       if (sample.condition.condition_template) {  // condition was set | ||||
|         this.api.put('/sample/' + sample._id, {condition: sample.condition}); | ||||
|       } | ||||
| @@ -334,24 +389,45 @@ export class SampleComponent implements OnInit, AfterContentChecked { | ||||
|           this.api.delete('/measurement/' + measurement._id); | ||||
|         } | ||||
|       }); | ||||
|       this.measurementDeleteList.forEach(measurement => { | ||||
|         this.api.delete('/measurement/' + measurement); | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     this.router.navigate(['/samples']); | ||||
|   } | ||||
|  | ||||
|   restoreMeasurements() { | ||||
|     let spectrumCounter = 0;  // generate charts for spectrum measurements | ||||
|     const measurementCount = this.samples[0].measurements.length; | ||||
|     this.measurementRestoreData.forEach((measurement, i) => { | ||||
|       this.api.put('/measurement/restore/' + measurement._id, {}, () => { | ||||
|         this.samples[0].measurements.push(measurement); | ||||
|         this.charts[0].push(cloneDeep(this.chartInit)); | ||||
|         if (measurement.values.dpt) { | ||||
|           setTimeout(() => { | ||||
|             this.generateChart(measurement.values.dpt, 0, measurementCount + i); | ||||
|           }, spectrumCounter * 20);  // generate charts one after another to avoid freezing the UI | ||||
|           spectrumCounter ++; | ||||
|         } | ||||
|         this.checkFormAfterInit = true; | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   // set material based on found material name | ||||
|   findMaterial(name) { | ||||
|     const res = this.d.arr.materials.find(e => e.name === name);  // search for match | ||||
|     if (res) {  // material found | ||||
|       this.material = cloneDeep(res); | ||||
|       this.sample.material_id = this.material._id; | ||||
|       this.baseSample.material_id = this.material._id; | ||||
|     } | ||||
|     else {  // no matching material found | ||||
|       if (this.sample.material_id !== null) {  // reset previous match | ||||
|       if (this.baseSample.material_id !== null) {  // reset previous match | ||||
|         this.material = new MaterialModel(); | ||||
|         this.material.properties.material_template = this.d.latest.materialTemplates.find(e => e.name === 'plastic')._id; | ||||
|       } | ||||
|       this.sample.material_id = null; | ||||
|       this.baseSample.material_id = null; | ||||
|     } | ||||
|     this.setNewMaterial(); | ||||
|   } | ||||
| @@ -359,9 +435,9 @@ export class SampleComponent implements OnInit, AfterContentChecked { | ||||
|   // set newMaterial, if value === null -> toggle | ||||
|   setNewMaterial(value = null) { | ||||
|     if (value === null) {  // toggle dialog | ||||
|       this.newMaterial = !this.sample.material_id; | ||||
|       this.newMaterial = !this.baseSample.material_id; | ||||
|     } | ||||
|     else if (value || (!value && this.sample.material_id !== null )) {  // set to false only if material already exists | ||||
|     else if (value || (!value && this.baseSample.material_id !== null )) {  // set to false only if material already exists | ||||
|       this.newMaterial = value; | ||||
|     } | ||||
|     if (this.newMaterial) {  // set validators if dialog is open | ||||
| @@ -376,10 +452,9 @@ export class SampleComponent implements OnInit, AfterContentChecked { | ||||
|  | ||||
|   // add a new measurement for generated sample at index | ||||
|   addMeasurement(gIndex) { | ||||
|     this.generatedSamples[gIndex].measurements.push( | ||||
|     this.samples[gIndex].measurements.push( | ||||
|       new MeasurementModel(this.d.latest.measurementTemplates.find(e => e.name === 'spectrum')._id) | ||||
|     ); | ||||
|     console.log(this.d.latest.measurementTemplates.find(e => e.name === 'spectrum')); | ||||
|     if (!this.charts[gIndex]) {  // add array if there are no charts yet | ||||
|       this.charts[gIndex] = []; | ||||
|     } | ||||
| @@ -387,22 +462,21 @@ export class SampleComponent implements OnInit, AfterContentChecked { | ||||
|   } | ||||
|  | ||||
|   // remove the measurement at the specified index | ||||
|   removeMeasurement(gIndex, mIndex) { | ||||
|     if (this.generatedSamples[gIndex].measurements[mIndex]._id !== null) { | ||||
|       this.api.delete('/measurement/' + this.generatedSamples[gIndex].measurements[mIndex]._id); | ||||
|   removeMeasurement(gIndex, mIndex) {  // TODO: do not delete directly but only after confirmation | ||||
|     if (this.samples[gIndex].measurements[mIndex]._id !== null) { | ||||
|       this.measurementDeleteList.push(this.samples[gIndex].measurements[mIndex]._id); | ||||
|     } | ||||
|     this.generatedSamples[gIndex].measurements.splice(mIndex, 1); | ||||
|     this.samples[gIndex].measurements.splice(mIndex, 1); | ||||
|     this.charts[gIndex].splice(mIndex, 1); | ||||
|   } | ||||
|  | ||||
|   // remove measurement data at the specified index | ||||
|   // clear entered measurement data at the specified index due to template change | ||||
|   clearMeasurement(gIndex, mIndex) { | ||||
|     this.charts[gIndex][mIndex][0].data = []; | ||||
|     this.generatedSamples[gIndex].measurements[mIndex].values = {}; | ||||
|     this.samples[gIndex].measurements[mIndex].values = {}; | ||||
|   } | ||||
|  | ||||
|   fileToArray(files, gIndex, mIndex, parameter) { | ||||
|     console.log(files); | ||||
|     for (const i in files) { | ||||
|       if (files.hasOwnProperty(i)) { | ||||
|         const fileReader = new FileReader(); | ||||
| @@ -410,14 +484,14 @@ export class SampleComponent implements OnInit, AfterContentChecked { | ||||
|           let index: number = mIndex; | ||||
|           if (Number(i) > 0) {  // append further spectra | ||||
|             this.addMeasurement(gIndex); | ||||
|             index = this.generatedSamples[gIndex].measurements.length - 1; | ||||
|             index = this.samples[gIndex].measurements.length - 1; | ||||
|           } | ||||
|           this.generatedSamples[gIndex].measurements[index].values.device = | ||||
|             this.generatedSamples[gIndex].measurements[mIndex].values.device; | ||||
|           this.generatedSamples[gIndex].measurements[index].values.filename = files[i].name; | ||||
|           this.generatedSamples[gIndex].measurements[index].values[parameter] = | ||||
|             fileReader.result.toString().split('\r\n').map(e => e.split(',')); | ||||
|           this.generateChart(this.generatedSamples[gIndex].measurements[index].values[parameter], gIndex, index); | ||||
|           this.samples[gIndex].measurements[index].values.device = | ||||
|             this.samples[gIndex].measurements[mIndex].values.device; | ||||
|           this.samples[gIndex].measurements[index].values.filename = files[i].name; | ||||
|           this.samples[gIndex].measurements[index].values[parameter] = | ||||
|             fileReader.result.toString().split('\r\n').map(e => e.split(',')).filter(el => el.length === 2); | ||||
|           this.generateChart(this.samples[gIndex].measurements[index].values[parameter], gIndex, index); | ||||
|         }; | ||||
|         fileReader.readAsText(files[i]); | ||||
|       } | ||||
| @@ -457,7 +531,7 @@ export class SampleComponent implements OnInit, AfterContentChecked { | ||||
|   deleteConfirm(modal) { | ||||
|     this.modal.open(modal).then(result => { | ||||
|       if (result) { | ||||
|         this.api.delete('/sample/' + this.sample._id); | ||||
|         this.api.delete('/sample/' + this.baseSample._id); | ||||
|         this.router.navigate(['/samples']); | ||||
|       } | ||||
|     }); | ||||
| @@ -494,7 +568,6 @@ export class SampleComponent implements OnInit, AfterContentChecked { | ||||
|         this.api.get<{ _id: string, number: string }[]>( | ||||
|           '/samples?status[]=validated&status[]=new&page-size=25&sort=number-asc&fields[]=number&fields[]=_id&' + | ||||
|           'filters[]=%7B%22mode%22%3A%22stringin%22%2C%22field%22%3A%22number%22%2C%22values%22%3A%5B%22' + value + '%22%5D%7D', data => { | ||||
|             console.log(data); | ||||
|             this.sampleReferenceAutocomplete[this.currentSRIndex] = data.map(e => e.number); | ||||
|             this.sampleReferenceFinds = data; | ||||
|             observer.next(data.map(e => e.number)); | ||||
|   | ||||
| @@ -4,14 +4,16 @@ | ||||
|   <a routerLink="/samples/new" *ngIf="login.isLevel.write"> | ||||
|     <rb-icon-button icon="add" mode="primary" class="space-left">New sample</rb-icon-button> | ||||
|   </a> | ||||
|   <rb-icon-button *ngIf="validation" mode="secondary" icon="close" (click)="validation = false" class="validation-close" | ||||
|                   iconOnly></rb-icon-button> | ||||
|   <rb-icon-button *ngIf="login.isLevel.dev" [icon]="validation ? 'checkmark' : 'clear-all'" | ||||
|   <rb-icon-button *ngIf="sampleSelect === 2" mode="secondary" icon="close" (click)="sampleSelect = 0" | ||||
|                   class="validation-close" iconOnly></rb-icon-button> | ||||
|   <rb-icon-button *ngIf="login.isLevel.dev" [icon]="sampleSelect === 2 ? 'checkmark' : 'clear-all'" | ||||
|                   mode="secondary" (click)="validate()"> | ||||
|     {{validation ? 'Validate' : 'Validation'}} | ||||
|     {{sampleSelect === 2 ? 'Validate' : 'Validation'}} | ||||
|   </rb-icon-button> | ||||
| </div> | ||||
|  | ||||
| <!--FILTERS--> | ||||
|  | ||||
| <rb-accordion> | ||||
|   <rb-accordion-title [open]="false"><span class="rb-ic rb-ic-filter"></span>  Filter</rb-accordion-title> | ||||
|   <rb-accordion-body> | ||||
| @@ -105,18 +107,23 @@ | ||||
|           </ng-container> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       <rb-icon-button icon="forward-right" mode="primary" (click)="loadSamples({firstPage: true})"> | ||||
|         Apply filters | ||||
|       </rb-icon-button> | ||||
|     </form> | ||||
|  | ||||
|     <rb-icon-button icon="forward-right" mode="primary" (click)="loadSamples({firstPage: true})"> | ||||
|       Apply filters | ||||
|     </rb-icon-button> | ||||
|   </rb-accordion-body> | ||||
| </rb-accordion> | ||||
|  | ||||
|  | ||||
| <ng-container *ngTemplateOutlet="paging"></ng-container> | ||||
|  | ||||
| <rb-loading-spinner class="samples-loading" *ngIf="loading"></rb-loading-spinner> | ||||
| <div class="samples-loading" *ngIf="loading"> | ||||
|   <rb-loading-spinner></rb-loading-spinner> | ||||
|   <div>Loading...</div> | ||||
| </div> | ||||
|  | ||||
| <!--DOWNLOAD BUTTONS--> | ||||
|  | ||||
| <div class="download space-below" *ngIf="login.isLevel.dev"> | ||||
|   <rb-icon-button class="space-right" icon="download" mode="secondary" [rbModal]="linkModal"> | ||||
| @@ -146,10 +153,12 @@ | ||||
|   </a> | ||||
| </div> | ||||
|  | ||||
| <!--TABLE--> | ||||
|  | ||||
| <rb-table class="samples-table" scrollTop ellipsis> | ||||
|   <tr> | ||||
|     <th *ngIf="validation"> | ||||
|       <rb-form-checkbox name="validate-all" (change)="selectAll($event)">all</rb-form-checkbox> | ||||
|     <th *ngIf="sampleSelect"> | ||||
|       <rb-form-checkbox name="select-all" (change)="selectAll($event)">all</rb-form-checkbox> | ||||
|     </th> | ||||
|     <th *ngFor="let key of activeKeys" [title]="key.label"> | ||||
|       <div class="sort-header"> | ||||
| @@ -164,13 +173,16 @@ | ||||
|         </div> | ||||
|       </div> | ||||
|     </th> | ||||
|     <th *ngIf="login.isLevel.write"></th> | ||||
|     <th *ngIf="login.isLevel.write"> | ||||
|       <span class="rb-ic rb-ic-edit clickable" *ngIf="login.isLevel.dev" (click)="batchEdit()"></span> | ||||
|       <span class="rb-ic rb-ic-close clickable" *ngIf="sampleSelect === 1" (click)="sampleSelect = 0"></span> | ||||
|     </th> | ||||
|   </tr> | ||||
|  | ||||
|   <tr *ngFor="let sample of samples; index as i" class="clickable" (click)="sampleDetails(sample._id, sampleModal)"> | ||||
|     <td *ngIf="validation"> | ||||
|     <td *ngIf="sampleSelect"> | ||||
|       <rb-form-checkbox *ngIf="sample.status !== 'deleted'" [name]="'validate-' + i" (click)="stopPropagation($event)" | ||||
|                         [(ngModel)]="sample.validate"> | ||||
|                         [(ngModel)]="sample.selected"> | ||||
|       </rb-form-checkbox> | ||||
|     </td> | ||||
|     <td *ngIf="isActiveKey['number']">{{sample.number}}</td> | ||||
| @@ -221,6 +233,8 @@ | ||||
|   </div> | ||||
| </ng-template> | ||||
|  | ||||
| <!--SAMPLE DETAILS--> | ||||
|  | ||||
| <ng-template #sampleModal> | ||||
|   <rb-loading-spinner *ngIf="sampleDetailsSample === null; else sampleDetailsTemplate"></rb-loading-spinner> | ||||
|   <ng-template #sampleDetailsTemplate> | ||||
|   | ||||
| @@ -237,7 +237,19 @@ textarea.linkmodal { | ||||
|  | ||||
| .samples-loading { | ||||
|   float: left; | ||||
|   transform: scale(0.5); | ||||
|   display: grid; | ||||
|   grid-template-columns: auto auto; | ||||
|   grid-template-rows: 1fr auto 1fr; | ||||
|  | ||||
|   rb-loading-spinner { | ||||
|     grid-row: span 3; | ||||
|     transform: scale(0.5); | ||||
|   } | ||||
|  | ||||
|   & > div { | ||||
|     grid-column: 2 / 3; | ||||
|     grid-row: 2 / 3; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .reset-preferences { | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import {LoginService} from '../services/login.service'; | ||||
| import {ModalService} from '@inst-iot/bosch-angular-ui-components'; | ||||
| import {DataService} from '../services/data.service'; | ||||
| import {LocalStorageService} from 'angular-2-local-storage'; | ||||
| import {Router} from '@angular/router'; | ||||
|  | ||||
| // TODO: turn off sort field | ||||
| // TODO reset sort when field is excluded | ||||
| @@ -63,6 +64,7 @@ export class SamplesComponent implements OnInit { | ||||
|       {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: 'notes.comment', label: 'Comment', active: false, autocomplete: [], mode: 'eq', values: ['']}, | ||||
|       {field: 'added', label: 'Added', active: false, autocomplete: [], mode: 'eq', values: ['']} | ||||
|     ] | ||||
|   }; | ||||
| @@ -78,6 +80,7 @@ export class SamplesComponent implements OnInit { | ||||
|     {id: 'type', label: 'Type', active: true, sortable: true}, | ||||
|     {id: 'color', label: 'Color', active: false, sortable: true}, | ||||
|     {id: 'batch', label: 'Batch', active: true, sortable: true}, | ||||
|     // {id: 'notes.comment', label: 'Comment', active: false, sortable: false}, | ||||
|     {id: 'notes', label: 'Notes', active: false, sortable: false}, | ||||
|     {id: 'status', label: 'Status', active: false, sortable: true}, | ||||
|     {id: 'added', label: 'Added', active: true, sortable: true} | ||||
| @@ -86,7 +89,7 @@ export class SamplesComponent implements OnInit { | ||||
|   activeKeys: KeyInterface[] = []; | ||||
|   activeTemplateKeys = {material: [], condition: [], measurements: []}; | ||||
|   sampleDetailsSample: any = null; | ||||
|   validation = false;  // true to activate validation mode | ||||
|   sampleSelect = 0;  // modes: 0 - no selection, 1 - sample edit selection, 2 - validation selection | ||||
|   loading = 0; | ||||
|  | ||||
|  | ||||
| @@ -97,7 +100,8 @@ export class SamplesComponent implements OnInit { | ||||
|     private modalService: ModalService, | ||||
|     public d: DataService, | ||||
|     private storage: LocalStorageService, | ||||
|     private window: Window | ||||
|     private window: Window, | ||||
|     private router: Router | ||||
|   ) { | ||||
|   } | ||||
|  | ||||
| @@ -285,7 +289,7 @@ export class SamplesComponent implements OnInit { | ||||
|   } | ||||
|  | ||||
|   loadPage(delta) { | ||||
|     if (!/[0-9]+/.test(delta) || (this.page <= 1 && delta < 0)) {  // invalid delta | ||||
|     if (!/[0-9]+/.test(delta) || this.page + delta < 1 || this.page + delta > this.pages) {  // invalid delta | ||||
|       return; | ||||
|     } | ||||
|     this.page += delta; | ||||
| @@ -414,17 +418,27 @@ export class SamplesComponent implements OnInit { | ||||
|   } | ||||
|  | ||||
|   validate() { | ||||
|     if (this.validation) { | ||||
|     if (this.sampleSelect) { | ||||
|       this.samples.forEach(sample => { | ||||
|         if (sample.validate) { | ||||
|         if (sample.selected) { | ||||
|           this.api.put('/sample/validate/' + sample._id); | ||||
|         } | ||||
|       }); | ||||
|       this.loadSamples(); | ||||
|       this.validation = false; | ||||
|       this.sampleSelect = 0; | ||||
|     } | ||||
|     else { | ||||
|       this.validation = true; | ||||
|       this.sampleSelect = 2; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   batchEdit() { | ||||
|     if (this.sampleSelect) { | ||||
|       this.router.navigate(['/samples/edit/' + this.samples.filter(e => e.selected).map(e => e._id).join(',')]); | ||||
|       this.sampleSelect = 0; | ||||
|     } | ||||
|     else { | ||||
|       this.sampleSelect = 1; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -442,10 +456,10 @@ export class SamplesComponent implements OnInit { | ||||
|   selectAll(event) { | ||||
|     this.samples.forEach(sample => { | ||||
|       if (sample.status !== 'deleted') { | ||||
|         sample.validate = event.target.checked; | ||||
|         sample.selected = event.target.checked; | ||||
|       } | ||||
|       else { | ||||
|         sample.validate = false; | ||||
|         sample.selected = false; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import {Observable} from 'rxjs'; | ||||
| import {ErrorComponent} from '../error/error.component'; | ||||
| import {ModalService} from '@inst-iot/bosch-angular-ui-components'; | ||||
|  | ||||
| // TODO: find solution when client wants to visit subpage but is not logged in to redirect to login without showing request failed errors | ||||
|  | ||||
| @Injectable({ | ||||
|   providedIn: 'root' | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import {ApiService} from './api.service'; | ||||
| import {MaterialModel} from '../models/material.model'; | ||||
| import {BaseModel} from '../models/base.model'; | ||||
| import {UserModel} from '../models/user.model'; | ||||
| import {ModelItemModel} from '../models/model-item.model'; | ||||
|  | ||||
| @Injectable({ | ||||
|   providedIn: 'root' | ||||
| @@ -15,14 +16,15 @@ export class DataService { | ||||
|   ) { } | ||||
|  | ||||
|   private collectionMap = { | ||||
|     materials: {path: '/materials?status=all', model: MaterialModel, type: 'array'}, | ||||
|     materialSuppliers: {path: '/material/suppliers', model: null, type: 'array'}, | ||||
|     materialGroups: {path: '/material/groups', model: null, type: 'array'}, | ||||
|     materials: {path: '/materials?status=all', model: MaterialModel, type: 'idArray'}, | ||||
|     materialSuppliers: {path: '/material/suppliers', model: null, type: 'idArray'}, | ||||
|     materialGroups: {path: '/material/groups', model: null, type: 'idArray'}, | ||||
|     materialTemplates: {path: '/template/materials', model: TemplateModel, type: 'template'}, | ||||
|     measurementTemplates: {path: '/template/measurements', model: TemplateModel, type: 'template'}, | ||||
|     conditionTemplates: {path: '/template/conditions', model: TemplateModel, type: 'template'}, | ||||
|     sampleNotesFields: {path: '/sample/notes/fields', model: TemplateModel, type: 'array'}, | ||||
|     users: {path: '/users', model: UserModel, type: 'array'}, | ||||
|     sampleNotesFields: {path: '/sample/notes/fields', model: TemplateModel, type: 'idArray'}, | ||||
|     users: {path: '/users', model: UserModel, type: 'idArray'}, | ||||
|     modelGroups: {path: '/model/groups', model: ModelItemModel, type: 'array'}, | ||||
|     user: {path: '/user', model: UserModel, type: 'string'}, | ||||
|     userKey: {path: '/user/key', model: BaseModel, type: 'string'} | ||||
|   }; | ||||
| @@ -32,6 +34,8 @@ export class DataService { | ||||
|   id: {[key: string]: {[id: string]: any}} = {};  // data in format _id: data | ||||
|   d: {[key: string]: any} = {};      // data not in array format | ||||
|  | ||||
|   contact = 'dominic.lingenfelser@bosch.com'; | ||||
|  | ||||
|   load(collection, f = () => {}) {  // load data | ||||
|     if (this.arr[collection]) { // data already loaded | ||||
|       f(); | ||||
| @@ -41,20 +45,8 @@ export class DataService { | ||||
|         if (this.collectionMap[collection].type !== 'string') {  // array data | ||||
|           this.arr[collection] = data | ||||
|             .map(e => this.collectionMap[collection].model ? new this.collectionMap[collection].model().deserialize(e) : e); | ||||
|           this.idReload(collection); | ||||
|           if (this.collectionMap[collection].type === 'template') { | ||||
|             const tmpTemplates = {}; | ||||
|             this.arr[collection].forEach(template => { | ||||
|               if (tmpTemplates[template.first_id]) {  // already found another version | ||||
|                 if (template.version > tmpTemplates[template.first_id].version) { | ||||
|                   tmpTemplates[template.first_id] = template; | ||||
|                 } | ||||
|               } | ||||
|               else { | ||||
|                 tmpTemplates[template.first_id] = template; | ||||
|               } | ||||
|             }); | ||||
|             this.latest[collection] = Object.values(tmpTemplates); | ||||
|           if (this.collectionMap[collection].type === 'idArray' || this.collectionMap[collection].type === 'template') { | ||||
|             this.idReload(collection); | ||||
|           } | ||||
|         } | ||||
|         else {  // not array data | ||||
| @@ -67,5 +59,19 @@ export class DataService { | ||||
|  | ||||
|   idReload(collection) { | ||||
|     this.id[collection] = this.arr[collection].reduce((s, e) => {s[e._id] = e; return s; }, {}); | ||||
|     if (this.collectionMap[collection].type === 'template') { | ||||
|       const tmpTemplates = {}; | ||||
|       this.arr[collection].forEach(template => { | ||||
|         if (tmpTemplates[template.first_id]) {  // already found another version | ||||
|           if (template.version > tmpTemplates[template.first_id].version) { | ||||
|             tmpTemplates[template.first_id] = template; | ||||
|           } | ||||
|         } | ||||
|         else { | ||||
|           tmpTemplates[template.first_id] = template; | ||||
|         } | ||||
|       }); | ||||
|       this.latest[collection] = Object.values(tmpTemplates); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -103,6 +103,14 @@ export class ValidationService { | ||||
|     return {ok: true, error: ''}; | ||||
|   } | ||||
|  | ||||
|   url(data) { | ||||
|     const {ignore, error} = Joi.string().uri().validate(data); | ||||
|     if (error) { | ||||
|       return {ok: false, error: `must be a valid URL`}; | ||||
|     } | ||||
|     return {ok: true, error: ''}; | ||||
|   } | ||||
|  | ||||
|   unique(data, list) { | ||||
|     const {ignore, error} = Joi.string().allow('').invalid(...list.map(e => e.toString())).validate(data); | ||||
|     if (error) { | ||||
|   | ||||
| @@ -7,9 +7,7 @@ $rb-extended-breakpoints: false;  // whether to use extended breakpoints xxl and | ||||
|   src: url(/assets/fonts/BoschMono.ttf); | ||||
| } | ||||
|  | ||||
| * { | ||||
|   margin: 0; | ||||
|   padding: 0; | ||||
| *, ::after, ::before { | ||||
|   box-sizing: border-box; | ||||
| } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Veit Lukas (PEA4-Fe)
					Veit Lukas (PEA4-Fe)