Add help system with markdown-based content, video support, and help menu integration; update related UI components, backend services, and frontend store to enable contextual help functionality.
This commit is contained in:
parent
22051135ad
commit
8742d24b62
28 changed files with 2010 additions and 51 deletions
5
pom.xml
5
pom.xml
|
|
@ -130,6 +130,11 @@
|
||||||
<artifactId>fastexcel</artifactId>
|
<artifactId>fastexcel</artifactId>
|
||||||
<version>0.19.0</version>
|
<version>0.19.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.commonmark</groupId>
|
||||||
|
<artifactId>commonmark</artifactId>
|
||||||
|
<version>0.22.0</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-devtools</artifactId>
|
<artifactId>spring-boot-devtools</artifactId>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<the-notification-system />
|
<the-notification-system />
|
||||||
|
<the-help-system />
|
||||||
<the-header></the-header>
|
<the-header></the-header>
|
||||||
<router-view v-slot="slotProps">
|
<router-view v-slot="slotProps">
|
||||||
<transition name="route" mode="out-in">
|
<transition name="route" mode="out-in">
|
||||||
|
|
@ -13,9 +14,10 @@
|
||||||
|
|
||||||
import TheHeader from "@/components/layout/TheHeader.vue";
|
import TheHeader from "@/components/layout/TheHeader.vue";
|
||||||
import TheNotificationSystem from "@/components/UI/TheNotificationSystem.vue";
|
import TheNotificationSystem from "@/components/UI/TheNotificationSystem.vue";
|
||||||
|
import TheHelpSystem from "@/components/layout/help/TheHelpSystem.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {TheNotificationSystem, TheHeader},
|
components: {TheHelpSystem, TheNotificationSystem, TheHeader},
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ export default {
|
||||||
return; // Allow keyboard scrolling inside modal
|
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];
|
const scrollKeys = [32, 33, 34, 35, 36, 37, 38, 39, 40];
|
||||||
if (scrollKeys.includes(e.keyCode)) {
|
if (scrollKeys.includes(e.keyCode)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
<PhCaretLeft :size="18" /> Previous
|
<PhCaretLeft :size="18" /> Previous
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- First page -->
|
<!-- First helppages -->
|
||||||
<button
|
<button
|
||||||
v-if="showFirstPage"
|
v-if="showFirstPage"
|
||||||
class="pagination-btn page-number"
|
class="pagination-btn page-number"
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
<!-- First ellipsis -->
|
<!-- First ellipsis -->
|
||||||
<span v-if="showFirstEllipsis" class="ellipsis">...</span>
|
<span v-if="showFirstEllipsis" class="ellipsis">...</span>
|
||||||
|
|
||||||
<!-- Page numbers around current page -->
|
<!-- Page numbers around current helppages -->
|
||||||
<button
|
<button
|
||||||
v-for="pageNum in visiblePages"
|
v-for="pageNum in visiblePages"
|
||||||
:key="pageNum"
|
:key="pageNum"
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
<!-- Last ellipsis -->
|
<!-- Last ellipsis -->
|
||||||
<span v-if="showLastEllipsis" class="ellipsis">...</span>
|
<span v-if="showLastEllipsis" class="ellipsis">...</span>
|
||||||
|
|
||||||
<!-- Last page -->
|
<!-- Last helppages -->
|
||||||
<button
|
<button
|
||||||
v-if="showLastPage"
|
v-if="showLastPage"
|
||||||
class="pagination-btn page-number"
|
class="pagination-btn page-number"
|
||||||
|
|
@ -90,7 +90,7 @@ export default {
|
||||||
default: 5
|
default: 5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['page-change'],
|
emits: ['helppages-change'],
|
||||||
computed: {
|
computed: {
|
||||||
visiblePages() {
|
visiblePages() {
|
||||||
const delta = Math.floor(this.maxVisiblePages / 2);
|
const delta = Math.floor(this.maxVisiblePages / 2);
|
||||||
|
|
@ -130,7 +130,7 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
goToPage(pageNumber) {
|
goToPage(pageNumber) {
|
||||||
if (pageNumber >= 1 && pageNumber <= this.pageCount && pageNumber !== this.page) {
|
if (pageNumber >= 1 && pageNumber <= this.pageCount && pageNumber !== this.page) {
|
||||||
this.$emit('page-change', pageNumber);
|
this.$emit('helppages-change', pageNumber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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>
|
||||||
|
|
@ -34,7 +34,7 @@ import {
|
||||||
PhUpload,
|
PhUpload,
|
||||||
PhWarning,
|
PhWarning,
|
||||||
PhX,
|
PhX,
|
||||||
PhExclamationMark, PhMapPin, PhEmpty, PhShippingContainer, PhPackage, PhVectorThree, PhTag
|
PhExclamationMark, PhMapPin, PhEmpty, PhShippingContainer, PhPackage, PhVectorThree, PhTag, PhInfo
|
||||||
} from "@phosphor-icons/vue";
|
} from "@phosphor-icons/vue";
|
||||||
import {setupSessionRefresh} from "@/store/activeuser.js";
|
import {setupSessionRefresh} from "@/store/activeuser.js";
|
||||||
|
|
||||||
|
|
@ -81,6 +81,7 @@ app.component("PhMapPin", PhMapPin);
|
||||||
app.component("PhPackage", PhPackage);
|
app.component("PhPackage", PhPackage);
|
||||||
app.component("PhVectorThree", PhVectorThree);
|
app.component("PhVectorThree", PhVectorThree);
|
||||||
app.component("PhTag", PhTag);
|
app.component("PhTag", PhTag);
|
||||||
|
app.component("PhInfo", PhInfo);
|
||||||
|
|
||||||
|
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="start-calculation-container">
|
<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">
|
<div class="part-numbers-headers">
|
||||||
|
|
@ -23,8 +32,15 @@
|
||||||
<textarea v-model="partNumberField" name="partNumbers" cols="140" rows="15"></textarea>
|
<textarea v-model="partNumberField" name="partNumbers" cols="140" rows="15"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="part-number-modal-action">
|
<div class="part-number-modal-action">
|
||||||
<basic-button @click="parsePartNumbers" icon="CloudArrowUp">Analyze input</basic-button>
|
<div class="part-number-modal-action-help">
|
||||||
<basic-button @click="closeModal('partNumber')" :show-icon="false" variant="secondary">Cancel</basic-button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</modal>
|
||||||
|
|
@ -88,11 +104,22 @@ import CreateNewNode from "@/components/layout/node/CreateNewNode.vue";
|
||||||
import Checkbox from "@/components/UI/Checkbox.vue";
|
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||||
import {UrlSafeBase64} from "@/common.js";
|
import {UrlSafeBase64} from "@/common.js";
|
||||||
import {useNotificationStore} from "@/store/notification.js";
|
import {useNotificationStore} from "@/store/notification.js";
|
||||||
|
import IconButton from "@/components/UI/IconButton.vue";
|
||||||
|
import {useHelpStore} from "@/store/help.js";
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "CalculationAssistant",
|
name: "CalculationAssistant",
|
||||||
components: {Checkbox, CreateNewNode, Modal, SupplierItem, MaterialItem, BasicButton, AutosuggestSearchbar},
|
components: {
|
||||||
|
IconButton,
|
||||||
|
Checkbox,
|
||||||
|
CreateNewNode,
|
||||||
|
Modal,
|
||||||
|
SupplierItem,
|
||||||
|
MaterialItem,
|
||||||
|
BasicButton,
|
||||||
|
AutosuggestSearchbar
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(useNodeStore, useAssistantStore, useNotificationStore),
|
...mapStores(useNodeStore, useAssistantStore, useNotificationStore),
|
||||||
showPartNumberModal() {
|
showPartNumberModal() {
|
||||||
|
|
@ -108,6 +135,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
useHelpStore,
|
||||||
setUseExisting(useExisting) {
|
setUseExisting(useExisting) {
|
||||||
this.assistantStore.setCreateEmpty(!useExisting);
|
this.assistantStore.setCreateEmpty(!useExisting);
|
||||||
},
|
},
|
||||||
|
|
@ -160,7 +188,7 @@ export default {
|
||||||
parsePartNumbers() {
|
parsePartNumbers() {
|
||||||
this.closeModal('partNumber');
|
this.closeModal('partNumber');
|
||||||
|
|
||||||
if(this.partNumberField.trim().length !== 0)
|
if (this.partNumberField.trim().length !== 0)
|
||||||
this.assistantStore.getMaterialsAndSuppliers(this.partNumberField);
|
this.assistantStore.getMaterialsAndSuppliers(this.partNumberField);
|
||||||
|
|
||||||
this.partNumberField = '';
|
this.partNumberField = '';
|
||||||
|
|
@ -179,6 +207,22 @@ export default {
|
||||||
|
|
||||||
<style scoped>
|
<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 {
|
.start-calculation-footer-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
@ -232,10 +276,17 @@ textarea {
|
||||||
gap: 1.6rem;
|
gap: 1.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.part-number-modal-action-help {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
.part-number-modal-action {
|
.part-number-modal-action {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: space-between;
|
||||||
gap: 1.6rem
|
gap: 1.6rem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -246,6 +297,13 @@ textarea {
|
||||||
margin-bottom: 1.6rem;
|
margin-bottom: 1.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.part-number-modal-action-buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 1.6rem
|
||||||
|
}
|
||||||
|
|
||||||
.item-list {
|
.item-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,19 @@
|
||||||
<div class="edit-calculation-container"
|
<div class="edit-calculation-container"
|
||||||
:class="{ 'has-selection': hasSelection, 'apply-filter': applyFilter, 'add-all': addAll }">
|
:class="{ 'has-selection': hasSelection, 'apply-filter': applyFilter, 'add-all': addAll }">
|
||||||
<div class="header-container">
|
<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">
|
<div class="header-controls">
|
||||||
<basic-button :show-icon="true"
|
<basic-button :show-icon="true"
|
||||||
:disabled="disableButtons"
|
:disabled="disableButtons"
|
||||||
|
|
@ -173,6 +185,8 @@ import DestMassCreate from "@/components/layout/edit/destination/mass/DestMassCr
|
||||||
import ModalDialog from "@/components/UI/ModalDialog.vue";
|
import ModalDialog from "@/components/UI/ModalDialog.vue";
|
||||||
import destinationEdit from "@/components/layout/edit/destination/DestinationEdit.vue";
|
import destinationEdit from "@/components/layout/edit/destination/DestinationEdit.vue";
|
||||||
import logger from "@/logger.js";
|
import logger from "@/logger.js";
|
||||||
|
import IconButton from "@/components/UI/IconButton.vue";
|
||||||
|
import {useHelpStore} from "@/store/help.js";
|
||||||
|
|
||||||
|
|
||||||
const COMPONENT_TYPES = {
|
const COMPONENT_TYPES = {
|
||||||
|
|
@ -187,6 +201,7 @@ const COMPONENT_TYPES = {
|
||||||
export default {
|
export default {
|
||||||
name: "MassEdit",
|
name: "MassEdit",
|
||||||
components: {
|
components: {
|
||||||
|
IconButton,
|
||||||
ModalDialog,
|
ModalDialog,
|
||||||
SortButton,
|
SortButton,
|
||||||
Modal,
|
Modal,
|
||||||
|
|
@ -298,6 +313,7 @@ export default {
|
||||||
window.removeEventListener('keyup', this.handleKeyUp);
|
window.removeEventListener('keyup', this.handleKeyUp);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
useHelpStore,
|
||||||
handleKeyDown(event) {
|
handleKeyDown(event) {
|
||||||
if (event.key === 'Control') {
|
if (event.key === 'Control') {
|
||||||
this.isCtrlPressed = true;
|
this.isCtrlPressed = true;
|
||||||
|
|
@ -742,4 +758,20 @@ export default {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1.6rem;
|
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>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="edit-calculation-container">
|
<div class="edit-calculation-container">
|
||||||
<div class="header-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">
|
<div class="header-controls">
|
||||||
<basic-button @click="close" :show-icon="false" :disabled="premiseSingleEditStore.showLoadingSpinner"
|
<basic-button @click="close" :show-icon="false" :disabled="premiseSingleEditStore.showLoadingSpinner"
|
||||||
variant="secondary"> {{ fromMassEdit ? 'Back' : 'Close' }}
|
variant="secondary"> {{ fromMassEdit ? 'Back' : 'Close' }}
|
||||||
|
|
@ -107,6 +122,7 @@ import {UrlSafeBase64} from "@/common.js";
|
||||||
import {usePremiseSingleEditStore} from "@/store/premiseSingleEdit.js";
|
import {usePremiseSingleEditStore} from "@/store/premiseSingleEdit.js";
|
||||||
import {useNotificationStore} from "@/store/notification.js";
|
import {useNotificationStore} from "@/store/notification.js";
|
||||||
import Spinner from "@/components/UI/Spinner.vue";
|
import Spinner from "@/components/UI/Spinner.vue";
|
||||||
|
import {useHelpStore} from "@/store/help.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "SingleEdit",
|
name: "SingleEdit",
|
||||||
|
|
@ -161,6 +177,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
useHelpStore,
|
||||||
|
|
||||||
async startCalculation() {
|
async startCalculation() {
|
||||||
this.showCalculationModal = true;
|
this.showCalculationModal = true;
|
||||||
|
|
@ -209,6 +226,23 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<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 {
|
.edit-calculation-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<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>
|
<h2 class="page-sub-header">{{ greeting }}</h2>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<h3 class="sub-header">Status</h3>
|
<h3 class="sub-header">Status</h3>
|
||||||
|
|
||||||
<the-dashboard></the-dashboard>
|
<the-dashboard></the-dashboard>
|
||||||
|
|
||||||
<h3 class="sub-header">Drafts</h3>
|
<h3 class="sub-header">Drafts</h3>
|
||||||
<div class="calculation-list-container">
|
<div class="calculation-list-container">
|
||||||
|
|
@ -79,6 +87,7 @@ import modal from "@/components/UI/Modal.vue";
|
||||||
import {useActiveUserStore} from "@/store/activeuser.js";
|
import {useActiveUserStore} from "@/store/activeuser.js";
|
||||||
import TheDashboard from "@/components/layout/calculation/TheDashboard.vue";
|
import TheDashboard from "@/components/layout/calculation/TheDashboard.vue";
|
||||||
import Box from "@/components/UI/Box.vue";
|
import Box from "@/components/UI/Box.vue";
|
||||||
|
import {useHelpStore} from "@/store/help.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Calculation",
|
name: "Calculation",
|
||||||
|
|
@ -186,6 +195,7 @@ export default {
|
||||||
await this.executeSearch();
|
await this.executeSearch();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
useHelpStore,
|
||||||
async handleModalAction(action) {
|
async handleModalAction(action) {
|
||||||
if (action === 'dismiss') {
|
if (action === 'dismiss') {
|
||||||
this.modal.state = false;
|
this.modal.state = false;
|
||||||
|
|
@ -361,6 +371,21 @@ export default {
|
||||||
|
|
||||||
<style scoped>
|
<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 {
|
.page-header {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,26 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="header-container">
|
<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">
|
<div class="page-header-badges">
|
||||||
<basic-badge variant="primary" v-if="period">{{ period }}</basic-badge>
|
<basic-badge variant="primary" v-if="period">{{ period }}</basic-badge>
|
||||||
<basic-badge variant="secondary" v-if="partNumber">{{ partNumber }}</basic-badge>
|
<basic-badge variant="secondary" v-if="partNumber">{{ partNumber }}</basic-badge>
|
||||||
</div>
|
</div>
|
||||||
</h2>
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="header-controls">
|
<div class="header-controls">
|
||||||
<basic-button @click="createReport" icon="file">Create report</basic-button>
|
<basic-button @click="createReport" icon="file">Create report</basic-button>
|
||||||
<basic-button :disabled="!hasReport" variant="secondary" @click="downloadReport" icon="Download">Export
|
<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 Report from "@/components/layout/report/Report.vue";
|
||||||
import BasicBadge from "@/components/UI/BasicBadge.vue";
|
import BasicBadge from "@/components/UI/BasicBadge.vue";
|
||||||
import {buildDate} from "@/common.js";
|
import {buildDate} from "@/common.js";
|
||||||
|
import IconButton from "@/components/UI/IconButton.vue";
|
||||||
|
import {useHelpStore} from "@/store/help.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Reporting",
|
name: "Reporting",
|
||||||
components: {BasicBadge, Report, ReportChart, Spinner, Box, SelectForReport, BasicButton, Modal},
|
components: {IconButton, BasicBadge, Report, ReportChart, Spinner, Box, SelectForReport, BasicButton, Modal},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showModal: false,
|
showModal: false,
|
||||||
|
|
@ -79,13 +95,13 @@ export default {
|
||||||
|
|
||||||
for (let i = 0; i < scale.length; i++) {
|
for (let i = 0; i < scale.length; i++) {
|
||||||
for (const report of reports) {
|
for (const report of reports) {
|
||||||
if(report.destinations.length > i) {
|
if (report.destinations.length > i) {
|
||||||
scale[i] = Math.max(scale[i], report.destinations[i].sections.length);
|
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() {
|
reports() {
|
||||||
return this.reportsStore.reports
|
return this.reportsStore.reports
|
||||||
|
|
@ -113,6 +129,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
useHelpStore,
|
||||||
downloadReport() {
|
downloadReport() {
|
||||||
this.reportsStore.downloadReport();
|
this.reportsStore.downloadReport();
|
||||||
},
|
},
|
||||||
|
|
@ -140,6 +157,22 @@ export default {
|
||||||
|
|
||||||
<style scoped>
|
<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 {
|
.page-header-align {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -150,6 +183,7 @@ export default {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.8rem;
|
gap: 0.8rem;
|
||||||
|
margin-bottom: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.space-around {
|
.space-around {
|
||||||
|
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -20,7 +20,6 @@ export default defineConfig({
|
||||||
src: 'assets/map.json',
|
src: 'assets/map.json',
|
||||||
dest: 'assets/'
|
dest: 'assets/'
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
|
|
@ -35,4 +34,18 @@ export default defineConfig({
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
'@': 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -18,10 +18,5 @@ public class ShutdownListener {
|
||||||
Runtime runtime = Runtime.getRuntime();
|
Runtime runtime = Runtime.getRuntime();
|
||||||
long usedMemory = (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024;
|
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.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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
243
src/main/java/de/avatic/lcc/service/help/MarkdownService.java
Normal file
243
src/main/java/de/avatic/lcc/service/help/MarkdownService.java
Normal file
|
|
@ -0,0 +1,243 @@
|
||||||
|
package de.avatic.lcc.service.help;
|
||||||
|
|
||||||
|
import org.commonmark.node.Node;
|
||||||
|
import org.commonmark.parser.Parser;
|
||||||
|
import org.commonmark.renderer.html.HtmlRenderer;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class MarkdownService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(MarkdownService.class);
|
||||||
|
|
||||||
|
@Value(value = "${lcc.help.static:}")
|
||||||
|
private String imagesBaseUrl;
|
||||||
|
|
||||||
|
@Value("${help.markdown.cache.enabled:true}")
|
||||||
|
private boolean cacheEnabled;
|
||||||
|
|
||||||
|
private final Parser parser;
|
||||||
|
private final HtmlRenderer renderer;
|
||||||
|
private final Map<String, String> contentCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public MarkdownService() {
|
||||||
|
|
||||||
|
this.parser = Parser.builder()
|
||||||
|
|
||||||
|
.build();
|
||||||
|
|
||||||
|
this.renderer = HtmlRenderer.builder()
|
||||||
|
|
||||||
|
.escapeHtml(false) // HTML in Markdown erlauben
|
||||||
|
.sanitizeUrls(true) // URLs sanitizen
|
||||||
|
.build();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konvertiert Markdown-String zu HTML
|
||||||
|
*/
|
||||||
|
public String convertToHtml(String markdown, String imagePrefix) {
|
||||||
|
if (markdown == null || markdown.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Node document = parser.parse(markdown);
|
||||||
|
String html = renderer.render(document);
|
||||||
|
|
||||||
|
// Bild-URLs anpassen
|
||||||
|
return replaceImageUrls(html, imagePrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt Markdown-Datei und konvertiert zu HTML
|
||||||
|
*/
|
||||||
|
public String loadAndConvertMarkdown(String fileName, String imagePrefix) throws IOException {
|
||||||
|
// Cache-Key erstellen
|
||||||
|
String cacheKey = fileName + "_" + imagesBaseUrl.hashCode();
|
||||||
|
|
||||||
|
// Aus Cache laden wenn aktiviert
|
||||||
|
if (cacheEnabled && contentCache.containsKey(cacheKey)) {
|
||||||
|
logger.debug("Loading help content from cache: {}", fileName);
|
||||||
|
return contentCache.get(cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Loading help content from file: {}", fileName);
|
||||||
|
|
||||||
|
// Markdown-Datei laden
|
||||||
|
String markdown = loadMarkdownFile(fileName);
|
||||||
|
|
||||||
|
// Zu HTML konvertieren
|
||||||
|
String html = convertToHtml(markdown, imagePrefix);
|
||||||
|
|
||||||
|
// In HTML-Template einbetten
|
||||||
|
// html = wrapInHtmlTemplate(html, fileName);
|
||||||
|
|
||||||
|
// Im Cache speichern
|
||||||
|
if (cacheEnabled) {
|
||||||
|
contentCache.put(cacheKey, html);
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt Markdown-Datei aus Resources
|
||||||
|
*/
|
||||||
|
private String loadMarkdownFile(String fileName) throws IOException {
|
||||||
|
// .md Extension hinzufügen falls nicht vorhanden
|
||||||
|
if (!fileName.endsWith(".md")) {
|
||||||
|
fileName = fileName + ".md";
|
||||||
|
}
|
||||||
|
|
||||||
|
Resource resource = new ClassPathResource("static/help/" + fileName);
|
||||||
|
|
||||||
|
if (!resource.exists()) {
|
||||||
|
throw new IOException("Help file not found: " + fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ersetzt relative Bild-URLs durch absolute URLs zum Azure Blob Storage
|
||||||
|
*/
|
||||||
|
private String replaceImageUrls(String html, String imagePrefix) {
|
||||||
|
if (imagesBaseUrl == null || imagesBaseUrl.isEmpty()) {
|
||||||
|
logger.warn("Images base URL not configured");
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verschiedene Bild-URL-Formate ersetzen
|
||||||
|
html = html.replaceAll("src=\"images/", "src=\"" + imagesBaseUrl + "/" + imagePrefix + "-");
|
||||||
|
html = html.replaceAll("src='images/", "src='" + imagesBaseUrl + "/" + imagePrefix + "-");
|
||||||
|
html = html.replaceAll("src=\"./images/", "src=\"" + imagesBaseUrl + "/" + imagePrefix + "-");
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bettet HTML-Inhalt in vollständiges HTML-Dokument ein
|
||||||
|
*/
|
||||||
|
private String wrapInHtmlTemplate(String content, String title) {
|
||||||
|
return """
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>%s</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 100%%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #2c3e50;
|
||||||
|
border-bottom: 2px solid #3498db;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
color: #34495e;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
color: #34495e;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: #3498db;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
tr:nth-child(even) {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
blockquote {
|
||||||
|
border-left: 4px solid #3498db;
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin-left: 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
ul, ol {
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
%s
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
""".formatted(title, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leert den Content-Cache
|
||||||
|
*/
|
||||||
|
public void clearCache() {
|
||||||
|
contentCache.clear();
|
||||||
|
logger.info("Help content cache cleared");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt Cache-Statistiken zurück
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getCacheStats() {
|
||||||
|
return Map.of(
|
||||||
|
"cacheEnabled", cacheEnabled,
|
||||||
|
"cachedFiles", contentCache.size(),
|
||||||
|
"imagesBaseUrl", imagesBaseUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
113
src/main/resources/static/help/assistant.md
Normal file
113
src/main/resources/static/help/assistant.md
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
|
||||||
|
> The logistics cost calculation tool lets you create cost calculations for combinations of supplier and part number. You can set up several supplier/part number combos at once. The assistant helps you import part numbers, assign existing suppliers, add new suppliers, and then automatically generates all the calculations you need.
|
||||||
|
|
||||||
|
### Step 1: Start the Assistant for a New Calculation
|
||||||
|
|
||||||
|
In the main view of the logistics cost calculation tool, open the assistant by clicking the **“New Calculation”** button.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Step 2: Paste Part Numbers from Text
|
||||||
|
|
||||||
|
When you start the assistant, a window pops up where you can paste your part numbers. Just copy the relevant part numbers from a text, like an email, and drop them into the input field.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Step 3: Analyze the Text and Detect Part Numbers
|
||||||
|
|
||||||
|
Click **“Analyse Input”** in the assistant so the tool can scan the pasted text for part numbers and pull them out.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
All detected part numbers will then be listed in the assistant, ready for the next steps.
|
||||||
|
|
||||||
|
### Step 4: Reset Detected Part Numbers Input Step
|
||||||
|
|
||||||
|
If you spot a mistake or want to start over with different part numbers, click **“Drop Part Numbers”** at the top of the assistant. This will remove all currently detected part numbers and reset the input step.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You can then paste in new or corrected part numbers if needed.
|
||||||
|
|
||||||
|
### Step 5: Enter Part Numbers Manually and Analyze Again
|
||||||
|
|
||||||
|
You can also type in part numbers yourself. Enter the part numbers into the text field, separated by any delimiter you like (such as comma, semicolon, space, or line break).
|
||||||
|
Then click **“Analyse Input”** again so the tool can detect and extract the part numbers you entered.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The assistant will show the successfully recognized part numbers. The tool will also try to suggest matching suppliers if calculations have been made for certain part numbers and suppliers before. These will appear below the part numbers.
|
||||||
|
|
||||||
|
### Step 6: Remove Supplier and Part Number Entries
|
||||||
|
|
||||||
|
If you want to remove a supplier from your current selection, click the trash can icon next to that supplier.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You can remove a single part number from the list the same way.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This lets you tidy up any unnecessary combinations before creating your calculations.
|
||||||
|
|
||||||
|
### Step 7: Search for and Add Existing Suppliers
|
||||||
|
|
||||||
|
To add another supplier to one or more part numbers, use the search box in the assistant.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Enter the name of the supplier you want, pick them from the results list, and click **“Bietvorschlag”** to add them.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The supplier will then be assigned to the selected part numbers.
|
||||||
|
|
||||||
|
### Step 8: Create a New Supplier
|
||||||
|
|
||||||
|
If you don’t find the supplier you’re looking for, you can add a new one by clicking the **“Create New Supplier”** button.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
In the dialog that opens, type in the new supplier’s name and then enter their address.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
For example, you can enter the street, house number, zip code, and city (like “Bozener Straße, Ludwigshafen”). Click **“Check Address”** afterward so the tool can check and, if needed, correct the address.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
A map will be shown so you can confirm the address was correctly recognized.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Once all details are correct, click the **“Create”** button to add the new supplier and insert them directly into your current form.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Step 9: Review the Number of Calculations to be Created
|
||||||
|
|
||||||
|
After you’ve assigned all relevant part numbers and suppliers, the tool will show you at the bottom how many calculations will be created from the available combinations.
|
||||||
|
Each unique pairing of a part number and supplier results in its own logistics calculation.
|
||||||
|
|
||||||
|
### Step 10: Set Up Data Transfer from Existing Calculations
|
||||||
|
|
||||||
|
At the end of the assistant, there’s a checkbox that lets you choose whether to pull in data from already existing calculations:
|
||||||
|
|
||||||
|
* If the box is checked, the tool will try to import any available calculation data and pre-fill the new calculations, saving you manual entry later.
|
||||||
|
* If you leave it unchecked, the new calculations will be created from scratch, without any pre-filled info.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Step 11: Generate Calculations
|
||||||
|
|
||||||
|
Finally, click the **“Create”** button to set up all calculations for your selected combinations of part numbers and suppliers.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Your new logistics calculations will now be available in the system for further editing.
|
||||||
116
src/main/resources/static/help/dashboard.md
Normal file
116
src/main/resources/static/help/dashboard.md
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
|
||||||
|
> This document explains how the personal dashboard in the Logistics Cost Calculation Tool is set up, how to use filters, selection features, and the different actions available for one or multiple calculations.
|
||||||
|
|
||||||
|
### Step 1: Opening the Dashboard
|
||||||
|
|
||||||
|
When you start the Logistics Cost Calculation Tool, your personal dashboard opens automatically. You can also get to the dashboard anytime by clicking **“My Calculations”** at the top of the main menu.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Step 2: Understanding the Dashboard Layout
|
||||||
|
|
||||||
|
The dashboard is divided into two main sections:
|
||||||
|
The **top section** displays stats about your calculations. The **bottom section** is a table listing all your calculations and drafts.
|
||||||
|
|
||||||
|
In the stats area, you can quickly see:
|
||||||
|
- how many calculations you’ve already completed,
|
||||||
|
- how many are currently in draft mode,
|
||||||
|
- how many have been put in the queue and are waiting to run,
|
||||||
|
- and how many of those queued calculations have failed.
|
||||||
|
|
||||||
|
As soon as a calculation finishes successfully, it gets added to the **“Completed”** count in the stats section.
|
||||||
|
|
||||||
|
### Step 3: Filtering the Calculations Table
|
||||||
|
|
||||||
|
The lower part of the dashboard shows a table with all your calculations. There’s a search bar above the table so you can filter the list.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You can use this search bar to filter by **supplier** or **part number**. The table updates automatically as you type.
|
||||||
|
|
||||||
|
There’s also a status dropdown to choose which calculations you want to see. For example, you can filter to show only **completed** calculations.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
### Step 4: Navigating the Table
|
||||||
|
|
||||||
|
At the bottom of the table, you’ll find controls for **page navigation** so you can flip through multiple pages of calculations.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
### Step 5: Selecting One or Multiple Calculations
|
||||||
|
|
||||||
|
In the first column of the table, there are **checkboxes** so you can select one or more calculations.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
You can even select calculations across different pages—the selection is **persistent** as you move from page to page.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
Once you’ve selected at least one calculation, a **toolbar** pops up at the bottom of the screen. It shows how many calculations you’ve picked and gives you buttons for available actions, as well as a button to clear your selection.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
### Step 6: Using Actions for Single Calculations
|
||||||
|
|
||||||
|
In each row of the table, there are buttons for actions you can take on that specific calculation:
|
||||||
|
|
||||||
|
- **Pencil icon**: Edit the calculation
|
||||||
|
- **Trash can icon**: Delete the calculation
|
||||||
|
- **Archive icon**: Archive the calculation
|
||||||
|
|
||||||
|
Not all actions are available for every status:
|
||||||
|
- Editing is only possible for calculations in **draft mode**.
|
||||||
|
- Deleting is also only possible for **draft** calculations.
|
||||||
|
- Archiving is only available for **completed** calculations.
|
||||||
|
|
||||||
|
### Step 7: Working with Multiple Calculations at Once
|
||||||
|
|
||||||
|
If you select multiple calculations using the checkboxes,
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
the toolbar at the bottom lets you apply the same actions (like **edit**, **delete**, **archive**) to all selected calculations at once.
|
||||||
|
|
||||||
|
If you, for example, select both completed calculations and drafts, then hit **Edit**,
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
you’ll see an error message. This message tells you that completed calculations can’t be edited directly. You’ll get a few options:
|
||||||
|
|
||||||
|
- Create a **copy** of the completed calculations and edit those copies.
|
||||||
|
- Open **only** the calculations that are still in **draft mode**.
|
||||||
|
- Or use **Cancel** to stop the action.
|
||||||
|
|
||||||
|
### Step 8: Deleting Multiple Calculations
|
||||||
|
|
||||||
|
If you select several calculations and then click the **Delete** button in the toolbar,
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
you’ll get an error message if any of the selected calculations can’t be deleted. The message will tell you that only calculations in **draft mode** can be deleted—so, for example, two out of four selected items.
|
||||||
|
|
||||||
|
To confirm the deletion, just click the **Delete** button again. You can stop the process with the **Cancel** button.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Step 9: Archiving Multiple Calculations
|
||||||
|
|
||||||
|
If you select several calculations and click the **Archive** button in the toolbar,
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
you’ll see a message letting you know that only **completed** calculations can be archived. If you’ve selected drafts too, they won’t be archived.
|
||||||
|
|
||||||
|
Click the **Archive** button in the message to archive the selected completed calculations. Or, hit **Cancel** to cancel the action.
|
||||||
|
|
||||||
|

|
||||||
198
src/main/resources/static/help/mass-edit-basics.md
Normal file
198
src/main/resources/static/help/mass-edit-basics.md
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
|
||||||
|
> This guide explains how to edit multiple calculations at once in the mass edit view, how to switch between mass and single editing, and how to set, update, or copy values from other calculations as needed.
|
||||||
|
|
||||||
|
### Step 1: Open the Mass Edit Mode
|
||||||
|
|
||||||
|
Whenever you select several calculations at the same time and start to edit them, the mass edit mode will automatically open.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
After picking multiple calculations, you can begin editing.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
In mass edit mode, each row in the table represents one calculation. Every calculation always refers to a combination of material and supplier.
|
||||||
|
|
||||||
|
### Step 2: Edit Individual Rows in Mass Edit View
|
||||||
|
|
||||||
|
At the end of each row, there’s a pencil icon you can click to open that specific calculation in single edit mode.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
When you click the pencil, you’ll switch to the detailed view for that calculation, where you can set all parameters just like usual.
|
||||||
|
|
||||||
|
To get back to the mass edit view from single edit mode, use the back button.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
There’s also an X icon in each row. Clicking this removes the row from the mass edit view, but it doesn’t delete the underlying calculation itself.
|
||||||
|
|
||||||
|
### Step 3: Edit Parameters Through Cells in the Mass View
|
||||||
|
|
||||||
|
The columns in the mass edit view match the parameter groups from the single edit screen.
|
||||||
|
For example, if you click the cell for “Packaging” in the first row,
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
the same packaging input window opens as in single edit mode.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You can enter the values as usual in this window.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Here, you’ll enter for example the data for a Euro pallet,
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
including the weight
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
and the number of units per package.
|
||||||
|
|
||||||
|
Important
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
is to
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
pick the right dimensions.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Once you’re done, hit “OK” to confirm your entry,
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
and the values will be saved for that cell.
|
||||||
|
|
||||||
|
Cells with a red exclamation mark show that values are missing. Make sure to fill these in before your calculation is complete.
|
||||||
|
|
||||||
|
### Step 4: Edit Multiple Calculations at the Same Time
|
||||||
|
|
||||||
|
To update several calculations at once, you can select multiple rows in the mass edit view using the checkbox at the beginning of each row.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
As soon as you select more than one row, an editing bar will pop up at the bottom.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This bar shows how many rows you have selected and lets you edit common parameters for all the marked calculations at once.
|
||||||
|
|
||||||
|
For example, clicking the “Price” field in the editing bar
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
opens the price input window.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You might enter a price of eighty-five euros here,
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
along with a seventy percent Oversea Share. Then you check FCA fee.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
and confirm everything with “OK.” The values you entered will be applied to all the selected rows.
|
||||||
|
|
||||||
|
If you want to deselect the currently chosen rows, just click the “Cancel” button in the editing bar.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Step 5: Select Rows by Material or Supplier
|
||||||
|
|
||||||
|
If you want to pick specific rows, like all those with the same material number, hold down the Ctrl key and click on a material cell.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This will select all rows with that material number. While holding Ctrl, your cursor will change to show you’re in multi-select mode.
|
||||||
|
|
||||||
|
The same approach works for suppliers: If you hold Ctrl and click on a supplier,
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
all rows with that supplier get selected.
|
||||||
|
|
||||||
|
If you want to combine different suppliers or materials, start by holding Ctrl
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
and clicking the first material or supplier cell.
|
||||||
|
Then, hold both Ctrl and Shift and click on another material number.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This lets you add more rows to your current selection.
|
||||||
|
|
||||||
|
### Step 6: Set Shared Parameters (e.g. Packaging) for Selected Rows
|
||||||
|
|
||||||
|
For all currently selected items
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
you can edit the packaging together, for example.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
In the window that pops up, enter the new packaging values.
|
||||||
|
|
||||||
|
Important: If you only fill in certain fields (like just the HU length) and hit “OK,” only that specific field gets updated. Any other values that already exist in the other fields will stay the same.
|
||||||
|
|
||||||
|
For example, if you enter 1800 as the HU length in one field
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
and then click “OK,”
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
the HU length in that row will be set to 1800, but any previous values like 800 and 500 millimeters in other rows will remain.
|
||||||
|
|
||||||
|
### Step 7: Copy and Adjust Values Between Calculations
|
||||||
|
|
||||||
|
Another option is to copy values from one calculation into others.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Start by selecting the target calculations.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Next, click on the parameter group (such as Packaging, Price, etc.) that you want to copy values for.
|
||||||
|
The cursor will change to a special copy symbol to show that you can transfer values.
|
||||||
|
|
||||||
|
Now, if you click directly on the source cell (not through the editing bar at the bottom),
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
the values from that cell will be pulled into the input window.
|
||||||
|
You can tweak these values if needed
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
before confirming them with “OK.”
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The updated values will only go into the rows you picked earlier. The original source calculation stays unchanged.
|
||||||
|
|
||||||
|

|
||||||
270
src/main/resources/static/help/mass-edit-destinations.md
Normal file
270
src/main/resources/static/help/mass-edit-destinations.md
Normal file
|
|
@ -0,0 +1,270 @@
|
||||||
|
|
||||||
|
> This guide walks you through fully managing calculations in bulk edit mode—from setting up destinations (using the Destination Manager), distributing annual quantities, and handling routes, to entering handling, repackaging, and other costs. You'll also see how to manage Door-to-Door routes and efficiently edit values for multiple combinations at once.
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### Step 1: Understanding Warnings for Empty Cells
|
||||||
|
|
||||||
|
When you open calculations in bulk edit mode, you might notice empty values in the “Routes” or “Annual Quantity” columns. Clicking these cells will show a warning.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This video explains what the warning means and how to handle it.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The warning pops up because destinations haven't been set up yet for these calculations.
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### Step 2: Open Destination Manager in Bulk Edit Mode
|
||||||
|
|
||||||
|
To add destinations in bulk edit mode, click the “Destination Manager” button at the top.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
A summary will pop up showing all your material and supplier combinations.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
There's a search bar at the top where you can look up specific plants (for example, type “Aschaffenburg” and pick it from the suggestions).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You can then search for another plant (like “Geisa”)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
and add that one too.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### Step 3: Assign Destinations to Combinations with Checkboxes
|
||||||
|
|
||||||
|
Once you've added your destinations, you'll see checkboxes for each material and supplier combination. Use these to assign the right destinations to each calculation.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
For example, you can set it up so a specific combo is delivered to both destinations.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Set checkmarks for all the combinations as needed.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### Step 4: Apply Destinations and Auto-fill Cells
|
||||||
|
|
||||||
|
After assigning your destinations, click the “OK” button at the bottom to process the data.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The previously empty cells in the bulk edit table will now be filled in. You can always reopen the Destination Manager to adjust these settings
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
and confirm your changes again with “OK”.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The main table in bulk edit mode will update accordingly.
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### Step 5: Open Annual Quantity and Routes Menus
|
||||||
|
|
||||||
|
Now, when you click a cell in the “Annual Quantity” or “Routes” columns, the respective editing menu will pop up.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The easiest way to manage annual quantities and routes is to first select all relevant rows, then open the correct menu at the bottom by clicking the “Annual Quantity” or “Routes” button.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This menu will include all materials and part numbers you've selected, so you can edit them together.
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### Step 6: Enter Annual Quantities for Destinations
|
||||||
|
|
||||||
|
In the annual quantity menu, you'll see all materials, suppliers, and destinations listed. Enter the quantity in the cell to show how much of a material, from a certain supplier, should go to a certain destination.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You can type values directly into each cell
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
or select several rows at once
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
and enter the value in the input field at the top.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
For example, enter “600” for Aschaffenburg and “200” for Geisa, then confirm the entry for the selected calculations by clicking the checkmark. If you make a mistake, use the “X” to cancel. Click the checkmark to confirm your values.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Grayed-out cells show that, according to the Destination Manager, this material and supplier combo isn't delivered to that plant.
|
||||||
|
|
||||||
|
Now finish filling out the table.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### Step 7: Edit Handling, Repackaging Costs, and Routes Using Tabs
|
||||||
|
|
||||||
|
At the top, you’ll see tabs for editing handling and repackaging costs, as well as routes, for all selected calculations.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Since all rows in bulk edit are selected, you can set all routes in one go.
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### Step 8: Set Routes via Dropdown and Door-to-Door Routing
|
||||||
|
|
||||||
|
Use the dropdown menus to choose the right route for each supplier and destination combination.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Pick the appropriate route for every combination.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Besides predefined routes, you can also use Door-to-Door routing.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
If you pick Door-to-Door, a small red exclamation mark appears. This means you still need to fill in a Door-to-Door rate and lead time. Click the little pencil icon
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
and enter the Door-to-Door rate and lead time.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Click “OK” to confirm.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The exclamation mark will disappear.
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### Step 9: Enter Handling and Repackaging Costs Individually
|
||||||
|
|
||||||
|
In the “Handling and Repackaging” tab, you can enter individual handling, repackaging, and disposal costs.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
First, acknowledge the warning by checking the box to enable editing.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You'll see a list with all material, supplier, and destination combinations. Now, you can enter handling, repackaging, and disposal costs for each one individually.
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### Step 10: Use Ctrl to Multi-select and Set Values at Once
|
||||||
|
|
||||||
|
Both in the handling/repackaging tab and the annual quantity menu—just like in bulk edit—you can select the same material number multiple times by holding down the Ctrl key and clicking the material number.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Then, as with annual quantities, enter a value in the input bar at the top
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
and use the checkmark to apply it to all the selected rows.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This also works for specific destinations or suppliers.
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### Step 11: Use Ctrl + Shift for Advanced Multi-selection
|
||||||
|
|
||||||
|
To select several suppliers at once—just like in the bulk editor—hold down the Shift key along with Ctrl, then click another supplier.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This will add that supplier to your current selection.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You can then enter values at the top again. Empty cells will be left as they are; only cells where you’ve typed something new will be changed. Existing values (like “300”) stay the same.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This kind of selection is especially handy in the annual quantity menu.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
When you’re done, click the “OK” button to save your changes.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Now, your annual quantities and routes are fully filled out for bulk calculations.
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### Step 12: Start or Close Bulk Calculation
|
||||||
|
|
||||||
|
You can start the bulk calculation with the “Calculate and close” button. This works the same way as in single edit mode. Or, if you're done, close bulk editing with the “Close” button—your changes are saved automatically, so you won't lose any work.
|
||||||
|
|
||||||
|
If you hit “Close” while something is still missing, you'll get an error message (just like in single edit mode) showing which values need fixing.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
For example, if all rows are selected and you copy packaging data from one cell and confirm with “OK,”
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
all relevant values will be filled in and you can kick off the calculation with “Calculate and close.”
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Your calculations will then go into a queue and be processed in the background.
|
||||||
119
src/main/resources/static/help/report.md
Normal file
119
src/main/resources/static/help/report.md
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
|
||||||
|
> This guide shows you how to create a report from completed calculations, select the relevant suppliers, interpret the report’s contents (charts, key metrics, assumptions, and transport cost fluctuations), and export the report as an Excel file.
|
||||||
|
|
||||||
|
### Step 1: Switch to the Reporting View
|
||||||
|
|
||||||
|
First, switch to the Reporting view to start a new report.
|
||||||
|
Just click on the **"Reporting"** tab at the top of the app.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Step 2: Create a New Report
|
||||||
|
|
||||||
|
Once you’re in the Reporting view, click the **"Create Report"** button to make a new report.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Step 3: Search for Material or Part Number
|
||||||
|
|
||||||
|
A new window will pop up where you can search for the material or **part number** you need.
|
||||||
|
Start typing the part number into the search box until you see a suggestion that matches what you entered.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Click on the suggested part number to select it.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Step 4: Choose Suppliers for the Report
|
||||||
|
|
||||||
|
After picking the part number, you'll see a list of all suppliers that already have a calculation for that part.
|
||||||
|
Now, select the suppliers you want to include in your report.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
Confirm your selection by clicking **"OK"**.
|
||||||
|
|
||||||
|
Note: As soon as you pick a supplier, some other suppliers might disappear from the list. This happens because not all calculations are directly comparable. So you can only look at a certain subset of suppliers together in the report.
|
||||||
|
|
||||||
|
Click **"OK"** again to generate and display the report.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Step 5: Get an Overview of the Report Structure
|
||||||
|
|
||||||
|
When your report opens, you might see a warning that some of the reports can’t be fully compared—like if target plants or annual purchase quantities are different. At the bottom, you’ll see all the target plants, so you can spot if the calculations are for different locations (for example, Geisern, Aschaffenburg, or a mix).
|
||||||
|
|
||||||
|
At the top of the report, you’ll see:
|
||||||
|
|
||||||
|
- the time period for the calculations
|
||||||
|
- the part number the report is about
|
||||||
|
- several columns—one for each supplier you picked
|
||||||
|
|
||||||
|
You’ll also see a chart that breaks down the cost items for a quick overview:
|
||||||
|
|
||||||
|
- The **gray area** at the bottom is **MEK A**.
|
||||||
|
- The **green area** at the top is **logistics costs**.
|
||||||
|
- There’s also a thin **gray spread** showing logistics cost fluctuations:
|
||||||
|
- the top of the black bar: the highest possible logistics costs (based on past data)
|
||||||
|
- the bottom of the black bar: the lowest possible logistics costs (also from past data)
|
||||||
|
|
||||||
|
### Step 6: Understanding Key Metrics and Cost Breakdown
|
||||||
|
|
||||||
|
Further down in the report, the chart values are laid out in a table. Here, you’ll find:
|
||||||
|
|
||||||
|
- **MEK A price**, **logistics costs**, and **MEK B price**
|
||||||
|
- each shown as **absolute values**
|
||||||
|
- and as **percentages** relative to the MEK B price
|
||||||
|
|
||||||
|
You’ll also get a **cost breakdown**, weighted across all the target plants included in the calculations. This lets you see how each part of the logistics costs affects the total MEK B price.
|
||||||
|
|
||||||
|
The breakdown appears in both absolute numbers and percentages based on the MEK B price.
|
||||||
|
|
||||||
|
### Step 7: Check Transport Cost Fluctuations in Detail
|
||||||
|
|
||||||
|
The logistics cost fluctuations you see in the chart (the black area) are also available as actual numbers in the **"Transport cost fluctuations"** section.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Here you’ll find:
|
||||||
|
|
||||||
|
- the **total values** for the fluctuations
|
||||||
|
- the **percentage values** compared to the calculated MEK B price
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
### Step 8: View Assumptions and Logistics Route
|
||||||
|
|
||||||
|
At the bottom of the report, you’ll see the assumptions used for the calculation. This includes:
|
||||||
|
|
||||||
|
- the **material** used
|
||||||
|
- the **packaging dimensions** you entered
|
||||||
|
|
||||||
|
Expand the **"Target plant"** section to see more details.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Here, you’ll see the **logistics route** used as the basis for the calculation. If you hover your mouse over the route sections, you’ll get more detailed info, like:
|
||||||
|
|
||||||
|
- the **transport rate** used
|
||||||
|
- required **transport time**
|
||||||
|
- the **calculation model** applied
|
||||||
|
- whether the route section is based on a **container rate** or was calculated using a **kilometer cost matrix**
|
||||||
|
|
||||||
|
Below that, you’ll find extra assumptions specific to the target plant, like:
|
||||||
|
|
||||||
|
- **purchase quantity**
|
||||||
|
- **total transport time**
|
||||||
|
- the result of the **container calculation**
|
||||||
|
|
||||||
|
### Step 9: Export the Report as an Excel File
|
||||||
|
|
||||||
|
At the top of the report, you can download it as an **Excel file**.
|
||||||
|
|
||||||
|
The Excel file will include:
|
||||||
|
|
||||||
|
- all the information shown in the report
|
||||||
|
- plus a **simple summary** of the most important metrics and contents.
|
||||||
210
src/main/resources/static/help/single-edit.md
Normal file
210
src/main/resources/static/help/single-edit.md
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
|
||||||
|
> In this application, you’ll edit logistics cost calculations for a specific supplier and part number combination. The detailed view is divided into several sections where you’ll enter material details, pricing info, packaging data, and destination plants. At the end, you’ll start the calculation, and if there are any errors, you’ll be prompted with hints on how to fix them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 1: Open the detailed view of a calculation
|
||||||
|
|
||||||
|
In the overview, click on the pencil icon for the calculation you want to work on to switch to the detailed view for logistics cost calculation.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
At the top of the detailed view, you’ll see supplier information, and below that, the input forms divided into these sections:
|
||||||
|
- Material / part number
|
||||||
|
- Pricing information
|
||||||
|
- Packaging (handling unit, e.g. large load carrier)
|
||||||
|
- Calculation targets (plants to be supplied)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 2: Check and correct material and customs data as needed
|
||||||
|
|
||||||
|
In the first section, you’ll enter information about the part number, especially:
|
||||||
|
- HS code
|
||||||
|
- Customs tariff rate
|
||||||
|
|
||||||
|
When you create a calculation, the tool automatically looks up tariff rates via an API.
|
||||||
|
- If the lookup is clear, the HS code and customs tariff rate are prefilled and locked; you don’t need to enter anything.
|
||||||
|
- If the lookup isn’t clear (shown by a message at the bottom of the section), you’ll need to research and enter the correct customs tariff rate yourself. By default, 3% is prefilled here.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 3: Enter purchase price and overseas share
|
||||||
|
|
||||||
|
In the second section, you’ll enter pricing information:
|
||||||
|
|
||||||
|
1. Enter the MEK A purchase price.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This MEK A price should always be considered the FCA price.
|
||||||
|
|
||||||
|
2. If the price you have isn’t an FCA price, but an Ex-Works price, check the “include FCA fee” box.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This will automatically add a percentage fee to the MEK A price in the calculation.
|
||||||
|
|
||||||
|
3. If you already have an FCA price, uncheck the box so the extra fee isn’t added.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
4. In the “Overseas share” field, enter the percentage of suppliers who ship from overseas (not just for this calculation, but across all suppliers). Enter the number as a percentage, e.g. 70%.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 4: Configure packaging data and handling unit
|
||||||
|
|
||||||
|
Go to the third input section for packaging:
|
||||||
|
|
||||||
|
1. Enter the dimensions of the handling unit (typically the large load carrier; if you have small quantities, this might be the small load carrier—just keep that in mind for later).
|
||||||
|
For example, enter the measurements for a Euro pallet.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
2. Select the correct unit for these measurements (e.g. millimeters, if that’s what you entered).
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
3. Enter the weight of the handling unit and choose the right weight unit (e.g. kilograms).
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
4. Enter the number of individual units inside a handling unit (e.g. 200 pieces per large or small load carrier).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
5. Set the two important checkboxes on the right:
|
||||||
|
- Checkbox 1: “Mixed transport allowed” (the handling unit can be shipped together with other units or part numbers).
|
||||||
|
If you uncheck this box, the part number will always be shipped separately. This affects container utilization calculations:
|
||||||
|
- Unchecked: The tool uses an algorithm to determine the actual container utilization.
|
||||||
|
- Checked: A standard container utilization rate is used.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- Checkbox 2: “Stacking allowed” (units can be stacked).
|
||||||
|
If you uncheck this, only one layer is counted in the container (e.g. for fragile parts), which reduces utilization.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
6. Check both boxes again if the part number can be shipped mixed and stacked.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 5: Add destination plants and routes using the standard logic
|
||||||
|
|
||||||
|
At the bottom, you’ll now set the destinations for this calculation—meaning, the plants that will receive this part number:
|
||||||
|
|
||||||
|
1. In the search box, type in the plant you want (e.g. “Châtellerault”) and select it from the suggestions.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
2. In the window that opens, enter the annual quantity this plant will receive (e.g. 3,000 parts).
|
||||||
|
|
||||||
|
3. At the bottom, pick the route the containers will take to get to the plant (e.g. a route through a specific intermediate warehouse).
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
4. Switch to the second tab if you want to enter custom handling and repackaging costs. By default, the tool calculates these costs automatically.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
5. If your actual handling and repackaging costs are very different from the automatically calculated ones, check the box to confirm the warning and enter your own costs.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
6. Then, enter your individual costs in the three fields that appear (always based on the previously defined handling unit):
|
||||||
|
- Repackaging costs: The cost to repackage from the disposable pallet to the packaging needed at the plant (e.g. €6).
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
- Handling costs: The cost for goods receipt and storage (e.g. €3.12).
|
||||||
|
- Disposal costs: The cost for disposing of disposable packaging (e.g. €0 if there’s no charge).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
7. Confirm everything for this plant by clicking the “OK” button.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The plant now appears in the destinations list.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 6: Add another plant with a custom door-to-door rate
|
||||||
|
|
||||||
|
1. Add another plant, e.g. “Luzzara”.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
2. If no route can be determined from the supplier for this plant, select the option to set a custom rate.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. Enter a door-to-door rate that covers all costs from pickup at the supplier to delivery at the plant (e.g. €2,000).
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
4. Enter the lead time (e.g. 47 days) and the plant’s annual quantity (e.g. 1,000 pieces).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
5. Confirm this plant as well by clicking the “OK” button.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Now both plants receiving this part are listed in the destinations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 7: Run the calculation or cancel input
|
||||||
|
|
||||||
|
Once all data is entered, you can:
|
||||||
|
- Use the “Calculate and Close” button to start the calculation and close the form.
|
||||||
|
- Use the “Close” button to exit the form without starting the calculation.
|
||||||
|
|
||||||
|
All your entries are saved continuously, so nothing is lost when closing the form. You can reopen and continue the calculation anytime.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 8: Identify and fix input errors
|
||||||
|
|
||||||
|
To see how error handling works, try making a deliberate mistake, for example:
|
||||||
|
|
||||||
|
1. Accidentally select “centimeters” instead of “millimeters” for the dimensions. This will make the packaging seem way too big.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
2. Then start the calculation.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You’ll see an error message at the bottom of the screen showing which values need to be fixed.
|
||||||
|
|
||||||
|
3. Correct the wrong unit or the value in question.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
4. Start the calculation again.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The calculation will now be queued and processed. You can see its status in your dashboard.
|
||||||
|
|
@ -7,26 +7,6 @@ VALUES ('USR001', 'john.doe@company.com', 'John', 'Doe', TRUE),
|
||||||
('USR005', 'david.chen@company.com', 'David', 'Chen', TRUE)
|
('USR005', 'david.chen@company.com', 'David', 'Chen', TRUE)
|
||||||
ON DUPLICATE KEY UPDATE email = VALUES(email);
|
ON DUPLICATE KEY UPDATE email = VALUES(email);
|
||||||
|
|
||||||
INSERT INTO sys_group(group_name, group_description)
|
|
||||||
VALUES ('none', 'no rights');
|
|
||||||
INSERT INTO sys_group(group_name, group_description)
|
|
||||||
VALUES ('basic', 'Login, generate reports');
|
|
||||||
INSERT INTO sys_group(group_name, group_description)
|
|
||||||
VALUES ('calculation', 'Login, generate reports, do calculations');
|
|
||||||
INSERT INTO sys_group(group_name, group_description)
|
|
||||||
VALUES ('freight', 'Login, generate reports, edit freight rates');
|
|
||||||
INSERT INTO sys_group(group_name, group_description)
|
|
||||||
VALUES ('packaging', 'Login, generate reports, edit packaging data');
|
|
||||||
INSERT INTO sys_group(group_name, group_description)
|
|
||||||
VALUES ('material', 'Login, generate reports, edit material data');
|
|
||||||
INSERT INTO sys_group(group_name, group_description)
|
|
||||||
VALUES ('super',
|
|
||||||
'Login, generate reports, do calculations, edit freight rates, edit packaging data');
|
|
||||||
INSERT INTO sys_group(group_name, group_description)
|
|
||||||
VALUES ('service', 'Register API Tokens');
|
|
||||||
INSERT INTO sys_group(group_name, group_description)
|
|
||||||
VALUES ('right-management',
|
|
||||||
'Add/Remove users, groups, etc.');
|
|
||||||
|
|
||||||
INSERT INTO sys_user_group_mapping (group_id, user_id)
|
INSERT INTO sys_user_group_mapping (group_id, user_id)
|
||||||
VALUES ((SELECT id FROM sys_group WHERE group_name = 'super'),
|
VALUES ((SELECT id FROM sys_group WHERE group_name = 'super'),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue