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:
Jan 2026-01-10 19:19:38 +01:00
parent 22051135ad
commit 8742d24b62
28 changed files with 2010 additions and 51 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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();

View file

@ -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);
} }
} }
} }

View 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>

View 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>

View 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>

View 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>

View 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>

View file

@ -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);

View file

@ -1,6 +1,15 @@
<template> <template>
<div class="start-calculation-container"> <div class="start-calculation-container">
<div class="start-calculation-header">
<div>
<h2 class="page-header">Create Calculation</h2> <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">
<div class="part-number-modal-action-help">
<icon-button v-if="useHelpStore().enableHelp" icon="info"
@click="useHelpStore().activateHelp('assistant')"></icon-button>
</div>
<div class="part-number-modal-action-buttons">
<basic-button @click="parsePartNumbers" icon="CloudArrowUp">Analyze input</basic-button> <basic-button @click="parsePartNumbers" icon="CloudArrowUp">Analyze input</basic-button>
<basic-button @click="closeModal('partNumber')" :show-icon="false" variant="secondary">Cancel</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);
}, },
@ -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;

View file

@ -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">
<div class="header-caption-container">
<div>
<h2 class="page-header">Mass edit calculation</h2> <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>

View file

@ -1,7 +1,22 @@
<template> <template>
<div class="edit-calculation-container"> <div class="edit-calculation-container">
<div class="header-container"> <div class="header-container">
<div class="header-container">
<div>
<h2 class="page-header">Edit calculation</h2> <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;

View file

@ -1,12 +1,20 @@
<template> <template>
<div> <div>
<div class="header-container">
<div>
<h2 class="page-header"> My calculations</h2> <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>
@ -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;

View file

@ -1,12 +1,26 @@
<template> <template>
<div> <div>
<div class="header-container"> <div class="header-container">
<div class="header-caption-container">
<div>
<h2 class="page-header page-header-align">Reporting <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,
@ -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 {

View 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;
}
}
});

View file

@ -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
}
}
}
}) })

View file

@ -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();
} }
} }

View file

@ -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;
}
}

View 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;
}
}

View 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
);
}
}

View 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.
![Screenshot 1](images/image1.jpg)
### 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.
![Screenshot 2](images/image2.jpg)
### 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.
![Screenshot 3](images/image3.jpg)
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.
![Screenshot 4](images/image4.jpg)
![Screenshot 5](images/image5.jpg)
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.
![Screenshot 6](images/image6.jpg)
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.
![Screenshot 7](images/image7.jpg)
You can remove a single part number from the list the same way.
![Screenshot 8](images/image8.jpg)
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.
![Screenshot 9](images/image9.jpg)
Enter the name of the supplier you want, pick them from the results list, and click **“Bietvorschlag”** to add them.
![Screenshot 10](images/image10.jpg)
![Screenshot 11](images/image11.jpg)
The supplier will then be assigned to the selected part numbers.
### Step 8: Create a New Supplier
If you dont find the supplier youre looking for, you can add a new one by clicking the **“Create New Supplier”** button.
![Screenshot 12](images/image12.jpg)
![Screenshot 13](images/image13.jpg)
In the dialog that opens, type in the new suppliers name and then enter their address.
![Screenshot 14](images/image14.jpg)
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.
![Screenshot 15](images/image15.jpg)
A map will be shown so you can confirm the address was correctly recognized.
![Screenshot 16](images/image16.jpg)
Once all details are correct, click the **“Create”** button to add the new supplier and insert them directly into your current form.
![Screenshot 17](images/image17.jpg)
### Step 9: Review the Number of Calculations to be Created
After youve 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, theres 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.
![Screenshot 18](images/image18.jpg)
### Step 11: Generate Calculations
Finally, click the **“Create”** button to set up all calculations for your selected combinations of part numbers and suppliers.
![Screenshot 19](images/image19.jpg)
Your new logistics calculations will now be available in the system for further editing.

View 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.
![Screenshot 1](images/image1.jpg)
### 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 youve 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. Theres a search bar above the table so you can filter the list.
![Screenshot 2](images/image2.jpg)
You can use this search bar to filter by **supplier** or **part number**. The table updates automatically as you type.
Theres also a status dropdown to choose which calculations you want to see. For example, you can filter to show only **completed** calculations.
![Screenshot 3](images/image3.jpg)
![Screenshot 5](images/image4.jpg)
### Step 4: Navigating the Table
At the bottom of the table, youll find controls for **page navigation** so you can flip through multiple pages of calculations.
![Screenshot 6](images/image5.jpg)
![Screenshot 7](images/image6.jpg)
### 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.
![Screenshot 8](images/image7.jpg)
![Screenshot 9](images/image8.jpg)
You can even select calculations across different pages—the selection is **persistent** as you move from page to page.
![Screenshot 10](images/image9.jpg)
![Screenshot 12](images/image10.jpg)
Once youve selected at least one calculation, a **toolbar** pops up at the bottom of the screen. It shows how many calculations youve picked and gives you buttons for available actions, as well as a button to clear your selection.
![Screenshot 13](images/image11.jpg)
![Screenshot 14](images/image12.jpg)
### 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,
![Screenshot 15](images/image13.jpg)
![Screenshot 19](images/image14.jpg)
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**,
![Screenshot 20](images/image15.jpg)
![Screenshot 21](images/image16.jpg)
youll see an error message. This message tells you that completed calculations cant be edited directly. Youll 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,
![Screenshot 22](images/image17.jpg)
![Screenshot 23](images/image18.jpg)
youll get an error message if any of the selected calculations cant 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.
![Screenshot 24](images/image19.jpg)
### Step 9: Archiving Multiple Calculations
If you select several calculations and click the **Archive** button in the toolbar,
![Screenshot 25](images/image20.jpg)
youll see a message letting you know that only **completed** calculations can be archived. If youve selected drafts too, they wont be archived.
Click the **Archive** button in the message to archive the selected completed calculations. Or, hit **Cancel** to cancel the action.
![Screenshot 26](images/image21.jpg)

View 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.
![Screenshot 1](images/image1.jpg)
After picking multiple calculations, you can begin editing.
![Screenshot 2](images/image2.jpg)
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, theres a pencil icon you can click to open that specific calculation in single edit mode.
![Screenshot 3](images/image3.jpg)
When you click the pencil, youll 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.
![Screenshot 4](images/image4.jpg)
Theres also an X icon in each row. Clicking this removes the row from the mass edit view, but it doesnt 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,
![Screenshot 5](images/image5.jpg)
the same packaging input window opens as in single edit mode.
![Screenshot 6](images/image6.jpg)
You can enter the values as usual in this window.
![Screenshot 7](images/image7.jpg)
![Screenshot 8](images/image8.jpg)
Here, youll enter for example the data for a Euro pallet,
![Screenshot 9](images/image9.jpg)
![Screenshot 10](images/image10.jpg)
including the weight
![Screenshot 11](images/image11.jpg)
and the number of units per package.
Important
![Screenshot 12](images/image12.jpg)
is to
![Screenshot 13](images/image13.jpg)
pick the right dimensions.
![Screenshot 14](images/image14.jpg)
![Screenshot 15](images/image15.jpg)
Once youre done, hit “OK” to confirm your entry,
![Screenshot 16](images/image16.jpg)
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.
![Screenshot 17](images/image17.jpg)
![Screenshot 18](images/image18.jpg)
As soon as you select more than one row, an editing bar will pop up at the bottom.
![Screenshot 19](images/image19.jpg)
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
![Screenshot 20](images/image20.jpg)
opens the price input window.
![Screenshot 21](images/image21.jpg)
You might enter a price of eighty-five euros here,
![Screenshot 22](images/image22.jpg)
along with a seventy percent Oversea Share. Then you check FCA fee.
![Screenshot 23](images/image23.jpg)
![Screenshot 24](images/image24.jpg)
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.
![Screenshot 25](images/image25.jpg)
### 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.
![Screenshot 26](images/image26.jpg)
This will select all rows with that material number. While holding Ctrl, your cursor will change to show youre in multi-select mode.
The same approach works for suppliers: If you hold Ctrl and click on a supplier,
![Screenshot 27](images/image27.jpg)
all rows with that supplier get selected.
If you want to combine different suppliers or materials, start by holding Ctrl
![Screenshot 28](images/image28.jpg)
and clicking the first material or supplier cell.
Then, hold both Ctrl and Shift and click on another material number.
![Screenshot 29](images/image29.jpg)
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
![Screenshot 30](images/image30.jpg)
you can edit the packaging together, for example.
![Screenshot 31](images/image31.jpg)
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
![Screenshot 32](images/image32.jpg)
and then click “OK,”
![Screenshot 33](images/image33.jpg)
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.
![Screenshot 34](images/image34.jpg)
Start by selecting the target calculations.
![Screenshot 35](images/image35.jpg)
![Screenshot 36](images/image36.jpg)
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),
![Screenshot 37](images/image37.jpg)
the values from that cell will be pulled into the input window.
You can tweak these values if needed
![Screenshot 38](images/image38.jpg)
before confirming them with “OK.”
![Screenshot 39](images/image39.jpg)
The updated values will only go into the rows you picked earlier. The original source calculation stays unchanged.
![Screenshot 40](images/image40.jpg)

View 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.
![Screenshot 1](images/image1.jpg)
This video explains what the warning means and how to handle it.
![Screenshot 2](images/image2.jpg)
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.
![Screenshot 3](images/image3.jpg)
A summary will pop up showing all your material and supplier combinations.
![Screenshot 4](images/image4.jpg)
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).
![Screenshot 5](images/image5.jpg)
You can then search for another plant (like “Geisa”)
![Screenshot 6](images/image6.jpg)
and add that one too.
![Screenshot 7](images/image7.jpg)
***
### 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.
![Screenshot 8](images/image8.jpg)
For example, you can set it up so a specific combo is delivered to both destinations.
![Screenshot 9](images/image9.jpg)
Set checkmarks for all the combinations as needed.
![Screenshot 10](images/image10.jpg)
![Screenshot 17](images/image11.jpg)
***
### Step 4: Apply Destinations and Auto-fill Cells
After assigning your destinations, click the “OK” button at the bottom to process the data.
![Screenshot 18](images/image12.jpg)
![Screenshot 20](images/image13.jpg)
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
![Screenshot 21](images/image14.jpg)
and confirm your changes again with “OK”.
![Screenshot 22](images/image15.jpg)
![Screenshot 26](images/image16.jpg)
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.
![Screenshot 27](images/image17.jpg)
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.
![Screenshot 28](images/image18.jpg)
![Screenshot 30](images/image19.jpg)
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.
![Screenshot 31](images/image20.jpg)
![Screenshot 32](images/image21.jpg)
You can type values directly into each cell
![Screenshot 33](images/image22.jpg)
or select several rows at once
![Screenshot 34](images/image23.jpg)
![Screenshot 37](images/image24.jpg)
and enter the value in the input field at the top.
![Screenshot 38](images/image25.jpg)
![Screenshot 39](images/image26.jpg)
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.
![Screenshot 40](images/image27.jpg)
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.
![Screenshot 41](images/image28.jpg)
![Screenshot 49](images/image29.jpg)
***
### Step 7: Edit Handling, Repackaging Costs, and Routes Using Tabs
At the top, youll see tabs for editing handling and repackaging costs, as well as routes, for all selected calculations.
![Screenshot 50](images/image30.jpg)
![Screenshot 53](images/image31.jpg)
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.
![Screenshot 54](images/image32.jpg)
![Screenshot 55](images/image33.jpg)
Pick the appropriate route for every combination.
![Screenshot 56](images/image34.jpg)
![Screenshot 60](images/image35.jpg)
Besides predefined routes, you can also use Door-to-Door routing.
![Screenshot 61](images/image36.jpg)
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
![Screenshot 62](images/image37.jpg)
and enter the Door-to-Door rate and lead time.
![Screenshot 63](images/image38.jpg)
![Screenshot 64](images/image39.jpg)
Click “OK” to confirm.
![Screenshot 65](images/image40.jpg)
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.
![Screenshot 66](images/image41.jpg)
First, acknowledge the warning by checking the box to enable editing.
![Screenshot 67](images/image42.jpg)
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.
![Screenshot 68](images/image43.jpg)
Then, as with annual quantities, enter a value in the input bar at the top
![Screenshot 69](images/image44.jpg)
![Screenshot 70](images/image45.jpg)
and use the checkmark to apply it to all the selected rows.
![Screenshot 71](images/image46.jpg)
![Screenshot 73](images/image47.jpg)
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.
![Screenshot 76](images/image48.jpg)
This will add that supplier to your current selection.
![Screenshot 77](images/image49.jpg)
You can then enter values at the top again. Empty cells will be left as they are; only cells where youve typed something new will be changed. Existing values (like “300”) stay the same.
![Screenshot 78](images/image50.jpg)
![Screenshot 79](images/image51.jpg)
This kind of selection is especially handy in the annual quantity menu.
![Screenshot 80](images/image52.jpg)
![Screenshot 81](images/image53.jpg)
When youre done, click the “OK” button to save your changes.
![Screenshot 82](images/image54.jpg)
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.
![Screenshot 83](images/image55.jpg)
For example, if all rows are selected and you copy packaging data from one cell and confirm with “OK,”
![Screenshot 84](images/image56.jpg)
![Screenshot 85](images/image57.jpg)
all relevant values will be filled in and you can kick off the calculation with “Calculate and close.”
![Screenshot 86](images/image58.jpg)
Your calculations will then go into a queue and be processed in the background.

View file

@ -0,0 +1,119 @@
> This guide shows you how to create a report from completed calculations, select the relevant suppliers, interpret the reports 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.
![Screenshot 1](images/image1.jpg)
### Step 2: Create a New Report
Once youre in the Reporting view, click the **"Create Report"** button to make a new report.
![Screenshot 2](images/image2.jpg)
### 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.
![Screenshot 3](images/image3.jpg)
Click on the suggested part number to select it.
![Screenshot 4](images/image4.jpg)
### 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.
![Screenshot 5](images/image5.jpg)
![Screenshot 7](images/image6.jpg)
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.
![Screenshot 8](images/image7.jpg)
### Step 5: Get an Overview of the Report Structure
When your report opens, you might see a warning that some of the reports cant be fully compared—like if target plants or annual purchase quantities are different. At the bottom, youll 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, youll see:
- the time period for the calculations
- the part number the report is about
- several columns—one for each supplier you picked
Youll 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**.
- Theres 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, youll find:
- **MEK A price**, **logistics costs**, and **MEK B price**
- each shown as **absolute values**
- and as **percentages** relative to the MEK B price
Youll 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.
![Screenshot 9](images/image8.jpg)
Here youll find:
- the **total values** for the fluctuations
- the **percentage values** compared to the calculated MEK B price
![Screenshot 10](images/image9.jpg)
![Screenshot 11](images/image10.jpg)
### Step 8: View Assumptions and Logistics Route
At the bottom of the report, youll 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.
![Screenshot 12](images/image11.jpg)
Here, youll see the **logistics route** used as the basis for the calculation. If you hover your mouse over the route sections, youll 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, youll 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.

View file

@ -0,0 +1,210 @@
> In this application, youll edit logistics cost calculations for a specific supplier and part number combination. The detailed view is divided into several sections where youll enter material details, pricing info, packaging data, and destination plants. At the end, youll start the calculation, and if there are any errors, youll 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.
![Screenshot 1](images/image1.jpg)
At the top of the detailed view, youll 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, youll 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 dont need to enter anything.
- If the lookup isnt clear (shown by a message at the bottom of the section), youll 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, youll enter pricing information:
1. Enter the MEK A purchase price.
![Screenshot 2](images/image2.jpg)
This MEK A price should always be considered the FCA price.
2. If the price you have isnt an FCA price, but an Ex-Works price, check the “include FCA fee” box.
![Screenshot 3](images/image3.jpg)
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 isnt added.
![Screenshot 4](images/image4.jpg)
![Screenshot 5](images/image5.jpg)
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%.
![Screenshot 6](images/image6.jpg)
---
### 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.
![Screenshot 7](images/image7.jpg)
![Screenshot 8](images/image8.jpg)
2. Select the correct unit for these measurements (e.g. millimeters, if thats what you entered).
![Screenshot 9](images/image9.jpg)
![Screenshot 12](images/image10.jpg)
3. Enter the weight of the handling unit and choose the right weight unit (e.g. kilograms).
![Screenshot 13](images/image11.jpg)
![Screenshot 14](images/image12.jpg)
4. Enter the number of individual units inside a handling unit (e.g. 200 pieces per large or small load carrier).
![Screenshot 15](images/image13.jpg)
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.
![Screenshot 16](images/image14.jpg)
- 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.
![Screenshot 17](images/image15.jpg)
6. Check both boxes again if the part number can be shipped mixed and stacked.
![Screenshot 18](images/image16.jpg)
![Screenshot 19](images/image17.jpg)
---
### Step 5: Add destination plants and routes using the standard logic
At the bottom, youll 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.
![Screenshot 20](images/image18.jpg)
![Screenshot 21](images/image19.jpg)
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).
![Screenshot 22](images/image20.jpg)
![Screenshot 23](images/image21.jpg)
4. Switch to the second tab if you want to enter custom handling and repackaging costs. By default, the tool calculates these costs automatically.
![Screenshot 24](images/image22.jpg)
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.
![Screenshot 25](images/image23.jpg)
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).
![Screenshot 26](images/image24.jpg)
![Screenshot 27](images/image25.jpg)
- 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 theres no charge).
![Screenshot 28](images/image26.jpg)
7. Confirm everything for this plant by clicking the “OK” button.
![Screenshot 29](images/image27.jpg)
The plant now appears in the destinations list.
![Screenshot 30](images/image28.jpg)
---
### Step 6: Add another plant with a custom door-to-door rate
1. Add another plant, e.g. “Luzzara”.
![Screenshot 31](images/image29.jpg)
![Screenshot 32](images/image30.jpg)
2. If no route can be determined from the supplier for this plant, select the option to set a custom rate.
![Screenshot 33](images/image31.jpg)
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).
![Screenshot 34](images/image32.jpg)
![Screenshot 35](images/image33.jpg)
4. Enter the lead time (e.g. 47 days) and the plants annual quantity (e.g. 1,000 pieces).
![Screenshot 36](images/image34.jpg)
5. Confirm this plant as well by clicking the “OK” button.
![Screenshot 37](images/image35.jpg)
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.
![Screenshot 38](images/image36.jpg)
![Screenshot 39](images/image37.jpg)
2. Then start the calculation.
![Screenshot 40](images/image38.jpg)
Youll 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.
![Screenshot 41](images/image39.jpg)
![Screenshot 42](images/image40.jpg)
4. Start the calculation again.
![Screenshot 43](images/image41.jpg)
The calculation will now be queued and processed. You can see its status in your dashboard.

View file

@ -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'),