495 lines
19 KiB
TypeScript
495 lines
19 KiB
TypeScript
import _ from 'lodash';
|
|
import {
|
|
AfterContentChecked,
|
|
Component,
|
|
OnInit,
|
|
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';
|
|
|
|
// TODO: tests
|
|
// TODO: confirmation for new group/supplier
|
|
// TODO: work on better recognition for file input
|
|
// TODO: only show condition (if not set) and measurements in edit sample dialog at first
|
|
// TODO: multiple spectra
|
|
// TODO: multiple samples for base data, extend multiple measurements, conditions
|
|
|
|
// TODO: material properties, color (in material and sample (not required))
|
|
|
|
// TODO: device autocomplete
|
|
|
|
@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
|
|
suppliers: string[] = []; // all suppliers
|
|
groups: string[] = []; // all groups
|
|
conditionTemplates: TemplateModel[]; // all conditions
|
|
condition: TemplateModel | null = null; // selected condition
|
|
materialTemplates: TemplateModel[]; // all material templates
|
|
materialTemplate: TemplateModel | null = null; // selected material template
|
|
materialNames = []; // names of all materials
|
|
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;
|
|
charts = []; // chart data for spectrums
|
|
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
|
|
) { }
|
|
|
|
ngOnInit(): void {
|
|
this.new = this.router.url === '/samples/new';
|
|
this.loading = 7;
|
|
this.api.get<MaterialModel[]>('/materials?status=all', (data: any) => {
|
|
this.materials = data.map(e => new MaterialModel().deserialize(e));
|
|
this.materialNames = data.map(e => e.name);
|
|
this.loading--;
|
|
});
|
|
this.api.get<string[]>('/material/suppliers', (data: any) => {
|
|
this.suppliers = data;
|
|
this.loading--;
|
|
});
|
|
this.api.get<string[]>('/material/groups', (data: any) => {
|
|
this.groups = data;
|
|
this.loading--;
|
|
});
|
|
this.api.get<TemplateModel[]>('/template/conditions', data => {
|
|
this.conditionTemplates = data.map(e => new TemplateModel().deserialize(e));
|
|
this.loading--;
|
|
});
|
|
this.api.get<TemplateModel[]>('/template/materials', data => {
|
|
this.materialTemplates = data.map(e => new TemplateModel().deserialize(e));
|
|
this.selectMaterialTemplate(this.materialTemplates[0]._id);
|
|
this.loading--;
|
|
});
|
|
this.api.get<TemplateModel[]>('/template/measurements', data => {
|
|
this.measurementTemplates = data.map(e => new TemplateModel().deserialize(e));
|
|
if (!this.new) {
|
|
this.loading++;
|
|
this.api.get<SampleModel>('/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<SampleModel>('/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<TemplateModel[]>('/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<void>(resolve => {
|
|
if (this.newMaterial) { // save material first if new one exists
|
|
this.material.numbers = this.material.numbers.filter(e => e !== '');
|
|
this.api.post<MaterialModel>('/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<SampleModel>(resolve => {
|
|
if (this.new) {
|
|
this.api.post<SampleModel>('/sample/new', this.sample.sendFormat(), resolve);
|
|
}
|
|
else {
|
|
this.api.put<SampleModel>('/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<MeasurementModel>('/measurement/new', measurement.sendFormat());
|
|
}
|
|
else { // update measurement
|
|
this.api.put<MeasurementModel>('/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();
|
|
}
|
|
}
|
|
|
|
// TODO: rework later
|
|
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.materialNames])]);
|
|
}
|
|
this.sampleForm.form.get('materialname').updateValueAndValidity();
|
|
}
|
|
|
|
handleMaterialNumbers() {
|
|
const fieldNo = this.material.numbers.length;
|
|
const filledFields = this.material.numbers.filter(e => e !== '').length;
|
|
// append new field
|
|
if (filledFields === fieldNo) {
|
|
this.material.numbers.push('');
|
|
}
|
|
// remove if two end fields are empty
|
|
if (fieldNo > 1 && this.material.numbers[fieldNo - 1] === '' && this.material.numbers[fieldNo - 2] === '') {
|
|
this.material.numbers.pop();
|
|
}
|
|
}
|
|
|
|
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[0]._id));
|
|
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) {
|
|
const fileReader = new FileReader();
|
|
fileReader.onload = () => {
|
|
this.sample.measurements[mIndex].values[parameter] = fileReader.result.toString().split('\r\n').map(e => e.split(','));
|
|
this.generateChart(this.sample.measurements[mIndex].values[parameter], mIndex);
|
|
};
|
|
fileReader.readAsText(files[0]);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
adjustCustomFields(value, index) {
|
|
this.customFields[index][0] = value;
|
|
const fieldNo = this.customFields.length;
|
|
let filledFields = 0;
|
|
this.customFields.forEach(field => {
|
|
if (field[0] !== '') {
|
|
filledFields ++;
|
|
}
|
|
});
|
|
// append new field
|
|
if (filledFields === fieldNo) {
|
|
this.customFields.push(['', '']);
|
|
}
|
|
// remove if two end fields are empty
|
|
if (fieldNo > 1 && this.customFields[fieldNo - 1][0] === '' && this.customFields[fieldNo - 2][0] === '') {
|
|
this.customFields.pop();
|
|
}
|
|
}
|
|
|
|
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.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^^
|