Compare commits
26 Commits
master
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
| 14ccf08e07 | |||
| cc2d2ffe6a | |||
| c2f8c414e6 | |||
| 55dd52565c | |||
| 8b6341691c | |||
| be837f760d | |||
| 024a6d1a35 | |||
| e256a30b33 | |||
| 58957b584b | |||
| f960b3b95d | |||
| 28109deabc | |||
| c10c6f5f9f | |||
| d44995fb05 | |||
| 2879d4f0fe | |||
| 7c135b01e0 | |||
| 219e4757a5 | |||
| 1a30692b35 | |||
| f51d2ff515 | |||
| c4dde09a55 | |||
| 0365ae6d5c | |||
| 7920e0c673 | |||
| e8afe6a7e5 | |||
| fe451e4716 | |||
| d3184dfbe5 | |||
| 82ed1e476f | |||
| 695772949f |
16
.gitea/workflows/build-deploy.yaml
Normal file
16
.gitea/workflows/build-deploy.yaml
Normal 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
|
||||
21
.idea/LNKD.tech Editor.xml
generated
21
.idea/LNKD.tech Editor.xml
generated
@@ -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
5
.idea/misc.xml
generated
@@ -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
2
.idea/vcs.xml
generated
@@ -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
21
Dockerfile
Normal 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"]
|
||||
@@ -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#",
|
||||
|
||||
@@ -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
73
client-web/LICENSE
Normal 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.
|
||||
@@ -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
26
client-web/nginx.conf
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
template: '<h1>Hello World from Angular!</h1>',
|
||||
styles: []
|
||||
})
|
||||
export class AppComponent { }
|
||||
15
client-web/src/app/app.config.ts
Normal file
15
client-web/src/app/app.config.ts
Normal 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 }
|
||||
]
|
||||
};
|
||||
71
client-web/src/app/app.css
Normal file
71
client-web/src/app/app.css
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
6
client-web/src/app/app.html
Normal file
6
client-web/src/app/app.html
Normal 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>
|
||||
@@ -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 { }
|
||||
11
client-web/src/app/app.routes.ts
Normal file
11
client-web/src/app/app.routes.ts
Normal 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 },
|
||||
];
|
||||
23
client-web/src/app/app.spec.ts
Normal file
23
client-web/src/app/app.spec.ts
Normal 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
23
client-web/src/app/app.ts
Normal 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');
|
||||
|
||||
|
||||
}
|
||||
99
client-web/src/app/navbar/navbar.css
Normal file
99
client-web/src/app/navbar/navbar.css
Normal 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;
|
||||
}
|
||||
}
|
||||
10
client-web/src/app/navbar/navbar.html
Normal file
10
client-web/src/app/navbar/navbar.html
Normal 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>
|
||||
23
client-web/src/app/navbar/navbar.spec.ts
Normal file
23
client-web/src/app/navbar/navbar.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
16
client-web/src/app/navbar/navbar.ts
Normal file
16
client-web/src/app/navbar/navbar.ts
Normal 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 {
|
||||
|
||||
}
|
||||
184
client-web/src/app/produktbaum/produktbaum.css
Normal file
184
client-web/src/app/produktbaum/produktbaum.css
Normal 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;
|
||||
}
|
||||
99
client-web/src/app/produktbaum/produktbaum.html
Normal file
99
client-web/src/app/produktbaum/produktbaum.html
Normal 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>
|
||||
23
client-web/src/app/produktbaum/produktbaum.spec.ts
Normal file
23
client-web/src/app/produktbaum/produktbaum.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
952
client-web/src/app/produktbaum/produktbaum.ts
Normal file
952
client-web/src/app/produktbaum/produktbaum.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
137
client-web/src/app/risikoobjektView/risikoobjektView.css
Normal file
137
client-web/src/app/risikoobjektView/risikoobjektView.css
Normal 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;
|
||||
}
|
||||
}
|
||||
94
client-web/src/app/risikoobjektView/risikoobjektView.html
Normal file
94
client-web/src/app/risikoobjektView/risikoobjektView.html
Normal 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>
|
||||
23
client-web/src/app/risikoobjektView/risikoobjektView.spec.ts
Normal file
23
client-web/src/app/risikoobjektView/risikoobjektView.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
81
client-web/src/app/risikoobjektView/risikoobjektView.ts
Normal file
81
client-web/src/app/risikoobjektView/risikoobjektView.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
27
client-web/src/app/route-reuse.strategy.ts
Normal file
27
client-web/src/app/route-reuse.strategy.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
27
client-web/src/app/services/risikoobjekt.service.ts
Normal file
27
client-web/src/app/services/risikoobjekt.service.ts
Normal 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([]);
|
||||
}
|
||||
}
|
||||
BIN
client-web/src/assets/logo_kapdion.gif
Normal file
BIN
client-web/src/assets/logo_kapdion.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
|
||||
8
client-web/src/proxy.conf.json
Normal file
8
client-web/src/proxy.conf.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"/produktwissen-app/produktApi": {
|
||||
"target": "http://localhost:9090/",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"logLevel": "debug"
|
||||
}
|
||||
}
|
||||
1
client-web/src/styles.css
Normal file
1
client-web/src/styles.css
Normal file
@@ -0,0 +1 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/out-tsc",
|
||||
"types": []
|
||||
},
|
||||
"files": ["src/main.ts"],
|
||||
"include": ["src/**/*.d.ts"]
|
||||
}
|
||||
@@ -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
8
docker-compose.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
services:
|
||||
productdefinitions-development-service:
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
container_name: productdefinitions_development_container
|
||||
ports:
|
||||
- "9080:9090"
|
||||
restart: always
|
||||
@@ -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>
|
||||
<!-- <!– 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>-->
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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!
|
||||
}
|
||||
}
|
||||
@@ -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=:,
|
||||
|
||||
Reference in New Issue
Block a user