-
-
-
Hello, {{ title() }}
-
Congratulations! Your app is running. 🎉
-
-
-
-
- @for (item of [
- { title: 'Explore the Docs', link: 'https://angular.dev' },
- { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' },
- { title: 'Prompt and best practices for AI', link: 'https://angular.dev/ai/develop-with-ai'},
- { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' },
- { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' },
- { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' },
- ]; track item.title) {
-
- {{ item.title }}
-
-
- }
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index dc39edb..bec57b0 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -1,3 +1,10 @@
import { Routes } from '@angular/router';
+import { App} from './app';
+import { RisikoobjektView} from './risikoobjektView/risikoobjektView';
+import {Produktbaum} from './produktbaum/produktbaum';
-export const routes: Routes = [];
+export const routes: Routes = [
+ { path: '', redirectTo: '/produktbaum', pathMatch: 'full' },
+ { path: 'produktbaum', component: Produktbaum },
+ { path: 'risikoobjekte', component: RisikoobjektView },
+];
diff --git a/src/app/app.ts b/src/app/app.ts
index 4312c38..14fc063 100644
--- a/src/app/app.ts
+++ b/src/app/app.ts
@@ -1,12 +1,23 @@
-import { Component, signal } from '@angular/core';
-import { RouterOutlet } from '@angular/router';
+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: [RouterOutlet],
+ imports: [CommonModule, FormsModule, Navbar, RouterOutlet],
templateUrl: './app.html',
styleUrl: './app.css'
})
+@Injectable({providedIn: 'root'})
export class App {
protected readonly title = signal('OMDSAngularWebClient');
+
+
}
diff --git a/src/app/navbar/navbar.css b/src/app/navbar/navbar.css
new file mode 100644
index 0000000..3af0f1a
--- /dev/null
+++ b/src/app/navbar/navbar.css
@@ -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;
+ }
+}
diff --git a/src/app/navbar/navbar.html b/src/app/navbar/navbar.html
new file mode 100644
index 0000000..3b7255a
--- /dev/null
+++ b/src/app/navbar/navbar.html
@@ -0,0 +1,10 @@
+
diff --git a/src/app/navbar/navbar.spec.ts b/src/app/navbar/navbar.spec.ts
new file mode 100644
index 0000000..8893c93
--- /dev/null
+++ b/src/app/navbar/navbar.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { Navbar } from './navbar';
+
+describe('Navbar', () => {
+ let component: Navbar;
+ let fixture: ComponentFixture
;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [Navbar]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(Navbar);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/navbar/navbar.ts b/src/app/navbar/navbar.ts
new file mode 100644
index 0000000..4ecfbd4
--- /dev/null
+++ b/src/app/navbar/navbar.ts
@@ -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 {
+
+}
diff --git a/src/app/produktbaum/produktbaum.css b/src/app/produktbaum/produktbaum.css
new file mode 100644
index 0000000..dbf2665
--- /dev/null
+++ b/src/app/produktbaum/produktbaum.css
@@ -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;
+}
diff --git a/src/app/produktbaum/produktbaum.html b/src/app/produktbaum/produktbaum.html
new file mode 100644
index 0000000..824e94d
--- /dev/null
+++ b/src/app/produktbaum/produktbaum.html
@@ -0,0 +1,99 @@
+
+
+
Angular
+
+
+
+
+
Produktbaum
+
+
+
+ -
+
+
+ 0" style="color: red" > {{baustein.bez}}
+ {{baustein.bez}}
+
+
+
+
+
+
+
+
+ 0"
+ [ngTemplateOutlet]="bausteinTemplate"
+ [ngTemplateOutletContext]="{ $implicit: baustein.unterbausteine }">
+
+
+
+
+
+
+
+
+
diff --git a/src/app/produktbaum/produktbaum.spec.ts b/src/app/produktbaum/produktbaum.spec.ts
new file mode 100644
index 0000000..712bdde
--- /dev/null
+++ b/src/app/produktbaum/produktbaum.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { Produktbaum } from './produktbaum';
+
+describe('Produktbaum', () => {
+ let component: Produktbaum;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [Produktbaum]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(Produktbaum);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/produktbaum/produktbaum.ts b/src/app/produktbaum/produktbaum.ts
new file mode 100644
index 0000000..9fc7560
--- /dev/null
+++ b/src/app/produktbaum/produktbaum.ts
@@ -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 {
+ private 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('/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.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.actualOcc = {value: 1};
+ produkt.occByParent = 1;
+
+ return produkt as Produktbaustein;
+ }
+
+ buildTree() {
+
+ this.addMissingAprioriItems();
+
+ 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);
+ }
+ }
+ }
+ }
+
+ 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('/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.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.risikoobjekte.push(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();
+ this.createIdCount(idCount, verkaufsprodukt);
+
+ const iriMap = new Map();
+ this.createIriMap(verkaufsprodukt, baseIri, idCount, iriMap);
+
+ this.addProduktToModel(verkaufsprodukt, model, baseIri, null, iriMap);
+
+ return model
+ }
+
+ async modelToJsonld(model: Store) : Promise {
+ 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, 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,
+ iriMap: Map
+ ): 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
+ ) {
+ 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 {
+ 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)
+ }
+ }
+}
diff --git a/src/app/risikoobjektView/risikoobjektView.css b/src/app/risikoobjektView/risikoobjektView.css
new file mode 100644
index 0000000..7670e9f
--- /dev/null
+++ b/src/app/risikoobjektView/risikoobjektView.css
@@ -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;
+ }
+}
diff --git a/src/app/risikoobjektView/risikoobjektView.html b/src/app/risikoobjektView/risikoobjektView.html
new file mode 100644
index 0000000..77cc102
--- /dev/null
+++ b/src/app/risikoobjektView/risikoobjektView.html
@@ -0,0 +1,94 @@
+
+
+
+
Konfiguration
+
+
+
+
+
+
+
+
Es müssen alle Felder ausgefüllt werden
+
+
+
+
Risikoobjekte
+
+
+ -
+ {{ r.handelsbezeichnung }}({{r.baujahr}})
+
+
+
+
diff --git a/src/app/risikoobjektView/risikoobjektView.spec.ts b/src/app/risikoobjektView/risikoobjektView.spec.ts
new file mode 100644
index 0000000..311c1ab
--- /dev/null
+++ b/src/app/risikoobjektView/risikoobjektView.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RisikoobjektView } from './risikoobjektView';
+
+describe('Risikoobjekt', () => {
+ let component: RisikoobjektView;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [RisikoobjektView]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(RisikoobjektView);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/risikoobjektView/risikoobjektView.ts b/src/app/risikoobjektView/risikoobjektView.ts
new file mode 100644
index 0000000..625a7ad
--- /dev/null
+++ b/src/app/risikoobjektView/risikoobjektView.ts
@@ -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);
+ }
+}
diff --git a/src/app/services/risikoobjekt.service.ts b/src/app/services/risikoobjekt.service.ts
new file mode 100644
index 0000000..5d72fc5
--- /dev/null
+++ b/src/app/services/risikoobjekt.service.ts
@@ -0,0 +1,27 @@
+import { Injectable, signal } from '@angular/core';
+import { FahrzeugType } from '../risikoobjektView/risikoobjektView';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class RisikoobjektService {
+ private risikoobjekteSignal = signal([]);
+
+ 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([]);
+ }
+}
diff --git a/src/assets/logo_kapdion.gif b/src/assets/logo_kapdion.gif
new file mode 100644
index 0000000..03709e3
Binary files /dev/null and b/src/assets/logo_kapdion.gif differ
diff --git a/src/proxy.conf.json b/src/proxy.conf.json
new file mode 100644
index 0000000..71f0321
--- /dev/null
+++ b/src/proxy.conf.json
@@ -0,0 +1,12 @@
+{
+ "/ProductsRequest": {
+ "target": "http://localhost:9090",
+ "secure": false,
+ "logLevel": "debug"
+ },
+ "/CalculateRequest": {
+ "target": "http://localhost:9090",
+ "secure": false,
+ "logLevel": "debug"
+ }
+}