bug button, data service, user level adjustments, multiple sample support for edit dialog, validation functionality

This commit is contained in:
VLE2FE
2020-08-06 08:18:57 +02:00
parent 72ecdad573
commit e8ad6aaa7a
25 changed files with 841 additions and 402 deletions

View File

@ -1,9 +1,15 @@
<script src="samples.component.ts"></script>
<div class="header-addnew">
<h2>Samples</h2>
<a routerLink="/samples/new">
<rb-icon-button icon="add" mode="primary">New sample</rb-icon-button>
<a routerLink="/samples/new" *ngIf="login.isLevel.write">
<rb-icon-button icon="add" mode="primary" class="space-left">New sample</rb-icon-button>
</a>
<rb-icon-button *ngIf="validation" mode="secondary" icon="close" (click)="validation = false" class="validation-close"
iconOnly></rb-icon-button>
<rb-icon-button *ngIf="login.isLevel.dev" [icon]="validation? 'checkmark' : 'clear-all'"
mode="secondary" (click)="validate()">
Validate
</rb-icon-button>
</div>
<rb-accordion>
@ -86,13 +92,15 @@
<ng-container *ngTemplateOutlet="paging"></ng-container>
<div class="download">
<rb-icon-button icon="download" mode="secondary" [rbModal]="linkModal">JSON download link</rb-icon-button>
<div class="download space-below" *ngIf="login.isLevel.dev">
<rb-icon-button class="space-right" icon="download" mode="secondary" [rbModal]="linkModal">
JSON download link
</rb-icon-button>
<ng-template #linkModal>
<label for="jsonUrl">URL for JSON download</label>
<textarea class="linkmodal" id="jsonUrl" #linkarea [value]="sampleUrl({export: true, host: true})"
(keydown)="preventDefault($event)"></textarea>
<rb-form-checkbox name="download-csv" [(ngModel)]="downloadCsv">
<rb-form-checkbox class="space-below" name="download-csv" [(ngModel)]="downloadCsv">
add spectra
</rb-form-checkbox>
<rb-icon-button icon="clipboard" mode="secondary" (click)="clipboard()">Copy to clipboard</rb-icon-button>
@ -104,8 +112,11 @@
</a>
</div>
<rb-table>
<rb-table class="samples-table">
<tr>
<th *ngIf="validation">
<rb-form-checkbox name="validate-all" (change)="selectAll($event)">all</rb-form-checkbox>
</th>
<th *ngFor="let key of activeKeys">
<div class="sort-header">
<span>{{key.label}}</span>
@ -119,17 +130,21 @@
</ng-container>
</div>
</th>
<th></th>
<th *ngIf="login.isLevel.write"></th>
</tr>
<tr *ngFor="let sample of samples">
<tr *ngFor="let sample of samples; index as i" class="clickable" (click)="sampleDetails(sample._id, sampleModal)">
<td *ngIf="validation">
<rb-form-checkbox [name]="'validate-' + i" (click)="stopPropagation($event)" [(ngModel)]="sample.validate">
</rb-form-checkbox>
</td>
<td *ngIf="isActiveKey['number']">{{sample.number}}</td>
<td *ngIf="isActiveKey['material.numbers']">{{materials[sample.material_id].numbers}}</td>
<td *ngIf="isActiveKey['material.name']">{{materials[sample.material_id].name}}</td>
<td *ngIf="isActiveKey['material.supplier']">{{materials[sample.material_id].supplier}}</td>
<td *ngIf="isActiveKey['material.group']">{{materials[sample.material_id].group}}</td>
<td *ngIf="isActiveKey['material.numbers']">{{d.id.materials[sample.material_id].numbers}}</td>
<td *ngIf="isActiveKey['material.name']">{{d.id.materials[sample.material_id].name}}</td>
<td *ngIf="isActiveKey['material.supplier']">{{d.id.materials[sample.material_id].supplier}}</td>
<td *ngIf="isActiveKey['material.group']">{{d.id.materials[sample.material_id].group}}</td>
<td *ngFor="let key of activeTemplateKeys.material">
{{materials[sample.material_id].properties[key[2]] | exists}}
{{d.id.materials[sample.material_id].properties[key[2]] | exists}}
</td>
<td *ngIf="isActiveKey['type']">{{sample.type}}</td>
<td *ngIf="isActiveKey['color']">{{sample.color}}</td>
@ -137,7 +152,12 @@
<td *ngIf="isActiveKey['notes']">{{sample.notes | object: ['_id', 'sample_references']}}</td>
<td *ngFor="let key of activeTemplateKeys.measurements">{{sample[key[1]] | exists: key[2]}}</td>
<td *ngIf="isActiveKey['added']">{{sample.added | date:'dd/MM/yy'}}</td>
<td><a [routerLink]="'/samples/edit/' + sample._id"><span class="rb-ic rb-ic-edit"></span></a></td>
<td *ngIf="login.isLevel.write">
<a [routerLink]="'/samples/edit/' + sample._id"
*ngIf="login.isLevel.dev || (login.isLevel.write && sample.user_id === login.userId)">
<span class="rb-ic rb-ic-edit"></span>
</a>
</td>
</tr>
</rb-table>
@ -159,3 +179,35 @@
</div>
</ng-template>
<ng-template #sampleModal>
<rb-loading-spinner *ngIf="sampleDetailsSample === null; else sampleDetailsTemplate"></rb-loading-spinner>
<ng-template #sampleDetailsTemplate>
<h3>{{sampleDetailsSample.number}}</h3>
<rb-table class="sample-details-table">
<tr><th>Material</th><td>{{sampleDetailsSample.material.name}}</td></tr>
<tr><th>Supplier</th><td>{{sampleDetailsSample.material.supplier}}</td></tr>
<tr><th>Group</th><td>{{sampleDetailsSample.material.group}}</td></tr>
<tr><th>Type</th><td>{{sampleDetailsSample.type}}</td></tr>
<tr><th>color</th><td>{{sampleDetailsSample.color}}</td></tr>
<tr><th>Batch</th><td>{{sampleDetailsSample.batch}}</td></tr>
<tr><th>Comment</th><td>{{sampleDetailsSample.notes.comment | exists}}</td></tr>
<tr *ngFor="let customField of sampleDetailsSample.notes.custom_fields_entries">
<th>{{customField[0]}}</th>
<td>{{customField[0]}}</td>
</tr>
<tr *ngFor="let reference of sampleDetailsSample.notes.sample_references">
<th>{{reference.relation}}</th>
<td>
<button class="rb-btn rb-link" (click)="sampleDetails(reference.sample_id, sampleModal)">
{{reference.number}}
</button>
</td>
</tr>
<tr *ngFor="let measurement of sampleDetailsSample.measurement_entries">
<th>{{measurement.name}}</th>
<td>{{measurement.value}}</td>
</tr>
<tr><th>User</th><td>{{sampleDetailsSample.user}}</td></tr>
</rb-table>
</ng-template>
</ng-template>

View File

@ -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;
}

View File

@ -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<HTMLTextAreaElement>;
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<string[]>('/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<string[]>('/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<any>) {
this.sampleDetailsSample = null;
this.api.get<SampleModel>('/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<SampleModel>('/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);