- intermediate commit

This commit is contained in:
Jan 2025-10-12 13:41:37 +02:00
parent 7eaba06e6a
commit 79dea999ad
67 changed files with 1997 additions and 1170 deletions

View file

@ -32,6 +32,7 @@
<mockito.version>5.18.0</mockito.version> <mockito.version>5.18.0</mockito.version>
</properties> </properties>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId> <artifactId>spring-boot-starter-batch</artifactId>

View file

@ -0,0 +1,165 @@
{
"version": 8,
"name": "Custom Style",
"sources": {
"raster-tiles": {
"type": "raster",
"tiles": ["https://atlas.microsoft.com/map/tile?api-version=2.0&tilesetId=microsoft.base.road&zoom={z}&x={x}&y={y}"],
"tileSize": 256,
"attribution": "© Microsoft"
}
},
"sprite": "https://atlas.microsoft.com/map/sprites/atlas?api-version=2.0",
"glyphs": "https://atlas.microsoft.com/map/fonts/{fontstack}/{range}.pbf?api-version=2.0",
"layers": [
{
"id": "background",
"type": "background",
"paint": {
"background-color": "#f8fafc"
}
},
{
"id": "landcover",
"type": "fill",
"source": "vectorTiles",
"source-layer": "Landcover",
"paint": {
"fill-color": "#ffffff",
"fill-opacity": 0.8
}
},
{
"id": "water",
"type": "fill",
"source": "vectorTiles",
"source-layer": "Water",
"paint": {
"fill-color": "#8DB3FE"
}
},
{
"id": "water-outline",
"type": "line",
"source": "vectorTiles",
"source-layer": "Water",
"paint": {
"line-color": "#6B869C",
"line-width": 1
}
},
{
"id": "park",
"type": "fill",
"source": "vectorTiles",
"source-layer": "Landuse",
"filter": ["==", ["get", "class"], "park"],
"paint": {
"fill-color": "#5AF0B4",
"fill-opacity": 0.3
}
},
{
"id": "building",
"type": "fill",
"source": "vectorTiles",
"source-layer": "Building",
"paint": {
"fill-color": "#6b7280",
"fill-opacity": 0.6
}
},
{
"id": "building-outline",
"type": "line",
"source": "vectorTiles",
"source-layer": "Building",
"paint": {
"line-color": "#002F54",
"line-width": 0.5
}
},
{
"id": "road-minor",
"type": "line",
"source": "vectorTiles",
"source-layer": "Road",
"filter": ["in", ["get", "class"], ["literal", ["service", "track"]]],
"paint": {
"line-color": "#ffffff",
"line-width": 2
}
},
{
"id": "road-major",
"type": "line",
"source": "vectorTiles",
"source-layer": "Road",
"filter": ["in", ["get", "class"], ["literal", ["primary", "secondary", "tertiary"]]],
"paint": {
"line-color": "#6B869C",
"line-width": 3
}
},
{
"id": "road-highway",
"type": "line",
"source": "vectorTiles",
"source-layer": "Road",
"filter": ["in", ["get", "class"], ["literal", ["motorway", "trunk"]]],
"paint": {
"line-color": "#BC2B72",
"line-width": 4
}
},
{
"id": "road-label",
"type": "symbol",
"source": "vectorTiles",
"source-layer": "Road",
"layout": {
"text-field": ["get", "name"],
"text-font": ["Poppins"],
"text-size": 12,
"symbol-placement": "line"
},
"paint": {
"text-color": "#002F54",
"text-halo-color": "#ffffff",
"text-halo-width": 2
}
},
{
"id": "place-label",
"type": "symbol",
"source": "vectorTiles",
"source-layer": "Place",
"layout": {
"text-field": ["get", "name"],
"text-font": ["Poppins"],
"text-size": 14
},
"paint": {
"text-color": "#002F54",
"text-halo-color": "#ffffff",
"text-halo-width": 2
}
},
{
"id": "poi-label",
"type": "symbol",
"source": "vectorTiles",
"source-layer": "POI",
"layout": {
"text-field": ["get", "name"],
"text-font": ["Poppins"],
"text-size": 11
},
"paint": {
"text-color": "#5AF0B4",
"text-halo-color": "#002F54",
"text-halo-width": 1.5
}
}
]
}

View file

@ -10,7 +10,9 @@
"dependencies": { "dependencies": {
"@phosphor-icons/vue": "^2.2.1", "@phosphor-icons/vue": "^2.2.1",
"@vueuse/core": "^13.6.0", "@vueuse/core": "^13.6.0",
"azure-maps-control": "^3.6.1",
"chart.js": "^4.5.0", "chart.js": "^4.5.0",
"leaflet": "^1.9.4",
"loglevel": "^1.9.2", "loglevel": "^1.9.2",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"vite-plugin-static-copy": "^3.1.3", "vite-plugin-static-copy": "^3.1.3",
@ -41,6 +43,27 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@azure/msal-browser": {
"version": "2.39.0",
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-2.39.0.tgz",
"integrity": "sha512-kks/n2AJzKUk+DBqZhiD+7zeQGBl+WpSOQYzWy6hff3bU0ZrYFqr4keFLlzB5VKuKZog0X59/FGHb1RPBDZLVg==",
"license": "MIT",
"dependencies": {
"@azure/msal-common": "13.3.3"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-common": {
"version": "13.3.3",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-13.3.3.tgz",
"integrity": "sha512-n278DdCXKeiWhLwhEL7/u9HRMyzhUXLefeajiknf6AmEedoiOiv2r5aRJ7LXdT3NGPyubkdIbthaJlVtmuEqvA==",
"license": "MIT",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.27.1", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@ -957,6 +980,46 @@
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@mapbox/jsonlint-lines-primitives": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
"integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/@mapbox/mapbox-gl-supported": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-2.0.1.tgz",
"integrity": "sha512-HP6XvfNIzfoMVfyGjBckjiAOQK9WfX0ywdLubuPMPv+Vqf5fj0uCbgBQYpiqcWZT6cbyyRnTSXDheT1ugvF6UQ==",
"license": "BSD-3-Clause"
},
"node_modules/@mapbox/unitbezier": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz",
"integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==",
"license": "BSD-2-Clause"
},
"node_modules/@maplibre/maplibre-gl-style-spec": {
"version": "20.4.0",
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.4.0.tgz",
"integrity": "sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==",
"license": "ISC",
"dependencies": {
"@mapbox/jsonlint-lines-primitives": "~2.0.2",
"@mapbox/unitbezier": "^0.0.1",
"json-stringify-pretty-compact": "^4.0.0",
"minimist": "^1.2.8",
"quickselect": "^2.0.0",
"rw": "^1.3.3",
"tinyqueue": "^3.0.0"
},
"bin": {
"gl-style-format": "dist/gl-style-format.mjs",
"gl-style-migrate": "dist/gl-style-migrate.mjs",
"gl-style-validate": "dist/gl-style-validate.mjs"
}
},
"node_modules/@phosphor-icons/vue": { "node_modules/@phosphor-icons/vue": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@phosphor-icons/vue/-/vue-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@phosphor-icons/vue/-/vue-2.2.1.tgz",
@ -1282,6 +1345,12 @@
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/geojson": {
"version": "7946.0.16",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
"license": "MIT"
},
"node_modules/@types/web-bluetooth": { "node_modules/@types/web-bluetooth": {
"version": "0.0.21", "version": "0.0.21",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
@ -1627,6 +1696,18 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/azure-maps-control": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/azure-maps-control/-/azure-maps-control-3.6.1.tgz",
"integrity": "sha512-EqJ96GOjUcCG9XizUbyqDu92x3KKT9C9AwRL3hmPicQjn00ql7em6RbBqJYO4nvIoH53DG6MOITj9t/zv1mQYg==",
"license": "SEE LICENSE.TXT",
"dependencies": {
"@azure/msal-browser": "^2.32.1",
"@mapbox/mapbox-gl-supported": "^2.0.1",
"@maplibre/maplibre-gl-style-spec": "^20.0.0",
"@types/geojson": "^7946.0.14"
}
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -2290,6 +2371,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/json-stringify-pretty-compact": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz",
"integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==",
"license": "MIT"
},
"node_modules/json5": { "node_modules/json5": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@ -2322,6 +2409,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/leaflet": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause"
},
"node_modules/loglevel": { "node_modules/loglevel": {
"version": "1.9.2", "version": "1.9.2",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
@ -2354,6 +2447,15 @@
"@jridgewell/sourcemap-codec": "^1.5.5" "@jridgewell/sourcemap-codec": "^1.5.5"
} }
}, },
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mitt": { "node_modules/mitt": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
@ -2598,6 +2700,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/quickselect": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz",
"integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==",
"license": "ISC"
},
"node_modules/readdirp": { "node_modules/readdirp": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@ -2681,6 +2789,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/rw": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
"license": "BSD-3-Clause"
},
"node_modules/semver": { "node_modules/semver": {
"version": "6.3.1", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@ -2801,6 +2915,12 @@
"url": "https://github.com/sponsors/SuperchupuDev" "url": "https://github.com/sponsors/SuperchupuDev"
} }
}, },
"node_modules/tinyqueue": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
"license": "ISC"
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",

View file

@ -14,7 +14,9 @@
"dependencies": { "dependencies": {
"@phosphor-icons/vue": "^2.2.1", "@phosphor-icons/vue": "^2.2.1",
"@vueuse/core": "^13.6.0", "@vueuse/core": "^13.6.0",
"azure-maps-control": "^3.6.1",
"chart.js": "^4.5.0", "chart.js": "^4.5.0",
"leaflet": "^1.9.4",
"loglevel": "^1.9.2", "loglevel": "^1.9.2",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"vite-plugin-static-copy": "^3.1.3", "vite-plugin-static-copy": "^3.1.3",

View file

@ -16,6 +16,7 @@ import ErrorNotification from "@/components/UI/ErrorNotifcation.vue";
export default { export default {
components: {ErrorNotification, TheHeader}, components: {ErrorNotification, TheHeader},
} }
</script> </script>

View file

@ -123,7 +123,6 @@ const executeRequest = async (requestingStore, request) => {
throw e; throw e;
}); });
let data = null; let data = null;
if (request.expectResponse) { if (request.expectResponse) {
try { try {

View file

@ -1,346 +0,0 @@
<template>
<div ref="mapContainer" class="map-container"></div>
</template>
<script>
import { config } from '@/config';
// Annahme: Sie haben bereits MSAL oder ähnliche Entra ID Integration
// import { msalInstance } from '@/auth/msal'; // Ihr MSAL Setup
export default {
name: 'AzureMapsComponent',
props: {
coordinates: {
type: Object,
required: true,
validator(value) {
return value &&
typeof value.latitude === 'number' &&
typeof value.longitude === 'number';
}
},
draggable: {
type: Boolean,
default: false
}
},
data() {
return {
map: null,
marker: null,
datasource: null,
azureMapsClientId: null // Wird vom Backend geholt
};
},
mounted() {
this.initializeMap();
},
beforeUnmount() {
if (this.map) {
this.map.dispose();
}
},
watch: {
coordinates: {
handler(newCoords) {
if (this.marker && this.map) {
this.updateMarkerPosition(newCoords);
}
},
deep: true
},
draggable(newValue) {
if (this.marker) {
this.updateMarkerDraggable(newValue);
}
}
},
methods: {
async initializeMap() {
try {
// Azure Maps SDK laden
if (!window.atlas) {
await this.loadAzureMapsSDK();
}
// Azure Maps Client ID vom Backend abrufen
await this.fetchAzureMapsConfig();
// Karte mit Entra ID-Authentifizierung initialisieren
this.map = new window.atlas.Map(this.$refs.mapContainer, {
center: [this.coordinates.longitude, this.coordinates.latitude],
zoom: 15,
authOptions: {
authType: 'aad',
clientId: this.azureMapsClientId,
aadAppId: this.azureMapsClientId,
aadTenant: 'common', // oder Ihre spezifische Tenant ID
getToken: this.getEntraIdTokenForMaps
}
});
// Warten bis die Karte geladen ist
this.map.events.add('ready', () => {
this.setupMap();
});
} catch (error) {
console.error('Fehler beim Initialisieren der Karte:', error);
this.$emit('error', error);
}
},
/**
* Azure Maps Konfiguration vom Backend abrufen
*/
async fetchAzureMapsConfig() {
try {
const response = await fetch(`${config.backendUrl}/maps/config`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${await this.getCurrentUserToken()}`
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const configData = await response.json();
this.azureMapsClientId = configData.clientId;
console.debug('Azure Maps Konfiguration erfolgreich abgerufen');
} catch (error) {
console.error('Fehler beim Abrufen der Azure Maps Konfiguration:', error);
throw new Error('Konnte Azure Maps Konfiguration nicht abrufen');
}
},
/**
* Entra ID Token für Azure Maps abrufen
*/
async getEntraIdTokenForMaps() {
try {
// Azure Maps spezifischen Scope verwenden
const tokenRequest = {
scopes: ['https://atlas.microsoft.com/user_impersonation'],
account: msalInstance.getActiveAccount()
};
const response = await msalInstance.acquireTokenSilent(tokenRequest);
return response.accessToken;
} catch (error) {
console.error('Fehler beim Abrufen des Azure Maps Tokens:', error);
// Fallback: Interactive Token Acquisition
try {
const response = await msalInstance.acquireTokenPopup(tokenRequest);
return response.accessToken;
} catch (popupError) {
console.error('Interactive Token-Abruf fehlgeschlagen:', popupError);
throw new Error('Konnte kein Token für Azure Maps abrufen');
}
}
},
/**
* Aktuelles Benutzer-Token für Backend-Aufrufe
*/
async getCurrentUserToken() {
try {
const account = msalInstance.getActiveAccount();
if (!account) {
throw new Error('Kein aktiver Benutzer gefunden');
}
const tokenRequest = {
scopes: [`${config.backendUrl}/.default`], // Backend API Scope
account: account
};
const response = await msalInstance.acquireTokenSilent(tokenRequest);
return response.accessToken;
} catch (error) {
console.error('Fehler beim Abrufen des Backend-Tokens:', error);
throw new Error('Konnte Backend-Token nicht abrufen');
}
},
loadAzureMapsSDK() {
return new Promise((resolve, reject) => {
if (window.atlas) {
resolve();
return;
}
// CSS laden
const linkElement = document.createElement('link');
linkElement.rel = 'stylesheet';
linkElement.href = 'https://atlas.microsoft.com/sdk/javascript/mapcontrol/2/atlas.min.css';
document.head.appendChild(linkElement);
// JavaScript SDK laden
const scriptElement = document.createElement('script');
scriptElement.src = 'https://atlas.microsoft.com/sdk/javascript/mapcontrol/2/atlas.min.js';
scriptElement.onload = resolve;
scriptElement.onerror = reject;
document.head.appendChild(scriptElement);
});
},
setupMap() {
// Datenquelle für den Marker erstellen
this.datasource = new window.atlas.source.DataSource();
this.map.sources.add(this.datasource);
// Symbol-Layer für den Marker hinzufügen
this.map.layers.add(new window.atlas.layer.SymbolLayer(this.datasource, null, {
iconOptions: {
allowOverlap: true,
ignorePlacement: true
}
}));
// Marker erstellen
this.createMarker();
// Event-Listener für draggable Funktionalität
if (this.draggable) {
this.enableMarkerDrag();
}
},
createMarker() {
const point = new window.atlas.data.Point([
this.coordinates.longitude,
this.coordinates.latitude
]);
this.marker = new window.atlas.data.Feature(point, {
id: 'marker',
draggable: this.draggable
});
this.datasource.add(this.marker);
},
updateMarkerPosition(coords) {
if (this.marker && this.datasource) {
this.marker.geometry.coordinates = [coords.longitude, coords.latitude];
this.datasource.setShapes([this.marker]);
this.map.setCamera({
center: [coords.longitude, coords.latitude]
});
}
},
updateMarkerDraggable(isDraggable) {
if (isDraggable) {
this.enableMarkerDrag();
} else {
this.disableMarkerDrag();
}
},
enableMarkerDrag() {
this.map.events.add('mousedown', this.datasource, this.onMarkerMouseDown);
this.map.events.add('mousemove', this.onMarkerMouseMove);
this.map.events.add('mouseup', this.onMarkerMouseUp);
this.map.events.add('touchstart', this.datasource, this.onMarkerTouchStart);
this.map.events.add('touchmove', this.onMarkerTouchMove);
this.map.events.add('touchend', this.onMarkerTouchEnd);
this.map.getCanvasContainer().style.cursor = 'pointer';
},
disableMarkerDrag() {
this.map.events.remove('mousedown', this.datasource, this.onMarkerMouseDown);
this.map.events.remove('mousemove', this.onMarkerMouseMove);
this.map.events.remove('mouseup', this.onMarkerMouseUp);
this.map.events.remove('touchstart', this.datasource, this.onMarkerTouchStart);
this.map.events.remove('touchmove', this.onMarkerTouchMove);
this.map.events.remove('touchend', this.onMarkerTouchEnd);
this.map.getCanvasContainer().style.cursor = 'default';
},
onMarkerMouseDown(e) {
if (e.shapes && e.shapes.length > 0) {
this.isDragging = true;
this.map.getCanvasContainer().style.cursor = 'grabbing';
}
},
onMarkerMouseMove(e) {
if (this.isDragging) {
this.marker.geometry.coordinates = [e.position[0], e.position[1]];
this.datasource.setShapes([this.marker]);
this.$emit('coordinates-changed', {
latitude: e.position[1],
longitude: e.position[0]
});
}
},
onMarkerMouseUp() {
if (this.isDragging) {
this.isDragging = false;
this.map.getCanvasContainer().style.cursor = 'pointer';
}
},
onMarkerTouchStart(e) {
if (e.shapes && e.shapes.length > 0) {
this.isDragging = true;
}
},
onMarkerTouchMove(e) {
if (this.isDragging && e.position) {
this.marker.geometry.coordinates = [e.position[0], e.position[1]];
this.datasource.setShapes([this.marker]);
this.$emit('coordinates-changed', {
latitude: e.position[1],
longitude: e.position[0]
});
}
},
onMarkerTouchEnd() {
this.isDragging = false;
},
// Öffentliche Methoden
setCenter(coords) {
if (this.map) {
this.map.setCamera({
center: [coords.longitude, coords.latitude]
});
}
},
setZoom(zoom) {
if (this.map) {
this.map.setCamera({ zoom });
}
}
}
};
</script>
<style scoped>
.map-container {
width: 100%;
height: 250px;
min-height: 150px;
}
</style>

View file

@ -0,0 +1,191 @@
<template>
<div ref="mapContainer" class="map-container"></div>
</template>
<script>
export default {
name: 'OpenStreetMapEmbed',
props: {
coordinates: {
type: Object,
default: () => ({ latitude: 51.1657, longitude: 10.4515 })
},
zoom: {
type: Number,
default: 13,
validator: (value) => value >= 1 && value <= 19
},
showMarker: {
type: Boolean,
default: true
},
markerColor: {
type: String,
default: '#002F54'
},
tileStyle: {
type: String,
default: 'standard',
validator: (value) => ['standard', 'dark', 'humanitarian', 'topo'].includes(value)
},
customFilter: {
type: String,
default: ''
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '450px'
}
},
data() {
return {
map: null,
marker: null
}
},
computed: {
tileLayerUrl() {
const styles = {
standard: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
dark: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
humanitarian: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
topo: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png'
};
return styles[this.tileStyle] || styles.standard;
},
tileAttribution() {
return '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
}
},
mounted() {
this.initMap();
},
beforeUnmount() {
if (this.map) {
this.map.remove();
}
},
watch: {
coordinates: {
handler(newCoords) {
if (this.map) {
const { latitude, longitude } = newCoords;
this.map.setView([latitude, longitude], this.zoom);
if (this.marker) {
this.marker.setLatLng([latitude, longitude]);
}
}
},
deep: true
},
zoom(newZoom) {
if (this.map) {
this.map.setZoom(newZoom);
}
},
showMarker(show) {
if (this.map) {
if (show && !this.marker) {
this.addMarker();
} else if (!show && this.marker) {
this.map.removeLayer(this.marker);
this.marker = null;
}
}
},
tileStyle() {
if (this.map) {
this.updateTileLayer();
}
}
},
methods: {
async initMap() {
// Dynamically import Leaflet to avoid SSR issues
const L = await import('leaflet');
await import('leaflet/dist/leaflet.css');
const { latitude, longitude } = this.coordinates;
this.map = L.map(this.$refs.mapContainer, {
center: [latitude, longitude],
zoom: this.zoom,
zoomControl: true,
scrollWheelZoom: true
});
this.updateTileLayer();
if (this.showMarker) {
this.addMarker();
}
},
async updateTileLayer() {
const L = await import('leaflet');
// Remove existing tile layers
this.map.eachLayer((layer) => {
if (layer instanceof L.TileLayer) {
this.map.removeLayer(layer);
}
});
// Add new tile layer
L.tileLayer(this.tileLayerUrl, {
attribution: this.tileAttribution,
maxZoom: 19
}).addTo(this.map);
},
async addMarker() {
const L = await import('leaflet');
const { latitude, longitude } = this.coordinates;
// Custom marker icon with color
const markerHtml = `
<svg width="25" height="41" viewBox="0 0 25 41" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5 0C5.596 0 0 5.596 0 12.5c0 9.375 12.5 28.125 12.5 28.125S25 21.875 25 12.5C25 5.596 19.404 0 12.5 0z"
fill="${this.markerColor}" stroke="#fff" stroke-width="2"/>
<circle cx="12.5" cy="12.5" r="4" fill="#fff"/>
</svg>
`;
const customIcon = L.divIcon({
html: markerHtml,
className: 'custom-marker',
iconSize: [25, 41],
iconAnchor: [12.5, 41]
});
this.marker = L.marker([latitude, longitude], { icon: customIcon }).addTo(this.map);
}
}
}
</script>
<style scoped>
.map-container {
width: v-bind(width);
height: v-bind(height);
position: relative;
z-index: 0;
}
.map-container :deep(.leaflet-tile-pane) {
filter: v-bind(customFilter);
}
:deep(.custom-marker) {
background: transparent;
border: none;
}
:deep(.leaflet-control-attribution) {
font-size: 10px;
background: rgba(255, 255, 255, 0.8);
}
</style>

View file

@ -16,7 +16,7 @@
<div class="input-field">{{ coordinatesDMS }}</div> <div class="input-field">{{ coordinatesDMS }}</div>
</div> </div>
<div class="supplier-map"> <div class="supplier-map">
<azure-maps-component :draggable="true" :coordinates="supplierCoordinates"></azure-maps-component> <open-street-map-embed :coordinates="supplierCoordinates" zoom="15" width="600px" height="300px" custom-filter="grayscale(0.8) sepia(0.5) hue-rotate(180deg) saturate(0.5) brightness(1.0)"></open-street-map-embed>
</div> </div>
<div class="footer"> <div class="footer">
<modal :state="selectSupplierModalState" @close="closeEditModal"> <modal :state="selectSupplierModalState" @close="closeEditModal">
@ -37,11 +37,11 @@ import {PhUser} from "@phosphor-icons/vue";
import ModalDialog from "@/components/UI/ModalDialog.vue"; import ModalDialog from "@/components/UI/ModalDialog.vue";
import SelectNode from "@/components/layout/node/SelectNode.vue"; import SelectNode from "@/components/layout/node/SelectNode.vue";
import Modal from "@/components/UI/Modal.vue"; import Modal from "@/components/UI/Modal.vue";
import AzureMapsComponent from "@/components/UI/AzureMapsComponent.vue"; import OpenStreetMapEmbed from "@/components/UI/OpenStreetMapEmbed.vue";
export default { export default {
name: "SupplierView", name: "SupplierView",
components: {AzureMapsComponent, Modal, SelectNode, ModalDialog, PhUser, Flag, InputField, IconButton}, components: { OpenStreetMapEmbed, Modal, SelectNode, ModalDialog, PhUser, Flag, InputField, IconButton},
emits: ['updateSupplier'], emits: ['updateSupplier'],
props: { props: {
supplierName: { supplierName: {
@ -140,6 +140,8 @@ export default {
.supplier-map { .supplier-map {
display: flex; display: flex;
justify-content: center; justify-content: center;
flex: 1;
padding: 1.6rem;
} }
.user-icon { .user-icon {
@ -177,7 +179,9 @@ export default {
.container { .container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 1.6rem; justify-content: space-around;
align-items: center;
gap: 3.6rem;
flex: 1 1 auto; flex: 1 1 auto;
} }

View file

@ -1,7 +1,7 @@
import log from 'loglevel' import log from 'loglevel'
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
log.setLevel('silent') log.setLevel('debug') //TODO change back to 'silent'
} else { } else {
log.setLevel('debug') log.setLevel('debug')
} }

View file

@ -5,7 +5,6 @@ import Config from "@/pages/Config.vue";
import CalculationSingleEdit from "@/pages/CalculationSingleEdit.vue"; import CalculationSingleEdit from "@/pages/CalculationSingleEdit.vue";
import CalculationMassEdit from "@/pages/CalculationMassEdit.vue"; import CalculationMassEdit from "@/pages/CalculationMassEdit.vue";
import CalculationAssistant from "@/pages/CalculationAssistant.vue"; import CalculationAssistant from "@/pages/CalculationAssistant.vue";
import ErrorLog from "@/pages/ErrorLog.vue";
import CalculationDump from "@/components/layout/dev/CalculationDump.vue"; import CalculationDump from "@/components/layout/dev/CalculationDump.vue";
import DevPage from "@/pages/DevPage.vue"; import DevPage from "@/pages/DevPage.vue";
import {useActiveUserStore} from "@/store/activeuser.js"; import {useActiveUserStore} from "@/store/activeuser.js";
@ -139,5 +138,4 @@ const router = createRouter({
] ]
}) })
export default router; export default router;

View file

@ -21,8 +21,9 @@ export const useActiveUserStore = defineStore('activeUser', {
return state.user.groups?.includes("super") || state.user.groups?.includes("freight") || state.user.groups?.includes("packaging"); return state.user.groups?.includes("super") || state.user.groups?.includes("freight") || state.user.groups?.includes("packaging");
}, },
allowReporting(state) { allowReporting(state) {
return state.user !== null; if (state.user === null)
return false;
return state.user.groups?.includes("super") || state.user.groups?.includes("freight") || state.user.groups?.includes("packaging") || state.user.groups?.includes("basic") || state.user.groups?.includes("calculation");
}, },
isSuper(state) { isSuper(state) {
if (state.user === null) if (state.user === null)

View file

@ -1,6 +1,7 @@
import {defineStore} from 'pinia' import {defineStore} from 'pinia'
import {config} from '@/config' import {config} from '@/config'
import {useErrorStore} from "@/store/error.js"; import {useErrorStore} from "@/store/error.js";
import performRequest from "@/backend.js";
export const useAssistantStore = defineStore('assistant', { export const useAssistantStore = defineStore('assistant', {
@ -27,71 +28,16 @@ export const useAssistantStore = defineStore('assistant', {
const materialIds = this.materials.map((material) => material.id); const materialIds = this.materials.map((material) => material.id);
const supplierIds = this.suppliers.filter(s => s.id.startsWith('s')).map((supplier) => supplier.origId); const supplierIds = this.suppliers.filter(s => s.id.startsWith('s')).map((supplier) => supplier.origId);
const userSupplierIds = this.suppliers.filter(s => s.id.startsWith('u')).map((supplier) => supplier.origId); const userSupplierIds = this.suppliers.filter(s => s.id.startsWith('u')).map((supplier) => supplier.origId);
const jsonBody = JSON.stringify({ const body = {
material: materialIds, material: materialIds,
supplier: supplierIds, supplier: supplierIds,
user_supplier: userSupplierIds, user_supplier: userSupplierIds,
from_scratch: this.createEmpty from_scratch: this.createEmpty
});
console.log(`Creation body: ${jsonBody}`);
const params = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: jsonBody
}; };
const request = {url: url, params: params}; const resp = await performRequest(this, 'POST', `${config.backendUrl}/calculation/create/`, body, true);
const response = await fetch(url, params).catch(e => { return resp.data.map(p => p.id);
this.error = {code: 'Network error.', message: "Please check your internet connection.", trace: null}
console.error(this.error);
this.loading = false;
console.error(this.error);
const errorStore = useErrorStore();
void errorStore.addError(this.error, {store: this, request: request});
throw e;
});
const data = await response.json().catch(e => {
this.error = {
code: 'Malformed response',
message: "Malformed server response. Please contact support.",
trace: null
}
console.error(this.error);
this.loading = false;
console.error(this.error);
const errorStore = useErrorStore();
void errorStore.addError(this.error, {store: this, request: request});
throw e;
});
if (!response.ok) {
this.error = {
code: data.error.code,
title: data.error.title,
message: data.error.message,
trace: data.error.trace
};
this.loading = false;
console.error(this.error);
const errorStore = useErrorStore();
void errorStore.addError(this.error, {store: this, request: request});
return;
}
return data.map(p => p.id);
}, },

View file

@ -3,6 +3,7 @@ import {config} from '@/config'
import {useErrorStore} from "@/store/error.js"; import {useErrorStore} from "@/store/error.js";
import {useStageStore} from "@/store/stage.js"; import {useStageStore} from "@/store/stage.js";
import {usePropertySetsStore} from "@/store/propertySets.js"; import {usePropertySetsStore} from "@/store/propertySets.js";
import performRequest from "@/backend.js";
export const useCountryStore = defineStore('country', { export const useCountryStore = defineStore('country', {
state() { state() {
@ -35,7 +36,7 @@ export const useCountryStore = defineStore('country', {
const url = `${config.backendUrl}/properties/country/${this.getSelectedCountry.iso_code}/${property.id}`; const url = `${config.backendUrl}/properties/country/${this.getSelectedCountry.iso_code}/${property.id}`;
const body = { value: String(property.value)}; const body = { value: String(property.value)};
await this.performRequest('PUT', url, body, false); await performRequest(this,'PUT', url, body, false);
prop.draft_value = property.reset ? null : property.value; prop.draft_value = property.reset ? null : property.value;
@ -62,8 +63,8 @@ export const useCountryStore = defineStore('country', {
params.append('property_set', this.selectedPeriodId); params.append('property_set', this.selectedPeriodId);
const url = `${config.backendUrl}/countries/${this.selectedCountryId}/${params.size === 0 ? '' : '?'}${params.toString()}`; const url = `${config.backendUrl}/countries/${this.selectedCountryId}/${params.size === 0 ? '' : '?'}${params.toString()}`;
this.countryDetail = await this.performRequest('GET', url, null); const resp = await performRequest(this,'GET', url, null);
this.countryDetail = resp.data;
this.loading = false; this.loading = false;
}, },
async setQuery(query) { async setQuery(query) {
@ -79,101 +80,8 @@ export const useCountryStore = defineStore('country', {
if (this.query !== null) if (this.query !== null)
params.append('filter', this.query); params.append('filter', this.query);
const url = `${config.backendUrl}/countries/all/${params.size === 0 ? '' : '?'}${params.toString()}`; const url = `${config.backendUrl}/countries/all/${params.size === 0 ? '' : '?'}${params.toString()}`;
this.countries = await this.performRequest('GET', url, null); const resp = await performRequest(this, 'GET', url, null);
}, this.countries = resp.data;
async performRequest(method, url, body, expectResponse = true) {
const params = {
method: method,
headers: {
'Content-Type': 'application/json'
}
};
if ((body ?? null) !== null) {
params.body = JSON.stringify(body);
}
const request = {url: url, params: params};
console.log("Request:", request);
const response = await fetch(url, params
).catch(e => {
const error = {
code: 'Network error.',
message: "Please check your internet connection.",
trace: null
}
console.error(error);
const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request});
throw e;
});
let data = null;
if (expectResponse) {
data = await response.json().catch(e => {
const error = {
code: 'Malformed response',
message: "Malformed server response. Please contact support.",
trace: null
}
console.error(error);
const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request});
throw e;
});
if (!response.ok) {
const error = {
code: data.error.code,
title: data.error.title,
message: data.error.message,
trace: data.error.trace
}
console.error(error);
const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request});
throw new Error('Internal backend error');
}
} else {
if (!response.ok) {
const data = await response.json().catch(e => {
const error = {
code: "Return code error " + response.status,
message: "Server returned wrong response code",
trace: null
}
console.error(error);
const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request});
throw new Error('Internal backend error');
});
const error = {
code: data.error.code,
title: data.error.title,
message: data.error.message,
trace: data.error.trace
}
console.error(error);
const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request});
throw new Error('Internal backend error');
}
}
console.log("Response:", data);
return data;
} }
}, },

View file

@ -60,50 +60,8 @@ export const usePremiseStore = defineStore('premise', {
const url = `${config.backendUrl}/calculation/view/${params.size === 0 ? '' : '?'}${params.toString()}`; const url = `${config.backendUrl}/calculation/view/${params.size === 0 ? '' : '?'}${params.toString()}`;
const request = {url: url, params: {method: 'GET'}}; const resp = await performRequest(this, 'GET', url, null, true);
const data = resp.data;
const response = await fetch(url).catch(e => {
this.error = {title: 'Network error.', message: "Please check your internet connection.", trace: null}
this.loading = false;
console.error(this.error);
const errorStore = useErrorStore();
void errorStore.addError(this.error, {store: this, request: request});
throw e;
});
const data = await response.json().catch(e => {
this.error = {
title: 'Malformed response',
message: "Malformed server response. Please contact support.",
trace: null
}
this.loading = false;
console.error(this.error);
const errorStore = useErrorStore();
void errorStore.addError(this.error, {store: this, request: request});
throw e;
});
if (!response.ok) {
this.error = {
code: data.error.code,
title: data.error.title,
message: data.error.message,
trace: data.error.trace
}
this.loading = false;
console.error(this.error);
const errorStore = useErrorStore();
void errorStore.addError(this.error, {store: this, request: request});
console.log(data);
return;
}
this.loading = false; this.loading = false;
this.empty = data.length === 0; this.empty = data.length === 0;

View file

@ -3,6 +3,7 @@ import {config} from '@/config'
import {useErrorStore} from "@/store/error.js"; import {useErrorStore} from "@/store/error.js";
import { useStageStore } from './stage.js' import { useStageStore } from './stage.js'
import {usePropertySetsStore} from "@/store/propertySets.js"; import {usePropertySetsStore} from "@/store/propertySets.js";
import performRequest from "@/backend.js";
export const usePropertiesStore = defineStore('properties', { export const usePropertiesStore = defineStore('properties', {
state() { state() {
@ -44,7 +45,7 @@ export const usePropertiesStore = defineStore('properties', {
const url = `${config.backendUrl}/properties/system/${property.id}`; const url = `${config.backendUrl}/properties/system/${property.id}`;
const body = { value: String(property.value)}; const body = { value: String(property.value)};
await this.performRequest('PUT', url, body, false); await performRequest(this,'PUT', url, body, false);
prop.draft_value = property.reset ? null : property.value; prop.draft_value = property.reset ? null : property.value;
@ -59,104 +60,11 @@ export const usePropertiesStore = defineStore('properties', {
if (period !== null) if (period !== null)
params.append('property_set', period); params.append('property_set', period);
const url = `${config.backendUrl}/properties/${params.size === 0 ? '' : '?'}${params.toString()}`; const url = `${config.backendUrl}/properties/${params.size === 0 ? '' : '?'}${params.toString()}`;
this.properties = await this.performRequest('GET', url, null); const resp = await performRequest(this,'GET', url, null);
this.properties = resp.data;
this.loadedPeriod = period; this.loadedPeriod = period;
this.loading = false; this.loading = false;
},
async performRequest(method, url, body, expectResponse = true) {
const params = {
method: method,
headers: {
'Content-Type': 'application/json'
}
};
if ((body ?? null) !== null) {
params.body = JSON.stringify(body);
}
const request = {url: url, params: params};
console.log("Request:", request);
const response = await fetch(url, params
).catch(e => {
const error = {
code: 'Network error.',
message: "Please check your internet connection.",
trace: null
}
console.error(error);
const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request});
throw e;
});
let data = null;
if (expectResponse) {
data = await response.json().catch(e => {
const error = {
code: 'Malformed response',
message: "Malformed server response. Please contact support.",
trace: null
}
console.error(error);
const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request});
throw e;
});
if (!response.ok) {
const error = {
code: data.error.code,
title: data.error.title,
message: data.error.message,
trace: data.error.trace
}
console.error(error);
const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request});
throw new Error('Internal backend error');
}
} else {
if (!response.ok) {
const data = await response.json().catch(e => {
const error = {
code: "Return code error " + response.status,
message: "Server returned wrong response code",
trace: null
}
console.error(error);
const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request});
throw new Error('Internal backend error');
});
const error = {
code: data.error.code,
title: data.error.title,
message: data.error.message,
trace: data.error.trace
}
console.error(error);
const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request});
throw new Error('Internal backend error');
}
}
console.log("Response:", data);
return data;
} }
} }
}); });

View file

@ -1,9 +1,6 @@
import {defineStore} from 'pinia' import {defineStore} from 'pinia'
import {config} from '@/config' import {config} from '@/config'
import {useErrorStore} from "@/store/error.js"; import performRequest from "@/backend.js";
import {useStageStore} from "@/store/stage.js";
import {usePropertySetsStore} from "@/store/propertySets.js";
import logger from "@/logger.js";
export const useReportSearchStore = defineStore('reportSearch', { export const useReportSearchStore = defineStore('reportSearch', {
state() { state() {
@ -84,107 +81,14 @@ export const useReportSearchStore = defineStore('reportSearch', {
params.append('material', this.getMaterialId); params.append('material', this.getMaterialId);
const url = `${config.backendUrl}/reports/search/${params.size === 0 ? '' : '?'}${params.toString()}`; const url = `${config.backendUrl}/reports/search/${params.size === 0 ? '' : '?'}${params.toString()}`;
this.suppliers = await this.performRequest('GET', url, null).catch(e => { const resp = await performRequest(this,'GET', url, null).catch(e => {
this.loading = false; this.loading = false;
}); });
this.suppliers = resp.data;
this.updateShownSuppliers(); this.updateShownSuppliers();
this.loading = false; this.loading = false;
},
async performRequest(method, url, body, expectResponse = true) {
const params = {
method: method,
headers: {
'Content-Type': 'application/json'
}
};
if (body) {
params.body = JSON.stringify(body);
}
const request = {url: url, params: params};
logger.info("Request:", request);
const response = await fetch(url, params
).catch(e => {
const error = {
code: 'Network error.',
message: "Please check your internet connection.",
trace: null
}
logger.error(error);
const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request});
throw e;
});
let data = null;
if (expectResponse) {
data = await response.json().catch(e => {
const error = {
code: 'Malformed response',
message: "Malformed server response. Please contact support.",
trace: null
}
logger.error(error);
const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request});
throw e;
});
if (!response.ok) {
const error = {
code: data.error.code,
title: data.error.title,
message: data.error.message,
trace: data.error.trace
}
logger.error(error);
const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request});
throw new Error('Internal backend error');
}
} else {
if (!response.ok) {
const data = await response.json().catch(e => {
const error = {
code: "Return code error " + response.status,
message: "Server returned wrong response code",
trace: null
}
logger.error(error);
const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request});
throw new Error('Internal backend error');
});
const error = {
code: data.error.code,
title: data.error.title,
message: data.error.message,
trace: data.error.trace
}
logger.error(error);
const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request});
throw new Error('Internal backend error');
}
}
logger.info("Response:", data);
return data;
} }
}, },

View file

@ -1,6 +1,7 @@
import {defineStore} from 'pinia' import {defineStore} from 'pinia'
import {config} from '@/config' import {config} from '@/config'
import {useErrorStore} from "@/store/error.js"; import {useErrorStore} from "@/store/error.js";
import performRequest from "@/backend.js";
export const useStageStore = defineStore('stage', { export const useStageStore = defineStore('stage', {
state() { state() {
@ -16,106 +17,13 @@ export const useStageStore = defineStore('stage', {
actions: { actions: {
async checkStagedChanges() { async checkStagedChanges() {
const url = `${config.backendUrl}/properties/staged_changes`; const url = `${config.backendUrl}/properties/staged_changes`;
this.stagedChanges = await this.performRequest('GET', url, null); const resp = await performRequest(this,'GET', url, null);
this.stagedChanges = resp.data;
}, },
async applyChanges() { async applyChanges() {
const url = `${config.backendUrl}/properties/staged_changes`; const url = `${config.backendUrl}/properties/staged_changes`;
await this.performRequest('PUT', url, null, false); await performRequest(this, 'PUT', url, null, false);
this.stagedChanges = false; this.stagedChanges = false;
},
async performRequest(method, url, body, expectResponse = true) {
const params = {
method: method,
headers: {
'Content-Type': 'application/json'
}
};
if ((body ?? null) !== null) {
params.body = JSON.stringify(body);
}
const request = {url: url, params: params};
console.log("Request:", request);
const response = await fetch(url, params
).catch(e => {
const error = {
code: 'Network error.',
message: "Please check your internet connection.",
trace: null
}
console.error(error);
const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request});
throw e;
});
let data = null;
if (expectResponse) {
data = await response.json().catch(e => {
const error = {
code: 'Malformed response',
message: "Malformed server response. Please contact support.",
trace: null
}
console.error(error);
const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request});
throw e;
});
if (!response.ok) {
const error = {
code: data.error.code,
title: data.error.title,
message: data.error.message,
trace: data.error.trace
}
console.error(error);
const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request});
throw new Error('Internal backend error');
}
} else {
if (!response.ok) {
const data = await response.json().catch(e => {
const error = {
code: "Return code error " + response.status,
message: "Server returned wrong response code",
trace: null
}
console.error(error);
const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request});
throw new Error('Internal backend error');
});
const error = {
code: data.error.code,
title: data.error.title,
message: data.error.message,
trace: data.error.trace
}
console.error(error);
const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request});
throw new Error('Internal backend error');
}
}
console.log("Response:", data);
return data;
} }
} }

View file

@ -15,7 +15,12 @@ export default defineConfig({
{ {
src: 'assets/flags/*', src: 'assets/flags/*',
dest: 'assets/flags' dest: 'assets/flags'
},
{
src: 'assets/map.json',
dest: 'assets/'
} }
] ]
}) })
], ],

View file

@ -1,39 +0,0 @@
package de.avatic.lcc.config;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import org.springframework.boot.web.client.RestTemplateBuilder;
import java.time.Duration;
/**
* Konfiguration für Azure Maps Integration
*/
@Configuration
@EnableCaching
public class AzureMapsConfig {
/**
* RestTemplate Bean für HTTP-Requests
*/
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.connectTimeout(Duration.ofSeconds(10))
.readTimeout(Duration.ofSeconds(30))
.build();
}
/**
* Cache Manager für Token-Caching
* Tokens werden für 50 Minuten gecacht (Azure Maps Tokens sind 60 Min gültig)
*/
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("azureMapsTokens");
}
}

View file

@ -45,6 +45,9 @@ public class CorsConfig implements WebMvcConfigurer {
.exposedHeaders("X-Total-Count", "X-Page-Count", "X-Current-Page") .exposedHeaders("X-Total-Count", "X-Page-Count", "X-Current-Page")
.allowCredentials(true); .allowCredentials(true);
} else { } else {
System.out.println("Applying PROD CORS configuration");
// Production CORS configuration // Production CORS configuration
registry.addMapping("/api/**") registry.addMapping("/api/**")
.allowedOrigins(allowedCors) .allowedOrigins(allowedCors)

View file

@ -46,13 +46,17 @@ public class DevUserEmulationFilter extends OncePerRequestFilter {
HttpSession session = request.getSession(true); HttpSession session = request.getSession(true);
Integer emulatedUserId = (Integer) session.getAttribute(DEV_USER_ID_SESSION_KEY); Integer emulatedUserId = (Integer) session.getAttribute(DEV_USER_ID_SESSION_KEY);
// Add logging to debug
System.out.println("DevUserEmulationFilter - Session ID: " + session.getId());
System.out.println("DevUserEmulationFilter - Emulated User ID: " + emulatedUserId);
// if(emulatedUserId != null) { if(emulatedUserId != null) {
User user = userRepository.getById(emulatedUserId == null ? 1 : emulatedUserId); User user = userRepository.getById(emulatedUserId);
if (user != null) { if (user != null) {
setEmulatedUser(user); setEmulatedUser(user);
System.out.println("DevUserEmulationFilter - Set user: " + user.getEmail());
}
} }
// }
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} }

View file

@ -1,11 +1,14 @@
package de.avatic.lcc.config; package de.avatic.lcc.config;
import de.avatic.lcc.model.users.Group;
import de.avatic.lcc.model.users.User;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo; import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import java.util.Collection; import java.util.Collection;
import java.util.List;
public class LccOidcUser extends DefaultOidcUser { public class LccOidcUser extends DefaultOidcUser {
@ -20,6 +23,22 @@ public class LccOidcUser extends DefaultOidcUser {
this.userId = userId; this.userId = userId;
} }
public static User createDatabaseUser(String email, String firstName, String lastName, String workdayId) {
User user = new User();
Group group = new Group();
group.setName("none");
user.setEmail(email);
user.setFirstName(firstName == null ? "" : firstName);
user.setLastName(lastName == null ? "" : lastName);
user.setGroups(List.of(group));
user.setWorkdayId(workdayId == null ? "" : workdayId);
user.setActive(false);
return user;
}
public Integer getSqlUserId() { public Integer getSqlUserId() {
return userId; return userId;
} }

View file

@ -0,0 +1,14 @@
package de.avatic.lcc.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

View file

@ -33,16 +33,18 @@ import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.function.Supplier; import java.util.function.Supplier;
@Configuration @Configuration
@EnableMethodSecurity @EnableMethodSecurity
public class SecurityConfig { public class SecurityConfig {
@Bean @Bean
@Profile("!dev & !test") // Only active when NOT in dev profile @Profile("!dev & !test & !dev_id") // Only active when NOT in dev profile
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http http
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
.requestMatchers("/api/**").authenticated() .requestMatchers("/api/**").authenticated()
.requestMatchers("/api/dev/**").denyAll()
.anyRequest().authenticated() .anyRequest().authenticated()
) )
.oauth2Login(oauth2 -> oauth2 .oauth2Login(oauth2 -> oauth2
@ -93,37 +95,43 @@ public class SecurityConfig {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>(oidcUser.getAuthorities()); Set<GrantedAuthority> mappedAuthorities = new HashSet<>(oidcUser.getAuthorities());
User user = null;
String workdayId = oidcUser.getAttribute("workday_id"); String workdayId = oidcUser.getAttribute("workday_id");
if (workdayId != null) {
User user = userRepository.getByWorkdayId(workdayId);
if (user != null) {
user.getGroups().forEach(group -> mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + group.getName())));
userId = user.getId();
}
}
// Try different ways to get email // Try different ways to get email
String email = oidcUser.getEmail(); String email = oidcUser.getEmail();
if (email == null) { if (email == null) {
email = oidcUser.getAttribute("email"); email = oidcUser.getAttribute("email");
} }
if (email == null) {
email = oidcUser.getAttribute("preferred_username");
}
if (email == null) { if (email == null) {
email = oidcUser.getAttribute("upn"); email = oidcUser.getAttribute("upn");
} }
if (email == null) {
email = oidcUser.getAttribute("preferred_username");
}
if (email != null) {
User user = userRepository.getByEmail(email); if (workdayId != null) {
user = userRepository.getByWorkdayId(workdayId);
if (user != null) {
user.getGroups().forEach(group -> mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + group.getName())));
userId = user.getId();
}
} else if (email != null) {
user = userRepository.getByEmail(email);
if (user != null) { if (user != null) {
user.getGroups().forEach(group -> mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + group.getName().toUpperCase()))); user.getGroups().forEach(group -> mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + group.getName().toUpperCase())));
userId = user.getId(); userId = user.getId();
} else {
mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_BASIC"));
} }
} }
if (user == null) {
userRepository.update(LccOidcUser.createDatabaseUser(email, oidcUser.getGivenName(), oidcUser.getFamilyName(), workdayId));
mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_NONE"));
}
return new LccOidcUser( return new LccOidcUser(
mappedAuthorities, mappedAuthorities,
oidcUser.getIdToken(), oidcUser.getIdToken(),
@ -132,9 +140,10 @@ public class SecurityConfig {
userId userId
); );
}; };
}
}
public static final class LccCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler { public static final class LccCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
private final CsrfTokenRequestHandler delegate = new CsrfTokenRequestAttributeHandler(); private final CsrfTokenRequestHandler delegate = new CsrfTokenRequestAttributeHandler();

View file

@ -1,17 +1,16 @@
package de.avatic.lcc.controller.configuration; package de.avatic.lcc.controller.configuration;
import de.avatic.lcc.dto.configuration.nodes.userNodes.LocateNodeDTO;
import de.avatic.lcc.dto.generic.NodeDTO; import de.avatic.lcc.dto.generic.NodeDTO;
import de.avatic.lcc.dto.configuration.nodes.view.NodeDetailDTO; import de.avatic.lcc.dto.configuration.nodes.view.NodeDetailDTO;
import de.avatic.lcc.dto.configuration.nodes.update.NodeUpdateDTO; import de.avatic.lcc.dto.configuration.nodes.update.NodeUpdateDTO;
import de.avatic.lcc.dto.generic.NodeType; import de.avatic.lcc.dto.generic.NodeType;
import de.avatic.lcc.dto.configuration.nodes.userNodes.AddUserNodeDTO; import de.avatic.lcc.dto.configuration.nodes.userNodes.AddUserNodeDTO;
import de.avatic.lcc.model.nodes.Location;
import de.avatic.lcc.repositories.pagination.SearchQueryResult; import de.avatic.lcc.repositories.pagination.SearchQueryResult;
import de.avatic.lcc.service.GeoApiService; import de.avatic.lcc.service.api.GeoApiService;
import de.avatic.lcc.service.access.NodeService; import de.avatic.lcc.service.access.NodeService;
import de.avatic.lcc.service.access.UserNodeService; import de.avatic.lcc.service.access.UserNodeService;
import de.avatic.lcc.util.Check; import de.avatic.lcc.util.Check;
import jakarta.annotation.security.RolesAllowed;
import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Min;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@ -77,7 +76,7 @@ public class NodeController {
@GetMapping("/locate") @GetMapping("/locate")
@PreAuthorize("hasAnyRole('SUPER', 'FREIGHT', 'PACKAGING')") @PreAuthorize("hasAnyRole('SUPER', 'FREIGHT', 'PACKAGING')")
public ResponseEntity<LocateNodeDTO> locateNode(@RequestParam String address) { public ResponseEntity<Location> locateNode(@RequestParam String address) {
return ResponseEntity.ok(geoApiService.locate(address)); return ResponseEntity.ok(geoApiService.locate(address));
} }

View file

@ -1,6 +1,6 @@
package de.avatic.lcc.controller.custom; package de.avatic.lcc.controller.custom;
import de.avatic.lcc.service.CustomApiService; import de.avatic.lcc.service.api.CustomApiService;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;

View file

@ -1,221 +1,48 @@
package de.avatic.lcc.controller.maps; package de.avatic.lcc.controller.maps;
import jakarta.annotation.security.RolesAllowed; import com.azure.core.credential.AccessToken;
import com.azure.identity.DefaultAzureCredentialBuilder;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.slf4j.Logger; import org.springframework.web.bind.annotation.GetMapping;
import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Instant; import java.time.OffsetDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* REST Controller für Azure Maps mit Entra ID Integration
*/
@RestController @RestController
@RequestMapping("/api/maps") @RequestMapping("/api/maps")
@CrossOrigin(origins = {"http://localhost:3000", "http://localhost:8080", "https://yourdomain.com"})
@RolesAllowed({"ROLE_SUPER", "ROLE_CALCULATION"})
public class AzureMapsController { public class AzureMapsController {
private static final Logger logger = LoggerFactory.getLogger(AzureMapsController.class);
@Value("${azure.maps.client.id}") @Value("${azure.maps.client.id}")
private String azureMapsClientId; private String mapsClientId;
@Value("${azure.maps.resource.id:}") @Value("${azure.maps.subscription.key}")
private String azureMapsResourceId; private String mapsSubscriptionKey;
/** @GetMapping("/token")
* Endpoint zum Abrufen der Azure Maps Konfiguration @PreAuthorize("isAuthenticated()")
* Nur authentifizierte Benutzer können diese Konfiguration abrufen public ResponseEntity<Map<String, Object>> getAzureMapsToken() {
*
* @return ConfigResponse mit Client ID und anderen Konfigurationsdaten
*/
@GetMapping("/config")
@PreAuthorize("hasAuthority('SCOPE_https://yourdomain.com/api.read')") // Anpassen an Ihre API-Scopes
public ResponseEntity<?> getConfig() {
try { try {
logger.debug("Providing Azure Maps configuration for authenticated user"); // Verwende die DefaultAzureCredential für die Authentifizierung
var credential = new DefaultAzureCredentialBuilder().build();
if (azureMapsClientId == null || azureMapsClientId.trim().isEmpty()) { // Fordere ein Token für Azure Maps an
logger.error("Azure Maps Client ID is null or empty - check application.properties"); AccessToken token = credential.getToken(
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) new com.azure.core.credential.TokenRequestContext()
.body(new ErrorResponse("Azure Maps Client ID not configured")); .addScopes("https://atlas.microsoft.com/.default")
} ).block();
ConfigResponse response = new ConfigResponse( Map<String, Object> response = new HashMap<>();
azureMapsClientId.trim(), response.put("token", token.getToken());
azureMapsResourceId != null ? azureMapsResourceId.trim() : null response.put("expiresOn", token.getExpiresAt().toEpochSecond());
);
logger.debug("Successfully providing Azure Maps configuration");
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("Error providing Azure Maps configuration: {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("Internal server error: " + e.getMessage()));
}
}
/**
* Health Check Endpoint für Azure Maps Integration
* Öffentlich zugänglich für Monitoring
*
* @return Status der Azure Maps Konfiguration
*/
@GetMapping("/health")
public ResponseEntity<?> healthCheck() {
try {
boolean isConfigured = azureMapsClientId != null && !azureMapsClientId.trim().isEmpty();
if (isConfigured) {
return ResponseEntity.ok(new HealthResponse("Azure Maps Entra ID configuration is valid", true));
} else {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(new HealthResponse("Azure Maps Client ID is not configured", false));
}
} catch (Exception e) {
logger.error("Health check failed", e);
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(new HealthResponse("Azure Maps service is unavailable", false));
}
}
/**
* Endpoint für rollenbasierte Zugriffskontrolle
* Prüft ob der Benutzer Azure Maps verwenden darf
*/
@GetMapping("/permissions")
@PreAuthorize("hasAuthority('SCOPE_https://yourdomain.com/api.read')")
public ResponseEntity<?> checkPermissions() {
try {
// Hier können Sie zusätzliche Berechtigungsprüfungen implementieren
// z.B. basierend auf Benutzerrolle, Gruppe, etc.
PermissionResponse response = new PermissionResponse(
true, // canUseAzureMaps
true, // canCreateMarkers
true // canModifyMarkers
);
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} catch (Exception e) { } catch (Exception e) {
logger.error("Error checking permissions: {}", e.getMessage(), e); return ResponseEntity.internalServerError().build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("Error checking permissions: " + e.getMessage()));
}
}
/**
* Response-Klasse für Konfiguration
*/
public static class ConfigResponse {
private final String clientId;
private final String resourceId;
private final long timestamp;
public ConfigResponse(String clientId, String resourceId) {
this.clientId = clientId;
this.resourceId = resourceId;
this.timestamp = Instant.now().toEpochMilli();
}
public String getClientId() {
return clientId;
}
public String getResourceId() {
return resourceId;
}
public long getTimestamp() {
return timestamp;
}
}
/**
* Response-Klasse für Berechtigungen
*/
public static class PermissionResponse {
private final boolean canUseAzureMaps;
private final boolean canCreateMarkers;
private final boolean canModifyMarkers;
private final long timestamp;
public PermissionResponse(boolean canUseAzureMaps, boolean canCreateMarkers, boolean canModifyMarkers) {
this.canUseAzureMaps = canUseAzureMaps;
this.canCreateMarkers = canCreateMarkers;
this.canModifyMarkers = canModifyMarkers;
this.timestamp = Instant.now().toEpochMilli();
}
public boolean isCanUseAzureMaps() {
return canUseAzureMaps;
}
public boolean isCanCreateMarkers() {
return canCreateMarkers;
}
public boolean isCanModifyMarkers() {
return canModifyMarkers;
}
public long getTimestamp() {
return timestamp;
}
}
/**
* Response-Klasse für Fehler-Antworten
*/
public static class ErrorResponse {
private final String error;
private final long timestamp;
public ErrorResponse(String error) {
this.error = error;
this.timestamp = Instant.now().toEpochMilli();
}
public String getError() {
return error;
}
public long getTimestamp() {
return timestamp;
}
}
/**
* Response-Klasse für Health Check
*/
public static class HealthResponse {
private final String message;
private final boolean healthy;
private final long timestamp;
public HealthResponse(String message, boolean healthy) {
this.message = message;
this.healthy = healthy;
this.timestamp = Instant.now().toEpochMilli();
}
public String getMessage() {
return message;
}
public boolean isHealthy() {
return healthy;
}
public long getTimestamp() {
return timestamp;
} }
} }
} }

View file

@ -0,0 +1,87 @@
package de.avatic.lcc.model.azuremaps;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class Address {
@JsonProperty("addressLine")
private String addressLine;
@JsonProperty("locality")
private String locality;
@JsonProperty("neighborhood")
private String neighborhood;
@JsonProperty("adminDistricts")
private List<AdminDistrict> adminDistricts;
@JsonProperty("postalCode")
private String postalCode;
@JsonProperty("countryRegion")
private CountryRegion countryRegion;
@JsonProperty("formattedAddress")
private String formattedAddress;
public String getAddressLine() {
return addressLine;
}
public void setAddressLine(String addressLine) {
this.addressLine = addressLine;
}
public String getLocality() {
return locality;
}
public void setLocality(String locality) {
this.locality = locality;
}
public String getNeighborhood() {
return neighborhood;
}
public void setNeighborhood(String neighborhood) {
this.neighborhood = neighborhood;
}
public List<AdminDistrict> getAdminDistricts() {
return adminDistricts;
}
public void setAdminDistricts(List<AdminDistrict> adminDistricts) {
this.adminDistricts = adminDistricts;
}
public String getPostalCode() {
return postalCode;
}
public void setPostalCode(String postalCode) {
this.postalCode = postalCode;
}
public CountryRegion getCountryRegion() {
return countryRegion;
}
public void setCountryRegion(CountryRegion countryRegion) {
this.countryRegion = countryRegion;
}
public String getFormattedAddress() {
return formattedAddress;
}
public void setFormattedAddress(String formattedAddress) {
this.formattedAddress = formattedAddress;
}
}

View file

@ -0,0 +1,30 @@
package de.avatic.lcc.model.azuremaps;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
public class AdminDistrict {
@JsonProperty("shortName")
private String shortName;
@JsonProperty("name")
private String name;
public String getShortName() {
return shortName;
}
public void setShortName(String shortName) {
this.shortName = shortName;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View file

@ -0,0 +1,28 @@
package de.avatic.lcc.model.azuremaps;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class BatchGeocodingRequest {
@JsonProperty("batchItems")
private List<BatchItem> batchItems;
public BatchGeocodingRequest() {
}
public BatchGeocodingRequest(List<BatchItem> batchItems) {
this.batchItems = batchItems;
}
public List<BatchItem> getBatchItems() {
return batchItems;
}
public void setBatchItems(List<BatchItem> batchItems) {
this.batchItems = batchItems;
}
}

View file

@ -0,0 +1,35 @@
package de.avatic.lcc.model.azuremaps;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class BatchGeocodingResponse {
@JsonProperty("summary")
private Summary summary;
@JsonProperty("batchItems")
private List<BatchResponseItem> batchItems;
public Summary getSummary() {
return summary;
}
public void setSummary(Summary summary) {
this.summary = summary;
}
public List<BatchResponseItem> getBatchItems() {
return batchItems;
}
public void setBatchItems(List<BatchResponseItem> batchItems) {
this.batchItems = batchItems;
}
}

View file

@ -0,0 +1,44 @@
package de.avatic.lcc.model.azuremaps;
import java.util.ArrayList;
import java.util.List;
public class BatchGeocodingResult {
private final List<GeocodingResult> results;
private final int successfulRequests;
private final int totalRequests;
public BatchGeocodingResult(List<GeocodingResult> results, int successfulRequests, int totalRequests) {
this.results = results;
this.successfulRequests = successfulRequests;
this.totalRequests = totalRequests;
}
public List<GeocodingResult> getResults() {
return results;
}
public int getSuccessfulRequests() {
return successfulRequests;
}
public int getTotalRequests() {
return totalRequests;
}
public int getFailedRequests() {
return totalRequests - successfulRequests;
}
public boolean isFullySuccessful() {
return successfulRequests == totalRequests;
}
public GeocodingResult getResult(int index) {
if (index >= 0 && index < results.size()) {
return results.get(index);
}
return null;
}
}

View file

@ -0,0 +1,56 @@
package de.avatic.lcc.model.azuremaps;
import com.fasterxml.jackson.annotation.JsonProperty;
public class BatchItem {
@JsonProperty("addressLine")
private String addressLine;
@JsonProperty("adminDistrict")
private String adminDistrict;
@JsonProperty("locality")
private String locality;
@JsonProperty("postalCode")
private String postalCode;
public BatchItem() {
}
public BatchItem(String addressLine) {
this.addressLine = addressLine;
}
public String getAddressLine() {
return addressLine;
}
public void setAddressLine(String addressLine) {
this.addressLine = addressLine;
}
public String getAdminDistrict() {
return adminDistrict;
}
public void setAdminDistrict(String adminDistrict) {
this.adminDistrict = adminDistrict;
}
public String getLocality() {
return locality;
}
public void setLocality(String locality) {
this.locality = locality;
}
public String getPostalCode() {
return postalCode;
}
public void setPostalCode(String postalCode) {
this.postalCode = postalCode;
}
}

View file

@ -0,0 +1,29 @@
package de.avatic.lcc.model.azuremaps;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
public class BatchResponseItem {
@JsonProperty("statusCode")
private Integer statusCode;
@JsonProperty("response")
private GeocodingResponse response;
public Integer getStatusCode() {
return statusCode;
}
public void setStatusCode(Integer statusCode) {
this.statusCode = statusCode;
}
public GeocodingResponse getResponse() {
return response;
}
public void setResponse(GeocodingResponse response) {
this.response = response;
}
}

View file

@ -0,0 +1,30 @@
package de.avatic.lcc.model.azuremaps;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
public class CountryRegion {
@JsonProperty("ISO")
private String iso;
@JsonProperty("name")
private String name;
public String getIso() {
return iso;
}
public void setIso(String iso) {
this.iso = iso;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View file

@ -0,0 +1,54 @@
package de.avatic.lcc.model.azuremaps;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class Feature {
@JsonProperty("type")
private String type;
@JsonProperty("properties")
private Properties properties;
@JsonProperty("geometry")
private Geometry geometry;
@JsonProperty("bbox")
private List<Double> bbox;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public Properties getProperties() {
return properties;
}
public void setProperties(Properties properties) {
this.properties = properties;
}
public Geometry getGeometry() {
return geometry;
}
public void setGeometry(Geometry geometry) {
this.geometry = geometry;
}
public List<Double> getBbox() {
return bbox;
}
public void setBbox(List<Double> bbox) {
this.bbox = bbox;
}
}

View file

@ -0,0 +1,43 @@
package de.avatic.lcc.model.azuremaps;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class GeocodePoint {
@JsonProperty("geometry")
private Geometry geometry;
@JsonProperty("calculationMethod")
private String calculationMethod;
@JsonProperty("usageTypes")
private List<String> usageTypes;
public Geometry getGeometry() {
return geometry;
}
public void setGeometry(Geometry geometry) {
this.geometry = geometry;
}
public String getCalculationMethod() {
return calculationMethod;
}
public void setCalculationMethod(String calculationMethod) {
this.calculationMethod = calculationMethod;
}
public List<String> getUsageTypes() {
return usageTypes;
}
public void setUsageTypes(List<String> usageTypes) {
this.usageTypes = usageTypes;
}
}

View file

@ -0,0 +1,32 @@
package de.avatic.lcc.model.azuremaps;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class GeocodingResponse {
@JsonProperty("type")
private String type;
@JsonProperty("features")
private List<Feature> features;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public List<Feature> getFeatures() {
return features;
}
public void setFeatures(List<Feature> features) {
this.features = features;
}
}

View file

@ -0,0 +1,54 @@
package de.avatic.lcc.model.azuremaps;
import de.avatic.lcc.model.nodes.Location;
import java.util.List;
public class GeocodingResult {
private final Feature feature;
public GeocodingResult(Feature feature) {
this.feature = feature;
}
public Location getLocation() {
if (feature.getGeometry() != null &&
feature.getGeometry().getCoordinates() != null &&
feature.getGeometry().getCoordinates().size() >= 2) {
// Azure Maps returns [longitude, latitude]
return new Location(
feature.getGeometry().getCoordinates().get(0),
feature.getGeometry().getCoordinates().get(1)
);
}
return null;
}
public Address getAddress() {
return feature.getProperties() != null ? feature.getProperties().getAddress() : null;
}
public String getConfidence() {
return feature.getProperties() != null ? feature.getProperties().getConfidence() : null;
}
public List<String> getMatchCodes() {
return feature.getProperties() != null ? feature.getProperties().getMatchCodes() : null;
}
public String getGeocodeLevel() {
return feature.getProperties() != null &&
feature.getProperties().getGeocodePoints() != null &&
!feature.getProperties().getGeocodePoints().isEmpty()
? feature.getProperties().getGeocodePoints().getFirst().getCalculationMethod() : null;
}
public List<GeocodePoint> getGeocodePoints() {
return feature.getProperties() != null ? feature.getProperties().getGeocodePoints() : null;
}
public Feature getRawFeature() {
return feature;
}
}

View file

@ -0,0 +1,32 @@
package de.avatic.lcc.model.azuremaps;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class Geometry {
@JsonProperty("type")
private String type;
@JsonProperty("coordinates")
private List<Double> coordinates;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public List<Double> getCoordinates() {
return coordinates;
}
public void setCoordinates(List<Double> coordinates) {
this.coordinates = coordinates;
}
}

View file

@ -0,0 +1,54 @@
package de.avatic.lcc.model.azuremaps;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class Properties {
@JsonProperty("confidence")
private String confidence;
@JsonProperty("matchCodes")
private List<String> matchCodes;
@JsonProperty("geocodePoints")
private List<GeocodePoint> geocodePoints;
@JsonProperty("address")
private Address address;
public String getConfidence() {
return confidence;
}
public void setConfidence(String confidence) {
this.confidence = confidence;
}
public List<String> getMatchCodes() {
return matchCodes;
}
public void setMatchCodes(List<String> matchCodes) {
this.matchCodes = matchCodes;
}
public List<GeocodePoint> getGeocodePoints() {
return geocodePoints;
}
public void setGeocodePoints(List<GeocodePoint> geocodePoints) {
this.geocodePoints = geocodePoints;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}

View file

@ -0,0 +1,36 @@
package de.avatic.lcc.model.azuremaps;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class Route {
private RouteSummary summary;
private List<RouteLeg> legs;
private List<RouteSection> sections;
public RouteSummary getSummary() {
return summary;
}
public void setSummary(RouteSummary summary) {
this.summary = summary;
}
public List<RouteLeg> getLegs() {
return legs;
}
public void setLegs(List<RouteLeg> legs) {
this.legs = legs;
}
public List<RouteSection> getSections() {
return sections;
}
public void setSections(List<RouteSection> sections) {
this.sections = sections;
}
}

View file

@ -0,0 +1,27 @@
package de.avatic.lcc.model.azuremaps;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class RouteDirectionsResponse {
private String formatVersion;
private List<Route> routes;
public String getFormatVersion() {
return formatVersion;
}
public void setFormatVersion(String formatVersion) {
this.formatVersion = formatVersion;
}
public List<Route> getRoutes() {
return routes;
}
public void setRoutes(List<Route> routes) {
this.routes = routes;
}
}

View file

@ -0,0 +1,27 @@
package de.avatic.lcc.model.azuremaps;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class RouteLeg {
private RouteSummary summary;
private List<RoutePoint> points;
public RouteSummary getSummary() {
return summary;
}
public void setSummary(RouteSummary summary) {
this.summary = summary;
}
public List<RoutePoint> getPoints() {
return points;
}
public void setPoints(List<RoutePoint> points) {
this.points = points;
}
}

View file

@ -0,0 +1,26 @@
package de.avatic.lcc.model.azuremaps;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class RoutePoint {
private Double latitude;
private Double longitude;
public Double getLatitude() {
return latitude;
}
public void setLatitude(Double latitude) {
this.latitude = latitude;
}
public Double getLongitude() {
return longitude;
}
public void setLongitude(Double longitude) {
this.longitude = longitude;
}
}

View file

@ -0,0 +1,44 @@
package de.avatic.lcc.model.azuremaps;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class RouteSection {
private Integer startPointIndex;
private Integer endPointIndex;
private String sectionType;
private String travelMode;
public Integer getStartPointIndex() {
return startPointIndex;
}
public void setStartPointIndex(Integer startPointIndex) {
this.startPointIndex = startPointIndex;
}
public Integer getEndPointIndex() {
return endPointIndex;
}
public void setEndPointIndex(Integer endPointIndex) {
this.endPointIndex = endPointIndex;
}
public String getSectionType() {
return sectionType;
}
public void setSectionType(String sectionType) {
this.sectionType = sectionType;
}
public String getTravelMode() {
return travelMode;
}
public void setTravelMode(String travelMode) {
this.travelMode = travelMode;
}
}

View file

@ -0,0 +1,62 @@
package de.avatic.lcc.model.azuremaps;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class RouteSummary {
private Integer lengthInMeters;
private Integer travelTimeInSeconds;
private Integer trafficDelayInSeconds;
private Integer trafficLengthInMeters;
private String departureTime;
private String arrivalTime;
public Integer getLengthInMeters() {
return lengthInMeters;
}
public void setLengthInMeters(Integer lengthInMeters) {
this.lengthInMeters = lengthInMeters;
}
public Integer getTravelTimeInSeconds() {
return travelTimeInSeconds;
}
public void setTravelTimeInSeconds(Integer travelTimeInSeconds) {
this.travelTimeInSeconds = travelTimeInSeconds;
}
public Integer getTrafficDelayInSeconds() {
return trafficDelayInSeconds;
}
public void setTrafficDelayInSeconds(Integer trafficDelayInSeconds) {
this.trafficDelayInSeconds = trafficDelayInSeconds;
}
public Integer getTrafficLengthInMeters() {
return trafficLengthInMeters;
}
public void setTrafficLengthInMeters(Integer trafficLengthInMeters) {
this.trafficLengthInMeters = trafficLengthInMeters;
}
public String getDepartureTime() {
return departureTime;
}
public void setDepartureTime(String departureTime) {
this.departureTime = departureTime;
}
public String getArrivalTime() {
return arrivalTime;
}
public void setArrivalTime(String arrivalTime) {
this.arrivalTime = arrivalTime;
}
}

View file

@ -0,0 +1,29 @@
package de.avatic.lcc.model.azuremaps;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
public class Summary {
@JsonProperty("successfulRequests")
private Integer successfulRequests;
@JsonProperty("totalRequests")
private Integer totalRequests;
public Integer getSuccessfulRequests() {
return successfulRequests;
}
public void setSuccessfulRequests(Integer successfulRequests) {
this.successfulRequests = successfulRequests;
}
public Integer getTotalRequests() {
return totalRequests;
}
public void setTotalRequests(Integer totalRequests) {
this.totalRequests = totalRequests;
}
}

View file

@ -4,8 +4,9 @@ import de.avatic.lcc.model.nodes.Distance;
import de.avatic.lcc.model.nodes.DistanceMatrixState; import de.avatic.lcc.model.nodes.DistanceMatrixState;
import de.avatic.lcc.model.nodes.Node; import de.avatic.lcc.model.nodes.Node;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowCallbackHandler;
import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@ -15,6 +16,8 @@ import java.util.Optional;
@Repository @Repository
public class DistanceMatrixRepository { public class DistanceMatrixRepository {
private static final Logger logger = LoggerFactory.getLogger(DistanceMatrixRepository.class);
private final JdbcTemplate jdbcTemplate; private final JdbcTemplate jdbcTemplate;
public DistanceMatrixRepository(JdbcTemplate jdbcTemplate) { public DistanceMatrixRepository(JdbcTemplate jdbcTemplate) {
@ -22,10 +25,9 @@ public class DistanceMatrixRepository {
} }
public Optional<Distance> getDistance(Node src, Node dest) { public Optional<Distance> getDistance(Node src, Node dest) {
String query = "SELECT * FROM distance_matrix WHERE from_node_id = ? AND to_node_id = ? AND state = ?"; String query = "SELECT * FROM distance_matrix WHERE from_node_id = ? AND to_node_id = ? AND state = ?";
var distance = jdbcTemplate.query(query, new DistanceMapper(), src.getId(), dest.getId(), DistanceMatrixState.VALID); var distance = jdbcTemplate.query(query, new DistanceMapper(), src.getId(), dest.getId(), DistanceMatrixState.VALID.name());
if(distance.isEmpty()) if(distance.isEmpty())
return Optional.empty(); return Optional.empty();
@ -33,6 +35,71 @@ public class DistanceMatrixRepository {
return Optional.of(distance.getFirst()); return Optional.of(distance.getFirst());
} }
public void saveDistance(Distance distance) {
try {
// First, check if an entry already exists
String checkQuery = "SELECT id FROM distance_matrix WHERE from_node_id = ? AND to_node_id = ?";
var existingIds = jdbcTemplate.query(checkQuery,
(rs, rowNum) -> rs.getInt("id"),
distance.getFromNodeId(),
distance.getToNodeId());
if (!existingIds.isEmpty()) {
// Update existing entry
String updateQuery = """
UPDATE distance_matrix
SET from_geo_lat = ?,
from_geo_lng = ?,
to_geo_lat = ?,
to_geo_lng = ?,
distance = ?,
state = ?,
updated_at = ?
WHERE from_node_id = ? AND to_node_id = ?
""";
jdbcTemplate.update(updateQuery,
distance.getFromGeoLat(),
distance.getFromGeoLng(),
distance.getToGeoLat(),
distance.getToGeoLng(),
distance.getDistance(),
distance.getState().name(),
distance.getUpdatedAt(),
distance.getFromNodeId(),
distance.getToNodeId());
logger.info("Updated existing distance entry for nodes {} -> {}",
distance.getFromNodeId(), distance.getToNodeId());
} else {
// Insert new entry
String insertQuery = """
INSERT INTO distance_matrix
(from_node_id, to_node_id, from_geo_lat, from_geo_lng, to_geo_lat, to_geo_lng, distance, state, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""";
jdbcTemplate.update(insertQuery,
distance.getFromNodeId(),
distance.getToNodeId(),
distance.getFromGeoLat(),
distance.getFromGeoLng(),
distance.getToGeoLat(),
distance.getToGeoLng(),
distance.getDistance(),
distance.getState().name(),
distance.getUpdatedAt());
logger.info("Inserted new distance entry for nodes {} -> {}",
distance.getFromNodeId(), distance.getToNodeId());
}
} catch (Exception e) {
logger.error("Error saving distance to database for nodes {} -> {}",
distance.getFromNodeId(), distance.getToNodeId(), e);
throw e;
}
}
private static class DistanceMapper implements RowMapper<Distance> { private static class DistanceMapper implements RowMapper<Distance> {
@Override @Override
public Distance mapRow(ResultSet rs, int rowNum) throws SQLException { public Distance mapRow(ResultSet rs, int rowNum) throws SQLException {
@ -48,7 +115,6 @@ public class DistanceMatrixRepository {
entity.setState(DistanceMatrixState.valueOf(rs.getString("state"))); entity.setState(DistanceMatrixState.valueOf(rs.getString("state")));
entity.setUpdatedAt(rs.getTimestamp("updated_at").toLocalDateTime()); entity.setUpdatedAt(rs.getTimestamp("updated_at").toLocalDateTime());
return entity; return entity;
} }
} }

View file

@ -108,8 +108,8 @@ public class UserRepository {
return jdbcTemplate.query( return jdbcTemplate.query(
query, query,
ps -> { ps -> {
for (int parameterIndex = 1; parameterIndex <= groups.size(); parameterIndex++) { for (int parameterIndex = 0; parameterIndex < groups.size(); parameterIndex++) {
ps.setString(parameterIndex, groups.get(parameterIndex)); ps.setString(parameterIndex + 1, groups.get(parameterIndex));
} }
}, },
(rs, rowNum) -> rs.getInt("id") (rs, rowNum) -> rs.getInt("id")
@ -117,26 +117,35 @@ public class UserRepository {
} }
private void updateUserGroupMappings(Integer userId, List<Integer> groups) { private void updateUserGroupMappings(Integer userId, List<Integer> groups) {
// Handle empty groups list case
if (groups.isEmpty()) {
// Delete all mappings for this user
jdbcTemplate.update("DELETE FROM sys_user_group_mapping WHERE user_id = ?", userId);
return;
} else
{
for (Integer groupId : groups) { for (Integer groupId : groups) {
jdbcTemplate.update( jdbcTemplate.update(
"INSERT IGNORE INTO sys_user_group_mapping (user_id, group_id) VALUES (?, ?)", "INSERT IGNORE INTO sys_user_group_mapping (user_id, group_id) VALUES (?, ?)",
userId, groupId userId, groupId
); );
} }
}
String placeholders = String.join(",", Collections.nCopies(groups.size(), "?")); String placeholders = String.join(",", Collections.nCopies(groups.size(), "?"));
String query = "DELETE FROM sys_user_group_mapping WHERE user_id = ? AND group_id NOT IN (" + placeholders + ")"; String query = "DELETE FROM sys_user_group_mapping WHERE user_id = ? AND group_id NOT IN (" + placeholders + ")";
jdbcTemplate.query( jdbcTemplate.update(
query, query,
ps -> { ps -> {
for (int parameterIndex = 0; parameterIndex < groups.size(); parameterIndex++) { ps.setInt(1, userId);
ps.setInt(parameterIndex + 1, groups.get(parameterIndex)); for (int index = 0; index < groups.size(); index++) {
ps.setInt(index + 2, groups.get(index)); // Parameters start at index 2
}
} }
},
(rs, rowNum) -> rs.getInt("id")
); );
} }
@ -152,7 +161,7 @@ public class UserRepository {
@Transactional @Transactional
public User getByWorkdayId(String workdayId) { public User getByWorkdayId(String workdayId) {
List<User> results = jdbcTemplate.query("SELECT id FROM sys_user WHERE workday_id = ?", List<User> results = jdbcTemplate.query("SELECT * FROM sys_user WHERE workday_id = ?",
new UserMapper(), new UserMapper(),
workdayId); workdayId);

View file

@ -1,15 +0,0 @@
package de.avatic.lcc.service;
import de.avatic.lcc.dto.configuration.nodes.userNodes.LocateNodeDTO;
import org.springframework.stereotype.Service;
@Service
public class GeoApiService {
public LocateNodeDTO locate(String address) {
//TODO implementation missing
return null;
}
}

View file

@ -0,0 +1,200 @@
package de.avatic.lcc.service.api;
import de.avatic.lcc.excelmodel.ExcelNode;
import de.avatic.lcc.model.azuremaps.*;
import de.avatic.lcc.model.bulk.BulkInstruction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class BatchGeoApiService {
private static final Logger logger = LoggerFactory.getLogger(BatchGeoApiService.class);
private static final String AZURE_MAPS_BATCH_GEOCODING_URL = "https://atlas.microsoft.com/geocode:batch";
private static final int MAX_BATCH_SIZE = 100; // Azure Maps batch limit
private final RestTemplate restTemplate;
private final String subscriptionKey;
public BatchGeoApiService(RestTemplate restTemplate,
@Value("${azure.maps.subscription.key}") String subscriptionKey) {
this.restTemplate = restTemplate;
this.subscriptionKey = subscriptionKey;
}
/**
* Geocode multiple addresses in a single batch request
* @param addresses List of address strings to geocode
* @return BatchGeocodingResult containing all results
*/
public BatchGeocodingResult geocodeBatch(List<BulkInstruction<ExcelNode>> nodes) {
if (nodes == null || nodes.isEmpty()) {
logger.warn("Address list is null or empty");
return new BatchGeocodingResult(new ArrayList<>(), 0, 0);
}
ArrayList<BulkInstruction<ExcelNode>> noGeo = new ArrayList<>();
for(var node : nodes) {
if(node.getEntity().getGeoLat() == null || node.getEntity().getGeoLng() == null) {
noGeo.add(node);
}
}
// Split into chunks if exceeds max batch size
if (noGeo.size() > MAX_BATCH_SIZE) {
return geocodeLargeBatch(noGeo);
}
List<BatchItem> batchItems = noGeo.stream()
.map(BulkInstruction::getEntity).map(ExcelNode::getAddress).map(BatchItem::new)
.collect(Collectors.toList());
return executeBatchRequest(batchItems);
}
/**
* Geocode multiple addresses with detailed address components
* @param batchItems List of BatchItem with detailed address information
* @return BatchGeocodingResult containing all results
*/
public BatchGeocodingResult geocodeBatchDetailed(List<BatchItem> batchItems) {
if (batchItems == null || batchItems.isEmpty()) {
logger.warn("Batch items list is null or empty");
return new BatchGeocodingResult(new ArrayList<>(), 0, 0);
}
// Split into chunks if exceeds max batch size
if (batchItems.size() > MAX_BATCH_SIZE) {
return geocodeLargeBatchDetailed(batchItems);
}
return executeBatchRequest(batchItems);
}
private BatchGeocodingResult executeBatchRequest(List<BatchItem> batchItems) {
try {
URI uri = UriComponentsBuilder.fromUriString(AZURE_MAPS_BATCH_GEOCODING_URL)
.queryParam("api-version", "2023-06-01")
.queryParam("subscription-key", subscriptionKey)
.build()
.toUri();
BatchGeocodingRequest request = new BatchGeocodingRequest(batchItems);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<BatchGeocodingRequest> entity = new HttpEntity<>(request, headers);
logger.debug("Calling Azure Maps Batch API for {} addresses", batchItems.size());
ResponseEntity<BatchGeocodingResponse> responseEntity = restTemplate.exchange(
uri,
HttpMethod.POST,
entity,
BatchGeocodingResponse.class
);
BatchGeocodingResponse response = responseEntity.getBody();
if (response != null) {
List<GeocodingResult> results = processResponse(response);
int successful = response.getSummary() != null ?
response.getSummary().getSuccessfulRequests() : 0;
int total = response.getSummary() != null ?
response.getSummary().getTotalRequests() : batchItems.size();
logger.info("Batch geocoding completed: {}/{} successful", successful, total);
return new BatchGeocodingResult(results, successful, total);
}
logger.warn("Received null response from batch geocoding");
return new BatchGeocodingResult(new ArrayList<>(), 0, batchItems.size());
} catch (Exception e) {
logger.error("Failed to execute batch geocoding request", e);
throw new RuntimeException("Batch geocoding failed", e);
}
}
private List<GeocodingResult> processResponse(BatchGeocodingResponse response) {
List<GeocodingResult> results = new ArrayList<>();
if (response.getBatchItems() != null) {
for (BatchResponseItem item : response.getBatchItems()) {
if (item.getStatusCode() == 200 &&
item.getResponse() != null &&
item.getResponse().getFeatures() != null &&
!item.getResponse().getFeatures().isEmpty()) {
Feature feature = item.getResponse().getFeatures().get(0);
results.add(new GeocodingResult(feature));
} else {
// Add null for failed requests to maintain index alignment
results.add(null);
logger.debug("Failed to geocode item at index {}: status code {}",
results.size() - 1, item.getStatusCode());
}
}
}
return results;
}
/**
* Handle batches larger than MAX_BATCH_SIZE by splitting into multiple requests
*/
private BatchGeocodingResult geocodeLargeBatch(List<BulkInstruction<ExcelNode>> addresses) {
logger.info("Processing large batch of {} addresses in chunks of {}",
addresses.size(), MAX_BATCH_SIZE);
List<GeocodingResult> allResults = new ArrayList<>();
int totalSuccessful = 0;
int totalRequests = addresses.size();
//
// for (int i = 0; i < addresses.size(); i += MAX_BATCH_SIZE) {
// int end = Math.min(i + MAX_BATCH_SIZE, addresses.size());
// List<String> chunk = addresses.subList(i, end);
//
// BatchGeocodingResult chunkResult = geocodeBatch(chunk);
// allResults.addAll(chunkResult.getResults());
// totalSuccessful += chunkResult.getSuccessfulRequests();
// }
return new BatchGeocodingResult(allResults, totalSuccessful, totalRequests);
}
/**
* Handle detailed batches larger than MAX_BATCH_SIZE
*/
private BatchGeocodingResult geocodeLargeBatchDetailed(List<BatchItem> batchItems) {
logger.info("Processing large detailed batch of {} items in chunks of {}",
batchItems.size(), MAX_BATCH_SIZE);
List<GeocodingResult> allResults = new ArrayList<>();
int totalSuccessful = 0;
int totalRequests = batchItems.size();
for (int i = 0; i < batchItems.size(); i += MAX_BATCH_SIZE) {
int end = Math.min(i + MAX_BATCH_SIZE, batchItems.size());
List<BatchItem> chunk = batchItems.subList(i, end);
BatchGeocodingResult chunkResult = geocodeBatchDetailed(chunk);
allResults.addAll(chunkResult.getResults());
totalSuccessful += chunkResult.getSuccessfulRequests();
}
return new BatchGeocodingResult(allResults, totalSuccessful, totalRequests);
}
}

View file

@ -1,4 +1,4 @@
package de.avatic.lcc.service; package de.avatic.lcc.service.api;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;

View file

@ -0,0 +1,109 @@
package de.avatic.lcc.service.api;
import de.avatic.lcc.model.azuremaps.RouteDirectionsResponse;
import de.avatic.lcc.model.nodes.Distance;
import de.avatic.lcc.model.nodes.DistanceMatrixState;
import de.avatic.lcc.model.nodes.Node;
import de.avatic.lcc.repositories.DistanceMatrixRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Optional;
@Service
public class DistanceApiService {
private static final Logger logger = LoggerFactory.getLogger(DistanceApiService.class);
private static final String AZURE_MAPS_ROUTE_API = "https://atlas.microsoft.com/route/directions/json";
private final DistanceMatrixRepository distanceMatrixRepository;
private final RestTemplate restTemplate;
@Value("${azure.maps.subscription.key}")
private String subscriptionKey;
public DistanceApiService(DistanceMatrixRepository distanceMatrixRepository,
RestTemplate restTemplate) {
this.distanceMatrixRepository = distanceMatrixRepository;
this.restTemplate = restTemplate;
}
public Optional<Distance> getDistance(Node from, Node to) {
if (from == null || to == null) {
logger.warn("Source or destination node is null");
return Optional.empty();
}
if (from.getGeoLat() == null || from.getGeoLng() == null ||
to.getGeoLat() == null || to.getGeoLng() == null) {
logger.warn("Missing geo coordinates for nodes: from={}, to={}", from.getId(), to.getId());
return Optional.empty();
}
// Check if distance exists in database and is valid
Optional<Distance> cachedDistance = distanceMatrixRepository.getDistance(from, to);
if (cachedDistance.isPresent()) {
logger.debug("Found cached distance from node {} to node {}", from.getId(), to.getId());
return cachedDistance;
}
// Distance not found or stale, fetch from Azure Maps
logger.info("Fetching distance from Azure Maps for nodes {} to {}", from.getId(), to.getId());
Optional<Distance> fetchedDistance = fetchDistanceFromAzureMaps(from, to);
if (fetchedDistance.isPresent()) {
// Store in database
distanceMatrixRepository.saveDistance(fetchedDistance.get());
return fetchedDistance;
}
return Optional.empty();
}
private Optional<Distance> fetchDistanceFromAzureMaps(Node from, Node to) {
try {
String url = UriComponentsBuilder.fromHttpUrl(AZURE_MAPS_ROUTE_API)
.queryParam("api-version", "1.0")
.queryParam("subscription-key", subscriptionKey)
.queryParam("query", String.format("%s,%s:%s,%s",
from.getGeoLat(), from.getGeoLng(),
to.getGeoLat(), to.getGeoLng()))
.toUriString();
RouteDirectionsResponse response = restTemplate.getForObject(url, RouteDirectionsResponse.class);
if (response != null && response.getRoutes() != null && !response.getRoutes().isEmpty()) {
Integer distanceInMeters = response.getRoutes().get(0).getSummary().getLengthInMeters();
Distance distance = new Distance();
distance.setFromNodeId(from.getId());
distance.setToNodeId(to.getId());
distance.setFromGeoLat(from.getGeoLat());
distance.setFromGeoLng(from.getGeoLng());
distance.setToGeoLat(to.getGeoLat());
distance.setToGeoLng(to.getGeoLng());
distance.setDistance(BigDecimal.valueOf(distanceInMeters));
distance.setState(DistanceMatrixState.VALID);
distance.setUpdatedAt(LocalDateTime.now());
logger.info("Successfully fetched distance: {} meters", distanceInMeters);
return Optional.of(distance);
} else {
logger.warn("No routes found in Azure Maps response");
}
} catch (Exception e) {
logger.error("Error fetching distance from Azure Maps", e);
}
return Optional.empty();
}
}

View file

@ -0,0 +1,68 @@
package de.avatic.lcc.service.api;
import de.avatic.lcc.model.azuremaps.GeocodingResponse;
import de.avatic.lcc.model.azuremaps.GeocodingResult;
import de.avatic.lcc.model.azuremaps.Feature;
import de.avatic.lcc.model.nodes.Location;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
@Service
public class GeoApiService {
private static final Logger logger = LoggerFactory.getLogger(GeoApiService.class);
private static final String AZURE_MAPS_GEOCODING_URL = "https://atlas.microsoft.com/geocode";
private final RestTemplate restTemplate;
private final String subscriptionKey;
public GeoApiService(RestTemplate restTemplate,
@Value("${azure.maps.subscription.key}") String subscriptionKey) {
this.restTemplate = restTemplate;
this.subscriptionKey = subscriptionKey;
}
public GeocodingResult geocode(String address) {
if (address == null || address.trim().isEmpty()) {
logger.warn("Address is null or empty");
return null;
}
try {
URI uri = UriComponentsBuilder.fromUriString(AZURE_MAPS_GEOCODING_URL)
.queryParam("api-version", "2023-06-01")
.queryParam("query", address)
.queryParam("subscription-key", subscriptionKey)
.build()
.toUri();
logger.info("Calling Azure Maps API for address: {}", address);
GeocodingResponse response = restTemplate.getForObject(uri, GeocodingResponse.class);
if (response != null && response.getFeatures() != null && !response.getFeatures().isEmpty()) {
Feature feature = response.getFeatures().getFirst();
logger.debug("Successfully geocoded address: {}", address);
return new GeocodingResult(feature);
}
logger.warn("No results found for address: {}", address);
return null;
} catch (Exception e) {
logger.error("Failed to geocode address: {}", address, e);
throw new RuntimeException("Geocoding failed for: " + address, e);
}
}
public Location locate(String address) {
GeocodingResult result = geocode(address);
return result != null ? result.getLocation() : null;
}
}

View file

@ -3,6 +3,7 @@ package de.avatic.lcc.service.bulk;
import de.avatic.lcc.model.bulk.BulkFileTypes; import de.avatic.lcc.model.bulk.BulkFileTypes;
import de.avatic.lcc.model.bulk.BulkOperation; import de.avatic.lcc.model.bulk.BulkOperation;
import de.avatic.lcc.repositories.NodeRepository; import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.service.api.BatchGeoApiService;
import de.avatic.lcc.service.bulk.bulkImport.*; import de.avatic.lcc.service.bulk.bulkImport.*;
import de.avatic.lcc.service.excelMapper.*; import de.avatic.lcc.service.excelMapper.*;
import de.avatic.lcc.service.transformer.generic.NodeTransformer; import de.avatic.lcc.service.transformer.generic.NodeTransformer;
@ -32,8 +33,9 @@ public class BulkImportService {
private final MaterialBulkImportService materialBulkImportService; private final MaterialBulkImportService materialBulkImportService;
private final MatrixRateImportService matrixRateImportService; private final MatrixRateImportService matrixRateImportService;
private final ContainerRateImportService containerRateImportService; private final ContainerRateImportService containerRateImportService;
private final BatchGeoApiService batchGeoApiService;
public BulkImportService(MatrixRateExcelMapper matrixRateExcelMapper, ContainerRateExcelMapper containerRateExcelMapper, MaterialExcelMapper materialExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper, NodeRepository nodeRepository, NodeTransformer nodeTransformer, NodeBulkImportService nodeBulkImportService, PackagingBulkImportService packagingBulkImportService, MaterialBulkImportService materialBulkImportService, MatrixRateImportService matrixRateImportService, ContainerRateImportService containerRateImportService) { public BulkImportService(MatrixRateExcelMapper matrixRateExcelMapper, ContainerRateExcelMapper containerRateExcelMapper, MaterialExcelMapper materialExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper, NodeRepository nodeRepository, NodeTransformer nodeTransformer, NodeBulkImportService nodeBulkImportService, PackagingBulkImportService packagingBulkImportService, MaterialBulkImportService materialBulkImportService, MatrixRateImportService matrixRateImportService, ContainerRateImportService containerRateImportService, BatchGeoApiService batchGeoApiService) {
this.matrixRateExcelMapper = matrixRateExcelMapper; this.matrixRateExcelMapper = matrixRateExcelMapper;
this.containerRateExcelMapper = containerRateExcelMapper; this.containerRateExcelMapper = containerRateExcelMapper;
this.materialExcelMapper = materialExcelMapper; this.materialExcelMapper = materialExcelMapper;
@ -46,6 +48,7 @@ public class BulkImportService {
this.materialBulkImportService = materialBulkImportService; this.materialBulkImportService = materialBulkImportService;
this.matrixRateImportService = matrixRateImportService; this.matrixRateImportService = matrixRateImportService;
this.containerRateImportService = containerRateImportService; this.containerRateImportService = containerRateImportService;
this.batchGeoApiService = batchGeoApiService;
} }
public void processOperation(BulkOperation op) throws IOException { public void processOperation(BulkOperation op) throws IOException {
@ -88,6 +91,7 @@ public class BulkImportService {
break; break;
case NODE: case NODE:
var nodeInstructions = nodeExcelMapper.extractSheet(sheet); var nodeInstructions = nodeExcelMapper.extractSheet(sheet);
// batchGeoApiService.geocodeBatch(nodeInstructions);
nodeInstructions.forEach(nodeBulkImportService::processNodeInstructions); nodeInstructions.forEach(nodeBulkImportService::processNodeInstructions);
break; break;
default: default:

View file

@ -154,9 +154,9 @@ public class ConstraintGenerator {
return result.toString(); return result.toString();
} }
public void validateDecimalConstraint(Row row, int columnIdx, double min, double max) { public void validateDecimalConstraint(Row row, int columnIdx, double min, double max, boolean allowEmpty) {
checkEmptyCell(row, columnIdx); checkEmptyCell(row, columnIdx, allowEmpty);
CellType cellType = row.getCell(columnIdx).getCellType(); CellType cellType = row.getCell(columnIdx).getCellType();
@ -173,6 +173,10 @@ public class ConstraintGenerator {
} }
public void validateDecimalConstraint(Row row, int columnIdx, double min, double max) {
validateDecimalConstraint(row, columnIdx, min, max, false);
}
public void validateBooleanConstraint(Row row, int columnIdx) { public void validateBooleanConstraint(Row row, int columnIdx) {
checkEmptyCell(row, columnIdx); checkEmptyCell(row, columnIdx);
@ -234,9 +238,9 @@ public class ConstraintGenerator {
} }
public void validateNumericCell(Row row, int columnIdx) { public void validateNumericCell(Row row, int columnIdx, boolean allowEmpty) {
checkEmptyCell(row, columnIdx); checkEmptyCell(row, columnIdx, allowEmpty);
CellType cellType = row.getCell(columnIdx).getCellType(); CellType cellType = row.getCell(columnIdx).getCellType();
@ -250,6 +254,10 @@ public class ConstraintGenerator {
} }
public void validateNumericCell(Row row, int columnIdx) {
validateNumericCell(row, columnIdx, false);
}
public void validateIntegerCell(Row row, int columnIdx) { public void validateIntegerCell(Row row, int columnIdx) {
checkEmptyCell(row, columnIdx); checkEmptyCell(row, columnIdx);

View file

@ -13,7 +13,7 @@ import de.avatic.lcc.repositories.packaging.PackagingPropertiesRepository;
import de.avatic.lcc.repositories.packaging.PackagingRepository; import de.avatic.lcc.repositories.packaging.PackagingRepository;
import de.avatic.lcc.repositories.premise.PremiseRepository; import de.avatic.lcc.repositories.premise.PremiseRepository;
import de.avatic.lcc.repositories.users.UserNodeRepository; import de.avatic.lcc.repositories.users.UserNodeRepository;
import de.avatic.lcc.service.CustomApiService; import de.avatic.lcc.service.api.CustomApiService;
import de.avatic.lcc.service.access.PremisesService; import de.avatic.lcc.service.access.PremisesService;
import de.avatic.lcc.service.users.AuthorizationService; import de.avatic.lcc.service.users.AuthorizationService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;

View file

@ -14,7 +14,7 @@ import de.avatic.lcc.repositories.packaging.PackagingPropertiesRepository;
import de.avatic.lcc.repositories.packaging.PackagingRepository; import de.avatic.lcc.repositories.packaging.PackagingRepository;
import de.avatic.lcc.repositories.premise.*; import de.avatic.lcc.repositories.premise.*;
import de.avatic.lcc.repositories.users.UserNodeRepository; import de.avatic.lcc.repositories.users.UserNodeRepository;
import de.avatic.lcc.service.CustomApiService; import de.avatic.lcc.service.api.CustomApiService;
import de.avatic.lcc.service.access.DestinationService; import de.avatic.lcc.service.access.DestinationService;
import de.avatic.lcc.service.access.PremisesService; import de.avatic.lcc.service.access.PremisesService;
import de.avatic.lcc.service.users.AuthorizationService; import de.avatic.lcc.service.users.AuthorizationService;

View file

@ -13,7 +13,7 @@ import de.avatic.lcc.repositories.packaging.PackagingPropertiesRepository;
import de.avatic.lcc.repositories.packaging.PackagingRepository; import de.avatic.lcc.repositories.packaging.PackagingRepository;
import de.avatic.lcc.repositories.premise.PremiseRepository; import de.avatic.lcc.repositories.premise.PremiseRepository;
import de.avatic.lcc.repositories.users.UserNodeRepository; import de.avatic.lcc.repositories.users.UserNodeRepository;
import de.avatic.lcc.service.CustomApiService; import de.avatic.lcc.service.api.CustomApiService;
import de.avatic.lcc.service.access.DestinationService; import de.avatic.lcc.service.access.DestinationService;
import de.avatic.lcc.service.transformer.generic.DimensionTransformer; import de.avatic.lcc.service.transformer.generic.DimensionTransformer;
import de.avatic.lcc.service.transformer.premise.PremiseTransformer; import de.avatic.lcc.service.transformer.premise.PremiseTransformer;

View file

@ -12,7 +12,7 @@ import de.avatic.lcc.model.properties.SystemPropertyMappingId;
import de.avatic.lcc.repositories.country.CountryPropertyRepository; import de.avatic.lcc.repositories.country.CountryPropertyRepository;
import de.avatic.lcc.repositories.premise.RouteNodeRepository; import de.avatic.lcc.repositories.premise.RouteNodeRepository;
import de.avatic.lcc.repositories.properties.PropertyRepository; import de.avatic.lcc.repositories.properties.PropertyRepository;
import de.avatic.lcc.service.CustomApiService; import de.avatic.lcc.service.api.CustomApiService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.math.BigDecimal; import java.math.BigDecimal;

View file

@ -144,8 +144,10 @@ public class NodeExcelMapper {
entity.setName(row.getCell(NodeHeader.NAME.ordinal()).getStringCellValue()); entity.setName(row.getCell(NodeHeader.NAME.ordinal()).getStringCellValue());
entity.setAddress(row.getCell(NodeHeader.ADDRESS.ordinal()).getStringCellValue()); entity.setAddress(row.getCell(NodeHeader.ADDRESS.ordinal()).getStringCellValue());
entity.setCountryId(IsoCode.valueOf(row.getCell(NodeHeader.COUNTRY.ordinal()).getStringCellValue())); entity.setCountryId(IsoCode.valueOf(row.getCell(NodeHeader.COUNTRY.ordinal()).getStringCellValue()));
entity.setGeoLat(BigDecimal.valueOf(row.getCell(NodeHeader.GEO_LATITUDE.ordinal()).getNumericCellValue()));
entity.setGeoLng(BigDecimal.valueOf(row.getCell(NodeHeader.GEO_LONGITUDE.ordinal()).getNumericCellValue())); entity.setGeoLat(mapGeoCoordinate(CellUtil.getCell(row, NodeHeader.GEO_LATITUDE.ordinal())));
entity.setGeoLng(mapGeoCoordinate(CellUtil.getCell(row, NodeHeader.GEO_LONGITUDE.ordinal())));
entity.setSource(Boolean.valueOf(row.getCell(NodeHeader.IS_SOURCE.ordinal()).getStringCellValue())); entity.setSource(Boolean.valueOf(row.getCell(NodeHeader.IS_SOURCE.ordinal()).getStringCellValue()));
entity.setIntermediate(Boolean.valueOf(row.getCell(NodeHeader.IS_INTERMEDIATE.ordinal()).getStringCellValue())); entity.setIntermediate(Boolean.valueOf(row.getCell(NodeHeader.IS_INTERMEDIATE.ordinal()).getStringCellValue()));
entity.setDestination(Boolean.valueOf(row.getCell(NodeHeader.IS_DESTINATION.ordinal()).getStringCellValue())); entity.setDestination(Boolean.valueOf(row.getCell(NodeHeader.IS_DESTINATION.ordinal()).getStringCellValue()));
@ -165,8 +167,9 @@ public class NodeExcelMapper {
constraintGenerator.validateStringCell(row, NodeHeader.NAME.ordinal()); constraintGenerator.validateStringCell(row, NodeHeader.NAME.ordinal());
constraintGenerator.validateStringCell(row, NodeHeader.ADDRESS.ordinal()); constraintGenerator.validateStringCell(row, NodeHeader.ADDRESS.ordinal());
constraintGenerator.validateDecimalConstraint(row, NodeHeader.GEO_LATITUDE.ordinal(), -90.0, 90.0); constraintGenerator.validateDecimalConstraint(row, NodeHeader.GEO_LATITUDE.ordinal(), -90.0, 90.0, true);
constraintGenerator.validateDecimalConstraint(row, NodeHeader.GEO_LONGITUDE.ordinal(), -180.0, 180.0); constraintGenerator.validateDecimalConstraint(row, NodeHeader.GEO_LONGITUDE.ordinal(), -180.0, 180.0, true);
constraintGenerator.validateBooleanConstraint(row, NodeHeader.IS_SOURCE.ordinal()); constraintGenerator.validateBooleanConstraint(row, NodeHeader.IS_SOURCE.ordinal());
constraintGenerator.validateBooleanConstraint(row, NodeHeader.IS_INTERMEDIATE.ordinal()); constraintGenerator.validateBooleanConstraint(row, NodeHeader.IS_INTERMEDIATE.ordinal());
constraintGenerator.validateBooleanConstraint(row, NodeHeader.IS_DESTINATION.ordinal()); constraintGenerator.validateBooleanConstraint(row, NodeHeader.IS_DESTINATION.ordinal());
@ -177,6 +180,12 @@ public class NodeExcelMapper {
} }
private BigDecimal mapGeoCoordinate(Cell geoCoordinate) {
if (geoCoordinate == null) return null;
if(geoCoordinate.getCellType() == CellType.BLANK) return null;
return BigDecimal.valueOf(geoCoordinate.getNumericCellValue());
}
private List<String> mapOutboundCountriesFromCell(String outboundCountryIds) { private List<String> mapOutboundCountriesFromCell(String outboundCountryIds) {
if (outboundCountryIds == null || outboundCountryIds.isBlank()) return Collections.emptyList(); if (outboundCountryIds == null || outboundCountryIds.isBlank()) return Collections.emptyList();
return Arrays.stream(outboundCountryIds.split(",")).map(String::trim).toList(); return Arrays.stream(outboundCountryIds.split(",")).map(String::trim).toList();

View file

@ -12,7 +12,7 @@ import de.avatic.lcc.repositories.premise.DestinationRepository;
import de.avatic.lcc.repositories.premise.PremiseRepository; import de.avatic.lcc.repositories.premise.PremiseRepository;
import de.avatic.lcc.repositories.premise.RouteRepository; import de.avatic.lcc.repositories.premise.RouteRepository;
import de.avatic.lcc.repositories.properties.PropertyRepository; import de.avatic.lcc.repositories.properties.PropertyRepository;
import de.avatic.lcc.service.CustomApiService; import de.avatic.lcc.service.api.CustomApiService;
import de.avatic.lcc.service.access.PropertyService; import de.avatic.lcc.service.access.PropertyService;
import de.avatic.lcc.service.transformer.generic.DimensionTransformer; import de.avatic.lcc.service.transformer.generic.DimensionTransformer;
import de.avatic.lcc.util.exception.internalerror.PremiseValidationError; import de.avatic.lcc.util.exception.internalerror.PremiseValidationError;

View file

@ -7,9 +7,8 @@ spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.sql.init.mode=never spring.sql.init.mode=never
lcc.bulk.sheet_password=secretSheet?! lcc.bulk.sheet_password=secretSheet?!
lcc.allowed_cors=${ALLOWED_CORS_DOMAIN} lcc.allowed_cors=${ALLOWED_CORS_DOMAIN}
azure.maps.client.id=${AZURE_MAPS_CLIENT_ID}
azure.maps.subscription.key=${AZURE_MAPS_SUBSCRIPTION_KEY} azure.maps.subscription.key=${AZURE_MAPS_SUBSCRIPTION_KEY}
azure.maps.client.id=your-app-registration-client-id
azure.maps.resource.id=/subscriptions/sub-id/resourceGroups/rg-name/providers/Microsoft.Maps/accounts/account-name
spring.servlet.multipart.max-file-size=30MB spring.servlet.multipart.max-file-size=30MB
spring.servlet.multipart.max-request-size=50MB spring.servlet.multipart.max-request-size=50MB
spring.cloud.azure.active-directory.enabled=true spring.cloud.azure.active-directory.enabled=true

View file

@ -7,6 +7,8 @@ VALUES ('USR001', 'john.doe@company.com', 'John', 'Doe', TRUE),
('USR005', 'david.chen@company.com', 'David', 'Chen', TRUE) ('USR005', 'david.chen@company.com', 'David', 'Chen', TRUE)
ON DUPLICATE KEY UPDATE email = VALUES(email); ON DUPLICATE KEY UPDATE email = VALUES(email);
INSERT INTO sys_group(group_name, group_description)
VALUES ('none', 'no rights');
INSERT INTO sys_group(group_name, group_description) INSERT INTO sys_group(group_name, group_description)
VALUES ('basic', 'Login, generate reports'); VALUES ('basic', 'Login, generate reports');
INSERT INTO sys_group(group_name, group_description) INSERT INTO sys_group(group_name, group_description)
@ -20,23 +22,23 @@ VALUES ('super',
'Login, generate reports, do calculations, edit freight rates, edit packaging data'); 'Login, generate reports, do calculations, edit freight rates, edit packaging data');
INSERT INTO sys_user_group_mapping (user_id, group_id) INSERT INTO sys_user_group_mapping (user_id, group_id)
VALUES ((SELECT id FROM sys_group WHERE group_name = 'LCE'), VALUES ((SELECT id FROM sys_group WHERE group_name = 'super'),
(SELECT id FROM sys_user WHERE email = 'john.doe@company.com')); (SELECT id FROM sys_user WHERE email = 'john.doe@company.com'));
INSERT INTO sys_user_group_mapping (user_id, group_id) INSERT INTO sys_user_group_mapping (user_id, group_id)
VALUES ((SELECT id FROM sys_group WHERE group_name = 'LCE'), VALUES ((SELECT id FROM sys_group WHERE group_name = 'basic'),
(SELECT id FROM sys_user WHERE email = 'sarah.smith@company.com')); (SELECT id FROM sys_user WHERE email = 'sarah.smith@company.com'));
INSERT INTO sys_user_group_mapping (user_id, group_id) INSERT INTO sys_user_group_mapping (user_id, group_id)
VALUES ((SELECT id FROM sys_group WHERE group_name = 'LCE'), VALUES ((SELECT id FROM sys_group WHERE group_name = 'calculation'),
(SELECT id FROM sys_user WHERE email = 'mike.johnson@company.com')); (SELECT id FROM sys_user WHERE email = 'mike.johnson@company.com'));
INSERT INTO sys_user_group_mapping (user_id, group_id) INSERT INTO sys_user_group_mapping (user_id, group_id)
VALUES ((SELECT id FROM sys_group WHERE group_name = 'LCE'), VALUES ((SELECT id FROM sys_group WHERE group_name = 'freight'),
(SELECT id FROM sys_user WHERE email = 'anna.mueller@company.com')); (SELECT id FROM sys_user WHERE email = 'anna.mueller@company.com'));
INSERT INTO sys_user_group_mapping (user_id, group_id) INSERT INTO sys_user_group_mapping (user_id, group_id)
VALUES ((SELECT id FROM sys_group WHERE group_name = 'LCE'), VALUES ((SELECT id FROM sys_group WHERE group_name = 'packaging'),
(SELECT id FROM sys_user WHERE email = 'david.chen@company.com')); (SELECT id FROM sys_user WHERE email = 'david.chen@company.com'));
INSERT INTO sys_user_node (user_id, country_id, name, address, geo_lat, geo_lng, is_deprecated) INSERT INTO sys_user_node (user_id, country_id, name, address, geo_lat, geo_lng, is_deprecated)