Properly indent all source files
This commit is contained in:
		@@ -13,43 +13,43 @@ import {DocumentationDatabaseComponent} from './documentation/documentation-data
 | 
			
		||||
import {PredictionComponent} from './prediction/prediction.component';
 | 
			
		||||
import {ModelTemplatesComponent} from './model-templates/model-templates.component';
 | 
			
		||||
import {DocumentationArchitectureComponent} from
 | 
			
		||||
    './documentation/documentation-architecture/documentation-architecture.component';
 | 
			
		||||
'./documentation/documentation-architecture/documentation-architecture.component';
 | 
			
		||||
import {MaterialsComponent} from './materials/materials.component';
 | 
			
		||||
import {MaterialComponent} from './material/material.component';
 | 
			
		||||
import {DocumentationModelsComponent} from './documentation/documentation-models/documentation-models.component';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
  {path: '', component: HomeComponent},
 | 
			
		||||
  {path: 'home', component: HomeComponent},
 | 
			
		||||
  {path: 'prediction', component: PredictionComponent},
 | 
			
		||||
  {path: 'models', component: ModelTemplatesComponent},
 | 
			
		||||
  {path: 'samples', component: SamplesComponent, canActivate: [LoginService]},
 | 
			
		||||
  {path: 'samples/new', component: SampleComponent, canActivate: [LoginService]},
 | 
			
		||||
  {path: 'samples/edit/:id', component: SampleComponent, canActivate: [LoginService]},
 | 
			
		||||
  {path: 'materials', component: MaterialsComponent, canActivate: [LoginService]},
 | 
			
		||||
  {path: 'materials/edit/:id', component: MaterialComponent, canActivate: [LoginService]},
 | 
			
		||||
  {path: 'templates', component: TemplatesComponent, canActivate: [LoginService]},
 | 
			
		||||
  {path: 'changelog', component: ChangelogComponent, canActivate: [LoginService]},
 | 
			
		||||
  {path: 'users', component: UsersComponent, canActivate: [LoginService]},
 | 
			
		||||
  {path: 'settings', component: SettingsComponent, canActivate: [LoginService]},
 | 
			
		||||
  {path: 'documentation', component: DocumentationComponent},
 | 
			
		||||
  {path: 'documentation/architecture', component: DocumentationArchitectureComponent},
 | 
			
		||||
  {path: 'documentation/database', component: DocumentationDatabaseComponent},
 | 
			
		||||
  {path: 'documentation/models', component: DocumentationModelsComponent},
 | 
			
		||||
	{path: '', component: HomeComponent},
 | 
			
		||||
	{path: 'home', component: HomeComponent},
 | 
			
		||||
	{path: 'prediction', component: PredictionComponent},
 | 
			
		||||
	{path: 'models', component: ModelTemplatesComponent},
 | 
			
		||||
	{path: 'samples', component: SamplesComponent, canActivate: [LoginService]},
 | 
			
		||||
	{path: 'samples/new', component: SampleComponent, canActivate: [LoginService]},
 | 
			
		||||
	{path: 'samples/edit/:id', component: SampleComponent, canActivate: [LoginService]},
 | 
			
		||||
	{path: 'materials', component: MaterialsComponent, canActivate: [LoginService]},
 | 
			
		||||
	{path: 'materials/edit/:id', component: MaterialComponent, canActivate: [LoginService]},
 | 
			
		||||
	{path: 'templates', component: TemplatesComponent, canActivate: [LoginService]},
 | 
			
		||||
	{path: 'changelog', component: ChangelogComponent, canActivate: [LoginService]},
 | 
			
		||||
	{path: 'users', component: UsersComponent, canActivate: [LoginService]},
 | 
			
		||||
	{path: 'settings', component: SettingsComponent, canActivate: [LoginService]},
 | 
			
		||||
	{path: 'documentation', component: DocumentationComponent},
 | 
			
		||||
	{path: 'documentation/architecture', component: DocumentationArchitectureComponent},
 | 
			
		||||
	{path: 'documentation/database', component: DocumentationDatabaseComponent},
 | 
			
		||||
	{path: 'documentation/models', component: DocumentationModelsComponent},
 | 
			
		||||
 | 
			
		||||
  // If not authenticated
 | 
			
		||||
  { path: '**', redirectTo: '' }
 | 
			
		||||
	// If not authenticated
 | 
			
		||||
	{ path: '**', redirectTo: '' }
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const routerOptions: ExtraOptions = {
 | 
			
		||||
  scrollPositionRestoration: 'enabled',
 | 
			
		||||
  anchorScrolling: 'enabled',
 | 
			
		||||
  scrollOffset: [0, 64],
 | 
			
		||||
	scrollPositionRestoration: 'enabled',
 | 
			
		||||
	anchorScrolling: 'enabled',
 | 
			
		||||
	scrollOffset: [0, 64],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  imports: [RouterModule.forRoot(routes, routerOptions)],
 | 
			
		||||
  exports: [RouterModule]
 | 
			
		||||
	imports: [RouterModule.forRoot(routes, routerOptions)],
 | 
			
		||||
	exports: [RouterModule]
 | 
			
		||||
})
 | 
			
		||||
export class AppRoutingModule { }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,73 +1,73 @@
 | 
			
		||||
<rb-full-header id="top">
 | 
			
		||||
  <nav *rbMainNavItems>
 | 
			
		||||
    <a routerLink="/home" routerLinkActive="active" rbLoadingLink>Home</a>
 | 
			
		||||
    <a routerLink="/prediction" routerLinkActive="active" rbLoadingLink *ngIf="login.hasPrediction">Prediction</a>
 | 
			
		||||
    <a routerLink="/models" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.dev">Models</a>
 | 
			
		||||
    <a routerLink="/samples" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.read">Samples</a>
 | 
			
		||||
    <a routerLink="/materials" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.dev">Materials</a>
 | 
			
		||||
    <a routerLink="/templates" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.dev">
 | 
			
		||||
      Templates
 | 
			
		||||
    </a>
 | 
			
		||||
    <a routerLink="/changelog" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.dev">Changelog</a>
 | 
			
		||||
    <a routerLink="/users" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.admin">Users</a>
 | 
			
		||||
    <a routerLink="/documentation" routerLinkActive="active" rbLoadingLink>Documentation</a>
 | 
			
		||||
  </nav>
 | 
			
		||||
	<nav *rbMainNavItems>
 | 
			
		||||
		<a routerLink="/home" routerLinkActive="active" rbLoadingLink>Home</a>
 | 
			
		||||
		<a routerLink="/prediction" routerLinkActive="active" rbLoadingLink *ngIf="login.hasPrediction">Prediction</a>
 | 
			
		||||
		<a routerLink="/models" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.dev">Models</a>
 | 
			
		||||
		<a routerLink="/samples" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.read">Samples</a>
 | 
			
		||||
		<a routerLink="/materials" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.dev">Materials</a>
 | 
			
		||||
		<a routerLink="/templates" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.dev">
 | 
			
		||||
			Templates
 | 
			
		||||
		</a>
 | 
			
		||||
		<a routerLink="/changelog" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.dev">Changelog</a>
 | 
			
		||||
		<a routerLink="/users" routerLinkActive="active" rbLoadingLink *ngIf="login.isLevel.admin">Users</a>
 | 
			
		||||
		<a routerLink="/documentation" routerLinkActive="active" rbLoadingLink>Documentation</a>
 | 
			
		||||
	</nav>
 | 
			
		||||
 | 
			
		||||
  <ng-container *ngIf="isDocumentation">
 | 
			
		||||
    <nav *rbSubNavItems>
 | 
			
		||||
      <a routerLink="/documentation" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">General</a>
 | 
			
		||||
      <a routerLink="/documentation/architecture" routerLinkActive="active">Architecture</a>
 | 
			
		||||
      <a routerLink="/documentation/database" routerLinkActive="active">Database</a>
 | 
			
		||||
      <a routerLink="/documentation/models" routerLinkActive="active">Models</a>
 | 
			
		||||
    </nav>
 | 
			
		||||
  </ng-container>
 | 
			
		||||
	<ng-container *ngIf="isDocumentation">
 | 
			
		||||
		<nav *rbSubNavItems>
 | 
			
		||||
			<a routerLink="/documentation" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">General</a>
 | 
			
		||||
			<a routerLink="/documentation/architecture" routerLinkActive="active">Architecture</a>
 | 
			
		||||
			<a routerLink="/documentation/database" routerLinkActive="active">Database</a>
 | 
			
		||||
			<a routerLink="/documentation/models" routerLinkActive="active">Models</a>
 | 
			
		||||
		</nav>
 | 
			
		||||
	</ng-container>
 | 
			
		||||
 | 
			
		||||
  <nav *rbMetaNavItems>
 | 
			
		||||
    <span class="rb-ic rb-ic-question-frame clickable space-above" (click)="help()"></span>
 | 
			
		||||
  </nav>
 | 
			
		||||
	<nav *rbMetaNavItems>
 | 
			
		||||
		<span class="rb-ic rb-ic-question-frame clickable space-above" (click)="help()"></span>
 | 
			
		||||
	</nav>
 | 
			
		||||
 | 
			
		||||
  <ng-container *ngIf="login.isLoggedIn">
 | 
			
		||||
    <nav *rbActionNavItems>
 | 
			
		||||
      <a href="javascript:"  [rbPopover]="userPopover" [anchor]="popoverAnchor">
 | 
			
		||||
        {{login.username}} <span class="rb-ic rb-ic-my-brand-frame" #popoverAnchor></span></a>
 | 
			
		||||
    </nav>
 | 
			
		||||
    <ng-template #userPopover let-close="close">
 | 
			
		||||
      <div class="spacing">
 | 
			
		||||
        <a routerLink="/settings" (click)="close()"><span class="rb-ic rb-ic-settings"></span>  Settings</a>
 | 
			
		||||
        <button type="button" class="rb-btn rb-primary" (click)="logout()">Logout</button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </ng-template>
 | 
			
		||||
  </ng-container>
 | 
			
		||||
	<ng-container *ngIf="login.isLoggedIn">
 | 
			
		||||
		<nav *rbActionNavItems>
 | 
			
		||||
			<a href="javascript:"  [rbPopover]="userPopover" [anchor]="popoverAnchor">
 | 
			
		||||
				{{login.username}} <span class="rb-ic rb-ic-my-brand-frame" #popoverAnchor></span></a>
 | 
			
		||||
		</nav>
 | 
			
		||||
		<ng-template #userPopover let-close="close">
 | 
			
		||||
			<div class="spacing">
 | 
			
		||||
				<a routerLink="/settings" (click)="close()"><span class="rb-ic rb-ic-settings"></span>  Settings</a>
 | 
			
		||||
				<button type="button" class="rb-btn rb-primary" (click)="logout()">Logout</button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</ng-template>
 | 
			
		||||
	</ng-container>
 | 
			
		||||
 | 
			
		||||
  <div *rbSubBrandHeader>
 | 
			
		||||
    <rb-icon-button icon="bug" mode="secondary" class="space-right" [rbModal]="bugModal">
 | 
			
		||||
      Bug
 | 
			
		||||
    </rb-icon-button>
 | 
			
		||||
    <ng-template let-close="close" #bugModal>
 | 
			
		||||
      <h3>Report a bug</h3>
 | 
			
		||||
      <rb-form-textarea class="bug-textarea" label="What did you do?" [(ngModel)]="bugReport.do"></rb-form-textarea>
 | 
			
		||||
      <rb-form-textarea class="bug-textarea" label="What did not work?" [(ngModel)]="bugReport.work"></rb-form-textarea>
 | 
			
		||||
      <a [href]="bugReportContent()">
 | 
			
		||||
        <rb-icon-button icon="mail" mode="primary" (mouseup)="closeBugReport(close)">Send report</rb-icon-button>
 | 
			
		||||
      </a>
 | 
			
		||||
    </ng-template>
 | 
			
		||||
    <span class="dev-label" *ngIf="devMode">DEVELOPMENT</span>
 | 
			
		||||
    DeFinMa
 | 
			
		||||
  </div>
 | 
			
		||||
	<div *rbSubBrandHeader>
 | 
			
		||||
		<rb-icon-button icon="bug" mode="secondary" class="space-right" [rbModal]="bugModal">
 | 
			
		||||
			Bug
 | 
			
		||||
		</rb-icon-button>
 | 
			
		||||
		<ng-template let-close="close" #bugModal>
 | 
			
		||||
			<h3>Report a bug</h3>
 | 
			
		||||
			<rb-form-textarea class="bug-textarea" label="What did you do?" [(ngModel)]="bugReport.do"></rb-form-textarea>
 | 
			
		||||
			<rb-form-textarea class="bug-textarea" label="What did not work?" [(ngModel)]="bugReport.work"></rb-form-textarea>
 | 
			
		||||
			<a [href]="bugReportContent()">
 | 
			
		||||
				<rb-icon-button icon="mail" mode="primary" (mouseup)="closeBugReport(close)">Send report</rb-icon-button>
 | 
			
		||||
			</a>
 | 
			
		||||
		</ng-template>
 | 
			
		||||
		<span class="dev-label" *ngIf="devMode">DEVELOPMENT</span>
 | 
			
		||||
		DeFinMa
 | 
			
		||||
	</div>
 | 
			
		||||
</rb-full-header>
 | 
			
		||||
 | 
			
		||||
<div class="container">
 | 
			
		||||
  <router-outlet></router-outlet>
 | 
			
		||||
	<router-outlet></router-outlet>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<rb-icon-button icon="up" mode="primary" iconOnly (click)="toTheTop()" *ngIf="window.scrollY > 300" class="to-the-top">
 | 
			
		||||
</rb-icon-button>
 | 
			
		||||
 | 
			
		||||
<rb-footer-nav>
 | 
			
		||||
  <span class="copyright">
 | 
			
		||||
    CR/APS1 and CR/ANA1 2020
 | 
			
		||||
  </span>
 | 
			
		||||
  <nav>
 | 
			
		||||
    <a [href]="'mailto:' + d.contact.mail">Contact</a>
 | 
			
		||||
  </nav>
 | 
			
		||||
	<span class="copyright">
 | 
			
		||||
		CR/APS1 and CR/ANA1 2020
 | 
			
		||||
	</span>
 | 
			
		||||
	<nav>
 | 
			
		||||
		<a [href]="'mailto:' + d.contact.mail">Contact</a>
 | 
			
		||||
	</nav>
 | 
			
		||||
</rb-footer-nav>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,26 +1,26 @@
 | 
			
		||||
.dev-label {
 | 
			
		||||
  color: #F00;
 | 
			
		||||
  font-size: 32px;
 | 
			
		||||
  margin-right: 40px;
 | 
			
		||||
	color: #F00;
 | 
			
		||||
	font-size: 32px;
 | 
			
		||||
	margin-right: 40px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.spacing {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: 1fr;
 | 
			
		||||
  grid-row-gap: 10px;
 | 
			
		||||
	display: grid;
 | 
			
		||||
	grid-template-columns: 1fr;
 | 
			
		||||
	grid-row-gap: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.bug-textarea {
 | 
			
		||||
  width: 800px;
 | 
			
		||||
	width: 800px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.container {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
	position: relative;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.to-the-top {
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  right: 1rem;
 | 
			
		||||
  bottom: 20px;
 | 
			
		||||
	position: fixed;
 | 
			
		||||
	right: 1rem;
 | 
			
		||||
	bottom: 20px;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,76 +8,76 @@ import {DataService} from './services/data.service';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-root',
 | 
			
		||||
  templateUrl: './app.component.html',
 | 
			
		||||
  styleUrls: ['./app.component.scss']
 | 
			
		||||
	selector: 'app-root',
 | 
			
		||||
	templateUrl: './app.component.html',
 | 
			
		||||
	styleUrls: ['./app.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class AppComponent implements OnInit{
 | 
			
		||||
 | 
			
		||||
  bugReport = {do: '', work: ''};  // Data from bug report inputs
 | 
			
		||||
  isDocumentation = false;         // True if user is on documentation pages
 | 
			
		||||
  devMode = false;
 | 
			
		||||
	bugReport = {do: '', work: ''};  // Data from bug report inputs
 | 
			
		||||
	isDocumentation = false;         // True if user is on documentation pages
 | 
			
		||||
		devMode = false;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    public login: LoginService,
 | 
			
		||||
    public router: Router,
 | 
			
		||||
    private route: ActivatedRoute,
 | 
			
		||||
    public window: Window,
 | 
			
		||||
    private modal: ModalService,
 | 
			
		||||
    public d: DataService
 | 
			
		||||
  ) {
 | 
			
		||||
    this.devMode = isDevMode();
 | 
			
		||||
    this.router.events.subscribe(event => {
 | 
			
		||||
      if (event instanceof NavigationStart) {
 | 
			
		||||
        this.isDocumentation = /\/documentation/.test(event.url);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
	constructor(
 | 
			
		||||
		public login: LoginService,
 | 
			
		||||
		public router: Router,
 | 
			
		||||
		private route: ActivatedRoute,
 | 
			
		||||
		public window: Window,
 | 
			
		||||
		private modal: ModalService,
 | 
			
		||||
		public d: DataService
 | 
			
		||||
	) {
 | 
			
		||||
		this.devMode = isDevMode();
 | 
			
		||||
		this.router.events.subscribe(event => {
 | 
			
		||||
			if (event instanceof NavigationStart) {
 | 
			
		||||
				this.isDocumentation = /\/documentation/.test(event.url);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    // Try to log in user
 | 
			
		||||
    this.login.login().then(res => {
 | 
			
		||||
      // Return to home page if log failed, except when on documentation pages
 | 
			
		||||
      if (!res && !(/\/documentation/.test(this.router.url))) {
 | 
			
		||||
        this.router.navigate(['/']);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
	ngOnInit() {
 | 
			
		||||
		// Try to log in user
 | 
			
		||||
		this.login.login().then(res => {
 | 
			
		||||
			// Return to home page if log failed, except when on documentation pages
 | 
			
		||||
			if (!res && !(/\/documentation/.test(this.router.url))) {
 | 
			
		||||
				this.router.navigate(['/']);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  logout() {
 | 
			
		||||
    this.login.logout();
 | 
			
		||||
    this.router.navigate(['/']);
 | 
			
		||||
  }
 | 
			
		||||
	logout() {
 | 
			
		||||
		this.login.logout();
 | 
			
		||||
		this.router.navigate(['/']);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  help() {
 | 
			
		||||
    this.modal.openComponent(HelpComponent);
 | 
			
		||||
  }
 | 
			
		||||
	help() {
 | 
			
		||||
		this.modal.openComponent(HelpComponent);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  bugReportContent() {
 | 
			
		||||
    return `mailto:${this.d.contact.mail}?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: ${this.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: ${this.window.innerWidth}
 | 
			
		||||
%0D%0AinnerHeight: ${this.window.innerHeight}`;
 | 
			
		||||
  }
 | 
			
		||||
	bugReportContent() {
 | 
			
		||||
		return `mailto:${this.d.contact.mail}?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: ${this.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: ${this.window.innerWidth}
 | 
			
		||||
		%0D%0AinnerHeight: ${this.window.innerHeight}`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  closeBugReport(close) {
 | 
			
		||||
    setTimeout(() => close(), 1);
 | 
			
		||||
  }
 | 
			
		||||
	closeBugReport(close) {
 | 
			
		||||
		setTimeout(() => close(), 1);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  toTheTop() {
 | 
			
		||||
    this.window.scroll(0, 0);
 | 
			
		||||
  }
 | 
			
		||||
	toTheTop() {
 | 
			
		||||
		this.window.scroll(0, 0);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -27,66 +27,66 @@ import { SettingsComponent } from './settings/settings.component';
 | 
			
		||||
import { UsersComponent } from './users/users.component';
 | 
			
		||||
import { ChangelogComponent } from './changelog/changelog.component';
 | 
			
		||||
import { DocumentationDatabaseComponent } from
 | 
			
		||||
    './documentation/documentation-database/documentation-database.component';
 | 
			
		||||
'./documentation/documentation-database/documentation-database.component';
 | 
			
		||||
import { PredictionComponent } from './prediction/prediction.component';
 | 
			
		||||
import { HelpComponent } from './help/help.component';
 | 
			
		||||
import { ModelTemplatesComponent } from './model-templates/model-templates.component';
 | 
			
		||||
import { SizePipe } from './size.pipe';
 | 
			
		||||
import { DocumentationArchitectureComponent } from
 | 
			
		||||
    './documentation/documentation-architecture/documentation-architecture.component';
 | 
			
		||||
'./documentation/documentation-architecture/documentation-architecture.component';
 | 
			
		||||
import { MaterialsComponent } from './materials/materials.component';
 | 
			
		||||
import { MaterialComponent } from './material/material.component';
 | 
			
		||||
import { DocumentationModelsComponent } from './documentation/documentation-models/documentation-models.component';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations: [
 | 
			
		||||
    AppComponent,
 | 
			
		||||
    LoginComponent,
 | 
			
		||||
    HomeComponent,
 | 
			
		||||
    SamplesComponent,
 | 
			
		||||
    SampleComponent,
 | 
			
		||||
    ValidateDirective,
 | 
			
		||||
    ErrorComponent,
 | 
			
		||||
    ObjectPipe,
 | 
			
		||||
    DocumentationComponent,
 | 
			
		||||
    ImgMagnifierComponent,
 | 
			
		||||
    ExistsPipe,
 | 
			
		||||
    TemplatesComponent,
 | 
			
		||||
    ParametersPipe,
 | 
			
		||||
    SettingsComponent,
 | 
			
		||||
    UsersComponent,
 | 
			
		||||
    ChangelogComponent,
 | 
			
		||||
    DocumentationDatabaseComponent,
 | 
			
		||||
    PredictionComponent,
 | 
			
		||||
    HelpComponent,
 | 
			
		||||
    ModelTemplatesComponent,
 | 
			
		||||
    SizePipe,
 | 
			
		||||
    DocumentationArchitectureComponent,
 | 
			
		||||
    MaterialsComponent,
 | 
			
		||||
    MaterialComponent,
 | 
			
		||||
    DocumentationModelsComponent
 | 
			
		||||
  ],
 | 
			
		||||
  imports: [
 | 
			
		||||
    LocalStorageModule.forRoot({
 | 
			
		||||
      prefix: 'definma',
 | 
			
		||||
      storageType: 'localStorage'
 | 
			
		||||
    }),
 | 
			
		||||
    BrowserModule,
 | 
			
		||||
    BrowserAnimationsModule,
 | 
			
		||||
    AppRoutingModule,
 | 
			
		||||
    RbUiComponentsModule,
 | 
			
		||||
    FormsModule,
 | 
			
		||||
    HttpClientModule,
 | 
			
		||||
    RbCustomInputsModule,
 | 
			
		||||
    ReactiveFormsModule,
 | 
			
		||||
    FormFieldsModule,
 | 
			
		||||
    CommonModule,
 | 
			
		||||
    ChartsModule
 | 
			
		||||
  ],
 | 
			
		||||
  providers: [
 | 
			
		||||
    ModalService,
 | 
			
		||||
    { provide: Window, useValue: window }
 | 
			
		||||
  ],
 | 
			
		||||
  bootstrap: [AppComponent]
 | 
			
		||||
	declarations: [
 | 
			
		||||
		AppComponent,
 | 
			
		||||
		LoginComponent,
 | 
			
		||||
		HomeComponent,
 | 
			
		||||
		SamplesComponent,
 | 
			
		||||
		SampleComponent,
 | 
			
		||||
		ValidateDirective,
 | 
			
		||||
		ErrorComponent,
 | 
			
		||||
		ObjectPipe,
 | 
			
		||||
		DocumentationComponent,
 | 
			
		||||
		ImgMagnifierComponent,
 | 
			
		||||
		ExistsPipe,
 | 
			
		||||
		TemplatesComponent,
 | 
			
		||||
		ParametersPipe,
 | 
			
		||||
		SettingsComponent,
 | 
			
		||||
		UsersComponent,
 | 
			
		||||
		ChangelogComponent,
 | 
			
		||||
		DocumentationDatabaseComponent,
 | 
			
		||||
		PredictionComponent,
 | 
			
		||||
		HelpComponent,
 | 
			
		||||
		ModelTemplatesComponent,
 | 
			
		||||
		SizePipe,
 | 
			
		||||
		DocumentationArchitectureComponent,
 | 
			
		||||
		MaterialsComponent,
 | 
			
		||||
		MaterialComponent,
 | 
			
		||||
		DocumentationModelsComponent
 | 
			
		||||
	],
 | 
			
		||||
	imports: [
 | 
			
		||||
		LocalStorageModule.forRoot({
 | 
			
		||||
			prefix: 'definma',
 | 
			
		||||
			storageType: 'localStorage'
 | 
			
		||||
		}),
 | 
			
		||||
		BrowserModule,
 | 
			
		||||
		BrowserAnimationsModule,
 | 
			
		||||
		AppRoutingModule,
 | 
			
		||||
		RbUiComponentsModule,
 | 
			
		||||
		FormsModule,
 | 
			
		||||
		HttpClientModule,
 | 
			
		||||
		RbCustomInputsModule,
 | 
			
		||||
		ReactiveFormsModule,
 | 
			
		||||
		FormFieldsModule,
 | 
			
		||||
		CommonModule,
 | 
			
		||||
		ChartsModule
 | 
			
		||||
	],
 | 
			
		||||
	providers: [
 | 
			
		||||
		ModalService,
 | 
			
		||||
		{ provide: Window, useValue: window }
 | 
			
		||||
	],
 | 
			
		||||
	bootstrap: [AppComponent]
 | 
			
		||||
})
 | 
			
		||||
export class AppModule { }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,46 +1,46 @@
 | 
			
		||||
<div class="header">
 | 
			
		||||
  <rb-form-date-input name="dateInput" label="older than" [options]="{enableTime: true}"
 | 
			
		||||
                      [(ngModel)]="timestamp" (ngModelChange)="loadChangelog()">
 | 
			
		||||
  </rb-form-date-input>
 | 
			
		||||
<rb-form-date-input name="dateInput" label="older than" [options]="{enableTime: true}"
 | 
			
		||||
[(ngModel)]="timestamp" (ngModelChange)="loadChangelog()">
 | 
			
		||||
</rb-form-date-input>
 | 
			
		||||
 | 
			
		||||
  <rb-form-select name="pageSizeSelection" label="page size" [(ngModel)]="pageSize" (ngModelChange)="loadChangelog()">
 | 
			
		||||
    <option value="3">3</option>
 | 
			
		||||
    <option value="10">10</option>
 | 
			
		||||
    <option value="25">25</option>
 | 
			
		||||
    <option value="50">50</option>
 | 
			
		||||
    <option value="100">100</option>
 | 
			
		||||
    <option value="250">250</option>
 | 
			
		||||
    <option value="500">500</option>
 | 
			
		||||
  </rb-form-select>
 | 
			
		||||
<rb-form-select name="pageSizeSelection" label="page size" [(ngModel)]="pageSize" (ngModelChange)="loadChangelog()">
 | 
			
		||||
<option value="3">3</option>
 | 
			
		||||
<option value="10">10</option>
 | 
			
		||||
<option value="25">25</option>
 | 
			
		||||
<option value="50">50</option>
 | 
			
		||||
<option value="100">100</option>
 | 
			
		||||
<option value="250">250</option>
 | 
			
		||||
<option value="500">500</option>
 | 
			
		||||
</rb-form-select>
 | 
			
		||||
 | 
			
		||||
  <button class="rb-btn rb-link" type="button" (click)="loadChangelog(1)">
 | 
			
		||||
    next page
 | 
			
		||||
    <span class="rb-ic  rb-ic-forward-right"></span>
 | 
			
		||||
  </button>
 | 
			
		||||
<button class="rb-btn rb-link" type="button" (click)="loadChangelog(1)">
 | 
			
		||||
next page
 | 
			
		||||
<span class="rb-ic  rb-ic-forward-right"></span>
 | 
			
		||||
</button>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<rb-table ellipsis>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <th>Date</th>
 | 
			
		||||
    <th>Action</th>
 | 
			
		||||
    <th>Data</th>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr *ngFor="let entry of changelog; index as i" class="clickable" (click)="showDetails(i, sampleModal)">
 | 
			
		||||
    <td>{{entry.date}}</td>
 | 
			
		||||
    <td>{{entry.action}}</td>
 | 
			
		||||
    <td>{{entry.data | json}}</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
<tr>
 | 
			
		||||
<th>Date</th>
 | 
			
		||||
<th>Action</th>
 | 
			
		||||
<th>Data</th>
 | 
			
		||||
</tr>
 | 
			
		||||
<tr *ngFor="let entry of changelog; index as i" class="clickable" (click)="showDetails(i, sampleModal)">
 | 
			
		||||
<td>{{entry.date}}</td>
 | 
			
		||||
<td>{{entry.action}}</td>
 | 
			
		||||
<td>{{entry.data | json}}</td>
 | 
			
		||||
</tr>
 | 
			
		||||
</rb-table>
 | 
			
		||||
 | 
			
		||||
<ng-template #sampleModal>
 | 
			
		||||
  <h4>Date</h4>
 | 
			
		||||
  <p class="space-below">{{changelog[modalDetail].date}}</p>
 | 
			
		||||
  <h4>Action</h4>
 | 
			
		||||
  <p class="space-below">{{changelog[modalDetail].action}}</p>
 | 
			
		||||
  <h4>Collection</h4>
 | 
			
		||||
  <p class="space-below">{{changelog[modalDetail].collection}}</p>
 | 
			
		||||
  <h4>Conditions</h4>
 | 
			
		||||
  <p class="space-below">{{changelog[modalDetail].conditions | json}}</p>
 | 
			
		||||
  <h4>Data</h4>
 | 
			
		||||
  <p class="space-below">{{changelog[modalDetail].data | json}}</p>
 | 
			
		||||
<h4>Date</h4>
 | 
			
		||||
<p class="space-below">{{changelog[modalDetail].date}}</p>
 | 
			
		||||
<h4>Action</h4>
 | 
			
		||||
<p class="space-below">{{changelog[modalDetail].action}}</p>
 | 
			
		||||
<h4>Collection</h4>
 | 
			
		||||
<p class="space-below">{{changelog[modalDetail].collection}}</p>
 | 
			
		||||
<h4>Conditions</h4>
 | 
			
		||||
<p class="space-below">{{changelog[modalDetail].conditions | json}}</p>
 | 
			
		||||
<h4>Data</h4>
 | 
			
		||||
<p class="space-below">{{changelog[modalDetail].data | json}}</p>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,20 +2,20 @@
 | 
			
		||||
 | 
			
		||||
.header {
 | 
			
		||||
 | 
			
		||||
  & > * {
 | 
			
		||||
    float: left;
 | 
			
		||||
  }
 | 
			
		||||
	& > * {
 | 
			
		||||
		float: left;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  button {
 | 
			
		||||
    float: right;
 | 
			
		||||
  }
 | 
			
		||||
	button {
 | 
			
		||||
		float: right;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tr.clickable {
 | 
			
		||||
  background: none;
 | 
			
		||||
  transition: background-color 0.5s;
 | 
			
		||||
	background: none;
 | 
			
		||||
	transition: background-color 0.5s;
 | 
			
		||||
 | 
			
		||||
  &:hover {
 | 
			
		||||
    background: $color-gray-mercury;
 | 
			
		||||
  }
 | 
			
		||||
	&:hover {
 | 
			
		||||
		background: $color-gray-mercury;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,44 +4,44 @@ import {ApiService} from '../services/api.service';
 | 
			
		||||
import {ModalService} from '@inst-iot/bosch-angular-ui-components';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-changelog',
 | 
			
		||||
  templateUrl: './changelog.component.html',
 | 
			
		||||
  styleUrls: ['./changelog.component.scss']
 | 
			
		||||
	selector: 'app-changelog',
 | 
			
		||||
	templateUrl: './changelog.component.html',
 | 
			
		||||
	styleUrls: ['./changelog.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class ChangelogComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  timestamp = new Date();  // Time from date input
 | 
			
		||||
  pageSize = 25;
 | 
			
		||||
  changelog: ChangelogModel[] = [];
 | 
			
		||||
  modalDetail = 0;        // Index of changelog element to show details of
 | 
			
		||||
	timestamp = new Date();  // Time from date input
 | 
			
		||||
	pageSize = 25;
 | 
			
		||||
	changelog: ChangelogModel[] = [];
 | 
			
		||||
	modalDetail = 0;        // Index of changelog element to show details of
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private api: ApiService,
 | 
			
		||||
    private modal: ModalService
 | 
			
		||||
  ) { }
 | 
			
		||||
	constructor(
 | 
			
		||||
		private api: ApiService,
 | 
			
		||||
		private modal: ModalService
 | 
			
		||||
	) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.loadChangelog();
 | 
			
		||||
  }
 | 
			
		||||
	ngOnInit(): void {
 | 
			
		||||
		this.loadChangelog();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  loadChangelog(page = 0) {  // Load changelog with page no relative to current page
 | 
			
		||||
    this.api.get<ChangelogModel[]>(`/changelog/${
 | 
			
		||||
      page > 0 ? this.changelog[0]._id :  // Use id if no new date was given
 | 
			
		||||
      Math.floor(new Date(
 | 
			
		||||
        new Date(this.timestamp).getTime() - new Date(this.timestamp).getTimezoneOffset() * 60000  // Adjust timezone
 | 
			
		||||
      ).getTime() / 1000).toString(16) + '0000000000000000'  // Id from time
 | 
			
		||||
    }/${page}/${this.pageSize}`, data => {
 | 
			
		||||
      this.changelog = data.map(e => new ChangelogModel().deserialize(e));
 | 
			
		||||
      if (page) {  // Adjust date picker to new first element when user clicked on next page
 | 
			
		||||
        this.timestamp = new Date(this.changelog[0].date);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
	loadChangelog(page = 0) {  // Load changelog with page no relative to current page
 | 
			
		||||
		this.api.get<ChangelogModel[]>(`/changelog/${
 | 
			
		||||
			page > 0 ? this.changelog[0]._id :  // Use id if no new date was given
 | 
			
		||||
					Math.floor(new Date(
 | 
			
		||||
						new Date(this.timestamp).getTime() - new Date(this.timestamp).getTimezoneOffset() * 60000  // Adjust timezone
 | 
			
		||||
			).getTime() / 1000).toString(16) + '0000000000000000'  // Id from time
 | 
			
		||||
		}/${page}/${this.pageSize}`, data => {
 | 
			
		||||
			this.changelog = data.map(e => new ChangelogModel().deserialize(e));
 | 
			
		||||
			if (page) {  // Adjust date picker to new first element when user clicked on next page
 | 
			
		||||
				this.timestamp = new Date(this.changelog[0].date);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  // Show details of a changelog element with reference to needed modal
 | 
			
		||||
  showDetails(i: number, modal: TemplateRef<any>) {
 | 
			
		||||
    this.modalDetail = i;
 | 
			
		||||
    this.modal.open(modal).then(() => {});
 | 
			
		||||
  }
 | 
			
		||||
	// Show details of a changelog element with reference to needed modal
 | 
			
		||||
	showDetails(i: number, modal: TemplateRef<any>) {
 | 
			
		||||
		this.modalDetail = i;
 | 
			
		||||
		this.modal.open(modal).then(() => {});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,50 +1,50 @@
 | 
			
		||||
<p>
 | 
			
		||||
  Bosch IoT Cloud space where all applications are hosted:
 | 
			
		||||
  <a href="https://apps.sys.de1.bosch-iot-cloud.com/organizations/b28baba5-f95f-4ce5-bc9c-3f45acd1dfb2">
 | 
			
		||||
    https://apps.sys.de1.bosch-iot-cloud.com/organizations/b28baba5-f95f-4ce5-bc9c-3f45acd1dfb2
 | 
			
		||||
  </a><br>
 | 
			
		||||
  Find the API documentation here:
 | 
			
		||||
  <a href="https://definma-api.apps.de1.bosch-iot-cloud.com/api-doc/">
 | 
			
		||||
    https://definma-api.apps.de1.bosch-iot-cloud.com/api-doc/
 | 
			
		||||
  </a><br>
 | 
			
		||||
  Admin database management page:
 | 
			
		||||
  <a href="https://definma-db.apps.de1.bosch-iot-cloud.com/">
 | 
			
		||||
    https://definma-db.apps.de1.bosch-iot-cloud.com/
 | 
			
		||||
  </a><br>
 | 
			
		||||
  Code repository UI
 | 
			
		||||
  <a href="https://sourcecode.socialcoding.bosch.com/users/vle2fe/repos/definma-ui">
 | 
			
		||||
    https://sourcecode.socialcoding.bosch.com/users/vle2fe/repos/definma-ui
 | 
			
		||||
  </a><br>
 | 
			
		||||
  Code repository API
 | 
			
		||||
  <a href="https://sourcecode.socialcoding.bosch.com/users/vle2fe/repos/definma-api">
 | 
			
		||||
    https://sourcecode.socialcoding.bosch.com/users/vle2fe/repos/definma-api
 | 
			
		||||
  </a><br>
 | 
			
		||||
  Code repository Models
 | 
			
		||||
  <a href="https://sourcecode.socialcoding.bosch.com/users/poe2rng/repos/definma-models">
 | 
			
		||||
    https://sourcecode.socialcoding.bosch.com/users/poe2rng/repos/definma-models
 | 
			
		||||
  </a><br>
 | 
			
		||||
Bosch IoT Cloud space where all applications are hosted:
 | 
			
		||||
<a href="https://apps.sys.de1.bosch-iot-cloud.com/organizations/b28baba5-f95f-4ce5-bc9c-3f45acd1dfb2">
 | 
			
		||||
	https://apps.sys.de1.bosch-iot-cloud.com/organizations/b28baba5-f95f-4ce5-bc9c-3f45acd1dfb2
 | 
			
		||||
</a><br>
 | 
			
		||||
Find the API documentation here:
 | 
			
		||||
<a href="https://definma-api.apps.de1.bosch-iot-cloud.com/api-doc/">
 | 
			
		||||
	https://definma-api.apps.de1.bosch-iot-cloud.com/api-doc/
 | 
			
		||||
</a><br>
 | 
			
		||||
Admin database management page:
 | 
			
		||||
<a href="https://definma-db.apps.de1.bosch-iot-cloud.com/">
 | 
			
		||||
	https://definma-db.apps.de1.bosch-iot-cloud.com/
 | 
			
		||||
</a><br>
 | 
			
		||||
Code repository UI
 | 
			
		||||
<a href="https://sourcecode.socialcoding.bosch.com/users/vle2fe/repos/definma-ui">
 | 
			
		||||
	https://sourcecode.socialcoding.bosch.com/users/vle2fe/repos/definma-ui
 | 
			
		||||
</a><br>
 | 
			
		||||
Code repository API
 | 
			
		||||
<a href="https://sourcecode.socialcoding.bosch.com/users/vle2fe/repos/definma-api">
 | 
			
		||||
	https://sourcecode.socialcoding.bosch.com/users/vle2fe/repos/definma-api
 | 
			
		||||
</a><br>
 | 
			
		||||
Code repository Models
 | 
			
		||||
<a href="https://sourcecode.socialcoding.bosch.com/users/poe2rng/repos/definma-models">
 | 
			
		||||
	https://sourcecode.socialcoding.bosch.com/users/poe2rng/repos/definma-models
 | 
			
		||||
</a><br>
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
<p>
 | 
			
		||||
  All applications are hosted in the bosch IoT Cloud. The API is hosted in a Node.js container, with a bound MongoDB
 | 
			
		||||
  instance. <br>
 | 
			
		||||
  The Angular UI is hosted in a staticfile container, serving the built Angular bundle. <br>
 | 
			
		||||
  Finally any machine learning models are hosted in Python containers.
 | 
			
		||||
All applications are hosted in the bosch IoT Cloud. The API is hosted in a Node.js container, with a bound MongoDB
 | 
			
		||||
instance. <br>
 | 
			
		||||
The Angular UI is hosted in a staticfile container, serving the built Angular bundle. <br>
 | 
			
		||||
Finally any machine learning models are hosted in Python containers.
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
<h4>Development setup</h4>
 | 
			
		||||
 | 
			
		||||
<p>
 | 
			
		||||
  In any case, it is good to have PxProxy working, following the
 | 
			
		||||
  <a href="https://inside-docupedia.bosch.com/confluence/pages/viewpage.action?pageId=608652557">
 | 
			
		||||
    Bosch installation guide
 | 
			
		||||
  </a>. <br>
 | 
			
		||||
  For pushing to the BIC, you need the CloudFoundry CLI as described in the
 | 
			
		||||
  <a href="https://inside-docupedia.bosch.com/confluence/display/BICI/Cloud+Foundry+CLI">BIC documentation</a>.
 | 
			
		||||
  If the the downloaded version of the CLI should not work, there is an old installer with a definitely working version
 | 
			
		||||
  on the file share in the bin folder.
 | 
			
		||||
  Please consider that you need to have a login to the BIC and be marked as space developer for the DeFinMa Org. <br>
 | 
			
		||||
  For front and back end development, you need a
 | 
			
		||||
  <a href="https://www.mongodb.com/try/download/community">local MongoDB server</a> as well as
 | 
			
		||||
  <a href="https://nodejs.org/en/">Node.js</a>.
 | 
			
		||||
In any case, it is good to have PxProxy working, following the
 | 
			
		||||
<a href="https://inside-docupedia.bosch.com/confluence/pages/viewpage.action?pageId=608652557">
 | 
			
		||||
	Bosch installation guide
 | 
			
		||||
</a>. <br>
 | 
			
		||||
For pushing to the BIC, you need the CloudFoundry CLI as described in the
 | 
			
		||||
<a href="https://inside-docupedia.bosch.com/confluence/display/BICI/Cloud+Foundry+CLI">BIC documentation</a>.
 | 
			
		||||
If the the downloaded version of the CLI should not work, there is an old installer with a definitely working version
 | 
			
		||||
on the file share in the bin folder.
 | 
			
		||||
Please consider that you need to have a login to the BIC and be marked as space developer for the DeFinMa Org. <br>
 | 
			
		||||
For front and back end development, you need a
 | 
			
		||||
<a href="https://www.mongodb.com/try/download/community">local MongoDB server</a> as well as
 | 
			
		||||
<a href="https://nodejs.org/en/">Node.js</a>.
 | 
			
		||||
</p>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,15 @@
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-documentation-architecture',
 | 
			
		||||
  templateUrl: './documentation-architecture.component.html',
 | 
			
		||||
  styleUrls: ['./documentation-architecture.component.scss']
 | 
			
		||||
	selector: 'app-documentation-architecture',
 | 
			
		||||
	templateUrl: './documentation-architecture.component.html',
 | 
			
		||||
	styleUrls: ['./documentation-architecture.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class DocumentationArchitectureComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
	constructor() { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
  }
 | 
			
		||||
	ngOnInit(): void {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,38 +1,38 @@
 | 
			
		||||
<p>
 | 
			
		||||
  The used database instance is a MongoDB instance running on the BIC, storing all application data.
 | 
			
		||||
  The admin database management page can be accessed <a href="https://definma-db.apps.de1.bosch-iot-cloud.com/api-doc/">here</a>.
 | 
			
		||||
  However, it is recommended to use a dedicated MongoDB application for anything apart from a quick look.
 | 
			
		||||
  The two tested applications are <a href="https://www.mongodb.com/products/compass">MongoDB Compass</a> and
 | 
			
		||||
  <a href="https://robomongo.org/s">Robo 3T</a> (recommended for trying queries).
 | 
			
		||||
The used database instance is a MongoDB instance running on the BIC, storing all application data.
 | 
			
		||||
The admin database management page can be accessed <a href="https://definma-db.apps.de1.bosch-iot-cloud.com/api-doc/">here</a>.
 | 
			
		||||
However, it is recommended to use a dedicated MongoDB application for anything apart from a quick look.
 | 
			
		||||
The two tested applications are <a href="https://www.mongodb.com/products/compass">MongoDB Compass</a> and
 | 
			
		||||
<a href="https://robomongo.org/s">Robo 3T</a> (recommended for trying queries).
 | 
			
		||||
</p>
 | 
			
		||||
<p>
 | 
			
		||||
  To connect to the BIC instance from your local application follow the
 | 
			
		||||
  <a href="https://inside-docupedia.bosch.com/confluence/display/BICI/MongoDB+Client+Tool+via+SSH+Tunnel">BIC guide</a>.
 | 
			
		||||
  <br>
 | 
			
		||||
  TL;DR:
 | 
			
		||||
To connect to the BIC instance from your local application follow the
 | 
			
		||||
<a href="https://inside-docupedia.bosch.com/confluence/display/BICI/MongoDB+Client+Tool+via+SSH+Tunnel">BIC guide</a>.
 | 
			
		||||
<br>
 | 
			
		||||
TL;DR:
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
<p>For the first time:</p>
 | 
			
		||||
<ul>
 | 
			
		||||
  <li>cf login</li>
 | 
			
		||||
  <li>cf enable-ssh definma-api</li>
 | 
			
		||||
  <li>cf create-service-key definmadb <key name, can be whatever></li>
 | 
			
		||||
	<li>cf login</li>
 | 
			
		||||
	<li>cf enable-ssh definma-api</li>
 | 
			
		||||
	<li>cf create-service-key definmadb <key name, can be whatever></li>
 | 
			
		||||
</ul>
 | 
			
		||||
<p>Every time:</p>
 | 
			
		||||
<ul>
 | 
			
		||||
  <li>cf ssh -L 1120:st0cvm200118.internal-mongodb.de1.bosch-iot-cloud.com:30000 definma-api</li>
 | 
			
		||||
  <li>
 | 
			
		||||
    Login into your application with localhost:1120 and use credentials and authentication database from the BIC
 | 
			
		||||
    (Data can be found at Home > DeFinMa > production > definma-api > Services > dots on the right of definmadb > View Credentials)
 | 
			
		||||
  </li>
 | 
			
		||||
	<li>cf ssh -L 1120:st0cvm200118.internal-mongodb.de1.bosch-iot-cloud.com:30000 definma-api</li>
 | 
			
		||||
	<li>
 | 
			
		||||
		Login into your application with localhost:1120 and use credentials and authentication database from the BIC
 | 
			
		||||
		(Data can be found at Home > DeFinMa > production > definma-api > Services > dots on the right of definmadb > View Credentials)
 | 
			
		||||
	</li>
 | 
			
		||||
</ul>
 | 
			
		||||
 | 
			
		||||
<h4>Backup Instructions</h4>
 | 
			
		||||
 | 
			
		||||
<p>
 | 
			
		||||
  For creating a database backup, you must follow the same steps from above (except the last one).
 | 
			
		||||
  Additionally you need the <a href="https://www.mongodb.com/try/download/database-tools">MongoDB Tools</a> installed.
 | 
			
		||||
  Extract the ZIP file to a location on your hard drive and launch a Git Bash in that directory.
 | 
			
		||||
For creating a database backup, you must follow the same steps from above (except the last one).
 | 
			
		||||
Additionally you need the <a href="https://www.mongodb.com/try/download/database-tools">MongoDB Tools</a> installed.
 | 
			
		||||
Extract the ZIP file to a location on your hard drive and launch a Git Bash in that directory.
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
<p>To back up the database from the BIC:</p>
 | 
			
		||||
@@ -51,377 +51,377 @@
 | 
			
		||||
</pre>
 | 
			
		||||
 | 
			
		||||
<p>
 | 
			
		||||
  More information can be found inside the
 | 
			
		||||
  <a href="https://docs.mongodb.com/database-tools/">documentation</a>. <br>
 | 
			
		||||
  The BIC service also creates an internal backup, which can be requested to restore, see
 | 
			
		||||
  <a href="https://inside-docupedia.bosch.com/confluence/pages/viewpage.action?pageId=565402281">MongoDB FAQ</a>
 | 
			
		||||
More information can be found inside the
 | 
			
		||||
<a href="https://docs.mongodb.com/database-tools/">documentation</a>. <br>
 | 
			
		||||
The BIC service also creates an internal backup, which can be requested to restore, see
 | 
			
		||||
<a href="https://inside-docupedia.bosch.com/confluence/pages/viewpage.action?pageId=565402281">MongoDB FAQ</a>
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
<h4>Database Model</h4>
 | 
			
		||||
 | 
			
		||||
<app-img-magnifier src="assets/imgs/db_structure_latest.svg" zoom="2" [magnifierSize]="{width: 400, height: 300}"
 | 
			
		||||
                   id="db-structure"></app-img-magnifier>
 | 
			
		||||
	id="db-structure"></app-img-magnifier>
 | 
			
		||||
 | 
			
		||||
<h4>Field Reference</h4>
 | 
			
		||||
 | 
			
		||||
<rb-table class="field-reference">
 | 
			
		||||
  <tr><th>samples</th><th></th><th>Example</th></tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>_id</td>
 | 
			
		||||
    <td>Automatically generated unique id</td>
 | 
			
		||||
    <td>'5f2e63c98d1c020f8cda6e06'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>type</td>
 | 
			
		||||
    <td>
 | 
			
		||||
      The material status of the sample, can be either <span class="name">as-delivered/raw</span> or
 | 
			
		||||
      <span class="name">processed</span>.
 | 
			
		||||
    </td>
 | 
			
		||||
    <td>'processed'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>number</td>
 | 
			
		||||
    <td>The sample number, generated by the server for new samples</td>
 | 
			
		||||
    <td>'An31'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>color</td>
 | 
			
		||||
    <td>Sample color</td>
 | 
			
		||||
    <td>'black'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>batch</td>
 | 
			
		||||
    <td>Batch number the sample was from</td>
 | 
			
		||||
    <td>'2264486614'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>status</td>
 | 
			
		||||
    <td>Status of the document, can be either <span class="name">new</span>, <span class="name">validated</span> or
 | 
			
		||||
    <span class="name">deleted</span>.</td>
 | 
			
		||||
    <td>'new'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>condition</td>
 | 
			
		||||
    <td>sample condition with <span class="name">condition_template</span> reference and all fields defined by the
 | 
			
		||||
    template</td>
 | 
			
		||||
    <td>{{'{'}}condition_template: '5f3151b5b8a886007d2de9ed', time in minutes: 30{{'}'}}</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>material_id</td>
 | 
			
		||||
    <td>Reference to the sample material</td>
 | 
			
		||||
    <td>'5f2e63118d1c020f8cda6a0a'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>note_id</td>
 | 
			
		||||
    <td>Reference to the sample note</td>
 | 
			
		||||
    <td>'5f2e63c98d1c020f8cda6e08'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>user_id</td>
 | 
			
		||||
    <td>Reference to the user who created the sample</td>
 | 
			
		||||
    <td>'5f294dd4aa9ea5085c7d7315'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
	<tr><th>samples</th><th></th><th>Example</th></tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>_id</td>
 | 
			
		||||
		<td>Automatically generated unique id</td>
 | 
			
		||||
		<td>'5f2e63c98d1c020f8cda6e06'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>type</td>
 | 
			
		||||
		<td>
 | 
			
		||||
			The material status of the sample, can be either <span class="name">as-delivered/raw</span> or
 | 
			
		||||
			<span class="name">processed</span>.
 | 
			
		||||
		</td>
 | 
			
		||||
		<td>'processed'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>number</td>
 | 
			
		||||
		<td>The sample number, generated by the server for new samples</td>
 | 
			
		||||
		<td>'An31'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>color</td>
 | 
			
		||||
		<td>Sample color</td>
 | 
			
		||||
		<td>'black'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>batch</td>
 | 
			
		||||
		<td>Batch number the sample was from</td>
 | 
			
		||||
		<td>'2264486614'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>status</td>
 | 
			
		||||
		<td>Status of the document, can be either <span class="name">new</span>, <span class="name">validated</span> or
 | 
			
		||||
			<span class="name">deleted</span>.</td>
 | 
			
		||||
		<td>'new'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>condition</td>
 | 
			
		||||
		<td>sample condition with <span class="name">condition_template</span> reference and all fields defined by the
 | 
			
		||||
			template</td>
 | 
			
		||||
		<td>{{'{'}}condition_template: '5f3151b5b8a886007d2de9ed', time in minutes: 30{{'}'}}</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>material_id</td>
 | 
			
		||||
		<td>Reference to the sample material</td>
 | 
			
		||||
		<td>'5f2e63118d1c020f8cda6a0a'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>note_id</td>
 | 
			
		||||
		<td>Reference to the sample note</td>
 | 
			
		||||
		<td>'5f2e63c98d1c020f8cda6e08'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>user_id</td>
 | 
			
		||||
		<td>Reference to the user who created the sample</td>
 | 
			
		||||
		<td>'5f294dd4aa9ea5085c7d7315'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
 | 
			
		||||
  <tr><th>notes</th><th></th><th></th></tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>_id</td>
 | 
			
		||||
    <td>Automatically generated unique id</td>
 | 
			
		||||
    <td>'5f2e63e98d1c020f8cda70cc'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>comment</td>
 | 
			
		||||
    <td>General remarks</td>
 | 
			
		||||
    <td>'stabilized'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>sample_references</td>
 | 
			
		||||
    <td>Array of references to other samples, each reference containing the referenced <span class="name">sample_id
 | 
			
		||||
    </span> as well as a <span class="name">relation</span> field describing the relationship</td>
 | 
			
		||||
    <td>{{'{'}}sample_id: '5f2e63d68d1c020f8cda701c', relation: 'belongs to'{{'}'}}</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>custom_fields</td>
 | 
			
		||||
    <td>Additional information as key value pairs for the sample, making it easier to process this information</td>
 | 
			
		||||
    <td>{{'{'}}vwz: '0 min'{{'}'}}</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
	<tr><th>notes</th><th></th><th></th></tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>_id</td>
 | 
			
		||||
		<td>Automatically generated unique id</td>
 | 
			
		||||
		<td>'5f2e63e98d1c020f8cda70cc'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>comment</td>
 | 
			
		||||
		<td>General remarks</td>
 | 
			
		||||
		<td>'stabilized'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>sample_references</td>
 | 
			
		||||
		<td>Array of references to other samples, each reference containing the referenced <span class="name">sample_id
 | 
			
		||||
			</span> as well as a <span class="name">relation</span> field describing the relationship</td>
 | 
			
		||||
			<td>{{'{'}}sample_id: '5f2e63d68d1c020f8cda701c', relation: 'belongs to'{{'}'}}</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>custom_fields</td>
 | 
			
		||||
		<td>Additional information as key value pairs for the sample, making it easier to process this information</td>
 | 
			
		||||
		<td>{{'{'}}vwz: '0 min'{{'}'}}</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
 | 
			
		||||
  <tr><th>note_fields</th><th></th><th></th></tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>_id</td>
 | 
			
		||||
    <td>Automatically generated unique id</td>
 | 
			
		||||
    <td>'5f2e63e98d1c020f8cda70ce'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>name</td>
 | 
			
		||||
    <td>name of the <span class="name">custom_fields</span> key</td>
 | 
			
		||||
    <td>'test series'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>qty</td>
 | 
			
		||||
    <td>number of notes with this key</td>
 | 
			
		||||
    <td>24</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
	<tr><th>note_fields</th><th></th><th></th></tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>_id</td>
 | 
			
		||||
		<td>Automatically generated unique id</td>
 | 
			
		||||
		<td>'5f2e63e98d1c020f8cda70ce'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>name</td>
 | 
			
		||||
		<td>name of the <span class="name">custom_fields</span> key</td>
 | 
			
		||||
		<td>'test series'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>qty</td>
 | 
			
		||||
		<td>number of notes with this key</td>
 | 
			
		||||
		<td>24</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
 | 
			
		||||
  <tr><th>materials</th><th></th><th></th></tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>_id</td>
 | 
			
		||||
    <td>Automatically generated unique id</td>
 | 
			
		||||
    <td>'5f2e63e98d1c020f8cda70d0'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>name</td>
 | 
			
		||||
    <td>Trade name of the material</td>
 | 
			
		||||
    <td>'Ultradur B4300 G6'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>numbers</td>
 | 
			
		||||
    <td>Bosch material part numbers</td>
 | 
			
		||||
    <td>['5515753021, '5515753404']</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>properties</td>
 | 
			
		||||
    <td>material class specific properties with <span class="name">material_template</span> reference and all fields
 | 
			
		||||
      defined by the template</td>
 | 
			
		||||
    <td>{{'{'}}material_template: '5f2e89874ac96c007fb06e83', mineral: 0, glass_fiber: 30, carbon_fiber: 0{{'}'}}</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>group_id</td>
 | 
			
		||||
    <td>Reference to the material group</td>
 | 
			
		||||
    <td>'5f2e631191c5d68f8a0708c4'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>supplier_id</td>
 | 
			
		||||
    <td>Reference to the material supplier</td>
 | 
			
		||||
    <td>'5f2e631191c5d68f8a0708c7'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>status</td>
 | 
			
		||||
    <td>Status of the document, can be either <span class="name">new</span>, <span class="name">validated</span> or
 | 
			
		||||
      <span class="name">deleted</span>.</td>
 | 
			
		||||
    <td>'new'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
	<tr><th>materials</th><th></th><th></th></tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>_id</td>
 | 
			
		||||
		<td>Automatically generated unique id</td>
 | 
			
		||||
		<td>'5f2e63e98d1c020f8cda70d0'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>name</td>
 | 
			
		||||
		<td>Trade name of the material</td>
 | 
			
		||||
		<td>'Ultradur B4300 G6'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>numbers</td>
 | 
			
		||||
		<td>Bosch material part numbers</td>
 | 
			
		||||
		<td>['5515753021, '5515753404']</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>properties</td>
 | 
			
		||||
		<td>material class specific properties with <span class="name">material_template</span> reference and all fields
 | 
			
		||||
			defined by the template</td>
 | 
			
		||||
		<td>{{'{'}}material_template: '5f2e89874ac96c007fb06e83', mineral: 0, glass_fiber: 30, carbon_fiber: 0{{'}'}}</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>group_id</td>
 | 
			
		||||
		<td>Reference to the material group</td>
 | 
			
		||||
		<td>'5f2e631191c5d68f8a0708c4'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>supplier_id</td>
 | 
			
		||||
		<td>Reference to the material supplier</td>
 | 
			
		||||
		<td>'5f2e631191c5d68f8a0708c7'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>status</td>
 | 
			
		||||
		<td>Status of the document, can be either <span class="name">new</span>, <span class="name">validated</span> or
 | 
			
		||||
			<span class="name">deleted</span>.</td>
 | 
			
		||||
		<td>'new'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
 | 
			
		||||
  <tr><th>material_groups</th><th></th><th></th></tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>_id</td>
 | 
			
		||||
    <td>Automatically generated unique id</td>
 | 
			
		||||
    <td>'5f2e631291c5d68f8a0708f9'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>name</td>
 | 
			
		||||
    <td>The chemical material type</td>
 | 
			
		||||
    <td>'PA66'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
	<tr><th>material_groups</th><th></th><th></th></tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>_id</td>
 | 
			
		||||
		<td>Automatically generated unique id</td>
 | 
			
		||||
		<td>'5f2e631291c5d68f8a0708f9'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>name</td>
 | 
			
		||||
		<td>The chemical material type</td>
 | 
			
		||||
		<td>'PA66'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
 | 
			
		||||
  <tr><th>material_supplier</th><th></th><th></th></tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>_id</td>
 | 
			
		||||
    <td>Automatically generated unique id</td>
 | 
			
		||||
    <td>'5f2e631991c5d68f8a0709c3'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>name</td>
 | 
			
		||||
    <td>The material supplier</td>
 | 
			
		||||
    <td>'BASF'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
	<tr><th>material_supplier</th><th></th><th></th></tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>_id</td>
 | 
			
		||||
		<td>Automatically generated unique id</td>
 | 
			
		||||
		<td>'5f2e631991c5d68f8a0709c3'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>name</td>
 | 
			
		||||
		<td>The material supplier</td>
 | 
			
		||||
		<td>'BASF'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
 | 
			
		||||
  <tr><th>measurements</th><th></th><th></th></tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>_id</td>
 | 
			
		||||
    <td>Automatically generated unique id</td>
 | 
			
		||||
    <td>'5f294d25aa9ea5085c7d7305'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>sample_id</td>
 | 
			
		||||
    <td>Reference to the sample this measurement belongs to</td>
 | 
			
		||||
    <td>'5f2e63c98d1c020f8cda6e06'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>measurement_template</td>
 | 
			
		||||
    <td>Reference to the Template defining the structure of the measurement values</td>
 | 
			
		||||
    <td>'5f294d25aa9ea5085c7d7305'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>values</td>
 | 
			
		||||
    <td>Measurement values in defined format</td>
 | 
			
		||||
    <td>{{'{'}}vn: 100.4{{'}'}}</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>status</td>
 | 
			
		||||
    <td>Status of the document, can be either <span class="name">new</span>, <span class="name">validated</span> or
 | 
			
		||||
      <span class="name">deleted</span>.</td>
 | 
			
		||||
    <td>'new'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
	<tr><th>measurements</th><th></th><th></th></tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>_id</td>
 | 
			
		||||
		<td>Automatically generated unique id</td>
 | 
			
		||||
		<td>'5f294d25aa9ea5085c7d7305'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>sample_id</td>
 | 
			
		||||
		<td>Reference to the sample this measurement belongs to</td>
 | 
			
		||||
		<td>'5f2e63c98d1c020f8cda6e06'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>measurement_template</td>
 | 
			
		||||
		<td>Reference to the Template defining the structure of the measurement values</td>
 | 
			
		||||
		<td>'5f294d25aa9ea5085c7d7305'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>values</td>
 | 
			
		||||
		<td>Measurement values in defined format</td>
 | 
			
		||||
		<td>{{'{'}}vn: 100.4{{'}'}}</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>status</td>
 | 
			
		||||
		<td>Status of the document, can be either <span class="name">new</span>, <span class="name">validated</span> or
 | 
			
		||||
			<span class="name">deleted</span>.</td>
 | 
			
		||||
		<td>'new'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
 | 
			
		||||
  <tr><th><collection>_templates</th><th></th><th></th></tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>_id</td>
 | 
			
		||||
    <td>Automatically generated unique id</td>
 | 
			
		||||
    <td>'5f2e63ee8d1c020f8cda7128'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>name</td>
 | 
			
		||||
    <td>Display name of the template</td>
 | 
			
		||||
    <td>'spectrum'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>version</td>
 | 
			
		||||
    <td>Version number of the template</td>
 | 
			
		||||
    <td>2</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>first_id</td>
 | 
			
		||||
    <td>Reference to the first instance of this template with <span class="name">version</span> number 1</td>
 | 
			
		||||
    <td>'5f2e89bb4ac96c007fb06e86'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>parameters</td>
 | 
			
		||||
    <td>Specified parameters of this template. The <span class="name">name</span> property is used as the key in the
 | 
			
		||||
      document using this template, the range can have the following properties: <span class="name">min</span> specifies
 | 
			
		||||
      the minimum numeric value, <span class="name">max</span> specifies the maximum numeric value, <span class="name">
 | 
			
		||||
      values</span> specifies an array of allowed values of this parameter and <span class="name">type: 'array'</span>
 | 
			
		||||
      specifies that this parameter must be an array</td>
 | 
			
		||||
    <td></td>
 | 
			
		||||
  </tr>
 | 
			
		||||
	<tr><th><collection>_templates</th><th></th><th></th></tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>_id</td>
 | 
			
		||||
		<td>Automatically generated unique id</td>
 | 
			
		||||
		<td>'5f2e63ee8d1c020f8cda7128'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>name</td>
 | 
			
		||||
		<td>Display name of the template</td>
 | 
			
		||||
		<td>'spectrum'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>version</td>
 | 
			
		||||
		<td>Version number of the template</td>
 | 
			
		||||
		<td>2</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>first_id</td>
 | 
			
		||||
		<td>Reference to the first instance of this template with <span class="name">version</span> number 1</td>
 | 
			
		||||
		<td>'5f2e89bb4ac96c007fb06e86'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>parameters</td>
 | 
			
		||||
		<td>Specified parameters of this template. The <span class="name">name</span> property is used as the key in the
 | 
			
		||||
			document using this template, the range can have the following properties: <span class="name">min</span> specifies
 | 
			
		||||
			the minimum numeric value, <span class="name">max</span> specifies the maximum numeric value, <span class="name">
 | 
			
		||||
				values</span> specifies an array of allowed values of this parameter and <span class="name">type: 'array'</span>
 | 
			
		||||
			specifies that this parameter must be an array</td>
 | 
			
		||||
		<td></td>
 | 
			
		||||
	</tr>
 | 
			
		||||
 | 
			
		||||
  <tr><th>users</th><th></th><th></th></tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>_id</td>
 | 
			
		||||
    <td>Automatically generated unique id</td>
 | 
			
		||||
    <td>'5f2e63cc8d1c020f8cda6e6a'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>name</td>
 | 
			
		||||
    <td>The username</td>
 | 
			
		||||
    <td>'admin'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>email</td>
 | 
			
		||||
    <td>The user's email address used for password reset</td>
 | 
			
		||||
    <td>'test@bosch.com'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>location</td>
 | 
			
		||||
    <td>The abbreviation of the Bosch site of the user</td>
 | 
			
		||||
    <td>'Rng'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>level</td>
 | 
			
		||||
    <td>The permission level, can be either <span class="name">read</span>, <span class="name">write</span>,
 | 
			
		||||
      <span class="name">dev</span> or <span class="name">admin</span>. The exact level permissions can be found at the
 | 
			
		||||
      <a routerLink="/documentation">general documentation</a></td>
 | 
			
		||||
    <td></td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>devices</td>
 | 
			
		||||
    <td>Array of all spectrum measurement devices the user has access to</td>
 | 
			
		||||
    <td>['Rng01', 'Rng02']</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>pass</td>
 | 
			
		||||
    <td>The user's password in hashed form using bcrypt</td>
 | 
			
		||||
    <td>'$2a$10$m8DqvZR3plZEv8EPwPo7Luvyrm/ZQDiPzfBh6bpU/1XFWOGONkJyG'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>key</td>
 | 
			
		||||
    <td>The API key, generated when the user is created</td>
 | 
			
		||||
    <td>'5f294dd4aa9ea5085c7d7314'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>models</td>
 | 
			
		||||
    <td>
 | 
			
		||||
      Reference to the prediction models the user should have access to. Ignored for <span class="name">dev</span> and
 | 
			
		||||
      <span class="name">admin</span> as they automatically have access to all models.
 | 
			
		||||
    </td>
 | 
			
		||||
    <td>['5f466fb1e566810dd8b3e919', '5f294d8aaa9ea5085c7d730b']</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>status</td>
 | 
			
		||||
    <td>Status of the document, can be either <span class="name">new</span>, <span class="name">validated</span> or
 | 
			
		||||
      <span class="name">deleted</span>.</td>
 | 
			
		||||
    <td>'new'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
	<tr><th>users</th><th></th><th></th></tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>_id</td>
 | 
			
		||||
		<td>Automatically generated unique id</td>
 | 
			
		||||
		<td>'5f2e63cc8d1c020f8cda6e6a'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>name</td>
 | 
			
		||||
		<td>The username</td>
 | 
			
		||||
		<td>'admin'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>email</td>
 | 
			
		||||
		<td>The user's email address used for password reset</td>
 | 
			
		||||
		<td>'test@bosch.com'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>location</td>
 | 
			
		||||
		<td>The abbreviation of the Bosch site of the user</td>
 | 
			
		||||
		<td>'Rng'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>level</td>
 | 
			
		||||
		<td>The permission level, can be either <span class="name">read</span>, <span class="name">write</span>,
 | 
			
		||||
			<span class="name">dev</span> or <span class="name">admin</span>. The exact level permissions can be found at the
 | 
			
		||||
			<a routerLink="/documentation">general documentation</a></td>
 | 
			
		||||
		<td></td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>devices</td>
 | 
			
		||||
		<td>Array of all spectrum measurement devices the user has access to</td>
 | 
			
		||||
		<td>['Rng01', 'Rng02']</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>pass</td>
 | 
			
		||||
		<td>The user's password in hashed form using bcrypt</td>
 | 
			
		||||
		<td>'$2a$10$m8DqvZR3plZEv8EPwPo7Luvyrm/ZQDiPzfBh6bpU/1XFWOGONkJyG'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>key</td>
 | 
			
		||||
		<td>The API key, generated when the user is created</td>
 | 
			
		||||
		<td>'5f294dd4aa9ea5085c7d7314'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>models</td>
 | 
			
		||||
		<td>
 | 
			
		||||
			Reference to the prediction models the user should have access to. Ignored for <span class="name">dev</span> and
 | 
			
		||||
			<span class="name">admin</span> as they automatically have access to all models.
 | 
			
		||||
		</td>
 | 
			
		||||
		<td>['5f466fb1e566810dd8b3e919', '5f294d8aaa9ea5085c7d730b']</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>status</td>
 | 
			
		||||
		<td>Status of the document, can be either <span class="name">new</span>, <span class="name">validated</span> or
 | 
			
		||||
			<span class="name">deleted</span>.</td>
 | 
			
		||||
		<td>'new'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
 | 
			
		||||
  <tr><th>models</th><th></th><th></th></tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>group</td>
 | 
			
		||||
    <td>group the model belongs to</td>
 | 
			
		||||
    <td>'VN'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>models</td>
 | 
			
		||||
    <td>models of the group with _id, name and url to the Python endpoint</td>
 | 
			
		||||
    <td>{{'{'}}_id: '5f466fb1e566810dd8b3e919', name: POM, url: 'http://localhost:9099/test'{{'}'}}</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
	<tr><th>models</th><th></th><th></th></tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>group</td>
 | 
			
		||||
		<td>group the model belongs to</td>
 | 
			
		||||
		<td>'VN'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>models</td>
 | 
			
		||||
		<td>models of the group with _id, name and url to the Python endpoint</td>
 | 
			
		||||
		<td>{{'{'}}_id: '5f466fb1e566810dd8b3e919', name: POM, url: 'http://localhost:9099/test'{{'}'}}</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
 | 
			
		||||
  <tr><th>model_files</th><th></th><th></th></tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>_id</td>
 | 
			
		||||
    <td>Automatically generated unique id</td>
 | 
			
		||||
    <td>'5f294d47aa9ea5085c7d7308'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>name</td>
 | 
			
		||||
    <td>The name of the model</td>
 | 
			
		||||
    <td>'humidity-1'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>data</td>
 | 
			
		||||
    <td>The Python model data in binary format</td>
 | 
			
		||||
    <td><binary data></td>
 | 
			
		||||
  </tr>
 | 
			
		||||
	<tr><th>model_files</th><th></th><th></th></tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>_id</td>
 | 
			
		||||
		<td>Automatically generated unique id</td>
 | 
			
		||||
		<td>'5f294d47aa9ea5085c7d7308'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>name</td>
 | 
			
		||||
		<td>The name of the model</td>
 | 
			
		||||
		<td>'humidity-1'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>data</td>
 | 
			
		||||
		<td>The Python model data in binary format</td>
 | 
			
		||||
		<td><binary data></td>
 | 
			
		||||
	</tr>
 | 
			
		||||
 | 
			
		||||
  <tr><th>changelogs</th><th></th><th></th></tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>_id</td>
 | 
			
		||||
    <td>Automatically generated unique id</td>
 | 
			
		||||
    <td>'5f2e63cc8d1c020f8cda6e6e'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>action</td>
 | 
			
		||||
    <td>The URL which invoked the database write access</td>
 | 
			
		||||
    <td>'POST /material/new'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>collection_name</td>
 | 
			
		||||
    <td>Collection that was written to</td>
 | 
			
		||||
    <td>'material_groups'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>conditions</td>
 | 
			
		||||
    <td>Condition arguments used when accessing the database</td>
 | 
			
		||||
    <td>{{'{'}}id: '5f2e63118d1c020f8cda6a0a'{{'}'}}</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>data</td>
 | 
			
		||||
    <td>data which was written to the database</td>
 | 
			
		||||
    <td>{{'{'}}name: 'PBT'{{'}'}}</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>user_id</td>
 | 
			
		||||
    <td>The user that executed this command</td>
 | 
			
		||||
    <td>'5f2e63118d1c020f8cda6a09'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
	<tr><th>changelogs</th><th></th><th></th></tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>_id</td>
 | 
			
		||||
		<td>Automatically generated unique id</td>
 | 
			
		||||
		<td>'5f2e63cc8d1c020f8cda6e6e'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>action</td>
 | 
			
		||||
		<td>The URL which invoked the database write access</td>
 | 
			
		||||
		<td>'POST /material/new'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>collection_name</td>
 | 
			
		||||
		<td>Collection that was written to</td>
 | 
			
		||||
		<td>'material_groups'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>conditions</td>
 | 
			
		||||
		<td>Condition arguments used when accessing the database</td>
 | 
			
		||||
		<td>{{'{'}}id: '5f2e63118d1c020f8cda6a0a'{{'}'}}</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>data</td>
 | 
			
		||||
		<td>data which was written to the database</td>
 | 
			
		||||
		<td>{{'{'}}name: 'PBT'{{'}'}}</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>user_id</td>
 | 
			
		||||
		<td>The user that executed this command</td>
 | 
			
		||||
		<td>'5f2e63118d1c020f8cda6a09'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
 | 
			
		||||
  <tr><th>help</th><th></th><th></th></tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>_id</td>
 | 
			
		||||
    <td>Automatically generated unique id</td>
 | 
			
		||||
    <td>'5f2e63d28d1c020f8cda6f86'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>key</td>
 | 
			
		||||
    <td>The key used to find the required help text</td>
 | 
			
		||||
    <td>'/documentation/database'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>level</td>
 | 
			
		||||
    <td>The minimum level required to read this help</td>
 | 
			
		||||
    <td>'write'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <td>text</td>
 | 
			
		||||
    <td>The actual help text</td>
 | 
			
		||||
    <td>'This page documents the database.'</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
	<tr><th>help</th><th></th><th></th></tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>_id</td>
 | 
			
		||||
		<td>Automatically generated unique id</td>
 | 
			
		||||
		<td>'5f2e63d28d1c020f8cda6f86'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>key</td>
 | 
			
		||||
		<td>The key used to find the required help text</td>
 | 
			
		||||
		<td>'/documentation/database'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>level</td>
 | 
			
		||||
		<td>The minimum level required to read this help</td>
 | 
			
		||||
		<td>'write'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td>text</td>
 | 
			
		||||
		<td>The actual help text</td>
 | 
			
		||||
		<td>'This page documents the database.'</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
</rb-table>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,14 @@
 | 
			
		||||
.field-reference td:nth-child(3) {
 | 
			
		||||
  font-family: boschmono, monospace;
 | 
			
		||||
	font-family: boschmono, monospace;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
span.name {
 | 
			
		||||
  font-style: italic;
 | 
			
		||||
	font-style: italic;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pre {
 | 
			
		||||
  padding: 1rem;
 | 
			
		||||
  color: #212529;
 | 
			
		||||
  background: #e6e6e6;
 | 
			
		||||
  white-space: pre-wrap;
 | 
			
		||||
	padding: 1rem;
 | 
			
		||||
	color: #212529;
 | 
			
		||||
	background: #e6e6e6;
 | 
			
		||||
	white-space: pre-wrap;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,15 @@
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-documentation-database',
 | 
			
		||||
  templateUrl: './documentation-database.component.html',
 | 
			
		||||
  styleUrls: ['./documentation-database.component.scss']
 | 
			
		||||
	selector: 'app-documentation-database',
 | 
			
		||||
	templateUrl: './documentation-database.component.html',
 | 
			
		||||
	styleUrls: ['./documentation-database.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class DocumentationDatabaseComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
	constructor() { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
  }
 | 
			
		||||
	ngOnInit(): void {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,38 +1,38 @@
 | 
			
		||||
<h4>Model file upload</h4>
 | 
			
		||||
 | 
			
		||||
<p>
 | 
			
		||||
  Model files have to be saved as a .pkl file in 80_Modelle_BIC. <br>
 | 
			
		||||
  For upload the upload script has to be opened (can be in every environment). The name of the model file has to be
 | 
			
		||||
  entered wih the .pkl ending in the first command and without .pkl in the second.
 | 
			
		||||
Model files have to be saved as a .pkl file in 80_Modelle_BIC. <br>
 | 
			
		||||
For upload the upload script has to be opened (can be in every environment). The name of the model file has to be
 | 
			
		||||
entered wih the .pkl ending in the first command and without .pkl in the second.
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
<h4 class="space-above">Adding new model scripts</h4>
 | 
			
		||||
 | 
			
		||||
<p>
 | 
			
		||||
  Open Spyder in the base environment and open the model.py within.
 | 
			
		||||
Open Spyder in the base environment and open the model.py within.
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
<ul>
 | 
			
		||||
  <li>Enter model file name without .pkl in fetchData</li>
 | 
			
		||||
  <li>Add new prediction function below others</li>
 | 
			
		||||
  <li>duplicate another @app.route and rename according to desired route name and prediction function name</li>
 | 
			
		||||
	<li>Enter model file name without .pkl in fetchData</li>
 | 
			
		||||
	<li>Add new prediction function below others</li>
 | 
			
		||||
	<li>duplicate another @app.route and rename according to desired route name and prediction function name</li>
 | 
			
		||||
</ul>
 | 
			
		||||
 | 
			
		||||
<h4 class="space-above">Testing locally</h4>
 | 
			
		||||
 | 
			
		||||
<p>
 | 
			
		||||
  To test the model Python script locally, you need to execute it in the base environment, in which all necessary
 | 
			
		||||
  packages  have to be installed. Note that Spyder also has to be opened in the base environment.
 | 
			
		||||
To test the model Python script locally, you need to execute it in the base environment, in which all necessary
 | 
			
		||||
packages  have to be installed. Note that Spyder also has to be opened in the base environment.
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
<ul>
 | 
			
		||||
  <li>Open a separate Anaconda Prompt and cd into the definma-UI folder</li>
 | 
			
		||||
  <li>Adapt the model-mock.json at definma-UI/assets/ and enter the new model URL and name</li>
 | 
			
		||||
  <li>Execute "python -m http.server" in the Anaconda Prompt</li>
 | 
			
		||||
  <li>Execute the model.py script in Spyder</li>
 | 
			
		||||
  <li>Navigate to <a href="http://localhost:8000">http://localhost:8000</a> in the browser</li>
 | 
			
		||||
  <li>If there are problems with cached data, open the Browser developer console (Ctrl+Shift+I) and tick
 | 
			
		||||
    "Disable cache" in the Network tab. The console then has to stay open.</li>
 | 
			
		||||
	<li>Open a separate Anaconda Prompt and cd into the definma-UI folder</li>
 | 
			
		||||
	<li>Adapt the model-mock.json at definma-UI/assets/ and enter the new model URL and name</li>
 | 
			
		||||
	<li>Execute "python -m http.server" in the Anaconda Prompt</li>
 | 
			
		||||
	<li>Execute the model.py script in Spyder</li>
 | 
			
		||||
	<li>Navigate to <a href="http://localhost:8000">http://localhost:8000</a> in the browser</li>
 | 
			
		||||
	<li>If there are problems with cached data, open the Browser developer console (Ctrl+Shift+I) and tick
 | 
			
		||||
		"Disable cache" in the Network tab. The console then has to stay open.</li>
 | 
			
		||||
</ul>
 | 
			
		||||
 | 
			
		||||
<h4 class="space-above">Model script upload</h4>
 | 
			
		||||
@@ -40,17 +40,17 @@
 | 
			
		||||
In the Windows Powershell:
 | 
			
		||||
 | 
			
		||||
<ul>
 | 
			
		||||
  <li>cf login (only at the first time)</li>
 | 
			
		||||
  <li>API endpoint: https://api.sys.de1.bosch-iot-cloud.com</li>
 | 
			
		||||
  <li>Enter email with .de.bosch.com</li>
 | 
			
		||||
  <li>Enter BIC password</li>
 | 
			
		||||
  <li>Select space to upload to (currently 3 - development)</li>
 | 
			
		||||
  <li>cd into the folder containing the manifest.yaml (here is also model.py)</li>
 | 
			
		||||
  <li>cf push</li>
 | 
			
		||||
	<li>cf login (only at the first time)</li>
 | 
			
		||||
	<li>API endpoint: https://api.sys.de1.bosch-iot-cloud.com</li>
 | 
			
		||||
	<li>Enter email with .de.bosch.com</li>
 | 
			
		||||
	<li>Enter BIC password</li>
 | 
			
		||||
	<li>Select space to upload to (currently 3 - development)</li>
 | 
			
		||||
	<li>cd into the folder containing the manifest.yaml (here is also model.py)</li>
 | 
			
		||||
	<li>cf push</li>
 | 
			
		||||
</ul>
 | 
			
		||||
 | 
			
		||||
<p>
 | 
			
		||||
  After upload the new model details have to be entered in the UI.
 | 
			
		||||
After upload the new model details have to be entered in the UI.
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,15 @@
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-documentation-models',
 | 
			
		||||
  templateUrl: './documentation-models.component.html',
 | 
			
		||||
  styleUrls: ['./documentation-models.component.scss']
 | 
			
		||||
	selector: 'app-documentation-models',
 | 
			
		||||
	templateUrl: './documentation-models.component.html',
 | 
			
		||||
	styleUrls: ['./documentation-models.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class DocumentationModelsComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
	constructor() { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
  }
 | 
			
		||||
	ngOnInit(): void {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,78 +1,78 @@
 | 
			
		||||
<p>
 | 
			
		||||
  <a [href]="api.hostName + '/static/intro-presentation/index.html'">
 | 
			
		||||
    View the presentation explaining the main functions
 | 
			
		||||
  </a><br>
 | 
			
		||||
  View the <a href="/assets/docs/Veit-Lukas_T3000.pdf">T3000 report</a> and
 | 
			
		||||
  <a href="/assets/docs/Veit-Lukas_Bachelor-Thesis.pdf">Bachelor Thesis</a> for an in-depth application description.
 | 
			
		||||
<a [href]="api.hostName + '/static/intro-presentation/index.html'">
 | 
			
		||||
	View the presentation explaining the main functions
 | 
			
		||||
</a><br>
 | 
			
		||||
View the <a href="/assets/docs/Veit-Lukas_T3000.pdf">T3000 report</a> and
 | 
			
		||||
<a href="/assets/docs/Veit-Lukas_Bachelor-Thesis.pdf">Bachelor Thesis</a> for an in-depth application description.
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
<h4>User levels</h4>
 | 
			
		||||
 | 
			
		||||
<rb-table>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <th></th>
 | 
			
		||||
    <th>prediction</th>
 | 
			
		||||
    <th>read</th>
 | 
			
		||||
    <th>write</th>
 | 
			
		||||
    <th>dev</th>
 | 
			
		||||
    <th>admin</th>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <th>use prediction models</th>
 | 
			
		||||
    <td>specified ones</td>
 | 
			
		||||
    <td>specified ones</td>
 | 
			
		||||
    <td>specified ones</td>
 | 
			
		||||
    <td>all</td>
 | 
			
		||||
    <td>all</td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <th>read sample data</th>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <th>add samples/edit own</th>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <th>read spectral data</th>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <th>edit other's data</th>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <th>maintain templates</th>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <th>edit users</th>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
  </tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<th></th>
 | 
			
		||||
		<th>prediction</th>
 | 
			
		||||
		<th>read</th>
 | 
			
		||||
		<th>write</th>
 | 
			
		||||
		<th>dev</th>
 | 
			
		||||
		<th>admin</th>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<th>use prediction models</th>
 | 
			
		||||
		<td>specified ones</td>
 | 
			
		||||
		<td>specified ones</td>
 | 
			
		||||
		<td>specified ones</td>
 | 
			
		||||
		<td>all</td>
 | 
			
		||||
		<td>all</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<th>read sample data</th>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<th>add samples/edit own</th>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<th>read spectral data</th>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<th>edit other's data</th>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<th>maintain templates</th>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<th>edit users</th>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-abort-frame"></span></td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-checkmark-frame"></span></td>
 | 
			
		||||
	</tr>
 | 
			
		||||
</rb-table>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,17 @@
 | 
			
		||||
@import "~@inst-iot/bosch-angular-ui-components/styles/variables/colors";
 | 
			
		||||
 | 
			
		||||
p {
 | 
			
		||||
  margin-bottom: 20px;
 | 
			
		||||
	margin-bottom: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
img#db-structure {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
	width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.rb-ic-checkmark-frame {
 | 
			
		||||
  color: $brand-success;
 | 
			
		||||
	color: $brand-success;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.rb-ic-abort-frame {
 | 
			
		||||
  color: $brand-danger;
 | 
			
		||||
	color: $brand-danger;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,17 +3,17 @@ import {ApiService} from '../services/api.service';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-documentation',
 | 
			
		||||
  templateUrl: './documentation.component.html',
 | 
			
		||||
  styleUrls: ['./documentation.component.scss']
 | 
			
		||||
	selector: 'app-documentation',
 | 
			
		||||
	templateUrl: './documentation.component.html',
 | 
			
		||||
	styleUrls: ['./documentation.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class DocumentationComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    public api: ApiService
 | 
			
		||||
  ) { }
 | 
			
		||||
	constructor(
 | 
			
		||||
		public api: ApiService
 | 
			
		||||
	) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
  }
 | 
			
		||||
	ngOnInit(): void {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
<rb-alert alertTitle="Error" type="error" okBtnLabel="Discard">
 | 
			
		||||
  <div class="space-below">
 | 
			
		||||
    {{message}}
 | 
			
		||||
  </div>
 | 
			
		||||
  <div *ngIf="details.length">
 | 
			
		||||
    <a href="javascript:" class="rb-details-toggle" rbDetailsToggle #triggerDetails="rbDetailsToggle">Details</a>
 | 
			
		||||
    <div *ngIf="triggerDetails.open">
 | 
			
		||||
      <p *ngFor="let detail of details">{{detail}}</p>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
	<div class="space-below">
 | 
			
		||||
		{{message}}
 | 
			
		||||
	</div>
 | 
			
		||||
	<div *ngIf="details.length">
 | 
			
		||||
		<a href="javascript:" class="rb-details-toggle" rbDetailsToggle #triggerDetails="rbDetailsToggle">Details</a>
 | 
			
		||||
		<div *ngIf="triggerDetails.open">
 | 
			
		||||
			<p *ngFor="let detail of details">{{detail}}</p>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</rb-alert>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,18 @@
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-error',
 | 
			
		||||
  templateUrl: './error.component.html',
 | 
			
		||||
  styleUrls: ['./error.component.scss']
 | 
			
		||||
	selector: 'app-error',
 | 
			
		||||
	templateUrl: './error.component.html',
 | 
			
		||||
	styleUrls: ['./error.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class ErrorComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  message = '';            // Main error message
 | 
			
		||||
  details: string[] = [];  // Array of error detail paragraphs
 | 
			
		||||
	message = '';            // Main error message
 | 
			
		||||
	details: string[] = [];  // Array of error detail paragraphs
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
	constructor() { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
  }
 | 
			
		||||
	ngOnInit(): void {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,8 @@ import { ExistsPipe } from './exists.pipe';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
describe('ExistsPipe', () => {
 | 
			
		||||
  it('create an instance', () => {
 | 
			
		||||
    const pipe = new ExistsPipe();
 | 
			
		||||
    expect(pipe).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
	it('create an instance', () => {
 | 
			
		||||
		const pipe = new ExistsPipe();
 | 
			
		||||
		expect(pipe).toBeTruthy();
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,13 @@
 | 
			
		||||
import { Pipe, PipeTransform } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
@Pipe({
 | 
			
		||||
  name: 'exists',
 | 
			
		||||
  pure: true
 | 
			
		||||
	name: 'exists',
 | 
			
		||||
	pure: true
 | 
			
		||||
})
 | 
			
		||||
export class ExistsPipe implements PipeTransform {
 | 
			
		||||
 | 
			
		||||
  transform(value: unknown, key?): unknown {
 | 
			
		||||
    return value || value === 0 ? (key ? value[key] : value) : '';
 | 
			
		||||
  }
 | 
			
		||||
	transform(value: unknown, key?): unknown {
 | 
			
		||||
		return value || value === 0 ? (key ? value[key] : value) : '';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,25 +1,25 @@
 | 
			
		||||
<h3>Help
 | 
			
		||||
  <span *ngIf="login.isLevel.dev" class="rb-ic rb-ic-edit clickable space-left" (click)="edit = true"></span>
 | 
			
		||||
	<span *ngIf="login.isLevel.dev" class="rb-ic rb-ic-edit clickable space-left" (click)="edit = true"></span>
 | 
			
		||||
</h3>
 | 
			
		||||
 | 
			
		||||
<div *ngIf="edit; else normalView">
 | 
			
		||||
  <rb-form-select label="level" [(ngModel)]="content.level">
 | 
			
		||||
    <option value="none">none</option>
 | 
			
		||||
    <option *ngFor="let level of login.levels" [value]="level">{{level}}</option>
 | 
			
		||||
  </rb-form-select>
 | 
			
		||||
  <rb-form-textarea label="text" [(ngModel)]="content.text"></rb-form-textarea>
 | 
			
		||||
  <rb-icon-button icon="save" mode="primary" (click)="saveHelp()">Save</rb-icon-button>
 | 
			
		||||
  <rb-icon-button icon="delete" mode="danger" (click)="deleteHelp()" class="delete-btn">Delete</rb-icon-button>
 | 
			
		||||
	<rb-form-select label="level" [(ngModel)]="content.level">
 | 
			
		||||
		<option value="none">none</option>
 | 
			
		||||
		<option *ngFor="let level of login.levels" [value]="level">{{level}}</option>
 | 
			
		||||
	</rb-form-select>
 | 
			
		||||
	<rb-form-textarea label="text" [(ngModel)]="content.text"></rb-form-textarea>
 | 
			
		||||
	<rb-icon-button icon="save" mode="primary" (click)="saveHelp()">Save</rb-icon-button>
 | 
			
		||||
	<rb-icon-button icon="delete" mode="danger" (click)="deleteHelp()" class="delete-btn">Delete</rb-icon-button>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<ng-template #normalView>
 | 
			
		||||
  <p *ngIf="content.text; else defaultContent" class="content-text">
 | 
			
		||||
    {{content.text}}
 | 
			
		||||
  </p>
 | 
			
		||||
  <ng-template #defaultContent>
 | 
			
		||||
    <ng-container *ngIf="content.text === ''">
 | 
			
		||||
      Sadly, currently there is no help available for this page. Please contact
 | 
			
		||||
      <a [href]="'mailto:' + d.contact.mail">{{d.contact.name}}</a> for further questions.
 | 
			
		||||
    </ng-container>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
	<p *ngIf="content.text; else defaultContent" class="content-text">
 | 
			
		||||
	{{content.text}}
 | 
			
		||||
	</p>
 | 
			
		||||
	<ng-template #defaultContent>
 | 
			
		||||
		<ng-container *ngIf="content.text === ''">
 | 
			
		||||
			Sadly, currently there is no help available for this page. Please contact
 | 
			
		||||
			<a [href]="'mailto:' + d.contact.mail">{{d.contact.name}}</a> for further questions.
 | 
			
		||||
		</ng-container>
 | 
			
		||||
	</ng-template>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
.delete-btn {
 | 
			
		||||
  float: right;
 | 
			
		||||
	float: right;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.content-text {
 | 
			
		||||
  white-space: pre-line;
 | 
			
		||||
	white-space: pre-line;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,48 +6,48 @@ import {HelpModel} from '../models/help.model';
 | 
			
		||||
import {LoginService} from '../services/login.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-help',
 | 
			
		||||
  templateUrl: './help.component.html',
 | 
			
		||||
  styleUrls: ['./help.component.scss']
 | 
			
		||||
	selector: 'app-help',
 | 
			
		||||
	templateUrl: './help.component.html',
 | 
			
		||||
	styleUrls: ['./help.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class HelpComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  content: HelpModel = new HelpModel().deserialize({text: null, level: 'none'});  // Help content
 | 
			
		||||
  edit = false;        // Set true to change to edit mode
 | 
			
		||||
  private route = '';  // URIComponent encoded route which serves as a key to fetch the help document
 | 
			
		||||
	content: HelpModel = new HelpModel().deserialize({text: null, level: 'none'});  // Help content
 | 
			
		||||
	edit = false;        // Set true to change to edit mode
 | 
			
		||||
	private route = '';  // URIComponent encoded route which serves as a key to fetch the help document
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private router: Router,
 | 
			
		||||
    public d: DataService,
 | 
			
		||||
    private api: ApiService,
 | 
			
		||||
    public login: LoginService
 | 
			
		||||
  ) { }
 | 
			
		||||
	constructor(
 | 
			
		||||
		private router: Router,
 | 
			
		||||
		public d: DataService,
 | 
			
		||||
		private api: ApiService,
 | 
			
		||||
		public login: LoginService
 | 
			
		||||
	) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    // Remove ids from path
 | 
			
		||||
    this.route = encodeURIComponent(this.router.url.replace(/\/[0-9a-f]{24}/, ''));
 | 
			
		||||
    this.api.get<HelpModel>('/help/' + this.route, (data, err) => {
 | 
			
		||||
      if (!err) {  // Content was found
 | 
			
		||||
        this.content = new HelpModel().deserialize(data);
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        this.content.text = '';
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
	ngOnInit(): void {
 | 
			
		||||
		// Remove ids from path
 | 
			
		||||
		this.route = encodeURIComponent(this.router.url.replace(/\/[0-9a-f]{24}/, ''));
 | 
			
		||||
		this.api.get<HelpModel>('/help/' + this.route, (data, err) => {
 | 
			
		||||
			if (!err) {  // Content was found
 | 
			
		||||
				this.content = new HelpModel().deserialize(data);
 | 
			
		||||
			}
 | 
			
		||||
			else {
 | 
			
		||||
				this.content.text = '';
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  saveHelp() {
 | 
			
		||||
    this.api.post('/help/' + this.route, this.content.sendFormat(), () => {
 | 
			
		||||
      this.edit = false;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
	saveHelp() {
 | 
			
		||||
		this.api.post('/help/' + this.route, this.content.sendFormat(), () => {
 | 
			
		||||
			this.edit = false;
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  deleteHelp() {
 | 
			
		||||
    this.api.delete('/help/' + this.route, (ignore, err) => {
 | 
			
		||||
      if (!err) {
 | 
			
		||||
        this.content = new HelpModel().deserialize({text: null, level: 'none'});
 | 
			
		||||
        this.edit = false;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
	deleteHelp() {
 | 
			
		||||
		this.api.delete('/help/' + this.route, (ignore, err) => {
 | 
			
		||||
			if (!err) {
 | 
			
		||||
				this.content = new HelpModel().deserialize({text: null, level: 'none'});
 | 
			
		||||
				this.edit = false;
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,20 @@
 | 
			
		||||
<div *ngIf="!login.isLoggedIn">
 | 
			
		||||
    <app-login></app-login>
 | 
			
		||||
    <img src="/assets/imgs/key-visual.png" alt="" class="key-visual">
 | 
			
		||||
	<app-login></app-login>
 | 
			
		||||
	<img src="/assets/imgs/key-visual.png" alt="" class="key-visual">
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<div *ngIf="login.isLoggedIn">
 | 
			
		||||
    <rb-form-multi-select name="groupSelect" idField="id" [items]="keys" [(ngModel)]="isActiveKey" label="Groups" class="selection">
 | 
			
		||||
        <span *rbFormMultiSelectOption="let key">{{key.id}}</span>
 | 
			
		||||
	<rb-form-multi-select name="groupSelect" idField="id" [items]="keys" [(ngModel)]="isActiveKey" label="Groups" class="selection">
 | 
			
		||||
		<span *rbFormMultiSelectOption="let key">{{key.id}}</span>
 | 
			
		||||
 | 
			
		||||
    </rb-form-multi-select> 
 | 
			
		||||
    <rb-icon-button icon="forward-right" mode="primary" (click)="updateGroups(isActiveKey)">
 | 
			
		||||
        Apply groups 
 | 
			
		||||
    </rb-icon-button>
 | 
			
		||||
	</rb-form-multi-select> 
 | 
			
		||||
	<rb-icon-button icon="forward-right" mode="primary" (click)="updateGroups(isActiveKey)">
 | 
			
		||||
		Apply groups 
 | 
			
		||||
	</rb-icon-button>
 | 
			
		||||
 | 
			
		||||
    <div id="divChart">
 | 
			
		||||
        <canvas id="myChart">
 | 
			
		||||
        </canvas>
 | 
			
		||||
    </div>
 | 
			
		||||
	<div id="divChart">
 | 
			
		||||
		<canvas id="myChart">
 | 
			
		||||
		</canvas>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,17 @@
 | 
			
		||||
app-login {
 | 
			
		||||
  float: left;
 | 
			
		||||
	float: left;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.key-visual {
 | 
			
		||||
  width: 70%;
 | 
			
		||||
  float: right;
 | 
			
		||||
	width: 70%;
 | 
			
		||||
	float: right;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.selection{
 | 
			
		||||
  float: left;
 | 
			
		||||
  width: 20%;
 | 
			
		||||
	float: left;
 | 
			
		||||
	width: 20%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
rb-form-multi-select {
 | 
			
		||||
  width: 20%;
 | 
			
		||||
	width: 20%;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,138 +5,138 @@ import { Chart } from 'chart.js';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
interface KeyInterface {
 | 
			
		||||
  id: string;
 | 
			
		||||
  count: number;
 | 
			
		||||
  active: boolean;
 | 
			
		||||
	id: string;
 | 
			
		||||
	count: number;
 | 
			
		||||
	active: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-home',
 | 
			
		||||
  templateUrl: './home.component.html',
 | 
			
		||||
  styleUrls: ['./home.component.scss']
 | 
			
		||||
	selector: 'app-home',
 | 
			
		||||
	templateUrl: './home.component.html',
 | 
			
		||||
	styleUrls: ['./home.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class HomeComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  keys: KeyInterface[] = [];
 | 
			
		||||
  isActiveKey: { [key: string]: boolean } = {};  // Object to check if key is currently active
 | 
			
		||||
  myChart: Chart;
 | 
			
		||||
	keys: KeyInterface[] = [];
 | 
			
		||||
	isActiveKey: { [key: string]: boolean } = {};  // Object to check if key is currently active
 | 
			
		||||
		myChart: Chart;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    public login: LoginService,
 | 
			
		||||
    public api: ApiService
 | 
			
		||||
  ) { }
 | 
			
		||||
	constructor(
 | 
			
		||||
		public login: LoginService,
 | 
			
		||||
		public api: ApiService
 | 
			
		||||
	) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    // Fetch all available groups  
 | 
			
		||||
    this.fetchData('/material/groups', data => this.createGroup(data));
 | 
			
		||||
  }
 | 
			
		||||
	ngOnInit() {
 | 
			
		||||
		// Fetch all available groups  
 | 
			
		||||
		this.fetchData('/material/groups', data => this.createGroup(data));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  // Api access with callback
 | 
			
		||||
  async fetchData(URL: string, processor: any) {
 | 
			
		||||
    this.api.get(URL, (sData, err, headers) => {
 | 
			
		||||
      processor(sData);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
	// Api access with callback
 | 
			
		||||
	async fetchData(URL: string, processor: any) {
 | 
			
		||||
		this.api.get(URL, (sData, err, headers) => {
 | 
			
		||||
			processor(sData);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  // Fill interface with data
 | 
			
		||||
  createGroup(data: any) {
 | 
			
		||||
    let temp: KeyInterface[] = [];
 | 
			
		||||
	// Fill interface with data
 | 
			
		||||
	createGroup(data: any) {
 | 
			
		||||
		let temp: KeyInterface[] = [];
 | 
			
		||||
 | 
			
		||||
    for (var i = 0; i < data.length; i++) {
 | 
			
		||||
      temp.push({ id: data[i], count: 0, active: false });
 | 
			
		||||
    }
 | 
			
		||||
    this.keys = temp; // Invoke update in rb-multiselect
 | 
			
		||||
    this.initChart();
 | 
			
		||||
		for (var i = 0; i < data.length; i++) {
 | 
			
		||||
			temp.push({ id: data[i], count: 0, active: false });
 | 
			
		||||
		}
 | 
			
		||||
		this.keys = temp; // Invoke update in rb-multiselect
 | 
			
		||||
		this.initChart();
 | 
			
		||||
 | 
			
		||||
    // Only neccesary if keys get preselected
 | 
			
		||||
    //this.calcFieldSelectKeys();
 | 
			
		||||
		// Only neccesary if keys get preselected
 | 
			
		||||
		//this.calcFieldSelectKeys();
 | 
			
		||||
 | 
			
		||||
    // Fetch all samples populated with according group
 | 
			
		||||
    this.getSamples();
 | 
			
		||||
  }
 | 
			
		||||
		// Fetch all samples populated with according group
 | 
			
		||||
		this.getSamples();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  // Iterate through active keys to assemble an api request for the required data
 | 
			
		||||
  getSamples() {
 | 
			
		||||
    let query = '/samples?status%5B%5D=validated&status=new&filters%5B%5D=%7B%22mode%22%3A%22in%22%2C%22field%22%3A%22material.group%22%2C%22values%22%3A%5B';
 | 
			
		||||
    let temp = '';
 | 
			
		||||
    this.keys.forEach(key => {
 | 
			
		||||
      temp += key.active ? '%22' + key.id.split("%").join("%25") + '%22%2C' : ""; // Replace split().join() with replaceAll() 
 | 
			
		||||
    });
 | 
			
		||||
    if (temp === '') {
 | 
			
		||||
      this.countSamples('');
 | 
			
		||||
    } else {
 | 
			
		||||
      query = query + temp.substr(0, temp.length - 3) + '%5D%7D&fields%5B%5D=material.group';
 | 
			
		||||
      this.fetchData(query, data => this.countSamples(data));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
	// Iterate through active keys to assemble an api request for the required data
 | 
			
		||||
	getSamples() {
 | 
			
		||||
		let query = '/samples?status%5B%5D=validated&status=new&filters%5B%5D=%7B%22mode%22%3A%22in%22%2C%22field%22%3A%22material.group%22%2C%22values%22%3A%5B';
 | 
			
		||||
		let temp = '';
 | 
			
		||||
		this.keys.forEach(key => {
 | 
			
		||||
			temp += key.active ? '%22' + key.id.split("%").join("%25") + '%22%2C' : ""; // Replace split().join() with replaceAll() 
 | 
			
		||||
		});
 | 
			
		||||
		if (temp === '') {
 | 
			
		||||
			this.countSamples('');
 | 
			
		||||
		} else {
 | 
			
		||||
			query = query + temp.substr(0, temp.length - 3) + '%5D%7D&fields%5B%5D=material.group';
 | 
			
		||||
			this.fetchData(query, data => this.countSamples(data));
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  // Loop through samples and count
 | 
			
		||||
  countSamples(data: any) {
 | 
			
		||||
    this.keys.map(key => key.count = 0);
 | 
			
		||||
    for (var i = 0; i < data.length; i++) {
 | 
			
		||||
      this.keys.forEach(key => {
 | 
			
		||||
        if (key.id === data[i].material.group) {
 | 
			
		||||
          key.count += 1;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    this.updateGraph();
 | 
			
		||||
  }
 | 
			
		||||
	// Loop through samples and count
 | 
			
		||||
	countSamples(data: any) {
 | 
			
		||||
		this.keys.map(key => key.count = 0);
 | 
			
		||||
		for (var i = 0; i < data.length; i++) {
 | 
			
		||||
			this.keys.forEach(key => {
 | 
			
		||||
				if (key.id === data[i].material.group) {
 | 
			
		||||
					key.count += 1;
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
		this.updateGraph();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  // Preset select
 | 
			
		||||
  calcFieldSelectKeys() {
 | 
			
		||||
    this.keys.forEach(key => {
 | 
			
		||||
      this.isActiveKey[key.id] = key.active;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
	// Preset select
 | 
			
		||||
	calcFieldSelectKeys() {
 | 
			
		||||
		this.keys.forEach(key => {
 | 
			
		||||
			this.isActiveKey[key.id] = key.active;
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  // Update keys based on select
 | 
			
		||||
  updateGroups(activeKeys: any) {
 | 
			
		||||
    this.keys.forEach(key => {
 | 
			
		||||
      if (activeKeys.hasOwnProperty(key.id)) {
 | 
			
		||||
        key.active = activeKeys[key.id];
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    this.getSamples();
 | 
			
		||||
  }
 | 
			
		||||
	// Update keys based on select
 | 
			
		||||
	updateGroups(activeKeys: any) {
 | 
			
		||||
		this.keys.forEach(key => {
 | 
			
		||||
			if (activeKeys.hasOwnProperty(key.id)) {
 | 
			
		||||
				key.active = activeKeys[key.id];
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
		this.getSamples();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  // Get data for graph based on active keys
 | 
			
		||||
  updateGraph() {
 | 
			
		||||
    let nameList: string[] = [];
 | 
			
		||||
    let dataList: number[] = [];
 | 
			
		||||
	// Get data for graph based on active keys
 | 
			
		||||
	updateGraph() {
 | 
			
		||||
		let nameList: string[] = [];
 | 
			
		||||
		let dataList: number[] = [];
 | 
			
		||||
 | 
			
		||||
    this.keys.forEach(key => {
 | 
			
		||||
      if (key.active) {
 | 
			
		||||
        nameList.push(key.id);
 | 
			
		||||
        dataList.push(key.count);
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    this.myChart.data.labels = nameList;
 | 
			
		||||
    this.myChart.data.datasets[0].data = dataList;
 | 
			
		||||
    this.myChart.update();
 | 
			
		||||
  }
 | 
			
		||||
		this.keys.forEach(key => {
 | 
			
		||||
			if (key.active) {
 | 
			
		||||
				nameList.push(key.id);
 | 
			
		||||
				dataList.push(key.count);
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
		this.myChart.data.labels = nameList;
 | 
			
		||||
		this.myChart.data.datasets[0].data = dataList;
 | 
			
		||||
		this.myChart.update();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  // Initialize graph
 | 
			
		||||
  async initChart() {
 | 
			
		||||
    this.myChart = new Chart("myChart", {
 | 
			
		||||
      type: 'bar',
 | 
			
		||||
      data: {
 | 
			
		||||
        labels: [],
 | 
			
		||||
        datasets: [{
 | 
			
		||||
          label: 'Number of samples per group',
 | 
			
		||||
          data: [],
 | 
			
		||||
          backgroundColor: 'rgba(0, 86, 145, 1)'
 | 
			
		||||
        }]
 | 
			
		||||
      },
 | 
			
		||||
      options: {
 | 
			
		||||
        scales: {
 | 
			
		||||
          yAxes: [{
 | 
			
		||||
            ticks: {
 | 
			
		||||
              beginAtZero: true
 | 
			
		||||
            }
 | 
			
		||||
          }]
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
	// Initialize graph
 | 
			
		||||
	async initChart() {
 | 
			
		||||
		this.myChart = new Chart("myChart", {
 | 
			
		||||
			type: 'bar',
 | 
			
		||||
			data: {
 | 
			
		||||
				labels: [],
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'Number of samples per group',
 | 
			
		||||
					data: [],
 | 
			
		||||
					backgroundColor: 'rgba(0, 86, 145, 1)'
 | 
			
		||||
				}]
 | 
			
		||||
			},
 | 
			
		||||
			options: {
 | 
			
		||||
				scales: {
 | 
			
		||||
					yAxes: [{
 | 
			
		||||
						ticks: {
 | 
			
		||||
							beginAtZero: true
 | 
			
		||||
						}
 | 
			
		||||
					}]
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,17 @@
 | 
			
		||||
<div class="img-container">
 | 
			
		||||
  <div class="magnifier"
 | 
			
		||||
       [ngStyle]="{
 | 
			
		||||
         'background-size': backgroundSize,
 | 
			
		||||
         'background-image': 'url(\'' + src + '\')',
 | 
			
		||||
         'background-position':  '-' + magnifierPos.x * zoom + 'px -' + magnifierPos.y * zoom + 'px',
 | 
			
		||||
         left: magnifierPos.x + 'px',
 | 
			
		||||
         top: magnifierPos.y + 'px',
 | 
			
		||||
         width: magnifierSize.width + 'px',
 | 
			
		||||
         height: magnifierSize.height + 'px'
 | 
			
		||||
       }"
 | 
			
		||||
       (mousemove)="calcPos($event)"
 | 
			
		||||
       (mouseleave)="showMagnifier = false"
 | 
			
		||||
       *ngIf="showMagnifier">
 | 
			
		||||
  </div>
 | 
			
		||||
  <img [src]="src" alt="" (mousemove)="calcPos($event)" (mouseenter)="showMagnifier = true" #mainImg>
 | 
			
		||||
	<div class="magnifier"
 | 
			
		||||
		[ngStyle]="{
 | 
			
		||||
		'background-size': backgroundSize,
 | 
			
		||||
		'background-image': 'url(\'' + src + '\')',
 | 
			
		||||
		'background-position':  '-' + magnifierPos.x * zoom + 'px -' + magnifierPos.y * zoom + 'px',
 | 
			
		||||
		left: magnifierPos.x + 'px',
 | 
			
		||||
		top: magnifierPos.y + 'px',
 | 
			
		||||
		width: magnifierSize.width + 'px',
 | 
			
		||||
		height: magnifierSize.height + 'px'
 | 
			
		||||
		}"
 | 
			
		||||
		(mousemove)="calcPos($event)"
 | 
			
		||||
		(mouseleave)="showMagnifier = false"
 | 
			
		||||
		*ngIf="showMagnifier">
 | 
			
		||||
	</div>
 | 
			
		||||
	<img [src]="src" alt="" (mousemove)="calcPos($event)" (mouseenter)="showMagnifier = true" #mainImg>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,18 @@
 | 
			
		||||
@import "~@inst-iot/bosch-angular-ui-components/styles/variables/colors";
 | 
			
		||||
 | 
			
		||||
.img-container {
 | 
			
		||||
  position:relative;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
	position:relative;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
 | 
			
		||||
  & > img {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
	& > img {
 | 
			
		||||
		width: 100%;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.magnifier {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  background: #FFF no-repeat -500px -500px;
 | 
			
		||||
  z-index: 99;
 | 
			
		||||
  border: 1px solid #FFF;
 | 
			
		||||
  box-shadow: 10px 10px 25px $color-bosch-light-gray-b25;
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	background: #FFF no-repeat -500px -500px;
 | 
			
		||||
	z-index: 99;
 | 
			
		||||
	border: 1px solid #FFF;
 | 
			
		||||
	box-shadow: 10px 10px 25px $color-bosch-light-gray-b25;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,48 +1,48 @@
 | 
			
		||||
import {AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-img-magnifier',
 | 
			
		||||
  templateUrl: './img-magnifier.component.html',
 | 
			
		||||
  styleUrls: ['./img-magnifier.component.scss']
 | 
			
		||||
	selector: 'app-img-magnifier',
 | 
			
		||||
	templateUrl: './img-magnifier.component.html',
 | 
			
		||||
	styleUrls: ['./img-magnifier.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class ImgMagnifierComponent implements OnInit, AfterViewInit {
 | 
			
		||||
 | 
			
		||||
  @Input() src: string;   // Image source
 | 
			
		||||
  @Input() zoom: number;  // Zoom level
 | 
			
		||||
  @Input() magnifierSize: {width: number, height: number};  // Size of the magnifier
 | 
			
		||||
  @ViewChild('mainImg') mainImg: ElementRef;
 | 
			
		||||
	@Input() src: string;   // Image source
 | 
			
		||||
	@Input() zoom: number;  // Zoom level
 | 
			
		||||
	@Input() magnifierSize: {width: number, height: number};  // Size of the magnifier
 | 
			
		||||
	@ViewChild('mainImg') mainImg: ElementRef;
 | 
			
		||||
 | 
			
		||||
  backgroundSize;
 | 
			
		||||
  magnifierPos = {x: 0, y: 0};  // Position of the magnifier
 | 
			
		||||
  showMagnifier = false;
 | 
			
		||||
	backgroundSize;
 | 
			
		||||
	magnifierPos = {x: 0, y: 0};  // Position of the magnifier
 | 
			
		||||
	showMagnifier = false;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private window: Window
 | 
			
		||||
  ) { }
 | 
			
		||||
	constructor(
 | 
			
		||||
		private window: Window
 | 
			
		||||
	) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
  }
 | 
			
		||||
	ngOnInit(): void {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  ngAfterViewInit() {
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      this.calcBackgroundSize();
 | 
			
		||||
    }, 1);
 | 
			
		||||
  }
 | 
			
		||||
	ngAfterViewInit() {
 | 
			
		||||
		setTimeout(() => {
 | 
			
		||||
			this.calcBackgroundSize();
 | 
			
		||||
		}, 1);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  calcPos(event) {  // Calculate the current magnifier position
 | 
			
		||||
    const img = this.mainImg.nativeElement.getBoundingClientRect();
 | 
			
		||||
    this.magnifierPos.x = Math.min(
 | 
			
		||||
      img.width - this.magnifierSize.width,
 | 
			
		||||
      Math.max(0, event.pageX - img.left - this.window.pageXOffset - this.magnifierSize.width / 2)
 | 
			
		||||
    );
 | 
			
		||||
    this.magnifierPos.y = Math.min(
 | 
			
		||||
      img.height - this.magnifierSize.height + 7,
 | 
			
		||||
      Math.max(0, event.pageY - img.top - this.window.pageYOffset - this.magnifierSize.height / 2)
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
	calcPos(event) {  // Calculate the current magnifier position
 | 
			
		||||
		const img = this.mainImg.nativeElement.getBoundingClientRect();
 | 
			
		||||
		this.magnifierPos.x = Math.min(
 | 
			
		||||
			img.width - this.magnifierSize.width,
 | 
			
		||||
			Math.max(0, event.pageX - img.left - this.window.pageXOffset - this.magnifierSize.width / 2)
 | 
			
		||||
		);
 | 
			
		||||
		this.magnifierPos.y = Math.min(
 | 
			
		||||
			img.height - this.magnifierSize.height + 7,
 | 
			
		||||
			Math.max(0, event.pageY - img.top - this.window.pageYOffset - this.magnifierSize.height / 2)
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  calcBackgroundSize() {
 | 
			
		||||
    this.backgroundSize = this.mainImg ? (this.mainImg.nativeElement.width * this.zoom - this.magnifierSize.width) +
 | 
			
		||||
      'px ' + (this.mainImg.nativeElement.height * this.zoom - this.magnifierSize.height) + 'px ' : '0 0';
 | 
			
		||||
  }
 | 
			
		||||
	calcBackgroundSize() {
 | 
			
		||||
		this.backgroundSize = this.mainImg ? (this.mainImg.nativeElement.width * this.zoom - this.magnifierSize.width) +
 | 
			
		||||
			'px ' + (this.mainImg.nativeElement.height * this.zoom - this.magnifierSize.height) + 'px ' : '0 0';
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,25 +1,25 @@
 | 
			
		||||
<div class="login-wrapper">
 | 
			
		||||
  <h2>Please log in</h2>
 | 
			
		||||
	<h2>Please log in</h2>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  <form #loginForm="ngForm">
 | 
			
		||||
    <rb-form-input name="username" label="username" appValidate="username" required [(ngModel)]="username"
 | 
			
		||||
                   #usernameInput="ngModel">
 | 
			
		||||
      <ng-template rbFormValidationMessage="failure">{{usernameInput.errors.failure}}</ng-template>
 | 
			
		||||
    </rb-form-input>
 | 
			
		||||
    <rb-form-input *ngIf="!passreset" type="password" name="password" label="password" appValidate="password" required
 | 
			
		||||
                   [(ngModel)]="password" #passwordInput="ngModel">
 | 
			
		||||
      <ng-template rbFormValidationMessage="failure">{{passwordInput.errors.failure}}</ng-template>
 | 
			
		||||
    </rb-form-input>
 | 
			
		||||
    <rb-form-input *ngIf="passreset" type="email" name="email" label="email" email required [(ngModel)]="email"
 | 
			
		||||
                   #emailInput="ngModel">
 | 
			
		||||
      <ng-template rbFormValidationMessage="failure">{{emailInput.errors.failure}}</ng-template>
 | 
			
		||||
    </rb-form-input>
 | 
			
		||||
    <a href="#" class="forgot-pass" (click)="passreset = !passreset">Forgot password</a>
 | 
			
		||||
    <button class="rb-btn rb-primary login-button" (click)="userLogin()" type="submit"
 | 
			
		||||
            [disabled]="!loginForm.form.valid">
 | 
			
		||||
      {{passreset ? 'Send' : 'Login'}}
 | 
			
		||||
    </button>
 | 
			
		||||
    <div class="message">{{message}}</div>
 | 
			
		||||
  </form>
 | 
			
		||||
	<form #loginForm="ngForm">
 | 
			
		||||
		<rb-form-input name="username" label="username" appValidate="username" required [(ngModel)]="username"
 | 
			
		||||
			#usernameInput="ngModel">
 | 
			
		||||
			<ng-template rbFormValidationMessage="failure">{{usernameInput.errors.failure}}</ng-template>
 | 
			
		||||
		</rb-form-input>
 | 
			
		||||
		<rb-form-input *ngIf="!passreset" type="password" name="password" label="password" appValidate="password" required
 | 
			
		||||
			[(ngModel)]="password" #passwordInput="ngModel">
 | 
			
		||||
			<ng-template rbFormValidationMessage="failure">{{passwordInput.errors.failure}}</ng-template>
 | 
			
		||||
		</rb-form-input>
 | 
			
		||||
		<rb-form-input *ngIf="passreset" type="email" name="email" label="email" email required [(ngModel)]="email"
 | 
			
		||||
			#emailInput="ngModel">
 | 
			
		||||
			<ng-template rbFormValidationMessage="failure">{{emailInput.errors.failure}}</ng-template>
 | 
			
		||||
		</rb-form-input>
 | 
			
		||||
		<a href="#" class="forgot-pass" (click)="passreset = !passreset">Forgot password</a>
 | 
			
		||||
		<button class="rb-btn rb-primary login-button" (click)="userLogin()" type="submit"
 | 
			
		||||
			[disabled]="!loginForm.form.valid">
 | 
			
		||||
			{{passreset ? 'Send' : 'Login'}}
 | 
			
		||||
		</button>
 | 
			
		||||
		<div class="message">{{message}}</div>
 | 
			
		||||
	</form>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,17 @@
 | 
			
		||||
.login-wrapper {
 | 
			
		||||
  max-width: 250px;
 | 
			
		||||
	max-width: 250px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.message {
 | 
			
		||||
  font-size: 13px;
 | 
			
		||||
  margin-top: 10px;
 | 
			
		||||
	font-size: 13px;
 | 
			
		||||
	margin-top: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.login-button {
 | 
			
		||||
  margin-right: 10px;
 | 
			
		||||
	margin-right: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.forgot-pass {
 | 
			
		||||
  display: block;
 | 
			
		||||
  margin-bottom: 1rem;
 | 
			
		||||
	display: block;
 | 
			
		||||
	margin-bottom: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,57 +6,57 @@ import {ApiService} from '../services/api.service';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-login',
 | 
			
		||||
  templateUrl: './login.component.html',
 | 
			
		||||
  styleUrls: ['./login.component.scss']
 | 
			
		||||
	selector: 'app-login',
 | 
			
		||||
	templateUrl: './login.component.html',
 | 
			
		||||
	styleUrls: ['./login.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class LoginComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  username = '';  // Credentials
 | 
			
		||||
  password = '';
 | 
			
		||||
  email = '';
 | 
			
		||||
  message = '';  // Message below login fields
 | 
			
		||||
  passreset = false;  // To toggle between normal login and password reset form
 | 
			
		||||
	username = '';  // Credentials
 | 
			
		||||
	password = '';
 | 
			
		||||
	email = '';
 | 
			
		||||
	message = '';  // Message below login fields
 | 
			
		||||
	passreset = false;  // To toggle between normal login and password reset form
 | 
			
		||||
 | 
			
		||||
  @ViewChild('loginForm') loginForm;
 | 
			
		||||
	@ViewChild('loginForm') loginForm;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private validate: ValidationService,
 | 
			
		||||
    private login: LoginService,
 | 
			
		||||
    private api: ApiService,
 | 
			
		||||
    private router: Router
 | 
			
		||||
  ) { }
 | 
			
		||||
	constructor(
 | 
			
		||||
		private validate: ValidationService,
 | 
			
		||||
		private login: LoginService,
 | 
			
		||||
		private api: ApiService,
 | 
			
		||||
		private router: Router
 | 
			
		||||
	) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
  }
 | 
			
		||||
	ngOnInit() {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  userLogin() {
 | 
			
		||||
    if (this.passreset) {  // Reset password
 | 
			
		||||
      this.api.post('/user/passreset', {name: this.username, email: this.email}, (data, err) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          this.message = 'Could not find a valid user';
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
          this.message = 'Password reset, check your inbox';
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      this.login.login(this.username, this.password).then(ok => {
 | 
			
		||||
        if (ok) {
 | 
			
		||||
          this.message = 'Login successful';
 | 
			
		||||
          if (this.login.isLevel.read) {
 | 
			
		||||
            this.router.navigate(['/samples']);
 | 
			
		||||
          }
 | 
			
		||||
          else {  // Navigate prediction users to prediction as they cannot access samples
 | 
			
		||||
            this.router.navigate(['/prediction']);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
          this.message = 'Wrong credentials!';
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
	userLogin() {
 | 
			
		||||
		if (this.passreset) {  // Reset password
 | 
			
		||||
			this.api.post('/user/passreset', {name: this.username, email: this.email}, (data, err) => {
 | 
			
		||||
				if (err) {
 | 
			
		||||
					this.message = 'Could not find a valid user';
 | 
			
		||||
				}
 | 
			
		||||
				else {
 | 
			
		||||
					this.message = 'Password reset, check your inbox';
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
			this.login.login(this.username, this.password).then(ok => {
 | 
			
		||||
				if (ok) {
 | 
			
		||||
					this.message = 'Login successful';
 | 
			
		||||
					if (this.login.isLevel.read) {
 | 
			
		||||
						this.router.navigate(['/samples']);
 | 
			
		||||
					}
 | 
			
		||||
					else {  // Navigate prediction users to prediction as they cannot access samples
 | 
			
		||||
						this.router.navigate(['/prediction']);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				else {
 | 
			
		||||
					this.message = 'Wrong credentials!';
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,65 +1,65 @@
 | 
			
		||||
<h2>{{material.name | exists}}</h2>
 | 
			
		||||
 | 
			
		||||
<form #materialForm="ngForm" *ngIf="!loading">
 | 
			
		||||
  <rb-form-input name="materialname" label="product name" appValidate="stringNin" [appValidateArgs]="[materialNames]"
 | 
			
		||||
                 required [(ngModel)]="material.name" #materialnameInput="ngModel">
 | 
			
		||||
    <ng-template rbFormValidationMessage="failure">{{materialnameInput.errors.failure}}</ng-template>
 | 
			
		||||
  </rb-form-input>
 | 
			
		||||
  <rb-form-input name="supplier" label="supplier"
 | 
			
		||||
                 [rbFormInputAutocomplete]="autocomplete.bind(this, d.arr.materialSuppliers)"
 | 
			
		||||
                 [rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required
 | 
			
		||||
                 [(ngModel)]="material.supplier" #supplierInput="ngModel"
 | 
			
		||||
                 (focusout)="checkTypo($event, 'materialSuppliers', 'supplier', modalWarning)"
 | 
			
		||||
                 title="material supplier, eg. BASF">
 | 
			
		||||
    <ng-template rbFormValidationMessage="failure">{{supplierInput.errors.failure}}</ng-template>
 | 
			
		||||
  </rb-form-input>
 | 
			
		||||
  <rb-form-input name="group" label="group"
 | 
			
		||||
                 [rbFormInputAutocomplete]="autocomplete.bind(this, d.arr.materialGroups)"
 | 
			
		||||
                 [rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required
 | 
			
		||||
                 [(ngModel)]="material.group" #groupInput="ngModel"
 | 
			
		||||
                 (focusout)="checkTypo($event, 'materialGroups', 'group', modalWarning)"
 | 
			
		||||
                 title="chemical material type, eg. PA66">
 | 
			
		||||
    <ng-template rbFormValidationMessage="failure">{{groupInput.errors.failure}}</ng-template>
 | 
			
		||||
  </rb-form-input>
 | 
			
		||||
	<rb-form-input name="materialname" label="product name" appValidate="stringNin" [appValidateArgs]="[materialNames]"
 | 
			
		||||
		required [(ngModel)]="material.name" #materialnameInput="ngModel">
 | 
			
		||||
		<ng-template rbFormValidationMessage="failure">{{materialnameInput.errors.failure}}</ng-template>
 | 
			
		||||
	</rb-form-input>
 | 
			
		||||
	<rb-form-input name="supplier" label="supplier"
 | 
			
		||||
		[rbFormInputAutocomplete]="autocomplete.bind(this, d.arr.materialSuppliers)"
 | 
			
		||||
		[rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required
 | 
			
		||||
		[(ngModel)]="material.supplier" #supplierInput="ngModel"
 | 
			
		||||
		(focusout)="checkTypo($event, 'materialSuppliers', 'supplier', modalWarning)"
 | 
			
		||||
		title="material supplier, eg. BASF">
 | 
			
		||||
		<ng-template rbFormValidationMessage="failure">{{supplierInput.errors.failure}}</ng-template>
 | 
			
		||||
	</rb-form-input>
 | 
			
		||||
	<rb-form-input name="group" label="group"
 | 
			
		||||
		[rbFormInputAutocomplete]="autocomplete.bind(this, d.arr.materialGroups)"
 | 
			
		||||
		[rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required
 | 
			
		||||
		[(ngModel)]="material.group" #groupInput="ngModel"
 | 
			
		||||
		(focusout)="checkTypo($event, 'materialGroups', 'group', modalWarning)"
 | 
			
		||||
		title="chemical material type, eg. PA66">
 | 
			
		||||
		<ng-template rbFormValidationMessage="failure">{{groupInput.errors.failure}}</ng-template>
 | 
			
		||||
	</rb-form-input>
 | 
			
		||||
 | 
			
		||||
  <ng-template #modalWarning>
 | 
			
		||||
    <rb-alert alertTitle="Warning" type="warning" okBtnLabel="Use suggestion" cancelBtnLabel="Keep value">
 | 
			
		||||
      The specified {{modalText.list}} could not be found in the list. <br>
 | 
			
		||||
      Did you mean {{modalText.suggestion}}?
 | 
			
		||||
    </rb-alert>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
  <rb-array-input [(ngModel)]="material.numbers" name="materialNumbers" [pushTemplate]="''">
 | 
			
		||||
    <rb-form-input *rbArrayInputItem="let item" [rbArrayInputListener]="'materialNumber'" [index]="item.i"
 | 
			
		||||
                   label="material number" appValidate="string" [name]="'materialNumber-' + item.i"
 | 
			
		||||
                   [ngModel]="item.value" title="Bosch material part number, eg. 5515753021"></rb-form-input>
 | 
			
		||||
  </rb-array-input>
 | 
			
		||||
  <rb-form-select name="propertiesSelect" label="Type" title="=overall material group specific properties"
 | 
			
		||||
                  [(ngModel)]="material.properties.material_template">
 | 
			
		||||
    <option *ngFor="let m of d.latest.materialTemplates" [value]="m._id">{{m.name}}</option>
 | 
			
		||||
  </rb-form-select>
 | 
			
		||||
  <rb-form-input *ngFor="let parameter of
 | 
			
		||||
                           d.id.materialTemplates[material.properties.material_template].parameters;
 | 
			
		||||
                         index as i" [name]="'materialParameter' + i"
 | 
			
		||||
                 [label]="parameter.name" appValidate="string" required
 | 
			
		||||
                 [(ngModel)]="material.properties[parameter.name]" #parameterInput="ngModel">
 | 
			
		||||
    <ng-template rbFormValidationMessage="failure">{{parameterInput.errors.failure}}</ng-template>
 | 
			
		||||
    <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
  </rb-form-input>
 | 
			
		||||
	<ng-template #modalWarning>
 | 
			
		||||
		<rb-alert alertTitle="Warning" type="warning" okBtnLabel="Use suggestion" cancelBtnLabel="Keep value">
 | 
			
		||||
			The specified {{modalText.list}} could not be found in the list. <br>
 | 
			
		||||
			Did you mean {{modalText.suggestion}}?
 | 
			
		||||
		</rb-alert>
 | 
			
		||||
	</ng-template>
 | 
			
		||||
	<rb-array-input [(ngModel)]="material.numbers" name="materialNumbers" [pushTemplate]="''">
 | 
			
		||||
		<rb-form-input *rbArrayInputItem="let item" [rbArrayInputListener]="'materialNumber'" [index]="item.i"
 | 
			
		||||
			label="material number" appValidate="string" [name]="'materialNumber-' + item.i"
 | 
			
		||||
			[ngModel]="item.value" title="Bosch material part number, eg. 5515753021"></rb-form-input>
 | 
			
		||||
	</rb-array-input>
 | 
			
		||||
	<rb-form-select name="propertiesSelect" label="Type" title="=overall material group specific properties"
 | 
			
		||||
		[(ngModel)]="material.properties.material_template">
 | 
			
		||||
		<option *ngFor="let m of d.latest.materialTemplates" [value]="m._id">{{m.name}}</option>
 | 
			
		||||
	</rb-form-select>
 | 
			
		||||
	<rb-form-input *ngFor="let parameter of
 | 
			
		||||
		d.id.materialTemplates[material.properties.material_template].parameters;
 | 
			
		||||
		index as i" [name]="'materialParameter' + i"
 | 
			
		||||
		[label]="parameter.name" appValidate="string" required
 | 
			
		||||
		[(ngModel)]="material.properties[parameter.name]" #parameterInput="ngModel">
 | 
			
		||||
		<ng-template rbFormValidationMessage="failure">{{parameterInput.errors.failure}}</ng-template>
 | 
			
		||||
		<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
	</rb-form-input>
 | 
			
		||||
 | 
			
		||||
  <rb-icon-button icon="save" mode="primary" type="submit" (click)="materialSave()"
 | 
			
		||||
                  [disabled]="materialForm.form.invalid">
 | 
			
		||||
    Save material
 | 
			
		||||
  </rb-icon-button>
 | 
			
		||||
  <rb-icon-button class="delete-material" icon="delete" mode="danger" (click)="deleteConfirm(modalDeleteConfirm)">
 | 
			
		||||
    Delete sample
 | 
			
		||||
  </rb-icon-button>
 | 
			
		||||
	<rb-icon-button icon="save" mode="primary" type="submit" (click)="materialSave()"
 | 
			
		||||
		[disabled]="materialForm.form.invalid">
 | 
			
		||||
		Save material
 | 
			
		||||
	</rb-icon-button>
 | 
			
		||||
	<rb-icon-button class="delete-material" icon="delete" mode="danger" (click)="deleteConfirm(modalDeleteConfirm)">
 | 
			
		||||
		Delete sample
 | 
			
		||||
	</rb-icon-button>
 | 
			
		||||
</form>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<ng-template #modalDeleteConfirm>
 | 
			
		||||
  <rb-alert alertTitle="Are you sure?" type="danger" [okBtnLabel]="'Delete material'"
 | 
			
		||||
            cancelBtnLabel="Cancel">
 | 
			
		||||
    Do you really want to delete {{material.name}}?
 | 
			
		||||
  </rb-alert>
 | 
			
		||||
	<rb-alert alertTitle="Are you sure?" type="danger" [okBtnLabel]="'Delete material'"
 | 
			
		||||
		cancelBtnLabel="Cancel">
 | 
			
		||||
		Do you really want to delete {{material.name}}?
 | 
			
		||||
	</rb-alert>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,3 @@
 | 
			
		||||
.delete-material {
 | 
			
		||||
  float: right;
 | 
			
		||||
	float: right;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,132 +11,132 @@ import {ValidationService} from '../services/validation.service';
 | 
			
		||||
import {ErrorComponent} from '../error/error.component';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-material',
 | 
			
		||||
  templateUrl: './material.component.html',
 | 
			
		||||
  styleUrls: ['./material.component.scss']
 | 
			
		||||
	selector: 'app-material',
 | 
			
		||||
	templateUrl: './material.component.html',
 | 
			
		||||
	styleUrls: ['./material.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class MaterialComponent implements OnInit, AfterContentChecked {
 | 
			
		||||
 | 
			
		||||
  @ViewChild('materialForm') materialForm: NgForm;
 | 
			
		||||
	@ViewChild('materialForm') materialForm: NgForm;
 | 
			
		||||
 | 
			
		||||
  material: MaterialModel;       // Material to edit
 | 
			
		||||
  materialNames: string[] = [];  // All other material names for unique validation
 | 
			
		||||
	material: MaterialModel;       // Material to edit
 | 
			
		||||
	materialNames: string[] = [];  // All other material names for unique validation
 | 
			
		||||
 | 
			
		||||
  modalText = {list: '', suggestion: ''};  // Modal for group and supplier correction
 | 
			
		||||
  loading = 0;                             // Number of loading instances
 | 
			
		||||
  checkFormAfterInit = true;               // Revalidate all fields on the next AfterContentChecked
 | 
			
		||||
		modalText = {list: '', suggestion: ''};  // Modal for group and supplier correction
 | 
			
		||||
			loading = 0;                             // Number of loading instances
 | 
			
		||||
		checkFormAfterInit = true;               // Revalidate all fields on the next AfterContentChecked
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private api: ApiService,
 | 
			
		||||
    private route: ActivatedRoute,
 | 
			
		||||
    public d: DataService,
 | 
			
		||||
    private modal: ModalService,
 | 
			
		||||
    public autocomplete: AutocompleteService,
 | 
			
		||||
    private router: Router,
 | 
			
		||||
    private validation: ValidationService
 | 
			
		||||
  ) { }
 | 
			
		||||
		constructor(
 | 
			
		||||
			private api: ApiService,
 | 
			
		||||
			private route: ActivatedRoute,
 | 
			
		||||
			public d: DataService,
 | 
			
		||||
			private modal: ModalService,
 | 
			
		||||
			public autocomplete: AutocompleteService,
 | 
			
		||||
			private router: Router,
 | 
			
		||||
			private validation: ValidationService
 | 
			
		||||
		) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.loading = 5;
 | 
			
		||||
    this.api.get<MaterialModel>('/material/' + this.route.snapshot.paramMap.get('id'), data => {
 | 
			
		||||
      this.material = new MaterialModel().deserialize(data);
 | 
			
		||||
      this.loading--;
 | 
			
		||||
      this.d.load('materials', () => {
 | 
			
		||||
        // Filter out name of the edited material as it can stay the same
 | 
			
		||||
        this.materialNames = this.d.arr.materials.map(e => e.name).filter(e => e !== this.material.name);
 | 
			
		||||
        this.loading--;
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    this.d.load('materialSuppliers', () => {
 | 
			
		||||
      this.loading--;
 | 
			
		||||
    });
 | 
			
		||||
    this.d.load('materialGroups', () => {
 | 
			
		||||
      this.loading--;
 | 
			
		||||
    });
 | 
			
		||||
    this.d.load('materialTemplates', () => {
 | 
			
		||||
      this.loading--;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
		ngOnInit(): void {
 | 
			
		||||
			this.loading = 5;
 | 
			
		||||
			this.api.get<MaterialModel>('/material/' + this.route.snapshot.paramMap.get('id'), data => {
 | 
			
		||||
				this.material = new MaterialModel().deserialize(data);
 | 
			
		||||
				this.loading--;
 | 
			
		||||
				this.d.load('materials', () => {
 | 
			
		||||
					// Filter out name of the edited material as it can stay the same
 | 
			
		||||
					this.materialNames = this.d.arr.materials.map(e => e.name).filter(e => e !== this.material.name);
 | 
			
		||||
					this.loading--;
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
			this.d.load('materialSuppliers', () => {
 | 
			
		||||
				this.loading--;
 | 
			
		||||
			});
 | 
			
		||||
			this.d.load('materialGroups', () => {
 | 
			
		||||
				this.loading--;
 | 
			
		||||
			});
 | 
			
		||||
			this.d.load('materialTemplates', () => {
 | 
			
		||||
				this.loading--;
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
  ngAfterContentChecked() {
 | 
			
		||||
    // Attach validators
 | 
			
		||||
    if (this.materialForm && 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.materialForm, 'materialParameter' + i, parameter.range);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
		ngAfterContentChecked() {
 | 
			
		||||
			// Attach validators
 | 
			
		||||
			if (this.materialForm && 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.materialForm, 'materialParameter' + i, parameter.range);
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
    // Revalidate
 | 
			
		||||
    if (this.checkFormAfterInit && this.materialForm !== undefined && this.materialForm.form.get('propertiesSelect')) {
 | 
			
		||||
      this.checkFormAfterInit = false;
 | 
			
		||||
      Object.keys(this.materialForm.form.controls).forEach(field => {
 | 
			
		||||
        this.materialForm.form.get(field).updateValueAndValidity();
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
			// Revalidate
 | 
			
		||||
			if (this.checkFormAfterInit && this.materialForm !== undefined && this.materialForm.form.get('propertiesSelect')) {
 | 
			
		||||
				this.checkFormAfterInit = false;
 | 
			
		||||
				Object.keys(this.materialForm.form.controls).forEach(field => {
 | 
			
		||||
					this.materialForm.form.get(field).updateValueAndValidity();
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
  // Attach validators specified in range to input with name
 | 
			
		||||
  attachValidator(form, name: string, range: {[prop: string]: any}) {
 | 
			
		||||
    if (form && form.form.get(name)) {
 | 
			
		||||
      const validators = [];
 | 
			
		||||
      if (range.hasOwnProperty('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]));
 | 
			
		||||
      }
 | 
			
		||||
      form.form.get(name).setValidators(validators);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
		// Attach validators specified in range to input with name
 | 
			
		||||
		attachValidator(form, name: string, range: {[prop: string]: any}) {
 | 
			
		||||
			if (form && form.form.get(name)) {
 | 
			
		||||
				const validators = [];
 | 
			
		||||
				if (range.hasOwnProperty('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]));
 | 
			
		||||
				}
 | 
			
		||||
				form.form.get(name).setValidators(validators);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
  materialSave() {
 | 
			
		||||
    this.api.put('/material/' + this.material._id, this.material.sendFormat(), () => {
 | 
			
		||||
      delete this.d.arr.materials;  // Reload materials
 | 
			
		||||
      this.d.load('materials');
 | 
			
		||||
      this.router.navigate(['/materials']);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
		materialSave() {
 | 
			
		||||
			this.api.put('/material/' + this.material._id, this.material.sendFormat(), () => {
 | 
			
		||||
				delete this.d.arr.materials;  // Reload materials
 | 
			
		||||
				this.d.load('materials');
 | 
			
		||||
				this.router.navigate(['/materials']);
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
  deleteConfirm(modal) {
 | 
			
		||||
    this.modal.open(modal).then(result => {
 | 
			
		||||
      if (result) {
 | 
			
		||||
        this.api.delete('/material/' + this.material._id, (ignore, error) => {
 | 
			
		||||
          if (error) {  // Material cannot be deleted as it is still referenced by active samples
 | 
			
		||||
            const modalRef = this.modal.openComponent(ErrorComponent);
 | 
			
		||||
            modalRef.instance.message = 'Cannot delete material as it is still in use!';
 | 
			
		||||
          }
 | 
			
		||||
          else {
 | 
			
		||||
            this.router.navigate(['/materials']);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
		deleteConfirm(modal) {
 | 
			
		||||
			this.modal.open(modal).then(result => {
 | 
			
		||||
				if (result) {
 | 
			
		||||
					this.api.delete('/material/' + this.material._id, (ignore, error) => {
 | 
			
		||||
						if (error) {  // Material cannot be deleted as it is still referenced by active samples
 | 
			
		||||
							const modalRef = this.modal.openComponent(ErrorComponent);
 | 
			
		||||
							modalRef.instance.message = 'Cannot delete material as it is still in use!';
 | 
			
		||||
						}
 | 
			
		||||
						else {
 | 
			
		||||
							this.router.navigate(['/materials']);
 | 
			
		||||
						}
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
  checkTypo(event, list, mKey, modal: TemplateRef<any>) {
 | 
			
		||||
    // User did not click on suggestion and entry is not in list
 | 
			
		||||
    if (!(event.relatedTarget && (event.relatedTarget.className.indexOf('rb-dropdown-item') >= 0 ||
 | 
			
		||||
      event.relatedTarget.className.indexOf('close-btn rb-btn rb-passive-link') >= 0)) &&
 | 
			
		||||
      this.d.arr[list].indexOf(this.material[mKey]) < 0) {
 | 
			
		||||
      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[mKey] = this.modalText.suggestion;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
		checkTypo(event, list, mKey, modal: TemplateRef<any>) {
 | 
			
		||||
			// User did not click on suggestion and entry is not in list
 | 
			
		||||
			if (!(event.relatedTarget && (event.relatedTarget.className.indexOf('rb-dropdown-item') >= 0 ||
 | 
			
		||||
										  event.relatedTarget.className.indexOf('close-btn rb-btn rb-passive-link') >= 0)) &&
 | 
			
		||||
											  this.d.arr[list].indexOf(this.material[mKey]) < 0) {
 | 
			
		||||
				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[mKey] = this.modalText.suggestion;
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,91 +1,91 @@
 | 
			
		||||
<div class="header-addnew">
 | 
			
		||||
  <rb-icon-button *ngIf="sampleSelect" mode="secondary" icon="close" (click)="sampleSelect = false"
 | 
			
		||||
                  class="validation-close" iconOnly></rb-icon-button>
 | 
			
		||||
  <rb-icon-button [icon]="sampleSelect ? 'checkmark' : 'clear-all'"
 | 
			
		||||
                  mode="secondary" (click)="validate()">
 | 
			
		||||
    {{sampleSelect ? 'Validate' : 'Validation'}}
 | 
			
		||||
  </rb-icon-button>
 | 
			
		||||
	<rb-icon-button *ngIf="sampleSelect" mode="secondary" icon="close" (click)="sampleSelect = false"
 | 
			
		||||
		class="validation-close" iconOnly></rb-icon-button>
 | 
			
		||||
	<rb-icon-button [icon]="sampleSelect ? 'checkmark' : 'clear-all'"
 | 
			
		||||
		mode="secondary" (click)="validate()">
 | 
			
		||||
		{{sampleSelect ? 'Validate' : 'Validation'}}
 | 
			
		||||
	</rb-icon-button>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="status-selection">
 | 
			
		||||
  <label class="label">Status</label>
 | 
			
		||||
  <rb-form-checkbox name="status-validated" [(ngModel)]="materialStatus.validated"
 | 
			
		||||
                    [disabled]="!materialStatus.new && !materialStatus.deleted"
 | 
			
		||||
                    (ngModelChange)="loadMaterials()">
 | 
			
		||||
    validated
 | 
			
		||||
  </rb-form-checkbox>
 | 
			
		||||
  <rb-form-checkbox name="status-new" [(ngModel)]="materialStatus.new"
 | 
			
		||||
                    [disabled]="!materialStatus.validated && !materialStatus.deleted"
 | 
			
		||||
                    (ngModelChange)="loadMaterials()">
 | 
			
		||||
    new
 | 
			
		||||
  </rb-form-checkbox>
 | 
			
		||||
  <rb-form-checkbox name="status-deleted" [(ngModel)]="materialStatus.deleted"
 | 
			
		||||
                    [disabled]="!materialStatus.validated && !materialStatus.new"
 | 
			
		||||
                    (ngModelChange)="loadMaterials()">
 | 
			
		||||
    deleted
 | 
			
		||||
  </rb-form-checkbox>
 | 
			
		||||
	<label class="label">Status</label>
 | 
			
		||||
	<rb-form-checkbox name="status-validated" [(ngModel)]="materialStatus.validated"
 | 
			
		||||
		[disabled]="!materialStatus.new && !materialStatus.deleted"
 | 
			
		||||
		(ngModelChange)="loadMaterials()">
 | 
			
		||||
		validated
 | 
			
		||||
	</rb-form-checkbox>
 | 
			
		||||
	<rb-form-checkbox name="status-new" [(ngModel)]="materialStatus.new"
 | 
			
		||||
		[disabled]="!materialStatus.validated && !materialStatus.deleted"
 | 
			
		||||
		(ngModelChange)="loadMaterials()">
 | 
			
		||||
		new
 | 
			
		||||
	</rb-form-checkbox>
 | 
			
		||||
	<rb-form-checkbox name="status-deleted" [(ngModel)]="materialStatus.deleted"
 | 
			
		||||
		[disabled]="!materialStatus.validated && !materialStatus.new"
 | 
			
		||||
		(ngModelChange)="loadMaterials()">
 | 
			
		||||
		deleted
 | 
			
		||||
	</rb-form-checkbox>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="material-search space-right">
 | 
			
		||||
  <rb-form-input label="search" [(ngModel)]="materialSearch" icon="close"></rb-form-input>
 | 
			
		||||
  <span class="rb-ic rb-ic-close clickable" (click)="materialSearch = ''"></span>
 | 
			
		||||
	<rb-form-input label="search" [(ngModel)]="materialSearch" icon="close"></rb-form-input>
 | 
			
		||||
	<span class="rb-ic rb-ic-close clickable" (click)="materialSearch = ''"></span>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<ng-container *ngTemplateOutlet="paging"></ng-container>
 | 
			
		||||
 | 
			
		||||
<rb-table ellipsis scrollTop>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <th *ngIf="sampleSelect">
 | 
			
		||||
      <rb-form-checkbox name="select-all" (change)="selectAll($event)">all</rb-form-checkbox>
 | 
			
		||||
    </th>
 | 
			
		||||
    <th>Name</th>
 | 
			
		||||
    <th>Supplier</th>
 | 
			
		||||
    <th>Group</th>
 | 
			
		||||
    <th *ngFor="let key of templateKeys">{{key.label}}</th>
 | 
			
		||||
    <th>Numbers</th>
 | 
			
		||||
    <th></th>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr *ngFor="let material of (materials || []).filter(materialFilter(materialSearch))
 | 
			
		||||
    .slice((page - 1) * pageSize, page * pageSize); index as i">
 | 
			
		||||
    <td *ngIf="sampleSelect">
 | 
			
		||||
      <rb-form-checkbox *ngIf="material.status !== 'deleted'" [name]="'validate-' + i"
 | 
			
		||||
                        [(ngModel)]="material.selected">
 | 
			
		||||
      </rb-form-checkbox>
 | 
			
		||||
    </td>
 | 
			
		||||
    <td>{{material.name}}</td>
 | 
			
		||||
    <td>{{material.supplier}}</td>
 | 
			
		||||
    <td>{{material.group}}</td>
 | 
			
		||||
    <td *ngFor="let key of templateKeys">{{material.properties[key.key] | exists}}</td>
 | 
			
		||||
    <td>{{material.numbers}}</td>
 | 
			
		||||
    <td>
 | 
			
		||||
      <a [routerLink]="'/materials/edit/' + material._id" *ngIf="material.status !== 'deleted'">
 | 
			
		||||
        <span class="rb-ic rb-ic-edit clickable"></span>
 | 
			
		||||
      </a>
 | 
			
		||||
      <span class="rb-ic rb-ic-undo clickable" *ngIf="material.status === 'deleted'"
 | 
			
		||||
            (click)="restoreMaterial(material._id, restoreConfirm)"></span>
 | 
			
		||||
  </td>
 | 
			
		||||
  </tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<th *ngIf="sampleSelect">
 | 
			
		||||
			<rb-form-checkbox name="select-all" (change)="selectAll($event)">all</rb-form-checkbox>
 | 
			
		||||
		</th>
 | 
			
		||||
		<th>Name</th>
 | 
			
		||||
		<th>Supplier</th>
 | 
			
		||||
		<th>Group</th>
 | 
			
		||||
		<th *ngFor="let key of templateKeys">{{key.label}}</th>
 | 
			
		||||
		<th>Numbers</th>
 | 
			
		||||
		<th></th>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr *ngFor="let material of (materials || []).filter(materialFilter(materialSearch))
 | 
			
		||||
		.slice((page - 1) * pageSize, page * pageSize); index as i">
 | 
			
		||||
		<td *ngIf="sampleSelect">
 | 
			
		||||
			<rb-form-checkbox *ngIf="material.status !== 'deleted'" [name]="'validate-' + i"
 | 
			
		||||
				[(ngModel)]="material.selected">
 | 
			
		||||
			</rb-form-checkbox>
 | 
			
		||||
		</td>
 | 
			
		||||
		<td>{{material.name}}</td>
 | 
			
		||||
		<td>{{material.supplier}}</td>
 | 
			
		||||
		<td>{{material.group}}</td>
 | 
			
		||||
		<td *ngFor="let key of templateKeys">{{material.properties[key.key] | exists}}</td>
 | 
			
		||||
		<td>{{material.numbers}}</td>
 | 
			
		||||
		<td>
 | 
			
		||||
			<a [routerLink]="'/materials/edit/' + material._id" *ngIf="material.status !== 'deleted'">
 | 
			
		||||
				<span class="rb-ic rb-ic-edit clickable"></span>
 | 
			
		||||
			</a>
 | 
			
		||||
			<span class="rb-ic rb-ic-undo clickable" *ngIf="material.status === 'deleted'"
 | 
			
		||||
				(click)="restoreMaterial(material._id, restoreConfirm)"></span>
 | 
			
		||||
		</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
</rb-table>
 | 
			
		||||
 | 
			
		||||
<ng-container *ngTemplateOutlet="paging"></ng-container>
 | 
			
		||||
 | 
			
		||||
<ng-template #paging>
 | 
			
		||||
  <div class="paging">
 | 
			
		||||
    <button class="rb-btn rb-link" type="button" (click)="page = page - 1" [disabled]="page === 1">
 | 
			
		||||
      <span class="rb-ic  rb-ic-back-left"></span>
 | 
			
		||||
    </button>
 | 
			
		||||
    <rb-form-input label="page" [(ngModel)]="page"></rb-form-input>
 | 
			
		||||
    <span>
 | 
			
		||||
      of {{pages}}
 | 
			
		||||
    </span>
 | 
			
		||||
    <button class="rb-btn rb-link" type="button" (click)="page = page + 1" [disabled]="page >= pages">
 | 
			
		||||
      <span class="rb-ic  rb-ic-forward-right"></span>
 | 
			
		||||
    </button>
 | 
			
		||||
  </div>
 | 
			
		||||
	<div class="paging">
 | 
			
		||||
		<button class="rb-btn rb-link" type="button" (click)="page = page - 1" [disabled]="page === 1">
 | 
			
		||||
			<span class="rb-ic  rb-ic-back-left"></span>
 | 
			
		||||
		</button>
 | 
			
		||||
		<rb-form-input label="page" [(ngModel)]="page"></rb-form-input>
 | 
			
		||||
		<span>
 | 
			
		||||
			of {{pages}}
 | 
			
		||||
		</span>
 | 
			
		||||
		<button class="rb-btn rb-link" type="button" (click)="page = page + 1" [disabled]="page >= pages">
 | 
			
		||||
			<span class="rb-ic  rb-ic-forward-right"></span>
 | 
			
		||||
		</button>
 | 
			
		||||
	</div>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
<ng-template #restoreConfirm>
 | 
			
		||||
  <rb-dialog dialogTitle="Restore sample">
 | 
			
		||||
    Do you really want to restore this sample?
 | 
			
		||||
  </rb-dialog>
 | 
			
		||||
	<rb-dialog dialogTitle="Restore sample">
 | 
			
		||||
		Do you really want to restore this sample?
 | 
			
		||||
	</rb-dialog>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,63 +1,63 @@
 | 
			
		||||
.paging {
 | 
			
		||||
  height: 50px;
 | 
			
		||||
  float: left;
 | 
			
		||||
	height: 50px;
 | 
			
		||||
	float: left;
 | 
			
		||||
 | 
			
		||||
  rb-form-input {
 | 
			
		||||
    max-width: 65px;
 | 
			
		||||
  }
 | 
			
		||||
	rb-form-input {
 | 
			
		||||
		max-width: 65px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  > * {
 | 
			
		||||
    float: left;
 | 
			
		||||
  }
 | 
			
		||||
	> * {
 | 
			
		||||
		float: left;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  > button {
 | 
			
		||||
    margin-top: 18px;
 | 
			
		||||
  }
 | 
			
		||||
	> button {
 | 
			
		||||
		margin-top: 18px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  > span {
 | 
			
		||||
    margin-top: 20px;
 | 
			
		||||
    margin-left: 5px;
 | 
			
		||||
  }
 | 
			
		||||
	> span {
 | 
			
		||||
		margin-top: 20px;
 | 
			
		||||
		margin-left: 5px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.status-selection {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  margin-bottom: 10px;
 | 
			
		||||
  float: left;
 | 
			
		||||
  margin-right: 15px;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
	margin-bottom: 10px;
 | 
			
		||||
	float: left;
 | 
			
		||||
	margin-right: 15px;
 | 
			
		||||
 | 
			
		||||
  label {
 | 
			
		||||
    display: block;
 | 
			
		||||
    font-weight: 700;
 | 
			
		||||
    font-size: 10px;
 | 
			
		||||
  }
 | 
			
		||||
	label {
 | 
			
		||||
		display: block;
 | 
			
		||||
		font-weight: 700;
 | 
			
		||||
		font-size: 10px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  rb-form-checkbox {
 | 
			
		||||
    float: left;
 | 
			
		||||
    margin-right: 10px;
 | 
			
		||||
    margin-top: -10px;
 | 
			
		||||
  }
 | 
			
		||||
	rb-form-checkbox {
 | 
			
		||||
		float: left;
 | 
			
		||||
		margin-right: 10px;
 | 
			
		||||
		margin-top: -10px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.header-addnew {
 | 
			
		||||
  & > * {
 | 
			
		||||
    display: inline;
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
  }
 | 
			
		||||
	& > * {
 | 
			
		||||
		display: inline;
 | 
			
		||||
		margin-bottom: 10px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  rb-icon-button {
 | 
			
		||||
    float: right;
 | 
			
		||||
  }
 | 
			
		||||
	rb-icon-button {
 | 
			
		||||
		float: right;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.material-search {
 | 
			
		||||
  width: 300px;
 | 
			
		||||
  float: left;
 | 
			
		||||
  position: relative;
 | 
			
		||||
	width: 300px;
 | 
			
		||||
	float: left;
 | 
			
		||||
	position: relative;
 | 
			
		||||
 | 
			
		||||
  span {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    right: 5px;
 | 
			
		||||
    top: 24px;
 | 
			
		||||
  }
 | 
			
		||||
	span {
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		right: 5px;
 | 
			
		||||
		top: 24px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,92 +6,92 @@ import {ModalService} from '@inst-iot/bosch-angular-ui-components';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-materials',
 | 
			
		||||
  templateUrl: './materials.component.html',
 | 
			
		||||
  styleUrls: ['./materials.component.scss']
 | 
			
		||||
	selector: 'app-materials',
 | 
			
		||||
	templateUrl: './materials.component.html',
 | 
			
		||||
	styleUrls: ['./materials.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class MaterialsComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  materials: MaterialModel[] = [];                    // All materials
 | 
			
		||||
  templateKeys: {key: string, label: string}[] = [];  // Material template keys
 | 
			
		||||
  materialStatus = {validated: true, new: true, deleted: false};  // Material statuses to show
 | 
			
		||||
  materialSearch = '';   // Material name search string
 | 
			
		||||
  sampleSelect = false;  // Set to true to show checkboxes for validation
 | 
			
		||||
	materials: MaterialModel[] = [];                    // All materials
 | 
			
		||||
	templateKeys: {key: string, label: string}[] = [];  // Material template keys
 | 
			
		||||
	materialStatus = {validated: true, new: true, deleted: false};  // Material statuses to show
 | 
			
		||||
	materialSearch = '';   // Material name search string
 | 
			
		||||
	sampleSelect = false;  // Set to true to show checkboxes for validation
 | 
			
		||||
 | 
			
		||||
  page = 1;  // Page settings
 | 
			
		||||
  pages = 0;
 | 
			
		||||
  pageSize = 25;
 | 
			
		||||
		page = 1;  // Page settings
 | 
			
		||||
		pages = 0;
 | 
			
		||||
		pageSize = 25;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private api: ApiService,
 | 
			
		||||
    public d: DataService,
 | 
			
		||||
    private modal: ModalService
 | 
			
		||||
  ) { }
 | 
			
		||||
		constructor(
 | 
			
		||||
			private api: ApiService,
 | 
			
		||||
			public d: DataService,
 | 
			
		||||
			private modal: ModalService
 | 
			
		||||
		) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.loadMaterials();
 | 
			
		||||
    this.d.load('materialTemplates', () => {
 | 
			
		||||
      this.d.arr.materialTemplates.forEach(template => {
 | 
			
		||||
        template.parameters.forEach(parameter => {
 | 
			
		||||
          this.templateKeys.push({key: parameter.name, label: `${this.ucFirst(template.name)} ${parameter.name}`});
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
      // Filter out duplicates
 | 
			
		||||
      this.templateKeys = this.templateKeys.filter((e, i, a) => !a.slice(0, i).find(el => el.key === e.key));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
		ngOnInit(): void {
 | 
			
		||||
			this.loadMaterials();
 | 
			
		||||
			this.d.load('materialTemplates', () => {
 | 
			
		||||
				this.d.arr.materialTemplates.forEach(template => {
 | 
			
		||||
					template.parameters.forEach(parameter => {
 | 
			
		||||
						this.templateKeys.push({key: parameter.name, label: `${this.ucFirst(template.name)} ${parameter.name}`});
 | 
			
		||||
					});
 | 
			
		||||
				});
 | 
			
		||||
				// Filter out duplicates
 | 
			
		||||
				this.templateKeys = this.templateKeys.filter((e, i, a) => !a.slice(0, i).find(el => el.key === e.key));
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
  loadMaterials() {
 | 
			
		||||
    this.api.get<MaterialModel[]>('/materials?' +
 | 
			
		||||
      Object.entries(this.materialStatus).filter(e => e[1]).map(e => 'status[]=' + e[0]).join('&'), data => {
 | 
			
		||||
      this.materials = data.map(e => new MaterialModel().deserialize(e));
 | 
			
		||||
      this.pages = Math.ceil(this.materials.length / this.pageSize);
 | 
			
		||||
      this.page = 1;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
		loadMaterials() {
 | 
			
		||||
			this.api.get<MaterialModel[]>('/materials?' +
 | 
			
		||||
										  Object.entries(this.materialStatus).filter(e => e[1]).map(e => 'status[]=' + e[0]).join('&'), data => {
 | 
			
		||||
				this.materials = data.map(e => new MaterialModel().deserialize(e));
 | 
			
		||||
				this.pages = Math.ceil(this.materials.length / this.pageSize);
 | 
			
		||||
				this.page = 1;
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
  validate() {
 | 
			
		||||
    if (this.sampleSelect) {  // Selection was done do actual validation
 | 
			
		||||
      this.materials.forEach(sample => {
 | 
			
		||||
        if (sample.selected) {
 | 
			
		||||
          this.api.put('/material/validate/' + sample._id);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      this.loadMaterials();
 | 
			
		||||
      this.sampleSelect = false;
 | 
			
		||||
    }
 | 
			
		||||
    else {  // Activate validation mode
 | 
			
		||||
      this.sampleSelect = true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
		validate() {
 | 
			
		||||
			if (this.sampleSelect) {  // Selection was done do actual validation
 | 
			
		||||
				this.materials.forEach(sample => {
 | 
			
		||||
					if (sample.selected) {
 | 
			
		||||
						this.api.put('/material/validate/' + sample._id);
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
				this.loadMaterials();
 | 
			
		||||
				this.sampleSelect = false;
 | 
			
		||||
			}
 | 
			
		||||
			else {  // Activate validation mode
 | 
			
		||||
				this.sampleSelect = true;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
  selectAll(event) {  // Toggle selection for all items except deleted ones
 | 
			
		||||
    this.materials.forEach(material => {
 | 
			
		||||
      if (material.status !== 'deleted') {
 | 
			
		||||
        material.selected = event.target.checked;
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        material.selected = false;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
		selectAll(event) {  // Toggle selection for all items except deleted ones
 | 
			
		||||
			this.materials.forEach(material => {
 | 
			
		||||
				if (material.status !== 'deleted') {
 | 
			
		||||
					material.selected = event.target.checked;
 | 
			
		||||
				}
 | 
			
		||||
				else {
 | 
			
		||||
					material.selected = false;
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
  restoreMaterial(id, modal) {
 | 
			
		||||
    this.modal.open(modal).then(res => {
 | 
			
		||||
      if (res) {
 | 
			
		||||
        this.api.put('/sample/restore/' + id, {}, ignore => {
 | 
			
		||||
          this.materials.find(e => e._id === id).status = 'new';
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
		restoreMaterial(id, modal) {
 | 
			
		||||
			this.modal.open(modal).then(res => {
 | 
			
		||||
				if (res) {
 | 
			
		||||
					this.api.put('/sample/restore/' + id, {}, ignore => {
 | 
			
		||||
						this.materials.find(e => e._id === id).status = 'new';
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
  ucFirst(string) {  // Convert first character of string to uppercase
 | 
			
		||||
    return string[0].toUpperCase() + string.slice(1);
 | 
			
		||||
  }
 | 
			
		||||
		ucFirst(string) {  // Convert first character of string to uppercase
 | 
			
		||||
			return string[0].toUpperCase() + string.slice(1);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
  materialFilter(ms) {  // Filter function for material names
 | 
			
		||||
    return e => e.name.indexOf(ms) >= 0;
 | 
			
		||||
  }
 | 
			
		||||
		materialFilter(ms) {  // Filter function for material names
 | 
			
		||||
			return e => e.name.indexOf(ms) >= 0;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,73 +1,73 @@
 | 
			
		||||
<rb-icon-button icon="add" mode="primary" (click)="newModel = !newModel; oldModelGroup = ''" class="space-below">
 | 
			
		||||
  New model
 | 
			
		||||
	New model
 | 
			
		||||
</rb-icon-button>
 | 
			
		||||
 | 
			
		||||
<form *ngIf="newModel" #modelForm="ngForm">
 | 
			
		||||
  <rb-form-input name="group" label="group" appValidate="string" required [(ngModel)]="modelGroup" #groupInput="ngModel"
 | 
			
		||||
                 [rbFormInputAutocomplete]="autocomplete.bind(this, groups)"
 | 
			
		||||
                 [rbDebounceTime]="0" [rbInitialOpen]="true">
 | 
			
		||||
    <ng-template rbFormValidationMessage="failure">{{groupInput.errors.failure}}</ng-template>
 | 
			
		||||
    <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
  </rb-form-input>
 | 
			
		||||
  <rb-form-input name="name" label="name" appValidate="string" required [(ngModel)]="model.name" #nameInput="ngModel">
 | 
			
		||||
    <ng-template rbFormValidationMessage="failure">{{nameInput.errors.failure}}</ng-template>
 | 
			
		||||
    <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
  </rb-form-input>
 | 
			
		||||
  <rb-form-input name="url" label="URL" appValidate="url" required [(ngModel)]="model.url" #urlInput="ngModel">
 | 
			
		||||
    <ng-template rbFormValidationMessage="failure">{{urlInput.errors.failure}}</ng-template>
 | 
			
		||||
    <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
  </rb-form-input>
 | 
			
		||||
	<rb-form-input name="group" label="group" appValidate="string" required [(ngModel)]="modelGroup" #groupInput="ngModel"
 | 
			
		||||
		[rbFormInputAutocomplete]="autocomplete.bind(this, groups)"
 | 
			
		||||
		[rbDebounceTime]="0" [rbInitialOpen]="true">
 | 
			
		||||
		<ng-template rbFormValidationMessage="failure">{{groupInput.errors.failure}}</ng-template>
 | 
			
		||||
		<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
	</rb-form-input>
 | 
			
		||||
	<rb-form-input name="name" label="name" appValidate="string" required [(ngModel)]="model.name" #nameInput="ngModel">
 | 
			
		||||
		<ng-template rbFormValidationMessage="failure">{{nameInput.errors.failure}}</ng-template>
 | 
			
		||||
		<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
	</rb-form-input>
 | 
			
		||||
	<rb-form-input name="url" label="URL" appValidate="url" required [(ngModel)]="model.url" #urlInput="ngModel">
 | 
			
		||||
		<ng-template rbFormValidationMessage="failure">{{urlInput.errors.failure}}</ng-template>
 | 
			
		||||
		<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
	</rb-form-input>
 | 
			
		||||
 | 
			
		||||
  <rb-icon-button icon="save" mode="primary" type="submit" [disabled]="!modelForm.form.valid" (click)="saveModel()">
 | 
			
		||||
    Save model
 | 
			
		||||
  </rb-icon-button>
 | 
			
		||||
	<rb-icon-button icon="save" mode="primary" type="submit" [disabled]="!modelForm.form.valid" (click)="saveModel()">
 | 
			
		||||
		Save model
 | 
			
		||||
	</rb-icon-button>
 | 
			
		||||
</form>
 | 
			
		||||
 | 
			
		||||
<rb-table class="space-above space-below">
 | 
			
		||||
  <tr>
 | 
			
		||||
    <th>Name</th>
 | 
			
		||||
    <th>URL</th>
 | 
			
		||||
    <th></th>
 | 
			
		||||
    <th></th>
 | 
			
		||||
  </tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<th>Name</th>
 | 
			
		||||
		<th>URL</th>
 | 
			
		||||
		<th></th>
 | 
			
		||||
		<th></th>
 | 
			
		||||
	</tr>
 | 
			
		||||
 | 
			
		||||
  <ng-container *ngFor="let group of d.arr.modelGroups">
 | 
			
		||||
    <tr><th>{{group.group}}</th><th></th><th></th><th></th><th></th></tr>
 | 
			
		||||
    <tr *ngFor="let modelItem of group.models">
 | 
			
		||||
      <td>{{modelItem.name}}</td>
 | 
			
		||||
      <td>{{modelItem.url}}</td>
 | 
			
		||||
      <td>
 | 
			
		||||
        <span class="rb-ic rb-ic-edit clickable"
 | 
			
		||||
              (click)="modelGroup = group.group;
 | 
			
		||||
                       oldModelGroup = group.group;
 | 
			
		||||
                       oldModelName = modelItem.name;
 | 
			
		||||
                       model = modelItem;
 | 
			
		||||
                       newModel = true;">
 | 
			
		||||
        </span>
 | 
			
		||||
      </td>
 | 
			
		||||
      <td>
 | 
			
		||||
        <span class="rb-ic rb-ic-delete clickable"
 | 
			
		||||
              (click)="delete(modalDeleteConfirm, modelItem.name, group.group)"></span>
 | 
			
		||||
      </td>
 | 
			
		||||
    </tr>
 | 
			
		||||
  </ng-container>
 | 
			
		||||
	<ng-container *ngFor="let group of d.arr.modelGroups">
 | 
			
		||||
		<tr><th>{{group.group}}</th><th></th><th></th><th></th><th></th></tr>
 | 
			
		||||
		<tr *ngFor="let modelItem of group.models">
 | 
			
		||||
			<td>{{modelItem.name}}</td>
 | 
			
		||||
			<td>{{modelItem.url}}</td>
 | 
			
		||||
			<td>
 | 
			
		||||
				<span class="rb-ic rb-ic-edit clickable"
 | 
			
		||||
					(click)="modelGroup = group.group;
 | 
			
		||||
					oldModelGroup = group.group;
 | 
			
		||||
					oldModelName = modelItem.name;
 | 
			
		||||
					model = modelItem;
 | 
			
		||||
					newModel = true;">
 | 
			
		||||
				</span>
 | 
			
		||||
			</td>
 | 
			
		||||
			<td>
 | 
			
		||||
				<span class="rb-ic rb-ic-delete clickable"
 | 
			
		||||
					(click)="delete(modalDeleteConfirm, modelItem.name, group.group)"></span>
 | 
			
		||||
			</td>
 | 
			
		||||
		</tr>
 | 
			
		||||
	</ng-container>
 | 
			
		||||
</rb-table>
 | 
			
		||||
 | 
			
		||||
<rb-table>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <th>Model files</th>
 | 
			
		||||
    <th></th>
 | 
			
		||||
    <th></th>
 | 
			
		||||
  </tr>
 | 
			
		||||
  <tr *ngFor="let file of d.arr.modelFiles">
 | 
			
		||||
    <td>{{file.name}}</td>
 | 
			
		||||
    <td>{{file.size | size:'M'}}</td>
 | 
			
		||||
    <td><span class="rb-ic rb-ic-delete clickable" (click)="delete(modalDeleteConfirm, file.name)"></span></td>
 | 
			
		||||
  </tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<th>Model files</th>
 | 
			
		||||
		<th></th>
 | 
			
		||||
		<th></th>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr *ngFor="let file of d.arr.modelFiles">
 | 
			
		||||
		<td>{{file.name}}</td>
 | 
			
		||||
		<td>{{file.size | size:'M'}}</td>
 | 
			
		||||
		<td><span class="rb-ic rb-ic-delete clickable" (click)="delete(modalDeleteConfirm, file.name)"></span></td>
 | 
			
		||||
	</tr>
 | 
			
		||||
</rb-table>
 | 
			
		||||
 | 
			
		||||
<ng-template #modalDeleteConfirm>
 | 
			
		||||
  <rb-alert alertTitle="Are you sure?" type="danger" okBtnLabel="Delete model" cancelBtnLabel="Cancel">
 | 
			
		||||
    Do you really want to delete?
 | 
			
		||||
  </rb-alert>
 | 
			
		||||
	<rb-alert alertTitle="Are you sure?" type="danger" okBtnLabel="Delete model" cancelBtnLabel="Cancel">
 | 
			
		||||
		Do you really want to delete?
 | 
			
		||||
	</rb-alert>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,77 +7,77 @@ import {ModalService} from '@inst-iot/bosch-angular-ui-components';
 | 
			
		||||
import omit from 'lodash/omit';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-model-templates',
 | 
			
		||||
  templateUrl: './model-templates.component.html',
 | 
			
		||||
  styleUrls: ['./model-templates.component.scss']
 | 
			
		||||
	selector: 'app-model-templates',
 | 
			
		||||
	templateUrl: './model-templates.component.html',
 | 
			
		||||
	styleUrls: ['./model-templates.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class ModelTemplatesComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  newModel = false;    // Display new model dialog
 | 
			
		||||
  modelGroup = '';     // Group of the edited model
 | 
			
		||||
  oldModelGroup = '';  // Group of the edited model before editing started
 | 
			
		||||
  oldModelName = '';   // Name of the edited model before editing started
 | 
			
		||||
  model = new ModelItemModel().models[0];  // Edited model
 | 
			
		||||
  groups = [];         // All model group names
 | 
			
		||||
	newModel = false;    // Display new model dialog
 | 
			
		||||
	modelGroup = '';     // Group of the edited model
 | 
			
		||||
	oldModelGroup = '';  // Group of the edited model before editing started
 | 
			
		||||
	oldModelName = '';   // Name of the edited model before editing started
 | 
			
		||||
	model = new ModelItemModel().models[0];  // Edited model
 | 
			
		||||
	groups = [];         // All model group names
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private api: ApiService,
 | 
			
		||||
    public autocomplete: AutocompleteService,
 | 
			
		||||
    public d: DataService,
 | 
			
		||||
    private modal: ModalService
 | 
			
		||||
  ) { }
 | 
			
		||||
	constructor(
 | 
			
		||||
		private api: ApiService,
 | 
			
		||||
		public autocomplete: AutocompleteService,
 | 
			
		||||
		public d: DataService,
 | 
			
		||||
		private modal: ModalService
 | 
			
		||||
	) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.loadGroups();
 | 
			
		||||
  }
 | 
			
		||||
	ngOnInit(): void {
 | 
			
		||||
		this.loadGroups();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  loadGroups() {
 | 
			
		||||
    delete this.d.arr.modelGroups;
 | 
			
		||||
    this.d.load('modelGroups', () => {
 | 
			
		||||
      this.groups = this.d.arr.modelGroups.map(e => e.group);
 | 
			
		||||
    });
 | 
			
		||||
    this.d.load('modelFiles');
 | 
			
		||||
  }
 | 
			
		||||
	loadGroups() {
 | 
			
		||||
		delete this.d.arr.modelGroups;
 | 
			
		||||
		this.d.load('modelGroups', () => {
 | 
			
		||||
			this.groups = this.d.arr.modelGroups.map(e => e.group);
 | 
			
		||||
		});
 | 
			
		||||
		this.d.load('modelFiles');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  saveModel() {
 | 
			
		||||
    // Group was changed, delete model in old group
 | 
			
		||||
    if (this.oldModelGroup !== '' && this.modelGroup !== this.oldModelGroup) {
 | 
			
		||||
      this.delete(null, this.oldModelName, this.oldModelGroup);
 | 
			
		||||
    }
 | 
			
		||||
    this.api.post('/model/' + this.modelGroup, omit(this.model, '_id'), () => {
 | 
			
		||||
      this.newModel = false;  // Reset model edit parameters
 | 
			
		||||
      this.loadGroups();
 | 
			
		||||
      this.modelGroup = '';
 | 
			
		||||
      this.oldModelGroup = '';
 | 
			
		||||
      this.oldModelName = '';
 | 
			
		||||
      this.model = new ModelItemModel().models[0];
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
	saveModel() {
 | 
			
		||||
		// Group was changed, delete model in old group
 | 
			
		||||
		if (this.oldModelGroup !== '' && this.modelGroup !== this.oldModelGroup) {
 | 
			
		||||
			this.delete(null, this.oldModelName, this.oldModelGroup);
 | 
			
		||||
		}
 | 
			
		||||
		this.api.post('/model/' + this.modelGroup, omit(this.model, '_id'), () => {
 | 
			
		||||
			this.newModel = false;  // Reset model edit parameters
 | 
			
		||||
			this.loadGroups();
 | 
			
		||||
			this.modelGroup = '';
 | 
			
		||||
			this.oldModelGroup = '';
 | 
			
		||||
			this.oldModelName = '';
 | 
			
		||||
			this.model = new ModelItemModel().models[0];
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  delete(modal, name, group = null) {
 | 
			
		||||
    new Promise(resolve => {
 | 
			
		||||
      if (modal) {  // If modal was given, wait for result
 | 
			
		||||
        this.modal.open(modal).then(result => {
 | 
			
		||||
          resolve(result);
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        resolve(true);
 | 
			
		||||
      }
 | 
			
		||||
    }).then(res => {
 | 
			
		||||
      if (res) {
 | 
			
		||||
        if (group) {  // Delete model group if given
 | 
			
		||||
          this.api.delete(`/model/${group}/${name}`, () => {
 | 
			
		||||
            this.loadGroups();
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
        else {  // Delete model file
 | 
			
		||||
          this.api.delete(`/model/file/${name}`, () => {
 | 
			
		||||
            this.d.arr.modelFiles.splice(this.d.arr.modelFiles.findIndex(e => e.name === name), 1);
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
	delete(modal, name, group = null) {
 | 
			
		||||
		new Promise(resolve => {
 | 
			
		||||
			if (modal) {  // If modal was given, wait for result
 | 
			
		||||
				this.modal.open(modal).then(result => {
 | 
			
		||||
					resolve(result);
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
			else {
 | 
			
		||||
				resolve(true);
 | 
			
		||||
			}
 | 
			
		||||
		}).then(res => {
 | 
			
		||||
			if (res) {
 | 
			
		||||
				if (group) {  // Delete model group if given
 | 
			
		||||
					this.api.delete(`/model/${group}/${name}`, () => {
 | 
			
		||||
						this.loadGroups();
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
				else {  // Delete model file
 | 
			
		||||
					this.api.delete(`/model/file/${name}`, () => {
 | 
			
		||||
						this.d.arr.modelFiles.splice(this.d.arr.modelFiles.findIndex(e => e.name === name), 1);
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { BaseModel } from './base.model';
 | 
			
		||||
 | 
			
		||||
describe('BaseModel', () => {
 | 
			
		||||
  it('should create an instance', () => {
 | 
			
		||||
    expect(new BaseModel()).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
	it('should create an instance', () => {
 | 
			
		||||
		expect(new BaseModel()).toBeTruthy();
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
export class BaseModel {
 | 
			
		||||
  deserialize(input: any): this {
 | 
			
		||||
    Object.assign(this, input);
 | 
			
		||||
    return this;
 | 
			
		||||
  }
 | 
			
		||||
	deserialize(input: any): this {
 | 
			
		||||
		Object.assign(this, input);
 | 
			
		||||
		return this;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  sendFormat(): this {
 | 
			
		||||
    return this;
 | 
			
		||||
  }
 | 
			
		||||
	sendFormat(): this {
 | 
			
		||||
		return this;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { ChangelogModel } from './changelog.model';
 | 
			
		||||
 | 
			
		||||
describe('ChangelogModel', () => {
 | 
			
		||||
  it('should create an instance', () => {
 | 
			
		||||
    expect(new ChangelogModel()).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
	it('should create an instance', () => {
 | 
			
		||||
		expect(new ChangelogModel()).toBeTruthy();
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,10 @@ import {BaseModel} from './base.model';
 | 
			
		||||
import {IdModel} from './id.model';
 | 
			
		||||
 | 
			
		||||
export class ChangelogModel extends BaseModel {
 | 
			
		||||
  _id: IdModel = null;
 | 
			
		||||
  date: Date;
 | 
			
		||||
  action: string;
 | 
			
		||||
  collection: string;
 | 
			
		||||
  conditions: {[key: string]: any};
 | 
			
		||||
  data: {[key: string]: any};
 | 
			
		||||
	_id: IdModel = null;
 | 
			
		||||
	date: Date;
 | 
			
		||||
	action: string;
 | 
			
		||||
	collection: string;
 | 
			
		||||
	conditions: {[key: string]: any};
 | 
			
		||||
	data: {[key: string]: any};
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { CustomFieldsModel } from './custom-fields.model';
 | 
			
		||||
 | 
			
		||||
describe('CustomFieldsModel', () => {
 | 
			
		||||
  it('should create an instance', () => {
 | 
			
		||||
    expect(new CustomFieldsModel()).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
	it('should create an instance', () => {
 | 
			
		||||
		expect(new CustomFieldsModel()).toBeTruthy();
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,6 @@ import {BaseModel} from './base.model';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export class CustomFieldsModel extends BaseModel {
 | 
			
		||||
  name = '';
 | 
			
		||||
  qty = 0;
 | 
			
		||||
	name = '';
 | 
			
		||||
	qty = 0;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { HelpModel } from './help.model';
 | 
			
		||||
 | 
			
		||||
describe('Help.Model', () => {
 | 
			
		||||
  it('should create an instance', () => {
 | 
			
		||||
    expect(new HelpModel()).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
	it('should create an instance', () => {
 | 
			
		||||
		expect(new HelpModel()).toBeTruthy();
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import {BaseModel} from './base.model';
 | 
			
		||||
 | 
			
		||||
export class HelpModel extends BaseModel {
 | 
			
		||||
  text = '';
 | 
			
		||||
  level = 'none';
 | 
			
		||||
	text = '';
 | 
			
		||||
	level = 'none';
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { MaterialModel } from './material.model';
 | 
			
		||||
 | 
			
		||||
describe('MaterialModel', () => {
 | 
			
		||||
  it('should create an instance', () => {
 | 
			
		||||
    expect(new MaterialModel()).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
	it('should create an instance', () => {
 | 
			
		||||
		expect(new MaterialModel()).toBeTruthy();
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -3,16 +3,16 @@ import {IdModel} from './id.model';
 | 
			
		||||
import {BaseModel} from './base.model';
 | 
			
		||||
 | 
			
		||||
export class MaterialModel extends BaseModel {
 | 
			
		||||
  _id: IdModel = null;
 | 
			
		||||
  name = '';
 | 
			
		||||
  supplier = '';
 | 
			
		||||
  group = '';
 | 
			
		||||
  properties: {material_template: string, [prop: string]: string} = {material_template: null};
 | 
			
		||||
  numbers: string[] = [''];
 | 
			
		||||
  selected = false;
 | 
			
		||||
  status = '';
 | 
			
		||||
	_id: IdModel = null;
 | 
			
		||||
	name = '';
 | 
			
		||||
	supplier = '';
 | 
			
		||||
	group = '';
 | 
			
		||||
	properties: {material_template: string, [prop: string]: string} = {material_template: null};
 | 
			
		||||
	numbers: string[] = [''];
 | 
			
		||||
	selected = false;
 | 
			
		||||
	status = '';
 | 
			
		||||
 | 
			
		||||
  sendFormat() {
 | 
			
		||||
    return pick(this, ['name', 'supplier', 'group', 'numbers', 'properties']);
 | 
			
		||||
  }
 | 
			
		||||
	sendFormat() {
 | 
			
		||||
		return pick(this, ['name', 'supplier', 'group', 'numbers', 'properties']);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { MeasurementModel } from './measurement.model';
 | 
			
		||||
 | 
			
		||||
describe('MeasurementModel', () => {
 | 
			
		||||
  it('should create an instance', () => {
 | 
			
		||||
    expect(new MeasurementModel()).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
	it('should create an instance', () => {
 | 
			
		||||
		expect(new MeasurementModel()).toBeTruthy();
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -4,28 +4,28 @@ import {IdModel} from './id.model';
 | 
			
		||||
import {BaseModel} from './base.model';
 | 
			
		||||
 | 
			
		||||
export class MeasurementModel extends BaseModel {
 | 
			
		||||
  _id: IdModel = null;
 | 
			
		||||
  sample_id: IdModel = null;
 | 
			
		||||
  measurement_template: IdModel;
 | 
			
		||||
  values: {[prop: string]: any} = {};
 | 
			
		||||
  status = '';
 | 
			
		||||
	_id: IdModel = null;
 | 
			
		||||
	sample_id: IdModel = null;
 | 
			
		||||
	measurement_template: IdModel;
 | 
			
		||||
	values: {[prop: string]: any} = {};
 | 
			
		||||
	status = '';
 | 
			
		||||
 | 
			
		||||
  constructor(measurementTemplate: IdModel = null) {
 | 
			
		||||
    super();
 | 
			
		||||
    this.measurement_template = measurementTemplate;
 | 
			
		||||
  }
 | 
			
		||||
	constructor(measurementTemplate: IdModel = null) {
 | 
			
		||||
		super();
 | 
			
		||||
		this.measurement_template = measurementTemplate;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  deserialize(input: any): this {
 | 
			
		||||
    Object.assign(this, input);
 | 
			
		||||
    Object.keys(this.values).forEach(key => {
 | 
			
		||||
      if (this.values[key] === null) {
 | 
			
		||||
        this.values[key] = '';
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    return this;
 | 
			
		||||
  }
 | 
			
		||||
	deserialize(input: any): this {
 | 
			
		||||
		Object.assign(this, input);
 | 
			
		||||
		Object.keys(this.values).forEach(key => {
 | 
			
		||||
			if (this.values[key] === null) {
 | 
			
		||||
				this.values[key] = '';
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
		return this;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  sendFormat(omitValues = []) {
 | 
			
		||||
    return omit(pick(this, ['sample_id', 'measurement_template', 'values']), omitValues);
 | 
			
		||||
  }
 | 
			
		||||
	sendFormat(omitValues = []) {
 | 
			
		||||
		return omit(pick(this, ['sample_id', 'measurement_template', 'values']), omitValues);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { ModelFileModel } from './model-file.model';
 | 
			
		||||
 | 
			
		||||
describe('ModelFile.Model', () => {
 | 
			
		||||
  it('should create an instance', () => {
 | 
			
		||||
    expect(new ModelFileModel()).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
	it('should create an instance', () => {
 | 
			
		||||
		expect(new ModelFileModel()).toBeTruthy();
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import {BaseModel} from './base.model';
 | 
			
		||||
 | 
			
		||||
export class ModelFileModel extends BaseModel {
 | 
			
		||||
  name = '';
 | 
			
		||||
  size = 0;
 | 
			
		||||
	name = '';
 | 
			
		||||
	size = 0;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { ModelItemModel } from './model-item.model';
 | 
			
		||||
 | 
			
		||||
describe('ModelItemModel', () => {
 | 
			
		||||
  it('should create an instance', () => {
 | 
			
		||||
    expect(new ModelItemModel()).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
	it('should create an instance', () => {
 | 
			
		||||
		expect(new ModelItemModel()).toBeTruthy();
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
import {BaseModel} from './base.model';
 | 
			
		||||
 | 
			
		||||
export class ModelItemModel extends BaseModel {
 | 
			
		||||
  group = '';
 | 
			
		||||
  models = [{
 | 
			
		||||
    name: '',
 | 
			
		||||
    url: ''
 | 
			
		||||
  }];
 | 
			
		||||
	group = '';
 | 
			
		||||
	models = [{
 | 
			
		||||
		name: '',
 | 
			
		||||
		url: ''
 | 
			
		||||
	}];
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { SampleModel } from './sample.model';
 | 
			
		||||
 | 
			
		||||
describe('SampleModel', () => {
 | 
			
		||||
  it('should create an instance', () => {
 | 
			
		||||
    expect(new SampleModel()).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
	it('should create an instance', () => {
 | 
			
		||||
		expect(new SampleModel()).toBeTruthy();
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -6,63 +6,63 @@ import {MeasurementModel} from './measurement.model';
 | 
			
		||||
import {BaseModel} from './base.model';
 | 
			
		||||
 | 
			
		||||
export class SampleModel extends BaseModel {
 | 
			
		||||
  _id: IdModel = null;
 | 
			
		||||
  color = '';
 | 
			
		||||
  number = '';
 | 
			
		||||
  type = '';
 | 
			
		||||
  batch = '';
 | 
			
		||||
  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;
 | 
			
		||||
  selected = false;
 | 
			
		||||
  notes: {
 | 
			
		||||
    comment: string,
 | 
			
		||||
    sample_references: {sample_id: IdModel, relation: string}[],
 | 
			
		||||
    custom_fields: {[prop: string]: string}
 | 
			
		||||
  } = {comment: '', sample_references: [], custom_fields: {}};
 | 
			
		||||
  status = '';
 | 
			
		||||
  added: Date = null;
 | 
			
		||||
	_id: IdModel = null;
 | 
			
		||||
	color = '';
 | 
			
		||||
	number = '';
 | 
			
		||||
	type = '';
 | 
			
		||||
	batch = '';
 | 
			
		||||
	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;
 | 
			
		||||
	selected = false;
 | 
			
		||||
	notes: {
 | 
			
		||||
		comment: string,
 | 
			
		||||
		sample_references: {sample_id: IdModel, relation: string}[],
 | 
			
		||||
		custom_fields: {[prop: string]: string}
 | 
			
		||||
	} = {comment: '', sample_references: [], custom_fields: {}};
 | 
			
		||||
	status = '';
 | 
			
		||||
	added: Date = null;
 | 
			
		||||
 | 
			
		||||
  deserialize(input: any): this {
 | 
			
		||||
    Object.assign(this, input);
 | 
			
		||||
    if (input.hasOwnProperty('material')) {
 | 
			
		||||
      this.material = new MaterialModel().deserialize(input.material);
 | 
			
		||||
      this.material_id = input.material._id;
 | 
			
		||||
    }
 | 
			
		||||
    if (input.hasOwnProperty('measurements')) {
 | 
			
		||||
      this.measurements = input.measurements.map(e => new MeasurementModel().deserialize(e));
 | 
			
		||||
    }
 | 
			
		||||
    if (input.hasOwnProperty('added')) {
 | 
			
		||||
      this.added = new Date(input.added);
 | 
			
		||||
    }
 | 
			
		||||
    return this;
 | 
			
		||||
  }
 | 
			
		||||
	deserialize(input: any): this {
 | 
			
		||||
		Object.assign(this, input);
 | 
			
		||||
		if (input.hasOwnProperty('material')) {
 | 
			
		||||
			this.material = new MaterialModel().deserialize(input.material);
 | 
			
		||||
			this.material_id = input.material._id;
 | 
			
		||||
		}
 | 
			
		||||
		if (input.hasOwnProperty('measurements')) {
 | 
			
		||||
			this.measurements = input.measurements.map(e => new MeasurementModel().deserialize(e));
 | 
			
		||||
		}
 | 
			
		||||
		if (input.hasOwnProperty('added')) {
 | 
			
		||||
			this.added = new Date(input.added);
 | 
			
		||||
		}
 | 
			
		||||
		return this;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  sendFormat(pickCondition = true) {
 | 
			
		||||
    const pickFields = ['color', 'type', 'batch', 'material_id', 'notes'];
 | 
			
		||||
    if (pickCondition) {
 | 
			
		||||
      pickFields.push('condition');
 | 
			
		||||
    }
 | 
			
		||||
    const tmp = pick(this.conditionTemplateCheck(), pickFields);
 | 
			
		||||
    Object.keys(tmp).forEach(key => {
 | 
			
		||||
      if (tmp[key] === undefined) {
 | 
			
		||||
        delete tmp[key];
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    if (this.material && this.material.name === undefined) {
 | 
			
		||||
      delete tmp.material_id;
 | 
			
		||||
    }
 | 
			
		||||
    return tmp;
 | 
			
		||||
  }
 | 
			
		||||
	sendFormat(pickCondition = true) {
 | 
			
		||||
		const pickFields = ['color', 'type', 'batch', 'material_id', 'notes'];
 | 
			
		||||
		if (pickCondition) {
 | 
			
		||||
			pickFields.push('condition');
 | 
			
		||||
		}
 | 
			
		||||
		const tmp = pick(this.conditionTemplateCheck(), pickFields);
 | 
			
		||||
		Object.keys(tmp).forEach(key => {
 | 
			
		||||
			if (tmp[key] === undefined) {
 | 
			
		||||
				delete tmp[key];
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
		if (this.material && this.material.name === undefined) {
 | 
			
		||||
			delete tmp.material_id;
 | 
			
		||||
		}
 | 
			
		||||
		return tmp;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  private conditionTemplateCheck() {
 | 
			
		||||
    const res = cloneDeep(this);
 | 
			
		||||
    if (res.condition.condition_template === null) {
 | 
			
		||||
      res.condition = {};
 | 
			
		||||
    }
 | 
			
		||||
    return res;
 | 
			
		||||
  }
 | 
			
		||||
	private conditionTemplateCheck() {
 | 
			
		||||
		const res = cloneDeep(this);
 | 
			
		||||
		if (res.condition.condition_template === null) {
 | 
			
		||||
			res.condition = {};
 | 
			
		||||
		}
 | 
			
		||||
		return res;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { TemplateModel } from './template.model';
 | 
			
		||||
 | 
			
		||||
describe('TemplateModel', () => {
 | 
			
		||||
  it('should create an instance', () => {
 | 
			
		||||
    expect(new TemplateModel()).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
	it('should create an instance', () => {
 | 
			
		||||
		expect(new TemplateModel()).toBeTruthy();
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,9 @@ import {IdModel} from './id.model';
 | 
			
		||||
import {BaseModel} from './base.model';
 | 
			
		||||
 | 
			
		||||
export class TemplateModel extends BaseModel {
 | 
			
		||||
  _id: IdModel = null;
 | 
			
		||||
  name = '';
 | 
			
		||||
  version = 0;
 | 
			
		||||
  first_id: IdModel = null;
 | 
			
		||||
  parameters: {name: string, range: {[prop: string]: any}, rangeString?: string}[] = [];
 | 
			
		||||
	_id: IdModel = null;
 | 
			
		||||
	name = '';
 | 
			
		||||
	version = 0;
 | 
			
		||||
	first_id: IdModel = null;
 | 
			
		||||
	parameters: {name: string, range: {[prop: string]: any}, rangeString?: string}[] = [];
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { UserModel } from './user.model';
 | 
			
		||||
 | 
			
		||||
describe('UserModel', () => {
 | 
			
		||||
  it('should create an instance', () => {
 | 
			
		||||
    expect(new UserModel()).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
	it('should create an instance', () => {
 | 
			
		||||
		expect(new UserModel()).toBeTruthy();
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -3,31 +3,31 @@ import {BaseModel} from './base.model';
 | 
			
		||||
import {IdModel} from './id.model';
 | 
			
		||||
 | 
			
		||||
export class UserModel extends BaseModel{
 | 
			
		||||
  _id: IdModel = null;
 | 
			
		||||
  name = '';
 | 
			
		||||
  origName = '';
 | 
			
		||||
  email = '';
 | 
			
		||||
  level = '';
 | 
			
		||||
  location = '';
 | 
			
		||||
  devices = [''];
 | 
			
		||||
  models = [''];
 | 
			
		||||
  status = 'new';
 | 
			
		||||
  edit = false;
 | 
			
		||||
	_id: IdModel = null;
 | 
			
		||||
	name = '';
 | 
			
		||||
	origName = '';
 | 
			
		||||
	email = '';
 | 
			
		||||
	level = '';
 | 
			
		||||
	location = '';
 | 
			
		||||
	devices = [''];
 | 
			
		||||
	models = [''];
 | 
			
		||||
	status = 'new';
 | 
			
		||||
	edit = false;
 | 
			
		||||
 | 
			
		||||
  deserialize(input: any): this {
 | 
			
		||||
    Object.assign(this, input);
 | 
			
		||||
    this.origName = this.name;
 | 
			
		||||
    return this;
 | 
			
		||||
  }
 | 
			
		||||
	deserialize(input: any): this {
 | 
			
		||||
		Object.assign(this, input);
 | 
			
		||||
		this.origName = this.name;
 | 
			
		||||
		return this;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  sendFormat(mode = 'user') {
 | 
			
		||||
    const keys = ['name', 'email', 'location', 'devices'];
 | 
			
		||||
    if (mode === 'admin') {
 | 
			
		||||
      keys.push('level');
 | 
			
		||||
      keys.push('models');
 | 
			
		||||
      this.devices = this.devices.filter(e => e);
 | 
			
		||||
      this.models = this.models.filter(e => e);
 | 
			
		||||
    }
 | 
			
		||||
    return pick(this, keys);
 | 
			
		||||
  }
 | 
			
		||||
	sendFormat(mode = 'user') {
 | 
			
		||||
		const keys = ['name', 'email', 'location', 'devices'];
 | 
			
		||||
		if (mode === 'admin') {
 | 
			
		||||
			keys.push('level');
 | 
			
		||||
			keys.push('models');
 | 
			
		||||
			this.devices = this.devices.filter(e => e);
 | 
			
		||||
			this.models = this.models.filter(e => e);
 | 
			
		||||
		}
 | 
			
		||||
		return pick(this, keys);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,8 @@ import { ObjectPipe } from './object.pipe';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
describe('ObjectPipe', () => {
 | 
			
		||||
  it('create an instance', () => {
 | 
			
		||||
    const pipe = new ObjectPipe();
 | 
			
		||||
    expect(pipe).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
	it('create an instance', () => {
 | 
			
		||||
		const pipe = new ObjectPipe();
 | 
			
		||||
		expect(pipe).toBeTruthy();
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -2,14 +2,14 @@ import { Pipe, PipeTransform } from '@angular/core';
 | 
			
		||||
import omit from 'lodash/omit';
 | 
			
		||||
 | 
			
		||||
@Pipe({
 | 
			
		||||
  name: 'object',
 | 
			
		||||
  pure: true
 | 
			
		||||
	name: 'object',
 | 
			
		||||
	pure: true
 | 
			
		||||
})
 | 
			
		||||
export class ObjectPipe implements PipeTransform {
 | 
			
		||||
 | 
			
		||||
  transform(value: object, omitValue: string[] = []): string {
 | 
			
		||||
    const res = omit(value, omitValue);
 | 
			
		||||
    return res && Object.keys(res).length ? JSON.stringify(res) : '';
 | 
			
		||||
  }
 | 
			
		||||
	transform(value: object, omitValue: string[] = []): string {
 | 
			
		||||
		const res = omit(value, omitValue);
 | 
			
		||||
		return res && Object.keys(res).length ? JSON.stringify(res) : '';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,8 @@ import { ParametersPipe } from './parameters.pipe';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
describe('ParametersPipe', () => {
 | 
			
		||||
  it('create an instance', () => {
 | 
			
		||||
    const pipe = new ParametersPipe();
 | 
			
		||||
    expect(pipe).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
	it('create an instance', () => {
 | 
			
		||||
		const pipe = new ParametersPipe();
 | 
			
		||||
		expect(pipe).toBeTruthy();
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,13 @@
 | 
			
		||||
import { Pipe, PipeTransform } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
@Pipe({
 | 
			
		||||
  name: 'parameters'
 | 
			
		||||
	name: 'parameters'
 | 
			
		||||
})
 | 
			
		||||
export class ParametersPipe implements PipeTransform {
 | 
			
		||||
 | 
			
		||||
  transform(value: {name: string, range: object}[]): string {
 | 
			
		||||
    return `{${value.map(e => `${e.name}: <${JSON.stringify(e.range).replace('{}', 'any')
 | 
			
		||||
      .replace(/["{}]/g, '')}>`).join(', ')}}`;
 | 
			
		||||
  }
 | 
			
		||||
	transform(value: {name: string, range: object}[]): string {
 | 
			
		||||
		return `{${value.map(e => `${e.name}: <${JSON.stringify(e.range).replace('{}', 'any')
 | 
			
		||||
							 .replace(/["{}]/g, '')}>`).join(', ')}}`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,93 +1,93 @@
 | 
			
		||||
<rb-tab-panel (tabChanged)="groupChange($event)">
 | 
			
		||||
  <ng-container *ngFor="let group of d.arr.modelGroups; index as i">
 | 
			
		||||
    <div *rbTabPanelItem="group.group; id: i"></div>
 | 
			
		||||
  </ng-container>
 | 
			
		||||
	<ng-container *ngFor="let group of d.arr.modelGroups; index as i">
 | 
			
		||||
		<div *rbTabPanelItem="group.group; id: i"></div>
 | 
			
		||||
	</ng-container>
 | 
			
		||||
</rb-tab-panel>
 | 
			
		||||
 | 
			
		||||
<rb-form-select label="Model" (change)="result = undefined" [(ngModel)]="activeModelIndex">
 | 
			
		||||
  <option *ngFor="let model of activeGroup.models; index as i" [value]="i">{{model.name}}</option>
 | 
			
		||||
	<option *ngFor="let model of activeGroup.models; index as i" [value]="i">{{model.name}}</option>
 | 
			
		||||
</rb-form-select>
 | 
			
		||||
 | 
			
		||||
<div *ngIf="result" class="result" [@inOut]>
 | 
			
		||||
  <ng-container *ngIf="multipleSamples; else singleSampleResult">
 | 
			
		||||
    <h4 *ngFor="let prediction of result.predictions; index as i">
 | 
			
		||||
      {{spectrumNames[i]}}:
 | 
			
		||||
      <span *ngFor="let predictionEntry of prediction">
 | 
			
		||||
        {{predictionEntry.category}} <span [ngStyle]="{color: predictionEntry.color}">{{predictionEntry.value}}</span> {{predictionEntry.label}}
 | 
			
		||||
      </span>
 | 
			
		||||
      <a [routerLink]='"."' fragment="disclaimer"><sup>#</sup></a>
 | 
			
		||||
    </h4>
 | 
			
		||||
  </ng-container>
 | 
			
		||||
  <ng-template #singleSampleResult>
 | 
			
		||||
    <h4>
 | 
			
		||||
      Average result:
 | 
			
		||||
    <span *ngFor="let predictionEntry of result.mean">
 | 
			
		||||
        {{predictionEntry.category}} <span [ngStyle]="{color: predictionEntry.color}">{{predictionEntry.value}}</span> {{predictionEntry.label}}  {{( predictionEntry.std !== '' ? ' (standard deviation: '+ predictionEntry.std+')' : '') }}
 | 
			
		||||
      </span>
 | 
			
		||||
    <a [routerLink]='"."' fragment="disclaimer"><sup>#</sup></a>
 | 
			
		||||
    </h4>
 | 
			
		||||
    <a class="rb-details-toggle" rbDetailsToggle #triggerDetails="rbDetailsToggle">Details</a>
 | 
			
		||||
    <div *ngIf="triggerDetails.open" class="space-below">
 | 
			
		||||
      <p *ngFor="let prediction of result.predictions; index as i">
 | 
			
		||||
        {{spectrumNames[i]}}:
 | 
			
		||||
        <span *ngFor="let predictionEntry of prediction">
 | 
			
		||||
          {{predictionEntry.category}} <span [ngStyle]="{color: predictionEntry.color}">{{predictionEntry.value}}</span> {{predictionEntry.label}}
 | 
			
		||||
        </span>
 | 
			
		||||
        <a [routerLink]='"."' fragment="disclaimer"><sup>#</sup></a>
 | 
			
		||||
      </p>
 | 
			
		||||
    </div>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
	<ng-container *ngIf="multipleSamples; else singleSampleResult">
 | 
			
		||||
		<h4 *ngFor="let prediction of result.predictions; index as i">
 | 
			
		||||
			{{spectrumNames[i]}}:
 | 
			
		||||
			<span *ngFor="let predictionEntry of prediction">
 | 
			
		||||
				{{predictionEntry.category}} <span [ngStyle]="{color: predictionEntry.color}">{{predictionEntry.value}}</span> {{predictionEntry.label}}
 | 
			
		||||
			</span>
 | 
			
		||||
			<a [routerLink]='"."' fragment="disclaimer"><sup>#</sup></a>
 | 
			
		||||
		</h4>
 | 
			
		||||
	</ng-container>
 | 
			
		||||
	<ng-template #singleSampleResult>
 | 
			
		||||
		<h4>
 | 
			
		||||
			Average result:
 | 
			
		||||
			<span *ngFor="let predictionEntry of result.mean">
 | 
			
		||||
				{{predictionEntry.category}} <span [ngStyle]="{color: predictionEntry.color}">{{predictionEntry.value}}</span> {{predictionEntry.label}}  {{( predictionEntry.std !== '' ? ' (standard deviation: '+ predictionEntry.std+')' : '') }}
 | 
			
		||||
			</span>
 | 
			
		||||
			<a [routerLink]='"."' fragment="disclaimer"><sup>#</sup></a>
 | 
			
		||||
		</h4>
 | 
			
		||||
		<a class="rb-details-toggle" rbDetailsToggle #triggerDetails="rbDetailsToggle">Details</a>
 | 
			
		||||
		<div *ngIf="triggerDetails.open" class="space-below">
 | 
			
		||||
			<p *ngFor="let prediction of result.predictions; index as i">
 | 
			
		||||
			{{spectrumNames[i]}}:
 | 
			
		||||
			<span *ngFor="let predictionEntry of prediction">
 | 
			
		||||
				{{predictionEntry.category}} <span [ngStyle]="{color: predictionEntry.color}">{{predictionEntry.value}}</span> {{predictionEntry.label}}
 | 
			
		||||
			</span>
 | 
			
		||||
			<a [routerLink]='"."' fragment="disclaimer"><sup>#</sup></a>
 | 
			
		||||
			</p>
 | 
			
		||||
		</div>
 | 
			
		||||
	</ng-template>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="file-input space-below">
 | 
			
		||||
  <rb-form-file name="spectrum-upload" label="spectrum file" maxSize="10000000" class="space-below" multiple
 | 
			
		||||
                (ngModelChange)="fileToArray($event)" placeholder="Select file or drag and drop" dragDrop ngModel>
 | 
			
		||||
  </rb-form-file>
 | 
			
		||||
	<rb-form-file name="spectrum-upload" label="spectrum file" maxSize="10000000" class="space-below" multiple
 | 
			
		||||
		(ngModelChange)="fileToArray($event)" placeholder="Select file or drag and drop" dragDrop ngModel>
 | 
			
		||||
	</rb-form-file>
 | 
			
		||||
 | 
			
		||||
  <rb-loading-spinner *ngIf="loading; else predictButton"></rb-loading-spinner>
 | 
			
		||||
  <ng-template #predictButton>
 | 
			
		||||
    <rb-icon-button icon="forward-right" mode="primary" *ngIf="spectrumNames.length; else placeholder"
 | 
			
		||||
                    (click)="loadPrediction()">
 | 
			
		||||
      Predict
 | 
			
		||||
    </rb-icon-button>
 | 
			
		||||
    <ng-template #placeholder><div></div></ng-template>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
	<rb-loading-spinner *ngIf="loading; else predictButton"></rb-loading-spinner>
 | 
			
		||||
	<ng-template #predictButton>
 | 
			
		||||
		<rb-icon-button icon="forward-right" mode="primary" *ngIf="spectrumNames.length; else placeholder"
 | 
			
		||||
			(click)="loadPrediction()">
 | 
			
		||||
			Predict
 | 
			
		||||
		</rb-icon-button>
 | 
			
		||||
		<ng-template #placeholder><div></div></ng-template>
 | 
			
		||||
	</ng-template>
 | 
			
		||||
 | 
			
		||||
  <div>
 | 
			
		||||
    Prediction of:
 | 
			
		||||
    <rb-form-radio name="multiple-samples" label="Single sample" [(ngModel)]="multipleSamples" [value]="false">
 | 
			
		||||
    </rb-form-radio>
 | 
			
		||||
    <rb-form-radio name="multiple-samples" label="Multiple samples" [(ngModel)]="multipleSamples" [value]="true">
 | 
			
		||||
    </rb-form-radio>
 | 
			
		||||
  </div>
 | 
			
		||||
	<div>
 | 
			
		||||
		Prediction of:
 | 
			
		||||
		<rb-form-radio name="multiple-samples" label="Single sample" [(ngModel)]="multipleSamples" [value]="false">
 | 
			
		||||
		</rb-form-radio>
 | 
			
		||||
		<rb-form-radio name="multiple-samples" label="Multiple samples" [(ngModel)]="multipleSamples" [value]="true">
 | 
			
		||||
		</rb-form-radio>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- CSV export -->
 | 
			
		||||
<rb-icon-button icon="forward-right" mode="secondary" (click)="exportCSV()" *ngIf="spectrumNames.length" style="margin-right: 0.5rem">
 | 
			
		||||
  Export to CSV
 | 
			
		||||
	Export to CSV
 | 
			
		||||
</rb-icon-button>
 | 
			
		||||
 | 
			
		||||
<!-- PDF exprot -->
 | 
			
		||||
<rb-icon-button icon="forward-right" mode="secondary" (click)="exportPDF()" *ngIf="spectrumNames.length">
 | 
			
		||||
  Export to PDF
 | 
			
		||||
	Export to PDF
 | 
			
		||||
</rb-icon-button>
 | 
			
		||||
 | 
			
		||||
<div class="dpt-chart space-below">
 | 
			
		||||
  <canvas baseChart
 | 
			
		||||
          class="dpt-chart"
 | 
			
		||||
          [datasets]="chart"
 | 
			
		||||
          [labels]="[]"
 | 
			
		||||
          [options]="chartOptions"
 | 
			
		||||
          [legend]="false"
 | 
			
		||||
          chartType="scatter">
 | 
			
		||||
  </canvas>
 | 
			
		||||
	<canvas baseChart
 | 
			
		||||
		class="dpt-chart"
 | 
			
		||||
		[datasets]="chart"
 | 
			
		||||
		[labels]="[]"
 | 
			
		||||
		[options]="chartOptions"
 | 
			
		||||
		[legend]="false"
 | 
			
		||||
		chartType="scatter">
 | 
			
		||||
	</canvas>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="shaded-container" id="disclaimer">
 | 
			
		||||
  <h4><sup>#</sup>Disclaimer: This tool is still under development</h4>
 | 
			
		||||
  <p>
 | 
			
		||||
    The prediction and classification of material parameters are validated only for certain conditions.
 | 
			
		||||
    These results may therefore under no circumstances be used to evaluate quality-relevant issues. <br>
 | 
			
		||||
    For more details please contact <a [href]="'mailto:' + d.contact.mail">{{d.contact.name}}</a>.
 | 
			
		||||
  </p>
 | 
			
		||||
	<h4><sup>#</sup>Disclaimer: This tool is still under development</h4>
 | 
			
		||||
	<p>
 | 
			
		||||
	The prediction and classification of material parameters are validated only for certain conditions.
 | 
			
		||||
	These results may therefore under no circumstances be used to evaluate quality-relevant issues. <br>
 | 
			
		||||
	For more details please contact <a [href]="'mailto:' + d.contact.mail">{{d.contact.name}}</a>.
 | 
			
		||||
	</p>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,20 @@
 | 
			
		||||
.dpt-chart {
 | 
			
		||||
  max-width: 800px;
 | 
			
		||||
  margin-left: auto;
 | 
			
		||||
  margin-right: auto;
 | 
			
		||||
  margin-top: 0.5rem;
 | 
			
		||||
	max-width: 800px;
 | 
			
		||||
	margin-left: auto;
 | 
			
		||||
	margin-right: auto;
 | 
			
		||||
	margin-top: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.file-input {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: 1fr auto;
 | 
			
		||||
  grid-column-gap: 1rem;
 | 
			
		||||
	display: grid;
 | 
			
		||||
	grid-template-columns: 1fr auto;
 | 
			
		||||
	grid-column-gap: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.result {
 | 
			
		||||
  margin: 30px 0;
 | 
			
		||||
	margin: 30px 0;
 | 
			
		||||
 | 
			
		||||
  h4 {
 | 
			
		||||
    margin-bottom: 1rem;
 | 
			
		||||
  }
 | 
			
		||||
	h4 {
 | 
			
		||||
		margin-bottom: 1rem;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,267 +15,266 @@ import * as pdfFonts from 'pdfmake/build/vfs_fonts';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-prediction',
 | 
			
		||||
  templateUrl: './prediction.component.html',
 | 
			
		||||
  styleUrls: ['./prediction.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}))
 | 
			
		||||
        ])
 | 
			
		||||
      ]
 | 
			
		||||
    )
 | 
			
		||||
  ]
 | 
			
		||||
	selector: 'app-prediction',
 | 
			
		||||
	templateUrl: './prediction.component.html',
 | 
			
		||||
	styleUrls: ['./prediction.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 PredictionComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  result: { predictions: any[], mean: any[] };  // Prediction result from python container
 | 
			
		||||
  loading = false;
 | 
			
		||||
  activeGroup: ModelItemModel = new ModelItemModel();
 | 
			
		||||
  activeModelIndex = 0;
 | 
			
		||||
  // If true, spectra belong to different samples, otherwise multiple spectra from the same sample are given
 | 
			
		||||
  multipleSamples = false;
 | 
			
		||||
  spectrumNames: string[] = [];
 | 
			
		||||
  spectrum: string[][] = [[]];
 | 
			
		||||
  flattenedSpectra = [];
 | 
			
		||||
  chart = [];
 | 
			
		||||
  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: {}}]
 | 
			
		||||
    },
 | 
			
		||||
    responsive: true,
 | 
			
		||||
    tooltips: {enabled: false},
 | 
			
		||||
    hover: {mode: null},
 | 
			
		||||
    maintainAspectRatio: true,
 | 
			
		||||
    plugins: {datalabels: {display: false}}
 | 
			
		||||
  };
 | 
			
		||||
	result: { predictions: any[], mean: any[] };  // Prediction result from python container
 | 
			
		||||
	loading = false;
 | 
			
		||||
	activeGroup: ModelItemModel = new ModelItemModel();
 | 
			
		||||
	activeModelIndex = 0;
 | 
			
		||||
	// If true, spectra belong to different samples, otherwise multiple spectra from the same sample are given
 | 
			
		||||
	multipleSamples = false;
 | 
			
		||||
	spectrumNames: string[] = [];
 | 
			
		||||
	spectrum: string[][] = [[]];
 | 
			
		||||
	flattenedSpectra = [];
 | 
			
		||||
	chart = [];
 | 
			
		||||
	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: {}}]
 | 
			
		||||
		},
 | 
			
		||||
		responsive: true,
 | 
			
		||||
		tooltips: {enabled: false},
 | 
			
		||||
		hover: {mode: null},
 | 
			
		||||
		maintainAspectRatio: true,
 | 
			
		||||
		plugins: {datalabels: {display: false}}
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private api: ApiService,
 | 
			
		||||
    public d: DataService,
 | 
			
		||||
    public login: LoginService
 | 
			
		||||
  ) {
 | 
			
		||||
    this.chart[0] = cloneDeep(this.chartInit);
 | 
			
		||||
  }
 | 
			
		||||
	constructor(
 | 
			
		||||
		private api: ApiService,
 | 
			
		||||
		public d: DataService,
 | 
			
		||||
		public login: LoginService
 | 
			
		||||
	) {
 | 
			
		||||
		this.chart[0] = cloneDeep(this.chartInit);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.d.load('modelGroups', () => {
 | 
			
		||||
      this.activeGroup = this.d.arr.modelGroups[0];
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
	ngOnInit(): void {
 | 
			
		||||
		this.d.load('modelGroups', () => {
 | 
			
		||||
			this.activeGroup = this.d.arr.modelGroups[0];
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  fileToArray(files) {
 | 
			
		||||
    this.loading = true;
 | 
			
		||||
    this.flattenedSpectra = [];
 | 
			
		||||
    this.chart = [];
 | 
			
		||||
    let load = files.length;
 | 
			
		||||
    this.spectrumNames = files.map(e => e.name);
 | 
			
		||||
    for (const i in files) {
 | 
			
		||||
      if (files.hasOwnProperty(i)) {
 | 
			
		||||
        const fileReader = new FileReader();
 | 
			
		||||
        fileReader.onload = () => {
 | 
			
		||||
          // Parse to database spectrum representation
 | 
			
		||||
          this.spectrum = fileReader.result.toString().split('\r\n').map(e => e.split(',').map(el => parseFloat(el)))
 | 
			
		||||
            .filter(el => el.length === 2) as any;
 | 
			
		||||
          // Flatten to format needed for prediction
 | 
			
		||||
          this.flattenedSpectra[i] = {labels: this.spectrum.map(e => e[0]), values: this.spectrum.map(e => e[1])};
 | 
			
		||||
          // Add to chart
 | 
			
		||||
          this.chart[i] = cloneDeep(this.chartInit);
 | 
			
		||||
          this.chart[i].data = this.spectrum.map(e => ({x: parseFloat(e[0]), y: parseFloat(e[1])}));
 | 
			
		||||
          load --;
 | 
			
		||||
          if (load <= 0) {  // All loaded
 | 
			
		||||
            this.loadPrediction();
 | 
			
		||||
          }
 | 
			
		||||
        };
 | 
			
		||||
        fileReader.readAsText(files[i]);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
	fileToArray(files) {
 | 
			
		||||
		this.loading = true;
 | 
			
		||||
		this.flattenedSpectra = [];
 | 
			
		||||
		this.chart = [];
 | 
			
		||||
		let load = files.length;
 | 
			
		||||
		this.spectrumNames = files.map(e => e.name);
 | 
			
		||||
		for (const i in files) {
 | 
			
		||||
			if (files.hasOwnProperty(i)) {
 | 
			
		||||
				const fileReader = new FileReader();
 | 
			
		||||
				fileReader.onload = () => {
 | 
			
		||||
					// Parse to database spectrum representation
 | 
			
		||||
					this.spectrum = fileReader.result.toString().split('\r\n').map(e => e.split(',').map(el => parseFloat(el)))
 | 
			
		||||
					.filter(el => el.length === 2) as any;
 | 
			
		||||
					// Flatten to format needed for prediction
 | 
			
		||||
					this.flattenedSpectra[i] = {labels: this.spectrum.map(e => e[0]), values: this.spectrum.map(e => e[1])};
 | 
			
		||||
					// Add to chart
 | 
			
		||||
					this.chart[i] = cloneDeep(this.chartInit);
 | 
			
		||||
					this.chart[i].data = this.spectrum.map(e => ({x: parseFloat(e[0]), y: parseFloat(e[1])}));
 | 
			
		||||
					load --;
 | 
			
		||||
					if (load <= 0) {  // All loaded
 | 
			
		||||
						this.loadPrediction();
 | 
			
		||||
					}
 | 
			
		||||
				};
 | 
			
		||||
				fileReader.readAsText(files[i]);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  loadPrediction() {
 | 
			
		||||
    this.loading = true;
 | 
			
		||||
    this.api.post<any>(this.activeGroup.models[this.activeModelIndex].url, this.flattenedSpectra, data => {
 | 
			
		||||
      let tmp = Object.entries(omit(data, ['mean', 'std', 'label']))  // Form: [[label, [{value, color}]]]
 | 
			
		||||
          .map((entry: any) => entry[1].map(e => ({category: entry[0], label: data.label[entry[0]], value: e.value, color: e.color})));  // Form: [[{category, label, value, color}]]
 | 
			
		||||
      this.result = {
 | 
			
		||||
        predictions: tmp[0].map((ignore, columnIndex) => tmp.map(row => row[columnIndex])),  // Transpose tmp
 | 
			
		||||
        mean: Object.entries(data.mean)
 | 
			
		||||
          .map((entry:any) => ({category: entry[0], label: data.label[entry[0]], value: entry[1].value, color: entry[1].color, std: data.std[entry[0]]}))  // Form: [{category, label, value, color}]
 | 
			
		||||
      };
 | 
			
		||||
      this.loading = false;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
	loadPrediction() {
 | 
			
		||||
		this.loading = true;
 | 
			
		||||
		this.api.post<any>(this.activeGroup.models[this.activeModelIndex].url, this.flattenedSpectra, data => {
 | 
			
		||||
			let tmp = Object.entries(omit(data, ['mean', 'std', 'label']))  // Form: [[label, [{value, color}]]]
 | 
			
		||||
			.map((entry: any) => entry[1].map(e => ({category: entry[0], label: data.label[entry[0]], value: e.value, color: e.color})));  // Form: [[{category, label, value, color}]]
 | 
			
		||||
			this.result = {
 | 
			
		||||
				predictions: tmp[0].map((ignore, columnIndex) => tmp.map(row => row[columnIndex])),  // Transpose tmp
 | 
			
		||||
				mean: Object.entries(data.mean)
 | 
			
		||||
				.map((entry:any) => ({category: entry[0], label: data.label[entry[0]], value: entry[1].value, color: entry[1].color, std: data.std[entry[0]]}))  // Form: [{category, label, value, color}]
 | 
			
		||||
			};
 | 
			
		||||
			this.loading = false;
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  groupChange(index) {  // Group was changed
 | 
			
		||||
    this.activeGroup = this.d.arr.modelGroups[index];
 | 
			
		||||
    this.activeModelIndex = 0;
 | 
			
		||||
    this.result = undefined;
 | 
			
		||||
  }
 | 
			
		||||
	groupChange(index) {  // Group was changed
 | 
			
		||||
		this.activeGroup = this.d.arr.modelGroups[index];
 | 
			
		||||
		this.activeModelIndex = 0;
 | 
			
		||||
		this.result = undefined;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  // Aggregates spectrum names and prediction values into an associative array
 | 
			
		||||
  prepareExport() {
 | 
			
		||||
    const zip = (a, b) => a.map((k, i) => [k, b[i]]);
 | 
			
		||||
    const values = this.result.predictions
 | 
			
		||||
      .map(prediction => prediction
 | 
			
		||||
           .filter(field => field.category === 'Prediction')
 | 
			
		||||
           .map(field => field.value));
 | 
			
		||||
    return zip(this.spectrumNames, values);
 | 
			
		||||
  }
 | 
			
		||||
	// Aggregates spectrum names and prediction values into an associative array
 | 
			
		||||
	prepareExport() {
 | 
			
		||||
		const zip = (a, b) => a.map((k, i) => [k, b[i]]);
 | 
			
		||||
		const values = this.result.predictions
 | 
			
		||||
		.map(prediction => prediction
 | 
			
		||||
			 .filter(field => field.category === 'Prediction')
 | 
			
		||||
			 .map(field => field.value));
 | 
			
		||||
			 return zip(this.spectrumNames, values);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  // Generates a timestamp suitable for file naming
 | 
			
		||||
  generateTimestamp() {
 | 
			
		||||
    let d = new Date();
 | 
			
		||||
    return d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate() + '--' + d.getHours() + "-" + d.getMinutes();
 | 
			
		||||
  }
 | 
			
		||||
	// Generates a timestamp suitable for file naming
 | 
			
		||||
	generateTimestamp() {
 | 
			
		||||
		let d = new Date();
 | 
			
		||||
		return d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate() + '--' + d.getHours() + "-" + d.getMinutes();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  // Converts the prediction results to a CSV file
 | 
			
		||||
  exportCSV() {
 | 
			
		||||
    const predictions = this.prepareExport();
 | 
			
		||||
    const csv = predictions.map(line => line.join(";")).join("\n");
 | 
			
		||||
    FileSaver.saveAs(new Blob([csv], { type: 'text/csv;charset=utf-8' }), 'predictions-' + this.generateTimestamp() + '.csv');
 | 
			
		||||
  }
 | 
			
		||||
	// Converts the prediction results to a CSV file
 | 
			
		||||
	exportCSV() {
 | 
			
		||||
		const predictions = this.prepareExport();
 | 
			
		||||
		const csv = predictions.map(line => line.join(";")).join("\n");
 | 
			
		||||
		FileSaver.saveAs(new Blob([csv], { type: 'text/csv;charset=utf-8' }), 'predictions-' + this.generateTimestamp() + '.csv');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  // Converts the prediction results to a PDF file
 | 
			
		||||
  exportPDF() {
 | 
			
		||||
    const doc = {
 | 
			
		||||
      content: [
 | 
			
		||||
        {
 | 
			
		||||
          text: 'DeFinMa - Decoding the Fingerprint of Materials by AI',
 | 
			
		||||
          style: 'header'
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          table: {
 | 
			
		||||
            widths: ['auto', '*', 'auto', 'auto', 'auto'],
 | 
			
		||||
            body: [
 | 
			
		||||
              [
 | 
			
		||||
                {text: new Date().toLocaleDateString(), style: 'tableHeader'},
 | 
			
		||||
                {text: 'Customer', style: 'tableHeader'},
 | 
			
		||||
                {text: 'Security class', style: 'tableHeader'},
 | 
			
		||||
                {text: 'Person in charge', style: 'tableHeader'},
 | 
			
		||||
                {text: 'Phone', style: 'tableHeader'}],
 | 
			
		||||
              [
 | 
			
		||||
                this.activeGroup.group,
 | 
			
		||||
                this.login.username,
 | 
			
		||||
                'Intern',
 | 
			
		||||
                {
 | 
			
		||||
                  stack: [
 | 
			
		||||
                    'CR/APS1-Lotter',
 | 
			
		||||
                    'CR/APS1-Lingenfelser'
 | 
			
		||||
                  ]
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  stack: [
 | 
			
		||||
                    '0711/811-49017',
 | 
			
		||||
                    '0711/811-6897'
 | 
			
		||||
                  ]
 | 
			
		||||
                }
 | 
			
		||||
              ]
 | 
			
		||||
            ]
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          text: 'Prediction of ' + this.activeGroup.group + ' (' + this.activeGroup.models[this.activeModelIndex].name + ')*',
 | 
			
		||||
          style: 'subheader'
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          text: this.result.mean.map(e => e.category + ' ' + e.value + ' ' + e.label + ' ' + (e.std !== '' ? (' (standard deviation: ' + e.std + ')') : ''))
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          table: {
 | 
			
		||||
            widths: ['*', 'auto'],
 | 
			
		||||
            body: [
 | 
			
		||||
              [{text: 'Input Data / Sample Name', style: 'tableHeader'}, {text: 'Prediction*', style: 'tableHeader'}]
 | 
			
		||||
            ].concat(this.prepareExport())
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          text: 'Reference Data',
 | 
			
		||||
          style: 'subheader'
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          image: document.getElementsByTagName('canvas')[0].toDataURL('image/png'),
 | 
			
		||||
          width: 500
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          table: {
 | 
			
		||||
            body: [[{
 | 
			
		||||
              stack: [
 | 
			
		||||
                {
 | 
			
		||||
                  text: '*Disclaimer: This tool is still under development and Testing',
 | 
			
		||||
                  style: 'subsubheader'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  text: [
 | 
			
		||||
                    'The prediction and classification of material parameters are validated only for certain conditions.',
 | 
			
		||||
                    'These results may therefore under no circumstances be used to evaluate quality-relevant issues.',
 | 
			
		||||
                    'For more details please contact ',
 | 
			
		||||
                    {
 | 
			
		||||
                      text: 'CR/APS1-Lingenfelser',
 | 
			
		||||
                      link: 'mailto:dominic.lingenfelser@bosch.com'
 | 
			
		||||
                    },
 | 
			
		||||
                    '.'
 | 
			
		||||
                  ]
 | 
			
		||||
                },
 | 
			
		||||
              ]
 | 
			
		||||
            }]],
 | 
			
		||||
          },
 | 
			
		||||
          margin: [25, 20]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          table: {
 | 
			
		||||
            widths: ['*', '*', 'auto'],
 | 
			
		||||
            body: [
 | 
			
		||||
              [{text: 'Pr\u00fcfung', style: 'tableHeader'}, {text: 'Freigabe', style: 'tableHeader'}, {text: 'Datum', style: 'tableHeader'}],
 | 
			
		||||
              ['CR/APS1-Lotter', 'CR/APS1-Lingenfelser', new Date().toLocaleDateString()]
 | 
			
		||||
            ],
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      footer: {
 | 
			
		||||
        text: '\u00a9 Alle Rechte bei Robert Bosch GmbH, auch f\u00fcr den Fall von Schutzreichtsanmeldungen. Jede Verf\u00fcgungsbefugnis, wie Kopier- und Weitergaberecht, bei uns.',
 | 
			
		||||
        fontSize: 8,
 | 
			
		||||
        alignment: 'center'
 | 
			
		||||
      },
 | 
			
		||||
      styles: {
 | 
			
		||||
        header: {
 | 
			
		||||
          fontSize: 18,
 | 
			
		||||
          bold: true,
 | 
			
		||||
          margin: [0, 10]
 | 
			
		||||
        },
 | 
			
		||||
        subheader: {
 | 
			
		||||
          fontSize: 15,
 | 
			
		||||
          bold: true,
 | 
			
		||||
          margin: [0, 8]
 | 
			
		||||
        },
 | 
			
		||||
        subsubheader: {
 | 
			
		||||
          bold: true,
 | 
			
		||||
          margin: [0, 5]
 | 
			
		||||
        },
 | 
			
		||||
        tableHeader: {
 | 
			
		||||
          bold: true,
 | 
			
		||||
          fontSize: 13,
 | 
			
		||||
          color: 'black'
 | 
			
		||||
		    }
 | 
			
		||||
      },
 | 
			
		||||
      pageMargins: [50, 50, 50, 15]
 | 
			
		||||
    };
 | 
			
		||||
	// Converts the prediction results to a PDF file
 | 
			
		||||
	exportPDF() {
 | 
			
		||||
		const doc = {
 | 
			
		||||
			content: [
 | 
			
		||||
				{
 | 
			
		||||
					text: 'DeFinMa - Decoding the Fingerprint of Materials by AI',
 | 
			
		||||
					style: 'header'
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					table: {
 | 
			
		||||
						widths: ['auto', '*', 'auto', 'auto', 'auto'],
 | 
			
		||||
						body: [[
 | 
			
		||||
							{text: new Date().toLocaleDateString(), style: 'tableHeader'},
 | 
			
		||||
							{text: 'Customer', style: 'tableHeader'},
 | 
			
		||||
							{text: 'Security class', style: 'tableHeader'},
 | 
			
		||||
							{text: 'Person in charge', style: 'tableHeader'},
 | 
			
		||||
							{text: 'Phone', style: 'tableHeader'}],
 | 
			
		||||
							[
 | 
			
		||||
								this.activeGroup.group,
 | 
			
		||||
								this.login.username,
 | 
			
		||||
								'Intern',
 | 
			
		||||
								{
 | 
			
		||||
									stack: [
 | 
			
		||||
										'CR/APS1-Lotter',
 | 
			
		||||
										'CR/APS1-Lingenfelser'
 | 
			
		||||
									]
 | 
			
		||||
								},
 | 
			
		||||
								{
 | 
			
		||||
									stack: [
 | 
			
		||||
										'0711/811-49017',
 | 
			
		||||
										'0711/811-6897'
 | 
			
		||||
									]
 | 
			
		||||
								}
 | 
			
		||||
							]
 | 
			
		||||
						]
 | 
			
		||||
					}
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					text: 'Prediction of ' + this.activeGroup.group + ' (' + this.activeGroup.models[this.activeModelIndex].name + ')*',
 | 
			
		||||
					style: 'subheader'
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					text: this.result.mean.map(e => e.category + ' ' + e.value + ' ' + e.label + ' ' + (e.std !== '' ? (' (standard deviation: ' + e.std + ')') : ''))
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					table: {
 | 
			
		||||
						widths: ['*', 'auto'],
 | 
			
		||||
						body: [
 | 
			
		||||
							[{text: 'Input Data / Sample Name', style: 'tableHeader'}, {text: 'Prediction*', style: 'tableHeader'}]
 | 
			
		||||
						].concat(this.prepareExport())
 | 
			
		||||
					}
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					text: 'Reference Data',
 | 
			
		||||
					style: 'subheader'
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					image: document.getElementsByTagName('canvas')[0].toDataURL('image/png'),
 | 
			
		||||
					width: 500
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					table: {
 | 
			
		||||
						body: [[{
 | 
			
		||||
							stack: [
 | 
			
		||||
								{
 | 
			
		||||
									text: '*Disclaimer: This tool is still under development and Testing',
 | 
			
		||||
									style: 'subsubheader'
 | 
			
		||||
								},
 | 
			
		||||
								{
 | 
			
		||||
									text: [
 | 
			
		||||
										'The prediction and classification of material parameters are validated only for certain conditions.',
 | 
			
		||||
										'These results may therefore under no circumstances be used to evaluate quality-relevant issues.',
 | 
			
		||||
										'For more details please contact ',
 | 
			
		||||
										{
 | 
			
		||||
											text: 'CR/APS1-Lingenfelser',
 | 
			
		||||
											link: 'mailto:dominic.lingenfelser@bosch.com'
 | 
			
		||||
										},
 | 
			
		||||
										'.'
 | 
			
		||||
									]
 | 
			
		||||
								},
 | 
			
		||||
							]
 | 
			
		||||
						}]],
 | 
			
		||||
					},
 | 
			
		||||
					margin: [25, 20]
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					table: {
 | 
			
		||||
						widths: ['*', '*', 'auto'],
 | 
			
		||||
						body: [
 | 
			
		||||
							[{text: 'Pr\u00fcfung', style: 'tableHeader'}, {text: 'Freigabe', style: 'tableHeader'}, {text: 'Datum', style: 'tableHeader'}],
 | 
			
		||||
							['CR/APS1-Lotter', 'CR/APS1-Lingenfelser', new Date().toLocaleDateString()]
 | 
			
		||||
						],
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			],
 | 
			
		||||
			footer: {
 | 
			
		||||
				text: '\u00a9 Alle Rechte bei Robert Bosch GmbH, auch f\u00fcr den Fall von Schutzreichtsanmeldungen. Jede Verf\u00fcgungsbefugnis, wie Kopier- und Weitergaberecht, bei uns.',
 | 
			
		||||
				fontSize: 8,
 | 
			
		||||
				alignment: 'center'
 | 
			
		||||
			},
 | 
			
		||||
			styles: {
 | 
			
		||||
				header: {
 | 
			
		||||
					fontSize: 18,
 | 
			
		||||
					bold: true,
 | 
			
		||||
					margin: [0, 10]
 | 
			
		||||
				},
 | 
			
		||||
				subheader: {
 | 
			
		||||
					fontSize: 15,
 | 
			
		||||
					bold: true,
 | 
			
		||||
					margin: [0, 8]
 | 
			
		||||
				},
 | 
			
		||||
				subsubheader: {
 | 
			
		||||
					bold: true,
 | 
			
		||||
					margin: [0, 5]
 | 
			
		||||
				},
 | 
			
		||||
				tableHeader: {
 | 
			
		||||
					bold: true,
 | 
			
		||||
					fontSize: 13,
 | 
			
		||||
					color: 'black'
 | 
			
		||||
				}
 | 
			
		||||
			},
 | 
			
		||||
			pageMargins: [50, 50, 50, 15]
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
    pdfMake.createPdf(doc).download('predictions-' + this.generateTimestamp() + '.pdf');
 | 
			
		||||
  }
 | 
			
		||||
		pdfMake.createPdf(doc).download('predictions-' + this.generateTimestamp() + '.pdf');
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,25 +2,25 @@ import { Injectable } from '@angular/core';
 | 
			
		||||
import {Observable, Subject} from 'rxjs';
 | 
			
		||||
 | 
			
		||||
@Injectable({
 | 
			
		||||
  providedIn: 'root'
 | 
			
		||||
	providedIn: 'root'
 | 
			
		||||
})
 | 
			
		||||
export class ArrayInputHelperService {
 | 
			
		||||
 | 
			
		||||
  com: Subject<{ id: string, index: number, value: any }> = new Subject();
 | 
			
		||||
	com: Subject<{ id: string, index: number, value: any }> = new Subject();
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
	constructor() { }
 | 
			
		||||
 | 
			
		||||
  values(id: string) {  // Observable which returns new values as they come for subscribed id
 | 
			
		||||
    return new Observable<{index: number, value: any}>(observer => {
 | 
			
		||||
      this.com.subscribe(data => {
 | 
			
		||||
        if (data.id === id) {
 | 
			
		||||
          observer.next({index: data.index, value: data.value});
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
	values(id: string) {  // Observable which returns new values as they come for subscribed id
 | 
			
		||||
		return new Observable<{index: number, value: any}>(observer => {
 | 
			
		||||
			this.com.subscribe(data => {
 | 
			
		||||
				if (data.id === id) {
 | 
			
		||||
					observer.next({index: data.index, value: data.value});
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  newValue(id: string, index: number, value: any) {  // Set new value
 | 
			
		||||
    this.com.next({id, index, value});
 | 
			
		||||
  }
 | 
			
		||||
	newValue(id: string, index: number, value: any) {  // Set new value
 | 
			
		||||
		this.com.next({id, index, value});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,3 @@
 | 
			
		||||
<ng-container *ngFor="let ignore of [].constructor(values.length); index as i">
 | 
			
		||||
  <ng-container *ngTemplateOutlet="item.templateRef; context: {$implicit: {i: i, value: values[i]}}"></ng-container>
 | 
			
		||||
	<ng-container *ngTemplateOutlet="item.templateRef; context: {$implicit: {i: i, value: values[i]}}"></ng-container>
 | 
			
		||||
</ng-container>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,13 @@
 | 
			
		||||
import {
 | 
			
		||||
  AfterViewInit,
 | 
			
		||||
  Component,
 | 
			
		||||
  ContentChild,
 | 
			
		||||
  Directive,
 | 
			
		||||
  forwardRef,
 | 
			
		||||
  HostListener,
 | 
			
		||||
  Input,
 | 
			
		||||
  OnInit,
 | 
			
		||||
  TemplateRef
 | 
			
		||||
	AfterViewInit,
 | 
			
		||||
	Component,
 | 
			
		||||
	ContentChild,
 | 
			
		||||
	Directive,
 | 
			
		||||
	forwardRef,
 | 
			
		||||
	HostListener,
 | 
			
		||||
	Input,
 | 
			
		||||
	OnInit,
 | 
			
		||||
	TemplateRef
 | 
			
		||||
} from '@angular/core';
 | 
			
		||||
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
 | 
			
		||||
import cloneDeep from 'lodash/cloneDeep';
 | 
			
		||||
@@ -15,150 +15,150 @@ import {ArrayInputHelperService} from './array-input-helper.service';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@Directive({  // Directive for template and input values
 | 
			
		||||
  // tslint:disable-next-line:directive-selector
 | 
			
		||||
  selector: '[rbArrayInputItem]'
 | 
			
		||||
	// tslint:disable-next-line:directive-selector
 | 
			
		||||
	selector: '[rbArrayInputItem]'
 | 
			
		||||
})
 | 
			
		||||
export class RbArrayInputItemDirective {
 | 
			
		||||
  constructor(public templateRef: TemplateRef<any>) {
 | 
			
		||||
  }
 | 
			
		||||
	constructor(public templateRef: TemplateRef<any>) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Directive({  // Directive for change detection
 | 
			
		||||
  // tslint:disable-next-line:directive-selector
 | 
			
		||||
  selector: '[rbArrayInputListener]'
 | 
			
		||||
	// tslint:disable-next-line:directive-selector
 | 
			
		||||
	selector: '[rbArrayInputListener]'
 | 
			
		||||
})
 | 
			
		||||
export class RbArrayInputListenerDirective {
 | 
			
		||||
 | 
			
		||||
  @Input() rbArrayInputListener: string;
 | 
			
		||||
  @Input() index;
 | 
			
		||||
	@Input() rbArrayInputListener: string;
 | 
			
		||||
	@Input() index;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private helperService: ArrayInputHelperService
 | 
			
		||||
  ) { }
 | 
			
		||||
	constructor(
 | 
			
		||||
		private helperService: ArrayInputHelperService
 | 
			
		||||
	) { }
 | 
			
		||||
 | 
			
		||||
  @HostListener('ngModelChange', ['$event'])
 | 
			
		||||
  onChange(event) {  // Emit new value
 | 
			
		||||
    this.helperService.newValue(this.rbArrayInputListener, this.index, event);
 | 
			
		||||
  }
 | 
			
		||||
	@HostListener('ngModelChange', ['$event'])
 | 
			
		||||
	onChange(event) {  // Emit new value
 | 
			
		||||
		this.helperService.newValue(this.rbArrayInputListener, this.index, event);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  // tslint:disable-next-line:component-selector
 | 
			
		||||
  selector: 'rb-array-input',
 | 
			
		||||
  templateUrl: './rb-array-input.component.html',
 | 
			
		||||
  styleUrls: ['./rb-array-input.component.scss'],
 | 
			
		||||
  providers: [{provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RbArrayInputComponent), multi: true}]
 | 
			
		||||
	// tslint:disable-next-line:component-selector
 | 
			
		||||
	selector: 'rb-array-input',
 | 
			
		||||
	templateUrl: './rb-array-input.component.html',
 | 
			
		||||
	styleUrls: ['./rb-array-input.component.scss'],
 | 
			
		||||
	providers: [{provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RbArrayInputComponent), multi: true}]
 | 
			
		||||
})
 | 
			
		||||
export class RbArrayInputComponent implements ControlValueAccessor, OnInit, AfterViewInit {
 | 
			
		||||
 | 
			
		||||
  pushTemplate: any = '';  // Array element template
 | 
			
		||||
  @Input('pushTemplate') set _pushTemplate(value) {
 | 
			
		||||
    this.pushTemplate = value;
 | 
			
		||||
    if (this.values.length) {
 | 
			
		||||
      this.updateArray();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  @Input() pushPath: string = null;
 | 
			
		||||
	pushTemplate: any = '';  // Array element template
 | 
			
		||||
	@Input('pushTemplate') set _pushTemplate(value) {
 | 
			
		||||
		this.pushTemplate = value;
 | 
			
		||||
		if (this.values.length) {
 | 
			
		||||
			this.updateArray();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	@Input() pushPath: string = null;
 | 
			
		||||
 | 
			
		||||
  @ContentChild(RbArrayInputItemDirective) item: RbArrayInputItemDirective;
 | 
			
		||||
  @ContentChild(RbArrayInputListenerDirective) item2: RbArrayInputListenerDirective;
 | 
			
		||||
	@ContentChild(RbArrayInputItemDirective) item: RbArrayInputItemDirective;
 | 
			
		||||
	@ContentChild(RbArrayInputListenerDirective) item2: RbArrayInputListenerDirective;
 | 
			
		||||
 | 
			
		||||
  values = [];  // Main array to display
 | 
			
		||||
	values = [];  // Main array to display
 | 
			
		||||
 | 
			
		||||
  onChange = (ignore?: any): void => {};
 | 
			
		||||
  onTouched = (ignore?: any): void => {};
 | 
			
		||||
	onChange = (ignore?: any): void => {};
 | 
			
		||||
	onTouched = (ignore?: any): void => {};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private helperService: ArrayInputHelperService
 | 
			
		||||
  ) { }
 | 
			
		||||
	constructor(
 | 
			
		||||
		private helperService: ArrayInputHelperService
 | 
			
		||||
	) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
  }
 | 
			
		||||
	ngOnInit(): void {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  ngAfterViewInit() {
 | 
			
		||||
    setTimeout(() => {  // Needed to find reference
 | 
			
		||||
      this.helperService.values(this.item2.rbArrayInputListener).subscribe(data => {  // Action on value change
 | 
			
		||||
        // Assign value
 | 
			
		||||
        if (this.pushPath) {
 | 
			
		||||
          this.values[data.index][this.pushPath] = data.value;
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
          this.values[data.index] = data.value;
 | 
			
		||||
        }
 | 
			
		||||
        this.updateArray();
 | 
			
		||||
      });
 | 
			
		||||
    }, 0);
 | 
			
		||||
  }
 | 
			
		||||
	ngAfterViewInit() {
 | 
			
		||||
		setTimeout(() => {  // Needed to find reference
 | 
			
		||||
			this.helperService.values(this.item2.rbArrayInputListener).subscribe(data => {  // Action on value change
 | 
			
		||||
				// Assign value
 | 
			
		||||
				if (this.pushPath) {
 | 
			
		||||
					this.values[data.index][this.pushPath] = data.value;
 | 
			
		||||
				}
 | 
			
		||||
				else {
 | 
			
		||||
					this.values[data.index] = data.value;
 | 
			
		||||
				}
 | 
			
		||||
				this.updateArray();
 | 
			
		||||
			});
 | 
			
		||||
		}, 0);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  updateArray() {
 | 
			
		||||
    let res;
 | 
			
		||||
    // Adjust fields if pushTemplate is specified
 | 
			
		||||
    if (this.pushTemplate !== null) {
 | 
			
		||||
      if (this.pushPath) {
 | 
			
		||||
        // Remove last element if last two are empty
 | 
			
		||||
        if (this.values[this.values.length - 1][this.pushPath] === '' &&
 | 
			
		||||
          this.values[this.values.length - 2][this.pushPath] === '') {
 | 
			
		||||
          this.values.pop();
 | 
			
		||||
        }
 | 
			
		||||
        // Add element if last all are filled
 | 
			
		||||
        else if (this.values.filter(e => e[this.pushPath] !== '').length === this.values.length) {
 | 
			
		||||
          this.values.push(cloneDeep(this.pushTemplate));
 | 
			
		||||
        }
 | 
			
		||||
        res = this.values.filter(e => e[this.pushPath] !== '');
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        // Remove last element if last two are empty
 | 
			
		||||
        if (this.values[this.values.length - 1] === '' && this.values[this.values.length - 2] === '') {
 | 
			
		||||
          this.values.pop();
 | 
			
		||||
        }
 | 
			
		||||
        else if (this.values.filter(e => e !== '').length === this.values.length) {  // Add element if all are is filled
 | 
			
		||||
          this.values.push(cloneDeep(this.pushTemplate));
 | 
			
		||||
        }
 | 
			
		||||
        res = this.values.filter(e => e !== '');
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      this.values = [this.values[0]];
 | 
			
		||||
      res = this.values;
 | 
			
		||||
    }
 | 
			
		||||
    if (!res.length) {
 | 
			
		||||
      res = [''];
 | 
			
		||||
    }
 | 
			
		||||
    this.onChange(res);  // Trigger ngModel with filled elements
 | 
			
		||||
  }
 | 
			
		||||
	updateArray() {
 | 
			
		||||
		let res;
 | 
			
		||||
		// Adjust fields if pushTemplate is specified
 | 
			
		||||
		if (this.pushTemplate !== null) {
 | 
			
		||||
			if (this.pushPath) {
 | 
			
		||||
				// Remove last element if last two are empty
 | 
			
		||||
				if (this.values[this.values.length - 1][this.pushPath] === '' &&
 | 
			
		||||
					this.values[this.values.length - 2][this.pushPath] === '') {
 | 
			
		||||
					this.values.pop();
 | 
			
		||||
				}
 | 
			
		||||
				// Add element if last all are filled
 | 
			
		||||
				else if (this.values.filter(e => e[this.pushPath] !== '').length === this.values.length) {
 | 
			
		||||
					this.values.push(cloneDeep(this.pushTemplate));
 | 
			
		||||
				}
 | 
			
		||||
				res = this.values.filter(e => e[this.pushPath] !== '');
 | 
			
		||||
			}
 | 
			
		||||
			else {
 | 
			
		||||
				// Remove last element if last two are empty
 | 
			
		||||
				if (this.values[this.values.length - 1] === '' && this.values[this.values.length - 2] === '') {
 | 
			
		||||
					this.values.pop();
 | 
			
		||||
				}
 | 
			
		||||
				else if (this.values.filter(e => e !== '').length === this.values.length) {  // Add element if all are is filled
 | 
			
		||||
					this.values.push(cloneDeep(this.pushTemplate));
 | 
			
		||||
				}
 | 
			
		||||
				res = this.values.filter(e => e !== '');
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
			this.values = [this.values[0]];
 | 
			
		||||
			res = this.values;
 | 
			
		||||
		}
 | 
			
		||||
		if (!res.length) {
 | 
			
		||||
			res = [''];
 | 
			
		||||
		}
 | 
			
		||||
		this.onChange(res);  // Trigger ngModel with filled elements
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  writeValue(obj: any) {  // Add empty value on init
 | 
			
		||||
    if (obj) {
 | 
			
		||||
      if (this.pushTemplate !== null) {
 | 
			
		||||
        // Filter out empty values
 | 
			
		||||
        if (this.pushPath) {
 | 
			
		||||
          this.values = [...obj.filter(e => e[this.pushPath] !== ''), cloneDeep(this.pushTemplate)];
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
          this.values = [...obj.filter(e => e !== ''), cloneDeep(this.pushTemplate)];
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        this.values = obj;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      if (this.pushTemplate !== null) {
 | 
			
		||||
        this.values = [cloneDeep(this.pushTemplate)];
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        this.values = [''];
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
	writeValue(obj: any) {  // Add empty value on init
 | 
			
		||||
		if (obj) {
 | 
			
		||||
			if (this.pushTemplate !== null) {
 | 
			
		||||
				// Filter out empty values
 | 
			
		||||
				if (this.pushPath) {
 | 
			
		||||
					this.values = [...obj.filter(e => e[this.pushPath] !== ''), cloneDeep(this.pushTemplate)];
 | 
			
		||||
				}
 | 
			
		||||
				else {
 | 
			
		||||
					this.values = [...obj.filter(e => e !== ''), cloneDeep(this.pushTemplate)];
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			else {
 | 
			
		||||
				this.values = obj;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
			if (this.pushTemplate !== null) {
 | 
			
		||||
				this.values = [cloneDeep(this.pushTemplate)];
 | 
			
		||||
			}
 | 
			
		||||
			else {
 | 
			
		||||
				this.values = [''];
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  registerOnChange(fn: any) {
 | 
			
		||||
    this.onChange = fn;
 | 
			
		||||
  }
 | 
			
		||||
	registerOnChange(fn: any) {
 | 
			
		||||
		this.onChange = fn;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  registerOnTouched(fn: any) {
 | 
			
		||||
    this.onTouched = fn;
 | 
			
		||||
  }
 | 
			
		||||
	registerOnTouched(fn: any) {
 | 
			
		||||
		this.onTouched = fn;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ import { NgModule } from '@angular/core';
 | 
			
		||||
import { CommonModule } from '@angular/common';
 | 
			
		||||
import { RbTableComponent } from './rb-table/rb-table.component';
 | 
			
		||||
import {RbArrayInputComponent, RbArrayInputListenerDirective, RbArrayInputItemDirective} from
 | 
			
		||||
    './rb-array-input/rb-array-input.component';
 | 
			
		||||
'./rb-array-input/rb-array-input.component';
 | 
			
		||||
import {RbUiComponentsModule} from '@inst-iot/bosch-angular-ui-components';
 | 
			
		||||
import {FormsModule} from '@angular/forms';
 | 
			
		||||
import { RbIconButtonComponent } from './rb-icon-button/rb-icon-button.component';
 | 
			
		||||
@@ -10,24 +10,24 @@ import { RbIconButtonComponent } from './rb-icon-button/rb-icon-button.component
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations: [
 | 
			
		||||
    RbTableComponent,
 | 
			
		||||
    RbArrayInputComponent,
 | 
			
		||||
    RbArrayInputListenerDirective,
 | 
			
		||||
    RbArrayInputItemDirective,
 | 
			
		||||
    RbIconButtonComponent
 | 
			
		||||
  ],
 | 
			
		||||
  imports: [
 | 
			
		||||
    CommonModule,
 | 
			
		||||
    FormsModule,
 | 
			
		||||
    RbUiComponentsModule
 | 
			
		||||
  ],
 | 
			
		||||
  exports: [
 | 
			
		||||
    RbTableComponent,
 | 
			
		||||
    RbArrayInputComponent,
 | 
			
		||||
    RbArrayInputListenerDirective,
 | 
			
		||||
    RbArrayInputItemDirective,
 | 
			
		||||
    RbIconButtonComponent
 | 
			
		||||
  ]
 | 
			
		||||
	declarations: [
 | 
			
		||||
		RbTableComponent,
 | 
			
		||||
		RbArrayInputComponent,
 | 
			
		||||
		RbArrayInputListenerDirective,
 | 
			
		||||
		RbArrayInputItemDirective,
 | 
			
		||||
		RbIconButtonComponent
 | 
			
		||||
	],
 | 
			
		||||
	imports: [
 | 
			
		||||
		CommonModule,
 | 
			
		||||
		FormsModule,
 | 
			
		||||
		RbUiComponentsModule
 | 
			
		||||
	],
 | 
			
		||||
	exports: [
 | 
			
		||||
		RbTableComponent,
 | 
			
		||||
		RbArrayInputComponent,
 | 
			
		||||
		RbArrayInputListenerDirective,
 | 
			
		||||
		RbArrayInputItemDirective,
 | 
			
		||||
		RbIconButtonComponent
 | 
			
		||||
	]
 | 
			
		||||
})
 | 
			
		||||
export class RbCustomInputsModule { }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
<button class="rb-btn rb" [ngClass]="'rb-' + mode" [type]="type" [disabled]="disabled">
 | 
			
		||||
  <span class="rb-ic" [ngClass]="'rb-ic-' + icon" [class.icon-space]="iconOnly === undefined"></span>
 | 
			
		||||
  <ng-content></ng-content>
 | 
			
		||||
	<span class="rb-ic" [ngClass]="'rb-ic-' + icon" [class.icon-space]="iconOnly === undefined"></span>
 | 
			
		||||
	<ng-content></ng-content>
 | 
			
		||||
</button>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
.icon-space {
 | 
			
		||||
  margin-right: 0.1em !important;
 | 
			
		||||
	margin-right: 0.1em !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button.rb-btn > span:not(.icon-space) {
 | 
			
		||||
  margin-right: -10px;
 | 
			
		||||
  margin-left: -10px;
 | 
			
		||||
	margin-right: -10px;
 | 
			
		||||
	margin-left: -10px;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,22 +2,22 @@ import {Component, Input, OnInit} from '@angular/core';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  // tslint:disable-next-line:component-selector
 | 
			
		||||
  selector: 'rb-icon-button',
 | 
			
		||||
  templateUrl: './rb-icon-button.component.html',
 | 
			
		||||
  styleUrls: ['./rb-icon-button.component.scss']
 | 
			
		||||
	// tslint:disable-next-line:component-selector
 | 
			
		||||
	selector: 'rb-icon-button',
 | 
			
		||||
	templateUrl: './rb-icon-button.component.html',
 | 
			
		||||
	styleUrls: ['./rb-icon-button.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class RbIconButtonComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  @Input() icon: string;
 | 
			
		||||
  @Input() mode: string;
 | 
			
		||||
  @Input() iconOnly;
 | 
			
		||||
  @Input() disabled;
 | 
			
		||||
  @Input() type = 'button';
 | 
			
		||||
	@Input() icon: string;
 | 
			
		||||
	@Input() mode: string;
 | 
			
		||||
	@Input() iconOnly;
 | 
			
		||||
	@Input() disabled;
 | 
			
		||||
	@Input() type = 'button';
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
	constructor() { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
  }
 | 
			
		||||
	ngOnInit(): void {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<div class="table-wrapper space-below" [class.scroll-top]="scrollTop !== undefined">
 | 
			
		||||
  <table [class.ellipsis]="ellipsis !== undefined">
 | 
			
		||||
    <ng-content></ng-content>
 | 
			
		||||
  </table>
 | 
			
		||||
	<table [class.ellipsis]="ellipsis !== undefined">
 | 
			
		||||
		<ng-content></ng-content>
 | 
			
		||||
	</table>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,38 +1,38 @@
 | 
			
		||||
@import "~@inst-iot/bosch-angular-ui-components/styles/variables/colors";
 | 
			
		||||
 | 
			
		||||
.table-wrapper.scroll-top {
 | 
			
		||||
  overflow-x: auto;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
	overflow-x: auto;
 | 
			
		||||
	width: 100%;
 | 
			
		||||
 | 
			
		||||
  &, & > table {  // scrollbar at the top
 | 
			
		||||
    transform:rotateX(180deg);
 | 
			
		||||
    -ms-transform:rotateX(180deg); /* IE 9 */
 | 
			
		||||
    -webkit-transform:rotateX(180deg); /* Safari and Chrome */
 | 
			
		||||
  }
 | 
			
		||||
	&, & > table {  // scrollbar at the top
 | 
			
		||||
		transform:rotateX(180deg);
 | 
			
		||||
		-ms-transform:rotateX(180deg); /* IE 9 */
 | 
			
		||||
		-webkit-transform:rotateX(180deg); /* Safari and Chrome */
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  border-collapse: collapse;
 | 
			
		||||
	width: 100%;
 | 
			
		||||
	border-collapse: collapse;
 | 
			
		||||
 | 
			
		||||
  ::ng-deep tr {
 | 
			
		||||
    border-bottom: 1px solid $color-gray-mercury;
 | 
			
		||||
	::ng-deep tr {
 | 
			
		||||
		border-bottom: 1px solid $color-gray-mercury;
 | 
			
		||||
 | 
			
		||||
    ::ng-deep td, ::ng-deep th {
 | 
			
		||||
      padding: 8px 5px;
 | 
			
		||||
    }
 | 
			
		||||
		::ng-deep td, ::ng-deep th {
 | 
			
		||||
			padding: 8px 5px;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
    ::ng-deep th {
 | 
			
		||||
      text-align: left;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
		::ng-deep th {
 | 
			
		||||
			text-align: left;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table.ellipsis {
 | 
			
		||||
  ::ng-deep td, ::ng-deep th {
 | 
			
		||||
    text-overflow: ellipsis;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    max-width: 200px;
 | 
			
		||||
  }
 | 
			
		||||
	::ng-deep td, ::ng-deep th {
 | 
			
		||||
		text-overflow: ellipsis;
 | 
			
		||||
		white-space: nowrap;
 | 
			
		||||
		overflow: hidden;
 | 
			
		||||
		max-width: 200px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,19 @@
 | 
			
		||||
import {Component, Input, OnInit} from '@angular/core';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  // tslint:disable-next-line:component-selector
 | 
			
		||||
  selector: 'rb-table',
 | 
			
		||||
  templateUrl: './rb-table.component.html',
 | 
			
		||||
  styleUrls: ['./rb-table.component.scss']
 | 
			
		||||
	// tslint:disable-next-line:component-selector
 | 
			
		||||
	selector: 'rb-table',
 | 
			
		||||
	templateUrl: './rb-table.component.html',
 | 
			
		||||
	styleUrls: ['./rb-table.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class RbTableComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  @Input() scrollTop;
 | 
			
		||||
  @Input() ellipsis;
 | 
			
		||||
	@Input() scrollTop;
 | 
			
		||||
	@Input() ellipsis;
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
	constructor() { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
  }
 | 
			
		||||
	ngOnInit(): void {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,335 +4,335 @@
 | 
			
		||||
 | 
			
		||||
<ng-template #content>
 | 
			
		||||
 | 
			
		||||
<!--BASE-->
 | 
			
		||||
	<!--BASE-->
 | 
			
		||||
 | 
			
		||||
  <form #sampleForm="ngForm" *ngIf="view.base">
 | 
			
		||||
    <div class="sample">
 | 
			
		||||
      <div>
 | 
			
		||||
        <rb-form-input name="materialname" label="product name" [rbDebounceTime]="0" [rbInitialOpen]="true"
 | 
			
		||||
                       [rbFormInputAutocomplete]="autocomplete.bind(this, materialNames)" appValidate="stringOf"
 | 
			
		||||
                       (keydown)="preventDefault($event)" (ngModelChange)="findMaterial($event)" ngModel
 | 
			
		||||
                       [appValidateArgs]="[materialNames]" required [(ngModel)]="material.name" [autofocus]="true"
 | 
			
		||||
                       *ngIf="mode === 'new' || (baseSample.material && baseSample.material.name !== undefined)"
 | 
			
		||||
                       title="trade name of the material, eg. Ultradur B4300 G6">
 | 
			
		||||
          <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
          <ng-template rbFormValidationMessage="failure">Unknown material, add properties for new material</ng-template>
 | 
			
		||||
        </rb-form-input>
 | 
			
		||||
        <rb-icon-button class="set-new-material space-below" icon="add" mode="secondary"
 | 
			
		||||
                        (click)="setNewMaterial(!newMaterial)"
 | 
			
		||||
                        *ngIf="mode === 'new' || (baseSample.material && baseSample.material.name !== undefined)">
 | 
			
		||||
          New material
 | 
			
		||||
        </rb-icon-button>
 | 
			
		||||
      </div>
 | 
			
		||||
	<form #sampleForm="ngForm" *ngIf="view.base">
 | 
			
		||||
		<div class="sample">
 | 
			
		||||
			<div>
 | 
			
		||||
				<rb-form-input name="materialname" label="product name" [rbDebounceTime]="0" [rbInitialOpen]="true"
 | 
			
		||||
					[rbFormInputAutocomplete]="autocomplete.bind(this, materialNames)" appValidate="stringOf"
 | 
			
		||||
					(keydown)="preventDefault($event)" (ngModelChange)="findMaterial($event)" ngModel
 | 
			
		||||
					[appValidateArgs]="[materialNames]" required [(ngModel)]="material.name" [autofocus]="true"
 | 
			
		||||
					*ngIf="mode === 'new' || (baseSample.material && baseSample.material.name !== undefined)"
 | 
			
		||||
					title="trade name of the material, eg. Ultradur B4300 G6">
 | 
			
		||||
					<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
					<ng-template rbFormValidationMessage="failure">Unknown material, add properties for new material</ng-template>
 | 
			
		||||
				</rb-form-input>
 | 
			
		||||
				<rb-icon-button class="set-new-material space-below" icon="add" mode="secondary"
 | 
			
		||||
					(click)="setNewMaterial(!newMaterial)"
 | 
			
		||||
					*ngIf="mode === 'new' || (baseSample.material && baseSample.material.name !== undefined)">
 | 
			
		||||
					New material
 | 
			
		||||
				</rb-icon-button>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
      <div class="material shaded-container" *ngIf="newMaterial" [@inOut]>
 | 
			
		||||
        <h4>Material properties</h4>
 | 
			
		||||
        <rb-form-input name="supplier" label="supplier"
 | 
			
		||||
                       [rbFormInputAutocomplete]="autocomplete.bind(this, d.arr.materialSuppliers)"
 | 
			
		||||
                       [rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required
 | 
			
		||||
                       [(ngModel)]="material.supplier" #supplierInput="ngModel"
 | 
			
		||||
                       (focusout)="checkTypo($event, 'materialSuppliers', 'supplier', modalWarning)"
 | 
			
		||||
                       title="material supplier, eg. BASF">
 | 
			
		||||
          <ng-template rbFormValidationMessage="failure">{{supplierInput.errors.failure}}</ng-template>
 | 
			
		||||
        </rb-form-input>
 | 
			
		||||
        <rb-form-input name="group" label="group"
 | 
			
		||||
                       [rbFormInputAutocomplete]="autocomplete.bind(this, d.arr.materialGroups)"
 | 
			
		||||
                       [rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required
 | 
			
		||||
                       [(ngModel)]="material.group" #groupInput="ngModel"
 | 
			
		||||
                       (focusout)="checkTypo($event, 'materialGroups', 'group', modalWarning)"
 | 
			
		||||
                       title="chemical material type, eg. PA66">
 | 
			
		||||
          <ng-template rbFormValidationMessage="failure">{{groupInput.errors.failure}}</ng-template>
 | 
			
		||||
        </rb-form-input>
 | 
			
		||||
        <ng-template #modalWarning>
 | 
			
		||||
          <rb-alert alertTitle="Warning" type="warning" okBtnLabel="Use suggestion" cancelBtnLabel="Keep value">
 | 
			
		||||
            The specified {{modalText.list}} could not be found in the list. <br>
 | 
			
		||||
            Did you mean {{modalText.suggestion}}?
 | 
			
		||||
          </rb-alert>
 | 
			
		||||
        </ng-template>
 | 
			
		||||
        <rb-array-input [(ngModel)]="material.numbers" name="materialNumbers" [pushTemplate]="''">
 | 
			
		||||
          <rb-form-input *rbArrayInputItem="let item" [rbArrayInputListener]="'materialNumber'" [index]="item.i"
 | 
			
		||||
                         label="material number" appValidate="string" [name]="'materialNumber-' + item.i"
 | 
			
		||||
                         [ngModel]="item.value" title="Bosch material part number, eg. 5515753021"></rb-form-input>
 | 
			
		||||
        </rb-array-input>
 | 
			
		||||
        <rb-form-select name="propertiesSelect" label="Type" title="=overall material group specific properties"
 | 
			
		||||
                        [(ngModel)]="material.properties.material_template">
 | 
			
		||||
          <option *ngFor="let m of d.latest.materialTemplates" [value]="m._id">{{m.name}}</option>
 | 
			
		||||
        </rb-form-select>
 | 
			
		||||
        <rb-form-input *ngFor="let parameter of
 | 
			
		||||
                         d.id.materialTemplates[material.properties.material_template].parameters;
 | 
			
		||||
                       index as i" [name]="'materialParameter' + i"
 | 
			
		||||
                       [label]="parameter.name" appValidate="string" required
 | 
			
		||||
                       [(ngModel)]="material.properties[parameter.name]" #parameterInput="ngModel">
 | 
			
		||||
          <ng-template rbFormValidationMessage="failure">{{parameterInput.errors.failure}}</ng-template>
 | 
			
		||||
          <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
        </rb-form-input>
 | 
			
		||||
      </div>
 | 
			
		||||
			<div class="material shaded-container" *ngIf="newMaterial" [@inOut]>
 | 
			
		||||
				<h4>Material properties</h4>
 | 
			
		||||
				<rb-form-input name="supplier" label="supplier"
 | 
			
		||||
					[rbFormInputAutocomplete]="autocomplete.bind(this, d.arr.materialSuppliers)"
 | 
			
		||||
					[rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required
 | 
			
		||||
					[(ngModel)]="material.supplier" #supplierInput="ngModel"
 | 
			
		||||
					(focusout)="checkTypo($event, 'materialSuppliers', 'supplier', modalWarning)"
 | 
			
		||||
					title="material supplier, eg. BASF">
 | 
			
		||||
					<ng-template rbFormValidationMessage="failure">{{supplierInput.errors.failure}}</ng-template>
 | 
			
		||||
				</rb-form-input>
 | 
			
		||||
				<rb-form-input name="group" label="group"
 | 
			
		||||
					[rbFormInputAutocomplete]="autocomplete.bind(this, d.arr.materialGroups)"
 | 
			
		||||
					[rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required
 | 
			
		||||
					[(ngModel)]="material.group" #groupInput="ngModel"
 | 
			
		||||
					(focusout)="checkTypo($event, 'materialGroups', 'group', modalWarning)"
 | 
			
		||||
					title="chemical material type, eg. PA66">
 | 
			
		||||
					<ng-template rbFormValidationMessage="failure">{{groupInput.errors.failure}}</ng-template>
 | 
			
		||||
				</rb-form-input>
 | 
			
		||||
				<ng-template #modalWarning>
 | 
			
		||||
					<rb-alert alertTitle="Warning" type="warning" okBtnLabel="Use suggestion" cancelBtnLabel="Keep value">
 | 
			
		||||
						The specified {{modalText.list}} could not be found in the list. <br>
 | 
			
		||||
						Did you mean {{modalText.suggestion}}?
 | 
			
		||||
					</rb-alert>
 | 
			
		||||
				</ng-template>
 | 
			
		||||
				<rb-array-input [(ngModel)]="material.numbers" name="materialNumbers" [pushTemplate]="''">
 | 
			
		||||
					<rb-form-input *rbArrayInputItem="let item" [rbArrayInputListener]="'materialNumber'" [index]="item.i"
 | 
			
		||||
						label="material number" appValidate="string" [name]="'materialNumber-' + item.i"
 | 
			
		||||
						[ngModel]="item.value" title="Bosch material part number, eg. 5515753021"></rb-form-input>
 | 
			
		||||
				</rb-array-input>
 | 
			
		||||
				<rb-form-select name="propertiesSelect" label="Type" title="=overall material group specific properties"
 | 
			
		||||
					[(ngModel)]="material.properties.material_template">
 | 
			
		||||
					<option *ngFor="let m of d.latest.materialTemplates" [value]="m._id">{{m.name}}</option>
 | 
			
		||||
				</rb-form-select>
 | 
			
		||||
				<rb-form-input *ngFor="let parameter of
 | 
			
		||||
					d.id.materialTemplates[material.properties.material_template].parameters;
 | 
			
		||||
					index as i" [name]="'materialParameter' + i"
 | 
			
		||||
					[label]="parameter.name" appValidate="string" required
 | 
			
		||||
					[(ngModel)]="material.properties[parameter.name]" #parameterInput="ngModel">
 | 
			
		||||
					<ng-template rbFormValidationMessage="failure">{{parameterInput.errors.failure}}</ng-template>
 | 
			
		||||
					<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
				</rb-form-input>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
      <div>
 | 
			
		||||
        <rb-form-select name="type" label="type" required [(ngModel)]="baseSample.type" ngModel
 | 
			
		||||
                        *ngIf="baseSample.type !== undefined"
 | 
			
		||||
                        title="material status of the sample">
 | 
			
		||||
          <option value="as-delivered/raw">as-delivered/raw</option>
 | 
			
		||||
          <option value="processed">processed</option>
 | 
			
		||||
          <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
        </rb-form-select>
 | 
			
		||||
        <rb-form-input name="color" label="color" appValidate="string" [(ngModel)]="baseSample.color"
 | 
			
		||||
                       *ngIf="baseSample.color !== undefined" #colorInput="ngModel" title="sample color, eg. black">
 | 
			
		||||
          <ng-template rbFormValidationMessage="failure">{{colorInput.errors.failure}}</ng-template>
 | 
			
		||||
          <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
        </rb-form-input>
 | 
			
		||||
        <rb-form-input name="batch" label="batch" appValidate="string" [(ngModel)]="baseSample.batch"
 | 
			
		||||
                       #batchInput="ngModel" *ngIf="baseSample.batch !== undefined"
 | 
			
		||||
                       title="batch number the sample was from, eg. 2264486614">
 | 
			
		||||
          <ng-template rbFormValidationMessage="failure">{{batchInput.errors.failure}}</ng-template>
 | 
			
		||||
        </rb-form-input>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
			<div>
 | 
			
		||||
				<rb-form-select name="type" label="type" required [(ngModel)]="baseSample.type" ngModel
 | 
			
		||||
					*ngIf="baseSample.type !== undefined"
 | 
			
		||||
					title="material status of the sample">
 | 
			
		||||
					<option value="as-delivered/raw">as-delivered/raw</option>
 | 
			
		||||
					<option value="processed">processed</option>
 | 
			
		||||
					<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
				</rb-form-select>
 | 
			
		||||
				<rb-form-input name="color" label="color" appValidate="string" [(ngModel)]="baseSample.color"
 | 
			
		||||
					*ngIf="baseSample.color !== undefined" #colorInput="ngModel" title="sample color, eg. black">
 | 
			
		||||
					<ng-template rbFormValidationMessage="failure">{{colorInput.errors.failure}}</ng-template>
 | 
			
		||||
					<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
				</rb-form-input>
 | 
			
		||||
				<rb-form-input name="batch" label="batch" appValidate="string" [(ngModel)]="baseSample.batch"
 | 
			
		||||
					#batchInput="ngModel" *ngIf="baseSample.batch !== undefined"
 | 
			
		||||
					title="batch number the sample was from, eg. 2264486614">
 | 
			
		||||
					<ng-template rbFormValidationMessage="failure">{{batchInput.errors.failure}}</ng-template>
 | 
			
		||||
				</rb-form-input>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
    <div class="notes" *ngIf="baseSample.notes !== undefined">
 | 
			
		||||
      <rb-form-input name="comment" label="comment" appValidate="stringLength" [appValidateArgs]="[512]"
 | 
			
		||||
                     [(ngModel)]="baseSample.notes.comment" #commentInput="ngModel"
 | 
			
		||||
                     title="general remarks that cannot be expressed in additional properties, eg. stabilized">
 | 
			
		||||
        <ng-template rbFormValidationMessage="failure">{{commentInput.errors.failure}}</ng-template>
 | 
			
		||||
      </rb-form-input>
 | 
			
		||||
      <h5>Sample references</h5>
 | 
			
		||||
      <div *ngFor="let reference of sampleReferences; index as i" class="two-col" [@inOut]>
 | 
			
		||||
        <div>
 | 
			
		||||
          <rb-form-input [name]="'sr-id' + i" label="sample number"
 | 
			
		||||
                         [rbFormInputAutocomplete]="sampleReferenceListBind()"
 | 
			
		||||
                         [rbDebounceTime]="300" appValidate="stringOf"
 | 
			
		||||
                         [appValidateArgs]="[sampleReferenceAutocomplete[i]]"
 | 
			
		||||
                         (ngModelChange)="checkSampleReference($event, i)" [ngModel]="reference[0]" ngModel
 | 
			
		||||
                         title="sample number of the referenced sample, eg. An31">
 | 
			
		||||
            <ng-template rbFormValidationMessage="failure">Unknown sample number</ng-template>
 | 
			
		||||
          </rb-form-input>
 | 
			
		||||
        </div>
 | 
			
		||||
        <rb-form-input [name]="'sr-relation' + i" label="relation" appValidate="string" [required]="reference[0] !== ''"
 | 
			
		||||
                       [(ngModel)]="reference[1]" title="description how the samples are connected, eg. belongs to">
 | 
			
		||||
          <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
        </rb-form-input>
 | 
			
		||||
      </div>
 | 
			
		||||
      <h5>Additional properties</h5>
 | 
			
		||||
      <rb-array-input [(ngModel)]="customFields" name="customFields" [pushTemplate]="['', '']" pushPath="0"
 | 
			
		||||
                      class="two-col" [@inOut]>
 | 
			
		||||
        <ng-container *rbArrayInputItem="let item">
 | 
			
		||||
          <div>
 | 
			
		||||
            <rb-form-input [name]="'cf-key' + item.i" label="key" [rbArrayInputListener]="'cf-key'" [index]="item.i"
 | 
			
		||||
                           [rbFormInputAutocomplete]="autocomplete.bind(this, availableCustomFields)"
 | 
			
		||||
                           [rbDebounceTime]="0"
 | 
			
		||||
                           [rbInitialOpen]="true" appValidate="unique" [appValidateArgs]="[uniqueCfValues(item.i)]"
 | 
			
		||||
                           [(ngModel)]="item.value[0]" #keyInput="ngModel" title="name of additional property, eg. vwz">
 | 
			
		||||
              <ng-template rbFormValidationMessage="failure">{{keyInput.errors.failure}}</ng-template>
 | 
			
		||||
            </rb-form-input>
 | 
			
		||||
          </div>
 | 
			
		||||
          <rb-form-input [name]="'cf-value' + item.i" label="value" appValidate="string"
 | 
			
		||||
                         [required]="item.value[0] !== ''"
 | 
			
		||||
                         [(ngModel)]="item.value[1]" title="value of additional property, eg. 0 min">
 | 
			
		||||
            <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
          </rb-form-input>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
      </rb-array-input>
 | 
			
		||||
    </div>
 | 
			
		||||
		<div class="notes" *ngIf="baseSample.notes !== undefined">
 | 
			
		||||
			<rb-form-input name="comment" label="comment" appValidate="stringLength" [appValidateArgs]="[512]"
 | 
			
		||||
				[(ngModel)]="baseSample.notes.comment" #commentInput="ngModel"
 | 
			
		||||
				title="general remarks that cannot be expressed in additional properties, eg. stabilized">
 | 
			
		||||
				<ng-template rbFormValidationMessage="failure">{{commentInput.errors.failure}}</ng-template>
 | 
			
		||||
			</rb-form-input>
 | 
			
		||||
			<h5>Sample references</h5>
 | 
			
		||||
			<div *ngFor="let reference of sampleReferences; index as i" class="two-col" [@inOut]>
 | 
			
		||||
				<div>
 | 
			
		||||
					<rb-form-input [name]="'sr-id' + i" label="sample number"
 | 
			
		||||
						[rbFormInputAutocomplete]="sampleReferenceListBind()"
 | 
			
		||||
						[rbDebounceTime]="300" appValidate="stringOf"
 | 
			
		||||
						[appValidateArgs]="[sampleReferenceAutocomplete[i]]"
 | 
			
		||||
						(ngModelChange)="checkSampleReference($event, i)" [ngModel]="reference[0]" ngModel
 | 
			
		||||
						title="sample number of the referenced sample, eg. An31">
 | 
			
		||||
						<ng-template rbFormValidationMessage="failure">Unknown sample number</ng-template>
 | 
			
		||||
					</rb-form-input>
 | 
			
		||||
				</div>
 | 
			
		||||
				<rb-form-input [name]="'sr-relation' + i" label="relation" appValidate="string" [required]="reference[0] !== ''"
 | 
			
		||||
					[(ngModel)]="reference[1]" title="description how the samples are connected, eg. belongs to">
 | 
			
		||||
					<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
				</rb-form-input>
 | 
			
		||||
			</div>
 | 
			
		||||
			<h5>Additional properties</h5>
 | 
			
		||||
			<rb-array-input [(ngModel)]="customFields" name="customFields" [pushTemplate]="['', '']" pushPath="0"
 | 
			
		||||
				class="two-col" [@inOut]>
 | 
			
		||||
				<ng-container *rbArrayInputItem="let item">
 | 
			
		||||
					<div>
 | 
			
		||||
						<rb-form-input [name]="'cf-key' + item.i" label="key" [rbArrayInputListener]="'cf-key'" [index]="item.i"
 | 
			
		||||
							[rbFormInputAutocomplete]="autocomplete.bind(this, availableCustomFields)"
 | 
			
		||||
							[rbDebounceTime]="0"
 | 
			
		||||
							[rbInitialOpen]="true" appValidate="unique" [appValidateArgs]="[uniqueCfValues(item.i)]"
 | 
			
		||||
							[(ngModel)]="item.value[0]" #keyInput="ngModel" title="name of additional property, eg. vwz">
 | 
			
		||||
							<ng-template rbFormValidationMessage="failure">{{keyInput.errors.failure}}</ng-template>
 | 
			
		||||
						</rb-form-input>
 | 
			
		||||
					</div>
 | 
			
		||||
					<rb-form-input [name]="'cf-value' + item.i" label="value" appValidate="string"
 | 
			
		||||
						[required]="item.value[0] !== ''"
 | 
			
		||||
						[(ngModel)]="item.value[1]" title="value of additional property, eg. 0 min">
 | 
			
		||||
						<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
					</rb-form-input>
 | 
			
		||||
				</ng-container>
 | 
			
		||||
			</rb-array-input>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
    <div *ngIf="samples.length; else generateSamples" class="space-below">
 | 
			
		||||
      <rb-icon-button icon="save" mode="primary" type="submit" (click)="saveSample()"
 | 
			
		||||
                      [disabled]="!sampleForm.form.valid">
 | 
			
		||||
        Save sample
 | 
			
		||||
      </rb-icon-button>
 | 
			
		||||
      <rb-icon-button class="delete-sample" icon="delete" mode="danger" *ngIf="samples.length > 1"
 | 
			
		||||
                      (click)="deleteConfirm(modalDeleteConfirm)">
 | 
			
		||||
        Delete samples
 | 
			
		||||
      </rb-icon-button>
 | 
			
		||||
    </div>
 | 
			
		||||
    <ng-template #generateSamples>
 | 
			
		||||
      <rb-form-input type="number" name="sample-count" label="number of samples" pattern="^\d+?$" required
 | 
			
		||||
                     rbNumberConverter rbMin="1" [(ngModel)]="sampleCount" class="sample-count"
 | 
			
		||||
                     title="number of samples to create with this base information">
 | 
			
		||||
        <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
        <ng-template rbFormValidationMessage="rbMin">Must be at least 1</ng-template>
 | 
			
		||||
      </rb-form-input>
 | 
			
		||||
      <button class="rb-btn rb-primary space-below" type="submit" (click)="saveSample()"
 | 
			
		||||
              [disabled]="!sampleForm.form.valid">
 | 
			
		||||
        Generate sample{{sampleCount > 1 ? 's' : ''}}
 | 
			
		||||
      </button>
 | 
			
		||||
    </ng-template>
 | 
			
		||||
  </form>
 | 
			
		||||
		<div *ngIf="samples.length; else generateSamples" class="space-below">
 | 
			
		||||
			<rb-icon-button icon="save" mode="primary" type="submit" (click)="saveSample()"
 | 
			
		||||
				[disabled]="!sampleForm.form.valid">
 | 
			
		||||
				Save sample
 | 
			
		||||
			</rb-icon-button>
 | 
			
		||||
			<rb-icon-button class="delete-sample" icon="delete" mode="danger" *ngIf="samples.length > 1"
 | 
			
		||||
				(click)="deleteConfirm(modalDeleteConfirm)">
 | 
			
		||||
				Delete samples
 | 
			
		||||
			</rb-icon-button>
 | 
			
		||||
		</div>
 | 
			
		||||
		<ng-template #generateSamples>
 | 
			
		||||
			<rb-form-input type="number" name="sample-count" label="number of samples" pattern="^\d+?$" required
 | 
			
		||||
				rbNumberConverter rbMin="1" [(ngModel)]="sampleCount" class="sample-count"
 | 
			
		||||
				title="number of samples to create with this base information">
 | 
			
		||||
				<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
				<ng-template rbFormValidationMessage="rbMin">Must be at least 1</ng-template>
 | 
			
		||||
			</rb-form-input>
 | 
			
		||||
			<button class="rb-btn rb-primary space-below" type="submit" (click)="saveSample()"
 | 
			
		||||
				[disabled]="!sampleForm.form.valid">
 | 
			
		||||
				Generate sample{{sampleCount > 1 ? 's' : ''}}
 | 
			
		||||
			</button>
 | 
			
		||||
		</ng-template>
 | 
			
		||||
	</form>
 | 
			
		||||
 | 
			
		||||
<!--BASE SUMMARY-->
 | 
			
		||||
	<!--BASE SUMMARY-->
 | 
			
		||||
 | 
			
		||||
  <div *ngIf="view.baseSum">
 | 
			
		||||
    <h3 *ngIf="mode === 'new'">Successfully added samples:</h3>
 | 
			
		||||
    <span class="rb-ic rb-ic-edit clickable" (click)="checkFormAfterInit = view.base = true; view.baseSum = false">
 | 
			
		||||
    </span>
 | 
			
		||||
    <rb-table id="response-data">
 | 
			
		||||
      <tr><td>Sample Number</td><td>{{baseSample.number}}</td></tr>
 | 
			
		||||
      <tr><td>Material</td><td>{{material.name}}</td></tr>
 | 
			
		||||
      <tr><td>Type</td><td>{{baseSample.type}}</td></tr>
 | 
			
		||||
      <tr><td>Color</td><td>{{baseSample.color}}</td></tr>
 | 
			
		||||
      <tr><td>Batch</td><td>{{baseSample.batch}}</td></tr>
 | 
			
		||||
      <tr><td>Comment</td><td>{{baseSample.notes | exists:'comment'}}</td></tr>
 | 
			
		||||
      <tr *ngFor="let reference of sampleReferences.slice(0, -1)">
 | 
			
		||||
        <td>Sample reference</td><td>{{reference[0]}} - {{reference[1]}}</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr *ngFor="let field of customFields"><td>{{field[0]}}</td><td>{{field[1]}}</td></tr>
 | 
			
		||||
    </rb-table>
 | 
			
		||||
  </div>
 | 
			
		||||
	<div *ngIf="view.baseSum">
 | 
			
		||||
		<h3 *ngIf="mode === 'new'">Successfully added samples:</h3>
 | 
			
		||||
		<span class="rb-ic rb-ic-edit clickable" (click)="checkFormAfterInit = view.base = true; view.baseSum = false">
 | 
			
		||||
		</span>
 | 
			
		||||
		<rb-table id="response-data">
 | 
			
		||||
			<tr><td>Sample Number</td><td>{{baseSample.number}}</td></tr>
 | 
			
		||||
			<tr><td>Material</td><td>{{material.name}}</td></tr>
 | 
			
		||||
			<tr><td>Type</td><td>{{baseSample.type}}</td></tr>
 | 
			
		||||
			<tr><td>Color</td><td>{{baseSample.color}}</td></tr>
 | 
			
		||||
			<tr><td>Batch</td><td>{{baseSample.batch}}</td></tr>
 | 
			
		||||
			<tr><td>Comment</td><td>{{baseSample.notes | exists:'comment'}}</td></tr>
 | 
			
		||||
			<tr *ngFor="let reference of sampleReferences.slice(0, -1)">
 | 
			
		||||
				<td>Sample reference</td><td>{{reference[0]}} - {{reference[1]}}</td>
 | 
			
		||||
			</tr>
 | 
			
		||||
			<tr *ngFor="let field of customFields"><td>{{field[0]}}</td><td>{{field[1]}}</td></tr>
 | 
			
		||||
		</rb-table>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
<!--CM-->
 | 
			
		||||
	<!--CM-->
 | 
			
		||||
 | 
			
		||||
  <div *ngIf="view.cm">
 | 
			
		||||
    <form #cmForm="ngForm" [ngClass]="{'cm-form': samples.length > 1}">
 | 
			
		||||
      <rb-tab-panel (tabChanged)="cmSampleIndex = $event" class="space-below" *ngIf="samples.length > 1">
 | 
			
		||||
        <ng-container *ngFor="let sample of samples; index as i">
 | 
			
		||||
          <div *rbTabPanelItem="sample.number; id: i"></div>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
      </rb-tab-panel>
 | 
			
		||||
      <div *ngFor="let sample of samples; index as gIndex"
 | 
			
		||||
           [ngStyle]="{display: gIndex == cmSampleIndex || gIndex == cmSampleIndex  + 1 ? 'initial' : 'none'}">
 | 
			
		||||
        <h4 *ngIf="samples.length > 1">{{sample.number}}</h4>
 | 
			
		||||
        <div class="conditions shaded-container space-below">
 | 
			
		||||
          <h5>
 | 
			
		||||
            Condition
 | 
			
		||||
            <button class="rb-btn rb-secondary condition-set" type="button" (click)="toggleCondition(sample)">
 | 
			
		||||
              {{sample.condition.condition_template ? 'Do not set condition' : 'Set condition'}}
 | 
			
		||||
            </button>
 | 
			
		||||
          </h5>
 | 
			
		||||
          <div *ngIf="sample.condition.condition_template" [@inOut]>
 | 
			
		||||
            <rb-form-select [name]="'conditionSelect-' + gIndex" label="Condition"
 | 
			
		||||
                            [(ngModel)]="sample.condition.condition_template"
 | 
			
		||||
                            (ngModelChange)="reValidate()">
 | 
			
		||||
              <option [value]="sample.condition.condition_template">
 | 
			
		||||
                {{d.id.conditionTemplates[sample.condition.condition_template].name}} - current
 | 
			
		||||
              </option>
 | 
			
		||||
              <option *ngFor="let c of d.latest.conditionTemplates" [value]="c._id">{{c.name}}</option>
 | 
			
		||||
            </rb-form-select>
 | 
			
		||||
	<div *ngIf="view.cm">
 | 
			
		||||
		<form #cmForm="ngForm" [ngClass]="{'cm-form': samples.length > 1}">
 | 
			
		||||
			<rb-tab-panel (tabChanged)="cmSampleIndex = $event" class="space-below" *ngIf="samples.length > 1">
 | 
			
		||||
				<ng-container *ngFor="let sample of samples; index as i">
 | 
			
		||||
					<div *rbTabPanelItem="sample.number; id: i"></div>
 | 
			
		||||
				</ng-container>
 | 
			
		||||
			</rb-tab-panel>
 | 
			
		||||
			<div *ngFor="let sample of samples; index as gIndex"
 | 
			
		||||
				[ngStyle]="{display: gIndex == cmSampleIndex || gIndex == cmSampleIndex  + 1 ? 'initial' : 'none'}">
 | 
			
		||||
				<h4 *ngIf="samples.length > 1">{{sample.number}}</h4>
 | 
			
		||||
				<div class="conditions shaded-container space-below">
 | 
			
		||||
					<h5>
 | 
			
		||||
						Condition
 | 
			
		||||
						<button class="rb-btn rb-secondary condition-set" type="button" (click)="toggleCondition(sample)">
 | 
			
		||||
							{{sample.condition.condition_template ? 'Do not set condition' : 'Set condition'}}
 | 
			
		||||
						</button>
 | 
			
		||||
					</h5>
 | 
			
		||||
					<div *ngIf="sample.condition.condition_template" [@inOut]>
 | 
			
		||||
						<rb-form-select [name]="'conditionSelect-' + gIndex" label="Condition"
 | 
			
		||||
							[(ngModel)]="sample.condition.condition_template"
 | 
			
		||||
							(ngModelChange)="reValidate()">
 | 
			
		||||
							<option [value]="sample.condition.condition_template">
 | 
			
		||||
							{{d.id.conditionTemplates[sample.condition.condition_template].name}} - current
 | 
			
		||||
							</option>
 | 
			
		||||
							<option *ngFor="let c of d.latest.conditionTemplates" [value]="c._id">{{c.name}}</option>
 | 
			
		||||
						</rb-form-select>
 | 
			
		||||
 | 
			
		||||
            <ng-container *ngFor="let parameter of
 | 
			
		||||
                          d.id.conditionTemplates[sample.condition.condition_template].parameters; index as i"
 | 
			
		||||
                          [ngSwitch]="(parameter.range.values ? 1 : 0)">
 | 
			
		||||
              <rb-form-select *ngSwitchCase="1"
 | 
			
		||||
                              [name]="'conditionParameter-' + gIndex + '-' + i"
 | 
			
		||||
                              [label]="parameter.name" [(ngModel)]="sample.condition[parameter.name]" ngModel>
 | 
			
		||||
                <option *ngFor="let value of parameter.range.values" [value]="value">{{value}}</option>
 | 
			
		||||
                <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
              </rb-form-select>
 | 
			
		||||
              <rb-form-input *ngSwitchDefault (focus)="checkFormAfterInit = true"
 | 
			
		||||
                             [name]="'conditionParameter-' + gIndex + '-' + i"
 | 
			
		||||
                             [label]="parameter.name" appValidate="string" required
 | 
			
		||||
                             [(ngModel)]="sample.condition[parameter.name]" #parameterInput="ngModel">
 | 
			
		||||
                <ng-template rbFormValidationMessage="failure">{{parameterInput.errors.failure}}</ng-template>
 | 
			
		||||
                <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
              </rb-form-input>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
						<ng-container *ngFor="let parameter of
 | 
			
		||||
							d.id.conditionTemplates[sample.condition.condition_template].parameters; index as i"
 | 
			
		||||
							[ngSwitch]="(parameter.range.values ? 1 : 0)">
 | 
			
		||||
							<rb-form-select *ngSwitchCase="1"
 | 
			
		||||
								[name]="'conditionParameter-' + gIndex + '-' + i"
 | 
			
		||||
								[label]="parameter.name" [(ngModel)]="sample.condition[parameter.name]" ngModel>
 | 
			
		||||
								<option *ngFor="let value of parameter.range.values" [value]="value">{{value}}</option>
 | 
			
		||||
								<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
							</rb-form-select>
 | 
			
		||||
							<rb-form-input *ngSwitchDefault (focus)="checkFormAfterInit = true"
 | 
			
		||||
								[name]="'conditionParameter-' + gIndex + '-' + i"
 | 
			
		||||
								[label]="parameter.name" appValidate="string" required
 | 
			
		||||
								[(ngModel)]="sample.condition[parameter.name]" #parameterInput="ngModel">
 | 
			
		||||
								<ng-template rbFormValidationMessage="failure">{{parameterInput.errors.failure}}</ng-template>
 | 
			
		||||
								<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
							</rb-form-input>
 | 
			
		||||
						</ng-container>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
        <div class="measurements shaded-container space-below">
 | 
			
		||||
          <h5>
 | 
			
		||||
            Measurements
 | 
			
		||||
            <rb-icon-button icon="undo" mode="secondary" *ngIf="measurementRestoreData.length"
 | 
			
		||||
                            class="restore-measurements" (click)="restoreMeasurements()">
 | 
			
		||||
              Restore measurements
 | 
			
		||||
            </rb-icon-button>
 | 
			
		||||
          </h5>
 | 
			
		||||
          <div *ngFor="let measurement of sample.measurements; index as mIndex" [@inOut]  class="space-below">
 | 
			
		||||
            <rb-form-select [name]="'measurementTemplateSelect-' + gIndex + '-' + mIndex" label="Template"
 | 
			
		||||
                            [(ngModel)]="measurement.measurement_template"
 | 
			
		||||
                            (ngModelChange)="clearMeasurement(gIndex, mIndex)">
 | 
			
		||||
              <option [value]="sample.condition.condition_template">
 | 
			
		||||
                {{d.id.measurementTemplates[measurement.measurement_template].name}} - old
 | 
			
		||||
              </option>
 | 
			
		||||
              <option *ngFor="let m of d.latest.measurementTemplates" [value]="m._id">{{m.name}}</option>
 | 
			
		||||
            </rb-form-select>
 | 
			
		||||
            <div *ngFor="let parameter of d.id.measurementTemplates[measurement.measurement_template].parameters;
 | 
			
		||||
                 index as pIndex">
 | 
			
		||||
              <ng-container [ngSwitch]="(parameter.range.type ? 1 : 0) + (parameter.range.values ? 2 : 0)">
 | 
			
		||||
                <rb-form-file *ngSwitchCase="1"
 | 
			
		||||
                              [name]="'measurementParameter-' + gIndex + '-' + mIndex + '-' + pIndex"
 | 
			
		||||
                              [label]="parameter.name" maxSize="10000000" multiple
 | 
			
		||||
                              [required]="measurement.values[parameter.name] &&
 | 
			
		||||
                              !measurement.values[parameter.name].length"
 | 
			
		||||
                              (ngModelChange)="fileToArray($event, gIndex, mIndex, parameter.name)"
 | 
			
		||||
                              placeholder="Select file or drag and drop" dragDrop ngModel>
 | 
			
		||||
                  <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
                </rb-form-file>
 | 
			
		||||
                <rb-form-select *ngSwitchCase="2"
 | 
			
		||||
                                [name]="'measurementParameter-' + gIndex + '-' + mIndex + '-' + pIndex"
 | 
			
		||||
                                [label]="parameter.name" [(ngModel)]="measurement.values[parameter.name]" ngModel>
 | 
			
		||||
                  <option *ngFor="let device of d.d.user.devices" [value]="device">{{device}}</option>
 | 
			
		||||
                  <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
                </rb-form-select>
 | 
			
		||||
                <rb-form-input *ngSwitchDefault
 | 
			
		||||
                               [name]="'measurementParameter-' + gIndex + '-' + mIndex + '-' + pIndex"
 | 
			
		||||
                               [label]="parameter.name" appValidate="string"
 | 
			
		||||
                               [(ngModel)]="measurement.values[parameter.name]" #parameterInput="ngModel">
 | 
			
		||||
                  <ng-template rbFormValidationMessage="failure">{{parameterInput.errors.failure}}</ng-template>
 | 
			
		||||
                  <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
                </rb-form-input>
 | 
			
		||||
              </ng-container>
 | 
			
		||||
              <canvas baseChart *ngIf="parameter.range.type && charts[gIndex][mIndex][0].data.length > 0"
 | 
			
		||||
                      class="dpt-chart"
 | 
			
		||||
                      [@inOut]
 | 
			
		||||
                      [datasets]="charts[gIndex][mIndex]"
 | 
			
		||||
                      [labels]="[]"
 | 
			
		||||
                      [options]="chartOptions"
 | 
			
		||||
                      [legend]="false"
 | 
			
		||||
                      chartType="scatter">
 | 
			
		||||
              </canvas>
 | 
			
		||||
            </div>
 | 
			
		||||
            <rb-icon-button icon="delete" mode="danger" (click)="removeMeasurement(gIndex, mIndex)">
 | 
			
		||||
              Delete measurement
 | 
			
		||||
            </rb-icon-button>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div>
 | 
			
		||||
            <rb-icon-button icon="add" mode="secondary" (click)="addMeasurement(gIndex)">
 | 
			
		||||
              New measurement
 | 
			
		||||
            </rb-icon-button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="buttons">
 | 
			
		||||
        <rb-icon-button icon="summary" mode="primary" type="submit" (click)="view.cm = false; view.cmSum = true"
 | 
			
		||||
                        [disabled]="!cmForm.form.valid">
 | 
			
		||||
          Summary
 | 
			
		||||
        </rb-icon-button>
 | 
			
		||||
        <rb-icon-button class="delete-sample" icon="delete" mode="danger" *ngIf="samples.length === 1"
 | 
			
		||||
                        (click)="deleteConfirm(modalDeleteConfirm)">
 | 
			
		||||
          Delete sample
 | 
			
		||||
        </rb-icon-button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </form>
 | 
			
		||||
  </div>
 | 
			
		||||
				<div class="measurements shaded-container space-below">
 | 
			
		||||
					<h5>
 | 
			
		||||
						Measurements
 | 
			
		||||
						<rb-icon-button icon="undo" mode="secondary" *ngIf="measurementRestoreData.length"
 | 
			
		||||
							class="restore-measurements" (click)="restoreMeasurements()">
 | 
			
		||||
							Restore measurements
 | 
			
		||||
						</rb-icon-button>
 | 
			
		||||
					</h5>
 | 
			
		||||
					<div *ngFor="let measurement of sample.measurements; index as mIndex" [@inOut]  class="space-below">
 | 
			
		||||
						<rb-form-select [name]="'measurementTemplateSelect-' + gIndex + '-' + mIndex" label="Template"
 | 
			
		||||
							[(ngModel)]="measurement.measurement_template"
 | 
			
		||||
							(ngModelChange)="clearMeasurement(gIndex, mIndex)">
 | 
			
		||||
							<option [value]="sample.condition.condition_template">
 | 
			
		||||
							{{d.id.measurementTemplates[measurement.measurement_template].name}} - old
 | 
			
		||||
							</option>
 | 
			
		||||
							<option *ngFor="let m of d.latest.measurementTemplates" [value]="m._id">{{m.name}}</option>
 | 
			
		||||
						</rb-form-select>
 | 
			
		||||
						<div *ngFor="let parameter of d.id.measurementTemplates[measurement.measurement_template].parameters;
 | 
			
		||||
							index as pIndex">
 | 
			
		||||
							<ng-container [ngSwitch]="(parameter.range.type ? 1 : 0) + (parameter.range.values ? 2 : 0)">
 | 
			
		||||
								<rb-form-file *ngSwitchCase="1"
 | 
			
		||||
									[name]="'measurementParameter-' + gIndex + '-' + mIndex + '-' + pIndex"
 | 
			
		||||
									[label]="parameter.name" maxSize="10000000" multiple
 | 
			
		||||
									[required]="measurement.values[parameter.name] &&
 | 
			
		||||
									!measurement.values[parameter.name].length"
 | 
			
		||||
									(ngModelChange)="fileToArray($event, gIndex, mIndex, parameter.name)"
 | 
			
		||||
									placeholder="Select file or drag and drop" dragDrop ngModel>
 | 
			
		||||
									<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
								</rb-form-file>
 | 
			
		||||
								<rb-form-select *ngSwitchCase="2"
 | 
			
		||||
									[name]="'measurementParameter-' + gIndex + '-' + mIndex + '-' + pIndex"
 | 
			
		||||
									[label]="parameter.name" [(ngModel)]="measurement.values[parameter.name]" ngModel>
 | 
			
		||||
									<option *ngFor="let device of d.d.user.devices" [value]="device">{{device}}</option>
 | 
			
		||||
									<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
								</rb-form-select>
 | 
			
		||||
								<rb-form-input *ngSwitchDefault
 | 
			
		||||
									[name]="'measurementParameter-' + gIndex + '-' + mIndex + '-' + pIndex"
 | 
			
		||||
									[label]="parameter.name" appValidate="string"
 | 
			
		||||
									[(ngModel)]="measurement.values[parameter.name]" #parameterInput="ngModel">
 | 
			
		||||
									<ng-template rbFormValidationMessage="failure">{{parameterInput.errors.failure}}</ng-template>
 | 
			
		||||
									<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
								</rb-form-input>
 | 
			
		||||
							</ng-container>
 | 
			
		||||
							<canvas baseChart *ngIf="parameter.range.type && charts[gIndex][mIndex][0].data.length > 0"
 | 
			
		||||
								class="dpt-chart"
 | 
			
		||||
								[@inOut]
 | 
			
		||||
								[datasets]="charts[gIndex][mIndex]"
 | 
			
		||||
								[labels]="[]"
 | 
			
		||||
								[options]="chartOptions"
 | 
			
		||||
								[legend]="false"
 | 
			
		||||
								chartType="scatter">
 | 
			
		||||
							</canvas>
 | 
			
		||||
						</div>
 | 
			
		||||
						<rb-icon-button icon="delete" mode="danger" (click)="removeMeasurement(gIndex, mIndex)">
 | 
			
		||||
							Delete measurement
 | 
			
		||||
						</rb-icon-button>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div>
 | 
			
		||||
						<rb-icon-button icon="add" mode="secondary" (click)="addMeasurement(gIndex)">
 | 
			
		||||
							New measurement
 | 
			
		||||
						</rb-icon-button>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="buttons">
 | 
			
		||||
				<rb-icon-button icon="summary" mode="primary" type="submit" (click)="view.cm = false; view.cmSum = true"
 | 
			
		||||
					[disabled]="!cmForm.form.valid">
 | 
			
		||||
					Summary
 | 
			
		||||
				</rb-icon-button>
 | 
			
		||||
				<rb-icon-button class="delete-sample" icon="delete" mode="danger" *ngIf="samples.length === 1"
 | 
			
		||||
					(click)="deleteConfirm(modalDeleteConfirm)">
 | 
			
		||||
					Delete sample
 | 
			
		||||
				</rb-icon-button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</form>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
<!--CM SUMMARY-->
 | 
			
		||||
	<!--CM SUMMARY-->
 | 
			
		||||
 | 
			
		||||
  <div *ngIf="view.cmSum">
 | 
			
		||||
    <span class="rb-ic rb-ic-edit clickable" (click)="checkFormAfterInit = view.cm = true; view.cmSum = false">
 | 
			
		||||
    </span>
 | 
			
		||||
    <rb-table>
 | 
			
		||||
      <ng-container *ngFor="let sample of samples; index as gIndex">
 | 
			
		||||
        <tr><th>{{sample.number}}</th><th></th></tr>
 | 
			
		||||
        <tr *ngFor="let parameter of (d.id.conditionTemplates[sample.condition.condition_template] || {parameters: []})
 | 
			
		||||
            .parameters">
 | 
			
		||||
          <td>{{parameter.name}}</td><td>{{sample.condition[parameter.name]}}</td>
 | 
			
		||||
        </tr>
 | 
			
		||||
        <ng-container *ngFor="let measurement of sample.measurements">
 | 
			
		||||
          <tr *ngFor="let parameter of d.id.measurementTemplates[measurement.measurement_template].parameters">
 | 
			
		||||
            <td>{{parameter.name}}</td><td>{{measurement.values[parameter.name]}}</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
      </ng-container>
 | 
			
		||||
    </rb-table>
 | 
			
		||||
    <rb-icon-button icon="save" mode="primary" type="submit" (click)="cmSave()">
 | 
			
		||||
      Save sample{{samples.length > 1 ? 's' : ''}}
 | 
			
		||||
    </rb-icon-button>
 | 
			
		||||
  </div>
 | 
			
		||||
	<div *ngIf="view.cmSum">
 | 
			
		||||
		<span class="rb-ic rb-ic-edit clickable" (click)="checkFormAfterInit = view.cm = true; view.cmSum = false">
 | 
			
		||||
		</span>
 | 
			
		||||
		<rb-table>
 | 
			
		||||
			<ng-container *ngFor="let sample of samples; index as gIndex">
 | 
			
		||||
				<tr><th>{{sample.number}}</th><th></th></tr>
 | 
			
		||||
				<tr *ngFor="let parameter of (d.id.conditionTemplates[sample.condition.condition_template] || {parameters: []})
 | 
			
		||||
					.parameters">
 | 
			
		||||
					<td>{{parameter.name}}</td><td>{{sample.condition[parameter.name]}}</td>
 | 
			
		||||
				</tr>
 | 
			
		||||
				<ng-container *ngFor="let measurement of sample.measurements">
 | 
			
		||||
					<tr *ngFor="let parameter of d.id.measurementTemplates[measurement.measurement_template].parameters">
 | 
			
		||||
						<td>{{parameter.name}}</td><td>{{measurement.values[parameter.name]}}</td>
 | 
			
		||||
					</tr>
 | 
			
		||||
				</ng-container>
 | 
			
		||||
			</ng-container>
 | 
			
		||||
		</rb-table>
 | 
			
		||||
		<rb-icon-button icon="save" mode="primary" type="submit" (click)="cmSave()">
 | 
			
		||||
			Save sample{{samples.length > 1 ? 's' : ''}}
 | 
			
		||||
		</rb-icon-button>
 | 
			
		||||
	</div>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
<ng-template #modalDeleteConfirm>
 | 
			
		||||
  <rb-alert alertTitle="Are you sure?" type="danger" [okBtnLabel]="'Delete sample' + (samples.length > 1 ? 's' : '')"
 | 
			
		||||
            cancelBtnLabel="Cancel">
 | 
			
		||||
    Do you really want to delete {{(samples.length > 1 ? 'samples ' : 'sample ') + sampleNames()}}?
 | 
			
		||||
  </rb-alert>
 | 
			
		||||
	<rb-alert alertTitle="Are you sure?" type="danger" [okBtnLabel]="'Delete sample' + (samples.length > 1 ? 's' : '')"
 | 
			
		||||
		cancelBtnLabel="Cancel">
 | 
			
		||||
		Do you really want to delete {{(samples.length > 1 ? 'samples ' : 'sample ') + sampleNames()}}?
 | 
			
		||||
	</rb-alert>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,49 +1,49 @@
 | 
			
		||||
::ng-deep rb-table#response-data > table {
 | 
			
		||||
  width: auto !important;
 | 
			
		||||
	width: auto !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
td:first-child {
 | 
			
		||||
  font-weight: bold;
 | 
			
		||||
	font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.condition-set {
 | 
			
		||||
  float: right;
 | 
			
		||||
	float: right;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.two-col {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: 1fr 1fr;
 | 
			
		||||
  grid-column-gap: 10px;
 | 
			
		||||
	display: grid;
 | 
			
		||||
	grid-template-columns: 1fr 1fr;
 | 
			
		||||
	grid-column-gap: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dpt-chart {
 | 
			
		||||
  max-width: 400px;
 | 
			
		||||
	max-width: 400px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sample-count {
 | 
			
		||||
  max-width: 150px;
 | 
			
		||||
  margin-right: 20px;
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
	max-width: 150px;
 | 
			
		||||
	margin-right: 20px;
 | 
			
		||||
	display: inline-block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.set-new-material {
 | 
			
		||||
  display: block;
 | 
			
		||||
	display: block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.delete-sample {
 | 
			
		||||
  float: right;
 | 
			
		||||
	float: right;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.cm-form {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: 1fr 1fr;
 | 
			
		||||
  grid-column-gap: 1rem;
 | 
			
		||||
	display: grid;
 | 
			
		||||
	grid-template-columns: 1fr 1fr;
 | 
			
		||||
	grid-column-gap: 1rem;
 | 
			
		||||
 | 
			
		||||
  rb-tab-panel, div.buttons {
 | 
			
		||||
    grid-column: span 2;
 | 
			
		||||
  }
 | 
			
		||||
	rb-tab-panel, div.buttons {
 | 
			
		||||
		grid-column: span 2;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.restore-measurements {
 | 
			
		||||
  float: right;
 | 
			
		||||
	float: right;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,292 +1,292 @@
 | 
			
		||||
<div class="header-addnew">
 | 
			
		||||
  <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="sampleSelect === 2" mode="secondary" icon="close" (click)="sampleSelect = 0"
 | 
			
		||||
    class="validation-close" iconOnly></rb-icon-button>
 | 
			
		||||
  <rb-icon-button *ngIf="login.isLevel.dev" [icon]="sampleSelect === 2 ? 'checkmark' : 'clear-all'" mode="secondary"
 | 
			
		||||
    (click)="validate()">
 | 
			
		||||
    {{sampleSelect === 2 ? 'Validate' : 'Validation'}}
 | 
			
		||||
  </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="sampleSelect === 2" mode="secondary" icon="close" (click)="sampleSelect = 0"
 | 
			
		||||
		class="validation-close" iconOnly></rb-icon-button>
 | 
			
		||||
	<rb-icon-button *ngIf="login.isLevel.dev" [icon]="sampleSelect === 2 ? 'checkmark' : 'clear-all'" mode="secondary"
 | 
			
		||||
		(click)="validate()">
 | 
			
		||||
		{{sampleSelect === 2 ? 'Validate' : 'Validation'}}
 | 
			
		||||
	</rb-icon-button>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!--FILTERS-->
 | 
			
		||||
 | 
			
		||||
<rb-accordion>
 | 
			
		||||
  <rb-accordion-title [open]="false"><span class="rb-ic rb-ic-filter"></span>  Filter</rb-accordion-title>
 | 
			
		||||
  <rb-accordion-body>
 | 
			
		||||
    <form class="filters">
 | 
			
		||||
      <div class="status-selection">
 | 
			
		||||
        <label class="label">Status</label>
 | 
			
		||||
        <rb-form-checkbox name="status-validated" [(ngModel)]="filters.status.validated"
 | 
			
		||||
          [disabled]="!filters.status.new && !filters.status.deleted" (ngModelChange)="loadSamples({firstPage: true})">
 | 
			
		||||
          validated
 | 
			
		||||
        </rb-form-checkbox>
 | 
			
		||||
        <rb-form-checkbox name="status-new" [(ngModel)]="filters.status.new"
 | 
			
		||||
          [disabled]="!filters.status.validated && !filters.status.deleted"
 | 
			
		||||
          (ngModelChange)="loadSamples({firstPage: true})">
 | 
			
		||||
          new
 | 
			
		||||
        </rb-form-checkbox>
 | 
			
		||||
        <rb-form-checkbox name="status-deleted" [(ngModel)]="filters.status.deleted"
 | 
			
		||||
          [disabled]="!filters.status.validated && !filters.status.new" (ngModelChange)="loadSamples({firstPage: true})"
 | 
			
		||||
          *ngIf="login.isLevel.dev">
 | 
			
		||||
          deleted
 | 
			
		||||
        </rb-form-checkbox>
 | 
			
		||||
      </div>
 | 
			
		||||
      <rb-form-select name="pageSizeSelection" label="page size" [(ngModel)]="filters.pageSize" class="selection"
 | 
			
		||||
        (ngModelChange)="loadSamples({firstPage: true})" #pageSizeSelection>
 | 
			
		||||
        <option value="3">3</option>
 | 
			
		||||
        <option value="10">10</option>
 | 
			
		||||
        <option value="25">25</option>
 | 
			
		||||
        <option value="50">50</option>
 | 
			
		||||
        <option value="100">100</option>
 | 
			
		||||
        <option value="250">250</option>
 | 
			
		||||
        <option value="500">500</option>
 | 
			
		||||
      </rb-form-select>
 | 
			
		||||
	<rb-accordion-title [open]="false"><span class="rb-ic rb-ic-filter"></span>  Filter</rb-accordion-title>
 | 
			
		||||
	<rb-accordion-body>
 | 
			
		||||
		<form class="filters">
 | 
			
		||||
			<div class="status-selection">
 | 
			
		||||
				<label class="label">Status</label>
 | 
			
		||||
				<rb-form-checkbox name="status-validated" [(ngModel)]="filters.status.validated"
 | 
			
		||||
					[disabled]="!filters.status.new && !filters.status.deleted" (ngModelChange)="loadSamples({firstPage: true})">
 | 
			
		||||
					validated
 | 
			
		||||
				</rb-form-checkbox>
 | 
			
		||||
				<rb-form-checkbox name="status-new" [(ngModel)]="filters.status.new"
 | 
			
		||||
					[disabled]="!filters.status.validated && !filters.status.deleted"
 | 
			
		||||
					(ngModelChange)="loadSamples({firstPage: true})">
 | 
			
		||||
					new
 | 
			
		||||
				</rb-form-checkbox>
 | 
			
		||||
				<rb-form-checkbox name="status-deleted" [(ngModel)]="filters.status.deleted"
 | 
			
		||||
					[disabled]="!filters.status.validated && !filters.status.new" (ngModelChange)="loadSamples({firstPage: true})"
 | 
			
		||||
					*ngIf="login.isLevel.dev">
 | 
			
		||||
					deleted
 | 
			
		||||
				</rb-form-checkbox>
 | 
			
		||||
			</div>
 | 
			
		||||
			<rb-form-select name="pageSizeSelection" label="page size" [(ngModel)]="filters.pageSize" class="selection"
 | 
			
		||||
				(ngModelChange)="loadSamples({firstPage: true})" #pageSizeSelection>
 | 
			
		||||
				<option value="3">3</option>
 | 
			
		||||
				<option value="10">10</option>
 | 
			
		||||
				<option value="25">25</option>
 | 
			
		||||
				<option value="50">50</option>
 | 
			
		||||
				<option value="100">100</option>
 | 
			
		||||
				<option value="250">250</option>
 | 
			
		||||
				<option value="500">500</option>
 | 
			
		||||
			</rb-form-select>
 | 
			
		||||
 | 
			
		||||
      <div id="multi">
 | 
			
		||||
        <rb-form-multi-select *ngFor="let item of categories | keyvalue: originalOrder" name="fieldSelect" idField="id"
 | 
			
		||||
          [items]="item.value" [(ngModel)]="isActiveKey" [label]="item.key" class="selection"
 | 
			
		||||
          (ngModelChange)="loadSamples({}, $event, item.key)">
 | 
			
		||||
          <span *rbFormMultiSelectOption="let item" class="load-first-page">{{item.label}}</span>
 | 
			
		||||
        </rb-form-multi-select>
 | 
			
		||||
      </div>
 | 
			
		||||
			<div id="multi">
 | 
			
		||||
				<rb-form-multi-select *ngFor="let item of categories | keyvalue: originalOrder" name="fieldSelect" idField="id"
 | 
			
		||||
					[items]="item.value" [(ngModel)]="isActiveKey" [label]="item.key" class="selection"
 | 
			
		||||
					(ngModelChange)="loadSamples({}, $event, item.key)">
 | 
			
		||||
					<span *rbFormMultiSelectOption="let item" class="load-first-page">{{item.label}}</span>
 | 
			
		||||
				</rb-form-multi-select>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
      <rb-icon-button icon="reset" mode="secondary" (click)="resetPreferences()" class="reset-preferences">
 | 
			
		||||
        Reset to default
 | 
			
		||||
      </rb-icon-button>
 | 
			
		||||
			<rb-icon-button icon="reset" mode="secondary" (click)="resetPreferences()" class="reset-preferences">
 | 
			
		||||
				Reset to default
 | 
			
		||||
			</rb-icon-button>
 | 
			
		||||
 | 
			
		||||
      <div class="fieldfilters">
 | 
			
		||||
        <div *ngFor="let filter of filters.filters">
 | 
			
		||||
          <ng-container *ngIf="isActiveKey[filter.field]">
 | 
			
		||||
            <rb-form-checkbox [name]="'filteractive-' + filter.field" [(ngModel)]="filter.active"></rb-form-checkbox>
 | 
			
		||||
            <rb-form-select [name]="'filtermode-' + filter.field" class="filtermode" [(ngModel)]="filter.mode"
 | 
			
		||||
              (ngModelChange)="updateFilterFields(filter.field)">
 | 
			
		||||
              <option value="eq" title="field is equal to value">=</option>
 | 
			
		||||
              <option value="ne" title="field is not equal to value">≠</option>
 | 
			
		||||
              <option value="lt" title="field is lower than value"><</option>
 | 
			
		||||
              <option value="lte" title="field is lower than or equal to value">≤</option>
 | 
			
		||||
              <option value="gt" title="field is greater than value">></option>
 | 
			
		||||
              <option value="gte" title="field is greater than or equal to value">≥</option>
 | 
			
		||||
              <option value="stringin" title="field contains value">⊇</option>
 | 
			
		||||
              <option value="in" title="field is one of the values">∈</option>
 | 
			
		||||
              <option value="nin" title="field is not one of the values">∉</option>
 | 
			
		||||
              <option value="null" title="field is null">∅</option>
 | 
			
		||||
            </rb-form-select>
 | 
			
		||||
            <div class="filter-inputs">
 | 
			
		||||
              <rb-array-input [(ngModel)]="filter.values" [name]="'filter-' + filter.field"
 | 
			
		||||
                [pushTemplate]="!(filter.mode === 'in' || filter.mode === 'nin') ? null :''"
 | 
			
		||||
                (ngModelChange)="updateFilterFields(filter.field)">
 | 
			
		||||
                <ng-container *rbArrayInputItem="let item" [ngSwitch]="(filter.autocomplete.length ? 'autocomplete' : '') +
 | 
			
		||||
                              (filter.field == 'added' ? 'date' : (filter.field == 'type' ? 'type' : ''))">
 | 
			
		||||
                  <rb-form-date-input *ngSwitchCase="'date'" [rbArrayInputListener]="'filter-' + filter.field"
 | 
			
		||||
                    [name]="'filter-' + filter.field + item.i" [index]="item.i" [label]="filter.label"
 | 
			
		||||
                    [(ngModel)]="item.value" [disabled]="filter.mode === 'null'"></rb-form-date-input>
 | 
			
		||||
                  <rb-form-input *ngSwitchCase="''" [rbArrayInputListener]="'filter-' + filter.field"
 | 
			
		||||
                    [name]="'filter-' + filter.field + item.i" [index]="item.i" [label]="filter.label"
 | 
			
		||||
                    [(ngModel)]="item.value" [disabled]="filter.mode === 'null'">
 | 
			
		||||
                  </rb-form-input>
 | 
			
		||||
                  <rb-form-input *ngSwitchCase="'autocomplete'" [rbArrayInputListener]="'filter-' + filter.field"
 | 
			
		||||
                    [name]="'filter-' + filter.field + item.i" [index]="item.i" [label]="filter.label"
 | 
			
		||||
                    [(ngModel)]="item.value" [rbDebounceTime]="0" (keydown)="preventDefault($event, 'Enter')"
 | 
			
		||||
                    [rbFormInputAutocomplete]="autocomplete.bind(this, filter.autocomplete)"
 | 
			
		||||
                    [disabled]="filter.mode === 'null'" ngModel></rb-form-input>
 | 
			
		||||
                  <rb-form-select *ngSwitchCase="'type'" [rbArrayInputListener]="'filter-' + filter.field"
 | 
			
		||||
                    [name]="'filter-' + filter.field + item.i" [index]="item.i" [label]="filter.label"
 | 
			
		||||
                    [(ngModel)]="item.value">
 | 
			
		||||
                    <option value="as-delivered/raw">as-delivered/raw</option>
 | 
			
		||||
                    <option value="processed">processed</option>
 | 
			
		||||
                  </rb-form-select>
 | 
			
		||||
                </ng-container>
 | 
			
		||||
              </rb-array-input>
 | 
			
		||||
            </div>
 | 
			
		||||
          </ng-container>
 | 
			
		||||
        </div>
 | 
			
		||||
        <rb-form-checkbox name="no-condition" [(ngModel)]="filters.no.condition" class="space-right">
 | 
			
		||||
          has no condition
 | 
			
		||||
        </rb-form-checkbox>
 | 
			
		||||
        <rb-form-checkbox name="no-measurements" [(ngModel)]="filters.no.measurements" class="space-right">
 | 
			
		||||
          has no measurements
 | 
			
		||||
        </rb-form-checkbox>
 | 
			
		||||
      </div>
 | 
			
		||||
    </form>
 | 
			
		||||
			<div class="fieldfilters">
 | 
			
		||||
				<div *ngFor="let filter of filters.filters">
 | 
			
		||||
					<ng-container *ngIf="isActiveKey[filter.field]">
 | 
			
		||||
						<rb-form-checkbox [name]="'filteractive-' + filter.field" [(ngModel)]="filter.active"></rb-form-checkbox>
 | 
			
		||||
						<rb-form-select [name]="'filtermode-' + filter.field" class="filtermode" [(ngModel)]="filter.mode"
 | 
			
		||||
							(ngModelChange)="updateFilterFields(filter.field)">
 | 
			
		||||
							<option value="eq" title="field is equal to value">=</option>
 | 
			
		||||
							<option value="ne" title="field is not equal to value">≠</option>
 | 
			
		||||
							<option value="lt" title="field is lower than value"><</option>
 | 
			
		||||
							<option value="lte" title="field is lower than or equal to value">≤</option>
 | 
			
		||||
							<option value="gt" title="field is greater than value">></option>
 | 
			
		||||
							<option value="gte" title="field is greater than or equal to value">≥</option>
 | 
			
		||||
							<option value="stringin" title="field contains value">⊇</option>
 | 
			
		||||
							<option value="in" title="field is one of the values">∈</option>
 | 
			
		||||
							<option value="nin" title="field is not one of the values">∉</option>
 | 
			
		||||
							<option value="null" title="field is null">∅</option>
 | 
			
		||||
						</rb-form-select>
 | 
			
		||||
						<div class="filter-inputs">
 | 
			
		||||
							<rb-array-input [(ngModel)]="filter.values" [name]="'filter-' + filter.field"
 | 
			
		||||
								[pushTemplate]="!(filter.mode === 'in' || filter.mode === 'nin') ? null :''"
 | 
			
		||||
								(ngModelChange)="updateFilterFields(filter.field)">
 | 
			
		||||
								<ng-container *rbArrayInputItem="let item" [ngSwitch]="(filter.autocomplete.length ? 'autocomplete' : '') +
 | 
			
		||||
									(filter.field == 'added' ? 'date' : (filter.field == 'type' ? 'type' : ''))">
 | 
			
		||||
									<rb-form-date-input *ngSwitchCase="'date'" [rbArrayInputListener]="'filter-' + filter.field"
 | 
			
		||||
										[name]="'filter-' + filter.field + item.i" [index]="item.i" [label]="filter.label"
 | 
			
		||||
										[(ngModel)]="item.value" [disabled]="filter.mode === 'null'"></rb-form-date-input>
 | 
			
		||||
									<rb-form-input *ngSwitchCase="''" [rbArrayInputListener]="'filter-' + filter.field"
 | 
			
		||||
										[name]="'filter-' + filter.field + item.i" [index]="item.i" [label]="filter.label"
 | 
			
		||||
										[(ngModel)]="item.value" [disabled]="filter.mode === 'null'">
 | 
			
		||||
									</rb-form-input>
 | 
			
		||||
									<rb-form-input *ngSwitchCase="'autocomplete'" [rbArrayInputListener]="'filter-' + filter.field"
 | 
			
		||||
										[name]="'filter-' + filter.field + item.i" [index]="item.i" [label]="filter.label"
 | 
			
		||||
										[(ngModel)]="item.value" [rbDebounceTime]="0" (keydown)="preventDefault($event, 'Enter')"
 | 
			
		||||
										[rbFormInputAutocomplete]="autocomplete.bind(this, filter.autocomplete)"
 | 
			
		||||
										[disabled]="filter.mode === 'null'" ngModel></rb-form-input>
 | 
			
		||||
									<rb-form-select *ngSwitchCase="'type'" [rbArrayInputListener]="'filter-' + filter.field"
 | 
			
		||||
										[name]="'filter-' + filter.field + item.i" [index]="item.i" [label]="filter.label"
 | 
			
		||||
										[(ngModel)]="item.value">
 | 
			
		||||
										<option value="as-delivered/raw">as-delivered/raw</option>
 | 
			
		||||
										<option value="processed">processed</option>
 | 
			
		||||
									</rb-form-select>
 | 
			
		||||
								</ng-container>
 | 
			
		||||
							</rb-array-input>
 | 
			
		||||
						</div>
 | 
			
		||||
					</ng-container>
 | 
			
		||||
				</div>
 | 
			
		||||
				<rb-form-checkbox name="no-condition" [(ngModel)]="filters.no.condition" class="space-right">
 | 
			
		||||
					has no condition
 | 
			
		||||
				</rb-form-checkbox>
 | 
			
		||||
				<rb-form-checkbox name="no-measurements" [(ngModel)]="filters.no.measurements" class="space-right">
 | 
			
		||||
					has no measurements
 | 
			
		||||
				</rb-form-checkbox>
 | 
			
		||||
			</div>
 | 
			
		||||
		</form>
 | 
			
		||||
 | 
			
		||||
    <rb-icon-button icon="forward-right" mode="primary" (click)="loadSamples({firstPage: true})">
 | 
			
		||||
      Apply filters
 | 
			
		||||
    </rb-icon-button>
 | 
			
		||||
  </rb-accordion-body>
 | 
			
		||||
		<rb-icon-button icon="forward-right" mode="primary" (click)="loadSamples({firstPage: true})">
 | 
			
		||||
			Apply filters
 | 
			
		||||
		</rb-icon-button>
 | 
			
		||||
	</rb-accordion-body>
 | 
			
		||||
</rb-accordion>
 | 
			
		||||
 | 
			
		||||
<ng-container *ngTemplateOutlet="paging"></ng-container>
 | 
			
		||||
 | 
			
		||||
<div class="samples-loading" *ngIf="loading">
 | 
			
		||||
  <rb-loading-spinner></rb-loading-spinner>
 | 
			
		||||
  <div>Loading...</div>
 | 
			
		||||
	<rb-loading-spinner></rb-loading-spinner>
 | 
			
		||||
	<div>Loading...</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!--DOWNLOAD BUTTONS-->
 | 
			
		||||
 | 
			
		||||
<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>
 | 
			
		||||
    <div class="link-dialog">
 | 
			
		||||
      <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 class="space-right" name="download-spectra" [(ngModel)]="downloadSpectra">
 | 
			
		||||
        add spectra
 | 
			
		||||
      </rb-form-checkbox>
 | 
			
		||||
      <rb-form-checkbox class="space-right" name="download-conditions" [(ngModel)]="downloadCondition">
 | 
			
		||||
        add conditions
 | 
			
		||||
      </rb-form-checkbox>
 | 
			
		||||
      <rb-form-checkbox class="space-right" name="download-flatten" [(ngModel)]="downloadFlatten">
 | 
			
		||||
        flatten object
 | 
			
		||||
      </rb-form-checkbox>
 | 
			
		||||
      <rb-icon-button icon="clipboard" mode="secondary" (click)="clipboard()">Copy to clipboard</rb-icon-button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
  <a [href]="csvUrl" download="samples.csv">
 | 
			
		||||
    <rb-icon-button icon="download" mode="secondary" (mousedown)="csvUrl = sampleUrl({csv: true, export: true})">
 | 
			
		||||
      Download result as CSV
 | 
			
		||||
    </rb-icon-button>
 | 
			
		||||
  </a>
 | 
			
		||||
	<rb-icon-button class="space-right" icon="download" mode="secondary" [rbModal]="linkModal">
 | 
			
		||||
		JSON download link
 | 
			
		||||
	</rb-icon-button>
 | 
			
		||||
	<ng-template #linkModal>
 | 
			
		||||
		<div class="link-dialog">
 | 
			
		||||
			<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 class="space-right" name="download-spectra" [(ngModel)]="downloadSpectra">
 | 
			
		||||
				add spectra
 | 
			
		||||
			</rb-form-checkbox>
 | 
			
		||||
			<rb-form-checkbox class="space-right" name="download-conditions" [(ngModel)]="downloadCondition">
 | 
			
		||||
				add conditions
 | 
			
		||||
			</rb-form-checkbox>
 | 
			
		||||
			<rb-form-checkbox class="space-right" name="download-flatten" [(ngModel)]="downloadFlatten">
 | 
			
		||||
				flatten object
 | 
			
		||||
			</rb-form-checkbox>
 | 
			
		||||
			<rb-icon-button icon="clipboard" mode="secondary" (click)="clipboard()">Copy to clipboard</rb-icon-button>
 | 
			
		||||
		</div>
 | 
			
		||||
	</ng-template>
 | 
			
		||||
	<a [href]="csvUrl" download="samples.csv">
 | 
			
		||||
		<rb-icon-button icon="download" mode="secondary" (mousedown)="csvUrl = sampleUrl({csv: true, export: true})">
 | 
			
		||||
			Download result as CSV
 | 
			
		||||
		</rb-icon-button>
 | 
			
		||||
	</a>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!--TABLE-->
 | 
			
		||||
 | 
			
		||||
<rb-table class="samples-table" scrollTop ellipsis>
 | 
			
		||||
  <tr>
 | 
			
		||||
    <th *ngIf="login.isLevel.write">
 | 
			
		||||
      <span class="rb-ic rb-ic-edit clickable" *ngIf="login.isLevel.dev" (click)="batchEdit()"></span>
 | 
			
		||||
      <span class="rb-ic rb-ic-close clickable" *ngIf="sampleSelect === 1" (click)="sampleSelect = 0"></span>
 | 
			
		||||
    </th>
 | 
			
		||||
    <th *ngIf="sampleSelect">
 | 
			
		||||
      <rb-form-checkbox name="select-all" (change)="selectAll($event)">all</rb-form-checkbox>
 | 
			
		||||
    </th>
 | 
			
		||||
    <th *ngFor="let key of activeKeys" [title]="key.label">
 | 
			
		||||
      <div class="sort-header">
 | 
			
		||||
        <span>{{key.label}}</span>
 | 
			
		||||
        <div *ngIf="key.sortable">
 | 
			
		||||
          <span class="rb-ic rb-ic-up sort-arr-up" (click)="setSort(key.id + '-' + 'desc')">
 | 
			
		||||
            <span *ngIf="filters.sort === key.id + '-' + 'desc'"></span>
 | 
			
		||||
          </span>
 | 
			
		||||
          <span class="rb-ic rb-ic-down sort-arr-down" (click)="setSort(key.id + '-' + 'asc')">
 | 
			
		||||
            <span *ngIf="filters.sort === key.id + '-' + 'asc'"></span>
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </th>
 | 
			
		||||
  </tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<th *ngIf="login.isLevel.write">
 | 
			
		||||
			<span class="rb-ic rb-ic-edit clickable" *ngIf="login.isLevel.dev" (click)="batchEdit()"></span>
 | 
			
		||||
			<span class="rb-ic rb-ic-close clickable" *ngIf="sampleSelect === 1" (click)="sampleSelect = 0"></span>
 | 
			
		||||
		</th>
 | 
			
		||||
		<th *ngIf="sampleSelect">
 | 
			
		||||
			<rb-form-checkbox name="select-all" (change)="selectAll($event)">all</rb-form-checkbox>
 | 
			
		||||
		</th>
 | 
			
		||||
		<th *ngFor="let key of activeKeys" [title]="key.label">
 | 
			
		||||
			<div class="sort-header">
 | 
			
		||||
				<span>{{key.label}}</span>
 | 
			
		||||
				<div *ngIf="key.sortable">
 | 
			
		||||
					<span class="rb-ic rb-ic-up sort-arr-up" (click)="setSort(key.id + '-' + 'desc')">
 | 
			
		||||
						<span *ngIf="filters.sort === key.id + '-' + 'desc'"></span>
 | 
			
		||||
					</span>
 | 
			
		||||
					<span class="rb-ic rb-ic-down sort-arr-down" (click)="setSort(key.id + '-' + 'asc')">
 | 
			
		||||
						<span *ngIf="filters.sort === key.id + '-' + 'asc'"></span>
 | 
			
		||||
					</span>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</th>
 | 
			
		||||
	</tr>
 | 
			
		||||
 | 
			
		||||
  <tr *ngFor="let sample of samples; index as i" class="clickable" (click)="sampleDetails(sample._id, sampleModal)">
 | 
			
		||||
    <td *ngIf="login.isLevel.write">
 | 
			
		||||
      <a [routerLink]="'/samples/edit/' + sample._id" *ngIf="sample.status !== 'deleted' &&
 | 
			
		||||
         (login.isLevel.dev || (login.isLevel.write && sample.user_id === login.userId))">
 | 
			
		||||
        <span class="rb-ic rb-ic-edit clickable"></span>
 | 
			
		||||
      </a>
 | 
			
		||||
      <span class="rb-ic rb-ic-undo clickable" *ngIf="sample.status === 'deleted' && login.isLevel.dev"
 | 
			
		||||
        (click)="restoreSample(sample._id, restoreConfirm, $event)"></span>
 | 
			
		||||
    </td>
 | 
			
		||||
    <td *ngIf="sampleSelect">
 | 
			
		||||
      <rb-form-checkbox *ngIf="sample.status !== 'deleted'" [name]="'validate-' + i" (click)="stopPropagation($event)"
 | 
			
		||||
        [(ngModel)]="sample.selected">
 | 
			
		||||
      </rb-form-checkbox>
 | 
			
		||||
    </td>
 | 
			
		||||
    <td *ngFor="let item of activeKeys; index as j">
 | 
			
		||||
      {{data[i].data[j]}}
 | 
			
		||||
    </td>
 | 
			
		||||
  </tr>
 | 
			
		||||
	<tr *ngFor="let sample of samples; index as i" class="clickable" (click)="sampleDetails(sample._id, sampleModal)">
 | 
			
		||||
		<td *ngIf="login.isLevel.write">
 | 
			
		||||
			<a [routerLink]="'/samples/edit/' + sample._id" *ngIf="sample.status !== 'deleted' &&
 | 
			
		||||
				(login.isLevel.dev || (login.isLevel.write && sample.user_id === login.userId))">
 | 
			
		||||
				<span class="rb-ic rb-ic-edit clickable"></span>
 | 
			
		||||
			</a>
 | 
			
		||||
			<span class="rb-ic rb-ic-undo clickable" *ngIf="sample.status === 'deleted' && login.isLevel.dev"
 | 
			
		||||
				(click)="restoreSample(sample._id, restoreConfirm, $event)"></span>
 | 
			
		||||
		</td>
 | 
			
		||||
		<td *ngIf="sampleSelect">
 | 
			
		||||
			<rb-form-checkbox *ngIf="sample.status !== 'deleted'" [name]="'validate-' + i" (click)="stopPropagation($event)"
 | 
			
		||||
				[(ngModel)]="sample.selected">
 | 
			
		||||
			</rb-form-checkbox>
 | 
			
		||||
		</td>
 | 
			
		||||
		<td *ngFor="let item of activeKeys; index as j">
 | 
			
		||||
			{{data[i].data[j]}}
 | 
			
		||||
		</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
</rb-table>
 | 
			
		||||
 | 
			
		||||
<ng-container *ngTemplateOutlet="paging"></ng-container>
 | 
			
		||||
 | 
			
		||||
<ng-template #paging>
 | 
			
		||||
  <div class="paging">
 | 
			
		||||
    <button class="rb-btn rb-link" type="button" (click)="loadPage(-1)" [disabled]="page === 1">
 | 
			
		||||
      <span class="rb-ic  rb-ic-back-left"></span>
 | 
			
		||||
    </button>
 | 
			
		||||
    <rb-form-input label="page" (ngModelChange)="loadPage($event - page)" [ngModel]="page">
 | 
			
		||||
    </rb-form-input>
 | 
			
		||||
    <span>
 | 
			
		||||
      of {{pages}} ({{totalSamples}} samples)
 | 
			
		||||
    </span>
 | 
			
		||||
    <button class="rb-btn rb-link" type="button" (click)="loadPage(1)" [disabled]="page >= pages">
 | 
			
		||||
      <span class="rb-ic  rb-ic-forward-right"></span>
 | 
			
		||||
    </button>
 | 
			
		||||
  </div>
 | 
			
		||||
	<div class="paging">
 | 
			
		||||
		<button class="rb-btn rb-link" type="button" (click)="loadPage(-1)" [disabled]="page === 1">
 | 
			
		||||
			<span class="rb-ic  rb-ic-back-left"></span>
 | 
			
		||||
		</button>
 | 
			
		||||
		<rb-form-input label="page" (ngModelChange)="loadPage($event - page)" [ngModel]="page">
 | 
			
		||||
		</rb-form-input>
 | 
			
		||||
		<span>
 | 
			
		||||
			of {{pages}} ({{totalSamples}} samples)
 | 
			
		||||
		</span>
 | 
			
		||||
		<button class="rb-btn rb-link" type="button" (click)="loadPage(1)" [disabled]="page >= pages">
 | 
			
		||||
			<span class="rb-ic  rb-ic-forward-right"></span>
 | 
			
		||||
		</button>
 | 
			
		||||
	</div>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
<!--SAMPLE DETAILS-->
 | 
			
		||||
 | 
			
		||||
<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[1]}}</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 conditionField of sampleDetailsSample.condition_entries">
 | 
			
		||||
        <th>{{conditionField[0]}}</th>
 | 
			
		||||
        <td>{{conditionField[1]}}</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>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <th>Status</th>
 | 
			
		||||
        <td>{{sampleDetailsSample.status}}</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </rb-table>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
	<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[1]}}</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 conditionField of sampleDetailsSample.condition_entries">
 | 
			
		||||
				<th>{{conditionField[0]}}</th>
 | 
			
		||||
				<td>{{conditionField[1]}}</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>
 | 
			
		||||
			<tr>
 | 
			
		||||
				<th>Status</th>
 | 
			
		||||
				<td>{{sampleDetailsSample.status}}</td>
 | 
			
		||||
			</tr>
 | 
			
		||||
		</rb-table>
 | 
			
		||||
	</ng-template>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
<ng-template #restoreConfirm>
 | 
			
		||||
  <rb-dialog dialogTitle="Restore sample">
 | 
			
		||||
    Do you really want to restore this sample?
 | 
			
		||||
  </rb-dialog>
 | 
			
		||||
</ng-template>
 | 
			
		||||
	<rb-dialog dialogTitle="Restore sample">
 | 
			
		||||
		Do you really want to restore this sample?
 | 
			
		||||
	</rb-dialog>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,249 +1,249 @@
 | 
			
		||||
@import "~@inst-iot/bosch-angular-ui-components/styles/variables/colors";
 | 
			
		||||
 | 
			
		||||
.header-addnew {
 | 
			
		||||
  margin-bottom: 40px;
 | 
			
		||||
  height: 42px;
 | 
			
		||||
	margin-bottom: 40px;
 | 
			
		||||
	height: 42px;
 | 
			
		||||
 | 
			
		||||
  & > * {
 | 
			
		||||
    display: inline;
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
  }
 | 
			
		||||
	& > * {
 | 
			
		||||
		display: inline;
 | 
			
		||||
		margin-bottom: 10px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  rb-icon-button {
 | 
			
		||||
    float: right;
 | 
			
		||||
  }
 | 
			
		||||
	rb-icon-button {
 | 
			
		||||
		float: right;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
rb-table {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
	width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 td .clickable.rb-ic {
 | 
			
		||||
  font-size: 1.1rem;
 | 
			
		||||
  color: $color-gray-silver-sand;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
td .clickable.rb-ic {
 | 
			
		||||
	font-size: 1.1rem;
 | 
			
		||||
	color: $color-gray-silver-sand;
 | 
			
		||||
	cursor: pointer;
 | 
			
		||||
 | 
			
		||||
  &:hover {
 | 
			
		||||
    color: #000;
 | 
			
		||||
  }
 | 
			
		||||
	&:hover {
 | 
			
		||||
		color: #000;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
rb-form-multi-select{
 | 
			
		||||
  min-width: 9rem;
 | 
			
		||||
	min-width: 9rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.status-selection {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  margin-bottom: 10px;
 | 
			
		||||
  float: left;
 | 
			
		||||
  margin-right: 15px;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
	margin-bottom: 10px;
 | 
			
		||||
	float: left;
 | 
			
		||||
	margin-right: 15px;
 | 
			
		||||
 | 
			
		||||
  label {
 | 
			
		||||
    display: block;
 | 
			
		||||
    font-weight: 700;
 | 
			
		||||
    font-size: 10px;
 | 
			
		||||
  }
 | 
			
		||||
	label {
 | 
			
		||||
		display: block;
 | 
			
		||||
		font-weight: 700;
 | 
			
		||||
		font-size: 10px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  rb-form-checkbox {
 | 
			
		||||
    float: left;
 | 
			
		||||
    margin-right: 10px;
 | 
			
		||||
    margin-top: -10px;
 | 
			
		||||
  }
 | 
			
		||||
	rb-form-checkbox {
 | 
			
		||||
		float: left;
 | 
			
		||||
		margin-right: 10px;
 | 
			
		||||
		margin-top: -10px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.selection {
 | 
			
		||||
  max-width: 230px;
 | 
			
		||||
  float: left;
 | 
			
		||||
	max-width: 230px;
 | 
			
		||||
	float: left;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.paging {
 | 
			
		||||
  height: 50px;
 | 
			
		||||
  float: left;
 | 
			
		||||
	height: 50px;
 | 
			
		||||
	float: left;
 | 
			
		||||
 | 
			
		||||
  rb-form-input {
 | 
			
		||||
    max-width: 65px;
 | 
			
		||||
  }
 | 
			
		||||
	rb-form-input {
 | 
			
		||||
		max-width: 65px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  > * {
 | 
			
		||||
    float: left;
 | 
			
		||||
  }
 | 
			
		||||
	> * {
 | 
			
		||||
		float: left;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  > button {
 | 
			
		||||
    margin-top: 18px;
 | 
			
		||||
  }
 | 
			
		||||
	> button {
 | 
			
		||||
		margin-top: 18px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  > span {
 | 
			
		||||
    margin-top: 20px;
 | 
			
		||||
    margin-left: 5px;
 | 
			
		||||
  }
 | 
			
		||||
	> span {
 | 
			
		||||
		margin-top: 20px;
 | 
			
		||||
		margin-left: 5px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sort-header {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  position: relative;
 | 
			
		||||
	width: 100%;
 | 
			
		||||
	position: relative;
 | 
			
		||||
 | 
			
		||||
  & > span:first-child {
 | 
			
		||||
    max-width: 180px;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    display: block;
 | 
			
		||||
    text-overflow: ellipsis;
 | 
			
		||||
    margin-right: 20px;
 | 
			
		||||
  }
 | 
			
		||||
	& > span:first-child {
 | 
			
		||||
		max-width: 180px;
 | 
			
		||||
		overflow: hidden;
 | 
			
		||||
		display: block;
 | 
			
		||||
		text-overflow: ellipsis;
 | 
			
		||||
		margin-right: 20px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  div {
 | 
			
		||||
    display: grid;
 | 
			
		||||
    grid-template-columns: 1fr;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    background: #FFF;
 | 
			
		||||
	div {
 | 
			
		||||
		display: grid;
 | 
			
		||||
		grid-template-columns: 1fr;
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		right: 0;
 | 
			
		||||
		top: 0;
 | 
			
		||||
		background: #FFF;
 | 
			
		||||
 | 
			
		||||
    :nth-child(1) {
 | 
			
		||||
      margin-bottom: -3px;
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
    }
 | 
			
		||||
		:nth-child(1) {
 | 
			
		||||
			margin-bottom: -3px;
 | 
			
		||||
			cursor: pointer;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
    :nth-child(2) {
 | 
			
		||||
      margin-top: -3px;
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
		:nth-child(2) {
 | 
			
		||||
			margin-top: -3px;
 | 
			
		||||
			cursor: pointer;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.filters:after {
 | 
			
		||||
  content:"";
 | 
			
		||||
  clear:both;
 | 
			
		||||
  display:block;
 | 
			
		||||
	content:"";
 | 
			
		||||
	clear:both;
 | 
			
		||||
	display:block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.download {
 | 
			
		||||
  margin-top: 5px;
 | 
			
		||||
  float: right;
 | 
			
		||||
	margin-top: 5px;
 | 
			
		||||
	float: right;
 | 
			
		||||
 | 
			
		||||
  & > rb-form-checkbox {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
  }
 | 
			
		||||
	& > rb-form-checkbox {
 | 
			
		||||
		display: inline-block;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  button {
 | 
			
		||||
    margin-right: 10px;
 | 
			
		||||
  }
 | 
			
		||||
	button {
 | 
			
		||||
		margin-right: 10px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sort-arr-up {
 | 
			
		||||
  position: relative;
 | 
			
		||||
	position: relative;
 | 
			
		||||
 | 
			
		||||
  & > span {
 | 
			
		||||
    width: 0;
 | 
			
		||||
    height: 0;
 | 
			
		||||
    border-left: 6.3px solid transparent;
 | 
			
		||||
    border-right: 6.3px solid transparent;
 | 
			
		||||
    border-bottom: 6.3px solid #000;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 5px;
 | 
			
		||||
    display: block;
 | 
			
		||||
    left: 2px;
 | 
			
		||||
  }
 | 
			
		||||
	& > span {
 | 
			
		||||
		width: 0;
 | 
			
		||||
		height: 0;
 | 
			
		||||
		border-left: 6.3px solid transparent;
 | 
			
		||||
		border-right: 6.3px solid transparent;
 | 
			
		||||
		border-bottom: 6.3px solid #000;
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		top: 5px;
 | 
			
		||||
		display: block;
 | 
			
		||||
		left: 2px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sort-arr-down {
 | 
			
		||||
  position: relative;
 | 
			
		||||
	position: relative;
 | 
			
		||||
 | 
			
		||||
  & > span {
 | 
			
		||||
    width: 0;
 | 
			
		||||
    height: 0;
 | 
			
		||||
    border-left: 6.3px solid transparent;
 | 
			
		||||
    border-right: 6.3px solid transparent;
 | 
			
		||||
    border-top: 6.3px solid #000;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 5px;
 | 
			
		||||
    display: block;
 | 
			
		||||
    left: 2px;
 | 
			
		||||
  }
 | 
			
		||||
	& > span {
 | 
			
		||||
		width: 0;
 | 
			
		||||
		height: 0;
 | 
			
		||||
		border-left: 6.3px solid transparent;
 | 
			
		||||
		border-right: 6.3px solid transparent;
 | 
			
		||||
		border-top: 6.3px solid #000;
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		top: 5px;
 | 
			
		||||
		display: block;
 | 
			
		||||
		left: 2px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fieldfilters {
 | 
			
		||||
  clear: both;
 | 
			
		||||
	clear: both;
 | 
			
		||||
 | 
			
		||||
  & > div {
 | 
			
		||||
    display: grid;
 | 
			
		||||
    grid-template-columns: auto auto 1fr;
 | 
			
		||||
    float: left;
 | 
			
		||||
    margin-right: 30px;
 | 
			
		||||
  }
 | 
			
		||||
	& > div {
 | 
			
		||||
		display: grid;
 | 
			
		||||
		grid-template-columns: auto auto 1fr;
 | 
			
		||||
		float: left;
 | 
			
		||||
		margin-right: 30px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  & > rb-form-checkbox {
 | 
			
		||||
    float: left;
 | 
			
		||||
  }
 | 
			
		||||
	& > rb-form-checkbox {
 | 
			
		||||
		float: left;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.filtermode {
 | 
			
		||||
  max-width: 82px;
 | 
			
		||||
	max-width: 82px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
textarea.linkmodal {
 | 
			
		||||
  display: block;
 | 
			
		||||
  min-width: 600px;
 | 
			
		||||
  min-height: 200px;
 | 
			
		||||
  border: none;
 | 
			
		||||
	display: block;
 | 
			
		||||
	min-width: 600px;
 | 
			
		||||
	min-height: 200px;
 | 
			
		||||
	border: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.filter-inputs > * {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  width: 220px;
 | 
			
		||||
	display: inline-block;
 | 
			
		||||
	width: 220px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sample-details-table {
 | 
			
		||||
 | 
			
		||||
  td {
 | 
			
		||||
    max-width: none;
 | 
			
		||||
  }
 | 
			
		||||
	td {
 | 
			
		||||
		max-width: none;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.validation-close {
 | 
			
		||||
  margin-left: -1px;
 | 
			
		||||
	margin-left: -1px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.samples-table tr.clickable {
 | 
			
		||||
  background: none;
 | 
			
		||||
  transition: background-color 0.5s;
 | 
			
		||||
	background: none;
 | 
			
		||||
	transition: background-color 0.5s;
 | 
			
		||||
 | 
			
		||||
  &:hover {
 | 
			
		||||
    background: $color-gray-mercury;
 | 
			
		||||
  }
 | 
			
		||||
	&:hover {
 | 
			
		||||
		background: $color-gray-mercury;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::ng-deep .samples-table rb-form-checkbox .input-wrapper {
 | 
			
		||||
  padding-top: 0;
 | 
			
		||||
  margin-top: -4.5px;
 | 
			
		||||
	padding-top: 0;
 | 
			
		||||
	margin-top: -4.5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.link-dialog {
 | 
			
		||||
  rb-form-checkbox {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
  }
 | 
			
		||||
	rb-form-checkbox {
 | 
			
		||||
		display: inline-block;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  rb-icon-button {
 | 
			
		||||
    float: right;
 | 
			
		||||
  }
 | 
			
		||||
	rb-icon-button {
 | 
			
		||||
		float: right;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.samples-loading {
 | 
			
		||||
  float: left;
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: auto auto;
 | 
			
		||||
  grid-template-rows: 1fr auto 1fr;
 | 
			
		||||
	float: left;
 | 
			
		||||
	display: grid;
 | 
			
		||||
	grid-template-columns: auto auto;
 | 
			
		||||
	grid-template-rows: 1fr auto 1fr;
 | 
			
		||||
 | 
			
		||||
  rb-loading-spinner {
 | 
			
		||||
    grid-row: span 3;
 | 
			
		||||
    transform: scale(0.5);
 | 
			
		||||
  }
 | 
			
		||||
	rb-loading-spinner {
 | 
			
		||||
		grid-row: span 3;
 | 
			
		||||
		transform: scale(0.5);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  & > div {
 | 
			
		||||
    grid-column: 2 / 3;
 | 
			
		||||
    grid-row: 2 / 3;
 | 
			
		||||
  }
 | 
			
		||||
	& > div {
 | 
			
		||||
		grid-column: 2 / 3;
 | 
			
		||||
		grid-row: 2 / 3;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.reset-preferences {
 | 
			
		||||
  float: right;
 | 
			
		||||
	float: right;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -7,93 +7,93 @@ import {ModalService} from '@inst-iot/bosch-angular-ui-components';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@Injectable({
 | 
			
		||||
  providedIn: 'root'
 | 
			
		||||
	providedIn: 'root'
 | 
			
		||||
})
 | 
			
		||||
export class ApiService {
 | 
			
		||||
 | 
			
		||||
  private host = isDevMode() ? '/api' : 'https://definma-api.apps.de1.bosch-iot-cloud.com';
 | 
			
		||||
	private host = isDevMode() ? '/api' : 'https://definma-api.apps.de1.bosch-iot-cloud.com';
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private http: HttpClient,
 | 
			
		||||
    private storage: LocalStorageService,
 | 
			
		||||
    private modalService: ModalService,
 | 
			
		||||
    private window: Window
 | 
			
		||||
  ) { }
 | 
			
		||||
		constructor(
 | 
			
		||||
			private http: HttpClient,
 | 
			
		||||
			private storage: LocalStorageService,
 | 
			
		||||
			private modalService: ModalService,
 | 
			
		||||
			private window: Window
 | 
			
		||||
	) { }
 | 
			
		||||
 | 
			
		||||
  get hostName() {
 | 
			
		||||
    return this.host;
 | 
			
		||||
  }
 | 
			
		||||
	get hostName() {
 | 
			
		||||
		return this.host;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  // Main HTTP methods
 | 
			
		||||
  get<T>(url, f: (data?: T, err?, headers?) => void = () => {}) {
 | 
			
		||||
    this.requestErrorHandler<T>(this.http.get(this.url(url), this.options()), f);
 | 
			
		||||
  }
 | 
			
		||||
	// Main HTTP methods
 | 
			
		||||
	get<T>(url, f: (data?: T, err?, headers?) => void = () => {}) {
 | 
			
		||||
		this.requestErrorHandler<T>(this.http.get(this.url(url), this.options()), f);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  post<T>(url, data = null, f: (data?: T, err?, headers?) => void = () => {}) {
 | 
			
		||||
    this.requestErrorHandler<T>(this.http.post(this.url(url), data, this.options()), f);
 | 
			
		||||
  }
 | 
			
		||||
	post<T>(url, data = null, f: (data?: T, err?, headers?) => void = () => {}) {
 | 
			
		||||
		this.requestErrorHandler<T>(this.http.post(this.url(url), data, this.options()), f);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  put<T>(url, data = null, f: (data?: T, err?, headers?) => void = () => {}) {
 | 
			
		||||
    this.requestErrorHandler<T>(this.http.put(this.url(url), data, this.options()), f);
 | 
			
		||||
  }
 | 
			
		||||
	put<T>(url, data = null, f: (data?: T, err?, headers?) => void = () => {}) {
 | 
			
		||||
		this.requestErrorHandler<T>(this.http.put(this.url(url), data, this.options()), f);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  delete<T>(url, f: (data?: T, err?, headers?) => void = () => {}) {
 | 
			
		||||
    this.requestErrorHandler<T>(this.http.delete(this.url(url), this.options()), f);
 | 
			
		||||
  }
 | 
			
		||||
	delete<T>(url, f: (data?: T, err?, headers?) => void = () => {}) {
 | 
			
		||||
		this.requestErrorHandler<T>(this.http.delete(this.url(url), this.options()), f);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  // Execute request and handle errors
 | 
			
		||||
  private requestErrorHandler<T>(observable: Observable<any>, f: (data?: T, err?, headers?) => void) {
 | 
			
		||||
    observable.subscribe(data => {  // Successful request
 | 
			
		||||
      f(
 | 
			
		||||
        data.body,
 | 
			
		||||
        undefined,
 | 
			
		||||
        data.headers.keys().reduce((s, e) => {s[e.toLowerCase()] = data.headers.get(e); return s; }, {})
 | 
			
		||||
      );
 | 
			
		||||
    }, err => {  // Error
 | 
			
		||||
      if (f.length > 1) {  // Pass on error
 | 
			
		||||
        f(undefined, err, undefined);
 | 
			
		||||
      }
 | 
			
		||||
      else {  // Handle directly
 | 
			
		||||
        this.requestError(err);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
	// Execute request and handle errors
 | 
			
		||||
	private requestErrorHandler<T>(observable: Observable<any>, f: (data?: T, err?, headers?) => void) {
 | 
			
		||||
		observable.subscribe(data => {  // Successful request
 | 
			
		||||
			f(
 | 
			
		||||
				data.body,
 | 
			
		||||
				undefined,
 | 
			
		||||
				data.headers.keys().reduce((s, e) => {s[e.toLowerCase()] = data.headers.get(e); return s; }, {})
 | 
			
		||||
			);
 | 
			
		||||
		}, err => {  // Error
 | 
			
		||||
			if (f.length > 1) {  // Pass on error
 | 
			
		||||
				f(undefined, err, undefined);
 | 
			
		||||
			}
 | 
			
		||||
			else {  // Handle directly
 | 
			
		||||
				this.requestError(err);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  requestError(err) {  // Network error dialog
 | 
			
		||||
    const modalRef = this.modalService.openComponent(ErrorComponent);
 | 
			
		||||
    modalRef.instance.message = 'Network request failed!';
 | 
			
		||||
    const details = [err.error.status];
 | 
			
		||||
    if (err.error.details) {
 | 
			
		||||
      details.push(err.error.details);
 | 
			
		||||
    }
 | 
			
		||||
    modalRef.instance.details = details;
 | 
			
		||||
    modalRef.result.then(() => {
 | 
			
		||||
      this.window.location.reload();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
	requestError(err) {  // Network error dialog
 | 
			
		||||
		const modalRef = this.modalService.openComponent(ErrorComponent);
 | 
			
		||||
		modalRef.instance.message = 'Network request failed!';
 | 
			
		||||
		const details = [err.error.status];
 | 
			
		||||
		if (err.error.details) {
 | 
			
		||||
			details.push(err.error.details);
 | 
			
		||||
		}
 | 
			
		||||
		modalRef.instance.details = details;
 | 
			
		||||
		modalRef.result.then(() => {
 | 
			
		||||
			this.window.location.reload();
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  private url(url) {  // Detect if host was given, otherwise use default host
 | 
			
		||||
    if (/http[s]?:\/\//.test(url)) {
 | 
			
		||||
      return url;
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      return this.host + url;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Generate request options
 | 
			
		||||
  private options(): {headers: HttpHeaders, observe: 'body'} {
 | 
			
		||||
    return {headers: this.authOptions(), observe: 'response' as 'body'};
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Generate Basic Auth
 | 
			
		||||
  private authOptions(): HttpHeaders {
 | 
			
		||||
    const auth = this.storage.get('basicAuth');
 | 
			
		||||
    if (auth) {
 | 
			
		||||
      return new HttpHeaders({Authorization: 'Basic ' + auth});
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      return new HttpHeaders();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
	private url(url) {  // Detect if host was given, otherwise use default host
 | 
			
		||||
		if (/http[s]?:\/\//.test(url)) {
 | 
			
		||||
			return url;
 | 
			
		||||
		}
 | 
			
		||||
else {
 | 
			
		||||
	return this.host + url;
 | 
			
		||||
}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Generate request options
 | 
			
		||||
	private options(): {headers: HttpHeaders, observe: 'body'} {
 | 
			
		||||
		return {headers: this.authOptions(), observe: 'response' as 'body'};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Generate Basic Auth
 | 
			
		||||
	private authOptions(): HttpHeaders {
 | 
			
		||||
		const auth = this.storage.get('basicAuth');
 | 
			
		||||
		if (auth) {
 | 
			
		||||
			return new HttpHeaders({Authorization: 'Basic ' + auth});
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
			return new HttpHeaders();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,18 +3,18 @@ import {QuickScore} from 'quick-score';
 | 
			
		||||
import {of} from 'rxjs';
 | 
			
		||||
 | 
			
		||||
@Injectable({
 | 
			
		||||
  providedIn: 'root'
 | 
			
		||||
	providedIn: 'root'
 | 
			
		||||
})
 | 
			
		||||
export class AutocompleteService {
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
	constructor() { }
 | 
			
		||||
 | 
			
		||||
  bind(ref, list: string[]) {
 | 
			
		||||
    return this.search.bind(ref, list);
 | 
			
		||||
  }
 | 
			
		||||
	bind(ref, list: string[]) {
 | 
			
		||||
		return this.search.bind(ref, list);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  search(arr: string[], str: string) {
 | 
			
		||||
    const qs = new QuickScore(arr);
 | 
			
		||||
    return of(str === '' ? [] :  qs.search(str).map(e => e.item));
 | 
			
		||||
  }
 | 
			
		||||
	search(arr: string[], str: string) {
 | 
			
		||||
		const qs = new QuickScore(arr);
 | 
			
		||||
		return of(str === '' ? [] :  qs.search(str).map(e => e.item));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,77 +8,77 @@ import {ModelItemModel} from '../models/model-item.model';
 | 
			
		||||
import {ModelFileModel} from '../models/model-file.model';
 | 
			
		||||
 | 
			
		||||
@Injectable({
 | 
			
		||||
  providedIn: 'root'
 | 
			
		||||
	providedIn: 'root'
 | 
			
		||||
})
 | 
			
		||||
export class DataService {
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private api: ApiService
 | 
			
		||||
  ) { }
 | 
			
		||||
	constructor(
 | 
			
		||||
		private api: ApiService
 | 
			
		||||
	) { }
 | 
			
		||||
 | 
			
		||||
  private collectionMap = {  // List of available collections
 | 
			
		||||
    materials: {path: '/materials?status[]=validated&status[]=new', model: MaterialModel, type: 'idArray'},
 | 
			
		||||
    materialSuppliers: {path: '/material/suppliers', model: null, type: 'idArray'},
 | 
			
		||||
    materialGroups: {path: '/material/groups', model: null, type: 'idArray'},
 | 
			
		||||
    materialTemplates: {path: '/template/materials', model: TemplateModel, type: 'template'},
 | 
			
		||||
    measurementTemplates: {path: '/template/measurements', model: TemplateModel, type: 'template'},
 | 
			
		||||
    conditionTemplates: {path: '/template/conditions', model: TemplateModel, type: 'template'},
 | 
			
		||||
    sampleNotesFields: {path: '/sample/notes/fields', model: TemplateModel, type: 'idArray'},
 | 
			
		||||
    users: {path: '/users', model: UserModel, type: 'idArray'},
 | 
			
		||||
    modelGroups: {path: '/model/groups', model: ModelItemModel, type: 'array'},
 | 
			
		||||
    modelFiles: {path: '/model/files', model: ModelFileModel, type: 'array'},
 | 
			
		||||
    user: {path: '/user', model: UserModel, type: 'string'},
 | 
			
		||||
    userKey: {path: '/user/key', model: BaseModel, type: 'string'}
 | 
			
		||||
  };
 | 
			
		||||
	private collectionMap = {  // List of available collections
 | 
			
		||||
		materials: {path: '/materials?status[]=validated&status[]=new', model: MaterialModel, type: 'idArray'},
 | 
			
		||||
		materialSuppliers: {path: '/material/suppliers', model: null, type: 'idArray'},
 | 
			
		||||
		materialGroups: {path: '/material/groups', model: null, type: 'idArray'},
 | 
			
		||||
		materialTemplates: {path: '/template/materials', model: TemplateModel, type: 'template'},
 | 
			
		||||
		measurementTemplates: {path: '/template/measurements', model: TemplateModel, type: 'template'},
 | 
			
		||||
		conditionTemplates: {path: '/template/conditions', model: TemplateModel, type: 'template'},
 | 
			
		||||
		sampleNotesFields: {path: '/sample/notes/fields', model: TemplateModel, type: 'idArray'},
 | 
			
		||||
		users: {path: '/users', model: UserModel, type: 'idArray'},
 | 
			
		||||
		modelGroups: {path: '/model/groups', model: ModelItemModel, type: 'array'},
 | 
			
		||||
		modelFiles: {path: '/model/files', model: ModelFileModel, type: 'array'},
 | 
			
		||||
		user: {path: '/user', model: UserModel, type: 'string'},
 | 
			
		||||
		userKey: {path: '/user/key', model: BaseModel, type: 'string'}
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
  arr: {[key: string]: any[]} = {};  // Array of data
 | 
			
		||||
  latest: {[key: string]: any[]} = {};  // Array of latest template versions
 | 
			
		||||
  id: {[key: string]: {[id: string]: any}} = {};  // Data in format _id: data
 | 
			
		||||
  d: {[key: string]: any} = {};      // Data not in array format
 | 
			
		||||
	arr: {[key: string]: any[]} = {};  // Array of data
 | 
			
		||||
	latest: {[key: string]: any[]} = {};  // Array of latest template versions
 | 
			
		||||
	id: {[key: string]: {[id: string]: any}} = {};  // Data in format _id: data
 | 
			
		||||
	d: {[key: string]: any} = {};      // Data not in array format
 | 
			
		||||
 | 
			
		||||
  contact = {name: 'CR/APS1-Lingenfelser', mail: 'dominic.lingenfelser@bosch.com'};  // Global contact data
 | 
			
		||||
	contact = {name: 'CR/APS1-Lingenfelser', mail: 'dominic.lingenfelser@bosch.com'};  // Global contact data
 | 
			
		||||
 | 
			
		||||
  load(collection, f = () => {}) {  // Load data
 | 
			
		||||
    if (this.arr[collection]) { // Data already loaded
 | 
			
		||||
      f();
 | 
			
		||||
    }
 | 
			
		||||
    else {  // Load data
 | 
			
		||||
      this.api.get<any>(this.collectionMap[collection].path, data => {
 | 
			
		||||
        if (this.collectionMap[collection].type !== 'string') {  // Array data
 | 
			
		||||
          this.arr[collection] = data
 | 
			
		||||
            .map(
 | 
			
		||||
              e => this.collectionMap[collection].model ?
 | 
			
		||||
                new this.collectionMap[collection].model().deserialize(e) : e
 | 
			
		||||
            );
 | 
			
		||||
          // Load ids
 | 
			
		||||
          if (this.collectionMap[collection].type === 'idArray' || this.collectionMap[collection].type === 'template') {
 | 
			
		||||
            this.idReload(collection);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        else {  // Not array data
 | 
			
		||||
          this.d[collection] = new this.collectionMap[collection].model().deserialize(data);
 | 
			
		||||
        }
 | 
			
		||||
        f();
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
	load(collection, f = () => {}) {  // Load data
 | 
			
		||||
		if (this.arr[collection]) { // Data already loaded
 | 
			
		||||
			f();
 | 
			
		||||
		}
 | 
			
		||||
		else {  // Load data
 | 
			
		||||
			this.api.get<any>(this.collectionMap[collection].path, data => {
 | 
			
		||||
				if (this.collectionMap[collection].type !== 'string') {  // Array data
 | 
			
		||||
					this.arr[collection] = data
 | 
			
		||||
					.map(
 | 
			
		||||
						e => this.collectionMap[collection].model ?
 | 
			
		||||
							new this.collectionMap[collection].model().deserialize(e) : e
 | 
			
		||||
					);
 | 
			
		||||
					// Load ids
 | 
			
		||||
					if (this.collectionMap[collection].type === 'idArray' || this.collectionMap[collection].type === 'template') {
 | 
			
		||||
						this.idReload(collection);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				else {  // Not array data
 | 
			
		||||
					this.d[collection] = new this.collectionMap[collection].model().deserialize(data);
 | 
			
		||||
				}
 | 
			
		||||
				f();
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  // Generate id object
 | 
			
		||||
  idReload(collection) {
 | 
			
		||||
    this.id[collection] = this.arr[collection].reduce((s, e) => {s[e._id] = e; return s; }, {});
 | 
			
		||||
    if (this.collectionMap[collection].type === 'template') {  // Generate array with latest templates
 | 
			
		||||
      const tmpTemplates = {};
 | 
			
		||||
      this.arr[collection].forEach(template => {
 | 
			
		||||
        if (tmpTemplates[template.first_id]) {  // Already found another version
 | 
			
		||||
          if (template.version > tmpTemplates[template.first_id].version) {
 | 
			
		||||
            tmpTemplates[template.first_id] = template;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
          tmpTemplates[template.first_id] = template;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      this.latest[collection] = Object.values(tmpTemplates);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
	// Generate id object
 | 
			
		||||
	idReload(collection) {
 | 
			
		||||
		this.id[collection] = this.arr[collection].reduce((s, e) => {s[e._id] = e; return s; }, {});
 | 
			
		||||
		if (this.collectionMap[collection].type === 'template') {  // Generate array with latest templates
 | 
			
		||||
			const tmpTemplates = {};
 | 
			
		||||
			this.arr[collection].forEach(template => {
 | 
			
		||||
				if (tmpTemplates[template.first_id]) {  // Already found another version
 | 
			
		||||
					if (template.version > tmpTemplates[template.first_id].version) {
 | 
			
		||||
						tmpTemplates[template.first_id] = template;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				else {
 | 
			
		||||
					tmpTemplates[template.first_id] = template;
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
			this.latest[collection] = Object.values(tmpTemplates);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,130 +6,130 @@ import {Observable} from 'rxjs';
 | 
			
		||||
import {DataService} from './data.service';
 | 
			
		||||
 | 
			
		||||
@Injectable({
 | 
			
		||||
  providedIn: 'root'
 | 
			
		||||
	providedIn: 'root'
 | 
			
		||||
})
 | 
			
		||||
export class LoginService implements CanActivate {
 | 
			
		||||
 | 
			
		||||
  private pathPermissions = [  // Minimum level needed for the specified paths
 | 
			
		||||
    {path: 'materials', permission: 'dev'},
 | 
			
		||||
    {path: 'templates', permission: 'dev'},
 | 
			
		||||
    {path: 'changelog', permission: 'dev'},
 | 
			
		||||
    {path: 'users', permission: 'admin'}
 | 
			
		||||
  ];
 | 
			
		||||
  readonly levels = [  // All user levels in ascending permissions order
 | 
			
		||||
    'predict',
 | 
			
		||||
    'read',
 | 
			
		||||
    'write',
 | 
			
		||||
    'dev',
 | 
			
		||||
    'admin'
 | 
			
		||||
  ];
 | 
			
		||||
  // Returns true or false depending on whether the user fulfills the minimum level
 | 
			
		||||
  isLevel: {[level: string]: boolean} = {};
 | 
			
		||||
  hasPrediction = false;  // True if user has prediction models specified
 | 
			
		||||
  userId = '';
 | 
			
		||||
	private pathPermissions = [  // Minimum level needed for the specified paths
 | 
			
		||||
		{path: 'materials', permission: 'dev'},
 | 
			
		||||
	{path: 'templates', permission: 'dev'},
 | 
			
		||||
	{path: 'changelog', permission: 'dev'},
 | 
			
		||||
	{path: 'users', permission: 'admin'}
 | 
			
		||||
	];
 | 
			
		||||
	readonly levels = [  // All user levels in ascending permissions order
 | 
			
		||||
		'predict',
 | 
			
		||||
		'read',
 | 
			
		||||
		'write',
 | 
			
		||||
		'dev',
 | 
			
		||||
		'admin'
 | 
			
		||||
	];
 | 
			
		||||
	// Returns true or false depending on whether the user fulfills the minimum level
 | 
			
		||||
	isLevel: {[level: string]: boolean} = {};
 | 
			
		||||
	hasPrediction = false;  // True if user has prediction models specified
 | 
			
		||||
		userId = '';
 | 
			
		||||
 | 
			
		||||
  private loggedIn;
 | 
			
		||||
	private loggedIn;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private api: ApiService,
 | 
			
		||||
    private storage: LocalStorageService,
 | 
			
		||||
    private router: Router,
 | 
			
		||||
    private d: DataService
 | 
			
		||||
  ) {
 | 
			
		||||
	constructor(
 | 
			
		||||
		private api: ApiService,
 | 
			
		||||
		private storage: LocalStorageService,
 | 
			
		||||
		private router: Router,
 | 
			
		||||
		private d: DataService
 | 
			
		||||
	) {
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  login(username = '', password = '') {
 | 
			
		||||
    return new Promise(resolve => {
 | 
			
		||||
      if (username !== '' || password !== '') {  // Some credentials given
 | 
			
		||||
        let credentials: string[];
 | 
			
		||||
        const credentialString: string = this.storage.get('basicAuth');
 | 
			
		||||
        if (credentialString) {  // Found stored credentials
 | 
			
		||||
          credentials = atob(credentialString).split(':');
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
          credentials = ['', ''];
 | 
			
		||||
        }
 | 
			
		||||
        if (username !== '' && password !== '') {  // All credentials given
 | 
			
		||||
          this.storage.set('basicAuth', btoa(username + ':' + password));
 | 
			
		||||
        }
 | 
			
		||||
        else if (username !== '') {  // Username given
 | 
			
		||||
          this.storage.set('basicAuth', btoa(username + ':' + credentials[1]));
 | 
			
		||||
        }
 | 
			
		||||
        else if (password !== '') {  // Password given
 | 
			
		||||
          this.storage.set('basicAuth', btoa(credentials[0] + ':' + password));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      this.api.get('/authorized', (data: any, error) => {
 | 
			
		||||
        if (!error) {
 | 
			
		||||
          if (data.status === 'Authorization successful') {
 | 
			
		||||
            this.loggedIn = true;
 | 
			
		||||
            this.levels.forEach(level => {
 | 
			
		||||
              this.isLevel[level] = this.levels.indexOf(data.level) >= this.levels.indexOf(level);
 | 
			
		||||
              if (this.isLevel.dev) {  // Set hasPrediction
 | 
			
		||||
                this.hasPrediction = true;
 | 
			
		||||
              }
 | 
			
		||||
              else {
 | 
			
		||||
                this.d.load('modelGroups', () => {
 | 
			
		||||
                  this.hasPrediction = this.d.arr.modelGroups.length > 0;
 | 
			
		||||
                });
 | 
			
		||||
              }
 | 
			
		||||
            });
 | 
			
		||||
            this.userId = data.user_id;
 | 
			
		||||
            resolve(true);
 | 
			
		||||
          } else {
 | 
			
		||||
            this.loggedIn = false;
 | 
			
		||||
            this.storage.remove('basicAuth');
 | 
			
		||||
            resolve(false);
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          this.loggedIn = false;
 | 
			
		||||
          this.storage.remove('basicAuth');
 | 
			
		||||
          resolve(false);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
	login(username = '', password = '') {
 | 
			
		||||
		return new Promise(resolve => {
 | 
			
		||||
			if (username !== '' || password !== '') {  // Some credentials given
 | 
			
		||||
				let credentials: string[];
 | 
			
		||||
				const credentialString: string = this.storage.get('basicAuth');
 | 
			
		||||
				if (credentialString) {  // Found stored credentials
 | 
			
		||||
					credentials = atob(credentialString).split(':');
 | 
			
		||||
				}
 | 
			
		||||
				else {
 | 
			
		||||
					credentials = ['', ''];
 | 
			
		||||
				}
 | 
			
		||||
				if (username !== '' && password !== '') {  // All credentials given
 | 
			
		||||
					this.storage.set('basicAuth', btoa(username + ':' + password));
 | 
			
		||||
				}
 | 
			
		||||
				else if (username !== '') {  // Username given
 | 
			
		||||
					this.storage.set('basicAuth', btoa(username + ':' + credentials[1]));
 | 
			
		||||
				}
 | 
			
		||||
				else if (password !== '') {  // Password given
 | 
			
		||||
					this.storage.set('basicAuth', btoa(credentials[0] + ':' + password));
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			this.api.get('/authorized', (data: any, error) => {
 | 
			
		||||
				if (!error) {
 | 
			
		||||
					if (data.status === 'Authorization successful') {
 | 
			
		||||
						this.loggedIn = true;
 | 
			
		||||
						this.levels.forEach(level => {
 | 
			
		||||
							this.isLevel[level] = this.levels.indexOf(data.level) >= this.levels.indexOf(level);
 | 
			
		||||
							if (this.isLevel.dev) {  // Set hasPrediction
 | 
			
		||||
								this.hasPrediction = true;
 | 
			
		||||
							}
 | 
			
		||||
							else {
 | 
			
		||||
								this.d.load('modelGroups', () => {
 | 
			
		||||
									this.hasPrediction = this.d.arr.modelGroups.length > 0;
 | 
			
		||||
								});
 | 
			
		||||
							}
 | 
			
		||||
						});
 | 
			
		||||
						this.userId = data.user_id;
 | 
			
		||||
						resolve(true);
 | 
			
		||||
					} else {
 | 
			
		||||
						this.loggedIn = false;
 | 
			
		||||
						this.storage.remove('basicAuth');
 | 
			
		||||
						resolve(false);
 | 
			
		||||
					}
 | 
			
		||||
				} else {
 | 
			
		||||
					this.loggedIn = false;
 | 
			
		||||
					this.storage.remove('basicAuth');
 | 
			
		||||
					resolve(false);
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  logout() {
 | 
			
		||||
    this.storage.remove('basicAuth');
 | 
			
		||||
    this.loggedIn = false;
 | 
			
		||||
    this.levels.forEach(level => {
 | 
			
		||||
      this.isLevel[level] = false;
 | 
			
		||||
    });
 | 
			
		||||
    this.hasPrediction = false;
 | 
			
		||||
  }
 | 
			
		||||
	logout() {
 | 
			
		||||
		this.storage.remove('basicAuth');
 | 
			
		||||
		this.loggedIn = false;
 | 
			
		||||
		this.levels.forEach(level => {
 | 
			
		||||
			this.isLevel[level] = false;
 | 
			
		||||
		});
 | 
			
		||||
		this.hasPrediction = false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  // CanActivate for Angular routing
 | 
			
		||||
  canActivate(route: ActivatedRouteSnapshot = null, state: RouterStateSnapshot = null): Observable<boolean> {
 | 
			
		||||
    return new Observable<boolean>(observer => {
 | 
			
		||||
      new Promise(resolve => {
 | 
			
		||||
        if (this.loggedIn === undefined) {
 | 
			
		||||
          this.login().then(res => {
 | 
			
		||||
            resolve(res);
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
          resolve(this.loggedIn);
 | 
			
		||||
        }
 | 
			
		||||
      }).then(res => {
 | 
			
		||||
        const pathPermission = this.pathPermissions.find(e => e.path.indexOf(route.url[0].path) >= 0);
 | 
			
		||||
        // Check if level is permitted for path
 | 
			
		||||
        const ok = res && (!pathPermission || this.isLevel[pathPermission.permission]);
 | 
			
		||||
        observer.next(ok);
 | 
			
		||||
        observer.complete();
 | 
			
		||||
        if (!ok) {
 | 
			
		||||
          this.router.navigate(['/']);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
	// CanActivate for Angular routing
 | 
			
		||||
	canActivate(route: ActivatedRouteSnapshot = null, state: RouterStateSnapshot = null): Observable<boolean> {
 | 
			
		||||
		return new Observable<boolean>(observer => {
 | 
			
		||||
			new Promise(resolve => {
 | 
			
		||||
				if (this.loggedIn === undefined) {
 | 
			
		||||
					this.login().then(res => {
 | 
			
		||||
						resolve(res);
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
				else {
 | 
			
		||||
					resolve(this.loggedIn);
 | 
			
		||||
				}
 | 
			
		||||
			}).then(res => {
 | 
			
		||||
				const pathPermission = this.pathPermissions.find(e => e.path.indexOf(route.url[0].path) >= 0);
 | 
			
		||||
				// Check if level is permitted for path
 | 
			
		||||
				const ok = res && (!pathPermission || this.isLevel[pathPermission.permission]);
 | 
			
		||||
			observer.next(ok);
 | 
			
		||||
			observer.complete();
 | 
			
		||||
			if (!ok) {
 | 
			
		||||
				this.router.navigate(['/']);
 | 
			
		||||
			}
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  get isLoggedIn() {
 | 
			
		||||
    return this.loggedIn;
 | 
			
		||||
  }
 | 
			
		||||
	get isLoggedIn() {
 | 
			
		||||
		return this.loggedIn;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  get username() {
 | 
			
		||||
    return atob(this.storage.get('basicAuth')).split(':')[0];
 | 
			
		||||
  }
 | 
			
		||||
	get username() {
 | 
			
		||||
		return atob(this.storage.get('basicAuth')).split(':')[0];
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,183 +3,183 @@ import Joi from '@hapi/joi';
 | 
			
		||||
import {AbstractControl} from '@angular/forms';
 | 
			
		||||
 | 
			
		||||
@Injectable({
 | 
			
		||||
  providedIn: 'root'
 | 
			
		||||
	providedIn: 'root'
 | 
			
		||||
})
 | 
			
		||||
export class ValidationService {
 | 
			
		||||
 | 
			
		||||
  private vUsername = Joi.string()
 | 
			
		||||
    .lowercase()
 | 
			
		||||
    .pattern(new RegExp('^[a-z0-9-_.]+$'))
 | 
			
		||||
    .min(1)
 | 
			
		||||
    .max(128);
 | 
			
		||||
	private vUsername = Joi.string()
 | 
			
		||||
	.lowercase()
 | 
			
		||||
	.pattern(new RegExp('^[a-z0-9-_.]+$'))
 | 
			
		||||
	.min(1)
 | 
			
		||||
	.max(128);
 | 
			
		||||
 | 
			
		||||
  private vPassword = Joi.string()
 | 
			
		||||
    .min(8)
 | 
			
		||||
    .max(128);
 | 
			
		||||
	private vPassword = Joi.string()
 | 
			
		||||
	.min(8)
 | 
			
		||||
	.max(128);
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
	constructor() { }
 | 
			
		||||
 | 
			
		||||
  generate(method, args) {  // Generate a Validator function
 | 
			
		||||
    return (control: AbstractControl): {[key: string]: any} | null => {
 | 
			
		||||
      let ok;
 | 
			
		||||
      let error;
 | 
			
		||||
      if (args) {
 | 
			
		||||
        ({ok, error} = this[method](control.value, ...args));
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        ({ok, error} = this[method](control.value));
 | 
			
		||||
      }
 | 
			
		||||
      return ok ? null : { failure: error };
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
	generate(method, args) {  // Generate a Validator function
 | 
			
		||||
		return (control: AbstractControl): {[key: string]: any} | null => {
 | 
			
		||||
			let ok;
 | 
			
		||||
			let error;
 | 
			
		||||
			if (args) {
 | 
			
		||||
				({ok, error} = this[method](control.value, ...args));
 | 
			
		||||
			}
 | 
			
		||||
			else {
 | 
			
		||||
				({ok, error} = this[method](control.value));
 | 
			
		||||
			}
 | 
			
		||||
			return ok ? null : { failure: error };
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  username(data) {
 | 
			
		||||
    const {ignore, error} = this.vUsername.validate(data);
 | 
			
		||||
    if (error) {
 | 
			
		||||
      return {ok: false, error: 'username must only contain a-z0-9-_.'};
 | 
			
		||||
    }
 | 
			
		||||
    return {ok: true, error: ''};
 | 
			
		||||
  }
 | 
			
		||||
	username(data) {
 | 
			
		||||
		const {ignore, error} = this.vUsername.validate(data);
 | 
			
		||||
		if (error) {
 | 
			
		||||
			return {ok: false, error: 'username must only contain a-z0-9-_.'};
 | 
			
		||||
		}
 | 
			
		||||
		return {ok: true, error: ''};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  password(data) {
 | 
			
		||||
    const {ignore, error} = this.vPassword.validate(data);
 | 
			
		||||
    if (error) {
 | 
			
		||||
      return {ok: false, error: 'password must have at least 8 characters'};
 | 
			
		||||
    }
 | 
			
		||||
    return {ok: true, error: ''};
 | 
			
		||||
  }
 | 
			
		||||
	password(data) {
 | 
			
		||||
		const {ignore, error} = this.vPassword.validate(data);
 | 
			
		||||
		if (error) {
 | 
			
		||||
			return {ok: false, error: 'password must have at least 8 characters'};
 | 
			
		||||
		}
 | 
			
		||||
		return {ok: true, error: ''};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  string(data, option = null) {
 | 
			
		||||
    let validator = Joi.string().max(128).allow('', true, false);
 | 
			
		||||
    let errorMsg = 'must contain max 128 characters';
 | 
			
		||||
    if (option === 'alphanum') {
 | 
			
		||||
      validator = validator.alphanum();
 | 
			
		||||
      errorMsg = 'must contain max 128 alphanumerical characters';
 | 
			
		||||
    }
 | 
			
		||||
    const {ignore, error} = validator.validate(data);
 | 
			
		||||
    if (error) {
 | 
			
		||||
      return {ok: false, error: errorMsg};
 | 
			
		||||
    }
 | 
			
		||||
    return {ok: true, error: ''};
 | 
			
		||||
  }
 | 
			
		||||
	string(data, option = null) {
 | 
			
		||||
		let validator = Joi.string().max(128).allow('', true, false);
 | 
			
		||||
		let errorMsg = 'must contain max 128 characters';
 | 
			
		||||
		if (option === 'alphanum') {
 | 
			
		||||
			validator = validator.alphanum();
 | 
			
		||||
			errorMsg = 'must contain max 128 alphanumerical characters';
 | 
			
		||||
		}
 | 
			
		||||
		const {ignore, error} = validator.validate(data);
 | 
			
		||||
		if (error) {
 | 
			
		||||
			return {ok: false, error: errorMsg};
 | 
			
		||||
		}
 | 
			
		||||
		return {ok: true, error: ''};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  stringOf(data, list) {  // String must be in list
 | 
			
		||||
    const {ignore, error} = Joi.string().allow('').valid(...list.map(e => e.toString())).validate(data);
 | 
			
		||||
    if (error) {
 | 
			
		||||
      return {ok: false, error: 'must be one of ' + list.join(', ')};
 | 
			
		||||
    }
 | 
			
		||||
    return {ok: true, error: ''};
 | 
			
		||||
  }
 | 
			
		||||
	stringOf(data, list) {  // String must be in list
 | 
			
		||||
		const {ignore, error} = Joi.string().allow('').valid(...list.map(e => e.toString())).validate(data);
 | 
			
		||||
		if (error) {
 | 
			
		||||
			return {ok: false, error: 'must be one of ' + list.join(', ')};
 | 
			
		||||
		}
 | 
			
		||||
		return {ok: true, error: ''};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  stringNin(data, list) {  // String must not be in list
 | 
			
		||||
    const {ignore, error} = Joi.string().invalid(...list).validate(data);
 | 
			
		||||
    if (error) {
 | 
			
		||||
      return {ok: false, error: 'value not allowed'};
 | 
			
		||||
    }
 | 
			
		||||
    return {ok: true, error: ''};
 | 
			
		||||
  }
 | 
			
		||||
	stringNin(data, list) {  // String must not be in list
 | 
			
		||||
		const {ignore, error} = Joi.string().invalid(...list).validate(data);
 | 
			
		||||
		if (error) {
 | 
			
		||||
			return {ok: false, error: 'value not allowed'};
 | 
			
		||||
		}
 | 
			
		||||
		return {ok: true, error: ''};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  stringLength(data, length) {  // String with maximum length
 | 
			
		||||
    const {ignore, error} = Joi.string().max(length).allow('').validate(data);
 | 
			
		||||
    if (error) {
 | 
			
		||||
      return {ok: false, error: 'must contain max ' + length + ' characters'};
 | 
			
		||||
    }
 | 
			
		||||
    return {ok: true, error: ''};
 | 
			
		||||
  }
 | 
			
		||||
	stringLength(data, length) {  // String with maximum length
 | 
			
		||||
		const {ignore, error} = Joi.string().max(length).allow('').validate(data);
 | 
			
		||||
		if (error) {
 | 
			
		||||
			return {ok: false, error: 'must contain max ' + length + ' characters'};
 | 
			
		||||
		}
 | 
			
		||||
		return {ok: true, error: ''};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  minMax(data, min, max) {  // Number between min and max
 | 
			
		||||
    const {ignore, error} = Joi.number().allow('').min(min).max(max).validate(data);
 | 
			
		||||
    if (error) {
 | 
			
		||||
      return {ok: false, error: `must be between ${min} and ${max}`};
 | 
			
		||||
    }
 | 
			
		||||
    return {ok: true, error: ''};
 | 
			
		||||
  }
 | 
			
		||||
	minMax(data, min, max) {  // Number between min and max
 | 
			
		||||
		const {ignore, error} = Joi.number().allow('').min(min).max(max).validate(data);
 | 
			
		||||
		if (error) {
 | 
			
		||||
			return {ok: false, error: `must be between ${min} and ${max}`};
 | 
			
		||||
		}
 | 
			
		||||
		return {ok: true, error: ''};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  min(data, min) {  // Number above min
 | 
			
		||||
    const {ignore, error} = Joi.number().allow('').min(min).validate(data);
 | 
			
		||||
    if (error) {
 | 
			
		||||
      return {ok: false, error: `must not be below ${min}`};
 | 
			
		||||
    }
 | 
			
		||||
    return {ok: true, error: ''};
 | 
			
		||||
  }
 | 
			
		||||
	min(data, min) {  // Number above min
 | 
			
		||||
		const {ignore, error} = Joi.number().allow('').min(min).validate(data);
 | 
			
		||||
		if (error) {
 | 
			
		||||
			return {ok: false, error: `must not be below ${min}`};
 | 
			
		||||
		}
 | 
			
		||||
		return {ok: true, error: ''};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  max(data, max) {  // Number below max
 | 
			
		||||
    const {ignore, error} = Joi.number().allow('').max(max).validate(data);
 | 
			
		||||
    if (error) {
 | 
			
		||||
      return {ok: false, error: `must not be above ${max}`};
 | 
			
		||||
    }
 | 
			
		||||
    return {ok: true, error: ''};
 | 
			
		||||
  }
 | 
			
		||||
	max(data, max) {  // Number below max
 | 
			
		||||
		const {ignore, error} = Joi.number().allow('').max(max).validate(data);
 | 
			
		||||
		if (error) {
 | 
			
		||||
			return {ok: false, error: `must not be above ${max}`};
 | 
			
		||||
		}
 | 
			
		||||
		return {ok: true, error: ''};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  url(data) {
 | 
			
		||||
    const {ignore, error} = Joi.string().uri().validate(data);
 | 
			
		||||
    if (error) {
 | 
			
		||||
      return {ok: false, error: `must be a valid URL`};
 | 
			
		||||
    }
 | 
			
		||||
    return {ok: true, error: ''};
 | 
			
		||||
  }
 | 
			
		||||
	url(data) {
 | 
			
		||||
		const {ignore, error} = Joi.string().uri().validate(data);
 | 
			
		||||
		if (error) {
 | 
			
		||||
			return {ok: false, error: `must be a valid URL`};
 | 
			
		||||
		}
 | 
			
		||||
		return {ok: true, error: ''};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  unique(data, list) {
 | 
			
		||||
    const {ignore, error} = Joi.string().allow('').invalid(...list.map(e => e.toString())).validate(data);
 | 
			
		||||
    if (error) {
 | 
			
		||||
      return {ok: false, error: `values must be unique`};
 | 
			
		||||
    }
 | 
			
		||||
    return {ok: true, error: ''};
 | 
			
		||||
  }
 | 
			
		||||
	unique(data, list) {
 | 
			
		||||
		const {ignore, error} = Joi.string().allow('').invalid(...list.map(e => e.toString())).validate(data);
 | 
			
		||||
		if (error) {
 | 
			
		||||
			return {ok: false, error: `values must be unique`};
 | 
			
		||||
		}
 | 
			
		||||
		return {ok: true, error: ''};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  equal(data, compare) {
 | 
			
		||||
    if (data !== compare) {
 | 
			
		||||
      return {ok: false, error: `must be equal`};
 | 
			
		||||
    }
 | 
			
		||||
    return {ok: true, error: ''};
 | 
			
		||||
  }
 | 
			
		||||
	equal(data, compare) {
 | 
			
		||||
		if (data !== compare) {
 | 
			
		||||
			return {ok: false, error: `must be equal`};
 | 
			
		||||
		}
 | 
			
		||||
		return {ok: true, error: ''};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  parameterName(data) {
 | 
			
		||||
    const {ignore, error} = Joi.string()
 | 
			
		||||
      .max(128)
 | 
			
		||||
      .invalid('condition_template', 'material_template')
 | 
			
		||||
      .allow('')
 | 
			
		||||
      .pattern(/^[^.]+$/)
 | 
			
		||||
      .required()
 | 
			
		||||
      .messages({'string.pattern.base': 'name must not contain a dot'})
 | 
			
		||||
      .validate(data);
 | 
			
		||||
    if (error) {
 | 
			
		||||
      return {ok: false, error: error.details[0].message};
 | 
			
		||||
    }
 | 
			
		||||
    return {ok: true, error: ''};
 | 
			
		||||
  }
 | 
			
		||||
	parameterName(data) {
 | 
			
		||||
		const {ignore, error} = Joi.string()
 | 
			
		||||
		.max(128)
 | 
			
		||||
		.invalid('condition_template', 'material_template')
 | 
			
		||||
		.allow('')
 | 
			
		||||
		.pattern(/^[^.]+$/)
 | 
			
		||||
		.required()
 | 
			
		||||
		.messages({'string.pattern.base': 'name must not contain a dot'})
 | 
			
		||||
		.validate(data);
 | 
			
		||||
		if (error) {
 | 
			
		||||
			return {ok: false, error: error.details[0].message};
 | 
			
		||||
		}
 | 
			
		||||
		return {ok: true, error: ''};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  parameterRange(data) {
 | 
			
		||||
    if (data) {
 | 
			
		||||
      try {
 | 
			
		||||
        const {ignore, error} = Joi.object({
 | 
			
		||||
          values: Joi.array()
 | 
			
		||||
            .min(1),
 | 
			
		||||
	parameterRange(data) {
 | 
			
		||||
		if (data) {
 | 
			
		||||
			try {
 | 
			
		||||
				const {ignore, error} = Joi.object({
 | 
			
		||||
					values: Joi.array()
 | 
			
		||||
					.min(1),
 | 
			
		||||
 | 
			
		||||
          min: Joi.number(),
 | 
			
		||||
					min: Joi.number(),
 | 
			
		||||
 | 
			
		||||
          max: Joi.number(),
 | 
			
		||||
					max: Joi.number(),
 | 
			
		||||
 | 
			
		||||
          type: Joi.string()
 | 
			
		||||
            .valid('string', 'number', 'boolean', 'array'),
 | 
			
		||||
					type: Joi.string()
 | 
			
		||||
					.valid('string', 'number', 'boolean', 'array'),
 | 
			
		||||
 | 
			
		||||
          required: Joi.boolean()
 | 
			
		||||
        })
 | 
			
		||||
          .oxor('values', 'min')
 | 
			
		||||
          .oxor('values', 'max')
 | 
			
		||||
          .required()
 | 
			
		||||
          .validate(JSON.parse(data));
 | 
			
		||||
        if (error) {
 | 
			
		||||
          return {ok: false, error: error.details[0].message};
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      catch (e) {
 | 
			
		||||
        return {ok: false, error: `no valid JSON`};
 | 
			
		||||
      }
 | 
			
		||||
      return {ok: true, error: ''};
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      return {ok: false, error: `no valid value`};
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
					required: Joi.boolean()
 | 
			
		||||
				})
 | 
			
		||||
				.oxor('values', 'min')
 | 
			
		||||
				.oxor('values', 'max')
 | 
			
		||||
				.required()
 | 
			
		||||
				.validate(JSON.parse(data));
 | 
			
		||||
				if (error) {
 | 
			
		||||
					return {ok: false, error: error.details[0].message};
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			catch (e) {
 | 
			
		||||
				return {ok: false, error: `no valid JSON`};
 | 
			
		||||
			}
 | 
			
		||||
			return {ok: true, error: ''};
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
			return {ok: false, error: `no valid value`};
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,46 +1,46 @@
 | 
			
		||||
<form #userForm="ngForm">
 | 
			
		||||
  <rb-form-input name="name" label="user name" appValidate="username" required [(ngModel)]="user.name"
 | 
			
		||||
                 #nameInput="ngModel">
 | 
			
		||||
    <ng-template rbFormValidationMessage="failure">{{nameInput.errors.failure}}</ng-template>
 | 
			
		||||
    <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
  </rb-form-input>
 | 
			
		||||
  <rb-form-input name="email" label="email" email required [(ngModel)]="user.email" ngModel>
 | 
			
		||||
    <ng-template rbFormValidationMessage="email">Invalid email</ng-template>
 | 
			
		||||
    <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
  </rb-form-input>
 | 
			
		||||
  <rb-form-input name="location" label="location" appValidate="string" required [appValidateArgs]="['alphanum']"
 | 
			
		||||
                 [(ngModel)]="user.location" #locationInput="ngModel">
 | 
			
		||||
    <ng-template rbFormValidationMessage="failure">{{locationInput.errors.failure}}</ng-template>
 | 
			
		||||
    <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
  </rb-form-input>
 | 
			
		||||
  <rb-array-input [(ngModel)]="user.devices" name="devices" [pushTemplate]="''">
 | 
			
		||||
    <rb-form-input *rbArrayInputItem="let item" rbArrayInputListener="devices" [index]="item.i"
 | 
			
		||||
                   label="device" appValidate="string" [name]="'device-' + item.i" [ngModel]="item.value"
 | 
			
		||||
                   #deviceInput="ngModel">
 | 
			
		||||
      <ng-template rbFormValidationMessage="failure">{{deviceInput.errors.failure}}</ng-template>
 | 
			
		||||
    </rb-form-input>
 | 
			
		||||
  </rb-array-input>
 | 
			
		||||
  <rb-icon-button icon="save" mode="primary" type="submit" [disabled]="!userForm.form.valid" (click)="saveUser()">
 | 
			
		||||
    Save change
 | 
			
		||||
  </rb-icon-button>
 | 
			
		||||
  <span class="message">{{messageUser}}</span>
 | 
			
		||||
	<rb-form-input name="name" label="user name" appValidate="username" required [(ngModel)]="user.name"
 | 
			
		||||
		#nameInput="ngModel">
 | 
			
		||||
		<ng-template rbFormValidationMessage="failure">{{nameInput.errors.failure}}</ng-template>
 | 
			
		||||
		<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
	</rb-form-input>
 | 
			
		||||
	<rb-form-input name="email" label="email" email required [(ngModel)]="user.email" ngModel>
 | 
			
		||||
		<ng-template rbFormValidationMessage="email">Invalid email</ng-template>
 | 
			
		||||
		<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
	</rb-form-input>
 | 
			
		||||
	<rb-form-input name="location" label="location" appValidate="string" required [appValidateArgs]="['alphanum']"
 | 
			
		||||
		[(ngModel)]="user.location" #locationInput="ngModel">
 | 
			
		||||
		<ng-template rbFormValidationMessage="failure">{{locationInput.errors.failure}}</ng-template>
 | 
			
		||||
		<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
	</rb-form-input>
 | 
			
		||||
	<rb-array-input [(ngModel)]="user.devices" name="devices" [pushTemplate]="''">
 | 
			
		||||
		<rb-form-input *rbArrayInputItem="let item" rbArrayInputListener="devices" [index]="item.i"
 | 
			
		||||
			label="device" appValidate="string" [name]="'device-' + item.i" [ngModel]="item.value"
 | 
			
		||||
			#deviceInput="ngModel">
 | 
			
		||||
			<ng-template rbFormValidationMessage="failure">{{deviceInput.errors.failure}}</ng-template>
 | 
			
		||||
		</rb-form-input>
 | 
			
		||||
	</rb-array-input>
 | 
			
		||||
	<rb-icon-button icon="save" mode="primary" type="submit" [disabled]="!userForm.form.valid" (click)="saveUser()">
 | 
			
		||||
		Save change
 | 
			
		||||
	</rb-icon-button>
 | 
			
		||||
	<span class="message">{{messageUser}}</span>
 | 
			
		||||
</form>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<h4 class="pass-heading">Change password</h4>
 | 
			
		||||
 | 
			
		||||
<form #passForm="ngForm">
 | 
			
		||||
  <rb-form-input name="passA" type="password" label="new password" appValidate="password" required
 | 
			
		||||
                 [(ngModel)]="password" #passAInput="ngModel">
 | 
			
		||||
    <ng-template rbFormValidationMessage="failure">{{passAInput.errors.failure}}</ng-template>
 | 
			
		||||
  </rb-form-input>
 | 
			
		||||
  <rb-form-input name="passB" type="password" label="confirm password" appValidate="equal"
 | 
			
		||||
                 [appValidateArgs]="[password]" required #passBInput="ngModel" ngModel>
 | 
			
		||||
    <ng-template rbFormValidationMessage="failure">{{passBInput.errors.failure}}</ng-template>
 | 
			
		||||
  </rb-form-input>
 | 
			
		||||
  <button class="rb-btn rb-primary" type="submit" [disabled]="!passForm.form.valid" (click)="savePass()">
 | 
			
		||||
    Change password
 | 
			
		||||
  </button>
 | 
			
		||||
  <span class="message">{{messagePass}}</span>
 | 
			
		||||
	<rb-form-input name="passA" type="password" label="new password" appValidate="password" required
 | 
			
		||||
		[(ngModel)]="password" #passAInput="ngModel">
 | 
			
		||||
		<ng-template rbFormValidationMessage="failure">{{passAInput.errors.failure}}</ng-template>
 | 
			
		||||
	</rb-form-input>
 | 
			
		||||
	<rb-form-input name="passB" type="password" label="confirm password" appValidate="equal"
 | 
			
		||||
		[appValidateArgs]="[password]" required #passBInput="ngModel" ngModel>
 | 
			
		||||
		<ng-template rbFormValidationMessage="failure">{{passBInput.errors.failure}}</ng-template>
 | 
			
		||||
	</rb-form-input>
 | 
			
		||||
	<button class="rb-btn rb-primary" type="submit" [disabled]="!passForm.form.valid" (click)="savePass()">
 | 
			
		||||
		Change password
 | 
			
		||||
	</button>
 | 
			
		||||
	<span class="message">{{messagePass}}</span>
 | 
			
		||||
</form>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
.pass-heading {
 | 
			
		||||
  margin-top: 40px;
 | 
			
		||||
	margin-top: 40px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.message {
 | 
			
		||||
  margin-left: 20px;
 | 
			
		||||
	margin-left: 20px;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,63 +6,63 @@ import {LoginService} from '../services/login.service';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-settings',
 | 
			
		||||
  templateUrl: './settings.component.html',
 | 
			
		||||
  styleUrls: ['./settings.component.scss']
 | 
			
		||||
	selector: 'app-settings',
 | 
			
		||||
	templateUrl: './settings.component.html',
 | 
			
		||||
	styleUrls: ['./settings.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class SettingsComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  user: UserModel = new UserModel();  // User to edit
 | 
			
		||||
  password = '';                      // New password
 | 
			
		||||
  messageUser = '';                   // Messages for user and pass part
 | 
			
		||||
  messagePass = '';
 | 
			
		||||
	user: UserModel = new UserModel();  // User to edit
 | 
			
		||||
	password = '';                      // New password
 | 
			
		||||
	messageUser = '';                   // Messages for user and pass part
 | 
			
		||||
		messagePass = '';
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private api: ApiService,
 | 
			
		||||
    private login: LoginService,
 | 
			
		||||
    private router: Router
 | 
			
		||||
  ) { }
 | 
			
		||||
	constructor(
 | 
			
		||||
		private api: ApiService,
 | 
			
		||||
		private login: LoginService,
 | 
			
		||||
		private router: Router
 | 
			
		||||
	) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.api.get<UserModel>('/user', data => {
 | 
			
		||||
      this.user.deserialize(data);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
	ngOnInit(): void {
 | 
			
		||||
		this.api.get<UserModel>('/user', data => {
 | 
			
		||||
			this.user.deserialize(data);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  saveUser() {
 | 
			
		||||
    this.api.put<UserModel>('/user', this.user.sendFormat(), (data, err) => {
 | 
			
		||||
      if (err) {
 | 
			
		||||
        this.messageUser = err.error.status;
 | 
			
		||||
      }
 | 
			
		||||
      else {  // Login with new credentials
 | 
			
		||||
        this.login.login(data.name).then(res => {
 | 
			
		||||
          if (res) {
 | 
			
		||||
            this.router.navigate(['/samples']);
 | 
			
		||||
          }
 | 
			
		||||
          else {
 | 
			
		||||
            this.messageUser = 'request not successful, try again';
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
	saveUser() {
 | 
			
		||||
		this.api.put<UserModel>('/user', this.user.sendFormat(), (data, err) => {
 | 
			
		||||
			if (err) {
 | 
			
		||||
				this.messageUser = err.error.status;
 | 
			
		||||
			}
 | 
			
		||||
			else {  // Login with new credentials
 | 
			
		||||
				this.login.login(data.name).then(res => {
 | 
			
		||||
					if (res) {
 | 
			
		||||
						this.router.navigate(['/samples']);
 | 
			
		||||
					}
 | 
			
		||||
					else {
 | 
			
		||||
						this.messageUser = 'request not successful, try again';
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  savePass() {
 | 
			
		||||
    this.api.put<UserModel>('/user', {pass: this.password}, (ignore, err) => {
 | 
			
		||||
      if (err) {
 | 
			
		||||
        this.messagePass = err.error.status;
 | 
			
		||||
      }
 | 
			
		||||
      else {  // Login with new credentials
 | 
			
		||||
        this.login.login('', this.password).then(res => {
 | 
			
		||||
          if (res) {
 | 
			
		||||
            this.router.navigate(['/samples']);
 | 
			
		||||
          }
 | 
			
		||||
          else {
 | 
			
		||||
            this.messagePass = 'request not successful, try again';
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
	savePass() {
 | 
			
		||||
		this.api.put<UserModel>('/user', {pass: this.password}, (ignore, err) => {
 | 
			
		||||
			if (err) {
 | 
			
		||||
				this.messagePass = err.error.status;
 | 
			
		||||
			}
 | 
			
		||||
			else {  // Login with new credentials
 | 
			
		||||
				this.login.login('', this.password).then(res => {
 | 
			
		||||
					if (res) {
 | 
			
		||||
						this.router.navigate(['/samples']);
 | 
			
		||||
					}
 | 
			
		||||
					else {
 | 
			
		||||
						this.messagePass = 'request not successful, try again';
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
import { SizePipe } from './size.pipe';
 | 
			
		||||
 | 
			
		||||
describe('SizePipe', () => {
 | 
			
		||||
  it('create an instance', () => {
 | 
			
		||||
    const pipe = new SizePipe();
 | 
			
		||||
    expect(pipe).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
	it('create an instance', () => {
 | 
			
		||||
		const pipe = new SizePipe();
 | 
			
		||||
		expect(pipe).toBeTruthy();
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,16 @@
 | 
			
		||||
import { Pipe, PipeTransform } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
@Pipe({
 | 
			
		||||
  name: 'size'
 | 
			
		||||
	name: 'size'
 | 
			
		||||
})
 | 
			
		||||
export class SizePipe implements PipeTransform {
 | 
			
		||||
 | 
			
		||||
  transform(value: number, exp: string): string {
 | 
			
		||||
    const divide = ['', 'k', 'M', 'G', 'T'].indexOf(exp);
 | 
			
		||||
    for (let i = 0; i < divide; i ++) {
 | 
			
		||||
      value = value / 1024;
 | 
			
		||||
    }
 | 
			
		||||
    return `${value.toFixed(2)} ${exp}B`;
 | 
			
		||||
  }
 | 
			
		||||
	transform(value: number, exp: string): string {
 | 
			
		||||
		const divide = ['', 'k', 'M', 'G', 'T'].indexOf(exp);
 | 
			
		||||
		for (let i = 0; i < divide; i ++) {
 | 
			
		||||
			value = value / 1024;
 | 
			
		||||
		}
 | 
			
		||||
		return `${value.toFixed(2)} ${exp}B`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,66 +1,66 @@
 | 
			
		||||
<rb-form-select name="collectionSelection" label="collection"
 | 
			
		||||
                [(ngModel)]="collection" (ngModelChange)="loadTemplates()">
 | 
			
		||||
  <option value="material">Materials</option>
 | 
			
		||||
  <option value="measurement">Measurements</option>
 | 
			
		||||
  <option value="condition">Conditions</option>
 | 
			
		||||
	[(ngModel)]="collection" (ngModelChange)="loadTemplates()">
 | 
			
		||||
	<option value="material">Materials</option>
 | 
			
		||||
	<option value="measurement">Measurements</option>
 | 
			
		||||
	<option value="condition">Conditions</option>
 | 
			
		||||
</rb-form-select>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<rb-icon-button icon="add" mode="primary" (click)="newTemplate()">New template</rb-icon-button>
 | 
			
		||||
 | 
			
		||||
<div class="list">
 | 
			
		||||
  <div class="row">
 | 
			
		||||
    <div class="header">Name</div>
 | 
			
		||||
    <div class="header">Version</div>
 | 
			
		||||
  </div>
 | 
			
		||||
	<div class="row">
 | 
			
		||||
		<div class="header">Name</div>
 | 
			
		||||
		<div class="header">Version</div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
  <ng-container *ngFor="let group of groupsView">
 | 
			
		||||
    <div class="row clickable">
 | 
			
		||||
      <div (click)="group.expanded = !group.expanded">{{group.name}}</div>
 | 
			
		||||
      <div (click)="group.expanded = !group.expanded">{{group.version}}</div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="row" *ngIf="group.expanded" [@inOut]>
 | 
			
		||||
      <div class="details">
 | 
			
		||||
        <ng-container *ngFor="let template of group.entries">
 | 
			
		||||
          <div>{{template.name}}</div>
 | 
			
		||||
          <div>{{template.version}}</div>
 | 
			
		||||
          <div>{{template.parameters | parameters}}</div>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
        <div class="template-actions">
 | 
			
		||||
          <form #templateForm="ngForm">
 | 
			
		||||
            <div *ngIf="group.edit">
 | 
			
		||||
              <rb-form-input [name]="'name-' + group.name" label="name" appValidate="string" required
 | 
			
		||||
                             [(ngModel)]="templateEdit[group.first_id].name" #supplierInput="ngModel">
 | 
			
		||||
                <ng-template rbFormValidationMessage="failure">{{supplierInput.errors.failure}}</ng-template>
 | 
			
		||||
              </rb-form-input>
 | 
			
		||||
              <rb-array-input [(ngModel)]="templateEdit[group.first_id].parameters" [name]="'parameters-' + group.name"
 | 
			
		||||
                              [pushTemplate]="{name: '', range: {}, rangeString: '{}'}" pushPath="name"
 | 
			
		||||
                              class="parameters">
 | 
			
		||||
                <ng-container *rbArrayInputItem="let item">
 | 
			
		||||
                  <rb-form-input [rbArrayInputListener]="'parameter-name-' + group.name" appValidate="parameterName"
 | 
			
		||||
                                 [index]="item.i" [name]="'parameter-name-' + group.name + item.i"
 | 
			
		||||
                                 label="parameter name" [ngModel]="item.value.name" #parameterName="ngModel">
 | 
			
		||||
                    <ng-template rbFormValidationMessage="failure">{{parameterName.errors.failure}}</ng-template>
 | 
			
		||||
                  </rb-form-input>
 | 
			
		||||
                  <rb-form-textarea [name]="'parameter-range-' + group.name + item.i" label="range"
 | 
			
		||||
                                    appValidate="parameterRange" [(ngModel)]="item.value.rangeString"
 | 
			
		||||
                                    #parameterRange="ngModel">
 | 
			
		||||
                    <ng-template rbFormValidationMessage="failure">{{parameterRange.errors.failure}}</ng-template>
 | 
			
		||||
                  </rb-form-textarea>
 | 
			
		||||
                </ng-container>
 | 
			
		||||
              </rb-array-input>
 | 
			
		||||
            </div>
 | 
			
		||||
            <rb-icon-button icon="edit" mode="secondary" (click)="group.edit = !group.edit">
 | 
			
		||||
              Edit template
 | 
			
		||||
            </rb-icon-button>
 | 
			
		||||
            <rb-icon-button icon="save" mode="primary" (click)="saveTemplate(group.first_id)" *ngIf="group.edit"
 | 
			
		||||
                            [disabled]="templateForm.invalid">
 | 
			
		||||
              Save template
 | 
			
		||||
            </rb-icon-button>
 | 
			
		||||
          </form>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </ng-container>
 | 
			
		||||
	<ng-container *ngFor="let group of groupsView">
 | 
			
		||||
		<div class="row clickable">
 | 
			
		||||
			<div (click)="group.expanded = !group.expanded">{{group.name}}</div>
 | 
			
		||||
			<div (click)="group.expanded = !group.expanded">{{group.version}}</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="row" *ngIf="group.expanded" [@inOut]>
 | 
			
		||||
			<div class="details">
 | 
			
		||||
				<ng-container *ngFor="let template of group.entries">
 | 
			
		||||
					<div>{{template.name}}</div>
 | 
			
		||||
					<div>{{template.version}}</div>
 | 
			
		||||
					<div>{{template.parameters | parameters}}</div>
 | 
			
		||||
				</ng-container>
 | 
			
		||||
				<div class="template-actions">
 | 
			
		||||
					<form #templateForm="ngForm">
 | 
			
		||||
						<div *ngIf="group.edit">
 | 
			
		||||
							<rb-form-input [name]="'name-' + group.name" label="name" appValidate="string" required
 | 
			
		||||
								[(ngModel)]="templateEdit[group.first_id].name" #supplierInput="ngModel">
 | 
			
		||||
								<ng-template rbFormValidationMessage="failure">{{supplierInput.errors.failure}}</ng-template>
 | 
			
		||||
							</rb-form-input>
 | 
			
		||||
							<rb-array-input [(ngModel)]="templateEdit[group.first_id].parameters" [name]="'parameters-' + group.name"
 | 
			
		||||
								[pushTemplate]="{name: '', range: {}, rangeString: '{}'}" pushPath="name"
 | 
			
		||||
								class="parameters">
 | 
			
		||||
								<ng-container *rbArrayInputItem="let item">
 | 
			
		||||
									<rb-form-input [rbArrayInputListener]="'parameter-name-' + group.name" appValidate="parameterName"
 | 
			
		||||
										[index]="item.i" [name]="'parameter-name-' + group.name + item.i"
 | 
			
		||||
										label="parameter name" [ngModel]="item.value.name" #parameterName="ngModel">
 | 
			
		||||
										<ng-template rbFormValidationMessage="failure">{{parameterName.errors.failure}}</ng-template>
 | 
			
		||||
									</rb-form-input>
 | 
			
		||||
									<rb-form-textarea [name]="'parameter-range-' + group.name + item.i" label="range"
 | 
			
		||||
										appValidate="parameterRange" [(ngModel)]="item.value.rangeString"
 | 
			
		||||
										#parameterRange="ngModel">
 | 
			
		||||
										<ng-template rbFormValidationMessage="failure">{{parameterRange.errors.failure}}</ng-template>
 | 
			
		||||
									</rb-form-textarea>
 | 
			
		||||
								</ng-container>
 | 
			
		||||
							</rb-array-input>
 | 
			
		||||
						</div>
 | 
			
		||||
						<rb-icon-button icon="edit" mode="secondary" (click)="group.edit = !group.edit">
 | 
			
		||||
							Edit template
 | 
			
		||||
						</rb-icon-button>
 | 
			
		||||
						<rb-icon-button icon="save" mode="primary" (click)="saveTemplate(group.first_id)" *ngIf="group.edit"
 | 
			
		||||
							[disabled]="templateForm.invalid">
 | 
			
		||||
							Save template
 | 
			
		||||
						</rb-icon-button>
 | 
			
		||||
					</form>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</ng-container>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,39 +2,39 @@
 | 
			
		||||
 | 
			
		||||
.list {
 | 
			
		||||
 | 
			
		||||
  .row {
 | 
			
		||||
    display: grid;
 | 
			
		||||
    grid-template-columns: 1fr 4fr;
 | 
			
		||||
    border-bottom: 1px solid $color-gray-mercury;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
	.row {
 | 
			
		||||
		display: grid;
 | 
			
		||||
		grid-template-columns: 1fr 4fr;
 | 
			
		||||
		border-bottom: 1px solid $color-gray-mercury;
 | 
			
		||||
		overflow: hidden;
 | 
			
		||||
 | 
			
		||||
    & > div {
 | 
			
		||||
      padding: 8px 5px;
 | 
			
		||||
		& > div {
 | 
			
		||||
			padding: 8px 5px;
 | 
			
		||||
 | 
			
		||||
      &.header {
 | 
			
		||||
        font-weight: bold;
 | 
			
		||||
      }
 | 
			
		||||
			&.header {
 | 
			
		||||
				font-weight: bold;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
      &.details {
 | 
			
		||||
        grid-column: span 2;
 | 
			
		||||
        display: grid;
 | 
			
		||||
        grid-template-columns: 1fr 1fr 3fr;
 | 
			
		||||
        background: $color-gray-alabaster;
 | 
			
		||||
			&.details {
 | 
			
		||||
				grid-column: span 2;
 | 
			
		||||
				display: grid;
 | 
			
		||||
				grid-template-columns: 1fr 1fr 3fr;
 | 
			
		||||
				background: $color-gray-alabaster;
 | 
			
		||||
 | 
			
		||||
        .template-actions {
 | 
			
		||||
          grid-column: span 3;
 | 
			
		||||
          margin-top: 10px;
 | 
			
		||||
				.template-actions {
 | 
			
		||||
					grid-column: span 3;
 | 
			
		||||
					margin-top: 10px;
 | 
			
		||||
 | 
			
		||||
          .parameters {
 | 
			
		||||
            display: grid;
 | 
			
		||||
            grid-template-columns: 1fr 2fr;
 | 
			
		||||
          }
 | 
			
		||||
					.parameters {
 | 
			
		||||
						display: grid;
 | 
			
		||||
						grid-template-columns: 1fr 2fr;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
          rb-icon-button[icon="save"] {
 | 
			
		||||
            float: right;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
					rb-icon-button[icon="save"] {
 | 
			
		||||
						float: right;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,132 +8,132 @@ import omit from 'lodash/omit';
 | 
			
		||||
import {DataService} from '../services/data.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-templates',
 | 
			
		||||
  templateUrl: './templates.component.html',
 | 
			
		||||
  styleUrls: ['./templates.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}))
 | 
			
		||||
        ])
 | 
			
		||||
      ]
 | 
			
		||||
    )
 | 
			
		||||
  ]
 | 
			
		||||
	selector: 'app-templates',
 | 
			
		||||
	templateUrl: './templates.component.html',
 | 
			
		||||
	styleUrls: ['./templates.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 TemplatesComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  collection = 'measurement';       // Collection to view
 | 
			
		||||
  templates: TemplateModel[] = [];  // All templates of current collection
 | 
			
		||||
  templateGroups: {[first_id: string]: TemplateModel[]} = {};  // Templates grouped by first_id
 | 
			
		||||
  templateEdit: {[first_id: string]: TemplateModel} = {};      // Latest template of each first_id for editing
 | 
			
		||||
  groupsView: {  // Grouped templates
 | 
			
		||||
    first_id: string,
 | 
			
		||||
    name: string,
 | 
			
		||||
    version: number,
 | 
			
		||||
    expanded: boolean,
 | 
			
		||||
    edit: boolean,
 | 
			
		||||
    entries: TemplateModel[]
 | 
			
		||||
  }[] = [];
 | 
			
		||||
	collection = 'measurement';       // Collection to view
 | 
			
		||||
	templates: TemplateModel[] = [];  // All templates of current collection
 | 
			
		||||
	templateGroups: {[first_id: string]: TemplateModel[]} = {};  // Templates grouped by first_id
 | 
			
		||||
	templateEdit: {[first_id: string]: TemplateModel} = {};      // Latest template of each first_id for editing
 | 
			
		||||
		groupsView: {  // Grouped templates
 | 
			
		||||
			first_id: string,
 | 
			
		||||
			name: string,
 | 
			
		||||
			version: number,
 | 
			
		||||
			expanded: boolean,
 | 
			
		||||
			edit: boolean,
 | 
			
		||||
			entries: TemplateModel[]
 | 
			
		||||
		}[] = [];
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private api: ApiService,
 | 
			
		||||
    private validate: ValidationService,
 | 
			
		||||
    public d: DataService
 | 
			
		||||
  ) { }
 | 
			
		||||
		constructor(
 | 
			
		||||
			private api: ApiService,
 | 
			
		||||
			private validate: ValidationService,
 | 
			
		||||
			public d: DataService
 | 
			
		||||
		) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.loadTemplates();
 | 
			
		||||
  }
 | 
			
		||||
		ngOnInit(): void {
 | 
			
		||||
			this.loadTemplates();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
  loadTemplates() {
 | 
			
		||||
    this.d.load(this.collection + 'Templates', () => {
 | 
			
		||||
      this.templates = this.d.arr[this.collection + 'Templates'];
 | 
			
		||||
      this.templateFormat();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
		loadTemplates() {
 | 
			
		||||
			this.d.load(this.collection + 'Templates', () => {
 | 
			
		||||
				this.templates = this.d.arr[this.collection + 'Templates'];
 | 
			
		||||
				this.templateFormat();
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
  templateFormat() {
 | 
			
		||||
    this.templateGroups = {};
 | 
			
		||||
    this.templateEdit = {};
 | 
			
		||||
    this.templates.forEach(template => {  // Group templates
 | 
			
		||||
      if (this.templateGroups[template.first_id]) {
 | 
			
		||||
        this.templateGroups[template.first_id].push(template);
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        this.templateGroups[template.first_id] = [template];
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    Object.keys(this.templateGroups).forEach(id => {
 | 
			
		||||
      this.templateGroups[id] = this.templateGroups[id].sort((a, b) => a.version - b.version);
 | 
			
		||||
      this.templateEdit[id] = cloneDeep(this.templateGroups[id][this.templateGroups[id].length - 1]);
 | 
			
		||||
      this.templateEdit[id].parameters = this.templateEdit[id].parameters
 | 
			
		||||
        .map(e => {e.rangeString = JSON.stringify(e.range, null, 2); return e; });
 | 
			
		||||
    });
 | 
			
		||||
    this.groupsView = Object.values(this.templateGroups)
 | 
			
		||||
      .map(e => ({
 | 
			
		||||
        first_id: e[e.length - 1].first_id,
 | 
			
		||||
        name: e[e.length - 1].name,
 | 
			
		||||
        version: e[e.length - 1].version,
 | 
			
		||||
        expanded: false,
 | 
			
		||||
        edit: false,
 | 
			
		||||
        entries: e
 | 
			
		||||
      }));
 | 
			
		||||
  }
 | 
			
		||||
		templateFormat() {
 | 
			
		||||
			this.templateGroups = {};
 | 
			
		||||
			this.templateEdit = {};
 | 
			
		||||
			this.templates.forEach(template => {  // Group templates
 | 
			
		||||
				if (this.templateGroups[template.first_id]) {
 | 
			
		||||
					this.templateGroups[template.first_id].push(template);
 | 
			
		||||
				}
 | 
			
		||||
				else {
 | 
			
		||||
					this.templateGroups[template.first_id] = [template];
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
			Object.keys(this.templateGroups).forEach(id => {
 | 
			
		||||
				this.templateGroups[id] = this.templateGroups[id].sort((a, b) => a.version - b.version);
 | 
			
		||||
				this.templateEdit[id] = cloneDeep(this.templateGroups[id][this.templateGroups[id].length - 1]);
 | 
			
		||||
				this.templateEdit[id].parameters = this.templateEdit[id].parameters
 | 
			
		||||
				.map(e => {e.rangeString = JSON.stringify(e.range, null, 2); return e; });
 | 
			
		||||
			});
 | 
			
		||||
			this.groupsView = Object.values(this.templateGroups)
 | 
			
		||||
			.map(e => ({
 | 
			
		||||
				first_id: e[e.length - 1].first_id,
 | 
			
		||||
				name: e[e.length - 1].name,
 | 
			
		||||
				version: e[e.length - 1].version,
 | 
			
		||||
				expanded: false,
 | 
			
		||||
				edit: false,
 | 
			
		||||
				entries: e
 | 
			
		||||
			}));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
  saveTemplate(first_id) {
 | 
			
		||||
    const template = cloneDeep(this.templateEdit[first_id]);
 | 
			
		||||
    template.parameters = template.parameters.filter(e => e.name !== '');
 | 
			
		||||
    let valid = true;
 | 
			
		||||
    valid = valid && this.validate.string(template.name).ok;
 | 
			
		||||
    template.parameters.forEach(parameter => {
 | 
			
		||||
      valid = valid && this.validate.parameterName(parameter.name).ok;
 | 
			
		||||
      valid = valid && this.validate.parameterRange(parameter.rangeString).ok;
 | 
			
		||||
      if (valid) {
 | 
			
		||||
        parameter.range = JSON.parse(parameter.rangeString);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    if (valid) {
 | 
			
		||||
      const sendData = {name: template.name, parameters: template.parameters.map(e => omit(e, ['rangeString']))};
 | 
			
		||||
      if (first_id === 'null') {  // New template
 | 
			
		||||
        this.api.post<TemplateModel>(`/template/${this.collection}/new`, sendData, () => {
 | 
			
		||||
          delete this.d.arr[this.collection + 'Templates'];
 | 
			
		||||
          this.d.load(this.collection + 'Templates', () => {
 | 
			
		||||
            this.templates = this.d.arr[this.collection + 'Templates'];
 | 
			
		||||
            this.templateFormat();
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        this.api.put<TemplateModel>(`/template/${this.collection}/${template.first_id}`, sendData, () => {
 | 
			
		||||
          delete this.d.arr[this.collection + 'Templates'];
 | 
			
		||||
          this.d.load(this.collection + 'Templates', () => {
 | 
			
		||||
            this.templates = this.d.arr[this.collection + 'Templates'];
 | 
			
		||||
            this.templateFormat();
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
		saveTemplate(first_id) {
 | 
			
		||||
			const template = cloneDeep(this.templateEdit[first_id]);
 | 
			
		||||
			template.parameters = template.parameters.filter(e => e.name !== '');
 | 
			
		||||
			let valid = true;
 | 
			
		||||
			valid = valid && this.validate.string(template.name).ok;
 | 
			
		||||
			template.parameters.forEach(parameter => {
 | 
			
		||||
				valid = valid && this.validate.parameterName(parameter.name).ok;
 | 
			
		||||
				valid = valid && this.validate.parameterRange(parameter.rangeString).ok;
 | 
			
		||||
				if (valid) {
 | 
			
		||||
					parameter.range = JSON.parse(parameter.rangeString);
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
			if (valid) {
 | 
			
		||||
				const sendData = {name: template.name, parameters: template.parameters.map(e => omit(e, ['rangeString']))};
 | 
			
		||||
				if (first_id === 'null') {  // New template
 | 
			
		||||
					this.api.post<TemplateModel>(`/template/${this.collection}/new`, sendData, () => {
 | 
			
		||||
						delete this.d.arr[this.collection + 'Templates'];
 | 
			
		||||
						this.d.load(this.collection + 'Templates', () => {
 | 
			
		||||
							this.templates = this.d.arr[this.collection + 'Templates'];
 | 
			
		||||
							this.templateFormat();
 | 
			
		||||
						});
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
				else {
 | 
			
		||||
					this.api.put<TemplateModel>(`/template/${this.collection}/${template.first_id}`, sendData, () => {
 | 
			
		||||
						delete this.d.arr[this.collection + 'Templates'];
 | 
			
		||||
						this.d.load(this.collection + 'Templates', () => {
 | 
			
		||||
							this.templates = this.d.arr[this.collection + 'Templates'];
 | 
			
		||||
							this.templateFormat();
 | 
			
		||||
						});
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
  newTemplate() {
 | 
			
		||||
    if (!this.templateEdit.null) {
 | 
			
		||||
      const template = new TemplateModel();
 | 
			
		||||
      template.name = 'new template';
 | 
			
		||||
      this.groupsView.push({
 | 
			
		||||
        first_id: 'null',
 | 
			
		||||
        name: 'new template',
 | 
			
		||||
        version: 0,
 | 
			
		||||
        expanded: true,
 | 
			
		||||
        edit: true,
 | 
			
		||||
        entries: [template]
 | 
			
		||||
      });
 | 
			
		||||
      this.templateEdit.null = new TemplateModel();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
		newTemplate() {
 | 
			
		||||
			if (!this.templateEdit.null) {
 | 
			
		||||
				const template = new TemplateModel();
 | 
			
		||||
				template.name = 'new template';
 | 
			
		||||
				this.groupsView.push({
 | 
			
		||||
					first_id: 'null',
 | 
			
		||||
					name: 'new template',
 | 
			
		||||
					version: 0,
 | 
			
		||||
					expanded: true,
 | 
			
		||||
					edit: true,
 | 
			
		||||
					entries: [template]
 | 
			
		||||
				});
 | 
			
		||||
				this.templateEdit.null = new TemplateModel();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user