From 72ecdad5735898d5bc04ac154a630d271e0d3a2a Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Thu, 30 Jul 2020 15:35:19 +0200 Subject: [PATCH 1/2] improved canActivate method to redirect and cover all cases --- cf_config/nginx.conf | 9 --------- src/app/app.component.ts | 2 +- src/app/services/login.service.ts | 26 ++++++++++++++------------ 3 files changed, 15 insertions(+), 22 deletions(-) delete mode 100644 cf_config/nginx.conf diff --git a/cf_config/nginx.conf b/cf_config/nginx.conf deleted file mode 100644 index 841957f..0000000 --- a/cf_config/nginx.conf +++ /dev/null @@ -1,9 +0,0 @@ -gzip on; -gzip_disable "msie6"; - -gzip_vary on; -gzip_proxied any; -gzip_comp_level 6; -gzip_buffers 16 8k; -gzip_http_version 1.1; -gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript; diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 22ec18b..45633aa 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -7,7 +7,7 @@ import {Router} from '@angular/router'; // TODO: filter by not completely filled/no measurements // TODO: validation of samples -// TODO: get rid of chart.js (+moment.js) and lodash +// TODO: get rid of chart.js (+moment.js) @Component({ selector: 'app-root', diff --git a/src/app/services/login.service.ts b/src/app/services/login.service.ts index f4816bc..fd22687 100644 --- a/src/app/services/login.service.ts +++ b/src/app/services/login.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import {ApiService} from './api.service'; -import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router'; +import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router'; import {LocalStorageService} from 'angular-2-local-storage'; import {Observable} from 'rxjs'; @@ -26,7 +26,8 @@ export class LoginService implements CanActivate { constructor( private api: ApiService, - private storage: LocalStorageService + private storage: LocalStorageService, + private router: Router ) { } @@ -79,23 +80,24 @@ export class LoginService implements CanActivate { canActivate(route: ActivatedRouteSnapshot = null, state: RouterStateSnapshot = null): Observable { return new Observable(observer => { - const pathPermission = this.pathPermissions.find(e => e.path.indexOf(route.url[0].path) >= 0); - if (!pathPermission || this.is(pathPermission.permission)) { // check if level is permitted for path + new Promise(resolve => { if (this.loggedIn === undefined) { this.login().then(res => { - observer.next(res as any); - observer.complete(); + resolve(res); }); } else { - observer.next(this.loggedIn); - observer.complete(); + resolve(this.loggedIn); } - } - else { - observer.next(false); + }).then(res => { + const pathPermission = this.pathPermissions.find(e => e.path.indexOf(route.url[0].path) >= 0); + const ok = res && !pathPermission || this.is(pathPermission.permission); // check if level is permitted for path + observer.next(ok); observer.complete(); - } + if (!ok) { + this.router.navigate(['/']); + } + }); }); } From e8ad6aaa7af6e58224e5f5030785f10ddad2af79 Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Thu, 6 Aug 2020 08:18:57 +0200 Subject: [PATCH 2/2] bug button, data service, user level adjustments, multiple sample support for edit dialog, validation functionality --- cf_config/headers.conf | 2 +- package-lock.json | 14 +- package.json | 3 +- src/app/app.component.html | 20 +- src/app/app.component.scss | 4 + src/app/app.component.ts | 28 + .../documentation.component.html | 64 ++- .../documentation.component.scss | 10 + src/app/models/sample.model.ts | 14 +- .../rb-icon-button.component.html | 2 +- .../rb-icon-button.component.scss | 8 + .../rb-icon-button.component.ts | 1 + .../rb-table/rb-table.component.html | 3 +- src/app/sample/sample.component.html | 241 +++++---- src/app/sample/sample.component.scss | 14 + src/app/sample/sample.component.ts | 483 ++++++++++-------- src/app/samples/samples.component.html | 80 ++- src/app/samples/samples.component.scss | 30 ++ src/app/samples/samples.component.ts | 118 ++++- src/app/services/data.service.spec.ts | 16 + src/app/services/data.service.ts | 52 ++ src/app/services/login.service.ts | 17 +- src/app/users/users.component.ts | 1 + src/index.html | 2 +- src/styles.scss | 16 + 25 files changed, 841 insertions(+), 402 deletions(-) create mode 100644 src/app/services/data.service.spec.ts create mode 100644 src/app/services/data.service.ts diff --git a/cf_config/headers.conf b/cf_config/headers.conf index 6a3526d..419a99f 100644 --- a/cf_config/headers.conf +++ b/cf_config/headers.conf @@ -1,7 +1,7 @@ add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; font-src 'self'; connect-src https://definma-api.apps.de1.bosch-iot-cloud.com; form-action 'none'; frame-ancestors 'none'; base-uri 'self'"; add_header X-Frame-Options DENY; add_header X-DNS-Prefetch-Control off; -add_header Strict-Transport-Security "max-age=15552000"; +add_header Strict-Transport-Security "max-age=15552000; includeSubDomains"; add_header X-Download-Options noopen; add_header X-Content-Type-Options nosniff; add_header X-Permitted-Cross-Domain-Policies none; diff --git a/package-lock.json b/package-lock.json index 5f6e05a..352f42d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1887,10 +1887,18 @@ } }, "@inst-iot/bosch-angular-ui-components": { - "version": "file:../Bosch-UI-Components/bosch-angular-ui-components/dist-lib/inst-iot-bosch-angular-ui-components-0.6.0.tgz", - "integrity": "sha512-A4nKOvpdKzq+GWSZlL7U511Ii1vdSA905Q0tru7jAzpBzjRaYqCTiCvsAjRDLM+gVPpzgZ8HpMYfNfhMoNlG/w==", + "version": "0.7.2", + "resolved": "https://rb-artifactory.bosch.com:443/artifactory/api/npm/iot-insights-release-local/@inst-iot/bosch-angular-ui-components/-/@inst-iot/bosch-angular-ui-components-0.7.2.tgz", + "integrity": "sha1-X0AkwkMAy9u/UsG69Zky40LaN5c=", "requires": { - "tslib": "^1.10.0" + "tslib": "^2.0.0" + }, + "dependencies": { + "tslib": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", + "integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==" + } } }, "@istanbuljs/schema": { diff --git a/package.json b/package.json index 6c9df32..64c4dca 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,7 @@ "@angular/platform-browser-dynamic": "~9.1.7", "@angular/router": "~9.1.7", "@hapi/joi": "^17.1.1", - "@inst-iot/bosch-angular-ui-components": - "file:../Bosch-UI-Components/bosch-angular-ui-components/dist-lib/inst-iot-bosch-angular-ui-components-0.6.0.tgz", + "@inst-iot/bosch-angular-ui-components": "^0.7.2", "angular-2-local-storage": "^3.0.2", "chart.js": "^2.9.3", "chartjs-plugin-datalabels": "^0.7.0", diff --git a/src/app/app.component.html b/src/app/app.component.html index 884286d..6244c7f 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -2,8 +2,10 @@ @@ -20,7 +22,19 @@ -
DEVELOPMENTDigital Fingerprint of Plastics
+
+ Bug + +

Report a bug

+ + + + Send report + +
+ DEVELOPMENT + DeFinMa +
diff --git a/src/app/app.component.scss b/src/app/app.component.scss index dd5d5f3..9a9e4b0 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -9,3 +9,7 @@ grid-template-columns: 1fr; grid-row-gap: 10px; } + +.bug-textarea { + width: 800px; +} diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 45633aa..b2a3a5b 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -6,6 +6,8 @@ import {Router} from '@angular/router'; // TODO: validation: DPT: filename // TODO: filter by not completely filled/no measurements // TODO: validation of samples +// TODO: centralize fetching of materials / templates, etc. +// TODO: PWA // TODO: get rid of chart.js (+moment.js) @@ -16,6 +18,8 @@ import {Router} from '@angular/router'; }) export class AppComponent { + bugReport = {do: '', work: ''}; + constructor( public loginService: LoginService, private router: Router @@ -30,4 +34,28 @@ export class AppComponent { this.loginService.logout(); this.router.navigate(['/']); } + + 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 --- +%0D%0A%0D%0ATime: ${new Date().toString()}%0D%0A +URL: ${window.location}%0D%0A%0D%0AWhat did you do?%0D%0A${encodeURIComponent(this.bugReport.do)} +%0D%0A%0D%0AWhat did not work?%0D%0A${encodeURIComponent(this.bugReport.work)}%0D%0A%0D%0ABrowser:%0D%0A +%0D%0AappCodeName: ${navigator.appCodeName} +%0D%0AappVersion: ${navigator.appVersion} +%0D%0Alanguage: ${navigator.language} +%0D%0AonLine: ${navigator.onLine} +%0D%0Aoscpu: ${navigator.oscpu} +%0D%0Aplatform: ${navigator.platform} +%0D%0AuserAgent: ${navigator.userAgent} +%0D%0AinnerWidth: ${window.innerWidth} +%0D%0AinnerHeight: ${window.innerHeight}`; + } + + closeBugReport(close) { + setTimeout(() => close(), 1); + } } + + + diff --git a/src/app/documentation/documentation.component.html b/src/app/documentation/documentation.component.html index 3ff12b0..51de978 100644 --- a/src/app/documentation/documentation.component.html +++ b/src/app/documentation/documentation.component.html @@ -1,9 +1,67 @@ -

Samples

+

Documentation

- Find the API documentation here: https://definma-api.apps.de1.bosch-iot-cloud.com/api-doc/ + Find the API documentation here: + + https://definma-api.apps.de1.bosch-iot-cloud.com/api-doc/ +

+

User levels

+ + + + + read sample data + add samples/edit own + read spectral data + edit other's data + maintain templates + edit users + + + + read + + + + + + + + + + write + + + + + + + + + + dev + + + + + + + + + + admin + + + + + + + + +

Database model

- + diff --git a/src/app/documentation/documentation.component.scss b/src/app/documentation/documentation.component.scss index 58a9ed8..49f650f 100644 --- a/src/app/documentation/documentation.component.scss +++ b/src/app/documentation/documentation.component.scss @@ -1,3 +1,5 @@ +@import "~@inst-iot/bosch-angular-ui-components/styles/variables/colors"; + p { margin-bottom: 20px; } @@ -5,3 +7,11 @@ p { img#db-structure { width: 100%; } + +.rb-ic-checkmark-frame { + color: $brand-success; +} + +.rb-ic-abort-frame { + color: $brand-danger; +} diff --git a/src/app/models/sample.model.ts b/src/app/models/sample.model.ts index 26cea2b..fd5f741 100644 --- a/src/app/models/sample.model.ts +++ b/src/app/models/sample.model.ts @@ -1,4 +1,5 @@ import pick from 'lodash/pick'; +import cloneDeep from 'lodash/cloneDeep'; import {IdModel} from './id.model'; import {MaterialModel} from './material.model'; import {MeasurementModel} from './measurement.model'; @@ -10,12 +11,13 @@ export class SampleModel extends BaseModel { number = ''; type = ''; batch = ''; - condition: {condition_template: string, [prop: string]: string} | {} = {}; + condition: {condition_template: string, [prop: string]: string} = {condition_template: null}; material_id: IdModel = null; material: MaterialModel; measurements: MeasurementModel[] = []; note_id: IdModel = null; user_id: IdModel = null; + validate = false; notes: { comment: string, sample_references: {sample_id: IdModel, relation: string}[], @@ -39,6 +41,14 @@ export class SampleModel extends BaseModel { } sendFormat() { - return pick(this, ['color', 'type', 'batch', 'condition', 'material_id', 'notes']); + return pick(this.conditionTemplateCheck(), ['color', 'type', 'batch', 'condition', 'material_id', 'notes']); + } + + private conditionTemplateCheck() { + const res = cloneDeep(this); + if (res.condition.condition_template === null) { + res.condition = {}; + } + return res; } } diff --git a/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.html b/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.html index 6e4f70d..fdfd5f0 100644 --- a/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.html +++ b/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.html @@ -1,4 +1,4 @@ diff --git a/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.scss b/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.scss index e69de29..8cb83b5 100644 --- a/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.scss +++ b/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.scss @@ -0,0 +1,8 @@ +.icon-space { + margin-right: 0.1em !important; +} + +button.rb-btn > span:not(.icon-space) { + margin-right: -10px; + margin-left: -10px; +} diff --git a/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.ts b/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.ts index 8896e99..413e430 100644 --- a/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.ts +++ b/src/app/rb-custom-inputs/rb-icon-button/rb-icon-button.component.ts @@ -11,6 +11,7 @@ export class RbIconButtonComponent implements OnInit { @Input() icon: string; @Input() mode: string; + @Input() iconOnly; @Input() disabled; @Input() type = 'button'; diff --git a/src/app/rb-custom-inputs/rb-table/rb-table.component.html b/src/app/rb-custom-inputs/rb-table/rb-table.component.html index 5e3c6a4..6bbb804 100644 --- a/src/app/rb-custom-inputs/rb-table/rb-table.component.html +++ b/src/app/rb-custom-inputs/rb-table/rb-table.component.html @@ -1,5 +1,4 @@ - -
+
diff --git a/src/app/sample/sample.component.html b/src/app/sample/sample.component.html index 5780178..6260eda 100644 --- a/src/app/sample/sample.component.html +++ b/src/app/sample/sample.component.html @@ -2,32 +2,33 @@ -
- +
+ [appValidateArgs]="[materialNames]" required [(ngModel)]="material.name" [autofocus]="true"> Cannot be empty Unknown material, add properties for new material - New material + New material

Material properties

- + (focusout)="checkTypo('materialSuppliers', 'supplier', modalWarning)"> {{supplierInput.errors.failure}} - + (focusout)="checkTypo('materialGroups', 'group', modalWarning)"> {{groupInput.errors.failure}} @@ -41,11 +42,12 @@ label="material number" appValidate="string" [name]="'materialNumber-' + item.i" [ngModel]="item.value"> - - + + - {{parameterInput.errors.failure}} @@ -53,15 +55,14 @@
-   -
- - {{typeInput.errors.failure}} + + + + Cannot be empty - - + {{colorInput.errors.failure}} Cannot be empty @@ -112,99 +113,125 @@
-   - -
-

- Condition - -

-
- - - - - - {{parameterInput.errors.failure}} - Cannot be empty - -
-
- -   - -
-

Measurements

-
- - - - -
- - {{parameterInput.errors.failure}} - Cannot be empty - - - Cannot be empty - - - -
- - Delete measurement - -
- -   - -
- - New measurement - -
-
- - -
+
+ + + Cannot be empty + Must be at least 1 + + + -
-

Successfully added sample:

- - Sample number{{responseData.number}} - Type{{responseData.type}} - color{{responseData.color}} - Batch{{responseData.batch}} - Material{{material.name}} - -   - -
- - - +
+
+

Successfully added samples:

+ + + Material{{generatedSamples[0].material.name}} + Type{{generatedSamples[0].type}} + color{{generatedSamples[0].color}} + Batch{{generatedSamples[0].batch}} +
+ +
+
+

{{gSample.number}}

+
+
+ Condition + +
+
+ + + + + + {{parameterInput.errors.failure}} + Cannot be empty + +
+
+ +
+
Measurements
+
+ + + + +
+ + {{parameterInput.errors.failure}} + Cannot be empty + + + Cannot be empty + + + +
+ + Delete measurement + +
+ +   + +
+ + New measurement + +
+
+
+ + Save sample{{generatedSamples.length > 1 ? 's' : ''}} + + + Delete sample + + + + Do you really want to delete {{sample.number}}? + + +
diff --git a/src/app/sample/sample.component.scss b/src/app/sample/sample.component.scss index 8b4e416..8dbbd6a 100644 --- a/src/app/sample/sample.component.scss +++ b/src/app/sample/sample.component.scss @@ -19,3 +19,17 @@ td:first-child { .dpt-chart { max-width: 400px; } + +.sample-count { + max-width: 150px; + margin-right: 20px; + display: inline-block; +} + +.set-new-material { + display: block; +} + +.delete-sample { + float: right; +} diff --git a/src/app/sample/sample.component.ts b/src/app/sample/sample.component.ts index 2bd4f6d..e484f4f 100644 --- a/src/app/sample/sample.component.ts +++ b/src/app/sample/sample.component.ts @@ -1,4 +1,6 @@ import cloneDeep from 'lodash/cloneDeep'; +import merge from 'lodash/merge'; +import omit from 'lodash/omit'; import strCompare from 'str-compare'; import { AfterContentChecked, @@ -13,14 +15,14 @@ import {MaterialModel} from '../models/material.model'; import {SampleModel} from '../models/sample.model'; import {NgForm, Validators} from '@angular/forms'; import {ValidationService} from '../services/validation.service'; -import {TemplateModel} from '../models/template.model'; import {MeasurementModel} from '../models/measurement.model'; import { ChartOptions } from 'chart.js'; import {animate, style, transition, trigger} from '@angular/animations'; import {Observable} from 'rxjs'; import {ModalService} from '@inst-iot/bosch-angular-ui-components'; -import {UserModel} from '../models/user.model'; +import {DataService} from '../services/data.service'; +// TODO: clean up this mess !!! // TODO: only show condition (if not set) and measurements in edit sample dialog at first // TODO: multiple samples for base data, extend multiple measurements, conditions @@ -47,34 +49,33 @@ import {UserModel} from '../models/user.model'; export class SampleComponent implements OnInit, AfterContentChecked { @ViewChild('sampleForm') sampleForm: NgForm; + @ViewChild('cmForm') cmForm: NgForm; + + sample = 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 - new; // true if new sample should be created - newMaterial = false; // true if new material should be created - materials: MaterialModel[] = []; // all materials - ac: {[group: string]: string[]} = { // autocomplete data - supplier: [], - group: [], - materialName: [] - }; - conditionTemplates: TemplateModel[]; // all conditions - condition: TemplateModel | null = null; // selected condition - materialTemplates: TemplateModel[]; // all material templates - materialTemplate: TemplateModel | null = null; // selected material template - material = new MaterialModel(); // object of current selected material - sample = new SampleModel(); - customFields: [string, string][]; sampleReferences: [string, string, string][] = [['', '', '']]; - sampleReferenceFinds: {_id: string, number: string}[] = []; // raw sample reference data from db - currentSRIndex = 0; // index of last entered sample reference - availableCustomFields: string[] = []; + sampleReferenceFinds: {_id: string, number: string}[] = []; // raw sample reference data from db + currentSRIndex = 0; // index of last entered sample reference sampleReferenceAutocomplete: string[][] = [[]]; - responseData: SampleModel; // gets filled with response data after saving the sample - measurementTemplates: TemplateModel[]; - loading = 0; // number of currently loading instances + + customFields: [string, string][] = []; + availableCustomFields: string[] = []; + + newMaterial = false; // true if new material should be created + materials: MaterialModel[] = []; // all materials + materialNames = []; // only names for autocomplete + 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 + loading = 0; // number of currently loading instances checkFormAfterInit = false; modalText = {list: '', suggestion: ''}; - charts = []; // chart data for spectra - defaultDevice = ''; + + charts = [[]]; // chart data for spectra readonly chartInit = [{ data: [], label: 'Spectrum', @@ -102,138 +103,138 @@ export class SampleComponent implements OnInit, AfterContentChecked { private api: ApiService, private validation: ValidationService, public autocomplete: AutocompleteService, - private modal: ModalService + private modal: ModalService, + public d: DataService ) { } ngOnInit(): void { this.new = this.router.url === '/samples/new'; this.loading = 7; - this.api.get('/materials?status=all', (data: any) => { - this.materials = data.map(e => new MaterialModel().deserialize(e)); - this.ac.materialName = data.map(e => e.name); + this.d.load('materials', () => { + this.materialNames = this.d.arr.materials.map(e => e.name); this.loading--; }); - this.api.get('/material/suppliers', (data: any) => { - this.ac.supplier = data; + this.d.load('materialSuppliers', () => { this.loading--; }); - this.api.get('/material/groups', (data: any) => { - this.ac.mgroup = data; + this.d.load('materialGroups', () => { this.loading--; }); - this.api.get('/template/conditions', data => { - this.conditionTemplates = data.map(e => new TemplateModel().deserialize(e)); + this.d.load('conditionTemplates', () => { this.loading--; }); - this.api.get('/template/materials', data => { - this.materialTemplates = data.map(e => new TemplateModel().deserialize(e)); - this.selectMaterialTemplate(this.materialTemplates[0]._id); + this.d.load('measurementTemplates', () => { this.loading--; }); - this.api.get('/user', data => { - this.defaultDevice = data.device_name; - }); - this.api.get('/template/measurements', data => { - this.measurementTemplates = data.map(e => new TemplateModel().deserialize(e)); - if (!this.new) { - this.loading++; - this.api.get('/sample/' + this.route.snapshot.paramMap.get('id'), sData => { - this.sample.deserialize(sData); - this.charts = []; - const spectrumTemplate = this.measurementTemplates.find(e => e.name === 'spectrum')._id; - let spectrumCounter = 0; - this.sample.measurements.forEach((measurement, i) => { - this.charts.push(cloneDeep(this.chartInit)); - if (measurement.measurement_template === spectrumTemplate) { - setTimeout(() => { - this.generateChart(measurement.values.dpt, i); - console.log(this.charts); - }, spectrumCounter * 20); - spectrumCounter ++; - } - }); - this.material = sData.material; - this.customFields = this.sample.notes.custom_fields && this.sample.notes.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) { - this.sampleReferences = []; - this.sampleReferenceAutocomplete = []; - let loadCounter = this.sample.notes.sample_references.length; - this.sample.notes.sample_references.forEach(reference => { - this.api.get('/sample/' + reference.sample_id, srData => { - this.sampleReferences.push([srData.number, reference.relation, reference.sample_id]); - this.sampleReferenceAutocomplete.push([srData.number]); - if (!--loadCounter) { - this.sampleReferences.push(['', '', '']); - this.sampleReferenceAutocomplete.push([]); - console.log(this.sampleReferences); - console.log(this.sampleReferenceAutocomplete); - } - }); - }); - } - if ('condition_template' in this.sample.condition) { - this.selectCondition(this.sample.condition.condition_template); - } - console.log('data loaded'); - this.loading--; - this.checkFormAfterInit = true; - }); + this.d.load('materialTemplates', () => { + if (!this.material.properties.material_template) { + this.material.properties.material_template = this.d.arr.materialTemplates.filter(e => e.name === 'plastic').reverse()[0]._id; } this.loading--; }); - this.api.get('/sample/notes/fields', data => { - this.availableCustomFields = data.map(e => e.name); + this.d.load('sampleNotesFields', () => { + this.availableCustomFields = this.d.arr.sampleNotesFields.map(e => e.name); this.loading--; }); + this.d.load('user', () => { + this.defaultDevice = this.d.d.user.device_name; + }); + if (!this.new) { + this.loading++; + this.api.get('/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('/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([]); + } + }); + }); + } + this.loading--; + this.checkFormAfterInit = true; + }); + } } ngAfterContentChecked() { - // attach validators to dynamic condition fields when all values are available and template was fully created - if (this.condition && this.condition.hasOwnProperty('parameters') && this.condition.parameters.length > 0 && - this.condition.parameters[0].hasOwnProperty('range') && this.sampleForm && this.sampleForm.form.get('conditionParameter0')) { - for (const i in this.condition.parameters) { - if (this.condition.parameters[i]) { - this.attachValidator('conditionParameter' + i, this.condition.parameters[i].range, true); - } - } - } - - // attach validators to dynamic material fields when all values are available and template was fully created - if (this.materialTemplate && this.materialTemplate.hasOwnProperty('parameters') && this.materialTemplate.parameters.length > 0 && - this.materialTemplate.parameters[0].hasOwnProperty('range') && this.sampleForm && this.sampleForm.form.get('materialParameter0')) { - for (const i in this.materialTemplate.parameters) { - if (this.materialTemplate.parameters[i]) { - this.attachValidator('materialParameter' + i, this.materialTemplate.parameters[i].range, true); - } - } - } - - if (this.sampleForm && this.sampleForm.form.get('measurementParameter0-0')) { - this.sample.measurements.forEach((measurement, mIndex) => { - const template = this.getMeasurementTemplate(measurement.measurement_template); - for (const i in template.parameters) { - if (template.parameters[i]) { - this.attachValidator('measurementParameter' + mIndex + '-' + i, template.parameters[i].range, false); - } + if (this.generatedSamples.length) { // conditions are displayed + this.generatedSamples.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); + }); } + gSample.measurements.forEach((measurement, mIndex) => { + this.d.id.measurementTemplates[measurement.measurement_template].parameters.forEach((parameter, pIndex) => { + this.attachValidator(this.cmForm, `measurementParameter-${gIndex}-${mIndex}-${pIndex}`, parameter.range, false); + }); + }); }); - if (this.checkFormAfterInit) { - this.checkFormAfterInit = false; - this.initialValidate(); + } + + if (this.sampleForm && this.material.properties.material_template) { // material template is set + this.d.id.materialTemplates[this.material.properties.material_template].parameters.forEach((parameter, i) => { + this.attachValidator(this.sampleForm, 'materialParameter' + i, parameter.range, true); + }); + } + + if (this.checkFormAfterInit) { + if (this.editSampleBase) { // validate sampleForm + if (this.sampleForm !== undefined && this.sampleForm.form.get('cf-key0')) { + this.checkFormAfterInit = false; + Object.keys(this.sampleForm.form.controls).forEach(field => { + this.sampleForm.form.get(field).updateValueAndValidity(); + }); + } + } + 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 + formReady = formReady && this.cmForm.form.get('conditionParameter-0-' + + (this.d.id.conditionTemplates[this.generatedSamples[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] + .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 => { + this.cmForm.form.get(field).updateValueAndValidity(); + }); + } } } } - initialValidate() { - console.log('initVal'); - Object.keys(this.sampleForm.form.controls).forEach(field => { - this.sampleForm.form.get(field).updateValueAndValidity(); - }); - } - - attachValidator(name: string, range: {[prop: string]: any}, required: boolean) { - if (this.sampleForm.form.get(name)) { + // attach validators specified in range to input with name + attachValidator(form, name: string, range: {[prop: string]: any}, required: boolean) { + if (form && form.form.get(name)) { const validators = []; if (required) { validators.push(Validators.required); @@ -250,15 +251,19 @@ export class SampleComponent implements OnInit, AfterContentChecked { else if (range.hasOwnProperty('max')) { validators.push(this.validation.generate('max', [range.max])); } - this.sampleForm.form.get(name).setValidators(validators); + form.form.get(name).setValidators(validators); } } + // save base sample saveSample() { + if (this.new) { + this.loading = this.sampleCount; // set up loading spinner + } new Promise(resolve => { if (this.newMaterial) { // save material first if new one exists this.api.post('/material/new', this.material.sendFormat(), data => { - this.materials.push(data); // add material to 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 resolve(); @@ -277,161 +282,173 @@ export class SampleComponent implements OnInit, AfterContentChecked { this.sample.notes.sample_references = this.sampleReferences .filter(e => e[0] && e[1] && e[2]) .map(e => ({sample_id: e[2], relation: e[1]})); - new Promise(resolve => { - if (this.new) { - this.api.post('/sample/new', this.sample.sendFormat(), resolve); + if (this.new) { + for (let i = 0; i < this.sampleCount; i ++) { + this.api.post('/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.loading --; + }); } - else { - this.api.put('/sample/' + this.sample._id, this.sample.sendFormat(), resolve); - } - }).then( data => { - this.responseData = new SampleModel().deserialize(data); - this.material = this.materials.find(e => e._id === this.responseData.material_id); - this.sample.measurements.forEach(measurement => { - if (Object.keys(measurement.values).map(e => measurement.values[e]).join('') !== '') { - Object.keys(measurement.values).forEach(key => { - measurement.values[key] = measurement.values[key] === '' ? null : measurement.values[key]; - }); - if (measurement._id === null) { // new measurement - measurement.sample_id = data._id; - this.api.post('/measurement/new', measurement.sendFormat()); - } - else { // update measurement - this.api.put('/measurement/' + measurement._id, - measurement.sendFormat(['sample_id', 'measurement_template'])); - } - } - else if (measurement._id !== null) { // existing measurement was left empty to delete - this.api.delete('/measurement/' + measurement._id); - } + } + else { + this.api.put('/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; }); - }); + } }); } + // save conditions and measurements + cmSave() { // save measurements and conditions + this.generatedSamples.forEach(sample => { + if (sample.condition.condition_template) { // condition was set + this.api.put('/sample/' + sample._id, {condition: sample.condition}); + } + sample.measurements.forEach(measurement => { // save measurements + if (Object.keys(measurement.values).map(e => measurement.values[e]).join('') !== '') { + Object.keys(measurement.values).forEach(key => { // map empty values to null + measurement.values[key] = measurement.values[key] === '' ? null : measurement.values[key]; + }); + if (measurement._id === null) { // new measurement + measurement.sample_id = sample._id; + this.api.post('/measurement/new', measurement.sendFormat()); + } + else { // update measurement + this.api.put('/measurement/' + measurement._id, + measurement.sendFormat(['sample_id', 'measurement_template'])); + } + } + else if (measurement._id !== null) { // existing measurement was left empty to delete + this.api.delete('/measurement/' + measurement._id); + } + }); + }); + + this.router.navigate(['/samples']); + } + + // set material based on found material name findMaterial(name) { - const res = this.materials.find(e => e.name === name); // search for match - if (res) { + 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; } - else { + else { // no matching material found if (this.sample.material_id !== null) { // reset previous match this.material = new MaterialModel(); + this.material.properties.material_template = this.d.arr.materialTemplates.filter(e => e.name === 'plastic').reverse()[0]._id; } this.sample.material_id = null; } this.setNewMaterial(); } - preventDefault(event) { - if (event.key && event.key === 'Enter' || event.type === 'dragover') { - event.preventDefault(); - } - } - + // set newMaterial, if value === null -> toggle setNewMaterial(value = null) { - if (value === null) { + if (value === null) { // toggle dialog this.newMaterial = !this.sample.material_id; } else if (value || (!value && this.sample.material_id !== null )) { // set to false only if material already exists this.newMaterial = value; } - if (this.newMaterial) { + if (this.newMaterial) { // set validators if dialog is open this.sampleForm.form.get('materialname').setValidators([Validators.required]); } - else { + else { // material name must be from list if dialog is closed this.sampleForm.form.get('materialname') - .setValidators([Validators.required, this.validation.generate('stringOf', [this.ac.materialName])]); + .setValidators([Validators.required, this.validation.generate('stringOf', [this.materialNames])]); } this.sampleForm.form.get('materialname').updateValueAndValidity(); } - selectCondition(id) { - this.condition = this.conditionTemplates.find(e => e._id === id); - console.log(this.condition); - console.log(this.sample); - if ('condition_template' in this.sample.condition) { - this.sample.condition.condition_template = id; + // add a new measurement for generated sample at index + addMeasurement(gIndex) { + this.generatedSamples[gIndex].measurements.push( + new MeasurementModel(this.d.arr.measurementTemplates.filter(e => e.name === 'spectrum').reverse()[0]._id) + ); + this.generatedSamples[gIndex].measurements[this.generatedSamples[gIndex].measurements.length - 1].values.device = this.defaultDevice; + if (!this.charts[gIndex]) { // add array if there are no charts yet + this.charts[gIndex] = []; } + this.charts[gIndex].push(cloneDeep(this.chartInit)); } - selectMaterialTemplate(id) { - this.materialTemplate = this.materialTemplates.find(e => e._id === id); - if ('material_template' in this.material.properties) { - this.material.properties.material_template = id; + // 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); } + this.generatedSamples[gIndex].measurements.splice(mIndex, 1); + this.charts[gIndex].splice(mIndex, 1); } - getMeasurementTemplate(id): TemplateModel { - return this.measurementTemplates && id ? this.measurementTemplates.find(e => e._id === id) : new TemplateModel(); + // remove measurement data at the specified index + clearMeasurement(gIndex, mIndex) { + this.charts[gIndex][mIndex][0].data = []; + this.generatedSamples[gIndex].measurements[mIndex].values = {}; } - addMeasurement() { - this.sample.measurements.push(new MeasurementModel(this.measurementTemplates.filter(e => e.name === 'spectrum').reverse()[0]._id)); - this.sample.measurements[this.sample.measurements.length - 1].values.device = this.defaultDevice; - this.charts.push(cloneDeep(this.chartInit)); - } - - removeMeasurement(index) { - if (this.sample.measurements[index]._id !== null) { - this.api.delete('/measurement/' + this.sample.measurements[index]._id); - } - this.sample.measurements.splice(index, 1); - this.charts.splice(index, 1); - } - - clearChart(index) { - this.charts[index][0].data = []; - } - - fileToArray(files, mIndex, parameter) { + fileToArray(files, gIndex, mIndex, parameter) { + console.log(files); for (const i in files) { if (files.hasOwnProperty(i)) { const fileReader = new FileReader(); fileReader.onload = () => { let index: number = mIndex; if (Number(i) > 0) { // append further spectra - this.addMeasurement(); - index = this.sample.measurements.length - 1; + this.addMeasurement(gIndex); + index = this.generatedSamples[gIndex].measurements.length - 1; } - this.sample.measurements[index].values[parameter] = + 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.sample.measurements[index].values[parameter], index); + this.generateChart(this.generatedSamples[gIndex].measurements[index].values[parameter], gIndex, index); }; fileReader.readAsText(files[i]); } } } - generateChart(spectrum, index) { - this.charts[index][0].data = spectrum.map(e => ({x: parseFloat(e[0]), y: parseFloat(e[1])})); + generateChart(spectrum, gIndex, mIndex) { + this.charts[gIndex][mIndex][0].data = spectrum.map(e => ({x: parseFloat(e[0]), y: parseFloat(e[1])})); } - toggleCondition() { - if (this.condition) { - this.condition = null; + toggleCondition(sample) { + if (sample.condition.condition_template) { + sample.condition.condition_template = null; } else { - this.sample.condition = {condition_template: null}; - this.selectCondition(this.conditionTemplates[0]._id); + sample.condition.condition_template = this.d.arr.conditionTemplates[0]._id; } } - checkTypo(list, modal: TemplateRef) { - if (this.ac[list].indexOf(this.material[list]) < 0) { // entry is not in lise - this.modalText.list = list; - this.modalText.suggestion = this.ac[list] // find possible entry from list - .map(e => ({v: e, s: strCompare.sorensenDice(e, this.material[list])})) + checkTypo(list, mKey, modal: TemplateRef) { + if (this.d.arr[list].indexOf(this.material[list]) < 0) { // entry is not in list + this.modalText.list = mKey; + this.modalText.suggestion = this.d.arr[list] // find possible entry from list + .map(e => ({v: e, s: strCompare.sorensenDice(e, this.material[mKey])})) .sort((a, b) => b.s - a.s)[0].v; this.modal.open(modal).then(result => { if (result) { // use suggestion - this.material[list] = this.modalText.suggestion; + this.material[mKey] = this.modalText.suggestion; } }); } } + deleteConfirm(modal) { + this.modal.open(modal).then(result => { + if (result) { + this.api.delete('/sample/' + this.sample._id); + this.router.navigate(['/samples']); + } + }); + } + checkSampleReference(value, index) { if (value) { this.sampleReferences[index][0] = value; @@ -459,16 +476,22 @@ export class SampleComponent implements OnInit, AfterContentChecked { sampleReferenceList(value) { return new Observable(observer => { - this.api.get<{_id: string, number: string}[]>( - '/samples?status=all&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)); + if (value !== '') { + this.api.get<{ _id: string, number: string }[]>( + '/samples?status=all&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)); + observer.complete(); + this.sampleReferenceIdFind(value); + }); + } + else { + observer.next([]); observer.complete(); - this.sampleReferenceIdFind(value); - }); + } }); } @@ -489,6 +512,12 @@ export class SampleComponent implements OnInit, AfterContentChecked { uniqueCfValues(index) { // returns all names until index for unique check return this.customFields ? this.customFields.slice(0, index).map(e => e[0]) : []; } + + preventDefault(event) { + if (event.key && event.key === 'Enter' || event.type === 'dragover') { + event.preventDefault(); + } + } } diff --git a/src/app/samples/samples.component.html b/src/app/samples/samples.component.html index 24f665b..022b8a9 100644 --- a/src/app/samples/samples.component.html +++ b/src/app/samples/samples.component.html @@ -1,9 +1,15 @@

Samples

- - New sample + + New sample + + + Validate +
@@ -86,13 +92,15 @@ -
- JSON download link +
+ + JSON download link + - + add spectra Copy to clipboard @@ -104,8 +112,11 @@
- + + + all +
{{key.label}} @@ -119,17 +130,21 @@
- + - + + + + + {{sample.number}} - {{materials[sample.material_id].numbers}} - {{materials[sample.material_id].name}} - {{materials[sample.material_id].supplier}} - {{materials[sample.material_id].group}} + {{d.id.materials[sample.material_id].numbers}} + {{d.id.materials[sample.material_id].name}} + {{d.id.materials[sample.material_id].supplier}} + {{d.id.materials[sample.material_id].group}} - {{materials[sample.material_id].properties[key[2]] | exists}} + {{d.id.materials[sample.material_id].properties[key[2]] | exists}} {{sample.type}} {{sample.color}} @@ -137,7 +152,12 @@ {{sample.notes | object: ['_id', 'sample_references']}} {{sample[key[1]] | exists: key[2]}} {{sample.added | date:'dd/MM/yy'}} - + + + + +
@@ -159,3 +179,35 @@
+ + + +

{{sampleDetailsSample.number}}

+ + Material{{sampleDetailsSample.material.name}} + Supplier{{sampleDetailsSample.material.supplier}} + Group{{sampleDetailsSample.material.group}} + Type{{sampleDetailsSample.type}} + color{{sampleDetailsSample.color}} + Batch{{sampleDetailsSample.batch}} + Comment{{sampleDetailsSample.notes.comment | exists}} + + {{customField[0]}} + {{customField[0]}} + + + {{reference.relation}} + + + + + + {{measurement.name}} + {{measurement.value}} + + User{{sampleDetailsSample.user}} + +
+
diff --git a/src/app/samples/samples.component.scss b/src/app/samples/samples.component.scss index 7d9f0d4..9684546 100644 --- a/src/app/samples/samples.component.scss +++ b/src/app/samples/samples.component.scss @@ -180,3 +180,33 @@ textarea.linkmodal { display: inline-block; width: 220px; } + +.sample-details-table { + + ::ng-deep .table-wrapper { + max-height: 80vh; + overflow-y: scroll; + } + + td { + max-width: none; + } +} + +.validation-close { + margin-left: -1px; +} + +.samples-table tr.clickable { + background: none; + transition: background-color 0.5s; + + &:hover { + background: $color-gray-mercury; + } +} + +::ng-deep .samples-table rb-form-checkbox .input-wrapper { + padding-top: 0; + margin-top: -4.5px; +} diff --git a/src/app/samples/samples.component.ts b/src/app/samples/samples.component.ts index a4dd544..0bfc48a 100644 --- a/src/app/samples/samples.component.ts +++ b/src/app/samples/samples.component.ts @@ -1,9 +1,12 @@ -import {Component, ElementRef, isDevMode, OnInit, ViewChild} from '@angular/core'; +import {Component, ElementRef, isDevMode, OnInit, TemplateRef, ViewChild} from '@angular/core'; import {ApiService} from '../services/api.service'; import {AutocompleteService} from '../services/autocomplete.service'; import cloneDeep from 'lodash/cloneDeep'; import pick from 'lodash/pick'; import {SampleModel} from '../models/sample.model'; +import {LoginService} from '../services/login.service'; +import {ModalService} from '@inst-iot/bosch-angular-ui-components'; +import {DataService} from '../services/data.service'; interface LoadSamplesOptions { @@ -32,7 +35,6 @@ export class SamplesComponent implements OnInit { @ViewChild('linkarea') linkarea: ElementRef; downloadCsv = false; - materials = {}; samples: SampleModel[] = []; totalSamples = 0; // total number of samples csvUrl = ''; // store url separate so it only has to be generated when clicking the download button @@ -60,7 +62,6 @@ export class SamplesComponent implements OnInit { page = 1; pages = 1; loadSamplesQueue = []; // arguments of queued up loadSamples() calls - apiKey = ''; keys: KeyInterface[] = [ {id: 'number', label: 'Number', active: true, sortable: true}, {id: 'material.numbers', label: 'Material numbers', active: true, sortable: false}, @@ -76,55 +77,55 @@ export class SamplesComponent implements OnInit { isActiveKey: {[key: string]: boolean} = {}; activeKeys: KeyInterface[] = []; activeTemplateKeys = {material: [], measurements: []}; + sampleDetailsSample: any = null; + validation = false; // true to activate validation mode constructor( private api: ApiService, - public autocomplete: AutocompleteService + public autocomplete: AutocompleteService, + public login: LoginService, + private modalService: ModalService, + public d: DataService ) { } ngOnInit(): void { this.calcFieldSelectKeys(); - this.api.get('/materials?status=all', (mData: any) => { - this.materials = {}; - mData.forEach(material => { - this.materials[material._id] = material; - }); - this.filters.filters.find(e => e.field === 'material.name').autocomplete = mData.map(e => e.name); + this.d.load('materials', () => { + this.filters.filters.find(e => e.field === 'material.name').autocomplete = this.d.arr.materials.map(e => e.name); this.filters.filters.find(e => e.field === 'color').autocomplete = - [...new Set(mData.reduce((s, e) => {s.push(...e.numbers.map(el => el.color)); return s; }, []))]; + [...new Set(this.d.arr.materials.reduce((s, e) => {s.push(...e.numbers.map(el => el.color)); return s; }, []))]; this.loadSamples(); }); - this.api.get('/user/key', (data: {key: string}) => { - this.apiKey = data.key; + this.d.load('materialSuppliers', () => { + this.filters.filters.find(e => e.field === 'material.supplier').autocomplete = this.d.arr.materialSuppliers; }); - this.api.get('/material/suppliers', (data: any) => { - this.filters.filters.find(e => e.field === 'material.supplier').autocomplete = data; + this.d.load('materialGroups', () => { + console.log(this.d.arr.materialGroups); + this.filters.filters.find(e => e.field === 'material.group').autocomplete = this.d.arr.materialGroups; }); - this.api.get('/material/groups', (data: any) => { - this.filters.filters.find(e => e.field === 'material.group').autocomplete = data; - }); - this.loadTemplateKeys('materials', 'type'); - this.loadTemplateKeys('measurements', 'added'); + this.d.load('userKey'); + this.loadTemplateKeys('material', 'type'); + this.loadTemplateKeys('measurement', 'added'); } loadTemplateKeys(collection, insertBefore) { - this.api.get('/template/' + collection, (data: {name: string, parameters: {name: string, range: object}[]}[]) => { + this.d.load(collection + 'Templates', () => { const templateKeys = []; - data.forEach(item => { + this.d.arr[collection + 'Templates'].forEach(item => { item.parameters.forEach(parameter => { const parameterName = encodeURIComponent(parameter.name); - // exclude spectrum + // exclude spectrum and duplicates if (parameter.name !== 'dpt' && !templateKeys.find(e => new RegExp('.' + parameterName + '$').test(e.id))) { templateKeys.push({ - id: `${collection === 'materials' ? 'material.properties' : collection + '.' + item.name}.${parameterName}`, + id: `${collection === 'material' ? 'material.properties' : collection + 's.' + item.name}.${parameterName}`, label: `${this.ucFirst(item.name)} ${this.ucFirst(parameter.name)}`, active: false, sortable: true }); this.filters.filters.push({ - field: `${collection === 'materials' ? 'material.properties' : collection + '.' + item.name}.${parameterName}`, + field: `${collection === 'material' ? 'material.properties' : collection + 's.' + item.name}.${parameterName}`, label: `${this.ucFirst(item.name)} ${this.ucFirst(parameter.name)}`, active: false, autocomplete: [], @@ -181,7 +182,7 @@ export class SamplesComponent implements OnInit { export?: boolean, host?: boolean }) { // return url to fetch samples - const additionalTableKeys = ['material_id', '_id']; // keys which should always be added if export = false + const additionalTableKeys = ['material_id', '_id', 'user_id']; // keys which should always be added if export = false const query: string[] = []; query.push( 'status=' + (this.filters.status.new && this.filters.status.validated ? 'all' : (this.filters.status.new ? 'new' : 'validated')) @@ -205,7 +206,7 @@ export class SamplesComponent implements OnInit { query.push('csv=true'); } if (options.export) { - query.push('key=' + this.apiKey); + query.push('key=' + this.d.d.userKey.key); } this.keys.forEach(key => { // do not load material properties for table @@ -278,12 +279,75 @@ export class SamplesComponent implements OnInit { }); } + // TODO: add measurements when ressource service is done + sampleDetails(id: string, modal: TemplateRef) { + this.sampleDetailsSample = null; + this.api.get('/sample/' + id, data => { + this.sampleDetailsSample = new SampleModel().deserialize(data); + if (data.notes.custom_fields) { // convert custom_fields for more optimized display + this.sampleDetailsSample.notes.custom_fields_entries = Object.entries(this.sampleDetailsSample.notes.custom_fields); + } + else { + this.sampleDetailsSample.custom_fields_entries = []; + } + this.sampleDetailsSample.measurement_entries = []; + this.sampleDetailsSample.measurements.forEach(measurement => { // convert measurements for more optimized display without dpt + const name = this.d.id.measurementTemplates[measurement.measurement_template].name; + this.sampleDetailsSample.measurement_entries.push(...Object.entries(measurement.values).filter(e => e[0] !== 'dpt') + .map(e => ({name: this.ucFirst(name) + ' ' + this.ucFirst(e[0]), value: e[1]}))); + }); + new Promise(resolve => { + if (data.notes.sample_references.length) { // load referenced samples if available + let loadingCounter = data.notes.sample_references.length; + this.sampleDetailsSample.notes.sample_references.forEach(reference => { + this.api.get('/sample/' + reference.sample_id, rData => { + reference.number = rData.number; + loadingCounter --; + if (!loadingCounter) { + resolve(); + } + }); + }); + } + else { + resolve(); + } + }).then(() => { + this.modalService.open(modal).then(() => {}); + }); + }); + } + + validate() { + if (this.validation) { + this.samples.forEach(sample => { + if (sample.validate) { + this.api.put('/sample/validate/' + sample._id); + } + }); + this.validation = false; + } + else { + this.validation = true; + } + } + + selectAll(event) { + this.samples.forEach(sample => { + sample.validate = event.target.checked; + }); + } + preventDefault(event, key = 'all') { if (key === 'all' || event.key === key) { event.preventDefault(); } } + stopPropagation(event) { + event.stopPropagation(); + } + clipboard() { this.linkarea.nativeElement.select(); this.linkarea.nativeElement.setSelectionRange(0, 99999); diff --git a/src/app/services/data.service.spec.ts b/src/app/services/data.service.spec.ts new file mode 100644 index 0000000..38e8d9e --- /dev/null +++ b/src/app/services/data.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { DataService } from './data.service'; + +describe('DataService', () => { + let service: DataService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(DataService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/data.service.ts b/src/app/services/data.service.ts new file mode 100644 index 0000000..b3196e4 --- /dev/null +++ b/src/app/services/data.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@angular/core'; +import {TemplateModel} from '../models/template.model'; +import {ApiService} from './api.service'; +import {MaterialModel} from '../models/material.model'; +import {BaseModel} from '../models/base.model'; +import {UserModel} from '../models/user.model'; + +@Injectable({ + providedIn: 'root' +}) +export class DataService { + + constructor( + private api: ApiService + ) { } + + private collectionMap = { + materials: {path: '/materials?status=all', model: MaterialModel, array: true}, + materialSuppliers: {path: '/material/suppliers', model: null, array: true}, + materialGroups: {path: '/material/groups', model: null, array: true}, + materialTemplates: {path: '/template/materials', model: TemplateModel, array: true}, + measurementTemplates: {path: '/template/measurements', model: TemplateModel, array: true}, + conditionTemplates: {path: '/template/conditions', model: TemplateModel, array: true}, + sampleNotesFields: {path: '/sample/notes/fields', model: TemplateModel, array: true}, + users: {path: '/users', model: UserModel, array: true}, + user: {path: '/user', model: UserModel, array: false}, + userKey: {path: '/user/key', model: BaseModel, array: false} + }; + + arr: {[key: string]: any[]} = {}; // array of data + id: {[key: string]: {[id: string]: any}} = {}; // data in format _id: data + d: {[key: string]: any} = {}; // data not in array format + + load(collection, f = () => {}) { // load data + if (this.arr[collection]) { // data already loaded + f(); + } + else { // load data + this.api.get(this.collectionMap[collection].path, data => { + if (this.collectionMap[collection].array) { // array data + this.arr[collection] = data + .map(e => this.collectionMap[collection].model ? new this.collectionMap[collection].model().deserialize(e) : e); + this.id[collection] = this.arr[collection].reduce((s, e) => {s[e._id] = e; return s; }, {}); + } + else { // not array data + this.d[collection] = new this.collectionMap[collection].model().deserialize(data); + } + f(); + }); + } + } +} diff --git a/src/app/services/login.service.ts b/src/app/services/login.service.ts index fd22687..bced041 100644 --- a/src/app/services/login.service.ts +++ b/src/app/services/login.service.ts @@ -10,19 +10,19 @@ import {Observable} from 'rxjs'; export class LoginService implements CanActivate { private pathPermissions = [ - {path: 'templates', permission: 'maintain'}, + {path: 'templates', permission: 'dev'}, {path: 'users', permission: 'admin'} ]; readonly levels = [ 'read', 'write', - 'maintain', 'dev', 'admin' ]; + isLevel: {[level: string]: boolean} = {}; + userId = ''; private loggedIn; - private level; constructor( private api: ApiService, @@ -57,7 +57,10 @@ export class LoginService implements CanActivate { if (!error) { if (data.status === 'Authorization successful') { this.loggedIn = true; - this.level = data.level; + this.levels.forEach(level => { + this.isLevel[level] = this.levels.indexOf(data.level) >= this.levels.indexOf(level); + }); + this.userId = data.user_id; resolve(true); } else { this.loggedIn = false; @@ -91,7 +94,7 @@ export class LoginService implements CanActivate { } }).then(res => { const pathPermission = this.pathPermissions.find(e => e.path.indexOf(route.url[0].path) >= 0); - const ok = res && !pathPermission || this.is(pathPermission.permission); // check if level is permitted for path + const ok = res && (!pathPermission || this.isLevel[pathPermission.permission]); // check if level is permitted for path observer.next(ok); observer.complete(); if (!ok) { @@ -105,10 +108,6 @@ export class LoginService implements CanActivate { return this.loggedIn; } - is(level) { - return this.levels.indexOf(this.level) >= this.levels.indexOf(level); - } - get username() { return atob(this.storage.get('basicAuth')).split(':')[0]; } diff --git a/src/app/users/users.component.ts b/src/app/users/users.component.ts index d80817b..58e77bf 100644 --- a/src/app/users/users.component.ts +++ b/src/app/users/users.component.ts @@ -37,6 +37,7 @@ export class UsersComponent implements OnInit { this.api.post('/user/new', {...this.newUser.sendFormat('admin'), pass: this.newUserPass}, data => { this.newUser = null; this.users.push(new UserModel().deserialize(data)); + this.newUserPass = ''; }); } diff --git a/src/index.html b/src/index.html index e6d56b7..bc0c164 100644 --- a/src/index.html +++ b/src/index.html @@ -2,7 +2,7 @@ - DFOP + DeFinMa diff --git a/src/styles.scss b/src/styles.scss index a0f6fd5..b3497c3 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -23,3 +23,19 @@ button::-moz-focus-inner { .clickable { cursor: pointer; } + +.space-below { + margin-bottom: $default-spacing; +} + +.space-above { + margin-top: $default-spacing; +} + +.space-right { + margin-right: $default-spacing; +} + +.space-left { + margin-left: $default-spacing; +}