import cloneDeep from 'lodash/cloneDeep'; import strCompare from 'str-compare'; import { AfterContentChecked, Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; import {ActivatedRoute, Router} from '@angular/router'; import {AutocompleteService} from '../services/autocomplete.service'; import {ApiService} from '../services/api.service'; 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'; // 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 @Component({ selector: 'app-sample', templateUrl: './sample.component.html', styleUrls: ['./sample.component.scss'], animations: [ trigger( 'inOut', [ transition(':enter', [ style({height: 0, opacity: 0}), animate('0.5s ease-out', style({height: '*', opacity: 1})) ]), transition(':leave', [ style({height: '*', opacity: 1}), animate('0.5s ease-in', style({height: 0, opacity: 0})) ]) ] ) ] }) export class SampleComponent implements OnInit, AfterContentChecked { @ViewChild('sampleForm') sampleForm: NgForm; 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[] = []; sampleReferenceAutocomplete: string[][] = [[]]; responseData: SampleModel; // gets filled with response data after saving the sample measurementTemplates: TemplateModel[]; loading = 0; // number of currently loading instances checkFormAfterInit = false; modalText = {list: '', suggestion: ''}; charts = []; // chart data for spectra defaultDevice = ''; readonly chartInit = [{ data: [], label: 'Spectrum', showLine: true, fill: false, pointRadius: 0, borderColor: '#00a8b0', borderWidth: 2 }]; readonly chartOptions: ChartOptions = { scales: { xAxes: [{ticks: {min: 400, max: 4000, stepSize: 400, reverse: true}}], yAxes: [{ticks: {min: 0, max: 1}}] }, responsive: true, tooltips: {enabled: false}, hover: {mode: null}, maintainAspectRatio: true, plugins: {datalabels: {display: false}} }; constructor( private router: Router, private route: ActivatedRoute, private api: ApiService, private validation: ValidationService, public autocomplete: AutocompleteService, private modal: ModalService ) { } 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.loading--; }); this.api.get('/material/suppliers', (data: any) => { this.ac.supplier = data; this.loading--; }); this.api.get('/material/groups', (data: any) => { this.ac.mgroup = data; this.loading--; }); this.api.get('/template/conditions', data => { this.conditionTemplates = data.map(e => new TemplateModel().deserialize(e)); this.loading--; }); this.api.get('/template/materials', data => { this.materialTemplates = data.map(e => new TemplateModel().deserialize(e)); this.selectMaterialTemplate(this.materialTemplates[0]._id); 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.loading--; }); this.api.get('/sample/notes/fields', data => { this.availableCustomFields = data.map(e => e.name); this.loading--; }); } 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.checkFormAfterInit) { this.checkFormAfterInit = false; this.initialValidate(); } } } 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)) { const validators = []; if (required) { validators.push(Validators.required); } if (range.hasOwnProperty('values')) { validators.push(this.validation.generate('stringOf', [range.values])); } else if (range.hasOwnProperty('min') && range.hasOwnProperty('max')) { validators.push(this.validation.generate('minMax', [range.min, range.max])); } else if (range.hasOwnProperty('min')) { validators.push(this.validation.generate('min', [range.min])); } else if (range.hasOwnProperty('max')) { validators.push(this.validation.generate('max', [range.max])); } this.sampleForm.form.get(name).setValidators(validators); } } saveSample() { 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.material = data; this.sample.material_id = data._id; // add new material id to sample data resolve(); }); } else { resolve(); } }).then(() => { // save sample this.sample.notes.custom_fields = {}; this.customFields.forEach(element => { if (element[0] !== '') { this.sample.notes.custom_fields[element[0]] = element[1]; } }); 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); } 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); } }); }); }); } findMaterial(name) { const res = this.materials.find(e => e.name === name); // search for match if (res) { this.material = cloneDeep(res); this.sample.material_id = this.material._id; } else { if (this.sample.material_id !== null) { // reset previous match this.material = new MaterialModel(); } this.sample.material_id = null; } this.setNewMaterial(); } preventDefault(event) { if (event.key && event.key === 'Enter' || event.type === 'dragover') { event.preventDefault(); } } setNewMaterial(value = null) { if (value === null) { 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) { this.sampleForm.form.get('materialname').setValidators([Validators.required]); } else { this.sampleForm.form.get('materialname') .setValidators([Validators.required, this.validation.generate('stringOf', [this.ac.materialName])]); } 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; } } selectMaterialTemplate(id) { this.materialTemplate = this.materialTemplates.find(e => e._id === id); if ('material_template' in this.material.properties) { this.material.properties.material_template = id; } } getMeasurementTemplate(id): TemplateModel { return this.measurementTemplates && id ? this.measurementTemplates.find(e => e._id === id) : new TemplateModel(); } 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) { 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.sample.measurements[index].values[parameter] = fileReader.result.toString().split('\r\n').map(e => e.split(',')); this.generateChart(this.sample.measurements[index].values[parameter], index); }; fileReader.readAsText(files[i]); } } } generateChart(spectrum, index) { this.charts[index][0].data = spectrum.map(e => ({x: parseFloat(e[0]), y: parseFloat(e[1])})); } toggleCondition() { if (this.condition) { this.condition = null; } else { this.sample.condition = {condition_template: null}; this.selectCondition(this.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])})) .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; } }); } } checkSampleReference(value, index) { if (value) { this.sampleReferences[index][0] = value; } this.currentSRIndex = index; const fieldNo = this.sampleReferences.length; let filledFields = 0; this.sampleReferences.forEach(field => { if (field[0] !== '') { filledFields ++; } }); // append new field if (filledFields === fieldNo) { this.sampleReferences.push(['', '', '']); this.sampleReferenceAutocomplete.push([]); } // remove if two end fields are empty if (fieldNo > 1 && this.sampleReferences[fieldNo - 1][0] === '' && this.sampleReferences[fieldNo - 2][0] === '') { this.sampleReferences.pop(); this.sampleReferenceAutocomplete.pop(); } this.sampleReferenceIdFind(value); } 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)); observer.complete(); this.sampleReferenceIdFind(value); }); }); } sampleReferenceIdFind(value) { const idFind = this.sampleReferenceFinds.find(e => e.number === value); if (idFind) { this.sampleReferences[this.currentSRIndex][2] = idFind._id; } else { this.sampleReferences[this.currentSRIndex][2] = ''; } } sampleReferenceListBind() { return this.sampleReferenceList.bind(this); } uniqueCfValues(index) { // returns all names until index for unique check return this.customFields ? this.customFields.slice(0, index).map(e => e[0]) : []; } } // 1. ngAfterViewInit wird ja jedes mal nach einem ngOnChanges aufgerufen, also zB wenn sich dein ngFor aufbaut. Du könntest also in der // Methode prüfen, ob die Daten schon da sind und dann dementsprechend handeln. Das wäre die Eleganteste Variante // 2. Der state "dirty" soll eigentlich anzeigen, wenn ein Form-Field vom User geändert wurde; damit missbrauchst du es hier etwas // 3. Die Dirty-Variante: Pack in deine ngFor ein {{ onFirstLoad(data) }} rein, das einfach ausgeführt wird. müsstest dann natürlich // abfangen, dass das nicht nach jedem view-cycle neu getriggert wird. Schön ist das nicht, aber besser als mit Timeouts^^