Hydra EntryPoint erstellt. vocab.jsonld angepasst

This commit is contained in:
2026-01-29 15:50:01 +01:00
parent 74161b4a79
commit 695772949f
47 changed files with 2340 additions and 191 deletions

View File

@@ -1,8 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: '<h1>Hello World from Angular!</h1>',
styles: []
})
export class AppComponent { }

View File

@@ -0,0 +1,15 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import {provideRouter, RouteReuseStrategy} from '@angular/router';
import { routes } from './app.routes';
import {provideHttpClient} from '@angular/common/http';
import {CustomRouteReuseStrategy} from './route-reuse.strategy';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes),
provideHttpClient(),
{ provide: RouteReuseStrategy, useClass: CustomRouteReuseStrategy }
]
};

View File

@@ -0,0 +1,71 @@
.baustein-list {
padding-left: 2rem;
}
.baustein-item {
margin-bottom: 0.5rem;
}
.baustein-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.baustein-name {
font-weight: 600;
}
.baustein-buttons {
display: flex;
gap: 0.5rem;
padding-right: 80rem;
}
.btn-control {
border: 1px solid #ccc;
background-color: #f9f9f9;
cursor: pointer;
padding: 0.25rem 0.5rem;
border-radius: 4px;
transition: background-color 0.2s ease;
}
.btn-control:hover:not(:disabled) {
background-color: #eee;
}
.btn-control:disabled {
opacity: 0.5;
}
.baustein-name.notIncluded {
color: darkgray;
font-style: italic;
}
table {
table-layout: auto;
border-collapse: collapse;
width: 500px;
margin-right: 5px;
}
td, th {
border: 1px solid #ddd;
padding: 8px;
font-size: 1rem;
width: 100%;
}
tr:hover {background-color: #ddd;}
th {
padding-top: 12px;
padding-bottom: 12px;
text-align: left;
background-color: #2244AA;
color: white;
}

View File

@@ -0,0 +1,6 @@
<main class="main">
<div class="content" style="text-align:left; margin-top:50px;">
<app-navbar></app-navbar>
<router-outlet></router-outlet>
</div>
</main>

View File

@@ -1,11 +0,0 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

View File

@@ -0,0 +1,11 @@
import { Routes } from '@angular/router';
import { App} from './app';
import { RisikoobjektView} from './risikoobjektView/risikoobjektView';
import {Produktbaum} from './produktbaum/produktbaum';
export const routes: Routes = [
// { path: '', redirectTo: '/produktbaum', pathMatch: 'full' },
{ path: '', component: Produktbaum },
// { path: 'produktbaum', component: Produktbaum },
{ path: 'risikoobjekte', component: RisikoobjektView },
];

View File

@@ -0,0 +1,23 @@
import { TestBed } from '@angular/core/testing';
import { App } from './app';
describe('App', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [App],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(App);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it('should render title', () => {
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, OMDSAngularWebClient');
});
});

23
client-web/src/app/app.ts Normal file
View File

@@ -0,0 +1,23 @@
import {Component, inject, Injectable, signal} from '@angular/core';
import {CommonModule} from '@angular/common';
import {HttpClient} from '@angular/common/http';
import {QueryEngine} from '@comunica/query-sparql';
import {DataFactory} from 'rdf-data-factory';
import {Store} from 'n3';
import jsonld from 'jsonld';
import {FormsModule} from '@angular/forms';
import {Navbar} from './navbar/navbar';
import {RouterOutlet} from '@angular/router';
@Component({
selector: 'app-root',
imports: [CommonModule, FormsModule, Navbar, RouterOutlet],
templateUrl: './app.html',
styleUrl: './app.css'
})
@Injectable({providedIn: 'root'})
export class App {
protected readonly title = signal('OMDSAngularWebClient');
}

View File

@@ -0,0 +1,99 @@
/* Navbar Grund-Container */
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
background-color: #ffffff;
padding: 10px 40px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
position: sticky;
top: 0;
z-index: 1000;
border-bottom: 3px solid #007bff; /* Passend zum Blau der Buttons */
}
/* Logo Bereich */
.navbar-logo {
display: flex;
align-items: center;
}
.navbar-logo img {
display: block;
object-fit: contain;
/* Falls das Logo bei 98px zu groß wirkt, kannst du hier max-height nutzen: */
max-height: 70px;
width: auto;
}
/* Navigations-Links */
.navbar-links {
display: flex;
list-style: none;
margin: 0;
padding: 0;
gap: 30px;
}
.navbar-links li {
margin: 0;
}
.navbar-links a {
text-decoration: none;
color: #495057;
font-weight: 600;
font-size: 1rem;
padding: 10px 15px;
border-radius: 6px;
transition: all 0.3s ease;
position: relative;
}
/* Hover-Effekt */
.navbar-links a:hover {
color: #007bff;
background-color: #f0f7ff;
}
/* Aktiver Link (Angular RouterLinkActive Style) */
/* Falls du [routerLinkActive]="'active'" im HTML nutzt: */
.navbar-links a.active {
color: #007bff;
background-color: #e7f1ff;
}
/* Kleiner Indikator-Strich unter dem Text beim Hover */
.navbar-links a::after {
content: '';
position: absolute;
bottom: 5px;
left: 50%;
width: 0;
height: 2px;
background: #007bff;
transition: all 0.3s ease;
transform: translateX(-50%);
}
.navbar-links a:hover::after {
width: 60%;
}
/* Mobile Optimierung */
@media (max-width: 768px) {
.navbar {
flex-direction: column;
padding: 15px;
gap: 15px;
}
.navbar-links {
gap: 10px;
}
.navbar-logo img {
max-height: 50px;
}
}

View File

@@ -0,0 +1,10 @@
<nav class="navbar">
<div class="navbar-logo">
<img ngSrc="assets/logo_kapdion.gif" alt="Kapdion Logo" height="98" width="180" priority fetchpriority="high" />
</div>
<ul class="navbar-links">
<li><a routerLink="/">Home</a></li>
<li><a routerLink="/risikoobjekte">Risikoobjekte</a></li>
</ul>
</nav>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Navbar } from './navbar';
describe('Navbar', () => {
let component: Navbar;
let fixture: ComponentFixture<Navbar>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Navbar]
})
.compileComponents();
fixture = TestBed.createComponent(Navbar);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,16 @@
import { Component } from '@angular/core';
import {RouterLink} from '@angular/router';
import {NgOptimizedImage} from '@angular/common';
@Component({
selector: 'app-navbar',
imports: [
RouterLink,
NgOptimizedImage
],
templateUrl: './navbar.html',
styleUrl: './navbar.css',
})
export class Navbar {
}

View File

@@ -0,0 +1,184 @@
/* Grundlayout & Schrift */
.main {
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
color: #333;
line-height: 1.6;
background-color: #f8f9fa;
padding: 20px;
min-height: 100vh;
}
.content {
max-width: 900px;
margin: 50px auto;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
/* Titel & Buttons oben */
h1, h2 {
color: #2c3e50;
border-bottom: 2px solid #eee;
padding-bottom: 10px;
}
button:not(.btn-control) {
background-color: #007bff;
color: white;
border: none;
padding: 10px 18px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
margin-right: 10px;
margin-bottom: 20px;
transition: background 0.2s;
}
button:not(.btn-control):hover {
background-color: #0056b3;
}
button:disabled {
background-color: #ccc !important;
cursor: not-allowed;
}
/* Der Produktbaum (Hierarchie) */
.baustein-list {
list-style: none;
padding-left: 25px;
border-left: 1px dashed #ced4da;
}
.baustein-item {
margin: 10px 0;
position: relative;
}
/* Verbindungslinien-Effekt */
.baustein-item::before {
content: "";
position: absolute;
top: 15px;
left: -25px;
width: 20px;
border-top: 1px dashed #ced4da;
}
.baustein-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
background: #fff;
border: 1px solid #e9ecef;
padding: 10px 15px;
border-radius: 6px;
transition: all 0.2s;
}
.baustein-row:hover {
border-color: #adb5bd;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
/* Baustein Status */
.baustein-name {
font-weight: 600;
color: #495057;
flex-grow: 1;
}
.notIncluded {
color: #adb5bd !important;
font-style: italic;
text-decoration: line-through;
}
/* +/- Buttons */
.baustein-buttons {
display: flex;
gap: 5px;
margin-left: 15px;
}
.btn-control {
width: 28px;
height: 28px;
border-radius: 4px;
border: 1px solid #dee2e6;
background: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-weight: bold;
}
.btn-control:hover:not(:disabled) {
background: #e2e6ea;
}
/* Details, Tabellen & Inputs */
.extraInfo-container {
background: #fdfdfe;
border: 1px solid #eef2f7;
padding: 15px;
border-radius: 4px;
margin-top: 10px;
width: 100%;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10px;
}
th {
text-align: left;
font-size: 0.85rem;
color: #6c757d;
text-transform: uppercase;
padding-bottom: 8px;
}
td {
padding: 6px 0;
border-bottom: 1px solid #f1f1f1;
}
input[type="text"],
input[type="number"] {
padding: 4px 8px;
border: 1px solid #ced4da;
border-radius: 3px;
width: 100%;
}
/* Fehlermeldungen */
.meldungen-container {
color: #dc3545;
background-color: #fff5f5;
padding: 8px;
border-left: 4px solid #dc3545;
margin-top: 10px;
font-size: 0.9rem;
}
/* Risikoobjekte */
.risikoobjekt-container {
margin-top: 15px;
padding-top: 10px;
border-top: 1px solid #eee;
}
.risikoobjekt-item-container {
display: flex;
align-items: center;
gap: 10px;
padding: 5px 0;
font-size: 0.9rem;
}

View File

@@ -0,0 +1,99 @@
<main class="main">
<div class="content" style="text-align:left; margin-top:50px;">
<h1>Angular</h1>
<button (click)="apriori()">Produktwissen abfragen</button>
<button (click)="calculate(verkaufsprodukte[0])">calculate</button>
<h2>Produktbaum</h2>
<ng-template #bausteinTemplate let-bausteine let-risikoobjekt="risikoobjekt">
<ul class="baustein-list">
<li *ngFor="let baustein of bausteine" class="baustein-item">
<div class="baustein-row">
<span
class="baustein-name"
[ngClass]="{ 'notIncluded': baustein.occByParent < 1 }"
(click)="toggleAttributes(baustein)"
style="cursor:pointer;"
>
<div *ngIf="baustein.meldungen.length > 0" style="color: red" > {{baustein.bez}} </div>
<div *ngIf="baustein.meldungen.length <= 0"> {{baustein.bez}} </div>
</span>
<div *ngIf="baustein.showAttribute" class="extraInfo-container" style="margin-left: 20px; margin-top: 5px;">
<table>
<div *ngFor="let attr of baustein.attribute" class="attribute-item">
<tr>
<td>
<strong>{{ attr.bez }}:</strong>
</td>
<td>
<input
*ngIf="getInputType(attr) === 'checkbox'"
type="checkbox"
[checked]="attr.value !== undefined ? attr.value : (attr.default !== undefined ? attr.default : false)"
(change)="setBooleanAttributValue($event, attr)"
[disabled]="attr.aenderbar !== undefined ? !attr.aenderbar : false"
/>
<input
*ngIf="getInputType(attr) !== 'checkbox'"
[(ngModel)]="attr.value"
[type]="getInputType(attr)"
[placeholder]="attr.value ?? attr.default ?? ''"
(ngModelChange)="attributValueListener(attr)"
[disabled]="attr.aenderbar !== undefined ? !attr.aenderbar : false"
/>
</td>
</tr>
</div>
</table>
<div *ngFor="let meldung of baustein.meldungen" class="meldungen-container">
<strong> {{meldung.errorMsg}} </strong>
</div>
<div *ngIf="risikoobjektService.risikoobjekte().length > 0" class="risikoobjekt-container">
<strong>Mit dem Baustein assoziiertes Risikoobjekt</strong>
<div *ngFor="let ro of risikoobjektService.risikoobjekte()" class="risikoobjekt-item-container">
<label [for]="'check-' + '{{ro.id}}'">
{{ro.handelsbezeichnung}} ({{ro.baujahr}})
</label>
<input
type="checkbox"
[id]="'check-' + '{{ro.id}}'"
(change)="risikoobjektListener(baustein, ro, $event)"
[checked]="baustein.risikoobjekte.includes(ro)"
/>
</div>
</div>
</div>
<div class="baustein-buttons">
<button
(click)="removeProdukt(baustein)"
[disabled]="baustein.occByParent <= baustein.minOcc"
class="btn-control">-</button>
<button
(click)="addProdukt(baustein)"
[disabled]="baustein.actualOcc.value >= baustein.maxOcc && baustein.occByParent >= baustein.maxOcc"
class="btn-control">+</button>
</div>
</div>
<ng-container
*ngIf="baustein.unterbausteine.length > 0"
[ngTemplateOutlet]="bausteinTemplate"
[ngTemplateOutletContext]="{ $implicit: baustein.unterbausteine }">
</ng-container>
</li>
</ul>
</ng-template>
<ng-container
[ngTemplateOutlet]="bausteinTemplate"
[ngTemplateOutletContext]="{ $implicit: verkaufsprodukte, risikoobjekt: risikoobjekte}">
</ng-container>
</div>
</main>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Produktbaum } from './produktbaum';
describe('Produktbaum', () => {
let component: Produktbaum;
let fixture: ComponentFixture<Produktbaum>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Produktbaum]
})
.compileComponents();
fixture = TestBed.createComponent(Produktbaum);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,952 @@
import {Component, inject, Injectable, Input, input, signal} from '@angular/core';
import {CommonModule} from '@angular/common';
import {HttpClient} from '@angular/common/http';
import {QueryEngine} from '@comunica/query-sparql';
import {DataFactory} from 'rdf-data-factory';
import {Store} from 'n3';
import jsonld from 'jsonld';
import {FormsModule} from '@angular/forms';
import {RouterOutlet} from '@angular/router';
import {FahrzeugType, Risikoobjekt, RisikoobjektView} from '../risikoobjektView/risikoobjektView';
import {RisikoobjektService} from '../services/risikoobjekt.service';
interface Produktbaustein {
id : string;
bez: string;
type: string;
praemienfaktor: string;
minOcc: number;
maxOcc: number;
occByParent: number;
actualOcc: { value: number };
showAttribute: boolean;
parent: Produktbaustein;
unterbausteine: Produktbaustein[];
risikoobjekte: (FahrzeugType)[];
attribute: Attribut[];
meldungen: Meldung[];
}
interface Meldung {
id: string
errorType: number;
errorMsg: string;
kommtVonPlausi: string;
}
interface Attribut {
id: string
bez: string;
aenderbar: boolean;
pflichtfeld: boolean;
produktId: string;
value?: string | number | boolean;
default?: string | boolean | number;
type: string
}
interface BooleanAttribut extends Attribut{
default: boolean;
value: boolean;
}
interface StringAttribut extends Attribut{
default: string;
value: string;
}
interface IntAttribut extends Attribut{
default: number;
value: number;
max: number;
min: number;
}
interface DecimalAttribut extends Attribut{
default: number;
value: number;
max: number;
min: number;
}
interface Plausi {
beschreibung: string;
query: string;
art: string;
}
@Component({
selector: 'app-root',
imports: [CommonModule, FormsModule],
templateUrl: './produktbaum.html',
styleUrl: './produktbaum.css'
})
@Injectable({providedIn: 'root'})
export class Produktbaum {
protected risikoobjektService = inject(RisikoobjektService);
risikoobjekte = this.risikoobjektService.risikoobjekte;
protected readonly title = signal('OMDSAngularWebClient');
http = inject(HttpClient);
produkte : Produktbaustein[] = [];
verkaufsprodukte : Produktbaustein[] = [];
attribute : Attribut[] = [];
plausis : Plausi[] = [];
meldungen : Meldung[] = []
aprioriProdukte : Produktbaustein[] = [];
apriori(){
this.http.post<any[]>('produktApi/ProductsRequest', { "stichtag": "2022-02-11" })
.subscribe(result => {
this.produkte = [];
this.attribute = [];
this.plausis = [];
this.meldungen = [];
const prod = 'http://vvo.pisanoapi.at/ProdElement';
const plaus = 'http://vvo.pisanoapi.at/Plausi';
const att = 'http://vvo.pisanoapi.at/Elem';
const meld = 'http://vvo.pisanoapi.at/Meldung';
result.forEach(p => {
const id : string = this.extractValue(p['@id']);
if (id.startsWith(meld)) {
this.meldungen.push(this.buildMeldung(p))
}else if (id.startsWith(plaus)) {
const tmpPlaus = this.buildPlausi(p);
this.plausis.push(tmpPlaus);
}else if (id.startsWith(att)){
this.attribute.push(this.buildAttribut(p))
}else if (id.startsWith(prod)){
this.produkte.push(this.buildProdukt(p))
}
})
this.produkte = this.sortProdukte(this.produkte);
for (const att of this.attribute){
for (const produkt of this.produkte) {
if (produkt.id.startsWith(att.produktId)) {
produkt.attribute.push(att);
}
}
}
this.buildTree();
this.verkaufsprodukte = this.produkte.filter(p => p.parent === undefined);
this.aprioriProdukte = structuredClone(this.produkte);
});
}
buildAttribut(p: any) : (DecimalAttribut | IntAttribut | BooleanAttribut | StringAttribut){
const keyMapping: { [key: string]: string } = {
'@id': 'id',
'http://vvo.pisanoapi.at/bez': 'bez',
'http://vvo.pisanoapi.at/required': 'required',
'http://vvo.pisanoapi.at/value': 'value',
'http://vvo.pisanoapi.at/max': 'max',
'http://vvo.pisanoapi.at/min': 'min',
'http://vvo.pisanoapi.at/default': 'default',
'http://vvo.pisanoapi.at/aenderbar': 'aenderbar',
'http://vvo.pisanoapi.at/ProdElement': 'produktId',
};
const attribut: any = {};
for (const key in p) {
const mappedKey = keyMapping[key];
if (mappedKey){
let value = this.extractValue(p[key]);
if (mappedKey === 'id' && typeof value === 'string') {
if (value.startsWith("vvo:")){
value = value.substring(4)
}else {
value = value.substring(24);
}
attribut.type = value.substring(4)
}
if (mappedKey === 'produktId' && typeof value === 'string') {
if (value.startsWith("vvo:")){
value = value.substring(15);
}else {
value = value.substring(35);
}
}
if (mappedKey === 'value') {
value = this.parseDynamicValue(value);
}
if (mappedKey === 'default') {
value = this.parseDynamicValue(value)
if (typeof value === "boolean" && value) {
attribut.value = true
}
}
if (mappedKey === 'aenderbar') {
value = this.parseDynamicValue(value)
}
attribut[mappedKey] = value;
}
}
return attribut as (DecimalAttribut | IntAttribut | BooleanAttribut | StringAttribut);
}
buildMeldung(p: any) : Meldung{
const keyMapping: { [key: string]: string } = {
'@id': 'id',
'http://vvo.pisanoapi.at/errorMsg': 'errorMsg',
'http://vvo.pisanoapi.at/errorType': 'errorType',
'vvo:errorMsg': 'errorMsg',
'vvo:errorType': 'errorType'
};
const meldung: any = {};
for (const key in p) {
const mappedKey = keyMapping[key];
if (mappedKey){
let value = this.extractValue(p[key]);
if (mappedKey === 'id' && typeof value === 'string') {
if (value.startsWith("vvo:")){
value = value.substring(4);
}else {
value = value.substring(24);
}
}
meldung[mappedKey] = value;
}
}
return meldung as Meldung;
}
parseDynamicValue(value: string): string | boolean | number {
if (value.toLowerCase() === 'true') return true;
if (value.toLowerCase() === 'false') return false;
if (!isNaN(Number(value)) && value.trim() !== '') {
const num = Number(value);
return Number.isInteger(num) ? num : parseFloat(value);
}
return value;
}
extractValue(data: any): any {
if (!data) return undefined;
const container = Array.isArray(data) ? data[0] : data;
if (container && typeof container === 'object' && container['@value'] !== undefined) {
return container['@value'].trim();
}else if (container && typeof container === 'object' && container['@id'] !== undefined) {
return container['@id'].trim();
}
return container;
}
buildPlausi(p: any): Plausi {
const keyMapping: { [key: string]: string } = {
'@id': 'id',
'http://vvo.pisanoapi.at/beschreibung': 'beschreibung',
'http://vvo.pisanoapi.at/query': 'query',
'http://vvo.pisanoapi.at/art': 'art',
'vvo:beschreibung': 'beschreibung',
'vvo:query': 'query',
'vvo:art': 'art'
};
const plausi: any = {};
for (const key in p) {
const mappedKey = keyMapping[key];
if (mappedKey){
let value = this.extractValue(p[key]);
plausi[mappedKey] = value;
}
}
return plausi as Plausi;
}
buildFahrzeug(p: any): FahrzeugType {
const keyMapping: { [key: string]: string } = {
'@id': 'id',
'http://vvo.pisanoapi.at/bez': 'handelsbezeichnung',
'http://vvo.pisanoapi.at/baujahr': 'baujahr',
'http://vvo.pisanoapi.at/erstzulassung': 'erstzulassung',
'http://vvo.pisanoapi.at/leistung': 'leistung',
'http://vvo.pisanoapi.at/listenpreis': 'listenpreis',
'http://vvo.pisanoapi.at/sonderausstattung': 'sonderausstattung',
'http://vvo.pisanoapi.at/kennzeichen': 'kennzeichen'
};
const fahrzeug: any = {};
for (const key in p) {
const mappedKey = keyMapping[key];
if (mappedKey){
let value = this.extractValue(p[key]);
if (mappedKey === 'id' && typeof value === 'string') {
value = value.substring(24);
}
if (mappedKey === 'baujahr' && typeof value === 'string') {
value = parseInt(value, 10);
if (isNaN(value)) value = undefined;
}
if (mappedKey === 'erstzulassung' && typeof value === 'string') {
value = new Date(value.substring(0, 10));
}
fahrzeug[mappedKey] = value;
}
}
return fahrzeug as FahrzeugType;
}
buildProdukt(p: any): Produktbaustein {
const keyMapping: { [key: string]: string } = {
'@id': 'id',
'id': 'id',
'http://vvo.pisanoapi.at/bez': 'bez',
'http://vvo.pisanoapi.at/minOccurrence': 'minOcc',
'http://vvo.pisanoapi.at/maxOccurrence': 'maxOcc',
'http://vvo.pisanoapi.at/type': 'type',
'http://vvo.pisanoapi.at/Parent': 'parent',
'http://vvo.pisanoapi.at/Meldung': 'meldungen',
'http://vvo.pisanoapi.at/VersichertesInteresseType' : 'risikoobjekte',
'http://vvo.pisanoapi.at/praemienfaktor' : 'praemienfaktor',
};
const produkt: any = {};
produkt.meldungen = [];
produkt.risikoobjekte = [];
for (const key in p) {
const mappedKey = keyMapping[key];
if (mappedKey){
let value = this.extractValue(p[key]);
if (mappedKey === 'risikoobjekte') {
const values = Array.isArray(p[key]) ? p[key] : [p[key]];
for (const v of values) {
const value = this.extractValue(v);
const ro = this.risikoobjektService.risikoobjekte().find(
r => r.id === value.substring(24)
);
if (ro) {
produkt.risikoobjekte.push(ro);
} else {
console.log("Risiko Objekt nicht gefunden: " + value);
}
}
}
if ((mappedKey === 'minOcc' || mappedKey === 'maxOcc') && typeof value === 'string') {
value = parseInt(value, 10);
if (isNaN(value)) value = undefined;
}
if (mappedKey === 'id' && typeof value === 'string') {
// if(value.includes("-")) {
// value = value.substring(0, value.indexOf("-"))
// }
if (value.startsWith("vvo:")) {
value = value.substring(15)
}else {
value = value.substring(35);
}
}
if (mappedKey === 'parent' && typeof value === 'string') {
if (value.startsWith("vvo:")){
value = this.produkte.find(p => p.id === value.substring(15));
}else {
value = this.produkte.find(p => p.id === value.substring(35));
}
}
if (mappedKey === 'meldungen' && typeof value === 'string') {
if (value.startsWith("vvo:")){
value = this.meldungen.find(p => p.id === value.substring(4));
}else {
value = this.meldungen.find(p => p.id === value.substring(24));
}
if (value !== undefined){
produkt[mappedKey].push(value)
}
}
if (mappedKey !== "meldungen" && mappedKey !== "risikoobjekte") {
produkt[mappedKey] = value;
}
}
}
produkt.unterbausteine = [];
produkt.attribute = [];
produkt.occByParent = (produkt.parent === undefined || produkt.minOcc > 0) ? 1 : 0;
produkt.actualOcc = { value: produkt.occByParent };
return produkt as Produktbaustein;
}
buildTree() {
for (const baustein of this.produkte) {
if (baustein.parent !== undefined){
let parent : (Produktbaustein | undefined) = this.produkte.find(p => p === baustein.parent);
if (parent !== undefined && !parent.unterbausteine.includes(baustein)) {
if (parent.occByParent < 1) {
baustein.occByParent = 0
}
parent.unterbausteine.push(baustein);
}
}
}
this.addMissingAprioriItems();
}
async addProdukt(produkt : Produktbaustein) {
for (const unterprodukt of produkt.parent.unterbausteine){
if (unterprodukt.id === produkt.id){
unterprodukt.occByParent++;
}
}
if (produkt.occByParent >= 2) {
const parent = this.produkte.find(p => p === produkt.parent)
parent?.unterbausteine.push(this.cloneProdukt(produkt, undefined));
}else {
produkt.actualOcc.value++;
}
this.addRequiredChildren(produkt)
await this.constructPlausi()
}
addParent(produkt : Produktbaustein) {
if (produkt.parent.occByParent < 1) {
produkt.parent.occByParent = 1
this.addParent(produkt.parent)
}
}
addRequiredChildren(produkt : Produktbaustein) {
for (const unter of produkt.unterbausteine) {
if (unter.minOcc > 0 && unter.occByParent < 1) {
unter.occByParent = 1
if (unter.unterbausteine.length > 0) {
this.addRequiredChildren(unter)
}
}
}
}
async constructPlausi() {
for (const plausi of this.plausis.filter(p => p.art === "graph")) {
for (const prod of this.produkte) {
for (const meld of prod.meldungen) {
if (meld.kommtVonPlausi === plausi.beschreibung) {
prod.meldungen.splice(prod.meldungen.indexOf(meld), 1);
}
}
}
let model = await this.buildRDFModel(this.verkaufsprodukte[0]);
model = await this.pruefePlausi(plausi, model);
const newTree = await this.modelToJsonld(model);
// this.produkte = [];
// this.attribute = [];
// this.meldungen = [];
const prod = 'http://vvo.pisanoapi.at/ProdElement';
const plaus = 'vvo:Plausi';
const att = 'vvo:Elem';
const meld = 'http://vvo.pisanoapi.at/Meldung';
let uniqueIds: any[] = [];
let wholeprodukts: any[] = []
newTree.forEach((p: any) => {if(p !== undefined) p.forEach((q: any) => {
if (!uniqueIds.includes(q["@id"])) {
uniqueIds.push(q["@id"]);
}
})});
for (let id of uniqueIds) {
let produkt: any[] = [];
newTree.forEach((p: any) => p.filter((p: any) => p["@id"] === id).forEach((i: any) => {
produkt.push(i)
}));
wholeprodukts.push(produkt);
}
for (const q of wholeprodukts) {
const merged = Object.assign({}, ...q);
if (q[0]["@id"].startsWith(meld)) {
this.meldungen.push(this.buildMeldung(merged))
} else if (q[0]["@id"].startsWith(plaus)) {
const tmpPlaus = this.buildPlausi(merged);
this.plausis.push(tmpPlaus);
} else if (q[0]["@id"].startsWith(att)) {
this.attribute.push(this.buildAttribut(merged))
}
}
for (const q of wholeprodukts) {
const merged = Object.assign({}, ...q);
if (q[0]["@id"].includes(prod)) {
let newProd = this.buildProdukt(merged);
if (newProd.bez === undefined) {
const prod = this.produkte.find(p => newProd.id.includes(p.id));
if (prod !== undefined) {
//für andere sachen als meldung genauso, wenn wir Plausis wollen, die mehr machen als Meldungen hinzufügen
for (const newMeldung of newProd.meldungen) {
newMeldung.kommtVonPlausi = plausi.beschreibung
prod.meldungen.push(newMeldung)
}
}
}
}
}
for (const att of this.attribute) {
for (const produkt of this.produkte) {
if (produkt.id.startsWith(att.produktId) && !produkt.attribute.includes(att)) {
produkt.attribute.push(att);
}
}
}
this.buildTree();
this.verkaufsprodukte = this.produkte.filter(p => p.parent === undefined);
}
}
cloneProdukt(produkt : Produktbaustein, newParent: (Produktbaustein | undefined)) : Produktbaustein {
const cloneProdukt = structuredClone(produkt);
if (newParent === undefined) {
cloneProdukt.parent = produkt.parent
}else {
cloneProdukt.parent = newParent;
}
cloneProdukt.unterbausteine = []
cloneProdukt.actualOcc = produkt.actualOcc;
cloneProdukt.actualOcc.value++;
for (const child of produkt.unterbausteine) {
cloneProdukt.unterbausteine.push(this.cloneProdukt(child, cloneProdukt))
}
this.produkte.push(cloneProdukt);
return cloneProdukt;
}
async removeProdukt(produkt : Produktbaustein) {
if (produkt.occByParent >= 2){
const parent = this.produkte.find(p => p === produkt.parent)
if (parent !== undefined){
const i = parent?.unterbausteine.indexOf(produkt);
const j = this.produkte.indexOf(produkt);
if (i > -1) {
parent.unterbausteine.splice(i, 1);
this.produkte.splice(j, 1);
}
}
}
produkt.actualOcc.value--;
for (const unterprodukt of produkt.parent.unterbausteine){
if (unterprodukt.id === produkt.id){
unterprodukt.occByParent--;
}
}
if (produkt.unterbausteine.length > 0) {
this.removeChildren(produkt);
}
await this.constructPlausi()
}
removeChildren(produkt : Produktbaustein) {
const tmp : string[] = [];
const toDelete : Produktbaustein[] = [];
for (const child of produkt.unterbausteine){
if (!tmp.includes(child.id)){
child.actualOcc.value = child.actualOcc.value - child.occByParent;
tmp.push(child.id);
}
if (child.occByParent > 1){
toDelete.push(child)
for (const unterbaustein of produkt.unterbausteine){
unterbaustein.occByParent--;
}
}
child.occByParent = 0;
this.removeChildren(child);
}
for (const e of toDelete){
produkt.unterbausteine.splice(produkt.unterbausteine.indexOf(e) ,1)
}
}
async calculate(verkaufsprodukt: Produktbaustein) {
const request = (await this.modelToJsonld(await this.buildRDFModel(verkaufsprodukt)));
this.produkte = [];
this.http.post<any[]>('produktApi/CalculateRequest', request )
.subscribe(result => {
this.produkte = [];
this.attribute = [];
const prod = 'http://vvo.pisanoapi.at/ProdElement';
const plaus = 'http://vvo.pisanoapi.at/Plausi';
const att = 'http://vvo.pisanoapi.at/Elem';
const meld = 'http://vvo.pisanoapi.at/Meldung';
const fz = 'http://vvo.pisanoapi.at/FahrzeugType';
result.forEach(p => {
const id : string = this.extractValue(p['@id']);
if (id.startsWith(meld)) {
this.meldungen.push(this.buildMeldung(p))
}else if (id.startsWith(plaus)) {
const tmpPlaus = this.buildPlausi(p);
this.plausis.push(tmpPlaus);
}else if (id.startsWith(att)){
this.attribute.push(this.buildAttribut(p))
}else if (id.startsWith(fz)){
const fahrzeug = this.buildFahrzeug(p);
let fahrzeugAlt =
this.risikoobjektService.risikoobjekte().find(r =>
r.handelsbezeichnung === fahrzeug.handelsbezeichnung
&& new Date(r.erstzulassung).getTime() === fahrzeug.erstzulassung.getTime())
if (fahrzeugAlt !== undefined){
//alle attribute die sich geädert haben könnten
fahrzeugAlt.id = fahrzeug.id;
fahrzeugAlt.baujahr = fahrzeug.baujahr;
} else {
console.log("neues fahrzeug")
this.risikoobjektService.addRisikoobjekt(fahrzeug);
}
}
})
result.forEach(p => {
const id : string = this.extractValue(p['@id']);
if (id.startsWith(prod)){
this.produkte.push(this.buildProdukt(p))
}
})
this.produkte = this.sortProdukte(this.produkte);
for (const att of this.attribute){
for (const produkt of this.produkte) {
if (produkt.id.startsWith(att.produktId)) {
produkt.attribute.push(att);
}
}
}
this.buildTree();
this.verkaufsprodukte = this.produkte.filter(p => p.parent === undefined);
});
}
sortProdukte(produkte: Produktbaustein[]) : Produktbaustein[] {
produkte.sort((a, b) => {
return a.bez < b.bez ? -1 : a.bez > b.bez ? 1 : 0
});
return produkte;
}
async buildRDFModel(verkaufsprodukt : Produktbaustein) {
const model = new Store();
const baseIri = 'http://vvo.pisanoapi.at/';
const idCount = new Map<string, number>();
this.createIdCount(idCount, verkaufsprodukt);
const iriMap = new Map<Produktbaustein, string>();
this.createIriMap(verkaufsprodukt, baseIri, idCount, iriMap);
this.addProduktToModel(verkaufsprodukt, model, baseIri, null, iriMap);
return model
}
async modelToJsonld(model: Store) : Promise<any[]> {
let result = []
const jsonldData = model.getQuads(null, null, null, null).map(q => ({
'@id': q.subject.value,
[q.predicate.value]: [
q.object.termType === 'Literal'
? { '@value': q.object.value }
: { '@id': q.object.value }
]
}));
const context = { vvo: 'http://vvo.pisanoapi.at/' };
result.push(await jsonld.expand(jsonldData));
return result;
}
createIdCount(idCount: Map<string, number>, vp: Produktbaustein): void {
const id = vp.id;
idCount.set(id, (idCount.get(id) ?? 0) + 1);
for (const child of vp.unterbausteine) {
this.createIdCount(idCount, child);
}
}
createIriMap(
produkt: Produktbaustein,
baseIri: string,
idCount: Map<string, number>,
iriMap: Map<Produktbaustein, string>
): void {
const baseElemIri = baseIri + 'ProdElement';
const id = produkt.id;
const count = idCount.get(id) ?? 1;
for (let i = 1; i <= count; i++) {
let tmpIri = `${baseElemIri}${id}-${i}`;
if (id.includes("-")){
tmpIri = `${baseElemIri}${id}`;
}
if (![...iriMap.values()].includes(tmpIri)) {
iriMap.set(produkt, tmpIri);
break;
}
}
for (const child of produkt.unterbausteine) {
this.createIriMap(child, baseIri, idCount, iriMap);
}
}
addProduktToModel(
produkt: Produktbaustein,
model: Store,
baseIri: string,
parentId: string | null,
iriMap: Map<Produktbaustein, string>
) {
const DF = new DataFactory()
const vpIri = DF.namedNode(iriMap.get(produkt)!);
if (produkt.occByParent >= 1) {
model.addQuad(vpIri, DF.namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'), DF.namedNode("http://vvo.pisanoapi.at/ProdElement"))
model.addQuad(vpIri, DF.namedNode(baseIri + 'bez'), DF.literal(produkt.bez));
if (produkt.minOcc !== undefined)
model.addQuad(vpIri, DF.namedNode(baseIri + 'minOccurrence'), DF.literal(produkt.minOcc.toString()));
if (produkt.maxOcc !== undefined)
model.addQuad(vpIri, DF.namedNode(baseIri + 'maxOccurrence'), DF.literal(produkt.maxOcc.toString()));
// if (produkt.verkaufsoffenVon)
// model.addQuad(vpIri, DF.namedNode(baseIri + 'salesFrom'), DF.literal(produkt.verkaufsoffenVon));
// if (produkt.verkaufsoffenBis)
// model.addQuad(vpIri, DF.namedNode(baseIri + 'salesTo'), DF.literal(produkt.verkaufsoffenBis));
if (produkt.type)
model.addQuad(vpIri, DF.namedNode(baseIri + 'type'), DF.literal(produkt.type));
// if (produkt.risikoobjektErforderlich)
// model.addQuad(vpIri, DF.namedNode(baseIri + 'risikoobjektType'), DF.literal('FahrzeugType'));
if (produkt.praemienfaktor)
model.addQuad(vpIri, DF.namedNode(baseIri + 'praemienfaktor'), DF.literal(produkt.praemienfaktor));
if (parentId)
model.addQuad(vpIri, DF.namedNode(baseIri + 'Parent'), DF.namedNode(parentId));
}
for (const at of produkt.attribute ?? []) {
let typeIri = baseIri;
if (at.type.toLowerCase().includes("decimal")){
typeIri += "ElemDecimal";
}else if (at.type.toLowerCase().includes("int")){
typeIri += "ElemInt";
}else if (at.type.toLowerCase().includes("bool")){
typeIri += "ElemBoolean";
}else {
typeIri += "ElemString";
}
const atIri = DF.namedNode(`${baseIri}${at.id}`);
model.addQuad(atIri, DF.namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'), DF.namedNode(typeIri));
model.addQuad(atIri, DF.namedNode(baseIri + 'bez'), DF.literal(at.bez));
model.addQuad(atIri, DF.namedNode(baseIri + 'ProdElement'), vpIri);
if (typeof at.value === "string") {
model.addQuad(atIri, DF.namedNode(baseIri + 'value'), DF.literal(at.value));
}else if (typeof at.value === "boolean"){
model.addQuad(atIri, DF.namedNode(baseIri + 'value'), DF.literal(at.value ? "true" : "false"));
} else if (at.value !== undefined) {
model.addQuad(atIri, DF.namedNode(baseIri + 'value'), DF.literal(at.value.toString()));
}
}
let i = 1;
for (const ro of produkt.risikoobjekte) {
const fahrzeugIri = DF.namedNode(baseIri + "FahrzeugType")
const spezFahrzeugIri = DF.namedNode(fahrzeugIri.value + ro.handelsbezeichnung + ro.baujahr);
model.addQuad(vpIri, DF.namedNode(baseIri + "VersichertesInteresseType"), spezFahrzeugIri);
const existingQuads = model.getQuads(spezFahrzeugIri, null, null, null);
if (existingQuads.length === 0) {
model.addQuad(spezFahrzeugIri, DF.namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'), fahrzeugIri);
if (ro.handelsbezeichnung) {
model.addQuad(spezFahrzeugIri, DF.namedNode(baseIri + 'bez'), DF.literal(ro.handelsbezeichnung));
}
if (ro.baujahr !== undefined) {
model.addQuad(spezFahrzeugIri, DF.namedNode(baseIri + 'baujahr'), DF.literal(ro.baujahr.toString()));
}
if (ro.erstzulassung) {
model.addQuad(spezFahrzeugIri, DF.namedNode(baseIri + 'erstzulassung'), DF.literal( ro.erstzulassung.toString()));
}
if (ro.kennzeichen) {
model.addQuad(spezFahrzeugIri, DF.namedNode(baseIri + 'kennzeichen'), DF.literal( ro.kennzeichen));
}
if (ro.leistung) {
model.addQuad(spezFahrzeugIri, DF.namedNode(baseIri + 'leistung'), DF.literal( ro.leistung.toString()));
}
if (ro.listenpreis) {
model.addQuad(spezFahrzeugIri, DF.namedNode(baseIri + 'listenpreis'), DF.literal( ro.listenpreis.toString()));
}
if (ro.sonderausstattung) {
model.addQuad(spezFahrzeugIri, DF.namedNode(baseIri + 'sonderausstattung'), DF.literal( ro.sonderausstattung.toString()));
}
}
i++;
}
for (const child of produkt.unterbausteine) {
const unterIri = iriMap.get(child)!;
if (produkt.occByParent >= 1) {
model.addQuad(vpIri, DF.namedNode(baseIri + 'Baustein'), DF.namedNode(unterIri));
}
this.addProduktToModel(child, model, baseIri, vpIri.value, iriMap);
}
}
async pruefePlausi(plausi: Plausi, model: Store): Promise<Store> {
const engine = new QueryEngine();
if (plausi.art === "graph"){
const result = await engine.queryQuads(plausi.query, { sources: [model],});
const quads = await result.toArray();
const erg : Store = new Store();
for (const quad of quads) {
erg.addQuad(quad.subject, quad.predicate, quad.object);
}
return erg;
}
return model;
}
toggleAttributes(baustein: Produktbaustein) {
baustein.showAttribute = !baustein.showAttribute;
}
getInputType(attr: Attribut): string {
const a = attr as BooleanAttribut | StringAttribut | IntAttribut | DecimalAttribut;
if (typeof a.value === 'boolean' || typeof a.default === 'boolean') return 'checkbox';
if (typeof a.value === 'number' || typeof a.default === 'number') return 'number';
return 'text';
}
attributValueListener(attr: Attribut) {
console.log(attr.value)
}
setBooleanAttributValue(event: Event, attr: Attribut) {
attr.value = (event.target as HTMLInputElement).checked
}
addMissingAprioriItems(){
for (const aprod of this.aprioriProdukte) {
let flag = false;
for (const prod of this.produkte) {
if (prod.bez === aprod.bez) {
flag = true
}
}
if (!flag) {
aprod.occByParent = 0
this.removeChildren(aprod)
const newParent = this.produkte.find(p => p.bez === aprod.parent.bez)
if (newParent !== undefined) {
aprod.parent = newParent
}
this.produkte.push(this.cloneProdukt(aprod, undefined))
}
}
}
risikoobjektListener(baustein: Produktbaustein, ro: FahrzeugType, event: Event) {
if ((event.target as HTMLInputElement).checked) {
baustein.risikoobjekte.push(ro)
} else {
baustein.risikoobjekte.splice(baustein.risikoobjekte.indexOf(ro), 1)
}
}
}

View File

@@ -0,0 +1,137 @@
/* Container Layout */
.container {
display: flex;
gap: 30px;
max-width: 1200px;
margin: 40px auto;
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
align-items: flex-start;
}
/* Linke Spalte: Konfiguration */
.left-pane {
flex: 2;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
/* Rechte Spalte: Liste der Objekte */
.right-pane {
flex: 1;
background: #fdfdfe;
padding: 25px;
border-radius: 8px;
border: 1px solid #e9ecef;
min-height: 200px;
}
/* Titel-Styling */
h2 {
color: #2c3e50;
border-bottom: 2px solid #eee;
padding-bottom: 10px;
margin-top: 0;
}
h3 {
grid-column: 1 / -1; /* Überschrift über beide Spalten im Grid */
color: #007bff;
font-size: 1.1rem;
margin: 20px 0 10px 0;
}
/* Formular Styling */
.grid-form {
display: grid;
grid-template-columns: 1fr 1fr; /* Zwei Spalten für Labels und Inputs */
gap: 15px 20px;
align-items: center;
margin-top: 20px;
}
.grid-form label {
font-weight: 600;
color: #495057;
font-size: 0.9rem;
}
/* Inputs & Select */
input, select {
width: 100%;
padding: 10px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 0.95rem;
transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
}
input:focus, select:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
}
/* Sonderfall: Alleinstehende Inputs ohne Label (Typ 1, 2 etc.) */
.grid-form input:not([id]) {
grid-column: 1 / -1;
}
/* Submit Button */
.btn-submit {
background-color: #28a745; /* Grün für "Hinzufügen" */
color: white;
border: none;
padding: 12px 24px;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
margin-top: 25px;
width: 100%;
transition: background 0.2s;
}
.btn-submit:hover:not(:disabled) {
background-color: #218838;
}
.btn-submit:disabled {
background-color: #ccc;
cursor: not-allowed;
}
/* Validierungshinweis */
h4 {
font-size: 0.85rem;
margin-top: 10px;
text-align: center;
}
/* Rechte Liste Styling */
.right-pane ul {
list-style: none;
padding: 0;
}
.right-pane li {
background: white;
margin-bottom: 8px;
padding: 10px 15px;
border-radius: 5px;
border-left: 4px solid #007bff;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
font-size: 0.9rem;
color: #495057;
}
/* Responsive Anpassung */
@media (max-width: 768px) {
.container {
flex-direction: column;
}
.grid-form {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,94 @@
<div class="container">
<div class="left-pane">
<h2>Konfiguration</h2>
<label>Typ auswählen:</label>
<select [(ngModel)]="selectedType">
<option value="">-- bitte wählen --</option>
<option *ngFor="let t of types" [value]="t">{{ t }}</option>
</select>
<div class="form-container" >
<form #f="ngForm" class="grid-form">
<ng-container *ngIf="selectedType === 'Versichertes objekt SachPrivat'">
<h3>Formular für Typ 1</h3>
<input placeholder="Feld A1">
<input placeholder="Feld A2">
<input placeholder="Feld A3">
<input placeholder="Feld A4">
</ng-container>
<ng-container *ngIf="selectedType === 'Versicherte Liegenschaft'">
<h3>Formular für Typ 2</h3>
<input placeholder="Feld B1">
<input placeholder="Feld B2">
<input placeholder="Feld B3">
<input placeholder="Feld B4">
</ng-container>
<ng-container *ngIf="selectedType === 'Fahrzeug'" >
<h3>Angaben zum neuen Fahrzeug</h3>
<label for="baujahr">Baujahr:</label>
<input [(ngModel)]="formData.Baujahr" id="baujahr" type="number" name="Baujahr" placeholder="Baujahr" ngModel required>
<label for="leistung">Leistung in kW:</label>
<input [(ngModel)]="formData.Leistung" id="leistung" type="number" name="Leistung" placeholder="Leistung" ngModel required>
<label for="listenpreis">Listenpreis:</label>
<input [(ngModel)]="formData.Listenpreis" id="listenpreis" type="number" name="Listenpreis" placeholder="Listenpreis" ngModel required>
<label for="sonderausstattung">Sonderausstattung:</label>
<input [(ngModel)]="formData.Sonderausstattung" id="sonderausstattung" type="number" name="Sonderausstattung" placeholder="Sonderausstattung" ngModel required>
<label for="handelsbezeichnung">Handelsbezeichnung:</label>
<input [(ngModel)]="formData.Handelsbezeichnung" id="handelsbezeichnung" name="Handelsbezeichnung" placeholder="Handelsbezeichnung" ngModel required>
<label for="erstzulassung">Erstzulassung:</label>
<input [(ngModel)]="formData.Erstzulassung" id="erstzulassung" type="date" name="Erstzulassung" placeholder="Erstzulassung" ngModel required>
<label for="kennzeichen">Kennzeichen:</label>
<input [(ngModel)]="formData.Kennzeichen" id="kennzeichen" name="Kennzeichen" placeholder="Kennzeichen" ngModel required>
</ng-container>
<ng-container *ngIf="selectedType === 'Risiko Gebaeude'">
<h3>Formular für Typ 4</h3>
<input placeholder="Feld D1">
<input placeholder="Feld D2">
<input placeholder="Feld D3">
<input placeholder="Feld D4">
</ng-container>
<ng-container *ngIf="selectedType === 'Versicherte Person'">
<h3>Formular für Typ 5</h3>
<input placeholder="Feld E1">
<input placeholder="Feld E2">
<input placeholder="Feld E3">
<input placeholder="Feld E4">
</ng-container>
<ng-container *ngIf="selectedType === 'Risiko Haushalt'">
<h3>Formular für Typ 6</h3>
<input placeholder="Feld F1">
<input placeholder="Feld F2">
<input placeholder="Feld F3">
<input placeholder="Feld F4">
</ng-container>
</form>
<button class="btn-submit" (click)="addRisikoobjekt()" [disabled]="!f.valid">Hinzufügen</button>
<h4 style="color: red" [hidden]="f.valid">Es müssen alle Felder ausgefüllt werden</h4>
</div>
</div>
<div class="right-pane">
<h2>Risikoobjekte</h2>
<ul>
<li *ngFor="let r of risikoobjekte()">
{{ r.handelsbezeichnung }}({{r.baujahr}})
</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RisikoobjektView } from './risikoobjektView';
describe('Risikoobjekt', () => {
let component: RisikoobjektView;
let fixture: ComponentFixture<RisikoobjektView>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RisikoobjektView]
})
.compileComponents();
fixture = TestBed.createComponent(RisikoobjektView);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,81 @@
import {Component, inject} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {NgForOf, NgIf} from '@angular/common';
import {RisikoobjektService} from '../services/risikoobjekt.service';
export interface Risikoobjekt {
}
export interface FahrzeugType extends Risikoobjekt{
id?: string
baujahr: number
handelsbezeichnung: string
erstzulassung: Date
kennzeichen: string
leistung: number
listenpreis: number
sonderausstattung: number
}
@Component({
selector: 'app-risikoobjekt',
imports: [
FormsModule,
NgForOf,
NgIf
],
templateUrl: './risikoobjektView.html',
styleUrl: './risikoobjektView.css',
})
export class RisikoobjektView {
private risikoobjektService = inject(RisikoobjektService);
risikoobjekte = this.risikoobjektService.risikoobjekte;
types = [
"Versichertes objekt SachPrivat",
"Versicherte Liegenschaft",
"Fahrzeug",
"Risiko Gebaeude",
"Versicherte Person",
"Risiko Haushalt"
];
selectedType: string = '';
formData = {
Baujahr: 0,
Handelsbezeichnung: '',
Erstzulassung: new Date(),
Kennzeichen: '',
Leistung: 0,
Listenpreis: 0,
Sonderausstattung: 0
};
addRisikoobjekt(){
if (this.formData.Baujahr === 0 ||
this.formData.Handelsbezeichnung === ''
|| this.formData.Erstzulassung === null
|| this.formData.Kennzeichen === ''
|| this.formData.Leistung === 0
|| this.formData.Listenpreis === 0
|| this.formData.Sonderausstattung === 0){
}
const fahrzeug : FahrzeugType = {
baujahr: this.formData.Baujahr,
handelsbezeichnung: this.formData.Handelsbezeichnung.replace(/ /g, '_'),
erstzulassung: this.formData.Erstzulassung,
kennzeichen: this.formData.Kennzeichen,
leistung: this.formData.Leistung,
listenpreis: this.formData.Listenpreis,
sonderausstattung: this.formData.Sonderausstattung
}
this.risikoobjektService.addRisikoobjekt(fahrzeug);
}
}

View File

@@ -0,0 +1,27 @@
import { RouteReuseStrategy, ActivatedRouteSnapshot, DetachedRouteHandle } from '@angular/router';
export class CustomRouteReuseStrategy implements RouteReuseStrategy {
private handlers: { [key: string]: DetachedRouteHandle } = {};
// Soll die Route gespeichert werden?
shouldDetach(route: ActivatedRouteSnapshot): boolean {
// Hier kannst du filtern, welche Route gespeichert werden soll (z.B. 'produktbaum')
return route.routeConfig?.path === 'produktbaum';
}
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
this.handlers[route.routeConfig?.path!] = handle;
}
shouldAttach(route: ActivatedRouteSnapshot): boolean {
return !!this.handlers[route.routeConfig?.path!];
}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
return this.handlers[route.routeConfig?.path!];
}
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
return future.routeConfig === curr.routeConfig;
}
}

View File

@@ -0,0 +1,27 @@
import { Injectable, signal } from '@angular/core';
import { FahrzeugType } from '../risikoobjektView/risikoobjektView';
@Injectable({
providedIn: 'root'
})
export class RisikoobjektService {
private risikoobjekteSignal = signal<FahrzeugType[]>([]);
get risikoobjekte() {
return this.risikoobjekteSignal.asReadonly();
}
addRisikoobjekt(risikoobjekt: FahrzeugType) {
this.risikoobjekteSignal.update(current => [...current, risikoobjekt]);
}
removeRisikoobjekt(risikoobjekt: FahrzeugType) {
this.risikoobjekteSignal.update(current =>
current.filter(ro => ro !== risikoobjekt)
);
}
clearRisikoobjekte() {
this.risikoobjekteSignal.set([]);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,11 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>WebFrontend</title>
<base href="/">
<meta charset="utf-8">
<title>OMDSAngularWebClient</title>
<base href="/produktwissen-app/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
<app-root></app-root>
</body>
</html>

View File

@@ -1,5 +1,6 @@
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
bootstrapApplication(App, appConfig)
.catch((err) => console.error(err));

View File

@@ -0,0 +1,8 @@
{
"/produktwissen-app/produktApi": {
"target": "http://localhost:9090/",
"secure": false,
"changeOrigin": true,
"logLevel": "debug"
}
}

View File

@@ -0,0 +1 @@
/* You can add global styles to this file, and also import other style files */