Compare commits

26 Commits

Author SHA1 Message Date
14ccf08e07 Dockerfile cleanup
All checks were successful
Productdefinitions Build & Deploy / deploy (push) Successful in 1m23s
2026-01-30 14:52:38 +01:00
cc2d2ffe6a execution goal hinzugefügt
All checks were successful
Productdefinitions Build & Deploy / deploy (push) Successful in 1m21s
2026-01-30 13:51:16 +01:00
c2f8c414e6 Dockerfile debug verbessert
Some checks failed
Productdefinitions Build & Deploy / deploy (push) Failing after 1m14s
2026-01-30 13:43:52 +01:00
55dd52565c Dockerfile überarbietet
All checks were successful
Productdefinitions Build & Deploy / deploy (push) Successful in 2m50s
2026-01-30 13:29:16 +01:00
8b6341691c debug entfernt
All checks were successful
Productdefinitions Build & Deploy / deploy (push) Successful in 2m59s
2026-01-30 13:21:19 +01:00
be837f760d fälschlichen / gelöscht
Some checks failed
Productdefinitions Build & Deploy / deploy (push) Failing after 9s
2026-01-30 13:19:45 +01:00
024a6d1a35 maven package zu install geändert
Some checks failed
Productdefinitions Build & Deploy / deploy (push) Failing after 11s
2026-01-30 13:17:23 +01:00
e256a30b33 -pl flag für maven start befehl hinzugefügt
Some checks failed
Productdefinitions Build & Deploy / deploy (push) Failing after 2m4s
2026-01-30 13:04:21 +01:00
58957b584b execution goal geändert
Some checks failed
Productdefinitions Build & Deploy / deploy (push) Failing after 1m58s
2026-01-30 12:59:43 +01:00
f960b3b95d spring-boot-starter goal angepasst
Some checks failed
Productdefinitions Build & Deploy / deploy (push) Failing after 2m0s
2026-01-30 12:52:04 +01:00
28109deabc Jar aufruf auf -jar geändert
All checks were successful
Productdefinitions Build & Deploy / deploy (push) Successful in 3m0s
2026-01-30 12:35:16 +01:00
c10c6f5f9f Jar Pfad der main Klasse mitgegeben
All checks were successful
Productdefinitions Build & Deploy / deploy (push) Successful in 3m0s
2026-01-30 12:21:11 +01:00
d44995fb05 Docker port geändert
All checks were successful
Productdefinitions Build & Deploy / deploy (push) Successful in 3m12s
2026-01-30 11:48:25 +01:00
2879d4f0fe server-app dependency auf web-client auskommentiert
All checks were successful
Productdefinitions Build & Deploy / deploy (push) Successful in 3m0s
2026-01-29 17:14:50 +01:00
7c135b01e0 Zuerst Parent dann server-app bauen
Some checks failed
Productdefinitions Build & Deploy / deploy (push) Failing after 2m18s
2026-01-29 17:06:13 +01:00
219e4757a5 Dockerfile position zurückgesetzt
Some checks failed
Productdefinitions Build & Deploy / deploy (push) Failing after 2m38s
2026-01-29 17:00:15 +01:00
1a30692b35 Dockerfile parent pom bauen
Some checks failed
Productdefinitions Build & Deploy / deploy (push) Failing after 1m53s
2026-01-29 16:55:08 +01:00
f51d2ff515 Dockerfile copy angepasst
Some checks failed
Productdefinitions Build & Deploy / deploy (push) Failing after 2m35s
2026-01-29 16:44:34 +01:00
c4dde09a55 Dockerfile debug befehl geadded
Some checks failed
Productdefinitions Build & Deploy / deploy (push) Failing after 9s
2026-01-29 16:42:08 +01:00
0365ae6d5c Dockerfile position geändert
Some checks failed
Productdefinitions Build & Deploy / deploy (push) Failing after 11s
2026-01-29 16:39:34 +01:00
7920e0c673 dokcer compose context geändert
Some checks failed
Productdefinitions Build & Deploy / deploy (push) Failing after 8s
2026-01-29 16:38:14 +01:00
e8afe6a7e5 yaml statt yml
Some checks failed
Productdefinitions Build & Deploy / deploy (push) Failing after 8s
2026-01-29 16:36:02 +01:00
fe451e4716 docker compose befehl file spezifiziert
Some checks failed
Productdefinitions Build & Deploy / deploy (push) Failing after 7s
2026-01-29 16:33:58 +01:00
d3184dfbe5 Gitea Workflow ordnerstruktur geändert
Some checks failed
Productdefinitions Build & Deploy / deploy (push) Failing after 8s
2026-01-29 16:12:23 +01:00
82ed1e476f Gitea Workflow hinzugefügt 2026-01-29 15:55:35 +01:00
695772949f Hydra EntryPoint erstellt. vocab.jsonld angepasst 2026-01-29 15:50:01 +01:00
51 changed files with 2413 additions and 212 deletions

View File

@@ -0,0 +1,16 @@
name: Productdefinitions Build & Deploy
on:
push:
branches: [development]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Build and Run
run: |
docker compose -f docker-compose.yaml up -d --build

View File

@@ -175,4 +175,25 @@
</map>
</option>
</component>
<component name="http://lnkd.tech/editor#GraphVisibilityConfig">
<option name="visibility">
<map>
<entry key="GENERAL">
<value>
<Visibility />
</value>
</entry>
<entry key="SHACL_DATA">
<value>
<Visibility />
</value>
</entry>
<entry key="SHACL_SHAPES">
<value>
<Visibility />
</value>
</entry>
</map>
</option>
</component>
</project>

5
.idea/misc.xml generated
View File

@@ -7,6 +7,11 @@
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
<option name="ignoredFiles">
<set>
<option value="$PROJECT_DIR$/client-web/pom.xml" />
</set>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />

2
.idea/vcs.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="" vcs="Git" />
</component>
</project>

21
Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /productdefinitions
RUN git clone -b feature/Produkte --single-branch https://bitbucket.org/omds/omdsservicedefinitions.git /tmp/lib \
&& cd /tmp/lib/OMDSServiceDefinition \
&& mvn clean install -DskipTests
COPY pom.xml .
COPY server-app/pom.xml server-app/
COPY . .
RUN mvn clean install -DskipTests -pl server-app -am
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /productdefinitions/server-app/target/server-app-*.jar app.jar
EXPOSE 9080
ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@@ -1,6 +1,6 @@
{
"@context": {
"api": "https://bureau.kapdion.com/produktwissen-app/produktApi/vocab#",
"api": "http://localhost:9090/produktwissen-app/produktApi/",
"vvo": "http://vvo.pisanoapi.at/",
"hydra": "http://www.w3.org/ns/hydra/core#",
"xsd": "http://www.w3.org/2001/XMLSchema#",

View File

@@ -5,16 +5,16 @@
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
"xsd": "http://www.w3.org/2001/XMLSchema#",
"schema": "http://schema.org/",
"api": "https://bureau.kapdion.com/produktwissen-app/produktApi/vocab#",
"api": "http://localhost:9090/produktwissen-app/produktApi/",
"vvo": "http://vvo.pisanoapi.at/"
},
"@id": "api:ApiDocumentation",
"@id": "http://localhost:9090/produktwissen-app/produktApi/vocab.jsonld",
"@type": "hydra:ApiDocumentation",
"hydra:title": "Produkt API",
"hydra:entrypoint": "https://bureau.kapdion.com/produktwissen-app/produktApi",
"hydra:entrypoint": "http://localhost:9090/produktwissen-app/produktApi/",
"hydra:supportedClass": [
{
"@id": "api:EntryPoint",
"@id": "hydra:EntryPoint",
"@type": "hydra:Class",
"hydra:title": "API Einstiegspunkt",
"hydra:supportedOperation": [
@@ -26,7 +26,10 @@
"hydra:expects": "api:ProductsRequest",
"hydra:returns": "api:ProductsResponse",
"hydra:possibleStatus": [
{ "hydra:statusCode": 200, "description": "Produktwissen erfolgreich abgefragt" }
{
"hydra:statusCode": 200,
"hydra:description": "Produktwissen erfolgreich abgefragt"
}
]
},
{
@@ -37,7 +40,10 @@
"hydra:expects": "api:CalculateRequest",
"hydra:returns": "api:CalculateResponse",
"hydra:possibleStatus": [
{ "hydra:statusCode": 200, "description": "Berechnung durchgeführt" }
{
"hydra:statusCode": 200,
"hydra:description": "Berechnung durchgeführt"
}
]
}
]
@@ -51,7 +57,7 @@
"hydra:property": "api:stichtag",
"hydra:title": "Stichtag für die Abfrage",
"hydra:required": true,
"range": "xsd:date"
"rdfs:range": "xsd:date"
}
]
},
@@ -60,11 +66,15 @@
"@type": "hydra:Class",
"hydra:title": "Berechnungsanfrage",
"hydra:supportedProperty": [
{ "hydra:property": "vvo:ProdElement" },
{ "hydra:property": "vvo:Meldung" },
{ "hydra:property": "vvo:FahrzeugType" }
{
"hydra:property": "vvo:ProdElement"
},
{
"hydra:property": "vvo:Meldung"
},
{
"hydra:property": "vvo:FahrzeugType"
}
]
},
{
@@ -72,8 +82,12 @@
"@type": "hydra:Class",
"hydra:title": "Antwort der Produktwissen-Abfrage",
"hydra:supportedProperty": [
{ "hydra:property": "vvo:ProdElement" },
{ "hydra:property": "vvo:Meldung" }
{
"hydra:property": "vvo:ProdElement"
},
{
"hydra:property": "vvo:Meldung"
}
]
},
{
@@ -81,8 +95,12 @@
"@type": "hydra:Class",
"hydra:title": "Antwort der Berechnung",
"hydra:supportedProperty": [
{ "hydra:property": "vvo:ProdElement" },
{ "hydra:property": "vvo:Meldung" }
{
"hydra:property": "vvo:ProdElement"
},
{
"hydra:property": "vvo:Meldung"
}
]
},
{
@@ -91,12 +109,30 @@
"hydra:title": "VVO Produkt-Element",
"hydra:description": "Ein Element innerhalb der Versicherungsstruktur (Baustein, Elementarprodukt etc.)",
"hydra:supportedProperty": [
{ "hydra:property": "vvo:bez", "hydra:title": "Bezeichnung" },
{ "hydra:property": "vvo:value", "hydra:title": "Wert" },
{ "hydra:property": "vvo:minOccurrence", "hydra:title": "Minimale Häufigkeit" },
{ "hydra:property": "vvo:maxOccurrence", "hydra:title": "Maximale Häufigkeit" },
{ "hydra:property": "vvo:Baustein", "hydra:title": "Referenz auf Unterbausteine" },
{ "hydra:property": "vvo:Parent", "hydra:title": "Referenz auf Elternelement" }
{
"hydra:property": "vvo:bez",
"hydra:title": "Bezeichnung"
},
{
"hydra:property": "vvo:value",
"hydra:title": "Wert"
},
{
"hydra:property": "vvo:minOccurrence",
"hydra:title": "Minimale Häufigkeit"
},
{
"hydra:property": "vvo:maxOccurrence",
"hydra:title": "Maximale Häufigkeit"
},
{
"hydra:property": "vvo:Baustein",
"hydra:title": "Referenz auf Unterbausteine"
},
{
"hydra:property": "vvo:Parent",
"hydra:title": "Referenz auf Elternelement"
}
]
},
{
@@ -104,11 +140,15 @@
"@type": "hydra:Class",
"hydra:title": "System-Meldung",
"hydra:supportedProperty": [
{ "hydra:property": "vvo:errorMsg", "hydra:title": "Fehlermeldung Text" },
{ "hydra:property": "vvo:errorType", "hydra:title": "Fehlertyp Code" }
{
"hydra:property": "vvo:errorMsg",
"hydra:title": "Fehlermeldung Text"
},
{
"hydra:property": "vvo:errorType",
"hydra:title": "Fehlertyp Code"
}
]
}
//To-Do: Genauer ausformulieren
]
}

73
client-web/LICENSE Normal file
View File

@@ -0,0 +1,73 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright 2025 KapDionOS
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,35 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"web-frontend": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"assets": [],
"styles": [],
"scripts": []
},
"configurations": {
"production": {
"budgets": [],
"outputHashing": "all"
}
}
}
}
}
}
}

26
client-web/nginx.conf Normal file
View File

@@ -0,0 +1,26 @@
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /produktApi/ProductsRequest {
proxy_pass http://192.168.2.186:9090/produktwissen-app/produktApi/ProductsRequest;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /produktApi/CalculateRequest {
proxy_pass http://192.168.2.186:9090/produktwissen-app/produktApi/CalculateRequest;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@@ -1,29 +0,0 @@
{
"name": "web-frontend",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build --configuration production",
"watch": "ng build --watch --configuration development"
},
"dependencies": {
"@angular/animations": "^17.0.0",
"@angular/common": "^17.0.0",
"@angular/compiler": "^17.0.0",
"@angular/core": "^17.0.0",
"@angular/forms": "^17.0.0",
"@angular/platform-browser": "^17.0.0",
"@angular/platform-browser-dynamic": "^17.0.0",
"@angular/router": "^17.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.0.0",
"@angular/cli": "^17.0.0",
"@angular/compiler-cli": "^17.0.0",
"typescript": "~5.2.0"
}
}

View File

@@ -8,35 +8,45 @@
<artifactId>productknowledge-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>client-web</artifactId>
<packaging>pom</packaging>
<build>
<plugins>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.12.1</version>
<executions>
<execution>
<id>install node and npm</id>
<goals><goal>install-node-and-npm</goal></goals>
<configuration>
<nodeVersion>v18.16.0</nodeVersion>
</configuration>
</execution>
<execution>
<id>npm install</id>
<goals><goal>npm</goal></goals>
</execution>
<execution>
<id>npm run build</id>
<goals><goal>npm</goal></goals>
<configuration>
<arguments>run build</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<plugins>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.15.0</version>
<executions>
<execution>
<id>install node and npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
<configuration>
<nodeVersion>v22.12.0</nodeVersion> </configuration>
</execution>
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>install</arguments>
</configuration>
</execution>
<execution>
<id>npm build</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run build --configuration=production</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

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 */

View File

@@ -1,9 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./dist/out-tsc",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
}

View File

@@ -1,29 +0,0 @@
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false,
"lib": ["ES2022", "dom"]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

8
docker-compose.yaml Normal file
View File

@@ -0,0 +1,8 @@
services:
productdefinitions-development-service:
build:
dockerfile: Dockerfile
container_name: productdefinitions_development_container
ports:
- "9080:9090"
restart: always

View File

@@ -26,14 +26,14 @@
<version>${project.version}</version>
</dependency>
<!-- warte auf web-frontend, damit die gebauten Seiten eingebettet werden können -->
<dependency>
<groupId>com.kapdion.pisano</groupId>
<artifactId>client-web</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>provided</scope>
</dependency>
<!-- &lt;!&ndash; warte auf web-frontend, damit die gebauten Seiten eingebettet werden können &ndash;&gt;-->
<!-- <dependency>-->
<!-- <groupId>com.kapdion.pisano</groupId>-->
<!-- <artifactId>client-web</artifactId>-->
<!-- <version>${project.version}</version>-->
<!-- <type>pom</type>-->
<!-- <scope>provided</scope>-->
<!-- </dependency>-->
<dependency>
<groupId>com.kapdion.pisano</groupId>
@@ -112,6 +112,26 @@
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<!-- Resource-plugin soll die gebauten Angular-Seiten in den Build-Output einbetten -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
@@ -133,19 +153,6 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

View File

@@ -7,8 +7,8 @@ import com.kapdion.omds.productdefinitions.calculate.CalculateService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.xml.datatype.DatatypeConfigurationException;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
@Slf4j
@RestController
@@ -16,13 +16,14 @@ import java.io.IOException;
public class EndpointsZentralesBOA {
@PostMapping("/ProductsRequest")
public String apriori(@RequestBody ProductsRequest productsRequest){
public HydraResponse<List<Object>> apriori(@RequestBody ProductsRequest productsRequest){
AprioriService as = new AprioriService();
String s = as.getProductsResponse(productsRequest);
List<Object> s = Collections.singletonList(as.getProductsResponse(productsRequest));
HydraResponse<List<Object>> r = new HydraResponse<>(s);
System.out.println("-----------------------");
System.out.println("Products request: " + s);
System.out.println("Products request: " + r.data.toString());
return s;
return r;
};
@PostMapping("/CalculateRequest")

View File

@@ -0,0 +1,30 @@
package com.kapdion.omds.productdefinitions;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@CrossOrigin(origins = "*", exposedHeaders = "Link")
public class EntryPointController {
@GetMapping(value = "/", produces = "application/ld+json")
public ResponseEntity<String> getEntryPoint() {
String body = """
{
"@context": "http://localhost:9090/produktwissen-app/produktApi/context.jsonld",
"@id": "http://localhost:9090/produktwissen-app/produktApi/",
"@type": "hydra:EntryPoint"
}
""";
return ResponseEntity.ok()
.header(
"Link",
"<http://localhost:9090/produktwissen-app/produktApi/vocab.jsonld>; rel=\"http://www.w3.org/ns/hydra/core#apiDocumentation\""
)
.body(body);
}
}

View File

@@ -0,0 +1,23 @@
package com.kapdion.omds.productdefinitions;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/produktApi") // Dein Pfad
public class HydraController {
@GetMapping(value = "/vocab", produces = "application/ld+json")
public Resource getVocab() {
// Lädt die Datei aus dem api-definition Modul
return new ClassPathResource("vocab.jsonld");
}
@GetMapping(value = "/context", produces = "application/ld+json")
public Resource getContext() {
return new ClassPathResource("context.jsonld");
}
}

View File

@@ -0,0 +1,27 @@
package com.kapdion.omds.productdefinitions;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@CrossOrigin(origins = "*") // UNBEDINGT HIER AUCH HINZUFÜGEN
public class HydraDocController {
// Lädt die Datei aus dem Classpath (kommt aus dem api-definition Modul)
private Resource loadFile(String name) {
return new ClassPathResource(name);
}
@GetMapping(value = "/context.jsonld", produces = "application/ld+json")
public Resource getContext() {
return loadFile("context.jsonld");
}
@GetMapping(value = "/vocab.jsonld", produces = "application/ld+json")
public Resource getVocab() {
return loadFile("vocab.jsonld");
}
}

View File

@@ -0,0 +1,22 @@
package com.kapdion.omds.productdefinitions;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class HydraHeaderFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
// Der Link zeigt auf dein Vocab-File
String linkHeader = "<http://localhost:9090/produktwissen-app/produktApi/vocab.jsonld>; rel=\"http://www.w3.org/ns/hydra/core#apiDocumentation\"";
httpServletResponse.setHeader("Link", linkHeader);
chain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,18 @@
package com.kapdion.omds.productdefinitions;
import com.fasterxml.jackson.annotation.JsonProperty;
public class HydraResponse<T> {
@JsonProperty("@context")
public String context = "/produktwissen-app/produktApi/context";
@JsonProperty("@type")
public String type = "api:CalculateResponse";
@JsonProperty("@graph")
public T data;
public HydraResponse(T data) {
this.data = data;
}
}

View File

@@ -0,0 +1,16 @@
package com.kapdion.omds.productdefinitions;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.exposedHeaders("Link"); // WICHTIG: Damit die Konsole den Link-Header lesen kann!
}
}

View File

@@ -1,4 +1,6 @@
spring.application.name=productdefinitions
server.port=9090
server.servlet.context-path=/produktApi
server.servlet.context-path=/produktwissen-app/produktApi
logging.level.org.springframework.web=DEBUG
server.tomcat.relaxed-header-chars=:,