commit
896a9eb3cd
147 changed files with 6556 additions and 1468 deletions
2
.mvn/wrapper/maven-wrapper.properties
vendored
2
.mvn/wrapper/maven-wrapper.properties
vendored
|
|
@ -16,4 +16,4 @@
|
|||
# under the License.
|
||||
wrapperVersion=3.3.4
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
image: mysql:8.4
|
||||
container_name: lcc-mysql-local
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
|
||||
|
|
|
|||
9
pom.xml
9
pom.xml
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.5.8</version>
|
||||
<version>3.5.9</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>de.avatic</groupId>
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
</scm>
|
||||
<properties>
|
||||
<java.version>23</java.version>
|
||||
<spring-cloud-azure.version>5.24.0</spring-cloud-azure.version>
|
||||
<spring-cloud-azure.version>5.24.1</spring-cloud-azure.version>
|
||||
<mockito.version>5.20.0</mockito.version>
|
||||
<flyway.version>11.18.0</flyway.version>
|
||||
</properties>
|
||||
|
|
@ -130,6 +130,11 @@
|
|||
<artifactId>fastexcel</artifactId>
|
||||
<version>0.19.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.commonmark</groupId>
|
||||
<artifactId>commonmark</artifactId>
|
||||
<version>0.22.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<the-notification-system />
|
||||
<the-help-system />
|
||||
<the-header></the-header>
|
||||
<router-view v-slot="slotProps">
|
||||
<transition name="route" mode="out-in">
|
||||
|
|
@ -13,9 +14,10 @@
|
|||
|
||||
import TheHeader from "@/components/layout/TheHeader.vue";
|
||||
import TheNotificationSystem from "@/components/UI/TheNotificationSystem.vue";
|
||||
import TheHelpSystem from "@/components/layout/help/TheHelpSystem.vue";
|
||||
|
||||
export default {
|
||||
components: {TheNotificationSystem, TheHeader},
|
||||
components: {TheHelpSystem, TheNotificationSystem, TheHeader},
|
||||
|
||||
}
|
||||
</script>
|
||||
|
|
@ -44,10 +46,12 @@ html.modal-open {
|
|||
color: #002F54;
|
||||
}
|
||||
|
||||
|
||||
.sub-header {
|
||||
font-weight: normal;
|
||||
font-size: 1.4rem;
|
||||
color: #6B869C;
|
||||
margin: 1.6rem 0;
|
||||
}
|
||||
|
||||
html {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,20 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="app-list-item">
|
||||
<div class="app-name-container"><div class="app-name-name">{{ app.name }}</div><div class="app-name-id">{{ app.client_id}}</div></div>
|
||||
<div class="app-name-container">
|
||||
<div class="app-name-name">{{ app.name }}</div>
|
||||
<div class="app-name-id">{{ app.client_id }}</div>
|
||||
</div>
|
||||
|
||||
<div class="badge-list">
|
||||
<basic-badge variant="secondary" icon="lock" v-for="group in groups" :key="group">{{ group }}</basic-badge>
|
||||
</div>
|
||||
|
||||
<div class="action-container">
|
||||
<icon-button icon="download" @click="exportClick"></icon-button>
|
||||
<icon-button icon="trash" @click="deleteClick"></icon-button>
|
||||
</div>
|
||||
|
||||
<div class="badge-list"> <basic-badge variant="secondary" icon="lock" v-for="group in groups" :key="group">{{group}}</basic-badge></div>
|
||||
<div class="action-container"> <icon-button icon="trash" @click="deleteClick"></icon-button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -18,7 +28,7 @@ import BasicBadge from "@/components/UI/BasicBadge.vue";
|
|||
export default {
|
||||
name: "AppListItem",
|
||||
components: {BasicBadge, IconButton, Box},
|
||||
emits: ["deleteApp"],
|
||||
emits: ["deleteApp", "exportApp"],
|
||||
props: {
|
||||
app: {
|
||||
type: Object,
|
||||
|
|
@ -33,6 +43,9 @@ export default {
|
|||
methods: {
|
||||
deleteClick() {
|
||||
this.$emit("deleteApp", this.app.id);
|
||||
},
|
||||
exportClick() {
|
||||
this.$emit("exportApp", this.app.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -76,7 +89,10 @@ export default {
|
|||
color: #6b7280;
|
||||
}
|
||||
|
||||
.action-container{
|
||||
.action-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1.2rem;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -133,9 +133,12 @@ export default {
|
|||
.box-content.collapsed {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
|
||||
margin: 0 !important; /* ← !important um alle margins zu überschreiben */
|
||||
padding: 0 !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
|
|
@ -128,7 +128,7 @@ export default {
|
|||
return; // Allow keyboard scrolling inside modal
|
||||
}
|
||||
|
||||
// Prevent scrolling via keyboard (arrow keys, space, page up/down)
|
||||
// Prevent scrolling via keyboard (arrow keys, space, helppages up/down)
|
||||
const scrollKeys = [32, 33, 34, 35, 36, 37, 38, 39, 40];
|
||||
if (scrollKeys.includes(e.keyCode)) {
|
||||
e.preventDefault();
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
<PhCaretLeft :size="18" /> Previous
|
||||
</button>
|
||||
|
||||
<!-- First page -->
|
||||
<!-- First helppages -->
|
||||
<button
|
||||
v-if="showFirstPage"
|
||||
class="pagination-btn page-number"
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
<!-- First ellipsis -->
|
||||
<span v-if="showFirstEllipsis" class="ellipsis">...</span>
|
||||
|
||||
<!-- Page numbers around current page -->
|
||||
<!-- Page numbers around current helppages -->
|
||||
<button
|
||||
v-for="pageNum in visiblePages"
|
||||
:key="pageNum"
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
<!-- Last ellipsis -->
|
||||
<span v-if="showLastEllipsis" class="ellipsis">...</span>
|
||||
|
||||
<!-- Last page -->
|
||||
<!-- Last helppages -->
|
||||
<button
|
||||
v-if="showLastPage"
|
||||
class="pagination-btn page-number"
|
||||
|
|
@ -90,7 +90,7 @@ export default {
|
|||
default: 5
|
||||
}
|
||||
},
|
||||
emits: ['page-change'],
|
||||
emits: ['helppages-change'],
|
||||
computed: {
|
||||
visiblePages() {
|
||||
const delta = Math.floor(this.maxVisiblePages / 2);
|
||||
|
|
@ -130,7 +130,7 @@ export default {
|
|||
methods: {
|
||||
goToPage(pageNumber) {
|
||||
if (pageNumber >= 1 && pageNumber <= this.pageCount && pageNumber !== this.page) {
|
||||
this.$emit('page-change', pageNumber);
|
||||
this.$emit('helppages-change', pageNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ export default {
|
|||
case 'warning':
|
||||
return 'secondary'
|
||||
case 'info':
|
||||
return 'primary'
|
||||
return 'secondary'
|
||||
case 'error':
|
||||
return 'exception'
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
<template>
|
||||
<div class="item-container" :class="{'selected-item': selected}">
|
||||
<flag :iso="isoCode" size="l"></flag>
|
||||
<div class="supplier-item-text">
|
||||
<div class="supplier-item-name"> <span class="user-icon" v-if="isUserSupplier"><ph-user weight="fill" ></ph-user></span> {{name}}</div>
|
||||
<div class="supplier-item-address">{{ address }}</div>
|
||||
|
||||
<div class="supplier-content">
|
||||
<flag :iso="isoCode" size="l"></flag>
|
||||
<div class="supplier-item-text">
|
||||
<div class="supplier-item-name">
|
||||
<span class="user-icon" v-if="isUserSupplier">
|
||||
<ph-user weight="fill"></ph-user>
|
||||
</span>
|
||||
{{name}}
|
||||
</div>
|
||||
<div class="supplier-item-address">{{ address }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<icon-button v-if="showTrash" icon="trash" @click="deleteClick"></icon-button>
|
||||
</div>
|
||||
|
|
@ -65,14 +71,13 @@ export default {
|
|||
|
||||
.item-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start; /* Statt space-between */
|
||||
align-items: center;
|
||||
padding: 3.6rem 3.6rem;
|
||||
background: white;
|
||||
border-radius: 0.8rem;
|
||||
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
gap: 2.4rem;
|
||||
flex: 0 0 50rem;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
|
@ -97,6 +102,20 @@ export default {
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.supplier-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2.4rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.supplier-item-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
|
||||
.supplier-item-name {
|
||||
font-size: 1.6rem;
|
||||
|
|
@ -112,4 +131,8 @@ export default {
|
|||
color: #6b7280;
|
||||
}
|
||||
|
||||
.item-container > .icon-button {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -20,6 +20,9 @@
|
|||
<div class="calculation-list-status-cell">
|
||||
<basic-badge :variant="variant" :icon="variantIcon">{{ premise.state }}</basic-badge>
|
||||
</div>
|
||||
<div class="calculation-list-date-cell">
|
||||
{{ buildDate(this.premise.created_at, true)}}
|
||||
</div>
|
||||
<div class="calculation-list-actions-cell">
|
||||
<icon-button :disabled="!isDraft" icon="pencil-simple" @click="editClick" help-text="Edit this calculation"
|
||||
help-text-position="left"></icon-button>
|
||||
|
|
@ -39,7 +42,7 @@ import Checkbox from "@/components/UI/Checkbox.vue";
|
|||
import {mapStores} from "pinia";
|
||||
import {usePremiseStore} from "@/store/premise.js";
|
||||
import Flag from "@/components/UI/Flag.vue";
|
||||
import {UrlSafeBase64} from "@/common.js";
|
||||
import {buildDate, UrlSafeBase64} from "@/common.js";
|
||||
|
||||
export default {
|
||||
name: "CalculationListItem",
|
||||
|
|
@ -68,7 +71,7 @@ export default {
|
|||
return 'grey';
|
||||
} else if (this.premise.state === 'COMPLETED') {
|
||||
return 'primary';
|
||||
} else if (this.premise.state === 'EXCEPTION') {
|
||||
} else if (this.premise.state === 'COMPLETED' && this.premise.calculation_state === 'EXCEPTION') {
|
||||
return 'exception';
|
||||
} else {
|
||||
return 'grey';
|
||||
|
|
@ -95,6 +98,7 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
buildDate,
|
||||
updateCheckBox(checked) {
|
||||
this.$emit('updateCheckbox', {checked: checked, id: this.id});
|
||||
},
|
||||
|
|
@ -124,7 +128,7 @@ export default {
|
|||
|
||||
.calculation-list-row {
|
||||
display: grid;
|
||||
grid-template-columns: 6rem 1fr 2fr 14rem 10rem;
|
||||
grid-template-columns: 6rem 1fr 2fr 14rem 20rem 10rem;
|
||||
gap: 1.6rem;
|
||||
padding: 1.6rem;
|
||||
border-bottom: 0.16rem solid #f3f4f6;
|
||||
|
|
@ -203,6 +207,14 @@ export default {
|
|||
justify-content: start;
|
||||
}
|
||||
|
||||
.calculation-list-date-cell {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
color: #6b7280;
|
||||
font-size: 1.4rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.calculation-list-actions-cell {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
|
|
|
|||
212
src/frontend/src/components/layout/calculation/TheDashboard.vue
Normal file
212
src/frontend/src/components/layout/calculation/TheDashboard.vue
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
<template>
|
||||
<div class="dashboard-container">
|
||||
<!-- Total Calculations -->
|
||||
<box>
|
||||
<div class="dashboard-box">
|
||||
<div class="dashboard-box-icon dashboard-box-icon--primary">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 256 256">
|
||||
<polygon points="32 80 128 136 224 80 128 24 32 80" fill="currentColor" opacity="0.2"/>
|
||||
<polygon points="32 128 128 184 224 128 128 72 32 128" fill="currentColor" opacity="0.2"/>
|
||||
<polygon points="32 176 128 232 224 176 128 120 32 176" fill="currentColor" opacity="0.2"/>
|
||||
<rect width="256" height="256" fill="none"/><polyline points="32 176 128 232 224 176" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="32 128 128 184 224 128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polygon points="32 80 128 136 224 80 128 24 32 80" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
|
||||
</div>
|
||||
<div class="dashboard-box-info">
|
||||
<div v-if="completed !== null" class="dashboard-box-number">{{ completed }}</div>
|
||||
<div v-else class="dashboard-box-number"><spinner size="s"/></div>
|
||||
<div class="dashboard-box-number-text">Completed</div>
|
||||
</div>
|
||||
</div>
|
||||
</box>
|
||||
|
||||
<!-- Draft Calculations -->
|
||||
<box>
|
||||
<div class="dashboard-box">
|
||||
<div class="dashboard-box-icon dashboard-box-icon--primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="42" height="42" viewBox="0 0 24 24">
|
||||
<rect x="9" y="-1" width="8" height="24" fill="currentcolor" opacity="0.2" transform="rotate(45 13 11)"/>
|
||||
<path d="M14.078 4.232l-12.64 12.639-1.438 7.129 7.127-1.438 12.641-12.64-5.69-5.69zm-10.369 14.893l-.85-.85 11.141-11.125.849.849-11.14 11.126zm2.008 2.008l-.85-.85 11.141-11.125.85.85-11.141 11.125zm18.283-15.444l-2.816 2.818-5.691-5.691 2.816-2.816 5.691 5.689z"/></svg>
|
||||
</div>
|
||||
<div class="dashboard-box-info">
|
||||
<div v-if="drafts !== null" class="dashboard-box-number">{{ drafts }}</div>
|
||||
<div v-else class="dashboard-box-number"><spinner size="s"/></div>
|
||||
<div class="dashboard-box-number-text">Drafts</div>
|
||||
</div>
|
||||
</div>
|
||||
</box>
|
||||
|
||||
<!-- Running Calculations -->
|
||||
<box>
|
||||
<div class="dashboard-box">
|
||||
<div class="dashboard-box-icon dashboard-box-icon--primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24">
|
||||
<circle cx="14" cy="14" r="10" fill="currentcolor" opacity="0.2"/>
|
||||
<path fill="currentcolor" d="M15.91 13.34l2.636-4.026-.454-.406-3.673 3.099c-.675-.138-1.402.068-1.894.618-.736.823-.665 2.088.159 2.824.824.736 2.088.665 2.824-.159.492-.55.615-1.295.402-1.95zm-3.91-10.646v-2.694h4v2.694c-1.439-.243-2.592-.238-4 0zm8.851 2.064l1.407-1.407 1.414 1.414-1.321 1.321c-.462-.484-.964-.927-1.5-1.328zm-18.851 4.242h8v2h-8v-2zm-2 4h8v2h-8v-2zm3 4h7v2h-7v-2zm21-3c0 5.523-4.477 10-10 10-2.79 0-5.3-1.155-7.111-3h3.28c1.138.631 2.439 1 3.831 1 4.411 0 8-3.589 8-8s-3.589-8-8-8c-1.392 0-2.693.369-3.831 1h-3.28c1.811-1.845 4.321-3 7.111-3 5.523 0 10 4.477 10 10z"/>
|
||||
</svg> </div>
|
||||
<div class="dashboard-box-info">
|
||||
<div v-if="running !== null" class="dashboard-box-number">{{ running }}</div>
|
||||
<div v-else class="dashboard-box-number"><spinner size="s"/></div>
|
||||
<div class="dashboard-box-number-text">Running</div>
|
||||
</div>
|
||||
</div>
|
||||
</box>
|
||||
|
||||
<!-- Failed Calculations -->
|
||||
<box>
|
||||
<div class="dashboard-box">
|
||||
<div class="dashboard-box-icon dashboard-box-icon--primary">
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24 4L44 40H4L24 4Z" fill="currentColor" opacity="0.2"/>
|
||||
<path d="M24 4L44 40H4L24 4Z" stroke="currentColor" stroke-width="3" stroke-linejoin="round" fill="none"/>
|
||||
<path d="M24 18V28" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
|
||||
<circle cx="24" cy="34" r="1.5" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="dashboard-box-info">
|
||||
<div v-if="failed !== null" class="dashboard-box-number">{{ failed }}</div>
|
||||
<div v-else class="dashboard-box-number"><spinner size="s"/></div>
|
||||
<div class="dashboard-box-number-text">Failed</div>
|
||||
</div>
|
||||
</div>
|
||||
</box>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Box from "@/components/UI/Box.vue";
|
||||
import { PhStack, PhPencilSimple, PhHourglassMedium, PhWarning } from "@phosphor-icons/vue";
|
||||
import {useDashboardStore} from "@/store/dashboard.js";
|
||||
import {mapStores} from "pinia";
|
||||
import {usePremiseStore} from "@/store/premise.js";
|
||||
import {useActiveUserStore} from "@/store/activeuser.js";
|
||||
import Spinner from "@/components/UI/Spinner.vue";
|
||||
|
||||
export default {
|
||||
name: "TheDashboard",
|
||||
components: {
|
||||
Spinner,
|
||||
PhStack,
|
||||
PhPencilSimple,
|
||||
PhHourglassMedium,
|
||||
PhWarning,
|
||||
Box
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useDashboardStore),
|
||||
completed() {
|
||||
return this.dashboardStore.completed
|
||||
},
|
||||
drafts() {
|
||||
return this.dashboardStore.drafts
|
||||
},
|
||||
running() {
|
||||
return this.dashboardStore.running
|
||||
},
|
||||
failed() {
|
||||
return this.dashboardStore.failed
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.dashboardStore.load();
|
||||
},
|
||||
mounted() {
|
||||
this.dashboardStore.startPulling();
|
||||
},
|
||||
unmounted() {
|
||||
this.dashboardStore.stopPulling();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1.6rem;
|
||||
margin: 1.6rem 0 3.6rem 0;
|
||||
}
|
||||
|
||||
.dashboard-box {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 2rem 2.4rem;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.dashboard-box-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dashboard-box-more-link {
|
||||
color: #002F54;
|
||||
font-weight: 400;
|
||||
font-size: 1.2rem;
|
||||
text-decoration: none;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.dashboard-box-icon--primary {
|
||||
color: #002F54;
|
||||
}
|
||||
|
||||
.dashboard-box-icon--draft {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.dashboard-box-icon--running {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.dashboard-box-icon--error {
|
||||
color: #BC2B72;
|
||||
}
|
||||
|
||||
.dashboard-box-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.dashboard-box-number {
|
||||
font-weight: 500;
|
||||
color: #002F54;
|
||||
font-size: 3.2rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.dashboard-box-number-text {
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05rem;
|
||||
font-size: 1.4rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1024px) {
|
||||
.dashboard-container {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.dashboard-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-box {
|
||||
padding: 1.6rem 2rem;
|
||||
}
|
||||
|
||||
.dashboard-box-number {
|
||||
font-size: 2.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
<template>
|
||||
<div class="apps-container">
|
||||
<div class="app-list-actions">
|
||||
|
||||
</div>
|
||||
<div class="app-list-header">
|
||||
<div>App</div>
|
||||
<div>Groups</div>
|
||||
|
|
@ -8,13 +10,20 @@
|
|||
</div>
|
||||
<div class="app-list">
|
||||
|
||||
<app-list-item v-for="app in apps" :app="app" @delete-app="deleteApp"></app-list-item>
|
||||
<app-list-item v-for="app in apps" :app="app" @delete-app="deleteApp" @export-app="exportApp"></app-list-item>
|
||||
|
||||
</div>
|
||||
|
||||
<modal :state="modalState">
|
||||
<add-app @close="closeModal"></add-app>
|
||||
</modal>
|
||||
<basic-button icon="Plus" @click="modalState = true">New App</basic-button>
|
||||
|
||||
|
||||
<div class="app-list-actions">
|
||||
|
||||
<basic-button icon="Upload" @click="importApp">Import</basic-button>
|
||||
<basic-button icon="Plus" @click="modalState = true">New App</basic-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
|
@ -27,6 +36,8 @@ import {mapStores} from "pinia";
|
|||
import {useAppsStore} from "@/store/apps.js";
|
||||
import Modal from "@/components/UI/Modal.vue";
|
||||
import AddApp from "@/components/layout/config/AddApp.vue";
|
||||
import Dropdown from "@/components/UI/Dropdown.vue";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
|
||||
export default {
|
||||
name: "Apps",
|
||||
|
|
@ -36,7 +47,7 @@ export default {
|
|||
default: false
|
||||
}
|
||||
},
|
||||
components: {AddApp, Modal, AppListItem, BasicButton},
|
||||
components: {IconButton, Dropdown, AddApp, Modal, AppListItem, BasicButton},
|
||||
computed: {
|
||||
...mapStores(useAppsStore),
|
||||
apps() {
|
||||
|
|
@ -45,7 +56,8 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
modalState: false
|
||||
modalState: false,
|
||||
exportedApp: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -57,6 +69,62 @@ export default {
|
|||
},
|
||||
deleteApp(id) {
|
||||
this.appsStore.deleteApp(id);
|
||||
},
|
||||
async exportApp(id) {
|
||||
const response = await this.appsStore.exportApp(id);
|
||||
const app = this.appsStore.getById(id);
|
||||
|
||||
if(response?.data) {
|
||||
const base64String = response.data;
|
||||
|
||||
const blob = new Blob([base64String], { type: 'text/plain' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${app.name}.app`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
},
|
||||
async importApp() {
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.app';
|
||||
|
||||
input.onchange = async (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
try {
|
||||
const fileContent = await this.readFileContent(file);
|
||||
await this.appsStore.importApp(fileContent);
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// File Dialog öffnen
|
||||
input.click();
|
||||
},
|
||||
|
||||
readFileContent(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
resolve(e.target.result);
|
||||
};
|
||||
|
||||
reader.onerror = (error) => {
|
||||
reject(error);
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
|
|
@ -85,6 +153,13 @@ export default {
|
|||
border-bottom: 0.1rem solid #E3EDFF;
|
||||
}
|
||||
|
||||
.app-list-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 2rem;
|
||||
gap: 1.6rem
|
||||
}
|
||||
|
||||
.app-list {
|
||||
margin-bottom: 2.4rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,26 @@
|
|||
<template>
|
||||
<div class="nodes-container">
|
||||
|
||||
<modal :state="showModal">
|
||||
<div class="node-modal-container">
|
||||
<div class="node-header">
|
||||
<h3 class="sub-header"> {{ node.name }}
|
||||
</h3>
|
||||
<icon-button icon="x" @click="showModal = false"></icon-button>
|
||||
</div>
|
||||
<div class="node-body">
|
||||
<div class="node-address"><flag :iso="node.country.iso_code" />{{ node.address }}</div>
|
||||
<div class="supplier-map" v-if="node.location">
|
||||
<open-street-map-embed :coordinates="node.location" :zoom="5" width="100%" 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>
|
||||
</modal>
|
||||
|
||||
<table-view ref="tableViewRef" :data-source="fetch" :columns="nodeColumns" :page="pagination.page"
|
||||
:page-size="pageSize" :page-count="pagination.pageCount"
|
||||
:total-count="pagination.totalCount"></table-view>
|
||||
:total-count="pagination.totalCount" @row-click="showDetails" :mouse-over="true"></table-view>
|
||||
|
||||
</div>
|
||||
|
||||
|
|
@ -12,6 +30,12 @@
|
|||
import TableView from "@/components/UI/TableView.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {useNodeStore} from "@/store/node.js";
|
||||
import Modal from "@/components/UI/Modal.vue";
|
||||
import ErrorModal from "@/components/layout/error/ErrorModal.vue";
|
||||
import TabContainer from "@/components/UI/TabContainer.vue";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import OpenStreetMapEmbed from "@/components/UI/OpenStreetMapEmbed.vue";
|
||||
import Flag from "@/components/UI/Flag.vue";
|
||||
|
||||
export default {
|
||||
name: "Nodes",
|
||||
|
|
@ -28,7 +52,7 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
components: {TableView},
|
||||
components: {Flag, OpenStreetMapEmbed, IconButton, TabContainer, ErrorModal, Modal, TableView},
|
||||
computed: {
|
||||
...mapStores(useNodeStore),
|
||||
},
|
||||
|
|
@ -40,10 +64,16 @@ export default {
|
|||
await this.nodeStore.setQuery(query);
|
||||
this.pagination = this.nodeStore.pagination;
|
||||
return this.nodeStore.nodes;
|
||||
},
|
||||
showDetails(node) {
|
||||
this.node = node;
|
||||
this.showModal = true;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showModal: false,
|
||||
node: null,
|
||||
nodeColumns: [
|
||||
{
|
||||
key: 'external_mapping_id',
|
||||
|
|
@ -95,4 +125,39 @@ export default {
|
|||
padding: 2.4rem;
|
||||
}
|
||||
|
||||
|
||||
.node-modal-container {
|
||||
height: 40rem;
|
||||
width: 60rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden; /* Verhindert Overflow */
|
||||
}
|
||||
|
||||
.node-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0; /* Header soll nicht schrumpfen */
|
||||
padding-bottom: 1.6rem; /* Optional: etwas Abstand */
|
||||
}
|
||||
|
||||
.node-body {
|
||||
flex: 1; /* Nimmt den restlichen Platz ein */
|
||||
min-height: 0; /* Wichtig für Flexbox-Scrolling */
|
||||
overflow: hidden; /* Container selbst soll nicht scrollen */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.node-address {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
|
||||
color: #6b7280;
|
||||
font-size: 1.4rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -178,15 +178,16 @@ export default {
|
|||
}
|
||||
|
||||
if (this.property.data_type === 'INT') {
|
||||
this.value = parseNumberFromString(this.value, 0);
|
||||
this.value = parseNumberFromString(this.value, 0, true);
|
||||
}
|
||||
|
||||
if (this.property.data_type === 'PERCENTAGE') {
|
||||
this.value = parseNumberFromString(this.value, 4);
|
||||
this.value = parseNumberFromString(this.value, 4, true);
|
||||
}
|
||||
|
||||
if (this.property.data_type === 'CURRENCY') {
|
||||
this.value = parseNumberFromString(this.value, 2);
|
||||
this.value = parseNumberFromString(this.value, 2, true);
|
||||
console.log(this.property.name, " parsed from 'currency' property: '", this.value, "'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<div class="caption-column">Oversea share [%]</div>
|
||||
<div class="caption-column">Overseas share [%]</div>
|
||||
<div class="input-column">
|
||||
<div class="text-container">
|
||||
<input ref="overseaShareInput" @keydown.enter="handleEnter('overseaShareInput', $event)" :value="overSeaSharePercent" @blur="validateOverSeaShare" class="input-field"
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export default {
|
|||
},
|
||||
repackaging: {
|
||||
get() {
|
||||
return this.destination?.repackaging_costs?.toFixed(2) ?? '0.00';
|
||||
return this.destination?.repackaging_costs?.toFixed(2) ?? '';
|
||||
},
|
||||
set(value) {
|
||||
return this.destination && (this.destination.repackaging_costs = value);
|
||||
|
|
@ -66,7 +66,7 @@ export default {
|
|||
},
|
||||
handling: {
|
||||
get() {
|
||||
return this.destination?.handling_costs?.toFixed(2) ?? '0.00';
|
||||
return this.destination?.handling_costs?.toFixed(2) ?? '';
|
||||
},
|
||||
set(value) {
|
||||
return this.destination && (this.destination.handling_costs = value);
|
||||
|
|
@ -74,7 +74,7 @@ export default {
|
|||
},
|
||||
disposal: {
|
||||
get() {
|
||||
return this.destination?.disposal_costs?.toFixed(2) ?? '0.00';
|
||||
return this.destination?.disposal_costs?.toFixed(2) ?? '';
|
||||
},
|
||||
set(value) {
|
||||
return this.destination && (this.destination.disposal_costs = value);
|
||||
|
|
|
|||
38
src/frontend/src/components/layout/help/Help.vue
Normal file
38
src/frontend/src/components/layout/help/Help.vue
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<template>
|
||||
<div>
|
||||
<video controls width="100%">
|
||||
<source :src="videoUrl" type="video/mp4">
|
||||
</video>
|
||||
<div v-html="text"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
import {mapStores} from "pinia";
|
||||
import {useHelpStore} from "@/store/help.js";
|
||||
|
||||
export default {
|
||||
name: "Help",
|
||||
data() {
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useHelpStore),
|
||||
currentPage() {
|
||||
return this.helpStore.currentPage;
|
||||
},
|
||||
videoUrl() {
|
||||
return this.helpStore.videoUrl;
|
||||
},
|
||||
text() {
|
||||
return this.helpStore.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
107
src/frontend/src/components/layout/help/HelpText.vue
Normal file
107
src/frontend/src/components/layout/help/HelpText.vue
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<template>
|
||||
<div class="help">
|
||||
<div class="help-text-content" v-html="text"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapStores} from "pinia";
|
||||
import {useHelpStore} from "@/store/help.js";
|
||||
|
||||
export default {
|
||||
name: "HelpText",
|
||||
computed: {
|
||||
...mapStores(useHelpStore),
|
||||
text() {
|
||||
return this.helpStore.text;
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.help {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.help-text-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
|
||||
/* Text content styling */
|
||||
.help-text-content :deep(blockquote) {
|
||||
margin: 0 0 2rem 0;
|
||||
padding: 1.6rem;
|
||||
background: #c3cfdf;
|
||||
color: #002F54;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
border-radius: 0.4rem;
|
||||
}
|
||||
|
||||
.help-text-content :deep(blockquote p) {
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
color: #002F54;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.help-text-content :deep(h3) {
|
||||
margin: 3rem 0 1.6rem 0;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
color: #6B869C;
|
||||
}
|
||||
|
||||
.help-text-content :deep(h3:first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.help-text-content :deep(p) {
|
||||
margin: 0 0 1.6rem 0;
|
||||
line-height: 1.6;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.help-text-content :deep(strong) {
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.help-text-content :deep(img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 1.6rem 0;
|
||||
border-radius: 0.8rem;
|
||||
box-shadow: 0 0.2rem 0.8rem rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.help-text-content :deep(ul) {
|
||||
margin: 0 0 1.6rem 0;
|
||||
padding-left: 2rem;
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.help-text-content :deep(li) {
|
||||
margin: 0.8rem 0;
|
||||
line-height: 1.6;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.help-text-content :deep(li::marker) {
|
||||
color: #c3cfdf;
|
||||
}
|
||||
</style>
|
||||
32
src/frontend/src/components/layout/help/HelpVideo.vue
Normal file
32
src/frontend/src/components/layout/help/HelpVideo.vue
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<template>
|
||||
<div class="help-content-container">
|
||||
<video controls width="80%" :key="videoUrl">
|
||||
<source :src="videoUrl" type="video/mp4">
|
||||
</video>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {mapStores} from "pinia";
|
||||
import {useHelpStore} from "@/store/help.js";
|
||||
|
||||
export default {
|
||||
name: "HelpVideo",
|
||||
computed: {
|
||||
...mapStores(useHelpStore),
|
||||
videoUrl() {
|
||||
return this.helpStore.videoUrl;
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.help-content-container {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
57
src/frontend/src/components/layout/help/TheHelpMenu.vue
Normal file
57
src/frontend/src/components/layout/help/TheHelpMenu.vue
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<div >
|
||||
<ul class="help-menu">
|
||||
<li v-for="page in pages" class="help-menu-element-container" @click="$emit('changePage', page.page)"><div :class="{'help-menu-element-active': isActive(page.page)}" class="help-menu-element">{{ page.title }}</div></li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "TheHelpMenu",
|
||||
emits: ['changePage'],
|
||||
props: {
|
||||
currentPage: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
pages: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isActive(page) {
|
||||
return page === this.currentPage;
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.help-menu-element {
|
||||
font-weight: 400;
|
||||
font-size: 1.4rem;
|
||||
color: #6B869C;
|
||||
height: 25px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.help-menu-element:hover, .help-menu-element-active {
|
||||
color: #002F54;
|
||||
border-bottom: 5px solid #5AF0B4;
|
||||
/* AE0055 */
|
||||
height: 25px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.help-menu-element-container {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
124
src/frontend/src/components/layout/help/TheHelpSystem.vue
Normal file
124
src/frontend/src/components/layout/help/TheHelpSystem.vue
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
<template>
|
||||
<teleport to="body">
|
||||
<modal :z-index="9001" :state="showHelp">
|
||||
<div class="help-modal-container">
|
||||
<div class="help-modal-header">
|
||||
<icon-button icon="x" @click="helpStore.closeHelp()"/>
|
||||
</div>
|
||||
<div class="help-container">
|
||||
<div class="help-menu-container">
|
||||
<the-help-menu @changePage="updatePage" :currentPage="currentPage" :pages="pages"/>
|
||||
</div>
|
||||
<div class="help-content-container">
|
||||
<h2 class="page-header">{{ title }}</h2>
|
||||
<tab-container :tabs="tabsConfig"></tab-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from "@/components/UI/Modal.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import Help from "@/components/layout/help/Help.vue";
|
||||
import {useHelpStore} from "@/store/help.js";
|
||||
import TheHelpMenu from "@/components/layout/help/TheHelpMenu.vue";
|
||||
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import TabContainer from "@/components/UI/TabContainer.vue";
|
||||
import {markRaw} from "vue";
|
||||
import Nodes from "@/components/layout/config/Nodes.vue";
|
||||
import HelpVideo from "@/components/layout/help/HelpVideo.vue";
|
||||
import HelpText from "@/components/layout/help/HelpText.vue";
|
||||
|
||||
export default {
|
||||
name: "TheHelpSystem",
|
||||
components: {TabContainer, IconButton, BasicButton, TheHelpMenu, Help, Modal},
|
||||
data() {
|
||||
return {
|
||||
tabsConfig: [
|
||||
{
|
||||
title: 'Video',
|
||||
component: markRaw(HelpVideo),
|
||||
props: {isSelected: false},
|
||||
},
|
||||
{
|
||||
title: 'Text',
|
||||
component: markRaw(HelpText),
|
||||
props: {isSelected: false},
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useHelpStore),
|
||||
showHelp() {
|
||||
return this.helpStore.showHelp;
|
||||
},
|
||||
currentPage() {
|
||||
return this.helpStore.currentPage;
|
||||
},
|
||||
title() {
|
||||
return this.helpStore.title;
|
||||
},
|
||||
pages() {
|
||||
return this.helpStore.pages;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async updatePage(page) {
|
||||
await this.helpStore.getContent(page);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.helpStore.loadPages();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
|
||||
.help-modal-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
width: min(80vw, 180rem);
|
||||
height: min(80vh, 120rem);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.help-modal-header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.help-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 2.4rem;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.help-menu-container {
|
||||
flex: 0 0 auto;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.help-content-container {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -7,42 +7,53 @@
|
|||
<report-chart
|
||||
title=""
|
||||
:mek_a="report.costs.mek_a.total"
|
||||
:logistics_costs="report.risk.mek_b.total-report.costs.mek_a.total"
|
||||
:chance_cost="report.risk.opportunity_scenario.total"
|
||||
:risk_cost="report.risk.risk_scenario.total"
|
||||
:logistics_costs="report.overview.mek_b.total-report.costs.mek_a.total"
|
||||
:chance_cost="report.overview.opportunity_scenario.total"
|
||||
:risk_cost="report.overview.risk_scenario.total"
|
||||
:scale="chartScale"
|
||||
></report-chart>
|
||||
</div>
|
||||
|
||||
<!-- summary -->
|
||||
<div class="box-gap">
|
||||
<collapsible-box :is-collapsable="false" variant="border" title="Overview" size="m" :stretch-content="true">
|
||||
<collapsible-box :is-collapsable="false" variant="border" title="Summary" size="m" :stretch-content="true">
|
||||
<div class="report-content-container--3-col">
|
||||
|
||||
<div class="report-content-row">
|
||||
<div class="report-content-row-highlight">MEK B</div>
|
||||
<div class="report-content-data-cell report-content-row-highlight">{{ report.risk.mek_b.total.toFixed(2) }} €</div>
|
||||
<div class="report-content-data-cell"></div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Opportunity scenario</div>
|
||||
<div class="report-content-data-cell">{{ report.risk.opportunity_scenario.total.toFixed(2) }} €</div>
|
||||
<div>MEK A</div>
|
||||
<div class="report-content-data-cell">{{ report.overview.mek_a.total.toFixed(2) }} €</div>
|
||||
<div class="report-content-data-cell">{{
|
||||
`${(report.risk.opportunity_scenario.percentage * 100).toFixed(2)} %`
|
||||
`${(report.overview.mek_a.percentage * 100).toFixed(2)} %`
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Risk scenario</div>
|
||||
<div class="report-content-data-cell">{{ report.risk.risk_scenario.total.toFixed(2) }} €</div>
|
||||
<div class="report-content-data-cell">{{ `${(report.risk.risk_scenario.percentage * 100).toFixed(2)} %`}}</div>
|
||||
<div>Logistics cost</div>
|
||||
<div class="report-content-data-cell">{{ report.overview.logistics.total.toFixed(2) }} €</div>
|
||||
<div class="report-content-data-cell">{{
|
||||
`${(report.overview.logistics.percentage * 100).toFixed(2)} %`
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="report-content-row">
|
||||
<div class="report-content-row-highlight">MEK B</div>
|
||||
<div class="report-content-data-cell report-content-row-highlight">{{
|
||||
report.overview.mek_b.total.toFixed(2)
|
||||
}} €
|
||||
</div>
|
||||
<div class="report-content-data-cell report-content-row-highlight">{{
|
||||
`${(report.overview.mek_b.percentage * 100).toFixed(2)} %`
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</collapsible-box>
|
||||
</div>
|
||||
<!-- weighted cost breakdown-->
|
||||
<div class="box-gap">
|
||||
<collapsible-box :is-collapsable="false" variant="border" title="Weighted cost breakdown" size="m"
|
||||
:stretch-content="true">
|
||||
|
|
@ -142,18 +153,155 @@
|
|||
<div class="report-content-data-cell">{{ (report.costs.capital.percentage * 100).toFixed(2) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div class="report-content-row-highlight">Total</div>
|
||||
<div class="report-content-data-cell report-content-row-highlight">{{
|
||||
report.costs.total.total.toFixed(2)
|
||||
}}
|
||||
</div>
|
||||
<div class="report-content-data-cell report-content-row-highlight">
|
||||
{{ (report.costs.total.percentage * 100).toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</collapsible-box>
|
||||
</div>
|
||||
|
||||
<!-- all time high/low container rate-->
|
||||
<div class="box-gap">
|
||||
<collapsible-box :is-collapsable="true" variant="border" title="Transport costs fluctuations"
|
||||
:initially-collapsed="true"
|
||||
:stretch-content="true">
|
||||
|
||||
<div class="box-gap" :key="premise.id" v-for="(premise, idx) in report.premises">
|
||||
<div class="report-content-container--3-col-2">
|
||||
|
||||
<collapsible-box class="report-content-container" variant="border" :title="premise.destination.name"
|
||||
<div class="report-content-row">
|
||||
<div></div>
|
||||
<div class="report-content-data-header-cell">total [€]</div>
|
||||
<div class="report-content-data-header-cell">of MEK B [%]</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div class="">Current scenario</div>
|
||||
<div class="report-content-data-cell">{{
|
||||
report.overview.mek_b.total.toFixed(2)
|
||||
}} €
|
||||
</div>
|
||||
<div class="report-content-data-cell">{{
|
||||
`${(report.overview.mek_b.percentage * 100).toFixed(2)}`
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Opportunity scenario</div>
|
||||
<div class="report-content-data-cell">{{ report.overview.opportunity_scenario.total.toFixed(2) }} €</div>
|
||||
<div class="report-content-data-cell">{{
|
||||
`${(report.overview.opportunity_scenario.percentage * 100).toFixed(2)}`
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Risk scenario</div>
|
||||
<div class="report-content-data-cell">{{ report.overview.risk_scenario.total.toFixed(2) }} €</div>
|
||||
<div class="report-content-data-cell">
|
||||
{{ `${(report.overview.risk_scenario.percentage * 100).toFixed(2)}` }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</collapsible-box>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- material and handling unit-->
|
||||
<div class="box-gap">
|
||||
<collapsible-box :is-collapsable="false" variant="border" title="Material" size="m"
|
||||
:stretch-content="true">
|
||||
|
||||
<div class="report-content-container--2-col">
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Part number</div>
|
||||
<div class="report-content-data-cell"> {{ report.material.part_number }}</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>HS code</div>
|
||||
<div class="report-content-data-cell"> {{ report.premises.hs_code }}</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Tariff rate</div>
|
||||
<div class="report-content-data-cell"> {{ (report.premises.tariff_rate * 100).toFixed(2) }}%</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Oversea share</div>
|
||||
<div class="report-content-data-cell">{{ (report.premises.oversea_share * 100).toFixed(2) }}%</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row" v-if="(report.premises.air_freight_share ?? null) !== null">
|
||||
<div>Airfreight share</div>
|
||||
<div class="report-content-data-cell">{{ (report.premises.air_freight_share * 100).toFixed(2) }}%</div>
|
||||
</div>
|
||||
<div class="report-content-row">
|
||||
<div>Safety stock [w-days]</div>
|
||||
<div class="report-content-data-cell">{{ report.premises.safety_stock }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="report-sub-header">Handling unit</div>
|
||||
|
||||
<div class="report-content-container--2-col">
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Dimensions [{{ report.premises.dimension_unit }}]</div>
|
||||
<div class="report-content-data-cell">{{
|
||||
toFixedDimension(report.premises.length, report.premises.dimension_unit)
|
||||
}} x
|
||||
{{ toFixedDimension(report.premises.width, report.premises.dimension_unit) }} x
|
||||
{{ toFixedDimension(report.premises.height, report.premises.dimension_unit) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Weight [{{ report.premises.weight_unit }}]</div>
|
||||
<div class="report-content-data-cell">{{
|
||||
toFixedWeight(report.premises.weight, report.premises.weight_unit)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Unit count</div>
|
||||
<div class="report-content-data-cell">{{ report.premises.hu_unit_count }}</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Mixed transport</div>
|
||||
<div class="report-content-data-cell">{{ report.premises.mixable ? 'Yes' : 'No' }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</collapsible-box>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- destinations -->
|
||||
<div class="box-gap" :key="destination.id" v-for="(destination, idx) in report.destinations">
|
||||
|
||||
<collapsible-box class="report-content-container" variant="border" :title="destination.destination.name"
|
||||
:stretch-content="true" :initially-collapsed="true">
|
||||
<div>
|
||||
<report-route :sections="premise.sections" :destination="premise.destination"
|
||||
<report-route :sections="destination.sections" :destination="destination.destination"
|
||||
:route-section-scale="routeSectionScale[idx]"></report-route>
|
||||
|
||||
<div class="report-sub-header">General</div>
|
||||
|
|
@ -162,69 +310,19 @@
|
|||
|
||||
<div class="report-content-row">
|
||||
<div>Annual Quantity</div>
|
||||
<div class="report-content-data-cell">{{ premise.annual_quantity }}</div>
|
||||
<div class="report-content-data-cell">{{ destination.annual_quantity }}</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>HS code</div>
|
||||
<div class="report-content-data-cell">{{ premise.hs_code }}</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Tariff rate</div>
|
||||
<div class="report-content-data-cell">{{ (premise.tariff_rate * 100).toFixed(2) }}%</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Oversea share</div>
|
||||
<div class="report-content-data-cell">{{ (premise.oversea_share * 100).toFixed(2) }}%</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row" v-if="(premise.air_freight_share ?? null) !== null">
|
||||
<div>Airfreight share</div>
|
||||
<div class="report-content-data-cell">{{ (premise.air_freight_share * 100).toFixed(2) }}%</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Transit time [days]</div>
|
||||
<div class="report-content-data-cell">{{ premise.transport_time }}</div>
|
||||
<div class="report-content-data-cell">{{ destination.transport_time }}</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Safety stock [w-days]</div>
|
||||
<div class="report-content-data-cell">{{ premise.safety_stock }}</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="report-sub-header">Handling unit</div>
|
||||
|
||||
<div class="report-content-container--2-col">
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Dimensions [{{ premise.dimension_unit }}]</div>
|
||||
<div class="report-content-data-cell">{{ toFixedDimension(premise.length, premise.dimension_unit) }} x
|
||||
{{ toFixedDimension(premise.width, premise.dimension_unit) }} x
|
||||
{{ toFixedDimension(premise.height, premise.dimension_unit) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Weight [{{ premise.weight_unit }}]</div>
|
||||
<div class="report-content-data-cell">{{ toFixedWeight(premise.weight, premise.weight_unit) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Unit count</div>
|
||||
<div class="report-content-data-cell">{{ premise.hu_unit_count }}</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Mixed transport</div>
|
||||
<div class="report-content-data-cell">{{ premise.mixed ? 'Yes' : 'No' }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="report-sub-header">Container</div>
|
||||
|
||||
|
|
@ -232,29 +330,33 @@
|
|||
|
||||
<div class="report-content-row">
|
||||
<div>Stacked layers</div>
|
||||
<div class="report-content-data-cell">{{ hasMainRun(premise.sections) ? premise.layer : '-' }}</div>
|
||||
<div class="report-content-data-cell">{{
|
||||
hasMainRunOrD2D(destination.sections) ? destination.layer : '-'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Container unit count</div>
|
||||
<div class="report-content-data-cell">
|
||||
{{ hasMainRun(premise.sections) ? (premise.unit_count * premise.hu_unit_count) : '-' }}
|
||||
{{
|
||||
hasMainRunOrD2D(destination.sections) ? (destination.unit_count * report.premises.hu_unit_count) : '-'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Container type</div>
|
||||
<div class="report-content-data-cell">
|
||||
{{ hasMainRun(premise.sections) ? getContainerTypeName(premise.container_type) : '-' }}
|
||||
{{ hasMainRunOrD2D(destination.sections) ? getContainerTypeName(destination.container_type) : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Limiting factor</div>
|
||||
<div class="report-content-data-cell">
|
||||
{{ hasMainRun(premise.sections) ? premise.weight_exceeded ? 'Weight' : 'Volume' : '-' }}
|
||||
{{ hasMainRunOrD2D(destination.sections) ? destination.weight_exceeded ? 'Weight' : 'Volume' : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -293,8 +395,8 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
hasMainRun(sections) {
|
||||
return sections.some(section => section.transport_type === 'SEA' || section.transport_type === 'RAIL');
|
||||
hasMainRunOrD2D(sections) {
|
||||
return sections.some(section => section.transport_type === 'SEA' || section.transport_type === 'RAIL' || section.rate_type === 'D2D');
|
||||
},
|
||||
shorten(text, length) {
|
||||
if (text !== null && text !== undefined && text.length > length) {
|
||||
|
|
@ -390,6 +492,14 @@ export default {
|
|||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.report-content-container--3-col-2 {
|
||||
display: grid;
|
||||
grid-template-columns: 5fr 3fr 3fr;
|
||||
gap: 1rem;
|
||||
margin-top: 1.6rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.report-content-row {
|
||||
display: contents;
|
||||
color: #6B869C;
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import {
|
|||
PhUpload,
|
||||
PhWarning,
|
||||
PhX,
|
||||
PhExclamationMark, PhMapPin, PhEmpty, PhShippingContainer, PhPackage, PhVectorThree, PhTag
|
||||
PhExclamationMark, PhMapPin, PhEmpty, PhShippingContainer, PhPackage, PhVectorThree, PhTag, PhInfo
|
||||
} from "@phosphor-icons/vue";
|
||||
import {setupSessionRefresh} from "@/store/activeuser.js";
|
||||
|
||||
|
|
@ -81,6 +81,7 @@ app.component("PhMapPin", PhMapPin);
|
|||
app.component("PhPackage", PhPackage);
|
||||
app.component("PhVectorThree", PhVectorThree);
|
||||
app.component("PhTag", PhTag);
|
||||
app.component("PhInfo", PhInfo);
|
||||
|
||||
|
||||
app.use(router);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
<template>
|
||||
<div class="start-calculation-container">
|
||||
<h2 class="page-header">Create Calculation</h2>
|
||||
<div class="start-calculation-header">
|
||||
<div>
|
||||
<h2 class="page-header">Create Calculation</h2>
|
||||
</div>
|
||||
<div class="start-calculation-help">
|
||||
<icon-button v-if="useHelpStore().enableHelp" icon="info"
|
||||
@click="useHelpStore().activateHelp('assistant')"></icon-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="part-numbers-headers">
|
||||
|
|
@ -23,8 +32,15 @@
|
|||
<textarea v-model="partNumberField" name="partNumbers" cols="140" rows="15"></textarea>
|
||||
</div>
|
||||
<div class="part-number-modal-action">
|
||||
<basic-button @click="parsePartNumbers" icon="CloudArrowUp">Analyze input</basic-button>
|
||||
<basic-button @click="closeModal('partNumber')" :show-icon="false" variant="secondary">Cancel</basic-button>
|
||||
<div class="part-number-modal-action-help">
|
||||
<icon-button v-if="useHelpStore().enableHelp" icon="info"
|
||||
@click="useHelpStore().activateHelp('assistant')"></icon-button>
|
||||
</div>
|
||||
<div class="part-number-modal-action-buttons">
|
||||
<basic-button @click="parsePartNumbers" icon="CloudArrowUp">Analyze input</basic-button>
|
||||
<basic-button @click="closeModal('partNumber')" :show-icon="false" variant="secondary">Cancel</basic-button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
|
|
@ -88,11 +104,22 @@ import CreateNewNode from "@/components/layout/node/CreateNewNode.vue";
|
|||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
import {UrlSafeBase64} from "@/common.js";
|
||||
import {useNotificationStore} from "@/store/notification.js";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import {useHelpStore} from "@/store/help.js";
|
||||
|
||||
|
||||
export default {
|
||||
name: "CalculationAssistant",
|
||||
components: {Checkbox, CreateNewNode, Modal, SupplierItem, MaterialItem, BasicButton, AutosuggestSearchbar},
|
||||
components: {
|
||||
IconButton,
|
||||
Checkbox,
|
||||
CreateNewNode,
|
||||
Modal,
|
||||
SupplierItem,
|
||||
MaterialItem,
|
||||
BasicButton,
|
||||
AutosuggestSearchbar
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useNodeStore, useAssistantStore, useNotificationStore),
|
||||
showPartNumberModal() {
|
||||
|
|
@ -108,6 +135,7 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
useHelpStore,
|
||||
setUseExisting(useExisting) {
|
||||
this.assistantStore.setCreateEmpty(!useExisting);
|
||||
},
|
||||
|
|
@ -160,7 +188,7 @@ export default {
|
|||
parsePartNumbers() {
|
||||
this.closeModal('partNumber');
|
||||
|
||||
if(this.partNumberField.trim().length !== 0)
|
||||
if (this.partNumberField.trim().length !== 0)
|
||||
this.assistantStore.getMaterialsAndSuppliers(this.partNumberField);
|
||||
|
||||
this.partNumberField = '';
|
||||
|
|
@ -179,6 +207,22 @@ export default {
|
|||
|
||||
<style scoped>
|
||||
|
||||
.start-calculation-help {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 1.6rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.start-calculation-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.start-calculation-footer-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -232,10 +276,17 @@ textarea {
|
|||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.part-number-modal-action-help {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.part-number-modal-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 1.6rem
|
||||
}
|
||||
|
||||
|
|
@ -246,6 +297,13 @@ textarea {
|
|||
margin-bottom: 1.6rem;
|
||||
}
|
||||
|
||||
.part-number-modal-action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 1.6rem
|
||||
}
|
||||
|
||||
.item-list {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,19 @@
|
|||
<div class="edit-calculation-container"
|
||||
:class="{ 'has-selection': hasSelection, 'apply-filter': applyFilter, 'add-all': addAll }">
|
||||
<div class="header-container">
|
||||
<h2 class="page-header">Mass edit calculation</h2>
|
||||
|
||||
<div class="header-caption-container">
|
||||
<div>
|
||||
<h2 class="page-header">Mass edit calculation</h2>
|
||||
</div>
|
||||
<div class="header-help-container">
|
||||
<icon-button v-if="useHelpStore().enableHelp" icon="info"
|
||||
@click="useHelpStore().activateHelp('mass-edit-basics')"></icon-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="header-controls">
|
||||
<basic-button :show-icon="true"
|
||||
:disabled="disableButtons"
|
||||
|
|
@ -173,6 +185,8 @@ import DestMassCreate from "@/components/layout/edit/destination/mass/DestMassCr
|
|||
import ModalDialog from "@/components/UI/ModalDialog.vue";
|
||||
import destinationEdit from "@/components/layout/edit/destination/DestinationEdit.vue";
|
||||
import logger from "@/logger.js";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import {useHelpStore} from "@/store/help.js";
|
||||
|
||||
|
||||
const COMPONENT_TYPES = {
|
||||
|
|
@ -187,6 +201,7 @@ const COMPONENT_TYPES = {
|
|||
export default {
|
||||
name: "MassEdit",
|
||||
components: {
|
||||
IconButton,
|
||||
ModalDialog,
|
||||
SortButton,
|
||||
Modal,
|
||||
|
|
@ -210,7 +225,7 @@ export default {
|
|||
modalType: null,
|
||||
modalProps: null,
|
||||
editIds: null,
|
||||
processingMessage: "Please wait. Calculating ...",
|
||||
processingMessage: "Please wait. Processing ...",
|
||||
showCalculationModal: false,
|
||||
isInitialLoad: true,
|
||||
modalDialogShow: false,
|
||||
|
|
@ -267,6 +282,9 @@ export default {
|
|||
return this.premiseEditStore.showProcessingModal || this.destinationEditStore.showProcessingModal;
|
||||
},
|
||||
shownProcessingMessage() {
|
||||
if (this.premiseEditStore.showProcessingModal)
|
||||
return "Please wait. Prepare calculation ..."
|
||||
|
||||
return this.processingMessage;
|
||||
}
|
||||
},
|
||||
|
|
@ -295,6 +313,7 @@ export default {
|
|||
window.removeEventListener('keyup', this.handleKeyUp);
|
||||
},
|
||||
methods: {
|
||||
useHelpStore,
|
||||
handleKeyDown(event) {
|
||||
if (event.key === 'Control') {
|
||||
this.isCtrlPressed = true;
|
||||
|
|
@ -441,6 +460,7 @@ export default {
|
|||
async closeEditModalAction(action) {
|
||||
|
||||
let massUpdate = false;
|
||||
let success = true;
|
||||
|
||||
if (this.modalType === 'amount' || this.modalType === 'routes' || this.modalType === "destinations") {
|
||||
|
||||
|
|
@ -450,18 +470,21 @@ export default {
|
|||
const setMatrix = this.$refs.modalComponent?.destMatrix;
|
||||
|
||||
if (setMatrix) {
|
||||
await this.destinationEditStore.massSetDestinations(setMatrix);
|
||||
success = await this.destinationEditStore.massSetDestinations(setMatrix);
|
||||
}
|
||||
} else {
|
||||
massUpdate = true
|
||||
}
|
||||
}
|
||||
|
||||
// Clear data
|
||||
this.fillData(this.modalType);
|
||||
this.modalType = null;
|
||||
|
||||
if(massUpdate) {
|
||||
if (success) {
|
||||
// Clear data
|
||||
this.fillData(this.modalType);
|
||||
this.modalType = null;
|
||||
} else return;
|
||||
|
||||
if (massUpdate) {
|
||||
await this.destinationEditStore.massUpdateDestinations(this.editIds);
|
||||
}
|
||||
|
||||
|
|
@ -487,6 +510,8 @@ export default {
|
|||
},
|
||||
fillData(type, id = -1, hideDescription = false) {
|
||||
|
||||
this.modalTitle = "Edit ".concat(type);
|
||||
|
||||
if (id === -1) {
|
||||
|
||||
if (type === 'price')
|
||||
|
|
@ -523,8 +548,6 @@ export default {
|
|||
} else {
|
||||
const premise = this.premiseEditStore.getById(id);
|
||||
|
||||
this.modalTitle = "Edit ".concat(type);
|
||||
|
||||
if (type === "price") {
|
||||
this.modalProps = {
|
||||
price: premise.material_cost,
|
||||
|
|
@ -735,4 +758,20 @@ export default {
|
|||
display: flex;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.header-help-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 1.6rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.header-caption-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,22 @@
|
|||
<template>
|
||||
<div class="edit-calculation-container">
|
||||
<div class="header-container">
|
||||
<h2 class="page-header">Edit calculation</h2>
|
||||
|
||||
|
||||
<div class="header-container">
|
||||
<div>
|
||||
<h2 class="page-header">Edit calculation</h2>
|
||||
</div>
|
||||
<div class="header-help-container">
|
||||
<icon-button v-if="useHelpStore().enableHelp" icon="info"
|
||||
@click="useHelpStore().activateHelp('single-edit')"></icon-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="header-controls">
|
||||
<basic-button @click="close" :show-icon="false" :disabled="premiseSingleEditStore.showLoadingSpinner"
|
||||
variant="secondary"> {{ fromMassEdit ? 'Back' : 'Close' }}
|
||||
|
|
@ -107,6 +122,7 @@ import {UrlSafeBase64} from "@/common.js";
|
|||
import {usePremiseSingleEditStore} from "@/store/premiseSingleEdit.js";
|
||||
import {useNotificationStore} from "@/store/notification.js";
|
||||
import Spinner from "@/components/UI/Spinner.vue";
|
||||
import {useHelpStore} from "@/store/help.js";
|
||||
|
||||
export default {
|
||||
name: "SingleEdit",
|
||||
|
|
@ -147,7 +163,7 @@ export default {
|
|||
if (this.premiseSingleEditStore.routing)
|
||||
return "Please wait. Routing ..."
|
||||
|
||||
return "Please wait. Calculating ...";
|
||||
return "Please wait. Prepare calculation ...";
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -161,6 +177,7 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
useHelpStore,
|
||||
|
||||
async startCalculation() {
|
||||
this.showCalculationModal = true;
|
||||
|
|
@ -209,6 +226,23 @@ export default {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.header-help-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 1.6rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.edit-calculation-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,25 @@
|
|||
<template>
|
||||
<div>
|
||||
|
||||
<h2 class="page-header">My calculations</h2>
|
||||
<div class="header-container">
|
||||
<div>
|
||||
<h2 class="page-header"> My calculations</h2>
|
||||
</div>
|
||||
<div class="header-help-container">
|
||||
<icon-button v-if="useHelpStore().enableHelp" icon="info"
|
||||
@click="useHelpStore().activateHelp('dashboard')"></icon-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<h2 class="page-sub-header">{{ greeting }}</h2>
|
||||
|
||||
|
||||
<h3 class="sub-header">Status</h3>
|
||||
|
||||
<the-dashboard></the-dashboard>
|
||||
|
||||
<h3 class="sub-header">Drafts</h3>
|
||||
<div class="calculation-list-container">
|
||||
|
||||
<the-calculation-search @execute-search="updateFilter"/>
|
||||
|
|
@ -17,6 +33,7 @@
|
|||
<div>Material</div>
|
||||
<div>Supplier</div>
|
||||
<div>Status</div>
|
||||
<div>Created at</div>
|
||||
<div>Action</div>
|
||||
</div>
|
||||
<transition name="list-container" mode="out-in">
|
||||
|
|
@ -44,7 +61,6 @@
|
|||
<list-edit :show="showListEdit" :select-count="premiseStore.selectedIds.length"
|
||||
@action="handleMultiselectAction"></list-edit>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
|
@ -68,10 +84,16 @@ import {UrlSafeBase64} from "@/common.js";
|
|||
import Pagination from "@/components/UI/Pagination.vue";
|
||||
import ModalDialog from "@/components/UI/ModalDialog.vue";
|
||||
import modal from "@/components/UI/Modal.vue";
|
||||
import {useActiveUserStore} from "@/store/activeuser.js";
|
||||
import TheDashboard from "@/components/layout/calculation/TheDashboard.vue";
|
||||
import Box from "@/components/UI/Box.vue";
|
||||
import {useHelpStore} from "@/store/help.js";
|
||||
|
||||
export default {
|
||||
name: "Calculation",
|
||||
components: {
|
||||
Box,
|
||||
TheDashboard,
|
||||
ModalDialog,
|
||||
Pagination,
|
||||
ListEdit,
|
||||
|
|
@ -79,9 +101,64 @@ export default {
|
|||
CalculationListItem, Checkbox, NotificationBar, IconButton, BasicBadge, TheCalculationSearch, Flag
|
||||
},
|
||||
computed: {
|
||||
...mapStores(usePremiseStore),
|
||||
...mapStores(usePremiseStore, useActiveUserStore),
|
||||
showListEdit() {
|
||||
return this.premiseStore.globallySomeChecked;
|
||||
},
|
||||
greeting() {
|
||||
const now = new Date();
|
||||
const hour = now.getHours();
|
||||
|
||||
// Get day of year as seed for consistent random selection throughout the day
|
||||
const dayOfYear = Math.floor((now - new Date(now.getFullYear(), 0, 0)) / 86400000);
|
||||
|
||||
let greetings = [];
|
||||
|
||||
// Morning: 5-12
|
||||
if (hour >= 5 && hour < 12) {
|
||||
greetings = [
|
||||
`Good morning, ${this.username}`,
|
||||
`Morning, ${this.username}!`,
|
||||
`Good morning, ${this.username}. Ready to calculate?`,
|
||||
`Morning ${this.username}, what's on the agenda today?`
|
||||
];
|
||||
}
|
||||
// Afternoon: 12-18
|
||||
else if (hour >= 12 && hour < 18) {
|
||||
greetings = [
|
||||
`Good afternoon, ${this.username}`,
|
||||
`Hi ${this.username}, welcome back`,
|
||||
`Afternoon, ${this.username}!`,
|
||||
`Hello ${this.username}, let's continue`,
|
||||
`Hi ${this.username}, ready to work?`
|
||||
];
|
||||
}
|
||||
// Evening: 18-22
|
||||
else if (hour >= 18 && hour < 22) {
|
||||
greetings = [
|
||||
`Good evening, ${this.username}`,
|
||||
`Evening, ${this.username}!`,
|
||||
`Hi ${this.username}, still working hard?`,
|
||||
`Good evening, ${this.username}, almost done for today?`
|
||||
];
|
||||
}
|
||||
// Night: 22-5
|
||||
else {
|
||||
greetings = [
|
||||
`Working late, ${this.username}?`,
|
||||
`Hi ${this.username}, burning the midnight oil?`,
|
||||
`Hello ${this.username}`,
|
||||
`Still here, ${this.username}?`,
|
||||
`Hi ${this.username}, don't stay up too late`
|
||||
];
|
||||
}
|
||||
|
||||
// Use day of year as seed for consistent selection
|
||||
const index = dayOfYear % greetings.length;
|
||||
return greetings[index];
|
||||
},
|
||||
username() {
|
||||
return this.activeUserStore.username;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
|
@ -118,6 +195,7 @@ export default {
|
|||
await this.executeSearch();
|
||||
},
|
||||
methods: {
|
||||
useHelpStore,
|
||||
async handleModalAction(action) {
|
||||
if (action === 'dismiss') {
|
||||
this.modal.state = false;
|
||||
|
|
@ -240,7 +318,7 @@ export default {
|
|||
}
|
||||
},
|
||||
updatePagination(resetPage = true) {
|
||||
if(resetPage) {
|
||||
if (resetPage) {
|
||||
this.updatePage(1);
|
||||
}
|
||||
this.pagination = this.premiseStore.pagination;
|
||||
|
|
@ -293,6 +371,36 @@ export default {
|
|||
|
||||
<style scoped>
|
||||
|
||||
.header-help-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
font-weight: normal;
|
||||
margin-bottom: 0;
|
||||
font-size: 2.4rem;
|
||||
color: #002F54;
|
||||
}
|
||||
|
||||
.page-sub-header {
|
||||
font-weight: normal;
|
||||
font-size: 1.8rem;
|
||||
color: #6B869C;
|
||||
margin-bottom: 2.4rem;
|
||||
}
|
||||
|
||||
|
||||
.space-around {
|
||||
margin: 3rem;
|
||||
}
|
||||
|
|
@ -372,7 +480,7 @@ export default {
|
|||
|
||||
.calculation-list-header {
|
||||
display: grid;
|
||||
grid-template-columns: 6rem 1fr 2fr 14rem 10rem;
|
||||
grid-template-columns: 6rem 1fr 2fr 14rem 20rem 10rem;
|
||||
gap: 1.6rem;
|
||||
padding: 1.6rem;
|
||||
background-color: #ffffff;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,26 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="header-container">
|
||||
<h2 class="page-header page-header-align">Reporting
|
||||
|
||||
<div class="header-caption-container">
|
||||
<div>
|
||||
<h2 class="page-header page-header-align">Reporting
|
||||
|
||||
</h2>
|
||||
|
||||
</div>
|
||||
<div class="header-help-container">
|
||||
<icon-button v-if="useHelpStore().enableHelp" icon="info"
|
||||
@click="useHelpStore().activateHelp('report')"></icon-button>
|
||||
</div>
|
||||
<div class="page-header-badges">
|
||||
<basic-badge variant="primary" v-if="period">{{ period }}</basic-badge>
|
||||
<basic-badge variant="secondary" v-if="partNumber">{{ partNumber }}</basic-badge>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="header-controls">
|
||||
<basic-button @click="createReport" icon="file">Create report</basic-button>
|
||||
<basic-button :disabled="!hasReport" variant="secondary" @click="downloadReport" icon="Download">Export
|
||||
|
|
@ -56,10 +70,12 @@ import ReportChart from "@/components/UI/ReportChart.vue";
|
|||
import Report from "@/components/layout/report/Report.vue";
|
||||
import BasicBadge from "@/components/UI/BasicBadge.vue";
|
||||
import {buildDate} from "@/common.js";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import {useHelpStore} from "@/store/help.js";
|
||||
|
||||
export default {
|
||||
name: "Reporting",
|
||||
components: {BasicBadge, Report, ReportChart, Spinner, Box, SelectForReport, BasicButton, Modal},
|
||||
components: {IconButton, BasicBadge, Report, ReportChart, Spinner, Box, SelectForReport, BasicButton, Modal},
|
||||
data() {
|
||||
return {
|
||||
showModal: false,
|
||||
|
|
@ -75,17 +91,17 @@ export default {
|
|||
},
|
||||
routeSectionScale() {
|
||||
const reports = this.reportsStore.reports;
|
||||
const scale = new Array(reports.map(r => r.premises.length).reduce((max, n) => Math.max(n, max), 0)).fill(0);
|
||||
const scale = new Array(reports.map(r => r.destinations.length).reduce((max, n) => Math.max(n, max), 0)).fill(0);
|
||||
|
||||
for (let i = 0; i < scale.length; i++) {
|
||||
for (const report of reports) {
|
||||
if(report.premises.length > i) {
|
||||
scale[i] = Math.max(scale[i], report.premises[i].sections.length);
|
||||
if (report.destinations.length > i) {
|
||||
scale[i] = Math.max(scale[i], report.destinations[i].sections.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scale.map(s => (s+1)*4);
|
||||
return scale.map(s => (s + 1) * 4);
|
||||
},
|
||||
reports() {
|
||||
return this.reportsStore.reports
|
||||
|
|
@ -113,6 +129,7 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
useHelpStore,
|
||||
downloadReport() {
|
||||
this.reportsStore.downloadReport();
|
||||
},
|
||||
|
|
@ -140,6 +157,22 @@ export default {
|
|||
|
||||
<style scoped>
|
||||
|
||||
.header-help-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 1.6rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.header-caption-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.page-header-align {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -150,6 +183,7 @@ export default {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.space-around {
|
||||
|
|
|
|||
|
|
@ -62,6 +62,11 @@ export const useActiveUserStore = defineStore('activeUser', {
|
|||
if (state.user === null)
|
||||
return false;
|
||||
return state.user.groups?.includes("super") || state.user.groups?.includes("freight");
|
||||
},
|
||||
username(state) {
|
||||
if (state.user === null)
|
||||
return null;
|
||||
return state.user.firstname; //+ ' ' + state.user.lastname;
|
||||
}
|
||||
|
||||
},
|
||||
|
|
|
|||
|
|
@ -24,6 +24,18 @@ export const useAppsStore = defineStore('apps', {
|
|||
this.apps = resp.data;
|
||||
this.loading = false;
|
||||
},
|
||||
async exportApp(id) {
|
||||
const url = `${config.backendUrl}/apps/export/${id}`;
|
||||
const resp = await performRequest(this, 'GET', url, null);
|
||||
return resp.data;
|
||||
},
|
||||
async importApp(app) {
|
||||
const url = `${config.backendUrl}/apps/import`;
|
||||
const resp = await performRequest(this, 'POST', url, { data: app },true);
|
||||
|
||||
if(resp.data)
|
||||
await this.loadApps();
|
||||
},
|
||||
async addApp(appName, appGroups) {
|
||||
const url = `${config.backendUrl}/apps`;
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export const useBulkOperationStore = defineStore('bulkOperation', {
|
|||
useNotificationStore().addNotification({
|
||||
title: 'Bulk operation',
|
||||
message: 'All your bulk operations have been completed.',
|
||||
type: 'success',
|
||||
variant: 'info',
|
||||
icon: 'stack',
|
||||
})
|
||||
|
||||
|
|
|
|||
74
src/frontend/src/store/dashboard.js
Normal file
74
src/frontend/src/store/dashboard.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import performRequest from "@/backend.js";
|
||||
import {config} from '@/config'
|
||||
import {useNotificationStore} from "@/store/notification.js";
|
||||
|
||||
export const useDashboardStore = defineStore('dashboard', {
|
||||
state: () => ({
|
||||
stats: null,
|
||||
pullInterval: 3000,
|
||||
pullTimer: null,
|
||||
}),
|
||||
getters: {
|
||||
completed(state) {
|
||||
if (state.stats)
|
||||
return state.stats.completed;
|
||||
|
||||
return null;
|
||||
},
|
||||
running(state) {
|
||||
if (state.stats)
|
||||
return state.stats.running;
|
||||
|
||||
return null;
|
||||
},
|
||||
drafts(state) {
|
||||
if (state.stats)
|
||||
return state.stats.drafts;
|
||||
|
||||
return null;
|
||||
},
|
||||
failed(state) {
|
||||
if (state.stats)
|
||||
return state.stats.failed;
|
||||
|
||||
return null;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async load() {
|
||||
const url = `${config.backendUrl}/dashboard`;
|
||||
const resp = await performRequest(this, 'GET', url, null);
|
||||
|
||||
if(this.stats?.running && this.stats.running > 0 && resp.data.running === 0) {
|
||||
useNotificationStore().addNotification({
|
||||
title: 'Calculation',
|
||||
message: 'All your calculations have been completed.',
|
||||
variant: 'info',
|
||||
icon: 'calculator',
|
||||
})
|
||||
}
|
||||
|
||||
this.stats = resp.data;
|
||||
},
|
||||
startPulling() {
|
||||
if (this.pullTimer) return
|
||||
|
||||
this.pullTimer = setTimeout(async () => {
|
||||
await this.pull()
|
||||
}, this.pullInterval)
|
||||
},
|
||||
stopPulling() {
|
||||
if (this.pullTimer) {
|
||||
clearTimeout(this.pullTimer)
|
||||
this.pullTimer = null
|
||||
}
|
||||
},
|
||||
async pull() {
|
||||
await this.load();
|
||||
this.stopPulling();
|
||||
this.startPulling();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import performRequest from "@/backend.js";
|
||||
import {config} from '@/config'
|
||||
import logger from "@/logger.js";
|
||||
import {useNotificationStore} from "@/store/notification.js";
|
||||
|
||||
export const useDestinationEditStore = defineStore('destinationEdit', {
|
||||
state: () => ({
|
||||
|
|
@ -83,30 +85,44 @@ export const useDestinationEditStore = defineStore('destinationEdit', {
|
|||
async massSetDestinations(updateMatrix) {
|
||||
this.loading = true;
|
||||
|
||||
const toBeAdded = {};
|
||||
const toBeDeletedMap = new Map();
|
||||
try {
|
||||
|
||||
updateMatrix.forEach(row => {
|
||||
toBeAdded[row.id] = row.destinations.filter(d => d.selected).map(d => d.id);
|
||||
toBeDeletedMap.set(row.id, row.destinations.filter(d => !d.selected).map(d => d.id));
|
||||
});
|
||||
const toBeAdded = {};
|
||||
const toBeDeletedMap = new Map();
|
||||
|
||||
const url = `${config.backendUrl}/calculation/destination`;
|
||||
const {
|
||||
data: data,
|
||||
headers: headers
|
||||
} = await performRequest(this, 'POST', url, {'destination_node_ids': toBeAdded});
|
||||
updateMatrix.forEach(row => {
|
||||
toBeAdded[row.id] = row.destinations.filter(d => d.selected).map(d => d.id);
|
||||
toBeDeletedMap.set(row.id, row.destinations.filter(d => !d.selected).map(d => d.id));
|
||||
});
|
||||
|
||||
this.destinations.forEach((destinations, premiseId) => {
|
||||
const toBeDeleted = toBeDeletedMap.get(premiseId);
|
||||
const url = `${config.backendUrl}/calculation/destination`;
|
||||
const {
|
||||
data: data,
|
||||
headers: headers
|
||||
} = await performRequest(this, 'POST', url, {'destination_node_ids': toBeAdded});
|
||||
|
||||
const filtered = destinations !== null ? destinations.filter(d => !toBeDeleted?.includes(d.destination_node.id)) : [];
|
||||
this.destinations.forEach((destinations, premiseId) => {
|
||||
const toBeDeleted = toBeDeletedMap.get(premiseId);
|
||||
|
||||
this.destinations.set(premiseId, [...filtered, ...data[premiseId]]);
|
||||
});
|
||||
const filtered = destinations !== null ? destinations.filter(d => !toBeDeleted?.includes(d.destination_node.id)) : [];
|
||||
|
||||
this.destinations.set(premiseId, [...filtered, ...data[premiseId]]);
|
||||
});
|
||||
|
||||
this.loading = false;
|
||||
} catch (error) {
|
||||
logger.error('Error in massSetDestinations:', error);
|
||||
useNotificationStore().addNotification({
|
||||
title: 'Unable to set destinations',
|
||||
message: error.message ?? error.toString(),
|
||||
variant: 'exception',
|
||||
icon: 'bug',
|
||||
})
|
||||
return false;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
async massUpdateDestinations(premiseIds) {
|
||||
this.loading = true;
|
||||
|
|
|
|||
53
src/frontend/src/store/help.js
Normal file
53
src/frontend/src/store/help.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import {defineStore} from "pinia";
|
||||
import {config} from "@/config.js";
|
||||
import performRequest from "@/backend.js";
|
||||
|
||||
export const useHelpStore = defineStore('help', {
|
||||
state() {
|
||||
return {
|
||||
currentPage: null,
|
||||
pages: null,
|
||||
content: null,
|
||||
showHelp: false,
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
title(state) {
|
||||
return state.pages?.find(p => p.page === state.currentPage)?.title;
|
||||
},
|
||||
videoUrl(state) {
|
||||
return state.content?.video;
|
||||
},
|
||||
text(state) {
|
||||
return state.content?.content;
|
||||
},
|
||||
showHelpVideo() {
|
||||
return this.baseUrl && this.baseUrl !== "";
|
||||
},
|
||||
enableHelp() {
|
||||
return this.pages !== null && this.pages.length > 0;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async getContent(pageId) {
|
||||
const url = `${config.backendUrl}/help/content/${pageId}`;
|
||||
const {data: data, headers: headers} = await performRequest(this, 'GET', url, null);
|
||||
console.log("help system", data);
|
||||
this.content = data;
|
||||
this.currentPage = pageId;
|
||||
},
|
||||
async loadPages() {
|
||||
const url = `${config.backendUrl}/help/content`;
|
||||
const {data: data, headers: headers} = await performRequest(this, 'GET', url, null);
|
||||
this.pages = data;
|
||||
},
|
||||
async activateHelp(pageId) {
|
||||
await this.getContent(pageId);
|
||||
this.showHelp = true;
|
||||
|
||||
},
|
||||
closeHelp() {
|
||||
this.showHelp = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -33,6 +33,9 @@ export const useNotificationStore = defineStore('notification', {
|
|||
return this.notifications.pop();
|
||||
},
|
||||
addNotification(notification) {
|
||||
|
||||
console.log("add notification", notification, this.notifications.length)
|
||||
|
||||
this.notifications.push({
|
||||
icon: notification.icon ?? null,
|
||||
message: notification.message ?? 'Unknown notification',
|
||||
|
|
|
|||
|
|
@ -16,10 +16,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
*/
|
||||
loading: false,
|
||||
|
||||
/**
|
||||
* set to true while the store sets the selected/deselected field in the premises.
|
||||
*/
|
||||
selectedLoading: false,
|
||||
processing: false,
|
||||
|
||||
throwsException: true,
|
||||
}
|
||||
|
|
@ -113,7 +110,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
},
|
||||
|
||||
showProcessingModal(state) {
|
||||
return state.processDestinationMassEdit;
|
||||
return state.processing;
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -187,7 +184,9 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
},
|
||||
async startCalculation() {
|
||||
|
||||
const body = this.premisses.map(p => p.id);
|
||||
this.processing = true;
|
||||
|
||||
const body = {premise_ids: this.premisses.map(p => p.id)};
|
||||
const url = `${config.backendUrl}/calculation/start/`;
|
||||
let error = null;
|
||||
|
||||
|
|
@ -196,6 +195,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
error = e.errorObj;
|
||||
})
|
||||
|
||||
this.processing = false;
|
||||
return error;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ export const usePremiseSingleEditStore = defineStore('premiseSingleEdit', {
|
|||
async startCalculation() {
|
||||
|
||||
this.calculating = true;
|
||||
const body = [this.premise?.id];
|
||||
const body = {premise_ids:[this.premise?.id]};
|
||||
const url = `${config.backendUrl}/calculation/start/`;
|
||||
let error = null;
|
||||
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ export const useReportsStore = defineStore('reports', {
|
|||
let max = 0;
|
||||
|
||||
state.reports.forEach(report => {
|
||||
max = Math.max(report.risk.mek_b.total, max);
|
||||
max = Math.max(report.risk.risk_scenario.total, max);
|
||||
max = Math.max(report.overview.mek_b.total, max);
|
||||
max = Math.max(report.overview.risk_scenario.total, max);
|
||||
})
|
||||
|
||||
const magnitude = Math.pow(10, Math.floor(Math.log10(max)));
|
||||
|
|
@ -37,7 +37,7 @@ export const useReportsStore = defineStore('reports', {
|
|||
return;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('material', this.materialId);
|
||||
params.append('materials', [this.materialId]);
|
||||
params.append('sources', this.supplierIds);
|
||||
params.append('userSources', this.userSupplierIds);
|
||||
|
||||
|
|
@ -74,14 +74,14 @@ export const useReportsStore = defineStore('reports', {
|
|||
this.showComparableWarning = false;
|
||||
for (const [idx, report] of this.reports.entries()) {
|
||||
for (const otherReport of this.reports.slice(idx + 1)) {
|
||||
if (report.premises.length !== otherReport.premises.length) {
|
||||
if (report.destinations.length !== otherReport.destinations.length) {
|
||||
this.showComparableWarning = true;
|
||||
break;
|
||||
}
|
||||
|
||||
for (const premise of report.premises) {
|
||||
for (const premise of report.destinations) {
|
||||
|
||||
const otherPremise = otherReport.premises.find(otherPremise => otherPremise.destination.external_mapping_id === premise.destination.external_mapping_id);
|
||||
const otherPremise = otherReport.destinations.find(otherPremise => otherPremise.destination.external_mapping_id === premise.destination.external_mapping_id);
|
||||
|
||||
if((otherPremise ?? null) == null) {
|
||||
this.showComparableWarning = true;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ export default defineConfig({
|
|||
src: 'assets/map.json',
|
||||
dest: 'assets/'
|
||||
}
|
||||
|
||||
]
|
||||
})
|
||||
],
|
||||
|
|
@ -35,4 +34,18 @@ export default defineConfig({
|
|||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
},
|
||||
})
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
secure: false
|
||||
},
|
||||
'/help': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
secure: false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -1,33 +1,41 @@
|
|||
package de.avatic.lcc.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.task.TaskExecutor;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
public class AsyncConfig {
|
||||
|
||||
@Bean(name = "calculationExecutor")
|
||||
public Executor taskExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(16);
|
||||
executor.setMaxPoolSize(32);
|
||||
executor.setQueueCapacity(500);
|
||||
executor.setThreadNamePrefix("calc-");
|
||||
executor.initialize();
|
||||
return executor;
|
||||
@Bean(name = "calculationJobScheduler")
|
||||
public ThreadPoolTaskScheduler calculationJobScheduler(
|
||||
@Value("${calculation.job.processor.pool-size:1}") int poolSize,
|
||||
@Value("${calculation.job.processor.thread-name-prefix:calc-job-}") String threadNamePrefix) {
|
||||
|
||||
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||
scheduler.setPoolSize(poolSize);
|
||||
scheduler.setThreadNamePrefix(threadNamePrefix);
|
||||
scheduler.setWaitForTasksToCompleteOnShutdown(true);
|
||||
scheduler.setAwaitTerminationSeconds(60);
|
||||
scheduler.initialize();
|
||||
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
@Bean(name = "customLookupExecutor")
|
||||
public Executor customLookupExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(16);
|
||||
executor.setCorePoolSize(2);
|
||||
executor.setMaxPoolSize(32);
|
||||
executor.setQueueCapacity(500);
|
||||
executor.setThreadNamePrefix("lookup-");
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
|
|||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
|
|
@ -111,7 +111,7 @@ public class SecurityConfig {
|
|||
.exceptionHandling(ex -> ex
|
||||
.defaultAuthenticationEntryPointFor(
|
||||
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
|
||||
new AntPathRequestMatcher("/api/**")
|
||||
PathPatternRequestMatcher.withDefaults().matcher("/api/**")
|
||||
)
|
||||
)
|
||||
.csrf(csrf -> csrf
|
||||
|
|
|
|||
|
|
@ -18,10 +18,5 @@ public class ShutdownListener {
|
|||
Runtime runtime = Runtime.getRuntime();
|
||||
long usedMemory = (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024;
|
||||
log.info("Memory: {} used, {} total, {} free, {} max ", usedMemory, runtime.totalMemory() / 1024 / 1024, runtime.freeMemory() / 1024 / 1024, runtime.maxMemory() / 1024 / 1024);
|
||||
|
||||
|
||||
|
||||
log.error("Thread stack dump:");
|
||||
Thread.dumpStack();
|
||||
}
|
||||
}
|
||||
|
|
@ -133,7 +133,7 @@ public class GlobalExceptionHandler {
|
|||
public ResponseEntity<ErrorResponseDTO> handlePremiseValidationException(PremiseValidationError exception) {
|
||||
ErrorDTO error = new ErrorDTO(
|
||||
exception.getClass().getName(),
|
||||
"Validation error",
|
||||
"Almost there - just need to fix a few things",
|
||||
exception.getMessage(),
|
||||
Arrays.asList(exception.getStackTrace())
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
package de.avatic.lcc.controller.calculation;
|
||||
|
||||
import de.avatic.lcc.dto.calculation.execution.CalculationProcessingOverviewDTO;
|
||||
import de.avatic.lcc.service.calculation.execution.CalculationJobProcessorManagementService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/dashboard")
|
||||
public class DashboardController {
|
||||
|
||||
private final CalculationJobProcessorManagementService calculationJobProcessorManagementService;
|
||||
|
||||
public DashboardController(CalculationJobProcessorManagementService calculationJobProcessorManagementService) {
|
||||
this.calculationJobProcessorManagementService = calculationJobProcessorManagementService;
|
||||
}
|
||||
|
||||
@GetMapping({"/", ""})
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
|
||||
public ResponseEntity<CalculationProcessingOverviewDTO> getDashboardData() {
|
||||
return ResponseEntity.ok(calculationJobProcessorManagementService.getCalculationOverview());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package de.avatic.lcc.controller.calculation;
|
||||
|
||||
|
||||
import de.avatic.lcc.dto.calculation.CalculationStatus;
|
||||
import de.avatic.lcc.dto.calculation.DestinationDTO;
|
||||
import de.avatic.lcc.dto.calculation.PremiseDTO;
|
||||
import de.avatic.lcc.dto.calculation.ResolvePremiseDTO;
|
||||
|
|
@ -15,10 +14,15 @@ import de.avatic.lcc.dto.calculation.edit.destination.DestinationUpdateDTO;
|
|||
import de.avatic.lcc.dto.calculation.edit.masterData.MaterialUpdateDTO;
|
||||
import de.avatic.lcc.dto.calculation.edit.masterData.PackagingUpdateDTO;
|
||||
import de.avatic.lcc.dto.calculation.edit.masterData.PriceUpdateDTO;
|
||||
import de.avatic.lcc.dto.calculation.execution.CalculationProcessingStateRequestDTO;
|
||||
import de.avatic.lcc.dto.calculation.execution.CalculationProcessingStateResponseDTO;
|
||||
import de.avatic.lcc.dto.calculation.execution.CalculationStartRequestDTO;
|
||||
import de.avatic.lcc.dto.calculation.execution.CalculationStartResponseDTO;
|
||||
import de.avatic.lcc.service.access.DestinationService;
|
||||
import de.avatic.lcc.service.access.PremisesService;
|
||||
import de.avatic.lcc.service.calculation.PremiseCreationService;
|
||||
import de.avatic.lcc.service.calculation.PremiseSearchStringAnalyzerService;
|
||||
import de.avatic.lcc.service.calculation.execution.CalculationJobProcessorManagementService;
|
||||
import de.avatic.lcc.util.exception.badrequest.InvalidArgumentException;
|
||||
import de.avatic.lcc.util.exception.base.BadRequestException;
|
||||
import jakarta.validation.Valid;
|
||||
|
|
@ -45,12 +49,14 @@ public class PremiseController {
|
|||
private final PremisesService premisesServices;
|
||||
private final PremiseCreationService premiseCreationService;
|
||||
private final DestinationService destinationService;
|
||||
private final CalculationJobProcessorManagementService calculationJobProcessorManagementService;
|
||||
|
||||
public PremiseController(PremiseSearchStringAnalyzerService premiseSearchStringAnalyzerService, PremisesService premisesServices, PremiseCreationService premiseCreationService, DestinationService destinationService) {
|
||||
public PremiseController(PremiseSearchStringAnalyzerService premiseSearchStringAnalyzerService, PremisesService premisesServices, PremiseCreationService premiseCreationService, DestinationService destinationService, CalculationJobProcessorManagementService calculationJobProcessorManagementService) {
|
||||
this.premiseSearchStringAnalyzerService = premiseSearchStringAnalyzerService;
|
||||
this.premisesServices = premisesServices;
|
||||
this.premiseCreationService = premiseCreationService;
|
||||
this.destinationService = destinationService;
|
||||
this.calculationJobProcessorManagementService = calculationJobProcessorManagementService;
|
||||
}
|
||||
|
||||
@GetMapping({"/view", "/view/"})
|
||||
|
|
@ -77,13 +83,13 @@ public class PremiseController {
|
|||
public ResponseEntity<PremiseSearchResultDTO> findMaterialsAndSuppliers(@RequestParam String search) {
|
||||
|
||||
try {
|
||||
// String decodedValue = URLDecoder.decode(search, StandardCharsets.UTF_8);
|
||||
return ResponseEntity.ok(premiseSearchStringAnalyzerService.findMaterialAndSuppliers(search));
|
||||
} catch (Exception e) {
|
||||
throw new BadRequestException("Bad string encoding", "Unable to decode request", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@PostMapping({"/create", "/create/"})
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
|
||||
public ResponseEntity<List<PremiseDetailDTO>> createPremises(@RequestBody @Valid CreatePremiseDTO dto) {
|
||||
|
|
@ -135,21 +141,16 @@ public class PremiseController {
|
|||
|
||||
@PutMapping({"/start", "/start/"})
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
|
||||
public ResponseEntity<Void> startCalculation(@RequestBody List<Integer> premiseIds) {
|
||||
premisesServices.startCalculation(premiseIds);
|
||||
return ResponseEntity.ok().build();
|
||||
public ResponseEntity<CalculationStartResponseDTO> startCalculation(@RequestBody @Valid CalculationStartRequestDTO requestDTO) {
|
||||
var response = calculationJobProcessorManagementService.startCalculation(requestDTO);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current status of a specific calculation processing operation.
|
||||
*
|
||||
* @param id The unique identifier of the operation (processing_id) to check its status.
|
||||
* @return A ResponseEntity with the bulk processing status payload.
|
||||
*/
|
||||
@GetMapping({"/status/{processing_id}", "/status/{processing_id}/"})
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
|
||||
public ResponseEntity<CalculationStatus> getCalculationStatus(@PathVariable("processing_id") Integer id) {
|
||||
return ResponseEntity.ok(premisesServices.getCalculationStatus(id));
|
||||
|
||||
@GetMapping({"/status/", "/status"})
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION', 'BASIC')")
|
||||
public ResponseEntity<CalculationProcessingStateResponseDTO> getCalculationStatus(@RequestBody CalculationProcessingStateRequestDTO requestDTO) {
|
||||
return ResponseEntity.ok(calculationJobProcessorManagementService.getCalculationStatus(requestDTO));
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -215,5 +216,4 @@ public class PremiseController {
|
|||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ package de.avatic.lcc.controller.configuration;
|
|||
|
||||
import com.azure.core.annotation.BodyParam;
|
||||
import de.avatic.lcc.dto.configuration.apps.AppDTO;
|
||||
import de.avatic.lcc.dto.configuration.apps.AppExchangeDTO;
|
||||
import de.avatic.lcc.service.apps.AppsService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
|
@ -16,21 +18,35 @@ public class AppsController {
|
|||
private final AppsService appsService;
|
||||
|
||||
public AppsController(AppsService appsService) {
|
||||
|
||||
this.appsService = appsService;
|
||||
}
|
||||
|
||||
@GetMapping({"", "/"})
|
||||
@PreAuthorize("hasRole('SERVICE')")
|
||||
public ResponseEntity<List<AppDTO>> listApps() {
|
||||
return ResponseEntity.ok(appsService.listApps());
|
||||
}
|
||||
|
||||
@PostMapping({"", "/"})
|
||||
@PreAuthorize("hasRole('SERVICE')")
|
||||
public ResponseEntity<AppDTO> updateApp(@RequestBody AppDTO dto) {
|
||||
return ResponseEntity.ok(appsService.updateApp(dto));
|
||||
}
|
||||
|
||||
@GetMapping({"/export/{id}", "/export/{id}/"})
|
||||
@PreAuthorize("hasRole('SERVICE')")
|
||||
public ResponseEntity<AppExchangeDTO> exportApp(@PathVariable Integer id) {
|
||||
return ResponseEntity.ok(appsService.exportApp(id));
|
||||
}
|
||||
|
||||
@PostMapping({"/import/", "/import"})
|
||||
@PreAuthorize("hasRole('SERVICE')")
|
||||
public ResponseEntity<Boolean> importApp(@RequestBody AppExchangeDTO dto) {
|
||||
return ResponseEntity.ok(appsService.importApp(dto));
|
||||
}
|
||||
|
||||
@DeleteMapping({"/{id}", "/{id}/"})
|
||||
@PreAuthorize("hasRole('SERVICE')")
|
||||
public ResponseEntity<Void> deleteApp(@PathVariable Integer id) {
|
||||
appsService.deleteApp(id);
|
||||
return ResponseEntity.ok().build();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import de.avatic.lcc.repositories.error.DumpRepository;
|
|||
import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
|
@ -20,11 +21,13 @@ public class CalculationDumpController {
|
|||
}
|
||||
|
||||
@GetMapping({"/dump/{id}", "/dump/{id}/"})
|
||||
@PreAuthorize("hasRole('SERVICE')")
|
||||
public ResponseEntity<CalculationJobDumpDTO> getDump(@PathVariable Integer id) {
|
||||
return ResponseEntity.ok(dumpRepository.getDump(id));
|
||||
}
|
||||
|
||||
@GetMapping({"/dump/", "/dump"})
|
||||
@PreAuthorize("hasRole('SERVICE')")
|
||||
public ResponseEntity<List<CalculationJobDumpDTO>> listDumps(
|
||||
@RequestParam(defaultValue = "20") @Min(1) int limit,
|
||||
@RequestParam(defaultValue = "1") @Min(1) int page) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
package de.avatic.lcc.controller.help;
|
||||
|
||||
import de.avatic.lcc.dto.help.HelpPage;
|
||||
import de.avatic.lcc.service.help.MarkdownService;
|
||||
import org.apache.commons.compress.utils.Lists;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/help")
|
||||
public class HelpController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(HelpController.class);
|
||||
|
||||
private final MarkdownService markdownService;
|
||||
|
||||
private final List<HelpPage> pages = List.of(
|
||||
new HelpPage("dashboard", "Dashboard"),
|
||||
new HelpPage("assistant", "Create Calculations"),
|
||||
new HelpPage("single-edit", "Single Edit"),
|
||||
new HelpPage("mass-edit-basics", "Mass Edit (Basics)"),
|
||||
new HelpPage("mass-edit-destinations", "Mass Edit (Advanced)"),
|
||||
new HelpPage("report", "Reporting"));
|
||||
|
||||
@Value(value = "${lcc.help.static:}")
|
||||
private String staticStorageUrl;
|
||||
|
||||
public HelpController(MarkdownService markdownService) {
|
||||
this.markdownService = markdownService;
|
||||
}
|
||||
|
||||
@GetMapping({"/content", "content"})
|
||||
public List<HelpPage> listContent() {
|
||||
if(staticStorageUrl == null|| staticStorageUrl.isBlank())
|
||||
return List.of();
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
@GetMapping("/content/{resource}")
|
||||
public Map<String, String> getHelpContent(@PathVariable String resource) throws IOException {
|
||||
|
||||
if (pages.stream().noneMatch(p -> p.getPage().equals(resource)))
|
||||
throw new IOException("Page not found");
|
||||
|
||||
Map<String, String> map = new HashMap<>();
|
||||
|
||||
var fileName = resource + ".md";
|
||||
var videoUrl = staticStorageUrl == null ? null : staticStorageUrl + "/" + resource + ".mp4";
|
||||
|
||||
logger.info("Loading help content: {}", fileName);
|
||||
String html = markdownService.loadAndConvertMarkdown(fileName, resource);
|
||||
|
||||
map.put("content", html);
|
||||
|
||||
if (videoUrl != null)
|
||||
map.put("video", videoUrl);
|
||||
|
||||
return map;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
package de.avatic.lcc.controller.maps;
|
||||
|
||||
import com.azure.core.credential.AccessToken;
|
||||
import com.azure.identity.DefaultAzureCredentialBuilder;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/maps")
|
||||
public class AzureMapsController {
|
||||
|
||||
@Value("${azure.maps.client.id}")
|
||||
private String mapsClientId;
|
||||
|
||||
@Value("${azure.maps.subscription.key}")
|
||||
private String mapsSubscriptionKey;
|
||||
|
||||
@GetMapping("/token")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ResponseEntity<Map<String, Object>> getAzureMapsToken() {
|
||||
try {
|
||||
// Verwende die DefaultAzureCredential für die Authentifizierung
|
||||
var credential = new DefaultAzureCredentialBuilder().build();
|
||||
|
||||
// Fordere ein Token für Azure Maps an
|
||||
AccessToken token = credential.getToken(
|
||||
new com.azure.core.credential.TokenRequestContext()
|
||||
.addScopes("https://atlas.microsoft.com/.default")
|
||||
).block();
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("token", token.getToken());
|
||||
response.put("expiresOn", token.getExpiresAt().toEpochSecond());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,17 +2,17 @@ package de.avatic.lcc.controller.report;
|
|||
|
||||
import de.avatic.lcc.dto.generic.NodeDTO;
|
||||
import de.avatic.lcc.dto.report.ReportDTO;
|
||||
import de.avatic.lcc.dto.report.ReportSearchRequestDTO;
|
||||
import de.avatic.lcc.service.report.ExcelReportingService;
|
||||
import de.avatic.lcc.service.report.ReportingService;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import org.apache.coyote.Response;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
|
@ -24,7 +24,6 @@ import java.util.List;
|
|||
@RequestMapping("/api/reports")
|
||||
public class ReportingController {
|
||||
|
||||
//TODO: rollenbeschränkung
|
||||
|
||||
private final ReportingService reportingService;
|
||||
private final ExcelReportingService excelReportingService;
|
||||
|
|
@ -41,6 +40,19 @@ public class ReportingController {
|
|||
this.excelReportingService = excelReportingService;
|
||||
}
|
||||
|
||||
@GetMapping({"/", ""})
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION', 'BASIC')")
|
||||
public ResponseEntity<String> listReports(@RequestBody(required = false) ReportSearchRequestDTO filter,
|
||||
@RequestParam(defaultValue = "20") @Min(1) int limit,
|
||||
@RequestParam(defaultValue = "1") @Min(1) int page) {
|
||||
|
||||
//TODO implement me
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetches a list of suppliers for reporting purposes, based on material ID.
|
||||
*
|
||||
|
|
@ -69,13 +81,13 @@ public class ReportingController {
|
|||
/**
|
||||
* Downloads an Excel report for the given material and source nodes.
|
||||
*
|
||||
* @param materialId The ID of the material for which the report will be downloaded.
|
||||
* @param materialIds The IDs of the materials for which the report will be downloaded.
|
||||
* @param nodeIds A list of node IDs (sources) to include in the downloaded report.
|
||||
* @return The Excel file as an attachment in the response.
|
||||
*/
|
||||
@GetMapping({"/download", "/download/"})
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION', 'BASIC')")
|
||||
public ResponseEntity<InputStreamResource> downloadReport(@RequestParam(value = "material") Integer materialId, @RequestParam(value = "sources", required = false) List<Integer> nodeIds, @RequestParam(value = "userSources", required = false) List<Integer> userNodeIds) {
|
||||
public ResponseEntity<InputStreamResource> downloadReport(@RequestParam(value = "materials") List<Integer> materialIds, @RequestParam(value = "sources", required = false) List<Integer> nodeIds, @RequestParam(value = "userSources", required = false) List<Integer> userNodeIds) {
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add("Content-Disposition", "attachment; filename=lcc_report.xlsx");
|
||||
|
|
@ -84,6 +96,6 @@ public class ReportingController {
|
|||
.ok()
|
||||
.headers(headers)
|
||||
.contentType(MediaType.parseMediaType("application/vnd.ms-excel"))
|
||||
.body(new InputStreamResource(excelReportingService.generateExcelReport(materialId, nodeIds, userNodeIds)));
|
||||
.body(new InputStreamResource(excelReportingService.generateExcelReport(materialIds, nodeIds, userNodeIds)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ public enum BulkFileType {
|
|||
this.fileType = fileType;
|
||||
}
|
||||
|
||||
|
||||
public String getFileType() {
|
||||
return fileType;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
package de.avatic.lcc.dto.calculation;
|
||||
|
||||
import de.avatic.lcc.model.db.calculations.CalculationJobState;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class CalculationStatus {
|
||||
|
||||
private Integer processId;
|
||||
|
||||
private Map<Integer, CalculationJobState> jobStates;
|
||||
private Map<Integer, String> exceptionMessages;
|
||||
|
||||
}
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
package de.avatic.lcc.dto.calculation;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import de.avatic.lcc.dto.generic.MaterialDTO;
|
||||
import de.avatic.lcc.dto.generic.NodeDTO;
|
||||
import de.avatic.lcc.model.db.calculations.CalculationJobState;
|
||||
import de.avatic.lcc.model.db.users.User;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class PremiseDTO {
|
||||
|
||||
private int id;
|
||||
|
|
@ -14,8 +18,11 @@ public class PremiseDTO {
|
|||
|
||||
private PremiseState state;
|
||||
|
||||
//TODO premise calculation result here
|
||||
// add owner information here (for superuser)
|
||||
@JsonProperty("calculation_state")
|
||||
private CalculationJobState calculationStatus;
|
||||
|
||||
@JsonProperty("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
private User owner;
|
||||
|
||||
|
|
@ -58,4 +65,20 @@ public class PremiseDTO {
|
|||
public void setState(PremiseState state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public CalculationJobState getCalculationStatus() {
|
||||
return calculationStatus;
|
||||
}
|
||||
|
||||
public void setCalculationStatus(CalculationJobState calculationStatus) {
|
||||
this.calculationStatus = calculationStatus;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
package de.avatic.lcc.dto.calculation;
|
||||
|
||||
public class TransitNodeDTO {
|
||||
|
||||
private Integer id;
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package de.avatic.lcc.dto.calculation.execution;
|
||||
|
||||
public class CalculationProcessingOverviewDTO {
|
||||
|
||||
private Integer completed;
|
||||
|
||||
private Integer running;
|
||||
|
||||
private Integer drafts;
|
||||
|
||||
private Integer failed;
|
||||
|
||||
|
||||
public Integer getCompleted() {
|
||||
return completed;
|
||||
}
|
||||
|
||||
public void setCompleted(Integer completed) {
|
||||
this.completed = completed;
|
||||
}
|
||||
|
||||
public Integer getRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
public void setRunning(Integer running) {
|
||||
this.running = running;
|
||||
}
|
||||
|
||||
public Integer getDrafts() {
|
||||
return drafts;
|
||||
}
|
||||
|
||||
public void setDrafts(Integer drafts) {
|
||||
this.drafts = drafts;
|
||||
}
|
||||
|
||||
public Integer getFailed() {
|
||||
return failed;
|
||||
}
|
||||
|
||||
public void setFailed(Integer failed) {
|
||||
this.failed = failed;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package de.avatic.lcc.dto.calculation.execution;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CalculationProcessingStateRequestDTO {
|
||||
|
||||
@JsonProperty("calculation_ids")
|
||||
private List<Integer> calculationIds;
|
||||
|
||||
public List<Integer> getCalculationIds() {
|
||||
return calculationIds;
|
||||
}
|
||||
|
||||
public void setCalculationIds(List<Integer> calculationIds) {
|
||||
this.calculationIds = calculationIds;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package de.avatic.lcc.dto.calculation.execution;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class CalculationProcessingStateResponseDTO {
|
||||
|
||||
Map<Integer, String> states;
|
||||
|
||||
public Map<Integer, String> getStates() {
|
||||
return states;
|
||||
}
|
||||
|
||||
public void setStates(Map<Integer, String> states) {
|
||||
this.states = states;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package de.avatic.lcc.dto.calculation.execution;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
public class CalculationStartRequestDTO {
|
||||
|
||||
@JsonProperty("premise_ids")
|
||||
@Size(min = 1)
|
||||
List<@NotNull Integer> premiseIds;
|
||||
|
||||
LocalDate date;
|
||||
|
||||
public List<Integer> getPremiseIds() {
|
||||
return premiseIds;
|
||||
}
|
||||
|
||||
public void setPremiseIds(List<Integer> premiseIds) {
|
||||
this.premiseIds = premiseIds;
|
||||
}
|
||||
|
||||
public LocalDate getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(LocalDate date) {
|
||||
this.date = date;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package de.avatic.lcc.dto.calculation.execution;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CalculationStartResponseDTO {
|
||||
|
||||
private List<Integer> calculationIds;
|
||||
|
||||
public List<Integer> getCalculationIds() {
|
||||
return calculationIds;
|
||||
}
|
||||
|
||||
public void setCalculationIds(List<Integer> calculationIds) {
|
||||
this.calculationIds = calculationIds;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package de.avatic.lcc.dto.configuration.apps;
|
||||
|
||||
public class AppExchangeDTO {
|
||||
|
||||
private String data;
|
||||
|
||||
public void setData(String data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public String getData() {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
package de.avatic.lcc.dto.generic;
|
||||
|
||||
public enum ContainerType {
|
||||
FEU(12030, 2350, 2390, 67.7, 24,21),
|
||||
TEU(5890 ,2350,2390, 33.0, 11,10),
|
||||
HC(12030, 2350, 2690, 76.4, 24,21),
|
||||
TRUCK(13600,2450, 2650, 88.3, 34, 33);
|
||||
FEU(12030, 2350, 2390, 67.7, 24,21, "40' GP"),
|
||||
TEU(5890 ,2350,2390, 33.0, 11,10, "20' GP"),
|
||||
HC(12030, 2350, 2690, 76.4, 24,21,"40' HC"),
|
||||
TRUCK(13600,2450, 2650, 88.3, 34, 33, "Truck");
|
||||
|
||||
private final int length;
|
||||
private final int width;
|
||||
|
|
@ -12,14 +12,16 @@ public enum ContainerType {
|
|||
private final double volume;
|
||||
private final int euroPalletCount;
|
||||
private final int industrialPalletCount;
|
||||
private final String description;
|
||||
|
||||
ContainerType(int length, int width, int height, double volume, int euroPalletCount, int industrialPalletCount) {
|
||||
ContainerType(int length, int width, int height, double volume, int euroPalletCount, int industrialPalletCount, String description) {
|
||||
this.length = length;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.volume = volume;
|
||||
this.euroPalletCount = euroPalletCount;
|
||||
this.industrialPalletCount = industrialPalletCount;
|
||||
this.description = description;
|
||||
|
||||
}
|
||||
public int getLength() {
|
||||
|
|
@ -40,4 +42,8 @@ public enum ContainerType {
|
|||
return palletType == PalletType.EURO_PALLET ? euroPalletCount : industrialPalletCount;
|
||||
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
28
src/main/java/de/avatic/lcc/dto/help/HelpPage.java
Normal file
28
src/main/java/de/avatic/lcc/dto/help/HelpPage.java
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package de.avatic.lcc.dto.help;
|
||||
|
||||
public class HelpPage {
|
||||
|
||||
String title;
|
||||
String page;
|
||||
|
||||
public HelpPage(String page, String title) {
|
||||
this.title = title;
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getPage() {
|
||||
return page;
|
||||
}
|
||||
|
||||
public void setPage(String page) {
|
||||
this.page = page;
|
||||
}
|
||||
}
|
||||
|
|
@ -23,10 +23,13 @@ public class ReportDTO {
|
|||
@JsonProperty("costs")
|
||||
public Map<String, ReportEntryDTO> cost;
|
||||
|
||||
@JsonProperty("risk")
|
||||
public Map<String, ReportEntryDTO> risk;
|
||||
@JsonProperty("overview")
|
||||
public Map<String, ReportEntryDTO> overview;
|
||||
|
||||
@JsonProperty("premises")
|
||||
private ReportPremisesDTO premises;
|
||||
|
||||
@JsonProperty("destinations")
|
||||
public List<ReportDestinationDTO> destinations;
|
||||
|
||||
public NodeDTO getSupplier() {
|
||||
|
|
@ -49,12 +52,12 @@ public class ReportDTO {
|
|||
this.cost = cost;
|
||||
}
|
||||
|
||||
public Map<String, ReportEntryDTO> getRisk() {
|
||||
return risk;
|
||||
public Map<String, ReportEntryDTO> getOverview() {
|
||||
return overview;
|
||||
}
|
||||
|
||||
public void setRisk(Map<String, ReportEntryDTO> risk) {
|
||||
this.risk = risk;
|
||||
public void setOverview(Map<String, ReportEntryDTO> overview) {
|
||||
this.overview = overview;
|
||||
}
|
||||
|
||||
public List<ReportDestinationDTO> getDestinations() {
|
||||
|
|
@ -88,4 +91,14 @@ public class ReportDTO {
|
|||
public void setEndDate(LocalDateTime endDate) {
|
||||
this.endDate = endDate;
|
||||
}
|
||||
|
||||
|
||||
public ReportPremisesDTO getPremises() {
|
||||
return premises;
|
||||
}
|
||||
|
||||
public void setPremises(ReportPremisesDTO premises) {
|
||||
this.premises = premises;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,50 +17,18 @@ public class ReportDestinationDTO {
|
|||
|
||||
/* general */
|
||||
|
||||
@JsonProperty("annual_quantity")
|
||||
private Integer annualQuantity;
|
||||
|
||||
@JsonProperty("hs_code")
|
||||
private String hsCode;
|
||||
|
||||
@JsonProperty("tariff_rate")
|
||||
private Number tariffRate;
|
||||
|
||||
@JsonProperty("oversea_share")
|
||||
private Double overseaShare;
|
||||
|
||||
@JsonProperty("air_freight_share")
|
||||
private Double airFreightShare;
|
||||
|
||||
@JsonProperty("transport_time")
|
||||
private Double transportTime;
|
||||
|
||||
@JsonProperty("safety_stock")
|
||||
private Integer safetyStock;
|
||||
|
||||
/* packaging */
|
||||
@JsonProperty("annual_quantity")
|
||||
private Integer annualQuantity;
|
||||
|
||||
private Double width;
|
||||
|
||||
private Double height;
|
||||
|
||||
private Double length;
|
||||
|
||||
private Double weight;
|
||||
|
||||
@JsonProperty("dimension_unit")
|
||||
private DimensionUnit dimensionUnit;
|
||||
|
||||
@JsonProperty("weight_unit")
|
||||
private WeightUnit weightUnit;
|
||||
|
||||
@JsonProperty("hu_unit_count")
|
||||
private Integer huUnitCount;
|
||||
|
||||
private Integer layer;
|
||||
|
||||
/* container */
|
||||
|
||||
private Integer layer;
|
||||
|
||||
@JsonProperty("unit_count")
|
||||
private Number unitCount;
|
||||
|
||||
|
|
@ -75,8 +43,6 @@ public class ReportDestinationDTO {
|
|||
@JsonProperty("container_rate")
|
||||
private Number rate;
|
||||
|
||||
private Boolean mixed;
|
||||
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
|
|
@ -110,110 +76,6 @@ public class ReportDestinationDTO {
|
|||
this.annualQuantity = annualQuantity;
|
||||
}
|
||||
|
||||
public String getHsCode() {
|
||||
return hsCode;
|
||||
}
|
||||
|
||||
public void setHsCode(String hsCode) {
|
||||
this.hsCode = hsCode;
|
||||
}
|
||||
|
||||
public Number getTariffRate() {
|
||||
return tariffRate;
|
||||
}
|
||||
|
||||
public void setTariffRate(Number tariffRate) {
|
||||
this.tariffRate = tariffRate;
|
||||
}
|
||||
|
||||
public Double getOverseaShare() {
|
||||
return overseaShare;
|
||||
}
|
||||
|
||||
public void setOverseaShare(Double overseaShare) {
|
||||
this.overseaShare = overseaShare;
|
||||
}
|
||||
|
||||
public Double getAirFreightShare() {
|
||||
return airFreightShare;
|
||||
}
|
||||
|
||||
public void setAirFreightShare(Double airFreightShare) {
|
||||
this.airFreightShare = airFreightShare;
|
||||
}
|
||||
|
||||
public Double getTransportTime() {
|
||||
return transportTime;
|
||||
}
|
||||
|
||||
public void setTransportTime(Double transportTime) {
|
||||
this.transportTime = transportTime;
|
||||
}
|
||||
|
||||
public Integer getSafetyStock() {
|
||||
return safetyStock;
|
||||
}
|
||||
|
||||
public void setSafetyStock(Integer safetyStock) {
|
||||
this.safetyStock = safetyStock;
|
||||
}
|
||||
|
||||
public Double getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public void setWidth(Double width) {
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
public Double getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setHeight(Double height) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public Double getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
public void setLength(Double length) {
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public Double getWeight() {
|
||||
return weight;
|
||||
}
|
||||
|
||||
public void setWeight(Double weight) {
|
||||
this.weight = weight;
|
||||
}
|
||||
|
||||
public DimensionUnit getDimensionUnit() {
|
||||
return dimensionUnit;
|
||||
}
|
||||
|
||||
public void setDimensionUnit(DimensionUnit dimensionUnit) {
|
||||
this.dimensionUnit = dimensionUnit;
|
||||
}
|
||||
|
||||
public WeightUnit getWeightUnit() {
|
||||
return weightUnit;
|
||||
}
|
||||
|
||||
public void setWeightUnit(WeightUnit weightUnit) {
|
||||
this.weightUnit = weightUnit;
|
||||
}
|
||||
|
||||
public Integer getHuUnitCount() {
|
||||
return huUnitCount;
|
||||
}
|
||||
|
||||
public void setHuUnitCount(Integer huUnitCount) {
|
||||
this.huUnitCount = huUnitCount;
|
||||
}
|
||||
|
||||
public Integer getLayer() {
|
||||
return layer;
|
||||
}
|
||||
|
|
@ -262,11 +124,11 @@ public class ReportDestinationDTO {
|
|||
this.rate = rate;
|
||||
}
|
||||
|
||||
public Boolean getMixed() {
|
||||
return mixed;
|
||||
public Double getTransportTime() {
|
||||
return transportTime;
|
||||
}
|
||||
|
||||
public void setMixed(Boolean mixed) {
|
||||
this.mixed = mixed;
|
||||
public void setTransportTime(Double transportTime) {
|
||||
this.transportTime = transportTime;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
155
src/main/java/de/avatic/lcc/dto/report/ReportPremisesDTO.java
Normal file
155
src/main/java/de/avatic/lcc/dto/report/ReportPremisesDTO.java
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
package de.avatic.lcc.dto.report;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import de.avatic.lcc.model.db.utils.DimensionUnit;
|
||||
import de.avatic.lcc.model.db.utils.WeightUnit;
|
||||
|
||||
public class ReportPremisesDTO {
|
||||
|
||||
|
||||
@JsonProperty("hs_code")
|
||||
private String hsCode;
|
||||
|
||||
@JsonProperty("tariff_rate")
|
||||
private Number tariffRate;
|
||||
|
||||
@JsonProperty("oversea_share")
|
||||
private Number overseaShare;
|
||||
|
||||
@JsonProperty("air_freight_share")
|
||||
private Number airFreightShare;
|
||||
|
||||
@JsonProperty("safety_stock")
|
||||
private Number safetyStock;
|
||||
|
||||
|
||||
|
||||
/* packaging */
|
||||
|
||||
private Double width;
|
||||
|
||||
private Double height;
|
||||
|
||||
private Double length;
|
||||
|
||||
private Double weight;
|
||||
|
||||
@JsonProperty("dimension_unit")
|
||||
private DimensionUnit dimensionUnit;
|
||||
|
||||
@JsonProperty("weight_unit")
|
||||
private WeightUnit weightUnit;
|
||||
|
||||
@JsonProperty("hu_unit_count")
|
||||
private Integer huUnitCount;
|
||||
|
||||
@JsonProperty("mixable")
|
||||
private Boolean mixable;
|
||||
|
||||
|
||||
public String getHsCode() {
|
||||
return hsCode;
|
||||
}
|
||||
|
||||
public void setHsCode(String hsCode) {
|
||||
this.hsCode = hsCode;
|
||||
}
|
||||
|
||||
public Number getTariffRate() {
|
||||
return tariffRate;
|
||||
}
|
||||
|
||||
public void setTariffRate(Number tariffRate) {
|
||||
this.tariffRate = tariffRate;
|
||||
}
|
||||
|
||||
public Double getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public void setWidth(Double width) {
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
public Double getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setHeight(Double height) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public Double getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
public void setLength(Double length) {
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public Double getWeight() {
|
||||
return weight;
|
||||
}
|
||||
|
||||
public void setWeight(Double weight) {
|
||||
this.weight = weight;
|
||||
}
|
||||
|
||||
public DimensionUnit getDimensionUnit() {
|
||||
return dimensionUnit;
|
||||
}
|
||||
|
||||
public void setDimensionUnit(DimensionUnit dimensionUnit) {
|
||||
this.dimensionUnit = dimensionUnit;
|
||||
}
|
||||
|
||||
public WeightUnit getWeightUnit() {
|
||||
return weightUnit;
|
||||
}
|
||||
|
||||
public void setWeightUnit(WeightUnit weightUnit) {
|
||||
this.weightUnit = weightUnit;
|
||||
}
|
||||
|
||||
public Integer getHuUnitCount() {
|
||||
return huUnitCount;
|
||||
}
|
||||
|
||||
public void setHuUnitCount(Integer huUnitCount) {
|
||||
this.huUnitCount = huUnitCount;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public Number getOverseaShare() {
|
||||
return overseaShare;
|
||||
}
|
||||
|
||||
public void setOverseaShare(Number overseaShare) {
|
||||
this.overseaShare = overseaShare;
|
||||
}
|
||||
|
||||
public Number getAirFreightShare() {
|
||||
return airFreightShare;
|
||||
}
|
||||
|
||||
public void setAirFreightShare(Number airFreightShare) {
|
||||
this.airFreightShare = airFreightShare;
|
||||
}
|
||||
|
||||
public Number getSafetyStock() {
|
||||
return safetyStock;
|
||||
}
|
||||
|
||||
public void setSafetyStock(Number safetyStock) {
|
||||
this.safetyStock = safetyStock;
|
||||
}
|
||||
|
||||
public Boolean getMixable() {
|
||||
return mixable;
|
||||
}
|
||||
|
||||
public void setMixable(Boolean mixable) {
|
||||
this.mixable = mixable;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package de.avatic.lcc.dto.report;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ReportSearchRequestDTO {
|
||||
|
||||
List<Integer> supplierIds;
|
||||
|
||||
List<Integer> materialIds;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -3,4 +3,14 @@ package de.avatic.lcc.model.bulk.header;
|
|||
public interface HeaderProvider {
|
||||
String getHeader();
|
||||
|
||||
default int occupiedCells() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
default int getColumn() { throw new UnsupportedOperationException();}
|
||||
|
||||
default boolean useOrdinal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ public enum PackagingHeader implements HeaderProvider {
|
|||
HU_DIMENSION_UNIT("HU Dimension unit"),
|
||||
HU_WEIGHT("HU gross weight"),
|
||||
HU_WEIGHT_UNIT("HU gross weight unit"),
|
||||
HU_UNIT_COUNT("Units/HU [pieces]");
|
||||
HU_UNIT_COUNT("SHU Units/HU [SHU pieces]");
|
||||
|
||||
|
||||
private final String header;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
package de.avatic.lcc.model.bulk.header;
|
||||
|
||||
public enum ReportSummaryHeader implements HeaderProvider{
|
||||
MATERIAL("Material", 0, 1),
|
||||
SUPPLIER("Supplier", 1, 1),
|
||||
MEK_A("MEK A", 2, 2),
|
||||
LOGISTICS_COST("Logistics costs", 4, 2),
|
||||
MEK_B("MEK B", 6, 2),
|
||||
TRANSPORT("Transport", 8, 1),
|
||||
HANDLING("Handling", 9, 1),
|
||||
STORAGE("Storage", 10, 1),
|
||||
REPACKAGING("Repackaging", 11, 1),
|
||||
DISPOSAL("Disposal", 12, 1),
|
||||
CAPITAL("Capital", 13, 1),
|
||||
CUSTOM("Custom", 14, 1),
|
||||
FCA_FEE("FCA Fee", 15, 1),
|
||||
AIR_FREIGHT("Air freight", 16, 1);
|
||||
|
||||
|
||||
private final int occupiedCells;
|
||||
private final String header;
|
||||
private final int column;
|
||||
|
||||
ReportSummaryHeader(String header, int column, int occupiedCells) {
|
||||
this.occupiedCells = occupiedCells;
|
||||
this.header = header;
|
||||
this.column = column;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useOrdinal() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHeader() {
|
||||
return header;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int occupiedCells() {
|
||||
return occupiedCells;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumn() {
|
||||
return column;
|
||||
}
|
||||
}
|
||||
|
|
@ -215,7 +215,7 @@ public class ContainerCalculationResult {
|
|||
* @return The total utilization value for the container.
|
||||
*/
|
||||
public double getTotalUtilizationByVolume() {
|
||||
return getHuUtilizationByVolume() * huUnitCount * layer;
|
||||
return getHuUtilizationByVolume() * huUnitCount;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -254,4 +254,7 @@ public class ContainerCalculationResult {
|
|||
return WeightUnit.KG.convertFromG(hu.getWeight()) / maxContainerWeight;
|
||||
}
|
||||
|
||||
public int getHuPerContainer() {
|
||||
return this.huUnitCount;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,35 @@ public class CalculationJob {
|
|||
|
||||
private Integer userId;
|
||||
|
||||
private CalculationJobPriority priority;
|
||||
|
||||
private Integer retries;
|
||||
|
||||
private Integer errorId;
|
||||
|
||||
public Integer getErrorId() {
|
||||
return errorId;
|
||||
}
|
||||
|
||||
public void setErrorId(Integer errorId) {
|
||||
this.errorId = errorId;
|
||||
}
|
||||
|
||||
public CalculationJobPriority getPriority() {
|
||||
return priority;
|
||||
}
|
||||
|
||||
public void setPriority(CalculationJobPriority priority) {
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
public Integer getRetries() {
|
||||
return retries;
|
||||
}
|
||||
|
||||
public void setRetries(Integer retries) {
|
||||
this.retries = retries;
|
||||
}
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package de.avatic.lcc.model.db.calculations;
|
||||
|
||||
public enum CalculationJobPriority {
|
||||
LOW, MEDIUM, HIGH
|
||||
}
|
||||
|
|
@ -40,6 +40,7 @@ public class Distance {
|
|||
|
||||
private DistanceMatrixState state;
|
||||
|
||||
private Integer retries;
|
||||
|
||||
private Integer fromNodeId;
|
||||
|
||||
|
|
@ -144,4 +145,12 @@ public class Distance {
|
|||
public void setToUserNodeId(Integer toUserNodeId) {
|
||||
this.toUserNodeId = toUserNodeId;
|
||||
}
|
||||
|
||||
public Integer getRetries() {
|
||||
return retries;
|
||||
}
|
||||
|
||||
public void setRetries(Integer retries) {
|
||||
this.retries = retries;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
package de.avatic.lcc.model.db.nodes;
|
||||
|
||||
public enum DistanceMatrixState {
|
||||
VALID, STALE
|
||||
VALID, STALE, EXCEPTION
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ public class Destination {
|
|||
|
||||
private Integer countryId;
|
||||
|
||||
private BigDecimal distanceD2d;
|
||||
|
||||
public Integer getLeadTimeD2d() {
|
||||
return leadTimeD2d;
|
||||
}
|
||||
|
|
@ -134,4 +136,12 @@ public class Destination {
|
|||
public void setDisposalCost(BigDecimal disposalCost) {
|
||||
this.disposalCost = disposalCost;
|
||||
}
|
||||
|
||||
public BigDecimal getDistanceD2d() {
|
||||
return distanceD2d;
|
||||
}
|
||||
|
||||
public void setDistanceD2d(BigDecimal distanceD2d) {
|
||||
this.distanceD2d = distanceD2d;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ public class RouteSection {
|
|||
|
||||
private Integer toRouteNodeId;
|
||||
|
||||
private Double distance;
|
||||
|
||||
public RateType getRateType() {
|
||||
return rateType;
|
||||
}
|
||||
|
|
@ -114,4 +116,12 @@ public class RouteSection {
|
|||
public void setToRouteNodeId(Integer toRouteNodeId) {
|
||||
this.toRouteNodeId = toRouteNodeId;
|
||||
}
|
||||
|
||||
public Double getDistance() {
|
||||
return distance;
|
||||
}
|
||||
|
||||
public void setDistance(Double distance) {
|
||||
this.distance = distance;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,67 +0,0 @@
|
|||
package de.avatic.lcc.model.zolltarifnummern;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.List;
|
||||
|
||||
public class ZolltarifnummernResponse {
|
||||
|
||||
String query;
|
||||
|
||||
String year;
|
||||
|
||||
String lang;
|
||||
|
||||
String version;
|
||||
|
||||
String total;
|
||||
|
||||
List<ZolltarifnummernResponseEntry> suggestions;
|
||||
|
||||
public String getQuery() {
|
||||
return query;
|
||||
}
|
||||
|
||||
public void setQuery(String query) {
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
public String getYear() {
|
||||
return year;
|
||||
}
|
||||
|
||||
public void setYear(String year) {
|
||||
this.year = year;
|
||||
}
|
||||
|
||||
public String getLang() {
|
||||
return lang;
|
||||
}
|
||||
|
||||
public void setLang(String lang) {
|
||||
this.lang = lang;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public String getTotal() {
|
||||
return total;
|
||||
}
|
||||
|
||||
public void setTotal(String total) {
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
public List<ZolltarifnummernResponseEntry> getSuggestions() {
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
public void setSuggestions(List<ZolltarifnummernResponseEntry> suggestions) {
|
||||
this.suggestions = suggestions;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
package de.avatic.lcc.model.zolltarifnummern;
|
||||
|
||||
public class ZolltarifnummernResponseEntry {
|
||||
|
||||
String code;
|
||||
|
||||
String score;
|
||||
|
||||
String value;
|
||||
|
||||
String data;
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getScore() {
|
||||
return score;
|
||||
}
|
||||
|
||||
public void setScore(String score) {
|
||||
this.score = score;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(String data) {
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
|
@ -29,10 +29,10 @@ public class DistanceMatrixRepository {
|
|||
String fromCol = isUsrFrom ? "from_user_node_id" : "from_node_id";
|
||||
String toCol = isUsrTo ? "to_user_node_id" : "to_node_id";
|
||||
|
||||
String query = "SELECT * FROM distance_matrix WHERE " + fromCol + " = ? AND " + toCol + " = ? AND state = ?";
|
||||
String query = "SELECT * FROM distance_matrix WHERE " + fromCol + " = ? AND " + toCol + " = ?";
|
||||
|
||||
var distance = jdbcTemplate.query(query, new DistanceMapper(),
|
||||
src.getId(), dest.getId(), DistanceMatrixState.VALID.name());
|
||||
src.getId(), dest.getId());
|
||||
|
||||
if (distance.isEmpty())
|
||||
return Optional.empty();
|
||||
|
|
@ -40,6 +40,12 @@ public class DistanceMatrixRepository {
|
|||
return Optional.of(distance.getFirst());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void updateRetries(Integer id) {
|
||||
String query = "UPDATE distance_matrix SET retries = retries + 1 WHERE id = ?";
|
||||
jdbcTemplate.update(query, id);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void saveDistance(Distance distance) {
|
||||
try {
|
||||
|
|
@ -82,7 +88,7 @@ public class DistanceMatrixRepository {
|
|||
toId);
|
||||
|
||||
logger.info("Updated existing distance entry for nodes {} -> {}",
|
||||
distance.getFromNodeId(), distance.getToNodeId());
|
||||
fromId, toId);
|
||||
} else {
|
||||
// Insert new entry
|
||||
String insertQuery = """
|
||||
|
|
@ -105,11 +111,14 @@ public class DistanceMatrixRepository {
|
|||
distance.getUpdatedAt());
|
||||
|
||||
logger.info("Inserted new distance entry for nodes {} -> {}",
|
||||
distance.getFromNodeId(), distance.getToNodeId());
|
||||
fromId, toId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Integer fromId = distance.getFromUserNodeId() != null ? distance.getFromUserNodeId() : distance.getFromNodeId();
|
||||
Integer toId = distance.getToUserNodeId() != null ? distance.getToUserNodeId() : distance.getToNodeId();
|
||||
|
||||
logger.error("Error saving distance to database for nodes {} -> {}",
|
||||
distance.getFromNodeId(), distance.getToNodeId(), e);
|
||||
fromId, toId, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
|
@ -119,10 +128,20 @@ public class DistanceMatrixRepository {
|
|||
public Distance mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||
Distance entity = new Distance();
|
||||
|
||||
entity.setFromNodeId(rs.getInt("from_node_id"));
|
||||
entity.setToNodeId(rs.getInt("to_node_id"));
|
||||
entity.setFromNodeId(rs.getInt("from_user_node_id"));
|
||||
entity.setToNodeId(rs.getInt("to_user_node_id"));
|
||||
entity.setId(rs.getInt("id"));
|
||||
|
||||
var fromNodeId = rs.getInt("from_node_id");
|
||||
entity.setFromNodeId(rs.wasNull() ? null : fromNodeId);
|
||||
|
||||
var toNodeId = rs.getInt("to_node_id");
|
||||
entity.setToNodeId(rs.wasNull() ? null : toNodeId);
|
||||
|
||||
var fromUserNodeId = rs.getInt("from_user_node_id");
|
||||
entity.setFromUserNodeId(rs.wasNull() ? null : fromUserNodeId);
|
||||
|
||||
var toUserNodeId = rs.getInt("to_user_node_id");
|
||||
entity.setToUserNodeId(rs.wasNull() ? null : toUserNodeId);
|
||||
|
||||
entity.setDistance(rs.getBigDecimal("distance"));
|
||||
entity.setFromGeoLng(rs.getBigDecimal("from_geo_lng"));
|
||||
entity.setFromGeoLat(rs.getBigDecimal("from_geo_lat"));
|
||||
|
|
@ -131,6 +150,9 @@ public class DistanceMatrixRepository {
|
|||
entity.setState(DistanceMatrixState.valueOf(rs.getString("state")));
|
||||
entity.setUpdatedAt(rs.getTimestamp("updated_at").toLocalDateTime());
|
||||
|
||||
var retries = rs.getInt("retries");
|
||||
entity.setRetries(rs.wasNull() ? 0 : retries);
|
||||
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ public class MaterialRepository {
|
|||
return jdbcTemplate.query(query, new MaterialMapper(), params.toArray());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Optional<Material> getByPartNumber(String partNumber) {
|
||||
if (partNumber == null) {
|
||||
return Optional.empty();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import de.avatic.lcc.model.db.ValidityTuple;
|
|||
import de.avatic.lcc.model.db.nodes.Node;
|
||||
import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
|
||||
import de.avatic.lcc.repositories.pagination.SearchQueryResult;
|
||||
import de.avatic.lcc.util.exception.internalerror.DatabaseException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
|
||||
|
|
@ -105,8 +106,8 @@ public class NodeRepository {
|
|||
entities = jdbcTemplate.query(query, new NodeMapper(), pagination.getLimit(), pagination.getOffset());
|
||||
totalCount = jdbcTemplate.queryForObject(countQuery, Integer.class);
|
||||
} else {
|
||||
entities = jdbcTemplate.query(query, new NodeMapper(), "%" + filter + "%", "%" + filter + "%", "%" + filter + "%", pagination.getLimit(), pagination.getOffset());
|
||||
totalCount = jdbcTemplate.queryForObject(countQuery, Integer.class, "%" + filter + "%", "%" + filter + "%", "%" + filter + "%");
|
||||
entities = jdbcTemplate.query(query, new NodeMapper(), "%" + filter + "%", "%" + filter + "%", "%" + filter + "%", "%" + filter + "%", pagination.getLimit(), pagination.getOffset());
|
||||
totalCount = jdbcTemplate.queryForObject(countQuery, Integer.class, "%" + filter + "%", "%" + filter + "%", "%" + filter + "%", "%" + filter + "%");
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -124,7 +125,7 @@ public class NodeRepository {
|
|||
queryBuilder.append(" AND node.is_deprecated = FALSE");
|
||||
}
|
||||
if (filter != null) {
|
||||
queryBuilder.append(" AND (node.name LIKE ? OR node.address LIKE ? OR country.iso_code LIKE ?)");
|
||||
queryBuilder.append(" AND (node.name LIKE ? OR node.external_mapping_id LIKE ? OR node.address LIKE ? OR country.iso_code LIKE ?)");
|
||||
}
|
||||
|
||||
return queryBuilder.toString();
|
||||
|
|
@ -142,7 +143,7 @@ public class NodeRepository {
|
|||
queryBuilder.append(" AND node.is_deprecated = FALSE");
|
||||
}
|
||||
if (filter != null) {
|
||||
queryBuilder.append(" AND (node.name LIKE ? OR node.address LIKE ? OR country.iso_code LIKE ?)");
|
||||
queryBuilder.append(" AND (node.name LIKE ? OR node.external_mapping_id LIKE ? OR node.address LIKE ? OR country.iso_code LIKE ?)");
|
||||
}
|
||||
queryBuilder.append(" ORDER BY node.id LIMIT ? OFFSET ?");
|
||||
return queryBuilder.toString();
|
||||
|
|
@ -165,6 +166,9 @@ public class NodeRepository {
|
|||
return Optional.empty();
|
||||
}
|
||||
|
||||
if(node.isUserNode())
|
||||
throw new DatabaseException("Cannot update user node in node repository.");
|
||||
|
||||
String updateNodeSql = """
|
||||
UPDATE node SET
|
||||
country_id = ?,
|
||||
|
|
@ -389,6 +393,25 @@ public class NodeRepository {
|
|||
@Transactional
|
||||
public List<Node> getByDistance(Node node, Integer regionRadius) {
|
||||
|
||||
if(node.isUserNode()) {
|
||||
String query = """
|
||||
SELECT * FROM node
|
||||
WHERE is_deprecated = FALSE AND
|
||||
(
|
||||
6371 * acos(
|
||||
cos(radians(?)) *
|
||||
cos(radians(geo_lat)) *
|
||||
cos(radians(geo_lng) - radians(?)) +
|
||||
sin(radians(?)) *
|
||||
sin(radians(geo_lat))
|
||||
)
|
||||
) <= ?
|
||||
""";
|
||||
|
||||
return jdbcTemplate.query(query, new NodeMapper(), node.getGeoLat(), node.getGeoLng(), node.getGeoLat(), regionRadius);
|
||||
}
|
||||
|
||||
|
||||
String query = """
|
||||
SELECT * FROM node
|
||||
WHERE is_deprecated = FALSE AND id != ? AND
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package de.avatic.lcc.repositories.calculation;
|
||||
|
||||
import de.avatic.lcc.model.db.calculations.CalculationJob;
|
||||
import de.avatic.lcc.model.db.calculations.CalculationJobPriority;
|
||||
import de.avatic.lcc.model.db.calculations.CalculationJobState;
|
||||
import de.avatic.lcc.util.exception.internalerror.DatabaseException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
|
@ -10,6 +11,8 @@ import org.springframework.stereotype.Repository;
|
|||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.sql.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
|
|
@ -20,31 +23,106 @@ public class CalculationJobRepository {
|
|||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Integer insert(CalculationJob job) {
|
||||
String sql = """
|
||||
INSERT INTO calculation_job (premise_id, calculation_date, validity_period_id, property_set_id, job_state, user_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""";
|
||||
@Transactional
|
||||
public Integer insert(CalculationJob job) {
|
||||
String sql = """
|
||||
INSERT INTO calculation_job (premise_id, calculation_date, validity_period_id, property_set_id, job_state, user_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""";
|
||||
|
||||
GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
|
||||
GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
|
||||
|
||||
jdbcTemplate.update(connection -> {
|
||||
PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
|
||||
ps.setInt(1, job.getPremiseId());
|
||||
jdbcTemplate.update(connection -> {
|
||||
PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
|
||||
ps.setInt(1, job.getPremiseId());
|
||||
|
||||
ps.setTimestamp(2, job.getCalculationDate() == null ? null : Timestamp.valueOf(job.getCalculationDate()));
|
||||
ps.setTimestamp(2, job.getCalculationDate() == null ? null : Timestamp.valueOf(job.getCalculationDate()));
|
||||
|
||||
ps.setInt(3, job.getValidityPeriodId());
|
||||
ps.setInt(4, job.getPropertySetId());
|
||||
ps.setString(5, job.getJobState().name()); // Convert enum to string
|
||||
ps.setInt(6, job.getUserId());
|
||||
return ps;
|
||||
}, keyHolder);
|
||||
ps.setInt(3, job.getValidityPeriodId());
|
||||
ps.setInt(4, job.getPropertySetId());
|
||||
ps.setString(5, job.getJobState().name()); // Convert enum to string
|
||||
ps.setInt(6, job.getUserId());
|
||||
return ps;
|
||||
}, keyHolder);
|
||||
|
||||
// Return the generated ID
|
||||
return keyHolder.getKey() != null ? keyHolder.getKey().intValue() : null;
|
||||
}
|
||||
// Return the generated ID
|
||||
return keyHolder.getKey() != null ? keyHolder.getKey().intValue() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and locks the next available calculation job based on priority and state.
|
||||
* Uses pessimistic locking (SELECT FOR UPDATE) to ensure thread-safety across multiple instances.
|
||||
* <p>
|
||||
* Priority order:
|
||||
* 1. CREATED with HIGH priority
|
||||
* 2. CREATED with MEDIUM priority
|
||||
* 3. CREATED with LOW priority
|
||||
* 4. EXCEPTION with retries < 3
|
||||
*
|
||||
* @return Optional containing the locked job, or empty if no job available
|
||||
*/
|
||||
@Transactional
|
||||
public Optional<CalculationJob> fetchAndLockNextJob() {
|
||||
String sql = """
|
||||
SELECT * FROM calculation_job
|
||||
WHERE (job_state = 'CREATED')
|
||||
OR (job_state = 'EXCEPTION' AND retries < 3)
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN job_state = 'CREATED' AND priority = 'HIGH' THEN 1
|
||||
WHEN job_state = 'CREATED' AND priority = 'MEDIUM' THEN 2
|
||||
WHEN job_state = 'CREATED' AND priority = 'LOW' THEN 3
|
||||
WHEN job_state = 'EXCEPTION' THEN 4
|
||||
END,
|
||||
calculation_date
|
||||
LIMIT 1
|
||||
FOR UPDATE SKIP LOCKED
|
||||
""";
|
||||
|
||||
var jobs = jdbcTemplate.query(sql, new CalculationJobMapper());
|
||||
|
||||
if (jobs.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
CalculationJob job = jobs.getFirst();
|
||||
|
||||
// Update state to SCHEDULED
|
||||
String updateSql = "UPDATE calculation_job SET job_state = ?, retries = retries + 1 WHERE id = ?";
|
||||
jdbcTemplate.update(updateSql, CalculationJobState.SCHEDULED.name(), job.getId());
|
||||
|
||||
job.setJobState(CalculationJobState.SCHEDULED);
|
||||
|
||||
return Optional.of(job);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a job as completed successfully
|
||||
*/
|
||||
@Transactional
|
||||
public void markAsValid(Integer id) {
|
||||
String sql = "UPDATE calculation_job SET job_state = ? WHERE id = ?";
|
||||
var affectedRows = jdbcTemplate.update(sql, CalculationJobState.VALID.name(), id);
|
||||
|
||||
if (1 != affectedRows) {
|
||||
throw new DatabaseException("Unable to mark calculation job as valid with id " + id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a job as failed and increments retry counter
|
||||
*/
|
||||
@Transactional
|
||||
public void markAsException(Integer id, Integer errorId) {
|
||||
String sql = "UPDATE calculation_job SET job_state = ?, error_id = ?, retries = retries + 1 WHERE id = ?";
|
||||
var affectedRows = jdbcTemplate.update(sql, CalculationJobState.EXCEPTION.name(), errorId, id);
|
||||
|
||||
if (1 != affectedRows) {
|
||||
throw new DatabaseException("Unable to mark calculation job as exception with id " + id);
|
||||
}
|
||||
}
|
||||
|
||||
/* ****************************** check ******************** */
|
||||
|
||||
@Transactional
|
||||
public Optional<CalculationJob> getCalculationJob(Integer id) {
|
||||
|
|
@ -52,7 +130,7 @@ public class CalculationJobRepository {
|
|||
|
||||
var job = jdbcTemplate.query(query, new CalculationJobMapper(), id);
|
||||
|
||||
if(job.isEmpty())
|
||||
if (job.isEmpty())
|
||||
return Optional.empty();
|
||||
|
||||
return Optional.of(job.getFirst());
|
||||
|
|
@ -64,7 +142,7 @@ public class CalculationJobRepository {
|
|||
|
||||
var affectedRows = jdbcTemplate.update(sql, CalculationJobState.CREATED.name(), java.time.LocalDateTime.now(), id);
|
||||
|
||||
if(1 != affectedRows) {
|
||||
if (1 != affectedRows) {
|
||||
throw new DatabaseException("Unable to update calculation job with id " + id);
|
||||
}
|
||||
}
|
||||
|
|
@ -77,7 +155,7 @@ public class CalculationJobRepository {
|
|||
|
||||
var job = jdbcTemplate.query(query, new CalculationJobMapper(), periodId, setId, nodeId, materialId);
|
||||
|
||||
if(job.isEmpty())
|
||||
if (job.isEmpty())
|
||||
return Optional.empty();
|
||||
|
||||
return Optional.of(job.getFirst());
|
||||
|
|
@ -91,21 +169,20 @@ public class CalculationJobRepository {
|
|||
|
||||
var job = jdbcTemplate.query(query, new CalculationJobMapper(), periodId, setId, userNodeId, materialId);
|
||||
|
||||
if(job.isEmpty())
|
||||
if (job.isEmpty())
|
||||
return Optional.empty();
|
||||
|
||||
return Optional.of(job.getFirst());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Transactional
|
||||
public void setStateTo(Integer id, CalculationJobState calculationJobState) {
|
||||
String sql = "UPDATE calculation_job SET job_state = ? WHERE id = ?";
|
||||
|
||||
var affectedRows = jdbcTemplate.update(sql, calculationJobState.name(), id);
|
||||
|
||||
if(1 != affectedRows) {
|
||||
if (1 != affectedRows) {
|
||||
throw new DatabaseException("Unable to update calculation job with id " + id);
|
||||
}
|
||||
}
|
||||
|
|
@ -131,6 +208,39 @@ public class CalculationJobRepository {
|
|||
jdbcTemplate.update(sql, CalculationJobState.INVALIDATED.name(), id);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CalculationJobState getLastStateFor(Integer premiseId) {
|
||||
|
||||
String sql = "SELECT job_state FROM calculation_job WHERE premise_id = ? ORDER BY calculation_date DESC LIMIT 1";
|
||||
var result = jdbcTemplate.query(sql, (rs, rowNum) -> CalculationJobState.valueOf(rs.getString("job_state")), premiseId);
|
||||
|
||||
if (result.isEmpty())
|
||||
return null;
|
||||
|
||||
return result.getFirst();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Map<Integer, CalculationJobState> getStateFor(List<Integer> ids) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public Integer getFailedJobByUserId(Integer userId) {
|
||||
|
||||
String sql = "SELECT COUNT(*) FROM calculation_job WHERE user_id = ? AND job_state = 'EXCEPTION' AND calculation_date > DATE_SUB(NOW(), INTERVAL 3 DAY)";
|
||||
|
||||
|
||||
|
||||
return jdbcTemplate.queryForObject(sql, Integer.class, userId);
|
||||
}
|
||||
|
||||
public Integer getSelfScheduledJobCountByUserId(Integer userId) {
|
||||
|
||||
String sql = "SELECT COUNT(*) FROM calculation_job WHERE user_id = ? AND ( job_state = 'SCHEDULED' or job_state = 'CREATED' )";
|
||||
|
||||
|
||||
return jdbcTemplate.queryForObject(sql, Integer.class, userId);
|
||||
}
|
||||
|
||||
private static class CalculationJobMapper implements RowMapper<CalculationJob> {
|
||||
@Override
|
||||
|
|
@ -146,6 +256,14 @@ public class CalculationJobRepository {
|
|||
entity.setUserId(rs.getInt("user_id"));
|
||||
entity.setCalculationDate(rs.getTimestamp("calculation_date").toLocalDateTime());
|
||||
|
||||
entity.setPriority(CalculationJobPriority.valueOf(rs.getString("priority")));
|
||||
entity.setRetries(rs.getInt("retries"));
|
||||
|
||||
Integer errorId = rs.getInt("error_id");
|
||||
if (!rs.wasNull()) {
|
||||
entity.setErrorId(errorId);
|
||||
}
|
||||
|
||||
return entity;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ public class DestinationRepository {
|
|||
}
|
||||
|
||||
@Transactional
|
||||
public void update(Integer id, Integer annualAmount, BigDecimal repackingCost, BigDecimal disposalCost, BigDecimal handlingCost, Boolean isD2d, BigDecimal d2dRate, BigDecimal d2dLeadTime) {
|
||||
public void update(Integer id, Integer annualAmount, BigDecimal repackingCost, BigDecimal disposalCost, BigDecimal handlingCost, Boolean isD2d, BigDecimal d2dRate, BigDecimal d2dLeadTime, BigDecimal distanceD2d) {
|
||||
if (id == null) {
|
||||
throw new InvalidArgumentException("ID cannot be null");
|
||||
}
|
||||
|
|
@ -99,6 +99,9 @@ public class DestinationRepository {
|
|||
setClauses.add("lead_time_d2d = :d2dLeadTime");
|
||||
parameters.put("d2dLeadTime", setD2d ? d2dLeadTime : null);
|
||||
|
||||
setClauses.add("distance_d2d = :distanceD2d");
|
||||
parameters.put("distanceD2d", distanceD2d);
|
||||
|
||||
|
||||
if (annualAmount != null) {
|
||||
setClauses.add("annual_amount = :annualAmount");
|
||||
|
|
@ -268,7 +271,7 @@ public class DestinationRepository {
|
|||
public Integer insert(Destination destination) {
|
||||
KeyHolder keyHolder = new GeneratedKeyHolder();
|
||||
|
||||
String query = "INSERT INTO premise_destination (annual_amount, premise_id, destination_node_id, country_id, rate_d2d, lead_time_d2d, is_d2d, repacking_cost, handling_cost, disposal_cost, geo_lat, geo_lng) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
String query = "INSERT INTO premise_destination (annual_amount, premise_id, destination_node_id, country_id, rate_d2d, lead_time_d2d, is_d2d, repacking_cost, handling_cost, disposal_cost, geo_lat, geo_lng, distance_d2d) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
jdbcTemplate.update(connection -> {
|
||||
var ps = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS);
|
||||
|
|
@ -297,6 +300,8 @@ public class DestinationRepository {
|
|||
ps.setBigDecimal(11, destination.getGeoLat());
|
||||
ps.setBigDecimal(12, destination.getGeoLng());
|
||||
|
||||
ps.setBigDecimal(13, destination.getDistanceD2d());
|
||||
|
||||
return ps;
|
||||
}, keyHolder);
|
||||
|
||||
|
|
@ -363,6 +368,8 @@ public class DestinationRepository {
|
|||
entity.setGeoLng(rs.getBigDecimal("geo_lng"));
|
||||
entity.setCountryId(rs.getInt("country_id"));
|
||||
|
||||
entity.setDistanceD2d(rs.getBigDecimal("distance_d2d"));
|
||||
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -710,6 +710,17 @@ public class PremiseRepository {
|
|||
return unlockedIds;
|
||||
}
|
||||
|
||||
public Integer getPremiseCompletedCountByUserId(Integer userId) {
|
||||
String sql = "SELECT COUNT(*) FROM premise WHERE user_id = ? AND state = 'COMPLETED'";
|
||||
|
||||
return jdbcTemplate.queryForObject(sql, Integer.class, userId);
|
||||
}
|
||||
|
||||
public Integer getPremiseDraftCountByUserId(Integer userId) {
|
||||
String sql = "SELECT COUNT(*) FROM premise WHERE user_id = ? AND state = 'DRAFT'";
|
||||
return jdbcTemplate.queryForObject(sql, Integer.class, userId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encapsulates SQL query building logic
|
||||
|
|
@ -843,7 +854,7 @@ public class PremiseRepository {
|
|||
entity.setState(de.avatic.lcc.dto.calculation.PremiseState.valueOf(rs.getString("p.state")));
|
||||
entity.setOwnerId(rs.getInt("p.user_id"));
|
||||
entity.setCreatedAt(rs.getTimestamp("p.created_at").toLocalDateTime());
|
||||
entity.setUpdatedAt(rs.getTimestamp("p.created_at").toLocalDateTime()); // Note: This looks like a bug, should be "p.updated_at"
|
||||
entity.setUpdatedAt(rs.getTimestamp("p.updated_at").toLocalDateTime());
|
||||
}
|
||||
|
||||
private Material mapMaterial(ResultSet rs) throws SQLException {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import org.springframework.jdbc.support.GeneratedKeyHolder;
|
|||
import org.springframework.jdbc.support.KeyHolder;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
|
@ -45,8 +46,8 @@ public class RouteSectionRepository {
|
|||
}
|
||||
|
||||
public Integer insert(RouteSection premiseRouteSection) {
|
||||
String sql = "INSERT INTO premise_route_section (premise_route_id, from_route_node_id, to_route_node_id, list_position, transport_type, rate_type, is_pre_run, is_main_run, is_post_run, is_outdated) " +
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
String sql = "INSERT INTO premise_route_section (premise_route_id, from_route_node_id, to_route_node_id, list_position, transport_type, rate_type, is_pre_run, is_main_run, is_post_run, is_outdated, distance) " +
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
KeyHolder keyHolder = new GeneratedKeyHolder();
|
||||
|
||||
jdbcTemplate.update(connection -> {
|
||||
|
|
@ -61,6 +62,7 @@ public class RouteSectionRepository {
|
|||
ps.setBoolean(8, premiseRouteSection.getMainRun());
|
||||
ps.setBoolean(9, premiseRouteSection.getPostRun());
|
||||
ps.setBoolean(10, premiseRouteSection.getOutdated());
|
||||
ps.setBigDecimal(11, premiseRouteSection.getDistance() == null ? null : BigDecimal.valueOf(premiseRouteSection.getDistance()));
|
||||
return ps;
|
||||
}, keyHolder);
|
||||
|
||||
|
|
@ -92,6 +94,9 @@ public class RouteSectionRepository {
|
|||
entity.setPostRun(rs.getBoolean("is_post_run"));
|
||||
entity.setOutdated(rs.getBoolean("is_outdated"));
|
||||
|
||||
var distance = rs.getBigDecimal("distance");
|
||||
entity.setDistance(rs.wasNull() ? null : distance.doubleValue());
|
||||
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ import org.springframework.transaction.annotation.Transactional;
|
|||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Repository class for managing and querying property sets.
|
||||
|
|
@ -72,8 +74,12 @@ public class PropertySetRepository {
|
|||
*
|
||||
* @return The {@link PropertySet} object in the valid state, or null if none exists.
|
||||
*/
|
||||
public PropertySet getValidSet() {
|
||||
return jdbcTemplate.queryForObject("SELECT id, start_date, end_date, state FROM property_set WHERE state = ?", new PropertySetMapper(), ValidityPeriodState.VALID.name());
|
||||
public Optional<PropertySet> getValidSet() {
|
||||
var queryResult = jdbcTemplate.query("SELECT id, start_date, end_date, state FROM property_set WHERE state = ?", new PropertySetMapper(), ValidityPeriodState.VALID.name());
|
||||
|
||||
if(queryResult.isEmpty()) return Optional.empty();
|
||||
return Optional.ofNullable(queryResult.getFirst());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -82,7 +88,7 @@ public class PropertySetRepository {
|
|||
* @return the ID of the valid {@link PropertySet}.
|
||||
*/
|
||||
public Integer getValidSetId() {
|
||||
return getValidSet().getId();
|
||||
return getValidSet().orElseThrow().getId();
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -148,6 +154,21 @@ public class PropertySetRepository {
|
|||
return set.getFirst();
|
||||
}
|
||||
|
||||
public Optional<PropertySet> getByDate(LocalDate date) {
|
||||
String query = """
|
||||
SELECT id, start_date, end_date, state
|
||||
FROM property_set
|
||||
WHERE DATE(start_date) <= ?
|
||||
AND (end_date IS NULL OR DATE(end_date) >= ?)
|
||||
ORDER BY start_date DESC
|
||||
LIMIT 1
|
||||
""";
|
||||
|
||||
var propertySets = jdbcTemplate.query(query, new PropertySetMapper(), date, date);
|
||||
|
||||
return propertySets.isEmpty() ? Optional.empty() : Optional.of(propertySets.getFirst());
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapper class for converting SQL query results into {@link PropertySet} objects.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
|
@ -327,6 +328,20 @@ public class ValidityPeriodRepository {
|
|||
jdbcTemplate.update(sql, increase);
|
||||
}
|
||||
|
||||
public Optional<ValidityPeriod> getByDate(LocalDate date) {
|
||||
String query = """
|
||||
SELECT * FROM validity_period
|
||||
WHERE DATE(start_date) <= ?
|
||||
AND (end_date IS NULL OR DATE(end_date) >= ?)
|
||||
ORDER BY start_date DESC
|
||||
LIMIT 1
|
||||
""";
|
||||
|
||||
var periods = jdbcTemplate.query(query, new ValidityPeriodMapper(), date, date);
|
||||
|
||||
return periods.isEmpty() ? Optional.empty() : Optional.of(periods.getFirst());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Maps rows of a {@link ResultSet} to {@link ValidityPeriod} objects.
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import de.avatic.lcc.model.db.premises.route.*;
|
|||
import de.avatic.lcc.repositories.NodeRepository;
|
||||
import de.avatic.lcc.repositories.premise.*;
|
||||
import de.avatic.lcc.repositories.users.UserNodeRepository;
|
||||
import de.avatic.lcc.service.calculation.DistanceService;
|
||||
import de.avatic.lcc.service.calculation.RoutingService;
|
||||
import de.avatic.lcc.service.transformer.premise.DestinationTransformer;
|
||||
import de.avatic.lcc.service.users.AuthorizationService;
|
||||
|
|
@ -35,8 +36,9 @@ public class DestinationService {
|
|||
private final PremiseRepository premiseRepository;
|
||||
private final UserNodeRepository userNodeRepository;
|
||||
private final AuthorizationService authorizationService;
|
||||
private final DistanceService distanceService;
|
||||
|
||||
public DestinationService(DestinationRepository destinationRepository, DestinationTransformer destinationTransformer, RouteRepository routeRepository, RouteSectionRepository routeSectionRepository, RouteNodeRepository routeNodeRepository, RoutingService routingService, NodeRepository nodeRepository, PremiseRepository premiseRepository, UserNodeRepository userNodeRepository, AuthorizationService authorizationService) {
|
||||
public DestinationService(DestinationRepository destinationRepository, DestinationTransformer destinationTransformer, RouteRepository routeRepository, RouteSectionRepository routeSectionRepository, RouteNodeRepository routeNodeRepository, RoutingService routingService, NodeRepository nodeRepository, PremiseRepository premiseRepository, UserNodeRepository userNodeRepository, AuthorizationService authorizationService, DistanceService distanceService) {
|
||||
this.destinationRepository = destinationRepository;
|
||||
this.destinationTransformer = destinationTransformer;
|
||||
this.routeRepository = routeRepository;
|
||||
|
|
@ -47,6 +49,7 @@ public class DestinationService {
|
|||
this.premiseRepository = premiseRepository;
|
||||
this.userNodeRepository = userNodeRepository;
|
||||
this.authorizationService = authorizationService;
|
||||
this.distanceService = distanceService;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -199,11 +202,30 @@ public class DestinationService {
|
|||
destinationUpdateDTO.getDisposalCost() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getDisposalCost().doubleValue()),
|
||||
destinationUpdateDTO.getHandlingCost() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getHandlingCost().doubleValue()),
|
||||
destinationUpdateDTO.getD2d(), destinationUpdateDTO.getRateD2d() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getRateD2d().doubleValue()),
|
||||
destinationUpdateDTO.getLeadtimeD2d() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getLeadtimeD2d())
|
||||
destinationUpdateDTO.getLeadtimeD2d() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getLeadtimeD2d()),
|
||||
destinationUpdateDTO.getD2d() ? getD2dDistance(id) : null
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
private BigDecimal getD2dDistance(Integer destinationId) {
|
||||
var dest = destinationRepository.getById(destinationId);
|
||||
|
||||
if(dest.isPresent()) {
|
||||
var premise = premiseRepository.getPremiseById(dest.get().getPremiseId());
|
||||
|
||||
if(premise.isPresent()) {
|
||||
boolean isUserNode = premise.get().getSupplierNodeId() == null;
|
||||
|
||||
var from = isUserNode ? userNodeRepository.getById(premise.get().getUserSupplierNodeId()) : nodeRepository.getById(premise.get().getSupplierNodeId());
|
||||
var to = nodeRepository.getById(dest.get().getDestinationNodeId());
|
||||
|
||||
return BigDecimal.valueOf(distanceService.getDistanceForNode(from.orElseThrow(), to.orElseThrow()));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Map<RouteIds, List<RouteInformation>> findRoutes(List<Premise> premisses, Map<Integer, List<Integer>> routingRequest) {
|
||||
|
||||
Map<RouteIds, List<RouteInformation>> routes = new HashMap<>();
|
||||
|
|
@ -267,6 +289,7 @@ public class DestinationService {
|
|||
premiseRouteSection.setFromRouteNodeId(fromNodeId);
|
||||
premiseRouteSection.setToRouteNodeId(toNodeId);
|
||||
|
||||
|
||||
routeSectionRepository.insert(premiseRouteSection);
|
||||
|
||||
fromNodeId = toNodeId;
|
||||
|
|
|
|||
|
|
@ -1,45 +1,28 @@
|
|||
package de.avatic.lcc.service.access;
|
||||
|
||||
import de.avatic.lcc.dto.calculation.CalculationStatus;
|
||||
import de.avatic.lcc.dto.calculation.ResolvePremiseDTO;
|
||||
import de.avatic.lcc.dto.calculation.PremiseDTO;
|
||||
import de.avatic.lcc.dto.calculation.ResolvePremiseDTO;
|
||||
import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO;
|
||||
import de.avatic.lcc.dto.calculation.edit.masterData.MaterialUpdateDTO;
|
||||
import de.avatic.lcc.dto.calculation.edit.masterData.PackagingUpdateDTO;
|
||||
import de.avatic.lcc.dto.calculation.edit.masterData.PriceUpdateDTO;
|
||||
import de.avatic.lcc.model.db.calculations.CalculationJob;
|
||||
import de.avatic.lcc.model.db.calculations.CalculationJobState;
|
||||
import de.avatic.lcc.model.db.premises.Premise;
|
||||
import de.avatic.lcc.model.db.premises.PremiseState;
|
||||
import de.avatic.lcc.repositories.calculation.CalculationJobDestinationRepository;
|
||||
import de.avatic.lcc.repositories.calculation.CalculationJobRepository;
|
||||
import de.avatic.lcc.repositories.calculation.CalculationJobRouteSectionRepository;
|
||||
import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
|
||||
import de.avatic.lcc.repositories.pagination.SearchQueryResult;
|
||||
import de.avatic.lcc.repositories.premise.PremiseRepository;
|
||||
import de.avatic.lcc.repositories.properties.PropertySetRepository;
|
||||
import de.avatic.lcc.repositories.rates.ValidityPeriodRepository;
|
||||
import de.avatic.lcc.service.calculation.execution.CalculationExecutionService;
|
||||
import de.avatic.lcc.service.calculation.execution.CalculationStatusService;
|
||||
import de.avatic.lcc.service.precalculation.PostCalculationCheckService;
|
||||
import de.avatic.lcc.service.precalculation.PreCalculationCheckService;
|
||||
import de.avatic.lcc.service.transformer.generic.DimensionTransformer;
|
||||
import de.avatic.lcc.service.transformer.premise.PremiseTransformer;
|
||||
import de.avatic.lcc.service.users.AuthorizationService;
|
||||
import de.avatic.lcc.util.exception.base.InternalErrorException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
@Service
|
||||
public class PremisesService {
|
||||
|
|
@ -51,33 +34,14 @@ public class PremisesService {
|
|||
private final PremiseTransformer premiseTransformer;
|
||||
private final DimensionTransformer dimensionTransformer;
|
||||
private final DestinationService destinationService;
|
||||
private final CalculationJobRepository calculationJobRepository;
|
||||
|
||||
private final PropertySetRepository propertySetRepository;
|
||||
private final ValidityPeriodRepository validityPeriodRepository;
|
||||
private final CalculationStatusService calculationStatusService;
|
||||
private final CalculationExecutionService calculationExecutionService;
|
||||
private final PreCalculationCheckService preCalculationCheckService;
|
||||
private final CalculationJobDestinationRepository calculationJobDestinationRepository;
|
||||
private final CalculationJobRouteSectionRepository calculationJobRouteSectionRepository;
|
||||
private final PostCalculationCheckService postCalculationCheckService;
|
||||
private final AuthorizationService authorizationService;
|
||||
|
||||
public PremisesService(PremiseRepository premiseRepository, PremiseTransformer premiseTransformer, DimensionTransformer dimensionTransformer, DestinationService destinationService, CalculationJobRepository calculationJobRepository, PropertySetRepository propertySetRepository, ValidityPeriodRepository validityPeriodRepository, CalculationStatusService calculationStatusService, CalculationExecutionService calculationExecutionService, PreCalculationCheckService preCalculationCheckService, CalculationJobDestinationRepository calculationJobDestinationRepository, CalculationJobRouteSectionRepository calculationJobRouteSectionRepository, PostCalculationCheckService postCalculationCheckService, AuthorizationService authorizationService) {
|
||||
public PremisesService(PremiseRepository premiseRepository, PremiseTransformer premiseTransformer, DimensionTransformer dimensionTransformer, DestinationService destinationService, AuthorizationService authorizationService) {
|
||||
this.premiseRepository = premiseRepository;
|
||||
this.premiseTransformer = premiseTransformer;
|
||||
this.dimensionTransformer = dimensionTransformer;
|
||||
this.destinationService = destinationService;
|
||||
this.calculationJobRepository = calculationJobRepository;
|
||||
|
||||
this.propertySetRepository = propertySetRepository;
|
||||
this.validityPeriodRepository = validityPeriodRepository;
|
||||
this.calculationStatusService = calculationStatusService;
|
||||
this.calculationExecutionService = calculationExecutionService;
|
||||
this.preCalculationCheckService = preCalculationCheckService;
|
||||
this.calculationJobDestinationRepository = calculationJobDestinationRepository;
|
||||
this.calculationJobRouteSectionRepository = calculationJobRouteSectionRepository;
|
||||
this.postCalculationCheckService = postCalculationCheckService;
|
||||
this.authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
|
|
@ -102,95 +66,6 @@ public class PremisesService {
|
|||
}
|
||||
|
||||
|
||||
public void startCalculation(List<Integer> premises) {
|
||||
var admin = authorizationService.isSuper();
|
||||
var userId = authorizationService.getUserId();
|
||||
|
||||
if (!admin)
|
||||
premiseRepository.checkOwner(premises, userId);
|
||||
|
||||
var validSetId = propertySetRepository.getValidSetId();
|
||||
var validPeriodId = validityPeriodRepository.getValidPeriodId().orElseThrow(() -> new InternalErrorException("no set of transport rates found that is VALID"));
|
||||
|
||||
var checkResult = premises.stream().map(premiseId -> preCalculationCheckService.doPrecheck(premiseId, validSetId, validPeriodId)).toList();
|
||||
CompletableFuture.allOf(checkResult.toArray(new CompletableFuture[0])).join();
|
||||
|
||||
var calculationIds = new ArrayList<Integer>();
|
||||
|
||||
premises.forEach(p -> {
|
||||
|
||||
var existingJob = calculationJobRepository.findJob(p, validSetId, validPeriodId);
|
||||
|
||||
if (existingJob.isPresent()) {
|
||||
if (CalculationJobState.EXCEPTION == existingJob.get().getJobState()) {
|
||||
calculationJobRepository.setStateTo(existingJob.get().getId(), CalculationJobState.CREATED);
|
||||
calculationIds.add(existingJob.get().getId());
|
||||
} else if (CalculationJobState.SCHEDULED == existingJob.get().getJobState() && existingJob.get().getCalculationDate().plusMinutes(15).isBefore(LocalDateTime.now())) {
|
||||
calculationJobRepository.reschedule(existingJob.get().getId());
|
||||
calculationIds.add(existingJob.get().getId());
|
||||
}
|
||||
} else {
|
||||
CalculationJob job = new CalculationJob();
|
||||
|
||||
job.setPremiseId(p);
|
||||
job.setJobState(CalculationJobState.CREATED);
|
||||
job.setCalculationDate(java.time.LocalDateTime.now());
|
||||
job.setUserId(userId);
|
||||
job.setPropertySetId(validSetId);
|
||||
job.setValidityPeriodId(validPeriodId);
|
||||
|
||||
calculationIds.add(calculationJobRepository.insert(job));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
premiseRepository.setStatus(premises, PremiseState.COMPLETED);
|
||||
|
||||
calculationIds.forEach(this::scheduleCalculation);
|
||||
|
||||
try {
|
||||
var futures = calculationIds.stream().map(calculationExecutionService::launchJobCalculation).toList();
|
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
||||
|
||||
for (var future : futures) {
|
||||
var jobResult = future.get();
|
||||
if (jobResult.getState().equals(CalculationJobState.EXCEPTION)) {
|
||||
calculationJobRepository.setStateTo(jobResult.getJobId(), CalculationJobState.EXCEPTION);
|
||||
throw new InternalErrorException("Calculation failed", "Calculation was not successful. Please contact Administrator.", new Exception(jobResult.getException()));
|
||||
} else {
|
||||
|
||||
postCalculationCheckService.doPostcheck(jobResult);
|
||||
|
||||
for (var destinationInfo : jobResult.getDestinationInfos()) {
|
||||
destinationInfo.destinationCalculationJob().setCalculationJobId(jobResult.getJobId());
|
||||
var destinationId = calculationJobDestinationRepository.insert(destinationInfo.destinationCalculationJob());
|
||||
|
||||
for (var sectionInfo : destinationInfo.sectionInfo()) {
|
||||
var section = sectionInfo.result();
|
||||
section.setCalculationJobDestinationId(destinationId);
|
||||
calculationJobRouteSectionRepository.insert(section);
|
||||
}
|
||||
}
|
||||
|
||||
calculationJobRepository.setStateTo(jobResult.getJobId(), CalculationJobState.VALID);
|
||||
log.info("Calculation job {} finished", jobResult.getJobId());
|
||||
}
|
||||
}
|
||||
|
||||
} catch (CompletionException | InterruptedException | ExecutionException e) {
|
||||
throw new InternalErrorException("Task execution of calculation failed", "Task execution of calculation was not successful. Please contact Administrator.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void scheduleCalculation(Integer id) {
|
||||
calculationJobRepository.setStateTo(id, CalculationJobState.SCHEDULED);
|
||||
}
|
||||
|
||||
public CalculationStatus getCalculationStatus(Integer processId) {
|
||||
return calculationStatusService.getCalculationStatus(processId);
|
||||
}
|
||||
|
||||
|
||||
public void updatePackaging(PackagingUpdateDTO packagingDTO) {
|
||||
var admin = authorizationService.isSuper();
|
||||
var userId = authorizationService.getUserId();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package de.avatic.lcc.service.api;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.avatic.lcc.model.azuremaps.route.RouteDirectionsResponse;
|
||||
import de.avatic.lcc.model.db.nodes.Distance;
|
||||
import de.avatic.lcc.model.db.nodes.DistanceMatrixState;
|
||||
|
|
@ -10,6 +12,7 @@ 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.HttpClientErrorException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
|
|
@ -25,14 +28,18 @@ public class DistanceApiService {
|
|||
|
||||
private final DistanceMatrixRepository distanceMatrixRepository;
|
||||
private final RestTemplate restTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final GeoApiService geoApiService;
|
||||
|
||||
@Value("${azure.maps.subscription.key}")
|
||||
private String subscriptionKey;
|
||||
|
||||
public DistanceApiService(DistanceMatrixRepository distanceMatrixRepository,
|
||||
RestTemplate restTemplate) {
|
||||
RestTemplate restTemplate, ObjectMapper objectMapper, GeoApiService geoApiService) {
|
||||
this.distanceMatrixRepository = distanceMatrixRepository;
|
||||
this.restTemplate = restTemplate;
|
||||
this.objectMapper = objectMapper;
|
||||
this.geoApiService = geoApiService;
|
||||
}
|
||||
|
||||
public Optional<Integer> getDistance(Location from, Location to) {
|
||||
|
|
@ -64,28 +71,63 @@ public class DistanceApiService {
|
|||
|
||||
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());
|
||||
logger.warn("Missing geo coordinates for nodes: from={}, to={}", from.getExternalMappingId(), to.getExternalMappingId());
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Optional<Distance> cachedDistance = distanceMatrixRepository.getDistance(from, isUsrFrom, to, isUsrTo);
|
||||
|
||||
if (cachedDistance.isPresent()) {
|
||||
logger.debug("Found cached distance from node {} to node {}", from.getId(), to.getId());
|
||||
if (cachedDistance.isPresent() && cachedDistance.get().getState() == DistanceMatrixState.VALID) {
|
||||
logger.info("Found cached distance from node {} (user: {}) to node {} (user {}) - {} meters", from.getExternalMappingId(), isUsrFrom, to.getExternalMappingId(), isUsrTo, cachedDistance.get().getDistance().doubleValue());
|
||||
return cachedDistance;
|
||||
}
|
||||
|
||||
logger.debug("Fetching distance from Azure Maps for nodes {} to {}", from.getId(), to.getId());
|
||||
Optional<Distance> fetchedDistance = fetchDistanceFromAzureMaps(from, isUsrFrom, to, isUsrTo);
|
||||
if (cachedDistance.isPresent() && cachedDistance.get().getState() == DistanceMatrixState.EXCEPTION) {
|
||||
if (cachedDistance.get().getRetries() >= 3)
|
||||
return Optional.empty();
|
||||
|
||||
if (fetchedDistance.isPresent()) {
|
||||
distanceMatrixRepository.saveDistance(fetchedDistance.get());
|
||||
return fetchedDistance;
|
||||
distanceMatrixRepository.updateRetries(cachedDistance.get().getId());
|
||||
}
|
||||
|
||||
logger.info("Fetching distance from Azure Maps for nodes {} to {}", from.getExternalMappingId() == null ? from.getName() : from.getExternalMappingId(), to.getExternalMappingId() == null ? to.getName() : to.getExternalMappingId());
|
||||
AzureMapResponse distanceResponse = fetchDistanceFromAzureMaps(from, isUsrFrom, to, isUsrTo, true);
|
||||
|
||||
if (distanceResponse.distance != null) {
|
||||
distanceMatrixRepository.saveDistance(distanceResponse.distance);
|
||||
return Optional.of(distanceResponse.distance);
|
||||
}
|
||||
|
||||
if (distanceResponse.errorType != AzureMapsErrorType.NO_ERROR) {
|
||||
distanceMatrixRepository.saveDistance(getErrorDistance(cachedDistance, from, isUsrFrom, to, isUsrTo));
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private Distance getErrorDistance(Optional<Distance> cachedDistance, Node from, boolean isUsrFrom, Node to, boolean isUsrTo) {
|
||||
var distance = cachedDistance.orElse(new Distance());
|
||||
|
||||
distance.setState(DistanceMatrixState.EXCEPTION);
|
||||
distance.setUpdatedAt(LocalDateTime.now());
|
||||
distance.setRetries(distance.getRetries() == null ? 0 : distance.getRetries() + 1);
|
||||
|
||||
if (cachedDistance.isEmpty()) {
|
||||
distance.setFromUserNodeId(isUsrFrom ? from.getId() : null);
|
||||
distance.setFromNodeId(isUsrFrom ? null : from.getId());
|
||||
distance.setToUserNodeId(isUsrTo ? to.getId() : null);
|
||||
distance.setToNodeId(isUsrTo ? null : to.getId());
|
||||
|
||||
distance.setFromGeoLat(from.getGeoLat());
|
||||
distance.setFromGeoLng(from.getGeoLng());
|
||||
distance.setToGeoLat(to.getGeoLat());
|
||||
distance.setToGeoLng(to.getGeoLng());
|
||||
|
||||
distance.setDistance(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
return distance;
|
||||
}
|
||||
|
||||
private RouteDirectionsResponse fetchDistanceFromAzureMaps(BigDecimal fromLat, BigDecimal fromLng, BigDecimal toLat, BigDecimal toLng) {
|
||||
String url = UriComponentsBuilder.fromUriString(AZURE_MAPS_ROUTE_API)
|
||||
.queryParam("api-version", "1.0")
|
||||
|
|
@ -99,50 +141,144 @@ public class DistanceApiService {
|
|||
return restTemplate.getForObject(url, RouteDirectionsResponse.class);
|
||||
}
|
||||
|
||||
private Optional<Distance> fetchDistanceFromAzureMaps(Node from, boolean isUsrFrom, Node to, boolean isUsrTo) {
|
||||
|
||||
private AzureMapResponse fetchDistanceFromAzureMaps(Node from, boolean isUsrFrom, Node to, boolean isUsrTo, boolean allowFixing) {
|
||||
try {
|
||||
|
||||
RouteDirectionsResponse response = fetchDistanceFromAzureMaps(from.getGeoLat(), from.getGeoLng(), to.getGeoLat(), to.getGeoLng());
|
||||
|
||||
if (response != null && response.getRoutes() != null && !response.getRoutes().isEmpty()) {
|
||||
Integer distanceInMeters = response.getRoutes().getFirst().getSummary().getLengthInMeters();
|
||||
|
||||
Distance distance = new Distance();
|
||||
|
||||
if (isUsrFrom) {
|
||||
distance.setFromUserNodeId(from.getId());
|
||||
distance.setFromNodeId(null);
|
||||
} else {
|
||||
distance.setFromUserNodeId(null);
|
||||
distance.setFromNodeId(from.getId());
|
||||
}
|
||||
|
||||
if (isUsrTo) {
|
||||
distance.setToUserNodeId(to.getId());
|
||||
distance.setToNodeId(null);
|
||||
} else {
|
||||
distance.setToUserNodeId(null);
|
||||
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");
|
||||
}
|
||||
return convertToDistance(response, from, isUsrFrom, to, isUsrTo);
|
||||
} catch (Exception e) {
|
||||
if (HttpClientErrorException.class.isAssignableFrom(e.getClass()))
|
||||
return handleAzureMapsError((HttpClientErrorException) e, from, isUsrFrom, to, isUsrTo, allowFixing);
|
||||
|
||||
logger.error("Error fetching distance from Azure Maps", e);
|
||||
return new AzureMapResponse(null, AzureMapsErrorType.OTHER_ERROR, null);
|
||||
}
|
||||
}
|
||||
|
||||
private AzureMapResponse convertToDistance(RouteDirectionsResponse response, Node from, boolean isUsrFrom, Node to, boolean isUsrTo) {
|
||||
if (response != null && response.getRoutes() != null && !response.getRoutes().isEmpty()) {
|
||||
Integer distanceInMeters = response.getRoutes().getFirst().getSummary().getLengthInMeters();
|
||||
|
||||
Distance distance = new Distance();
|
||||
|
||||
if (isUsrFrom) {
|
||||
distance.setFromUserNodeId(from.getId());
|
||||
distance.setFromNodeId(null);
|
||||
} else {
|
||||
distance.setFromUserNodeId(null);
|
||||
distance.setFromNodeId(from.getId());
|
||||
}
|
||||
|
||||
if (isUsrTo) {
|
||||
distance.setToUserNodeId(to.getId());
|
||||
distance.setToNodeId(null);
|
||||
} else {
|
||||
distance.setToUserNodeId(null);
|
||||
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());
|
||||
|
||||
// reset to 0 if on success
|
||||
distance.setRetries(0);
|
||||
|
||||
logger.info("Successfully fetched distance: {} meters", distanceInMeters);
|
||||
return new AzureMapResponse(distance, AzureMapsErrorType.NO_ERROR, null);
|
||||
} else {
|
||||
logger.error("No routes found in Azure Maps response");
|
||||
return new AzureMapResponse(null, AzureMapsErrorType.ROUTE_ERROR, null);
|
||||
}
|
||||
}
|
||||
|
||||
private AzureMapResponse handleAzureMapsError(HttpClientErrorException e,
|
||||
Node from, boolean isUsrFrom,
|
||||
Node to, boolean isUsrTo, boolean allowFixing) {
|
||||
|
||||
try {
|
||||
String responseBody = e.getResponseBodyAsString();
|
||||
JsonNode errorNode = objectMapper.readTree(responseBody);
|
||||
|
||||
String errorCode = errorNode.path("error").path("code").asText();
|
||||
String errorMessage = errorNode.path("error").path("message").asText();
|
||||
|
||||
logger.warn("Azure Maps API Error for nodes {} ({}) to {} ({}): {} - {}",
|
||||
from.getExternalMappingId() == null ? from.getName() : from.getExternalMappingId(), from.getId(), to.getExternalMappingId() == null ? to.getName() : to.getExternalMappingId(), to.getId(),
|
||||
errorCode, errorMessage);
|
||||
|
||||
if (errorMessage.contains("NO_ROUTE_FOUND"))
|
||||
return new AzureMapResponse(null, AzureMapsErrorType.ROUTE_ERROR, null);
|
||||
|
||||
if (errorMessage.contains("MAP_MATCHING_FAILURE")) {
|
||||
if (errorMessage.contains("Destination")) {
|
||||
if (allowFixing) {
|
||||
var fixedNode = fixNode(to);
|
||||
|
||||
if (fixedNode != null)
|
||||
return fetchDistanceFromAzureMaps(from, isUsrFrom, fixedNode, isUsrTo, false);
|
||||
}
|
||||
|
||||
return new AzureMapResponse(null, AzureMapsErrorType.NODE_ERROR, AzureMapsDefectiveNode.FROM);
|
||||
}
|
||||
|
||||
if (errorMessage.contains("Origin")) {
|
||||
if (allowFixing) {
|
||||
var fixedNode = fixNode(from);
|
||||
|
||||
if (fixedNode != null)
|
||||
return fetchDistanceFromAzureMaps(fixedNode, isUsrFrom, to, isUsrTo, false);
|
||||
}
|
||||
|
||||
return new AzureMapResponse(null, AzureMapsErrorType.NODE_ERROR, AzureMapsDefectiveNode.TO);
|
||||
}
|
||||
|
||||
return new AzureMapResponse(null, AzureMapsErrorType.NODE_ERROR, AzureMapsDefectiveNode.UNKNOWN);
|
||||
}
|
||||
|
||||
} catch (Exception parseException) {
|
||||
logger.error("Failed to parse Azure Maps error response", parseException);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
return new AzureMapResponse(null, AzureMapsErrorType.OTHER_ERROR, null);
|
||||
}
|
||||
|
||||
private Node fixNode(Node node) {
|
||||
|
||||
logger.info("Try to fix node {} ({}) ", node.getExternalMappingId(), node.getId());
|
||||
|
||||
Location location = geoApiService.locate(node.getAddress() + ", " + node.getCountryId());
|
||||
|
||||
if (location != null && location.getLatitude() != null && location.getLongitude() != null) {
|
||||
if (0 != BigDecimal.valueOf(location.getLatitude()).compareTo(node.getGeoLat()) || 0 != BigDecimal.valueOf(location.getLongitude()).compareTo(node.getGeoLng())) {
|
||||
logger.info("Fixed node {} ({}) coordinates {}, {} -> {}, {}", node.getExternalMappingId(), node.getId(), node.getGeoLat(), node.getGeoLng(), location.getLatitude(), location.getLongitude());
|
||||
|
||||
node.setGeoLng(BigDecimal.valueOf(location.getLongitude()));
|
||||
node.setGeoLat(BigDecimal.valueOf(location.getLatitude()));
|
||||
|
||||
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private enum AzureMapsErrorType {
|
||||
NO_ERROR, NODE_ERROR, ROUTE_ERROR, OTHER_ERROR
|
||||
}
|
||||
|
||||
private enum AzureMapsDefectiveNode {
|
||||
FROM, TO, UNKNOWN
|
||||
}
|
||||
|
||||
private record AzureMapResponse(Distance distance, AzureMapsErrorType errorType,
|
||||
AzureMapsDefectiveNode defectiveNode) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -53,12 +53,16 @@ public class EUTaxationApiService {
|
|||
request.setReferenceDate(getCurrentDate());
|
||||
request.setTradeMovement(TradeMovementCode.fromValue(tradeMovement));
|
||||
|
||||
logger.info("Lookup Measure for {} and {}", goodsCode, countryCode);
|
||||
|
||||
JAXBElement<GoodsMeasForWs> requestElement = objectFactory.createGoodsMeasForWs(request);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
JAXBElement<GoodsMeasForWsResponse> responseElement =
|
||||
(JAXBElement<GoodsMeasForWsResponse>) webServiceTemplate.marshalSendAndReceive(requestElement);
|
||||
|
||||
logger.info("Lookup Measure for {} and {} success: {} Measures received.", goodsCode, countryCode, responseElement.getValue().getReturn().getResult().getMeasures().getMeasure().size());
|
||||
|
||||
return CompletableFuture.completedFuture(responseElement.getValue());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ public class GeoApiService {
|
|||
}
|
||||
|
||||
public GeocodingResult geocode(String address) {
|
||||
|
||||
if (address == null || address.trim().isEmpty()) {
|
||||
logger.warn("Address is null or empty");
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import de.avatic.lcc.util.exception.base.InternalErrorException;
|
|||
import eu.europa.ec.taxation.taric.client.GoodsMeasForWsResponse;
|
||||
import eu.europa.ec.taxation.taric.client.GoodsMeasuresForWsResponse;
|
||||
import jakarta.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
|
@ -30,15 +32,16 @@ public class TaxationResolverService {
|
|||
private final CountryRepository countryRepository;
|
||||
private final EUTaxationApiService eUTaxationApiService;
|
||||
private final PropertyRepository propertyRepository;
|
||||
private final ZolltarifnummernApiService zolltarifnummernApiService;
|
||||
private final NomenclatureService nomenclatureService;
|
||||
private final CountryPropertyRepository countryPropertyRepository;
|
||||
|
||||
public TaxationResolverService(CountryRepository countryRepository, EUTaxationApiService eUTaxationApiService, PropertyRepository propertyRepository, ZolltarifnummernApiService zolltarifnummernApiService, NomenclatureService nomenclatureService, CountryPropertyRepository countryPropertyRepository) {
|
||||
private final Logger logger = LoggerFactory.getLogger(TaxationResolverService.class);
|
||||
|
||||
|
||||
public TaxationResolverService(CountryRepository countryRepository, EUTaxationApiService eUTaxationApiService, PropertyRepository propertyRepository, NomenclatureService nomenclatureService, CountryPropertyRepository countryPropertyRepository) {
|
||||
this.countryRepository = countryRepository;
|
||||
this.eUTaxationApiService = eUTaxationApiService;
|
||||
this.propertyRepository = propertyRepository;
|
||||
this.zolltarifnummernApiService = zolltarifnummernApiService;
|
||||
this.nomenclatureService = nomenclatureService;
|
||||
this.countryPropertyRepository = countryPropertyRepository;
|
||||
}
|
||||
|
|
@ -73,39 +76,30 @@ public class TaxationResolverService {
|
|||
var singleResponses = doSingleRequests(joined.toList());
|
||||
|
||||
return Stream.of(
|
||||
byCustomUnion.getOrDefault(CustomUnionType.NONE,Collections.emptyList()).stream().collect(Collectors.toMap(
|
||||
r -> r,
|
||||
r -> new TaxationResolverApiResponse(
|
||||
r.material(),
|
||||
true,
|
||||
singleResponses.keySet().stream().filter(k -> k.origin.equals(r)).map(singleResponses::get).toList()))),
|
||||
byCustomUnion.getOrDefault(CustomUnionType.NONE, Collections.emptyList()).stream().collect(Collectors.toMap(
|
||||
r -> r,
|
||||
r -> new TaxationResolverApiResponse(
|
||||
r.material(),
|
||||
true,
|
||||
singleResponses.keySet().stream().filter(k -> k.origin.equals(r)).map(singleResponses::get).toList()))),
|
||||
|
||||
byCustomUnion.getOrDefault(CustomUnionType.EU,Collections.emptyList()).stream().collect(Collectors.toMap(
|
||||
r -> r,
|
||||
r -> new TaxationResolverApiResponse(
|
||||
r.material(),
|
||||
false,
|
||||
null))))
|
||||
byCustomUnion.getOrDefault(CustomUnionType.EU, Collections.emptyList()).stream().collect(Collectors.toMap(
|
||||
r -> r,
|
||||
r -> new TaxationResolverApiResponse(
|
||||
r.material(),
|
||||
false,
|
||||
null))))
|
||||
|
||||
.flatMap(map -> map.entrySet().stream()).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,(r1,_) -> r1));
|
||||
.flatMap(map -> map.entrySet().stream()).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (r1, _) -> r1));
|
||||
|
||||
}
|
||||
|
||||
private List<TaxationResolverSingleRequest> resolveIncompleteHsCodesIntern(List<TaxationResolverRequest> request) {
|
||||
return request.stream().flatMap(r -> nomenclatureService.getNomenclature(r.material().getHsCode()).stream().map(hsCode -> new TaxationResolverSingleRequest(hsCode, r.countryId(), r))).toList();
|
||||
}
|
||||
var singleRequests = request.stream().flatMap(r -> nomenclatureService.getNomenclature(r.material().getHsCode()).stream().map(hsCode -> new TaxationResolverSingleRequest(hsCode, r.countryId(), r))).toList();
|
||||
|
||||
private List<TaxationResolverSingleRequest> resolveIncompleteHsCodes(List<TaxationResolverRequest> request) {
|
||||
logger.info("Resolved {} incomplete hs codes to {} hs code leaves ", request.size(), singleRequests.size());
|
||||
|
||||
var futures = request.stream()
|
||||
.collect(Collectors.toMap(
|
||||
r -> r,
|
||||
r -> zolltarifnummernApiService.getDeclarableHsCodes(r.material().getHsCode()))
|
||||
);
|
||||
|
||||
CompletableFuture.allOf(futures.values().toArray(new CompletableFuture[0])).join();
|
||||
|
||||
return futures.keySet().stream().flatMap(k -> futures.get(k).join().stream().map(resp -> new TaxationResolverSingleRequest(resp, k.countryId(), k))).toList();
|
||||
return singleRequests;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -244,6 +238,9 @@ public class TaxationResolverService {
|
|||
}
|
||||
|
||||
public List<TaxationResolverResponse> getTariffRates(List<TaxationResolverRequest> requests) {
|
||||
|
||||
logger.info("Do taxation resolution for {} requests", requests.size());
|
||||
|
||||
var goodMeasures = doRequests(requests);
|
||||
return goodMeasures.keySet().stream().map(r -> mapToResponse(r, goodMeasures.get(r))).toList();
|
||||
}
|
||||
|
|
@ -251,12 +248,15 @@ public class TaxationResolverService {
|
|||
private TaxationResolverResponse mapToResponse(TaxationResolverRequest request, TaxationResolverApiResponse apiResponse) {
|
||||
|
||||
// source is EU country.
|
||||
if(!apiResponse.requestExecuted)
|
||||
if (!apiResponse.requestExecuted)
|
||||
return new TaxationResolverResponse(0.0, null, request.material().getHsCode(), request.material(), request.countryId());
|
||||
|
||||
|
||||
List<GoodsMeasForWsResponse> measForWsResponse = apiResponse.apiResponse();
|
||||
|
||||
logger.info("============================");
|
||||
logger.info("Resolved measures for: {}, {}", request.material(), request.countryId());
|
||||
|
||||
try {
|
||||
String selectedHsCode = null;
|
||||
Double selectedDuty = null;
|
||||
|
|
@ -277,11 +277,20 @@ public class TaxationResolverService {
|
|||
var measureType = MeasureType.fromMeasureCode(measure.getMeasureType().getMeasureType());
|
||||
boolean maybeRelevant = measureType.map(MeasureType::containsRelevantDuty).orElse(false);
|
||||
|
||||
|
||||
if (maybeRelevant) {
|
||||
var duty = extractDuty(measure);
|
||||
|
||||
if (duty.isPresent()) {
|
||||
|
||||
logger.info("Measure ({}{}, {}): is_relevant: true, duty: {}, hs code: {}",
|
||||
measureType.map(MeasureType::getSeries).orElse("UNKNOWN"),
|
||||
measureType.map(MeasureType::getMeasureCode).orElse(""),
|
||||
measureType.map(MeasureType::name).orElse("UNKNOWN"),
|
||||
duty,
|
||||
entry.getKey().getReturn().getResult().getRequest().getGoodsCode());
|
||||
|
||||
|
||||
maxDuty = Math.max(maxDuty, duty.get());
|
||||
minDuty = Math.min(minDuty, duty.get());
|
||||
|
||||
|
|
@ -291,11 +300,23 @@ public class TaxationResolverService {
|
|||
selectedMeasure = measureType.map(MeasureType::getMeasureCode).orElse(null);
|
||||
selectedHsCode = entry.getKey().getReturn().getResult().getRequest().getGoodsCode();
|
||||
}
|
||||
} else {
|
||||
logger.info("Measure ({}{}, {}): is_relevant: true, no duty extracted",
|
||||
measureType.map(MeasureType::getSeries).orElse("UNKNOWN"),
|
||||
measureType.map(MeasureType::getMeasureCode).orElse(""),
|
||||
measureType.map(MeasureType::name).orElse("UNKNOWN"));
|
||||
}
|
||||
} else {
|
||||
logger.info("Measure ({}{}, {}): is_relevant: false", measureType.map(MeasureType::getSeries).orElse("UNKNOWN"), measureType.map(MeasureType::getMeasureCode).orElse(""), measureType.map(MeasureType::name).orElse("UNKNOWN"));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("============================");
|
||||
logger.info("Selected: measure: {}, duty: {}, max_duty: {}, min_duty: {}", selectedMeasure, selectedDuty, maxDuty, minDuty);
|
||||
logger.info("============================");
|
||||
|
||||
if (selectedDuty != null && (maxDuty - minDuty <= 0.02)) {
|
||||
return new TaxationResolverResponse(selectedDuty, selectedMeasure, selectedHsCode, request.material(), request.countryId());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
package de.avatic.lcc.service.api;
|
||||
|
||||
import de.avatic.lcc.model.zolltarifnummern.ZolltarifnummernResponse;
|
||||
import de.avatic.lcc.model.zolltarifnummern.ZolltarifnummernResponseEntry;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class ZolltarifnummernApiService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ZolltarifnummernApiService.class);
|
||||
|
||||
private static final String API_V_1 = "https://www.zolltarifnummern.de/api/v1/cnSuggest";
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
public ZolltarifnummernApiService(RestTemplate restTemplate) {
|
||||
this.restTemplate = restTemplate;
|
||||
}
|
||||
|
||||
@Async("customLookupExecutor")
|
||||
public CompletableFuture<Set<String>> getDeclarableHsCodes(String incompleteHsCode) {
|
||||
|
||||
try {
|
||||
String url = UriComponentsBuilder.fromUriString(API_V_1)
|
||||
.queryParam("term", incompleteHsCode)
|
||||
.queryParam("lang", "en")
|
||||
.encode()
|
||||
.toUriString();
|
||||
|
||||
var resp = restTemplate.getForObject(url, ZolltarifnummernResponse.class);
|
||||
|
||||
if (resp != null && resp.getSuggestions() != null) {
|
||||
return CompletableFuture.completedFuture(resp.getSuggestions().stream()
|
||||
.map(ZolltarifnummernResponseEntry::getCode)
|
||||
.filter(s -> s.startsWith(incompleteHsCode))
|
||||
.filter(s -> s.length() >= 10).collect(Collectors.toSet()));
|
||||
}
|
||||
|
||||
} catch (Throwable t) {
|
||||
logger.error("HS code lookup failed with exception \"{}\"", t.getMessage());
|
||||
// just continue
|
||||
}
|
||||
|
||||
logger.warn("Unable to load tarif numbers for HS code {}", incompleteHsCode);
|
||||
|
||||
return CompletableFuture.completedFuture(Collections.emptySet());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,13 +1,23 @@
|
|||
package de.avatic.lcc.service.apps;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.avatic.lcc.dto.configuration.apps.AppDTO;
|
||||
import de.avatic.lcc.dto.configuration.apps.AppExchangeDTO;
|
||||
import de.avatic.lcc.model.db.users.App;
|
||||
import de.avatic.lcc.repositories.users.AppRepository;
|
||||
import de.avatic.lcc.service.transformer.apps.AppTransformer;
|
||||
import de.avatic.lcc.util.exception.base.InternalErrorException;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import javax.crypto.*;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.*;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
|
@ -16,14 +26,31 @@ import java.util.UUID;
|
|||
@Service
|
||||
public class AppsService {
|
||||
|
||||
|
||||
private static final String HMAC_ALGORITHM = "HmacSHA256";
|
||||
private static final String ENCRYPTION_ALGORITHM = "AES/GCM/NoPadding";
|
||||
private static final int GCM_TAG_LENGTH = 128;
|
||||
private static final int GCM_IV_LENGTH = 12;
|
||||
private final AppRepository appRepository;
|
||||
private final AppTransformer appTransformer;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final Key signingKey;
|
||||
private final SecretKeySpec encryptionKey;
|
||||
|
||||
public AppsService(AppRepository appRepository, AppTransformer appTransformer, PasswordEncoder passwordEncoder) {
|
||||
|
||||
public AppsService(@Value("${jwt.secret}") String secret, AppRepository appRepository, AppTransformer appTransformer, PasswordEncoder passwordEncoder, ObjectMapper objectMapper) {
|
||||
this.appRepository = appRepository;
|
||||
this.appTransformer = appTransformer;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.signingKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
|
||||
this.objectMapper = objectMapper;
|
||||
|
||||
// AES-256 Key aus JWT Secret ableiten
|
||||
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] aesKey = new byte[32]; // 256 bit
|
||||
System.arraycopy(keyBytes, 0, aesKey, 0, Math.min(keyBytes.length, 32));
|
||||
this.encryptionKey = new SecretKeySpec(aesKey, "AES");
|
||||
}
|
||||
|
||||
public List<AppDTO> listApps() {
|
||||
|
|
@ -35,7 +62,7 @@ public class AppsService {
|
|||
var newApp = dto.getId() == null;
|
||||
String appSecret = null;
|
||||
|
||||
if(newApp) {
|
||||
if (newApp) {
|
||||
dto.setClientId(generateAppId());
|
||||
appSecret = generateAppSecret();
|
||||
dto.setClientSecret(passwordEncoder.encode(appSecret));
|
||||
|
|
@ -43,7 +70,7 @@ public class AppsService {
|
|||
|
||||
var id = appRepository.update(appTransformer.toAppEntity(dto));
|
||||
|
||||
if(newApp) {
|
||||
if (newApp) {
|
||||
dto.setId(id);
|
||||
dto.setClientSecret(appSecret);
|
||||
}
|
||||
|
|
@ -79,4 +106,103 @@ public class AppsService {
|
|||
|
||||
}
|
||||
|
||||
public AppExchangeDTO exportApp(Integer id) {
|
||||
var app = appRepository.getById(id).map(appTransformer::toAppDTOWithHashedSecret);
|
||||
|
||||
if (app.isEmpty()) {
|
||||
throw new IllegalArgumentException("App mit ID " + id + " nicht gefunden");
|
||||
}
|
||||
AppExchangeDTO exchangeDTO = new AppExchangeDTO();
|
||||
try {
|
||||
|
||||
String json = objectMapper.writeValueAsString(app);
|
||||
byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM);
|
||||
byte[] iv = new byte[GCM_IV_LENGTH];
|
||||
new SecureRandom().nextBytes(iv);
|
||||
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, gcmSpec);
|
||||
byte[] encryptedData = cipher.doFinal(jsonBytes);
|
||||
|
||||
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
|
||||
mac.init(new SecretKeySpec(signingKey.getEncoded(), HMAC_ALGORITHM));
|
||||
mac.update(iv);
|
||||
mac.update(encryptedData);
|
||||
byte[] signature = mac.doFinal();
|
||||
|
||||
byte[] bundle = new byte[iv.length + encryptedData.length + signature.length];
|
||||
System.arraycopy(iv, 0, bundle, 0, iv.length);
|
||||
System.arraycopy(encryptedData, 0, bundle, iv.length, encryptedData.length);
|
||||
System.arraycopy(signature, 0, bundle, iv.length + encryptedData.length, signature.length);
|
||||
|
||||
|
||||
exchangeDTO.setData(Base64.getEncoder().encodeToString(bundle));
|
||||
} catch (JsonProcessingException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException |
|
||||
InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException _) {
|
||||
throw new InternalErrorException("Fehler beim Exportieren der App");
|
||||
}
|
||||
|
||||
|
||||
return exchangeDTO;
|
||||
}
|
||||
|
||||
public boolean importApp(AppExchangeDTO exchangeDTO) {
|
||||
|
||||
try {
|
||||
|
||||
byte[] bundle = Base64.getDecoder().decode(exchangeDTO.getData());
|
||||
|
||||
// 2. Bundle aufteilen
|
||||
if (bundle.length < GCM_IV_LENGTH + 32) {
|
||||
throw new IllegalArgumentException("Ungültiges Export-Bundle");
|
||||
}
|
||||
|
||||
byte[] iv = new byte[GCM_IV_LENGTH];
|
||||
byte[] signature = new byte[32];
|
||||
byte[] encryptedData = new byte[bundle.length - GCM_IV_LENGTH - 32];
|
||||
|
||||
System.arraycopy(bundle, 0, iv, 0, GCM_IV_LENGTH);
|
||||
System.arraycopy(bundle, GCM_IV_LENGTH, encryptedData, 0, encryptedData.length);
|
||||
System.arraycopy(bundle, GCM_IV_LENGTH + encryptedData.length, signature, 0, 32);
|
||||
|
||||
// 3. Signatur verifizieren
|
||||
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
|
||||
mac.init(new SecretKeySpec(signingKey.getEncoded(), HMAC_ALGORITHM));
|
||||
mac.update(iv);
|
||||
mac.update(encryptedData);
|
||||
byte[] expectedSignature = mac.doFinal();
|
||||
|
||||
if (!java.security.MessageDigest.isEqual(signature, expectedSignature)) {
|
||||
throw new SecurityException("Ungültige Signatur - Daten wurden manipuliert oder stammen nicht von dieser Instanz");
|
||||
}
|
||||
|
||||
// 4. Entschlüsseln
|
||||
Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM);
|
||||
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
|
||||
cipher.init(Cipher.DECRYPT_MODE, encryptionKey, gcmSpec);
|
||||
byte[] decryptedData = cipher.doFinal(encryptedData);
|
||||
|
||||
// 5. JSON deserialisieren
|
||||
String json = new String(decryptedData, StandardCharsets.UTF_8);
|
||||
AppDTO dto = objectMapper.readValue(json, AppDTO.class);
|
||||
|
||||
// 6. Prüfen ob App mit dieser Client-ID bereits existiert
|
||||
var existingApp = appRepository.getByClientId(dto.getClientId());
|
||||
if (existingApp.isPresent()) {
|
||||
throw new IllegalStateException(
|
||||
"App mit Client-ID '" + dto.getClientId() + "' existiert bereits"
|
||||
);
|
||||
}
|
||||
|
||||
App app = appTransformer.toAppEntityWithHashedSecret(dto);
|
||||
appRepository.update(app);
|
||||
} catch (JsonProcessingException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException |
|
||||
InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException _) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ import de.avatic.lcc.dto.bulk.BulkFileType;
|
|||
import de.avatic.lcc.model.bulk.BulkFileTypes;
|
||||
import de.avatic.lcc.model.bulk.BulkOperation;
|
||||
import de.avatic.lcc.model.bulk.HiddenTableType;
|
||||
import de.avatic.lcc.repositories.rates.ValidityPeriodRepository;
|
||||
import de.avatic.lcc.service.bulk.helper.HeaderCellStyleProvider;
|
||||
import de.avatic.lcc.service.bulk.helper.CellStyleProvider;
|
||||
import de.avatic.lcc.service.excelMapper.*;
|
||||
import org.apache.poi.ss.usermodel.CellStyle;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
|
|
@ -21,10 +20,9 @@ import java.io.IOException;
|
|||
@Service
|
||||
public class BulkExportService {
|
||||
|
||||
private final HeaderCellStyleProvider headerCellStyleProvider;
|
||||
private final CellStyleProvider cellStyleProvider;
|
||||
private final ContainerRateExcelMapper containerRateExcelMapper;
|
||||
private final MatrixRateExcelMapper matrixRateExcelMapper;
|
||||
private final MaterialExcelMapper materialExcelMapper;
|
||||
private final PackagingExcelMapper packagingExcelMapper;
|
||||
private final NodeExcelMapper nodeExcelMapper;
|
||||
private final HiddenNodeExcelMapper hiddenNodeExcelMapper;
|
||||
|
|
@ -32,11 +30,10 @@ public class BulkExportService {
|
|||
private final String sheetPassword;
|
||||
private final MaterialFastExcelMapper materialFastExcelMapper;
|
||||
|
||||
public BulkExportService(@Value("${lcc.bulk.sheet_password}") String sheetPassword, HeaderCellStyleProvider headerCellStyleProvider, ContainerRateExcelMapper containerRateExcelMapper, MatrixRateExcelMapper matrixRateExcelMapper, MaterialExcelMapper materialExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper, HiddenNodeExcelMapper hiddenNodeExcelMapper, HiddenCountryExcelMapper hiddenCountryExcelMapper, MaterialFastExcelMapper materialFastExcelMapper) {
|
||||
this.headerCellStyleProvider = headerCellStyleProvider;
|
||||
public BulkExportService(@Value("${lcc.bulk.sheet_password}") String sheetPassword, CellStyleProvider cellStyleProvider, ContainerRateExcelMapper containerRateExcelMapper, MatrixRateExcelMapper matrixRateExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper, HiddenNodeExcelMapper hiddenNodeExcelMapper, HiddenCountryExcelMapper hiddenCountryExcelMapper, MaterialFastExcelMapper materialFastExcelMapper) {
|
||||
this.cellStyleProvider = cellStyleProvider;
|
||||
this.containerRateExcelMapper = containerRateExcelMapper;
|
||||
this.matrixRateExcelMapper = matrixRateExcelMapper;
|
||||
this.materialExcelMapper = materialExcelMapper;
|
||||
this.packagingExcelMapper = packagingExcelMapper;
|
||||
this.nodeExcelMapper = nodeExcelMapper;
|
||||
this.hiddenNodeExcelMapper = hiddenNodeExcelMapper;
|
||||
|
|
@ -68,7 +65,7 @@ public class BulkExportService {
|
|||
Workbook workbook = new XSSFWorkbook();
|
||||
Sheet worksheet = workbook.createSheet(BulkFileTypes.valueOf(bulkFileType.name()).getSheetName());
|
||||
|
||||
CellStyle style = headerCellStyleProvider.createHeaderCellStyle(workbook);
|
||||
CellStyle style = cellStyleProvider.createHeaderCellStyle(workbook);
|
||||
|
||||
|
||||
if (bulkFileType.equals(BulkFileType.COUNTRY_MATRIX) || bulkFileType.equals(BulkFileType.NODE)) {
|
||||
|
|
@ -93,10 +90,6 @@ public class BulkExportService {
|
|||
matrixRateExcelMapper.fillSheet(worksheet, style, periodId);
|
||||
matrixRateExcelMapper.createConstraints(workbook, worksheet);
|
||||
break;
|
||||
// case MATERIAL:
|
||||
// materialExcelMapper.fillSheet(worksheet, style);
|
||||
// materialExcelMapper.createConstraints(worksheet);
|
||||
// break;
|
||||
case PACKAGING:
|
||||
packagingExcelMapper.fillSheet(worksheet, style);
|
||||
packagingExcelMapper.createConstraints(workbook, worksheet);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package de.avatic.lcc.service.bulk;
|
|||
import de.avatic.lcc.dto.bulk.BulkFileType;
|
||||
import de.avatic.lcc.model.bulk.BulkFileTypes;
|
||||
import de.avatic.lcc.model.bulk.BulkOperation;
|
||||
import de.avatic.lcc.model.db.materials.Material;
|
||||
import de.avatic.lcc.service.api.BatchGeoApiService;
|
||||
import de.avatic.lcc.service.bulk.bulkImport.*;
|
||||
import de.avatic.lcc.service.excelMapper.*;
|
||||
|
|
@ -11,6 +12,8 @@ import org.apache.poi.ss.usermodel.Sheet;
|
|||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.apache.poi.util.RecordFormatException;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
|
@ -34,6 +37,10 @@ public class BulkImportService {
|
|||
private final BatchGeoApiService batchGeoApiService;
|
||||
private final MaterialFastExcelMapper materialFastExcelMapper;
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(BulkImportService.class);
|
||||
|
||||
|
||||
|
||||
public BulkImportService(MatrixRateExcelMapper matrixRateExcelMapper, ContainerRateExcelMapper containerRateExcelMapper, MaterialExcelMapper materialExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper, NodeBulkImportService nodeBulkImportService, PackagingBulkImportService packagingBulkImportService, MaterialBulkImportService materialBulkImportService, MatrixRateImportService matrixRateImportService, ContainerRateImportService containerRateImportService, BatchGeoApiService batchGeoApiService, MaterialFastExcelMapper materialFastExcelMapper) {
|
||||
this.matrixRateExcelMapper = matrixRateExcelMapper;
|
||||
this.containerRateExcelMapper = containerRateExcelMapper;
|
||||
|
|
@ -63,7 +70,15 @@ public class BulkImportService {
|
|||
|
||||
private void processOperationWithFastExcel(BulkOperation op) throws IOException {
|
||||
var materials = materialFastExcelMapper.importFromExcel(op.getFile());
|
||||
materials.forEach(materialBulkImportService::processMaterialInstructions);
|
||||
|
||||
int processed = 0;
|
||||
for(var material : materials) {
|
||||
|
||||
if(processed++ % 1000 == 0)
|
||||
log.info("Processed {} of {} materials", processed, materials.size());
|
||||
|
||||
materialBulkImportService.processMaterialInstructions(material);
|
||||
}
|
||||
}
|
||||
|
||||
private void processOperationWithApachePOI(BulkOperation op) throws IOException {
|
||||
|
|
@ -96,10 +111,6 @@ public class BulkImportService {
|
|||
var matrixRates = matrixRateExcelMapper.extractSheet(sheet);
|
||||
matrixRateImportService.processMatrixRates(matrixRates);
|
||||
break;
|
||||
// case MATERIAL:
|
||||
// var materials = materialExcelMapper.extractSheet(sheet);
|
||||
// materials.forEach(materialBulkImportService::processMaterialInstructions);
|
||||
// break;
|
||||
case PACKAGING:
|
||||
var packaging = packagingExcelMapper.extractSheet(sheet);
|
||||
packaging.forEach(packagingBulkImportService::processPackagingInstructions);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package de.avatic.lcc.service.bulk;
|
|||
import de.avatic.lcc.dto.bulk.BulkFileType;
|
||||
import de.avatic.lcc.model.bulk.*;
|
||||
import de.avatic.lcc.model.bulk.header.*;
|
||||
import de.avatic.lcc.service.bulk.helper.HeaderCellStyleProvider;
|
||||
import de.avatic.lcc.service.bulk.helper.CellStyleProvider;
|
||||
import de.avatic.lcc.service.bulk.helper.HeaderGenerator;
|
||||
import de.avatic.lcc.service.excelMapper.*;
|
||||
import org.apache.poi.ss.usermodel.CellStyle;
|
||||
|
|
@ -24,7 +24,7 @@ public class TemplateExportService {
|
|||
|
||||
|
||||
private final HeaderGenerator headerGenerator;
|
||||
private final HeaderCellStyleProvider headerCellStyleProvider;
|
||||
private final CellStyleProvider cellStyleProvider;
|
||||
private final HiddenNodeExcelMapper hiddenNodeExcelMapper;
|
||||
private final HiddenCountryExcelMapper hiddenCountryExcelMapper;
|
||||
private final String sheetPassword;
|
||||
|
|
@ -34,9 +34,9 @@ public class TemplateExportService {
|
|||
private final PackagingExcelMapper packagingExcelMapper;
|
||||
private final NodeExcelMapper nodeExcelMapper;
|
||||
|
||||
public TemplateExportService(@Value("${lcc.bulk.sheet_password}") String sheetPassword, HeaderGenerator headerGenerator, HeaderCellStyleProvider headerCellStyleProvider, HiddenNodeExcelMapper hiddenNodeExcelMapper, HiddenCountryExcelMapper hiddenCountryExcelMapper, ContainerRateExcelMapper containerRateExcelMapper, MatrixRateExcelMapper matrixRateExcelMapper, MaterialExcelMapper materialExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper) {
|
||||
public TemplateExportService(@Value("${lcc.bulk.sheet_password}") String sheetPassword, HeaderGenerator headerGenerator, CellStyleProvider cellStyleProvider, HiddenNodeExcelMapper hiddenNodeExcelMapper, HiddenCountryExcelMapper hiddenCountryExcelMapper, ContainerRateExcelMapper containerRateExcelMapper, MatrixRateExcelMapper matrixRateExcelMapper, MaterialExcelMapper materialExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper) {
|
||||
this.headerGenerator = headerGenerator;
|
||||
this.headerCellStyleProvider = headerCellStyleProvider;
|
||||
this.cellStyleProvider = cellStyleProvider;
|
||||
this.hiddenNodeExcelMapper = hiddenNodeExcelMapper;
|
||||
this.hiddenCountryExcelMapper = hiddenCountryExcelMapper;
|
||||
this.sheetPassword = sheetPassword;
|
||||
|
|
@ -53,7 +53,7 @@ public class TemplateExportService {
|
|||
|
||||
Sheet sheet = workbook.createSheet(BulkFileTypes.valueOf(bulkFileType.name()).getSheetName());
|
||||
|
||||
CellStyle style = headerCellStyleProvider.createHeaderCellStyle(workbook);
|
||||
CellStyle style = cellStyleProvider.createHeaderCellStyle(workbook);
|
||||
|
||||
|
||||
if (bulkFileType.equals(BulkFileType.COUNTRY_MATRIX) || bulkFileType.equals(BulkFileType.NODE)) {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ public class MaterialBulkImportService {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private void updateMaterial(Material material) {
|
||||
var foundMaterial = materialRepository.getByPartNumber(material.getNormalizedPartNumber());
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue