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 omit from 'lodash/omit'; 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'; import {LocalStorageService} from 'angular-2-local-storage'; import {Router} from '@angular/router'; // TODO: turn off sort field // TODO reset sort when field is excluded // TODO: material name to product // TODO: Eh DPT // TODO: filter button // TODO: check if connect-src to model works interface LoadSamplesOptions { toPage?: number; event?: Event; firstPage?: boolean; } interface KeyInterface { id: string; label: string; active: boolean; sortable: boolean; } @Component({ selector: 'app-samples', templateUrl: './samples.component.html', styleUrls: ['./samples.component.scss'] }) export class SamplesComponent implements OnInit { @ViewChild('pageSizeSelection') pageSizeSelection: ElementRef; @ViewChild('linkarea') linkarea: ElementRef; downloadSpectra = false; // TODO: streamline these options after csv option handling is clear downloadCondition = false; downloadFlatten = true; samples: SampleModel[] = []; totalSamples = 0; // total number of samples csvUrl = ''; // store url separate so it only has to be generated when clicking the download button filters = { status: {new: true, validated: true, deleted: false}, pageSize: 25, toPage: 0, sort: 'added-asc', filters: [ {field: 'number', label: 'Number', active: false, autocomplete: [], mode: 'eq', values: ['']}, {field: 'material.name', label: 'Material name', active: false, autocomplete: [], mode: 'eq', values: ['']}, {field: 'material.supplier', label: 'Supplier', active: false, autocomplete: [], mode: 'eq', values: ['']}, {field: 'material.group', label: 'Material', active: false, autocomplete: [], mode: 'eq', values: ['']}, {field: 'material.glass_fiber', label: 'GF', active: false, autocomplete: [], mode: 'eq', values: ['']}, {field: 'material.carbon_fiber', label: 'CF', active: false, autocomplete: [], mode: 'eq', values: ['']}, {field: 'material.mineral', label: 'M', active: false, autocomplete: [], mode: 'eq', values: ['']}, {field: 'type', label: 'Type', active: false, autocomplete: [], mode: 'eq', values: ['']}, {field: 'color', label: 'Color', active: false, autocomplete: [], mode: 'eq', values: ['']}, {field: 'batch', label: 'Batch', active: false, autocomplete: [], mode: 'eq', values: ['']}, {field: 'notes.comment', label: 'Comment', active: false, autocomplete: [], mode: 'eq', values: ['']}, {field: 'added', label: 'Added', active: false, autocomplete: [], mode: 'eq', values: ['']} ] }; page = 1; pages = 1; loadSamplesQueue = []; // arguments of queued up loadSamples() calls keys: KeyInterface[] = [ {id: 'number', label: 'Number', active: true, sortable: true}, {id: 'material.numbers', label: 'Material numbers', active: false, sortable: false}, {id: 'material.name', label: 'Material name', active: true, sortable: true}, {id: 'material.supplier', label: 'Supplier', active: false, sortable: true}, {id: 'material.group', label: 'Material', active: true, sortable: true}, {id: 'type', label: 'Type', active: true, sortable: true}, {id: 'color', label: 'Color', active: false, sortable: true}, {id: 'batch', label: 'Batch', active: true, sortable: true}, {id: 'notes.comment', label: 'Comment', active: false, sortable: false}, {id: 'notes', label: 'Notes', active: false, sortable: false}, {id: 'status', label: 'Status', active: false, sortable: true}, {id: 'added', label: 'Added', active: true, sortable: true} ]; isActiveKey: {[key: string]: boolean} = {}; activeKeys: KeyInterface[] = []; activeTemplateKeys = {material: [], condition: [], measurements: []}; sampleDetailsSample: any = null; sampleSelect = 0; // modes: 0 - no selection, 1 - sample edit selection, 2 - validation selection loading = 0; constructor( private api: ApiService, public autocomplete: AutocompleteService, public login: LoginService, private modalService: ModalService, public d: DataService, private storage: LocalStorageService, private window: Window, private router: Router ) { } ngOnInit(): void { this.loading = 8; const onLoad = () => { if ((--this.loading) <= 0) { this.loadSamples(); } }; this.calcFieldSelectKeys(); this.d.load('materials', () => { this.filters.filters.find(e => e.field === 'material.name').autocomplete = this.d.arr.materials.map(e => e.name); onLoad(); }); this.d.load('materialSuppliers', () => { this.filters.filters.find(e => e.field === 'material.supplier').autocomplete = this.d.arr.materialSuppliers; onLoad(); }); this.d.load('materialGroups', () => { this.filters.filters.find(e => e.field === 'material.group').autocomplete = this.d.arr.materialGroups; onLoad(); }); this.d.load('userKey', onLoad); this.d.load('conditionTemplates', onLoad); this.loadTemplateKeys('material', 'type', onLoad); this.loadTemplateKeys('condition', 'notes.comment', onLoad); this.loadTemplateKeys('measurement', 'status', onLoad); } loadTemplateKeys(collection, insertBefore, f) { this.d.load(collection + 'Templates', () => { const templateKeys = []; this.d.arr[collection + 'Templates'].forEach(item => { item.parameters.forEach(parameter => { const parameterName = encodeURIComponent(parameter.name); // exclude spectrum and duplicates if (parameter.name !== 'dpt' && !templateKeys.find(e => new RegExp('.' + parameterName + '$').test(e.id))) { const collectionNames = { material: 'material.properties', condition: 'condition', measurement: 'measurements.' + encodeURIComponent(item.name) }; templateKeys.push({ id: `${collectionNames[collection]}.${parameterName}`, label: `${this.ucFirst(item.name)} ${parameter.name}`, active: false, sortable: true }); this.filters.filters.push({ field: `${collectionNames[collection]}.${parameterName}`, label: `${this.ucFirst(item.name)} ${parameter.name}`, active: false, autocomplete: [], mode: 'eq', values: [''] }); } }); }); this.keys.splice(this.keys.findIndex(e => e.id === insertBefore), 0, ...templateKeys); this.keys = [...this.keys]; // complete overwrite array to invoke update in rb-multiselect this.loadPreferences(); f(); }); } loadSamples(options: LoadSamplesOptions = {}, event = null) { // set toPage to null to reload first page, queues calls if (event) { // adjust active keys this.keys.forEach(key => { if (event.hasOwnProperty(key.id)) { key.active = event[key.id]; } }); const sortId = this.filters.sort.replace(/(-asc|-desc)/, ''); if (event.hasOwnProperty(sortId) && !event[sortId]) { // reset sort if sort field was unselected this.setSort('_id-asc'); } this.updateActiveKeys(); } this.loadSamplesQueue.push(options); if (this.loadSamplesQueue.length <= 1) { // nothing queued up this.sampleLoader(this.loadSamplesQueue[0]); } this.storePreferences(); } private sampleLoader(options: LoadSamplesOptions) { // actual loading of the sample, do not call directly this.loading ++; this.api.get(this.sampleUrl({paging: true, pagingOptions: options}), (sData, err, headers) => { this.loading --; if (err) { this.storage.remove('samplesPreferences'); this.api.requestError(err); } else { if (!options.toPage && headers['x-total-items']) { this.totalSamples = headers['x-total-items']; } this.pages = Math.ceil(this.totalSamples / this.filters.pageSize); this.samples = sData as any; this.loadSamplesQueue.shift(); if (this.loadSamplesQueue.length > 0) { // execute next queue item this.sampleLoader(this.loadSamplesQueue[0]); } } }); } sampleUrl(options: { paging?: boolean, pagingOptions?: { firstPage?: boolean, toPage?: number, event?: Event }, csv?: boolean, export?: boolean, host?: boolean }) { // return url to fetch samples const additionalTableKeys = ['material_id', '_id', 'user_id']; // keys which should always be added if export = false const query: string[] = []; query.push(...Object.keys(this.filters.status).filter(e => this.filters.status[e]).map(e => 'status[]=' + e)); if (options.paging) { if (this.samples[0]) { // do not include from-id when page size was changed if (!options.pagingOptions.firstPage) { query.push('from-id=' + this.samples[0]._id); } else { this.page = 1; } } if (options.pagingOptions.toPage) { query.push('to-page=' + options.pagingOptions.toPage); } query.push('page-size=' + this.filters.pageSize); } query.push('sort=' + this.filters.sort); if (options.export) { query.push('key=' + this.d.d.userKey.key); } this.keys.forEach(key => { // do not load material properties for table if (key.active && (options.export || (!options.export && key.id.indexOf('material.') < 0))) { query.push('fields[]=' + key.id); } }); query.push(...cloneDeep(this.filters.filters) .map(e => { e.values = e.values.filter(el => el !== ''); // do not include empty values if (e.field === 'added') { // correct timezone e.values = e.values.map(el => new Date(new Date(el).getTime() - new Date(el).getTimezoneOffset() * 60000).toISOString()); } if (e.mode === 'null') { e.mode = 'in'; e.values = [null, '']; } return e; }) .filter(e => e.active && e.values.length > 0) .map(e => 'filters[]=' + encodeURIComponent(JSON.stringify(pick(e, ['mode', 'field', 'values'])))) ); if (!options.export) { additionalTableKeys.forEach(key => { if (query.indexOf('fields[]=' + key) < 0) { // add key if not already added query.push('fields[]=' + key); } }); } else { if (options.csv) { query.push('output=csv'); } else if (this.downloadFlatten) { query.push('output=flatten'); } if (this.downloadSpectra) { query.push('fields[]=measurements.spectrum.dpt'); } if (this.downloadCondition) { query.push('fields[]=condition'); } } return (options.host && isDevMode() ? this.window.location.host : '') + (options.export ? this.api.hostName : '') + '/samples?' + query.join('&'); } loadPage(delta) { if (!/[0-9]+/.test(delta) || this.page + delta < 1 || this.page + delta > this.pages) { // invalid delta return; } this.page += delta; this.loadSamples({toPage: delta}); } storePreferences() { const store = { filters: { ...pick(this.filters, ['status', 'pageSize', 'toPage', 'sort']), filters: this.filters.filters.map(e => pick(e, ['field', 'active', 'mode', 'values'])) }, keys: this.keys.map(e => pick(e, ['id', 'active'])) }; this.storage.set('samplesPreferences', store); } loadPreferences() { const store: any = this.storage.get('samplesPreferences'); if (store) { this.filters = {...this.filters, ...pick(store.filters, ['status', 'pageSize', 'toPage', 'sort'])}; store.filters.filters.forEach(filter => { const filterIndex = this.filters.filters.findIndex(e => e.field === filter.field); if (filterIndex >= 0) { this.filters.filters[filterIndex] = {...this.filters.filters[filterIndex], ...filter}; } }); store.keys.forEach(key => { const keyIndex = this.keys.findIndex(e => e.id === key.id); if (keyIndex >= 0) { this.keys[keyIndex].active = key.active; } }); } this.calcFieldSelectKeys(); this.updateActiveKeys(); } // TODO: avoid reloading resetPreferences() { this.storage.remove('samplesPreferences'); this.window.location.reload(); } updateFilterFields(field) { const filter = this.filters.filters.find(e => e.field === field); filter.active = !(filter.values.length === 1 && filter.values[0] === ''); } setSort(string) { this.filters.sort = string; this.loadSamples({firstPage: true}); } updateActiveKeys() { // array with all activeKeys this.activeKeys = this.keys.filter(e => e.active); this.filters.filters.forEach(filter => { // disable filters of fields not displayed if (!this.isActiveKey[filter.field]) { filter.active = false; } }); this.activeTemplateKeys.material = this.keys .filter(e => e.id.indexOf('material.properties.') >= 0 && e.active) .map(e => e.id.split('.') .map(el => decodeURIComponent(el))); this.activeTemplateKeys.condition = this.keys.filter(e => e.id.indexOf('condition.') >= 0 && e.active) .map(e => e.id.split('.') .map(el => decodeURIComponent(el))); this.activeTemplateKeys.measurements = this.keys.filter(e => e.id.indexOf('measurements.') >= 0 && e.active) .map(e => e.id.split('.') .map(el => decodeURIComponent(el))); } calcFieldSelectKeys() { this.keys.forEach(key => { this.isActiveKey[key.id] = key.active; }); } 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 = []; } if (Object.keys(data.condition).length) { this.sampleDetailsSample.condition_entries = Object.entries(omit(this.sampleDetailsSample.condition, ['condition_template'])) .map(e => { e[0] = `${this.ucFirst(this.d.id.conditionTemplates[this.sampleDetailsSample.condition.condition_template].name)} ${e[0]}`; return e; }); } else { this.sampleDetailsSample.condition_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) + ' ' + 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.sampleSelect) { this.samples.forEach(sample => { if (sample.selected) { this.api.put('/sample/validate/' + sample._id); } }); this.loadSamples(); this.sampleSelect = 0; } else { this.sampleSelect = 2; } } batchEdit() { if (this.sampleSelect) { this.router.navigate(['/samples/edit/' + this.samples.filter(e => e.selected).map(e => e._id).join(',')]); this.sampleSelect = 0; } else { this.sampleSelect = 1; } } restoreSample(id, modal, event) { this.stopPropagation(event); this.modalService.open(modal).then(res => { if (res) { this.api.put('/sample/restore/' + id, {}, ignore => { this.samples.find(e => e._id === id).status = 'new'; }); } }); } selectAll(event) { this.samples.forEach(sample => { if (sample.status !== 'deleted') { sample.selected = event.target.checked; } else { sample.selected = false; } }); } 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); document.execCommand('copy'); } ucFirst(string) { return string[0].toUpperCase() + string.slice(1); } }