Compare commits
No commits in common. "main" and "v1.0.3" have entirely different histories.
|
|
@ -107,19 +107,13 @@ jobs:
|
|||
IMAGE_BASE="${{ steps.tags.outputs.image_base }}"
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
|
||||
echo "DEBUG: ref_name = ${{ gitea.ref_name }}"
|
||||
echo "DEBUG: event_name = ${{ gitea.event_name }}"
|
||||
echo "DEBUG: base_ref = ${{ gitea.base_ref }}"
|
||||
|
||||
docker push ${IMAGE_BASE}:${VERSION}
|
||||
docker push ${IMAGE_BASE}:${{ gitea.sha }}
|
||||
|
||||
if [ "${{ gitea.ref_name }}" = "main" ]; then
|
||||
echo "Pushing latest and main tags..."
|
||||
docker push ${IMAGE_BASE}:latest
|
||||
docker push ${IMAGE_BASE}:main
|
||||
elif [ "${{ gitea.ref_name }}" = "dev" ]; then
|
||||
echo "Pushing dev tag..."
|
||||
docker push ${IMAGE_BASE}:dev
|
||||
fi
|
||||
|
||||
|
|
|
|||
2
.mvn/wrapper/maven-wrapper.properties
vendored
|
|
@ -16,4 +16,4 @@
|
|||
# under the License.
|
||||
wrapperVersion=3.3.4
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
services:
|
||||
mysql:
|
||||
image: mysql:8.4
|
||||
image: mysql:8.0
|
||||
container_name: lcc-mysql-local
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
|
||||
|
|
|
|||
27
pom.xml
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.5.9</version>
|
||||
<version>3.4.3</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>de.avatic</groupId>
|
||||
|
|
@ -28,9 +28,9 @@
|
|||
</scm>
|
||||
<properties>
|
||||
<java.version>23</java.version>
|
||||
<spring-cloud-azure.version>5.24.1</spring-cloud-azure.version>
|
||||
<mockito.version>5.20.0</mockito.version>
|
||||
<flyway.version>11.18.0</flyway.version>
|
||||
<spring-cloud-azure.version>5.23.0</spring-cloud-azure.version>
|
||||
<mockito.version>5.18.0</mockito.version>
|
||||
<flyway.version>11.1.0</flyway.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
|
|
@ -108,32 +108,27 @@
|
|||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi</artifactId>
|
||||
<version>5.5.1</version>
|
||||
<version>5.5.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.dhatim</groupId>
|
||||
<artifactId>fastexcel</artifactId>
|
||||
<version>0.19.0</version>
|
||||
<version>0.17.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.dhatim</groupId>
|
||||
<artifactId>fastexcel-reader</artifactId>
|
||||
<version>0.19.0</version>
|
||||
<version>0.17.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
<version>5.5.1</version>
|
||||
<version>5.5.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.dhatim</groupId>
|
||||
<artifactId>fastexcel</artifactId>
|
||||
<version>0.19.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.commonmark</groupId>
|
||||
<artifactId>commonmark</artifactId>
|
||||
<version>0.22.0</version>
|
||||
<version>0.18.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
|
@ -213,12 +208,12 @@
|
|||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>versions-maven-plugin</artifactId>
|
||||
<version>2.20.1</version>
|
||||
<version>2.18.0</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jvnet.jaxb</groupId>
|
||||
<artifactId>jaxb-maven-plugin</artifactId>
|
||||
<version>4.0.12</version>
|
||||
<version>4.0.11</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ Jump to [Database Documentation](#database-documentation)
|
|||
# LCC Backend API Documentation
|
||||
|
||||
**API Version:** v1.0
|
||||
**Last Updated:** Dec 01, 2025
|
||||
**Last Updated:** March 16, 2025
|
||||
|
||||
## Table of Contents
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<meta name="apple-mobile-web-app-title" content="MyWebSite" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<title>Logistics Cost Calculation Tool</title>
|
||||
<title>LCC</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
"dependencies": {
|
||||
"@phosphor-icons/vue": "^2.2.1",
|
||||
"@vueuse/core": "^13.6.0",
|
||||
"azure-maps-control": "^3.6.1",
|
||||
"chart.js": "^4.5.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"loglevel": "^1.9.2",
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 4.2 KiB |
|
|
@ -1,16 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><g clip-path="url(#SvgjsClipPath1184)"><rect width="1000" height="1000" fill="#ffffff"></rect><g transform="matrix(2.028221249963782,0,0,2.028221249963782,198.37307681163614,150)"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="297.43" height="345.13"><svg id="Ebene_1_Kopie" data-name="Ebene 1 Kopie" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 297.43 345.13">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #5af0b4;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #002f54;
|
||||
}
|
||||
</style>
|
||||
<clipPath id="SvgjsClipPath1184"><rect width="1000" height="1000" x="0" y="0" rx="200" ry="200"></rect></clipPath></defs>
|
||||
<polygon class="cls-1" points="201.84 201.83 201.84 257.02 201.85 257.02 249.64 229.43 249.64 229.42 297.43 201.83 297.43 257.02 249.65 284.61 249.64 284.61 201.84 312.21 154.05 339.8 154.05 174.24 201.84 146.64 249.64 119.05 297.43 91.46 297.43 146.64 249.64 174.23 249.64 174.24 201.84 201.83"></polygon>
|
||||
<polygon class="cls-2" points="289.44 82.78 289.44 82.79 241.65 110.38 193.85 137.97 146.06 165.57 98.27 137.97 50.47 110.38 2.68 82.79 2.68 82.78 50.47 55.19 98.26 27.6 98.27 27.6 98.27 27.59 146.06 0 193.85 27.59 193.85 27.6 146.06 55.19 98.27 82.78 98.27 82.79 146.06 110.38 193.85 82.79 193.86 82.79 241.65 55.19 241.66 55.19 289.44 82.78"></polygon>
|
||||
<polygon class="cls-2" points="143.38 289.94 143.38 345.13 95.59 317.54 47.79 289.94 0 262.35 0 96.79 47.79 124.38 47.79 234.76 95.58 262.35 95.59 262.35 143.38 289.94"></polygon>
|
||||
</svg></svg></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"name": "MyWebSite",
|
||||
"short_name": "MySite",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
|
@ -1,6 +1,5 @@
|
|||
<template>
|
||||
<the-notification-system />
|
||||
<the-help-system />
|
||||
<the-header></the-header>
|
||||
<router-view v-slot="slotProps">
|
||||
<transition name="route" mode="out-in">
|
||||
|
|
@ -14,10 +13,9 @@
|
|||
|
||||
import TheHeader from "@/components/layout/TheHeader.vue";
|
||||
import TheNotificationSystem from "@/components/UI/TheNotificationSystem.vue";
|
||||
import TheHelpSystem from "@/components/layout/help/TheHelpSystem.vue";
|
||||
|
||||
export default {
|
||||
components: {TheHelpSystem, TheNotificationSystem, TheHeader},
|
||||
components: {TheNotificationSystem, TheHeader},
|
||||
|
||||
}
|
||||
</script>
|
||||
|
|
@ -31,14 +29,6 @@ export default {
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
html.modal-open {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
font-weight: normal;
|
||||
margin-bottom: 3rem;
|
||||
|
|
@ -46,17 +36,15 @@ html.modal-open {
|
|||
color: #002F54;
|
||||
}
|
||||
|
||||
|
||||
.sub-header {
|
||||
font-weight: normal;
|
||||
font-size: 1.4rem;
|
||||
color: #6B869C;
|
||||
margin: 1.6rem 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 62.5%;
|
||||
font-family: 'Arial', sans-serif;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
|
|
|
|||
|
|
@ -1,20 +1,10 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="app-list-item">
|
||||
<div class="app-name-container">
|
||||
<div class="app-name-name">{{ app.name }}</div>
|
||||
<div class="app-name-id">{{ app.client_id }}</div>
|
||||
</div>
|
||||
|
||||
<div class="badge-list">
|
||||
<basic-badge variant="secondary" icon="lock" v-for="group in groups" :key="group">{{ group }}</basic-badge>
|
||||
</div>
|
||||
|
||||
<div class="action-container">
|
||||
<icon-button icon="download" @click="exportClick"></icon-button>
|
||||
<icon-button icon="trash" @click="deleteClick"></icon-button>
|
||||
</div>
|
||||
<div class="app-name-container"><div class="app-name-name">{{ app.name }}</div><div class="app-name-id">{{ app.client_id}}</div></div>
|
||||
|
||||
<div class="badge-list"> <basic-badge variant="secondary" icon="lock" v-for="group in groups" :key="group">{{group}}</basic-badge></div>
|
||||
<div class="action-container"> <icon-button icon="trash" @click="deleteClick"></icon-button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -28,7 +18,7 @@ import BasicBadge from "@/components/UI/BasicBadge.vue";
|
|||
export default {
|
||||
name: "AppListItem",
|
||||
components: {BasicBadge, IconButton, Box},
|
||||
emits: ["deleteApp", "exportApp"],
|
||||
emits: ["deleteApp"],
|
||||
props: {
|
||||
app: {
|
||||
type: Object,
|
||||
|
|
@ -43,9 +33,6 @@ export default {
|
|||
methods: {
|
||||
deleteClick() {
|
||||
this.$emit("deleteApp", this.app.id);
|
||||
},
|
||||
exportClick() {
|
||||
this.$emit("exportApp", this.app.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -89,10 +76,7 @@ export default {
|
|||
color: #6b7280;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1.2rem;
|
||||
.action-container{
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,6 @@ import Spinner from "@/components/UI/Spinner.vue";
|
|||
import Flag from "@/components/UI/Flag.vue";
|
||||
import {useDebounceFn} from "@vueuse/core";
|
||||
import BasicBadge from "@/components/UI/BasicBadge.vue";
|
||||
import logger from "@/logger.js";
|
||||
|
||||
export default {
|
||||
name: 'AutosuggestSearchbar',
|
||||
|
|
@ -183,7 +182,7 @@ export default {
|
|||
this.highlightedIndex = -1
|
||||
this.$emit('suggestions-loaded', this.suggestions)
|
||||
} catch (error) {
|
||||
logger.error('Error fetching suggestions:', error)
|
||||
console.error('Error fetching suggestions:', error)
|
||||
this.suggestions = []
|
||||
this.hideSuggestions()
|
||||
this.$emit('error', error)
|
||||
|
|
|
|||
|
|
@ -23,11 +23,6 @@ export default{
|
|||
type: String,
|
||||
default: 'primary',
|
||||
validator: (value) => ['primary', 'secondary', 'grey', 'exception', 'skeleton'].includes(value)
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator: (value) => ['default', 'compact'].includes(value)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -36,8 +31,7 @@ export default{
|
|||
},
|
||||
batchClasses() {
|
||||
return [
|
||||
`batch--${this.variant}`,
|
||||
`batch--${this.size}`
|
||||
`batch--${this.variant}`
|
||||
]
|
||||
},
|
||||
iconComponent() {
|
||||
|
|
@ -71,12 +65,6 @@ export default{
|
|||
height: 2.4rem;
|
||||
}
|
||||
|
||||
.batch-container.batch--compact {
|
||||
padding: 0.2rem 0.4rem;
|
||||
gap: 0.4rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.batch--primary {
|
||||
background-color: #5AF0B4;
|
||||
color: #002F54;
|
||||
|
|
|
|||
|
|
@ -1,24 +1,23 @@
|
|||
<template>
|
||||
<div class="checkbox-container">
|
||||
<label class="checkbox-item" :class="{ disabled: disabled }">
|
||||
<label class="checkbox-item" :class="{ disabled: disabled }" @change="setFilter">
|
||||
<input
|
||||
@keydown.enter="$emit('enter', $event)"
|
||||
type="checkbox"
|
||||
:checked="internalChecked"
|
||||
:checked="isChecked"
|
||||
:disabled="disabled"
|
||||
:indeterminate.prop="internalIndeterminate"
|
||||
@change="handleChange"
|
||||
:indeterminate.prop="isIndeterminate"
|
||||
v-model="isChecked"
|
||||
ref="checkboxInput"
|
||||
>
|
||||
<span class="checkmark" :class="{ indeterminate: internalIndeterminate }"></span>
|
||||
<span class="checkmark" :class="{ indeterminate: isIndeterminate }"></span>
|
||||
<span class="checkbox-label"><slot></slot></span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
emits: ["checkbox-changed", "enter"],
|
||||
export default{
|
||||
emits:["checkbox-changed"],
|
||||
props: {
|
||||
checked: {
|
||||
type: Boolean,
|
||||
|
|
@ -40,49 +39,42 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
internalChecked: this.checked,
|
||||
internalIndeterminate: this.indeterminate && !this.checked,
|
||||
internalIndeterminate: this.indeterminate,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isChecked: {
|
||||
get() {
|
||||
return this.internalChecked;
|
||||
},
|
||||
set(value) {
|
||||
if (this.disabled) return; // Prevent changes when disabled
|
||||
this.internalChecked = value;
|
||||
this.internalIndeterminate = false;
|
||||
this.$emit('checkbox-changed', value);
|
||||
}
|
||||
},
|
||||
isIndeterminate() {
|
||||
return this.internalIndeterminate && !this.internalChecked;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
checked(newVal) {
|
||||
this.internalChecked = newVal;
|
||||
// Wenn checked true ist, dann indeterminate deaktivieren
|
||||
if (newVal) {
|
||||
this.internalIndeterminate = false;
|
||||
this.updateIndeterminateState(false);
|
||||
}
|
||||
this.updateIndeterminateState(this.internalIndeterminate);
|
||||
},
|
||||
indeterminate(newVal) {
|
||||
// Indeterminate nur setzen, wenn checked false ist
|
||||
this.internalIndeterminate = newVal && !this.internalChecked;
|
||||
this.updateIndeterminateState(this.internalIndeterminate);
|
||||
this.internalIndeterminate = newVal;
|
||||
this.updateIndeterminateState(newVal);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// Beim Mount: checked hat Priorität über indeterminate
|
||||
if (this.internalChecked) {
|
||||
this.internalIndeterminate = false;
|
||||
}
|
||||
this.updateIndeterminateState(this.internalIndeterminate);
|
||||
this.updateIndeterminateState(this.isIndeterminate);
|
||||
},
|
||||
methods: {
|
||||
focus() {
|
||||
this.$refs.checkboxInput?.focus();
|
||||
},
|
||||
handleChange(event) {
|
||||
setFilter(event) {
|
||||
if (this.disabled) return;
|
||||
|
||||
const newValue = event.target.checked;
|
||||
const valueChanged = this.internalChecked !== newValue;
|
||||
|
||||
// Bei User-Interaktion: indeterminate zurücksetzen
|
||||
this.internalIndeterminate = false;
|
||||
this.internalChecked = newValue;
|
||||
this.updateIndeterminateState(false);
|
||||
|
||||
if (valueChanged) {
|
||||
this.$emit('checkbox-changed', newValue);
|
||||
}
|
||||
this.isChecked = event.target.checked;
|
||||
},
|
||||
updateIndeterminateState(value) {
|
||||
if (this.$refs.checkboxInput) {
|
||||
|
|
@ -92,8 +84,8 @@ export default {
|
|||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
.checkbox-container {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
|
|
|
|||
|
|
@ -1,127 +0,0 @@
|
|||
<template>
|
||||
<div class="circle-badge" :class="circleClasses">
|
||||
<component
|
||||
:is="iconComponent"
|
||||
weight="bold"
|
||||
:size="iconSize"
|
||||
class="circle-icon"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default{
|
||||
name: "CircleBadge",
|
||||
props:{
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'check'
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
validator: (value) => ['primary', 'secondary', 'grey', 'exception', 'skeleton', 'skeleton-grey'].includes(value)
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'small',
|
||||
validator: (value) => ['small', 'medium', 'large'].includes(value)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
circleClasses() {
|
||||
return [
|
||||
`circle-badge--${this.variant}`,
|
||||
`circle-badge--${this.size}`
|
||||
]
|
||||
},
|
||||
iconComponent() {
|
||||
const iconName = this.icon.charAt(0).toUpperCase() + this.icon.slice(1);
|
||||
return `Ph${iconName}`;
|
||||
},
|
||||
iconSize() {
|
||||
const sizes = {
|
||||
small: 12,
|
||||
medium: 16,
|
||||
large: 20
|
||||
};
|
||||
return sizes[this.size];
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.circle-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
border: 0.2rem solid transparent;
|
||||
transition: all 0.2s ease-in-out;
|
||||
outline: none;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Größen */
|
||||
.circle-badge--small {
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
}
|
||||
|
||||
.circle-badge--medium {
|
||||
width: 3.2rem;
|
||||
height: 3.2rem;
|
||||
}
|
||||
|
||||
.circle-badge--large {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
/* Varianten */
|
||||
.circle-badge--primary {
|
||||
background-color: #5AF0B4;
|
||||
color: #002F54;
|
||||
}
|
||||
|
||||
.circle-badge--secondary {
|
||||
background-color: #c3cfdf;
|
||||
color: #002F54;
|
||||
}
|
||||
|
||||
.circle-badge--exception {
|
||||
background-color: #BC2B72;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.circle-badge--grey {
|
||||
background-color: #DCDCDC;
|
||||
color: #002F54;
|
||||
}
|
||||
|
||||
.circle-badge--skeleton {
|
||||
border: 0.1rem solid #002F54;
|
||||
background-color: transparent;
|
||||
color: #002F54;
|
||||
}
|
||||
|
||||
.circle-badge--skeleton-grey {
|
||||
border: 0.1rem solid #6b7280;
|
||||
background-color: transparent;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.circle-badge--skeleton-primary {
|
||||
border: 0.1rem solid #5AF0B4;
|
||||
color: #5AF0B4;
|
||||
}
|
||||
|
||||
|
||||
.circle-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -133,12 +133,9 @@ export default {
|
|||
.box-content.collapsed {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
|
||||
margin: 0 !important; /* ← !important um alle margins zu überschreiben */
|
||||
padding: 0 !important;
|
||||
overflow: hidden;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
ref="trigger"
|
||||
class="dropdown-trigger"
|
||||
:class="{ 'dropdown-trigger--open': isOpen}"
|
||||
@click.stop="toggleDropdown"
|
||||
@click="toggleDropdown"
|
||||
@keydown="handleTriggerKeydown"
|
||||
:disabled="disabled"
|
||||
>
|
||||
|
|
@ -143,7 +143,6 @@ export default {
|
|||
return this.modelValue === option[this.valueKey]
|
||||
},
|
||||
handleClickOutside(event) {
|
||||
|
||||
if (!this.$refs.dropdown?.contains(event.target)) {
|
||||
this.closeDropdown()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,10 +66,13 @@ export default {
|
|||
|
||||
<style scoped>
|
||||
.tree-container {
|
||||
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
overflow-y: auto;
|
||||
height: 100%; /* Nimmt die volle Höhe des Parent-Containers */
|
||||
max-height: 100%; /* Verhindert Überlauf */
|
||||
/* Remove any fixed height constraints */
|
||||
min-height: fit-content;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.tree-container::-webkit-scrollbar {
|
||||
|
|
|
|||
|
|
@ -3,19 +3,14 @@
|
|||
name="list-edit-transition"
|
||||
tag="div"
|
||||
class="list-edit-container"
|
||||
|
||||
>
|
||||
<div v-if="show" class="list-edit">
|
||||
<div class="icon-container"><ph-selection size="24"/><span class="number-circle">{{selectCount}}</span></div>
|
||||
|
||||
|
||||
<basic-button icon="package" @click="handleAction('material')">Material</basic-button>
|
||||
<basic-button icon="tag" @click="handleAction('price')">Price</basic-button>
|
||||
<basic-button icon="vectorThree" @click="handleAction('packaging')">Packaging</basic-button>
|
||||
<basic-button icon="stack" @click="handleAction('amount')">Annual quantity</basic-button>
|
||||
<basic-button icon="MapPin" @click="handleAction('routes')">Routes</basic-button>
|
||||
|
||||
|
||||
<basic-button icon="X" @click="handleAction('deselect')">Cancel</basic-button>
|
||||
<div class="icon-container"><ph-pencil-simple size="24" /><span class="number-circle">{{selectCount}}</span></div>
|
||||
<div class="list-edit-button" @click="handleAction('material')">Material</div>
|
||||
<div class="list-edit-button" @click="handleAction('price')">Price</div>
|
||||
<div class="list-edit-button" @click="handleAction('packaging')">Packaging</div>
|
||||
<div class="list-edit-button" @click="handleAction('destinations')">Destinations & Routes</div>
|
||||
|
||||
</div>
|
||||
</transition>
|
||||
|
|
@ -24,12 +19,11 @@
|
|||
|
||||
<script>
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import {PhPencilSimple, PhSelection} from "@phosphor-icons/vue";
|
||||
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||
import {PhPencilSimple} from "@phosphor-icons/vue";
|
||||
|
||||
export default{
|
||||
name: "MassEditDialog",
|
||||
components: {BasicButton, PhSelection, PhPencilSimple, IconButton},
|
||||
components: {PhPencilSimple, IconButton},
|
||||
emits: ['action'],
|
||||
props: {
|
||||
show: {
|
||||
|
|
@ -76,7 +70,7 @@ export default{
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1.2rem;
|
||||
gap: 3.6rem;
|
||||
background-color: #5AF0B4;
|
||||
border-radius: 0.8rem;
|
||||
flex: 0 0 auto;
|
||||
|
|
|
|||
|
|
@ -11,12 +11,7 @@
|
|||
:style="modalAddStyle"
|
||||
>
|
||||
<div class="modal-container">
|
||||
<box
|
||||
@click.stop
|
||||
class="modal-box"
|
||||
@mouseenter="onModalMouseEnter"
|
||||
@mouseleave="onModalMouseLeave"
|
||||
>
|
||||
<box @click.stop class="modal-box">
|
||||
<slot></slot>
|
||||
</box>
|
||||
</div>
|
||||
|
|
@ -75,11 +70,6 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
preventScroll: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.isVisible) {
|
||||
this.handleOpen();
|
||||
|
|
@ -93,20 +83,6 @@ export default {
|
|||
this.$emit('close');
|
||||
},
|
||||
handleOpen() {
|
||||
// Prevent scroll via event listener
|
||||
this.preventScroll = (e) => {
|
||||
// Allow scrolling when mouse is over modal
|
||||
if (this.isMouseOverModal) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
// Prevent scroll, touch events and keyboard scrolling
|
||||
document.addEventListener('wheel', this.preventScroll, { passive: false });
|
||||
document.addEventListener('touchmove', this.preventScroll, { passive: false });
|
||||
document.addEventListener('keydown', this.preventScrollKeys, { passive: false });
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.modalOverlay) {
|
||||
this.$refs.modalOverlay.focus();
|
||||
|
|
@ -114,31 +90,6 @@ export default {
|
|||
});
|
||||
},
|
||||
handleClose() {
|
||||
// Re-enable scrolling
|
||||
if (this.preventScroll) {
|
||||
document.removeEventListener('wheel', this.preventScroll);
|
||||
document.removeEventListener('touchmove', this.preventScroll);
|
||||
document.removeEventListener('keydown', this.preventScrollKeys);
|
||||
}
|
||||
this.isMouseOverModal = false;
|
||||
},
|
||||
preventScrollKeys(e) {
|
||||
// Check if focus is inside modal
|
||||
if (this.$refs.modalOverlay && this.$refs.modalOverlay.contains(document.activeElement)) {
|
||||
return; // Allow keyboard scrolling inside modal
|
||||
}
|
||||
|
||||
// Prevent scrolling via keyboard (arrow keys, space, helppages up/down)
|
||||
const scrollKeys = [32, 33, 34, 35, 36, 37, 38, 39, 40];
|
||||
if (scrollKeys.includes(e.keyCode)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
onModalMouseEnter() {
|
||||
this.isMouseOverModal = true;
|
||||
},
|
||||
onModalMouseLeave() {
|
||||
this.isMouseOverModal = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -149,8 +100,8 @@ export default {
|
|||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw; /* Statt right: 0 */
|
||||
height: 100vh; /* Statt bottom: 0 */
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
<PhCaretLeft :size="18" /> Previous
|
||||
</button>
|
||||
|
||||
<!-- First pages -->
|
||||
<!-- First page -->
|
||||
<button
|
||||
v-if="showFirstPage"
|
||||
class="pagination-btn page-number"
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
<!-- First ellipsis -->
|
||||
<span v-if="showFirstEllipsis" class="ellipsis">...</span>
|
||||
|
||||
<!-- Page numbers around current pages -->
|
||||
<!-- Page numbers around current page -->
|
||||
<button
|
||||
v-for="pageNum in visiblePages"
|
||||
:key="pageNum"
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
<!-- Last ellipsis -->
|
||||
<span v-if="showLastEllipsis" class="ellipsis">...</span>
|
||||
|
||||
<!-- Last pages -->
|
||||
<!-- Last page -->
|
||||
<button
|
||||
v-if="showLastPage"
|
||||
class="pagination-btn page-number"
|
||||
|
|
|
|||
|
|
@ -1,507 +0,0 @@
|
|||
<template>
|
||||
<div class="route-dropdown" ref="dropdown">
|
||||
<button
|
||||
ref="trigger"
|
||||
class="route-dropdown-trigger"
|
||||
:class="{ 'route-dropdown-trigger--open': isOpen}"
|
||||
@click="toggleDropdown"
|
||||
@keydown="handleTriggerKeydown"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<span class="route-dropdown-trigger-content">
|
||||
<component
|
||||
v-if="selectedIcon"
|
||||
:is="selectedIcon"
|
||||
:size="16"
|
||||
class="route-icon"
|
||||
/>
|
||||
<span class="route-dropdown-trigger-text">
|
||||
{{ selectedDisplayText }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="route-dropdown-trigger-warning"><ph-warning-circle v-if="warnD2D" weight="fill"
|
||||
size="16"></ph-warning-circle></span>
|
||||
<svg
|
||||
class="route-dropdown-trigger-icon"
|
||||
:class="{ 'route-dropdown-trigger-icon--rotated': isOpen }"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="6,9 12,15 18,9"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<transition name="route-dropdown-fade">
|
||||
<ul
|
||||
v-if="isOpen"
|
||||
ref="menu"
|
||||
class="route-dropdown-menu"
|
||||
@keydown="handleMenuKeydown"
|
||||
tabindex="-1"
|
||||
>
|
||||
<!-- D2D Routing Option -->
|
||||
<li
|
||||
class="route-dropdown-option"
|
||||
:class="{
|
||||
'route-dropdown-option--selected': isD2DSelected,
|
||||
'route-dropdown-option--focused': focusedIndex === -1
|
||||
}"
|
||||
@click="selectD2D"
|
||||
@mouseenter="focusedIndex = -1"
|
||||
>
|
||||
<ph-shipping-container :size="16" class="route-icon"/>
|
||||
<span>D2D routing</span>
|
||||
</li>
|
||||
|
||||
<!-- Divider -->
|
||||
<li v-if="options.length > 0" class="route-dropdown-divider"></li>
|
||||
|
||||
<!-- Regular Route Options -->
|
||||
<li
|
||||
v-for="(option, index) in options"
|
||||
:key="option[valueKey]"
|
||||
class="route-dropdown-option"
|
||||
:class="{
|
||||
'route-dropdown-option--selected': isSelected(option),
|
||||
'route-dropdown-option--focused': focusedIndex === index
|
||||
}"
|
||||
@click="selectOption(option, $event)"
|
||||
@mouseenter="focusedIndex = index"
|
||||
>
|
||||
<component
|
||||
:is="getIconForType(option.type)"
|
||||
:size="16"
|
||||
class="route-icon"
|
||||
/>
|
||||
<span>{{ option[displayKey] }}</span>
|
||||
</li>
|
||||
|
||||
<li v-if="options.length === 0" class="route-dropdown-option route-dropdown-option--empty">
|
||||
{{ emptyText }}
|
||||
</li>
|
||||
</ul>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {PhTruck, PhBoat, PhTrain, PhShippingContainer, PhWarningCircle} from '@phosphor-icons/vue'
|
||||
|
||||
export default {
|
||||
name: 'RouteDropdown',
|
||||
components: {
|
||||
PhWarningCircle,
|
||||
PhTruck,
|
||||
PhBoat,
|
||||
PhTrain,
|
||||
PhShippingContainer
|
||||
},
|
||||
props: {
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number, Object],
|
||||
default: null
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Select a route'
|
||||
},
|
||||
displayKey: {
|
||||
type: String,
|
||||
default: 'routeDisplayString'
|
||||
},
|
||||
valueKey: {
|
||||
type: String,
|
||||
default: 'routeCompareString'
|
||||
},
|
||||
emptyText: {
|
||||
type: String,
|
||||
default: 'No routes available'
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showD2dWarn: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'change'],
|
||||
data() {
|
||||
return {
|
||||
isOpen: false,
|
||||
focusedIndex: -1,
|
||||
D2D_VALUE: 'D2D_ROUTING'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
warnD2D() {
|
||||
return this.isD2DSelected && this.showD2dWarn;
|
||||
},
|
||||
selectedOption() {
|
||||
if (!this.modelValue || this.modelValue === this.D2D_VALUE) return null
|
||||
return this.options?.find(option =>
|
||||
option[this.valueKey] === this.modelValue
|
||||
) ?? null
|
||||
},
|
||||
isD2DSelected() {
|
||||
return this.modelValue === this.D2D_VALUE
|
||||
},
|
||||
selectedDisplayText() {
|
||||
if (this.isD2DSelected) return 'D2D routing'
|
||||
if (this.selectedOption) return this.selectedOption[this.displayKey]
|
||||
return this.placeholder
|
||||
},
|
||||
selectedIcon() {
|
||||
if (this.isD2DSelected) return 'ph-shipping-container'
|
||||
if (this.selectedOption) return this.getIconForType(this.selectedOption.type)
|
||||
return null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isOpen(newVal) {
|
||||
if (newVal) {
|
||||
// Warte einen Tick, damit das Menü gerendert ist
|
||||
this.$nextTick(() => {
|
||||
document.addEventListener('click', this.handleClickOutside, true)
|
||||
})
|
||||
} else {
|
||||
document.removeEventListener('click', this.handleClickOutside, true)
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
document.removeEventListener('click', this.handleClickOutside, true)
|
||||
},
|
||||
methods: {
|
||||
getIconForType(type) {
|
||||
const iconMap = {
|
||||
'ROAD': 'ph-truck',
|
||||
'SEA': 'ph-boat',
|
||||
'RAIL': 'ph-train'
|
||||
}
|
||||
return iconMap[type] || null
|
||||
},
|
||||
toggleDropdown(event) {
|
||||
if (this.disabled) return
|
||||
|
||||
event.stopPropagation()
|
||||
this.isOpen = !this.isOpen
|
||||
|
||||
if (this.isOpen) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.menu?.focus()
|
||||
|
||||
if (this.isD2DSelected) {
|
||||
this.focusedIndex = -1
|
||||
} else if (this.selectedOption) {
|
||||
this.focusedIndex = this.options.findIndex(option =>
|
||||
option[this.valueKey] === this.modelValue
|
||||
)
|
||||
} else {
|
||||
this.focusedIndex = -1
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.focusedIndex = -1
|
||||
}
|
||||
},
|
||||
selectD2D(event) {
|
||||
event.stopPropagation()
|
||||
this.$emit('update:modelValue', this.D2D_VALUE)
|
||||
this.$emit('change', {type: 'D2D', value: this.D2D_VALUE})
|
||||
this.closeDropdown()
|
||||
this.$refs.trigger.focus()
|
||||
},
|
||||
selectOption(option, event) {
|
||||
event.stopPropagation()
|
||||
this.$emit('update:modelValue', option[this.valueKey])
|
||||
this.$emit('change', option)
|
||||
this.closeDropdown()
|
||||
this.$refs.trigger.focus()
|
||||
},
|
||||
closeDropdown() {
|
||||
this.isOpen = false
|
||||
this.focusedIndex = -1
|
||||
},
|
||||
isSelected(option) {
|
||||
return this.modelValue === option[this.valueKey]
|
||||
},
|
||||
handleClickOutside(event) {
|
||||
|
||||
if (!this.$refs.dropdown?.contains(event.target)) {
|
||||
this.closeDropdown()
|
||||
}
|
||||
},
|
||||
handleTriggerKeydown(event) {
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
case 'ArrowDown':
|
||||
event.preventDefault()
|
||||
if (!this.isOpen) {
|
||||
this.toggleDropdown(event)
|
||||
}
|
||||
break
|
||||
case 'ArrowUp':
|
||||
event.preventDefault()
|
||||
if (!this.isOpen) {
|
||||
this.toggleDropdown(event)
|
||||
}
|
||||
break
|
||||
case 'Escape':
|
||||
if (this.isOpen) {
|
||||
event.preventDefault()
|
||||
this.closeDropdown()
|
||||
}
|
||||
break
|
||||
}
|
||||
},
|
||||
handleMenuKeydown(event) {
|
||||
const totalOptions = this.options.length
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
event.preventDefault()
|
||||
if (this.focusedIndex === -1) {
|
||||
this.focusedIndex = totalOptions > 0 ? 0 : -1
|
||||
} else {
|
||||
this.focusedIndex = Math.min(this.focusedIndex + 1, totalOptions - 1)
|
||||
}
|
||||
break
|
||||
case 'ArrowUp':
|
||||
event.preventDefault()
|
||||
if (this.focusedIndex === 0) {
|
||||
this.focusedIndex = -1
|
||||
} else if (this.focusedIndex === -1) {
|
||||
this.focusedIndex = -1
|
||||
} else {
|
||||
this.focusedIndex = Math.max(this.focusedIndex - 1, 0)
|
||||
}
|
||||
break
|
||||
case 'Enter':
|
||||
event.preventDefault()
|
||||
if (this.focusedIndex === -1) {
|
||||
this.selectD2D(event)
|
||||
} else if (this.focusedIndex >= 0 && this.options[this.focusedIndex]) {
|
||||
this.selectOption(this.options[this.focusedIndex], event)
|
||||
}
|
||||
break
|
||||
case 'Escape':
|
||||
event.preventDefault()
|
||||
this.closeDropdown()
|
||||
this.$refs.trigger.focus()
|
||||
break
|
||||
case 'Tab':
|
||||
this.closeDropdown()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.route-dropdown {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.route-dropdown-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: white;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
border: 0.2rem solid #E3EDFF;
|
||||
transition: all 0.1s ease;
|
||||
flex: 1 0 auto;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.route-dropdown-trigger:hover {
|
||||
background: #EEF4FF;
|
||||
border: 0.2rem solid #8DB3FE;
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
.route-dropdown-trigger--open {
|
||||
border: 0.2rem solid #8DB3FE;
|
||||
background: #EEF4FF;
|
||||
}
|
||||
|
||||
.route-dropdown-trigger-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.route-dropdown-trigger-text {
|
||||
color: #2d3748;
|
||||
font: inherit;
|
||||
letter-spacing: -0.05em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.route-dropdown-trigger-warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #BC2B72;
|
||||
}
|
||||
|
||||
.route-icon {
|
||||
color: #6b7280;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.route-dropdown-trigger-icon {
|
||||
transition: transform 0.2s ease;
|
||||
color: #718096;
|
||||
flex-shrink: 0;
|
||||
margin-left: 0.8rem;
|
||||
}
|
||||
|
||||
.route-dropdown-trigger-icon--rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.route-dropdown-menu {
|
||||
font: inherit;
|
||||
outline: none;
|
||||
list-style: none;
|
||||
color: #2d3748;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 0;
|
||||
background: white;
|
||||
border: 0.1rem solid #E3EDFF;
|
||||
border-radius: 0.8rem;
|
||||
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
z-index: 1000;
|
||||
max-height: 50rem;
|
||||
overflow-y: auto;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
.route-dropdown-option {
|
||||
font: inherit;
|
||||
padding: 1.2rem 1.6rem;
|
||||
cursor: pointer;
|
||||
border-bottom: 0.16rem solid #f3f4f6;
|
||||
transition: background-color 0.2s ease;
|
||||
color: #2d3748;
|
||||
background: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
|
||||
|
||||
.route-dropdown-option:hover,
|
||||
.route-dropdown-option--focused {
|
||||
background-color: rgba(107, 134, 156, 0.05);
|
||||
}
|
||||
|
||||
.route-dropdown-option--selected {
|
||||
color: #2d3748;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.route-dropdown-option--selected:hover,
|
||||
.route-dropdown-option--selected.route-dropdown-option--focused {
|
||||
color: #2d3748;
|
||||
background-color: rgba(107, 134, 156, 0.05);
|
||||
}
|
||||
|
||||
.route-dropdown-option--empty {
|
||||
color: #001D33;
|
||||
cursor: default;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.route-dropdown-option--empty:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.route-dropdown-divider {
|
||||
height: 0.1rem;
|
||||
background-color: #e5e7eb;
|
||||
|
||||
border-bottom: none; /* Diese Zeile hinzufügen */
|
||||
}
|
||||
|
||||
.route-dropdown-option:has(+ .route-dropdown-divider) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Transition animations */
|
||||
.route-dropdown-fade-enter-active,
|
||||
.route-dropdown-fade-leave-active {
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.route-dropdown-fade-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
.route-dropdown-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
/* Disabled state */
|
||||
.route-dropdown-trigger:disabled {
|
||||
background: white;
|
||||
cursor: not-allowed;
|
||||
border: 0.2rem solid rgba(227, 237, 255, 0.5);
|
||||
color: rgba(0, 47, 84, 0.3);
|
||||
}
|
||||
|
||||
.route-dropdown-trigger:disabled:hover {
|
||||
border: 0.2rem solid rgba(227, 237, 255, 0.5);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.route-dropdown-trigger:disabled .route-dropdown-trigger-text {
|
||||
color: rgba(0, 47, 84, 0.5);
|
||||
}
|
||||
|
||||
.route-dropdown-trigger:disabled .route-dropdown-trigger-icon,
|
||||
.route-dropdown-trigger:disabled .route-icon {
|
||||
color: rgba(113, 128, 150, 0.5);
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 640px) {
|
||||
.route-dropdown-trigger {
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.route-dropdown-option {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
<template>
|
||||
<button
|
||||
@click="$emit('click')"
|
||||
class="sort-button"
|
||||
:class="{ 'active': active }"
|
||||
>
|
||||
<PhCaretUp weight="fill"
|
||||
:size="16"
|
||||
class="sort-icon"
|
||||
:class="{ 'rotate': direction === 'asc' }"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {PhArrowCircleUp, PhCaretUp, PhFunnelSimple} from '@phosphor-icons/vue';
|
||||
|
||||
export default {
|
||||
name: "SortButton",
|
||||
components: {
|
||||
PhCaretUp,
|
||||
PhArrowCircleUp,
|
||||
PhFunnelSimple
|
||||
},
|
||||
emits: ['click'],
|
||||
props: {
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: 'desc',
|
||||
validator: (d) => (d === 'desc' || d === 'asc'),
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sort-button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.sort-icon {
|
||||
color: #6B869C;
|
||||
transition: transform 0.3s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.sort-button.active .sort-icon {
|
||||
color: #002F54;
|
||||
}
|
||||
|
||||
.sort-icon.rotate {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -138,18 +138,17 @@ export default {
|
|||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
flex: 1; /* Take remaining space */
|
||||
min-height: 0; /* Allow shrinking */
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab-pane {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ export default {
|
|||
case 'warning':
|
||||
return 'secondary'
|
||||
case 'info':
|
||||
return 'secondary'
|
||||
return 'primary'
|
||||
case 'error':
|
||||
return 'exception'
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -1,16 +1,10 @@
|
|||
<template>
|
||||
<div class="item-container" :class="{'selected-item': selected}">
|
||||
<div class="supplier-content">
|
||||
<flag :iso="isoCode" size="l"></flag>
|
||||
<div class="supplier-item-text">
|
||||
<div class="supplier-item-name">
|
||||
<span class="user-icon" v-if="isUserSupplier">
|
||||
<ph-user weight="fill"></ph-user>
|
||||
</span>
|
||||
{{name}}
|
||||
</div>
|
||||
<div class="supplier-item-address">{{ address }}</div>
|
||||
</div>
|
||||
<flag :iso="isoCode" size="l"></flag>
|
||||
<div class="supplier-item-text">
|
||||
<div class="supplier-item-name"> <span class="user-icon" v-if="isUserSupplier"><ph-user weight="fill" ></ph-user></span> {{name}}</div>
|
||||
<div class="supplier-item-address">{{ address }}</div>
|
||||
|
||||
</div>
|
||||
<icon-button v-if="showTrash" icon="trash" @click="deleteClick"></icon-button>
|
||||
</div>
|
||||
|
|
@ -71,13 +65,14 @@ export default {
|
|||
|
||||
.item-container {
|
||||
display: flex;
|
||||
justify-content: flex-start; /* Statt space-between */
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 3.6rem 3.6rem;
|
||||
background: white;
|
||||
border-radius: 0.8rem;
|
||||
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
gap: 2.4rem;
|
||||
flex: 0 0 50rem;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
|
@ -102,20 +97,6 @@ export default {
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.supplier-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2.4rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.supplier-item-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
|
||||
.supplier-item-name {
|
||||
font-size: 1.6rem;
|
||||
|
|
@ -131,8 +112,4 @@ export default {
|
|||
color: #6b7280;
|
||||
}
|
||||
|
||||
.item-container > .icon-button {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -1,260 +1,149 @@
|
|||
<template>
|
||||
<div class="bulk-edit-row" @wheel="handleWheel">
|
||||
<div class="bulk-edit-row__checkbox">
|
||||
<div class="bulk-edit-row">
|
||||
<div class="edit-calculation-checkbox-cell">
|
||||
<checkbox :checked="isSelected" @checkbox-changed="updateSelected"></checkbox>
|
||||
</div>
|
||||
|
||||
<div class="bulk-edit-row__cell-container">
|
||||
<div
|
||||
class="bulk-edit-row__cell bulk-edit-row__cell--status bulk-edit-row__cell--clickable bulk-edit-row__cell--filterable edit-calculation-list-header-cell--copyable"
|
||||
@click="action($event,'material')"
|
||||
@mousedown="handleMouseDown">
|
||||
<div class="bulk-edit-row__data">
|
||||
<div class="bulk-edit-row__line">
|
||||
<ph-package size="16"/>
|
||||
{{ premise.material.part_number ?? 'N/A' }}
|
||||
</div>
|
||||
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
||||
HS:
|
||||
{{ premise.hs_code ?? 'N/A' }}
|
||||
</div>
|
||||
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
||||
<ph-hand-coins size="16"/>
|
||||
{{ toPercent(premise.tariff_rate) ?? 'N/A' }}
|
||||
</div>
|
||||
|
||||
<div class="edit-calculation-cell-container">
|
||||
<div class="edit-calculation-cell copyable-cell" @click="action('material')">
|
||||
<div class="edit-calculation-cell-line">{{ premise.material.part_number }}</div>
|
||||
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="premise.hs_code">
|
||||
HS Code:
|
||||
{{ premise.hs_code }}
|
||||
</div>
|
||||
<div class="bulk-edit-row__status">
|
||||
<transition name="badge-transition" mode="out-in">
|
||||
<circle-badge v-if="materialCheck && showMaterialCheck" :key="'check-' + id" variant="primary" icon="check"
|
||||
class="badge--check"></circle-badge>
|
||||
<circle-badge v-else-if="!materialCheck" :key="'error-' + id" variant="exception"
|
||||
icon="exclamation-mark"></circle-badge>
|
||||
</transition>
|
||||
<div class="edit-calculation-cell-line edit-calculation-cell-subline"
|
||||
v-if="(premise.tariff_rate ?? null) !== null">
|
||||
Tariff rate:
|
||||
{{ toPercent(premise.tariff_rate) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bulk-edit-row__cell-container">
|
||||
<div
|
||||
class="bulk-edit-row__cell bulk-edit-row__cell--status bulk-edit-row__cell--clickable edit-calculation-list-header-cell--copyable"
|
||||
@click="action($event,'price')">
|
||||
<div class="bulk-edit-row__data">
|
||||
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
||||
<ph-tag size="16"/>
|
||||
{{ toFixed(premise.material_cost, '€') }}
|
||||
</div>
|
||||
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
||||
<ph-chart-pie-slice size="16"/>
|
||||
{{ toPercent(premise.oversea_share) }}
|
||||
</div>
|
||||
<div class="bulk-edit-row__line bulk-edit-row__line--sub" v-if="premise.is_fca_enabled">
|
||||
<basic-badge icon="plus" size="compact" variant="primary">FCA</basic-badge>
|
||||
</div>
|
||||
|
||||
<div class="edit-calculation-cell-container">
|
||||
<div class="edit-calculation-cell copyable-cell" @click="action('price')" v-if="showPrice">
|
||||
<div class="edit-calculation-cell-line">{{ toFixed(premise.material_cost) }} EUR</div>
|
||||
<div class="edit-calculation-cell-line edit-calculation-cell-subline">Oversea share:
|
||||
{{ toPercent(premise.oversea_share) }} %
|
||||
</div>
|
||||
<div class="bulk-edit-row__status">
|
||||
<transition name="badge-transition" mode="out-in">
|
||||
<circle-badge v-if="priceCheck && showPriceCheck" :key="'check-price-' + id" variant="primary"
|
||||
icon="check" class="badge--check"></circle-badge>
|
||||
<circle-badge v-else-if="!priceCheck" :key="'error-price-' + id" variant="exception"
|
||||
icon="exclamation-mark"></circle-badge>
|
||||
</transition>
|
||||
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="premise.is_fca_enabled">
|
||||
<basic-badge icon="plus" variant="primary">FCA FEE</basic-badge>
|
||||
</div>
|
||||
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="showPriceIncomplete">
|
||||
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="edit-calculation-empty copyable-cell" v-else @click="action('price')">
|
||||
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="edit-calculation-cell-container">
|
||||
<div v-if="showHu" class="edit-calculation-cell copyable-cell"
|
||||
@click="action('packaging')">
|
||||
<div class="edit-calculation-cell-line">
|
||||
<PhVectorThree/>
|
||||
{{ premise.handling_unit.length }} x
|
||||
{{ premise.handling_unit.width }} x
|
||||
{{ premise.handling_unit.height }} {{ premise.handling_unit.dimension_unit }}
|
||||
</div>
|
||||
<div class="edit-calculation-cell-line edit-calculation-cell-subline">
|
||||
<PhBarbell/>
|
||||
<span>{{ premise.handling_unit.weight }} {{ premise.handling_unit.weight_unit }}</span>
|
||||
</div>
|
||||
<div class="edit-calculation-cell-line edit-calculation-cell-subline">
|
||||
<PhHash/>
|
||||
{{ premise.handling_unit.content_unit_count }} pcs.
|
||||
</div>
|
||||
<div class="edit-calculation-packaging-badges">
|
||||
<basic-badge v-if="premise.is_stackable" variant="primary" icon="stack">STACKABLE</basic-badge>
|
||||
<basic-badge v-if="premise.is_mixable" variant="skeleton" icon="shuffle">MIXABLE</basic-badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="edit-calculation-empty copyable-cell" v-else
|
||||
@click="action('packaging')">
|
||||
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="edit-calculation-cell-container">
|
||||
<div class="edit-calculation-cell" v-if="premise.supplier">
|
||||
<div class="calculation-list-supplier-data">
|
||||
<div class="edit-calculation-cell-line">{{ premise.supplier.name }}</div>
|
||||
<div class="edit-calculation-cell-subline"> {{ premise.supplier.address }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bulk-edit-row__cell-container">
|
||||
<div
|
||||
class="bulk-edit-row__cell bulk-edit-row__cell--status bulk-edit-row__cell--clickable edit-calculation-list-header-cell--copyable"
|
||||
@click="action($event,'packaging')">
|
||||
<div class="bulk-edit-row__data">
|
||||
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
||||
<PhVectorThree size="16"/>
|
||||
{{ toDimension(premise.handling_unit) }}
|
||||
</div>
|
||||
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
||||
<PhBarbell size="16"/>
|
||||
<span>{{ toFixed(premise.handling_unit.weight, premise.handling_unit.weight_unit, 0) }}</span>
|
||||
</div>
|
||||
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
||||
<PhHash size="16"/>
|
||||
{{ toFixed(premise.handling_unit.content_unit_count, 'pcs.', 0) }}
|
||||
</div>
|
||||
<div class="bulk-edit-row__badges">
|
||||
<basic-badge v-if="premise.is_stackable" size="compact" variant="primary" icon="stack">STACKABLE
|
||||
</basic-badge>
|
||||
<basic-badge v-if="premise.is_mixable" size="compact" variant="secondary" icon="shuffle">MIXABLE
|
||||
</basic-badge>
|
||||
</div>
|
||||
|
||||
<div class="edit-calculation-cell-container">
|
||||
<div class="edit-calculation-cell copyable-cell" v-if="showDestinations"
|
||||
@click="action('destinations')">
|
||||
<div class="edit-calculation-cell-line">
|
||||
<span class="number-circle"> {{ destinationsCount }} </span> Destinations
|
||||
</div>
|
||||
<div class="bulk-edit-row__status">
|
||||
<transition name="badge-transition" mode="out-in">
|
||||
<circle-badge v-if="packagingCheck && showPackagingCheck" :key="'check-packaging-' + id" variant="primary"
|
||||
icon="check" class="badge--check"></circle-badge>
|
||||
<circle-badge v-else-if="!packagingCheck" :key="'error-packaging-' + id" variant="exception"
|
||||
icon="exclamation-mark"></circle-badge>
|
||||
</transition>
|
||||
<div class="edit-calculation-cell-subline" v-for="name in destinationNames"> {{ name }}</div>
|
||||
<div class="edit-calculation-cell-subline" v-if="showDestinationIncomplete">
|
||||
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="edit-calculation-empty" v-else-if="showMassEdit">
|
||||
<spinner></spinner>
|
||||
</div>
|
||||
<div class="edit-calculation-empty copyable-cell" v-else
|
||||
@click="action('destinations')">
|
||||
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bulk-edit-row__cell-container">
|
||||
<div class="bulk-edit-row__cell bulk-edit-row__cell--status bulk-edit-row__cell--filterable"
|
||||
@click="action($event,'supplier')"
|
||||
@mousedown="handleMouseDown">
|
||||
<div class="bulk-edit-row__data">
|
||||
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
||||
<ph-factory style="display: inline-block; vertical-align: middle;" size="16"/>
|
||||
{{ premise.supplier.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bulk-edit-row__cell-container">
|
||||
<div
|
||||
class="bulk-edit-row__cell bulk-edit-row__cell--status bulk-edit-row__cell--clickable bulk-edit-row__cell--destinations"
|
||||
@click="action($event,'amount')">
|
||||
<div class="bulk-edit-row__data bulk-edit-row__data--destinations">
|
||||
<div class="bulk-edit-row__dest-line"
|
||||
v-for="(destination, index) in destinations.slice(0, 3)"
|
||||
:key="index">
|
||||
<div>
|
||||
<ph-stack size="16"/>
|
||||
</div>
|
||||
<div>{{ toFixed(destination.annual_amount, 'pcs.', 0) }}</div>
|
||||
<div>
|
||||
<basic-badge size="compact" variant="secondary">{{ toDestination(destination) }}</basic-badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bulk-edit-row__dest-line" v-if="destinations.length > 3">
|
||||
<div></div>
|
||||
<div> more ...</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expanded destinations overlay -->
|
||||
<div class="bulk-edit-row__destinations-expanded" v-if="destinations.length > 3">
|
||||
<div class="bulk-edit-row__dest-line"
|
||||
v-for="(destination, index) in destinations"
|
||||
:key="index">
|
||||
<div>
|
||||
<ph-stack size="16"/>
|
||||
</div>
|
||||
<div>{{ toFixed(destination.annual_amount, 'pcs.', 0) }}</div>
|
||||
<div>
|
||||
<basic-badge size="compact" variant="secondary">{{ toDestination(destination) }}</basic-badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bulk-edit-row__status">
|
||||
<transition name="badge-transition" mode="out-in">
|
||||
<circle-badge v-if="destinationCheck && showDestinationCheck" :key="'check-dest-' + id" variant="primary"
|
||||
icon="check" class="badge--check"></circle-badge>
|
||||
<circle-badge v-else-if="!destinationCheck" :key="'error-dest-' + id" variant="exception"
|
||||
icon="exclamation-mark"></circle-badge>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bulk-edit-row__cell-container">
|
||||
<div
|
||||
class="bulk-edit-row__cell bulk-edit-row__cell--status bulk-edit-row__cell--clickable bulk-edit-row__cell--destinations"
|
||||
@click="action($event,'routes')">
|
||||
<div class="bulk-edit-row__data">
|
||||
<div class="bulk-edit-row__route-line"
|
||||
v-for="(destination, index) in destinations.slice(0, 3)"
|
||||
:key="index">
|
||||
<div>
|
||||
<component :is="toRouteIcon(destination)" size="16"></component>
|
||||
</div>
|
||||
<div>{{ toRoute(destination) }}</div>
|
||||
<div>
|
||||
<basic-badge size="compact" variant="secondary">{{ toDestination(destination) }}</basic-badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bulk-edit-row__route-line" v-if="destinations.length > 3">
|
||||
<div></div>
|
||||
<div> more ...</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expanded destinations overlay -->
|
||||
<div class="bulk-edit-row__destinations-expanded" v-if="destinations.length > 3">
|
||||
<div class="bulk-edit-row__route-line"
|
||||
v-for="(destination, index) in destinations"
|
||||
:key="index">
|
||||
<div>
|
||||
<component :is="toRouteIcon(destination)" size="16"></component>
|
||||
</div>
|
||||
<div>{{ toRoute(destination) }}</div>
|
||||
<div>
|
||||
<basic-badge size="compact" variant="secondary">{{ toDestination(destination, 15) }}</basic-badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bulk-edit-row__status">
|
||||
<transition name="badge-transition" mode="out-in">
|
||||
<circle-badge v-if="routeCheck && showRouteCheck" :key="'check-route-' + id" variant="primary"
|
||||
icon="check" class="badge--check"></circle-badge>
|
||||
<circle-badge v-else-if="!routeCheck" :key="'error-route-' + id" variant="exception"
|
||||
icon="exclamation-mark"></circle-badge>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bulk-edit-row__actions">
|
||||
<div class="edit-calculation-actions-cell">
|
||||
<icon-button icon="pencil-simple" help-text="Edit this calculation" help-text-position="left"
|
||||
@click="editSingle"></icon-button>
|
||||
<icon-button icon="x" help-text="Remove from mass edit" help-text-position="left" @click="remove"></icon-button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import Flag from "@/components/UI/Flag.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {usePremiseEditStore} from "@/store/premiseEdit.js";
|
||||
import BasicBadge from "@/components/UI/BasicBadge.vue";
|
||||
import {
|
||||
PhBarbell, PhBoat,
|
||||
PhChartPieSlice,
|
||||
PhBarbell,
|
||||
PhBarcode,
|
||||
PhEmpty,
|
||||
PhFactory,
|
||||
PhHandCoins,
|
||||
PhHash,
|
||||
PhMapPinLine,
|
||||
PhPackage, PhPath,
|
||||
PhTag, PhTrain, PhTruck,
|
||||
PhVectorThree
|
||||
PhMapPin,
|
||||
PhPercent,
|
||||
PhVectorThree,
|
||||
PhVectorTwo
|
||||
} from "@phosphor-icons/vue";
|
||||
import {UrlSafeBase64} from "@/common.js";
|
||||
import CircleBadge from "@/components/UI/CircleBadge.vue";
|
||||
import {useDestinationEditStore} from "@/store/destinationEdit.js";
|
||||
import Spinner from "@/components/UI/Spinner.vue";
|
||||
|
||||
|
||||
export default {
|
||||
name: "BulkEditRow",
|
||||
emits: ['remove', 'action', 'select'],
|
||||
emits: ['remove', 'action'],
|
||||
components: {
|
||||
PhPath,
|
||||
PhMapPinLine,
|
||||
PhPackage,
|
||||
PhTag,
|
||||
PhChartPieSlice,
|
||||
PhHandCoins,
|
||||
CircleBadge,
|
||||
Spinner,
|
||||
PhMapPin,
|
||||
PhFactory,
|
||||
PhBarbell,
|
||||
PhHash,
|
||||
PhVectorThree,
|
||||
BasicBadge,
|
||||
IconButton,
|
||||
Checkbox
|
||||
PhPercent,
|
||||
PhBarcode, PhBarbell, PhHash, PhVectorThree, PhVectorTwo, PhEmpty, BasicBadge, Flag, IconButton, Checkbox
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
|
|
@ -266,245 +155,70 @@ export default {
|
|||
required: true,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// Flags to show check badges only on transition
|
||||
showMaterialCheck: false,
|
||||
showPriceCheck: false,
|
||||
showPackagingCheck: false,
|
||||
showDestinationCheck: false,
|
||||
showRouteCheck: false,
|
||||
// Flag to track if component has been initialized
|
||||
isInitialized: false,
|
||||
// Store the initial state to prevent false triggers on mount
|
||||
initialCheckStates: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
materialCheck() {
|
||||
return (this.premise?.material.part_number != null && this.premise?.tariff_rate != null)
|
||||
destinationsCount() {
|
||||
return this.premise.destinations?.length ?? 0;
|
||||
},
|
||||
priceCheck() {
|
||||
return (this.premise?.material_cost != null && this.premise?.oversea_share != null);
|
||||
destinationsText() {
|
||||
return this.premise.destinations.map(d => d.destination_node.name).join(', ');
|
||||
},
|
||||
destinationNames() {
|
||||
const spliceCnt = ((this.premise.destinations.length === 4) ? 4 : 3) - (this.showDestinationIncomplete ? 1 : 0);
|
||||
const names = this.premise.destinations.map(d => d.destination_node.name).slice(0, spliceCnt);
|
||||
if (this.premise.destinations.length > names.length) {
|
||||
names.push('and more ...');
|
||||
}
|
||||
|
||||
return names;
|
||||
},
|
||||
showDestinationIncomplete() {
|
||||
return this.premise.destinations.some(p => (
|
||||
(((p.annual_amount ?? null) === null) || p.annual_amount === 0 ||
|
||||
((p.routes?.every(r => !r.is_selected) && !p.is_d2d) ||
|
||||
(p.is_d2d && ((p.rate_d2d ?? null) === null || p.rate_d2d === 0 || (p.lead_time_d2d ?? null) === null || p.lead_time_d2d === 0))
|
||||
)
|
||||
)));
|
||||
},
|
||||
showDestinations() {
|
||||
return (this.destinationsCount > 0);
|
||||
},
|
||||
showMassEdit() {
|
||||
return this.premiseEditStore.showProcessingModal;
|
||||
},
|
||||
showHu() {
|
||||
return (this.hu.width && this.hu.length && this.hu.height && this.hu.weight && this.hu.content_unit_count)
|
||||
},
|
||||
showPrice() {
|
||||
return !(this.premise.material_cost === null) || (this.premise.material_cost === 0)
|
||||
},
|
||||
showPriceIncomplete() {
|
||||
return (this.premise.oversea_share === null)
|
||||
},
|
||||
isSelected() {
|
||||
return this.premise.selected;
|
||||
},
|
||||
hu() {
|
||||
return this.premise.handling_unit;
|
||||
},
|
||||
packagingCheck() {
|
||||
return this.hu?.length != null
|
||||
&& this.hu?.width != null
|
||||
&& this.hu?.height != null
|
||||
&& this.hu?.weight != null
|
||||
&& this.hu?.content_unit_count != null;
|
||||
},
|
||||
destinationCheck() {
|
||||
|
||||
if (((this.destinations ?? null) === null) || this.destinations.length === 0)
|
||||
return true;
|
||||
|
||||
return !this.destinations?.some(d => !d.annual_amount);
|
||||
|
||||
},
|
||||
routeCheck() {
|
||||
|
||||
if (((this.destinations ?? null) === null) || this.destinations.length === 0)
|
||||
return true;
|
||||
|
||||
return this.destinations?.every(d => (d.is_d2d && d.rate_d2d && d.lead_time_d2d) || (!d.is_d2d && d.routes?.some((route) => route.is_selected)));
|
||||
},
|
||||
isSelected() {
|
||||
return this.premiseEditStore.isChecked(this.id);
|
||||
},
|
||||
destinations() {
|
||||
return this.destinationEditStore.getByPremiseId(this.id) ?? [];
|
||||
},
|
||||
...mapStores(usePremiseEditStore, useDestinationEditStore),
|
||||
},
|
||||
watch: {
|
||||
materialCheck(newVal, oldVal) {
|
||||
if (this.isInitialized
|
||||
&& oldVal === false
|
||||
&& newVal === true
|
||||
&& this.initialCheckStates?.material !== true) {
|
||||
this.showMaterialCheck = true;
|
||||
// Reset initial state after first valid transition
|
||||
if (this.initialCheckStates) {
|
||||
this.initialCheckStates.material = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
priceCheck(newVal, oldVal) {
|
||||
if (this.isInitialized
|
||||
&& oldVal === false
|
||||
&& newVal === true
|
||||
&& this.initialCheckStates?.price !== true) {
|
||||
this.showPriceCheck = true;
|
||||
if (this.initialCheckStates) {
|
||||
this.initialCheckStates.price = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
packagingCheck(newVal, oldVal) {
|
||||
if (this.isInitialized
|
||||
&& oldVal === false
|
||||
&& newVal === true
|
||||
&& this.initialCheckStates?.packaging !== true) {
|
||||
this.showPackagingCheck = true;
|
||||
if (this.initialCheckStates) {
|
||||
this.initialCheckStates.packaging = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
destinationCheck(newVal, oldVal) {
|
||||
if (this.isInitialized
|
||||
&& oldVal === false
|
||||
&& newVal === true
|
||||
&& this.initialCheckStates?.destination === false) { // Hier war !== true, sollte === false sein
|
||||
this.showDestinationCheck = true;
|
||||
if (this.initialCheckStates) {
|
||||
this.initialCheckStates.destination = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
routeCheck(newVal, oldVal) {
|
||||
if (this.isInitialized
|
||||
&& oldVal === false
|
||||
&& newVal === true
|
||||
&& this.initialCheckStates?.route === false) { // Hier war !== true, sollte === false sein
|
||||
this.showRouteCheck = true;
|
||||
if (this.initialCheckStates) {
|
||||
this.initialCheckStates.route = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// Capture initial states BEFORE setting isInitialized
|
||||
// This prevents the watchers from triggering on mount
|
||||
this.initialCheckStates = {
|
||||
material: this.materialCheck,
|
||||
price: this.priceCheck,
|
||||
packaging: this.packagingCheck,
|
||||
destination: this.destinationCheck,
|
||||
route: this.routeCheck,
|
||||
};
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.isInitialized = true;
|
||||
});
|
||||
...mapStores(usePremiseEditStore),
|
||||
},
|
||||
methods: {
|
||||
toDestination(destination, limit = 15) {
|
||||
return this.toNode(destination.destination_node, limit);
|
||||
},
|
||||
toNode(node, limit = 5) {
|
||||
if (!node)
|
||||
return 'N/A';
|
||||
|
||||
const name = node.name;
|
||||
const mappingId = node.external_mapping_id;
|
||||
const needsShortName = name.length > limit;
|
||||
const useMappingId = ((mappingId ?? null) !== null) && ((name ?? null) === null || needsShortName);
|
||||
const shortName = name?.substring(0, limit).concat("...") ?? 'N/A';
|
||||
return `${useMappingId ? mappingId.replace("_", " ") : (needsShortName ? shortName : name)}`;
|
||||
|
||||
},
|
||||
toDimension(handlingUnit) {
|
||||
if (((handlingUnit ?? null) == null)
|
||||
|| ((handlingUnit.length ?? null) == null)
|
||||
|| ((handlingUnit.width ?? null) == null)
|
||||
|| ((handlingUnit.height ?? null) == null)
|
||||
) return 'N/A';
|
||||
|
||||
return `${this.toFixed(handlingUnit.length, null, 0)} x ${this.toFixed(handlingUnit.width, null, 0)} x ${this.toFixed(handlingUnit.height, null, 0)} ${handlingUnit.dimension_unit}`;
|
||||
},
|
||||
toRoute(destination, limit = 32) {
|
||||
const route = destination?.routes?.find((route) => route.is_selected) ?? null;
|
||||
|
||||
if (destination.is_d2d)
|
||||
return 'D2D Routing';
|
||||
|
||||
if (!route)
|
||||
return 'N/A';
|
||||
|
||||
const nodes = route.transit_nodes?.map((node) => this.toNode(node)) ?? [];
|
||||
|
||||
if (nodes.length === 0)
|
||||
return 'N/A';
|
||||
|
||||
const separator = " > ";
|
||||
let fullString = nodes.join(separator);
|
||||
|
||||
if (fullString.length <= limit)
|
||||
return fullString;
|
||||
|
||||
const front = nodes[0].concat(separator).concat("...").concat(separator);
|
||||
let back = [];
|
||||
|
||||
for (const node of nodes.slice().reverse()) {
|
||||
|
||||
back.unshift(node);
|
||||
const temp = front.concat(back.join(separator));
|
||||
|
||||
if (temp.length > limit) {
|
||||
return front.concat(back.slice(1).join(separator));
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
toRouteIcon(destination) {
|
||||
const route = destination?.routes?.find((route) => route.is_selected) ?? null;
|
||||
|
||||
if (destination.is_d2d)
|
||||
return 'PhShippingContainer';
|
||||
|
||||
if (route?.type === 'SEA')
|
||||
return 'PhBoat';
|
||||
|
||||
if (route?.type === 'RAIL')
|
||||
return 'PhTrain';
|
||||
|
||||
if (route?.type === 'ROAD')
|
||||
return 'PhTruck';
|
||||
|
||||
|
||||
return 'PhEmpty';
|
||||
},
|
||||
toFixed(value, unit = null, decimals = 2) {
|
||||
return value !== null ? `${(value).toFixed(decimals)} ${unit ?? ''}` : 'N/A';
|
||||
toFixed(value) {
|
||||
return value !== null ? (value).toFixed(2) : '0.00';
|
||||
},
|
||||
toPercent(value) {
|
||||
return value !== null ? `${(value * 100).toFixed(2)} %` : 'N/A';
|
||||
return value !== null ? (value * 100).toFixed(2) : '0.00';
|
||||
},
|
||||
updateSelected(value) {
|
||||
this.premiseEditStore.setSelectTo([this.id], value);
|
||||
},
|
||||
editSingle() {
|
||||
const bulkQuery = this.$route.params.ids;
|
||||
const urlStr = new UrlSafeBase64().encodeIds([this.id]);
|
||||
this.$router.push({name: 'bulk-single-edit', params: {id: urlStr, ids: bulkQuery}});
|
||||
},
|
||||
handleMouseDown(event) {
|
||||
if (event.shiftKey || event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
handleWheel(event) {
|
||||
if (event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
window.scrollBy(0, event.deltaY);
|
||||
}
|
||||
},
|
||||
action(event, action) {
|
||||
|
||||
if (event.ctrlKey && !event.shiftKey && (action === 'material' || action === 'supplier')) {
|
||||
this.$emit('action', {id: this.id, action: action.concat('-filter')});
|
||||
} else if (event.ctrlKey && event.shiftKey && (action === 'material' || action === 'supplier')) {
|
||||
this.$emit('action', {id: this.id, action: action.concat('-append')});
|
||||
} else if (action !== 'supplier') {
|
||||
this.$emit('action', {id: this.id, action: action});
|
||||
}
|
||||
|
||||
},
|
||||
updateSelected(value) {
|
||||
this.$emit("select", {id: this.id, checked: value});
|
||||
action(action) {
|
||||
this.$emit('action', {id: this.id, action: action});
|
||||
},
|
||||
remove() {
|
||||
this.premiseEditStore.removePremise(this.id);
|
||||
|
|
@ -515,231 +229,127 @@ export default {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Main container */
|
||||
|
||||
.edit-calculation-cell-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
justify-self: stretch;
|
||||
}
|
||||
|
||||
.edit-calculation-cell {
|
||||
flex: 1 1 auto;
|
||||
margin: 1.6rem 0;
|
||||
padding: 0.8rem;
|
||||
}
|
||||
|
||||
.edit-calculation-empty {
|
||||
flex: 1 1 auto;
|
||||
margin: 1.6rem 0;
|
||||
padding: 0.8rem;
|
||||
}
|
||||
|
||||
.copyable-cell {
|
||||
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
||||
/* Standard hover ohne copy mode */
|
||||
.copyable-cell:hover {
|
||||
cursor: pointer;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
||||
.bulk-edit-row {
|
||||
display: grid;
|
||||
grid-template-columns: 6rem 0.8fr 0.7fr 1fr 1fr 1.2fr 2fr 10rem;
|
||||
grid-template-columns: 6rem 1fr 1fr 1.5fr 1.5fr 1.5fr 10rem;
|
||||
gap: 1.6rem;
|
||||
padding: 0 2.4rem;
|
||||
border-bottom: 0.16rem solid #f3f4f6;
|
||||
align-items: stretch;
|
||||
align-items: center;
|
||||
transition: background-color 0.2s ease;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
height: 14rem;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bulk-edit-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Erhöhe z-index der gesamten Row beim Hover über destinations */
|
||||
.bulk-edit-row:has(.bulk-edit-row__destinations-expanded:hover) {
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
/* Checkbox cell */
|
||||
.bulk-edit-row__checkbox {
|
||||
.edit-calculation-checkbox-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Cell container */
|
||||
.bulk-edit-row__cell-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Cell */
|
||||
.bulk-edit-row__cell {
|
||||
flex: 1 1 auto;
|
||||
margin: 1.6rem 0;
|
||||
padding: 0.8rem;
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
||||
.bulk-edit-row__cell--status {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.bulk-edit-row__cell--clickable:hover {
|
||||
cursor: pointer;
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
|
||||
.bulk-edit-row__cell--destinations {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Cell data */
|
||||
.bulk-edit-row__data {
|
||||
.edit-calculation-cell--price {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.bulk-edit-row__data--destinations {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
.edit-calculation-cell--supplier {
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
height: 90%;
|
||||
}
|
||||
|
||||
/* Cell status */
|
||||
.bulk-edit-row__status {
|
||||
.edit-calculation-cell--supplier-container {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.edit-calculation-cell--supplier-flag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* Badge Transition Animation */
|
||||
.badge-transition-enter-active {
|
||||
animation: badge-enter 0.3s ease-in both;
|
||||
}
|
||||
|
||||
.badge-transition-leave-active {
|
||||
animation: badge-leave 0.3s ease-in;
|
||||
}
|
||||
|
||||
/* Check badge fade-out Animation - wird NACH der Enter-Animation ausgeführt */
|
||||
.badge--check {
|
||||
animation: badge-enter 0.3s ease-out 0.1s both,
|
||||
badge-zoom-fade-out 0.6s ease-out both;
|
||||
}
|
||||
|
||||
@keyframes badge-enter {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes badge-leave {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes badge-zoom-fade-out {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
30% {
|
||||
transform: scale(3);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(0.5);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Lines */
|
||||
.bulk-edit-row__line {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.bulk-edit-row__line--sub {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* Destination lines */
|
||||
.bulk-edit-row__dest-line {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr 2fr;
|
||||
gap: 0.4rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* Destination lines */
|
||||
.bulk-edit-row__route-line {
|
||||
display: grid;
|
||||
grid-template-columns: auto 2fr 1fr;
|
||||
gap: 0.4rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
.bulk-edit-row__badges {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.bulk-edit-row__warning-badge {
|
||||
position: absolute;
|
||||
top: 0.8rem;
|
||||
right: 0.8rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Expanded destinations overlay */
|
||||
.bulk-edit-row__destinations-expanded {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 0.1rem solid #E3EDFF;
|
||||
border-radius: 0.8rem;
|
||||
box-shadow: 0 0.4rem 0.6rem rgba(0, 0, 0, 0.1);
|
||||
padding: 0.8rem;
|
||||
z-index: 100;
|
||||
.edit-calculation-cell--packaging, .edit-calculation-cell--material, .edit-calculation-cell--destination {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
opacity: 0;
|
||||
transform: translateY(-0.4rem);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bulk-edit-row__cell--destinations:hover .bulk-edit-row__destinations-expanded {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
max-height: 50rem;
|
||||
.edit-calculation-cell-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.bulk-edit-row__cell--destinations:hover .bulk-edit-row__data--destinations {
|
||||
opacity: 0;
|
||||
.edit-calculation-cell-subline {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.bulk-edit-row__cell--destinations:not(:has(.bulk-edit-row__destinations-expanded)):hover .bulk-edit-row__data--destinations {
|
||||
opacity: 1;
|
||||
.edit-calculation-packaging-badges {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
.bulk-edit-row__actions {
|
||||
|
||||
|
||||
.edit-calculation-actions-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.number-circle {
|
||||
display: inline-block;
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
border-radius: 50%;
|
||||
background-color: #002F54;
|
||||
color: white;
|
||||
text-align: center;
|
||||
line-height: 1.6rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
|
@ -11,10 +11,9 @@
|
|||
</div>
|
||||
<div class="bulk-operation-status">
|
||||
<div v-if="operation.state === 'EXCEPTION'">
|
||||
<tooltip v-if="operation.error" min-width="500px" :text="shortend(operation.error?.message)" position="left">
|
||||
<tooltip min-width="500px" :text="shortend(operation.error.message)" position="left">
|
||||
<basic-badge variant="exception">ERROR</basic-badge>
|
||||
</tooltip>
|
||||
<basic-badge v-else variant="exception">ERROR</basic-badge>
|
||||
</div>
|
||||
<basic-badge v-else-if="operation.state === 'COMPLETED'">COMPLETED</basic-badge>
|
||||
<basic-badge v-else-if="operation.state === 'SCHEDULED'" variant="skeleton">SCHEDULED</basic-badge>
|
||||
|
|
|
|||
|
|
@ -20,9 +20,6 @@
|
|||
<div class="calculation-list-status-cell">
|
||||
<basic-badge :variant="variant" :icon="variantIcon">{{ premise.state }}</basic-badge>
|
||||
</div>
|
||||
<div class="calculation-list-date-cell">
|
||||
{{ buildDate(this.premise.created_at, true)}}
|
||||
</div>
|
||||
<div class="calculation-list-actions-cell">
|
||||
<icon-button :disabled="!isDraft" icon="pencil-simple" @click="editClick" help-text="Edit this calculation"
|
||||
help-text-position="left"></icon-button>
|
||||
|
|
@ -42,7 +39,7 @@ import Checkbox from "@/components/UI/Checkbox.vue";
|
|||
import {mapStores} from "pinia";
|
||||
import {usePremiseStore} from "@/store/premise.js";
|
||||
import Flag from "@/components/UI/Flag.vue";
|
||||
import {buildDate, UrlSafeBase64} from "@/common.js";
|
||||
import {UrlSafeBase64} from "@/common.js";
|
||||
|
||||
export default {
|
||||
name: "CalculationListItem",
|
||||
|
|
@ -71,7 +68,7 @@ export default {
|
|||
return 'grey';
|
||||
} else if (this.premise.state === 'COMPLETED') {
|
||||
return 'primary';
|
||||
} else if (this.premise.state === 'COMPLETED' && this.premise.calculation_state === 'EXCEPTION') {
|
||||
} else if (this.premise.state === 'EXCEPTION') {
|
||||
return 'exception';
|
||||
} else {
|
||||
return 'grey';
|
||||
|
|
@ -98,7 +95,6 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
buildDate,
|
||||
updateCheckBox(checked) {
|
||||
this.$emit('updateCheckbox', {checked: checked, id: this.id});
|
||||
},
|
||||
|
|
@ -128,7 +124,7 @@ export default {
|
|||
|
||||
.calculation-list-row {
|
||||
display: grid;
|
||||
grid-template-columns: 6rem 1fr 2fr 14rem 20rem 10rem;
|
||||
grid-template-columns: 6rem 1fr 2fr 14rem 10rem;
|
||||
gap: 1.6rem;
|
||||
padding: 1.6rem;
|
||||
border-bottom: 0.16rem solid #f3f4f6;
|
||||
|
|
@ -185,9 +181,8 @@ export default {
|
|||
.calculation-list-supplier-data {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
height: 90%
|
||||
}
|
||||
|
||||
.supplier-name {
|
||||
|
|
@ -207,14 +202,6 @@ export default {
|
|||
justify-content: start;
|
||||
}
|
||||
|
||||
.calculation-list-date-cell {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
color: #6b7280;
|
||||
font-size: 1.4rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.calculation-list-actions-cell {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
|
|
|
|||
|
|
@ -1,212 +0,0 @@
|
|||
<template>
|
||||
<div class="dashboard-container">
|
||||
<!-- Total Calculations -->
|
||||
<box>
|
||||
<div class="dashboard-box">
|
||||
<div class="dashboard-box-icon dashboard-box-icon--primary">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 256 256">
|
||||
<polygon points="32 80 128 136 224 80 128 24 32 80" fill="currentColor" opacity="0.2"/>
|
||||
<polygon points="32 128 128 184 224 128 128 72 32 128" fill="currentColor" opacity="0.2"/>
|
||||
<polygon points="32 176 128 232 224 176 128 120 32 176" fill="currentColor" opacity="0.2"/>
|
||||
<rect width="256" height="256" fill="none"/><polyline points="32 176 128 232 224 176" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="32 128 128 184 224 128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polygon points="32 80 128 136 224 80 128 24 32 80" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
|
||||
</div>
|
||||
<div class="dashboard-box-info">
|
||||
<div v-if="completed !== null" class="dashboard-box-number">{{ completed }}</div>
|
||||
<div v-else class="dashboard-box-number"><spinner size="s"/></div>
|
||||
<div class="dashboard-box-number-text">Completed</div>
|
||||
</div>
|
||||
</div>
|
||||
</box>
|
||||
|
||||
<!-- Draft Calculations -->
|
||||
<box>
|
||||
<div class="dashboard-box">
|
||||
<div class="dashboard-box-icon dashboard-box-icon--primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="42" height="42" viewBox="0 0 24 24">
|
||||
<rect x="9" y="-1" width="8" height="24" fill="currentcolor" opacity="0.2" transform="rotate(45 13 11)"/>
|
||||
<path d="M14.078 4.232l-12.64 12.639-1.438 7.129 7.127-1.438 12.641-12.64-5.69-5.69zm-10.369 14.893l-.85-.85 11.141-11.125.849.849-11.14 11.126zm2.008 2.008l-.85-.85 11.141-11.125.85.85-11.141 11.125zm18.283-15.444l-2.816 2.818-5.691-5.691 2.816-2.816 5.691 5.689z"/></svg>
|
||||
</div>
|
||||
<div class="dashboard-box-info">
|
||||
<div v-if="drafts !== null" class="dashboard-box-number">{{ drafts }}</div>
|
||||
<div v-else class="dashboard-box-number"><spinner size="s"/></div>
|
||||
<div class="dashboard-box-number-text">Drafts</div>
|
||||
</div>
|
||||
</div>
|
||||
</box>
|
||||
|
||||
<!-- Running Calculations -->
|
||||
<box>
|
||||
<div class="dashboard-box">
|
||||
<div class="dashboard-box-icon dashboard-box-icon--primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24">
|
||||
<circle cx="14" cy="14" r="10" fill="currentcolor" opacity="0.2"/>
|
||||
<path fill="currentcolor" d="M15.91 13.34l2.636-4.026-.454-.406-3.673 3.099c-.675-.138-1.402.068-1.894.618-.736.823-.665 2.088.159 2.824.824.736 2.088.665 2.824-.159.492-.55.615-1.295.402-1.95zm-3.91-10.646v-2.694h4v2.694c-1.439-.243-2.592-.238-4 0zm8.851 2.064l1.407-1.407 1.414 1.414-1.321 1.321c-.462-.484-.964-.927-1.5-1.328zm-18.851 4.242h8v2h-8v-2zm-2 4h8v2h-8v-2zm3 4h7v2h-7v-2zm21-3c0 5.523-4.477 10-10 10-2.79 0-5.3-1.155-7.111-3h3.28c1.138.631 2.439 1 3.831 1 4.411 0 8-3.589 8-8s-3.589-8-8-8c-1.392 0-2.693.369-3.831 1h-3.28c1.811-1.845 4.321-3 7.111-3 5.523 0 10 4.477 10 10z"/>
|
||||
</svg> </div>
|
||||
<div class="dashboard-box-info">
|
||||
<div v-if="running !== null" class="dashboard-box-number">{{ running }}</div>
|
||||
<div v-else class="dashboard-box-number"><spinner size="s"/></div>
|
||||
<div class="dashboard-box-number-text">Running</div>
|
||||
</div>
|
||||
</div>
|
||||
</box>
|
||||
|
||||
<!-- Failed Calculations -->
|
||||
<box>
|
||||
<div class="dashboard-box">
|
||||
<div class="dashboard-box-icon dashboard-box-icon--primary">
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24 4L44 40H4L24 4Z" fill="currentColor" opacity="0.2"/>
|
||||
<path d="M24 4L44 40H4L24 4Z" stroke="currentColor" stroke-width="3" stroke-linejoin="round" fill="none"/>
|
||||
<path d="M24 18V28" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
|
||||
<circle cx="24" cy="34" r="1.5" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="dashboard-box-info">
|
||||
<div v-if="failed !== null" class="dashboard-box-number">{{ failed }}</div>
|
||||
<div v-else class="dashboard-box-number"><spinner size="s"/></div>
|
||||
<div class="dashboard-box-number-text">Failed</div>
|
||||
</div>
|
||||
</div>
|
||||
</box>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Box from "@/components/UI/Box.vue";
|
||||
import { PhStack, PhPencilSimple, PhHourglassMedium, PhWarning } from "@phosphor-icons/vue";
|
||||
import {useDashboardStore} from "@/store/dashboard.js";
|
||||
import {mapStores} from "pinia";
|
||||
import {usePremiseStore} from "@/store/premise.js";
|
||||
import {useActiveUserStore} from "@/store/activeuser.js";
|
||||
import Spinner from "@/components/UI/Spinner.vue";
|
||||
|
||||
export default {
|
||||
name: "TheDashboard",
|
||||
components: {
|
||||
Spinner,
|
||||
PhStack,
|
||||
PhPencilSimple,
|
||||
PhHourglassMedium,
|
||||
PhWarning,
|
||||
Box
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useDashboardStore),
|
||||
completed() {
|
||||
return this.dashboardStore.completed
|
||||
},
|
||||
drafts() {
|
||||
return this.dashboardStore.drafts
|
||||
},
|
||||
running() {
|
||||
return this.dashboardStore.running
|
||||
},
|
||||
failed() {
|
||||
return this.dashboardStore.failed
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.dashboardStore.load();
|
||||
},
|
||||
mounted() {
|
||||
this.dashboardStore.startPulling();
|
||||
},
|
||||
unmounted() {
|
||||
this.dashboardStore.stopPulling();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1.6rem;
|
||||
margin: 1.6rem 0 3.6rem 0;
|
||||
}
|
||||
|
||||
.dashboard-box {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 2rem 2.4rem;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.dashboard-box-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dashboard-box-more-link {
|
||||
color: #002F54;
|
||||
font-weight: 400;
|
||||
font-size: 1.2rem;
|
||||
text-decoration: none;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.dashboard-box-icon--primary {
|
||||
color: #002F54;
|
||||
}
|
||||
|
||||
.dashboard-box-icon--draft {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.dashboard-box-icon--running {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.dashboard-box-icon--error {
|
||||
color: #BC2B72;
|
||||
}
|
||||
|
||||
.dashboard-box-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.dashboard-box-number {
|
||||
font-weight: 500;
|
||||
color: #002F54;
|
||||
font-size: 3.2rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.dashboard-box-number-text {
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05rem;
|
||||
font-size: 1.4rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1024px) {
|
||||
.dashboard-container {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.dashboard-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-box {
|
||||
padding: 1.6rem 2rem;
|
||||
}
|
||||
|
||||
.dashboard-box-number {
|
||||
font-size: 2.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,31 +1,20 @@
|
|||
<template>
|
||||
<div class="apps-container">
|
||||
<div class="app-list-actions">
|
||||
|
||||
</div>
|
||||
<div class="app-list-header">
|
||||
<div>App</div>
|
||||
<div>Groups</div>
|
||||
<div>Action</div>
|
||||
</div>
|
||||
<div class="app-list">
|
||||
|
||||
<app-list-item v-for="app in apps" :app="app" @delete-app="deleteApp" @export-app="exportApp"></app-list-item>
|
||||
|
||||
</div>
|
||||
|
||||
<modal :state="modalState">
|
||||
<add-app @close="closeModal"></add-app>
|
||||
</modal>
|
||||
|
||||
|
||||
<div class="app-list-actions">
|
||||
<div class="app-list-header">
|
||||
<div>App</div>
|
||||
<div>Groups</div>
|
||||
<div>Action</div>
|
||||
</div>
|
||||
<div class="app-list">
|
||||
|
||||
<basic-button icon="Upload" @click="importApp">Import</basic-button>
|
||||
<basic-button icon="Plus" @click="modalState = true">New App</basic-button>
|
||||
</div>
|
||||
<app-list-item v-for="app in apps" :app="app" @delete-app="deleteApp"></app-list-item>
|
||||
</div>
|
||||
|
||||
<modal :state="modalState">
|
||||
<add-app @close="closeModal"></add-app>
|
||||
</modal>
|
||||
<basic-button icon="Plus" @click="modalState = true">New App</basic-button>
|
||||
|
||||
</template>
|
||||
|
||||
|
|
@ -36,8 +25,6 @@ import {mapStores} from "pinia";
|
|||
import {useAppsStore} from "@/store/apps.js";
|
||||
import Modal from "@/components/UI/Modal.vue";
|
||||
import AddApp from "@/components/layout/config/AddApp.vue";
|
||||
import Dropdown from "@/components/UI/Dropdown.vue";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
|
||||
export default {
|
||||
name: "Apps",
|
||||
|
|
@ -47,7 +34,7 @@ export default {
|
|||
default: false
|
||||
}
|
||||
},
|
||||
components: {IconButton, Dropdown, AddApp, Modal, AppListItem, BasicButton},
|
||||
components: {AddApp, Modal, AppListItem, BasicButton},
|
||||
computed: {
|
||||
...mapStores(useAppsStore),
|
||||
apps() {
|
||||
|
|
@ -56,8 +43,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
modalState: false,
|
||||
exportedApp: null
|
||||
modalState: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -69,62 +55,6 @@ export default {
|
|||
},
|
||||
deleteApp(id) {
|
||||
this.appsStore.deleteApp(id);
|
||||
},
|
||||
async exportApp(id) {
|
||||
const response = await this.appsStore.exportApp(id);
|
||||
const app = this.appsStore.getById(id);
|
||||
|
||||
if(response?.data) {
|
||||
const base64String = response.data;
|
||||
|
||||
const blob = new Blob([base64String], { type: 'text/plain' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${app.name}.app`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
},
|
||||
async importApp() {
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.app';
|
||||
|
||||
input.onchange = async (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
try {
|
||||
const fileContent = await this.readFileContent(file);
|
||||
await this.appsStore.importApp(fileContent);
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// File Dialog öffnen
|
||||
input.click();
|
||||
},
|
||||
|
||||
readFileContent(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
resolve(e.target.result);
|
||||
};
|
||||
|
||||
reader.onerror = (error) => {
|
||||
reject(error);
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
|
|
@ -135,10 +65,6 @@ export default {
|
|||
|
||||
<style scoped>
|
||||
|
||||
.apps-container {
|
||||
padding: 2.4rem;
|
||||
}
|
||||
|
||||
.app-list-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr 0.5fr;
|
||||
|
|
@ -153,13 +79,6 @@ export default {
|
|||
border-bottom: 0.1rem solid #E3EDFF;
|
||||
}
|
||||
|
||||
.app-list-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 2rem;
|
||||
gap: 1.6rem
|
||||
}
|
||||
|
||||
.app-list {
|
||||
margin-bottom: 2.4rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -295,7 +295,6 @@ export default {
|
|||
}
|
||||
|
||||
.bulk-operations-container {
|
||||
margin: 2.4rem;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
|
|
|
|||
|
|
@ -1,148 +0,0 @@
|
|||
<template>
|
||||
<div class="dump-container">
|
||||
|
||||
<table-view :columns="columns" :data-source="fetch" :page-size="pageSize" :page="pagination.page"
|
||||
:page-count="pagination.pageCount" :total-count="pagination.totalCount" @row-click="showDump"
|
||||
:mouse-over="true"></table-view>
|
||||
|
||||
|
||||
<modal :state="showModal">
|
||||
<div class="single-dump-container">
|
||||
<div class="button-container">
|
||||
<basic-button @click="showModal = false" :show-icon="false" variant="secondary">Close</basic-button>
|
||||
<basic-button variant="secondary" :show-icon="false" @click="expand = !expand"> {{ expand === true ? 'Collapse all items' : 'Expand all items' }}</basic-button>
|
||||
</div>
|
||||
<json-tree-viewer :data="dump" :all-expanded="expand"></json-tree-viewer>
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import TableView from "@/components/UI/TableView.vue";
|
||||
import performRequest from "@/backend.js";
|
||||
import {config} from "@/config.js";
|
||||
import Modal from "@/components/UI/Modal.vue";
|
||||
import JsonTreeViewer from "@/components/UI/JsonTreeViewer.vue";
|
||||
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||
|
||||
export default {
|
||||
name: "CalculationDumps",
|
||||
components: {BasicButton, JsonTreeViewer, Modal, TableView},
|
||||
methods: {
|
||||
showDump(dump) {
|
||||
this.dump = dump;
|
||||
this.showModal = true;
|
||||
},
|
||||
async fetch(query) {
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (query?.searchTerm && query.searchTerm !== '')
|
||||
params.append('filter', query.searchTerm);
|
||||
if (query?.periodId)
|
||||
params.append('valid', query.periodId);
|
||||
|
||||
if (query?.page)
|
||||
params.append('page', query.page);
|
||||
|
||||
if (query?.pageSize)
|
||||
params.append('limit', query.pageSize);
|
||||
|
||||
const resp = await performRequest(null, "GET", `${config.backendUrl}/dumps/dump/${params.size === 0 ? '' : '?'}${params.toString()}`, null);
|
||||
this.dump = resp.data;
|
||||
this.pagination = {
|
||||
page: parseInt(resp.headers.get('X-Current-Page')),
|
||||
pageCount: parseInt(resp.headers.get('X-Page-Count')),
|
||||
totalCount: parseInt(resp.headers.get('X-Total-Count'))
|
||||
};
|
||||
|
||||
return this.dump;
|
||||
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showModal: false,
|
||||
dump: null,
|
||||
expand: false,
|
||||
|
||||
pageSize: 20,
|
||||
pagination: {page: 1, pageCount: 1, totalCount: 1},
|
||||
columns: [
|
||||
{
|
||||
key: 'id',
|
||||
label: 'ID',
|
||||
},
|
||||
{
|
||||
key: 'calculation_date',
|
||||
label: 'Calculation date',
|
||||
},
|
||||
{
|
||||
key: 'job_state',
|
||||
label: 'State',
|
||||
badgeResolver: (value) => {
|
||||
if (value === 'VALID')
|
||||
return [{text: value, variant: "primary"}];
|
||||
if (value === 'EXCEPTION')
|
||||
return [{text: value, variant: "exception"}]
|
||||
return [{text: value, variant: "secondary"}]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'user_id',
|
||||
label: 'User ID',
|
||||
},
|
||||
{
|
||||
key: 'premise.material.part_number',
|
||||
label: 'Material',
|
||||
},
|
||||
{
|
||||
key: 'premise.supplier.name',
|
||||
label: 'Supplier',
|
||||
},
|
||||
{
|
||||
key: 'premise.destinations',
|
||||
label: 'Destinations',
|
||||
formatter: (value) => {
|
||||
return value.map(v => v.destination_node.name).join(', ');
|
||||
}
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.dump-container {
|
||||
padding: 2.4rem;
|
||||
}
|
||||
|
||||
.single-dump-container {
|
||||
height: 80vh;
|
||||
width: 80vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.single-dump-container > :first-child {
|
||||
flex-shrink: 0; /* Button behält seine Größe */
|
||||
}
|
||||
|
||||
.single-dump-container > :last-child {
|
||||
flex: 1;
|
||||
min-height: 0; /* Wichtig für Flex-Children mit overflow */
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="materials-container">
|
||||
<div>
|
||||
<table-view ref="tableViewRef" :data-source="fetch" :columns="materialColumns" :page="pagination.page"
|
||||
:page-size="pageSize" :page-count="pagination.pageCount"
|
||||
:total-count="pagination.totalCount"></table-view>
|
||||
|
|
@ -74,8 +74,5 @@ export default {
|
|||
|
||||
|
||||
<style scoped>
|
||||
.materials-container {
|
||||
padding: 2.4rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -1,26 +1,8 @@
|
|||
<template>
|
||||
<div class="nodes-container">
|
||||
|
||||
<modal :state="showModal">
|
||||
<div class="node-modal-container">
|
||||
<div class="node-header">
|
||||
<h3 class="sub-header"> {{ node.name }}
|
||||
</h3>
|
||||
<icon-button icon="x" @click="showModal = false"></icon-button>
|
||||
</div>
|
||||
<div class="node-body">
|
||||
<div class="node-address"><flag :iso="node.country.iso_code" />{{ node.address }}</div>
|
||||
<div class="supplier-map" v-if="node.location">
|
||||
<open-street-map-embed :coordinates="node.location" :zoom="5" width="100%" height="300px"
|
||||
custom-filter="grayscale(0.8) sepia(0.5) hue-rotate(180deg) saturate(0.5) brightness(1.0)"></open-street-map-embed>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
<div>
|
||||
<table-view ref="tableViewRef" :data-source="fetch" :columns="nodeColumns" :page="pagination.page"
|
||||
:page-size="pageSize" :page-count="pagination.pageCount"
|
||||
:total-count="pagination.totalCount" @row-click="showDetails" :mouse-over="true"></table-view>
|
||||
:total-count="pagination.totalCount"></table-view>
|
||||
|
||||
</div>
|
||||
|
||||
|
|
@ -30,12 +12,6 @@
|
|||
import TableView from "@/components/UI/TableView.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {useNodeStore} from "@/store/node.js";
|
||||
import Modal from "@/components/UI/Modal.vue";
|
||||
import ErrorModal from "@/components/layout/error/ErrorModal.vue";
|
||||
import TabContainer from "@/components/UI/TabContainer.vue";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import OpenStreetMapEmbed from "@/components/UI/OpenStreetMapEmbed.vue";
|
||||
import Flag from "@/components/UI/Flag.vue";
|
||||
|
||||
export default {
|
||||
name: "Nodes",
|
||||
|
|
@ -52,7 +28,7 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
components: {Flag, OpenStreetMapEmbed, IconButton, TabContainer, ErrorModal, Modal, TableView},
|
||||
components: {TableView},
|
||||
computed: {
|
||||
...mapStores(useNodeStore),
|
||||
},
|
||||
|
|
@ -64,16 +40,10 @@ export default {
|
|||
await this.nodeStore.setQuery(query);
|
||||
this.pagination = this.nodeStore.pagination;
|
||||
return this.nodeStore.nodes;
|
||||
},
|
||||
showDetails(node) {
|
||||
this.node = node;
|
||||
this.showModal = true;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showModal: false,
|
||||
node: null,
|
||||
nodeColumns: [
|
||||
{
|
||||
key: 'external_mapping_id',
|
||||
|
|
@ -121,43 +91,4 @@ export default {
|
|||
|
||||
<style scoped>
|
||||
|
||||
.nodes-container {
|
||||
padding: 2.4rem;
|
||||
}
|
||||
|
||||
|
||||
.node-modal-container {
|
||||
height: 40rem;
|
||||
width: 60rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden; /* Verhindert Overflow */
|
||||
}
|
||||
|
||||
.node-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0; /* Header soll nicht schrumpfen */
|
||||
padding-bottom: 1.6rem; /* Optional: etwas Abstand */
|
||||
}
|
||||
|
||||
.node-body {
|
||||
flex: 1; /* Nimmt den restlichen Platz ein */
|
||||
min-height: 0; /* Wichtig für Flexbox-Scrolling */
|
||||
overflow: hidden; /* Container selbst soll nicht scrollen */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.node-address {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
|
||||
color: #6b7280;
|
||||
font-size: 1.4rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -166,9 +166,6 @@ export default {
|
|||
width: fit-content;
|
||||
}
|
||||
|
||||
.properties-container {
|
||||
padding: 2.4rem;
|
||||
}
|
||||
|
||||
.property-item-enter-from {
|
||||
opacity: 0;
|
||||
|
|
|
|||
|
|
@ -178,16 +178,15 @@ export default {
|
|||
}
|
||||
|
||||
if (this.property.data_type === 'INT') {
|
||||
this.value = parseNumberFromString(this.value, 0, true);
|
||||
this.value = parseNumberFromString(this.value, 0);
|
||||
}
|
||||
|
||||
if (this.property.data_type === 'PERCENTAGE') {
|
||||
this.value = parseNumberFromString(this.value, 4, true);
|
||||
this.value = parseNumberFromString(this.value, 4);
|
||||
}
|
||||
|
||||
if (this.property.data_type === 'CURRENCY') {
|
||||
this.value = parseNumberFromString(this.value, 2, true);
|
||||
console.log(this.property.name, " parsed from 'currency' property: '", this.value, "'")
|
||||
this.value = parseNumberFromString(this.value, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -236,7 +236,6 @@ export default {
|
|||
.container-rate-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.6rem;
|
||||
}
|
||||
|
||||
.container-rate-header {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div class="users-container">
|
||||
<div>
|
||||
<div class="user-list">
|
||||
<table-view ref="tableViewRef" :searchbar="false" :columns="columns" :data-source="fetch" @row-click="selectUser"
|
||||
:mouse-over="true"></table-view>
|
||||
<table-view ref="tableViewRef" :searchbar="false" :columns="columns" :data-source="fetch" @row-click="selectUser"
|
||||
:mouse-over="true"></table-view>
|
||||
</div>
|
||||
<modal :state="showModal">
|
||||
<edit-user @close="closeModal" v-model:user="selectedUser" :is-new-user="isNewUser"></edit-user>
|
||||
|
|
@ -119,9 +119,9 @@ export default {
|
|||
badgeResolver: (value) => {
|
||||
|
||||
const formattedValues = []
|
||||
value.slice(0, 5).forEach(v => formattedValues.push({text: v, variant: "secondary"}));
|
||||
value.slice(0,5).forEach(v => formattedValues.push({text: v, variant: "secondary"}));
|
||||
|
||||
if (value.length > 5)
|
||||
if(value.length > 5)
|
||||
formattedValues.push({text: "...", variant: "secondary"});
|
||||
|
||||
return formattedValues;
|
||||
|
|
@ -141,11 +141,6 @@ export default {
|
|||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.users-container {
|
||||
padding: 2.4rem;
|
||||
}
|
||||
|
||||
.user-list {
|
||||
margin-bottom: 2.4rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export default {
|
|||
components: {Box, BasicButton, ToggleSwitch, JsonTreeViewer},
|
||||
async created() {
|
||||
|
||||
const resp = await performRequest(null, "GET", `${config.backendUrl}/dumps/dump/${this.$route.params.id}`, null);
|
||||
const resp = await performRequest(null, "GET", `${config.backendUrl}/dev/dump/${this.$route.params.id}`, null);
|
||||
this.dump = resp.data;
|
||||
|
||||
},
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export default {
|
|||
if(query?.pageSize)
|
||||
params.append('limit', query.pageSize);
|
||||
|
||||
const resp = await performRequest(null, "GET", `${config.backendUrl}/dumps/dump/${params.size === 0 ? '' : '?'}${params.toString()}`, null);
|
||||
const resp = await performRequest(null, "GET", `${config.backendUrl}/dev/dump/${params.size === 0 ? '' : '?'}${params.toString()}`, null);
|
||||
this.dump = resp.data;
|
||||
this.pagination = { page: parseInt(resp.headers.get('X-Current-Page')), pageCount: parseInt(resp.headers.get('X-Page-Count')), totalCount: parseInt(resp.headers.get('X-Total-Count'))};
|
||||
|
||||
|
|
@ -92,5 +92,4 @@ export default {
|
|||
|
||||
<style scoped>
|
||||
|
||||
|
||||
</style>
|
||||
|
|
@ -98,6 +98,8 @@ export default {
|
|||
},
|
||||
editDestination(id) {
|
||||
|
||||
logger.log(id);
|
||||
|
||||
if (id) {
|
||||
const destination = this.premiseSingleEditStore.getDestinationById(id);
|
||||
logger.log(destination);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div class="outer-container">
|
||||
<div class="container" :class="{ 'responsive': responsive }" @focusout="focusLost">
|
||||
|
||||
|
||||
<!-- Wiederholende Felder als Array definieren und mit v-for rendern -->
|
||||
<template v-for="field in displayFields" :key="field.name">
|
||||
<div class="caption-column">{{ field.label }}</div>
|
||||
<div class="input-column">
|
||||
|
|
@ -15,7 +15,6 @@
|
|||
@blur="field.onBlur"
|
||||
class="input-field"
|
||||
autocomplete="off"
|
||||
:placeholder="fromMassEdit ? '<keep>' : ''"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -29,7 +28,7 @@
|
|||
<div>
|
||||
<div>Automatic tariff rate determination was ambiguous</div>
|
||||
<div class="tariff-rate-info-text">
|
||||
Please correct tariff rate or continue with default value.
|
||||
Please contact a customs expert to obtain correct HS code and tariff rate.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -93,10 +92,6 @@ export default {
|
|||
responsive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
fromMassEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
<div class="text-container">
|
||||
<input ref="lengthInput" :value="huLength" @blur="validateDimension('length', $event)"
|
||||
@keydown.enter="handleEnter('lengthInput', $event)" class="input-field"
|
||||
:placeholder="fromMassEdit ? '<keep>' : ''"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -21,7 +20,6 @@
|
|||
<div class="text-container">
|
||||
<input ref="widthInput" :value="huWidth" @blur="validateDimension('width', $event)"
|
||||
@keydown.enter="handleEnter('widthInput', $event)" class="input-field"
|
||||
:placeholder="fromMassEdit ? '<keep>' : ''"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -34,7 +32,6 @@
|
|||
<div class="text-container">
|
||||
<input ref="heightInput" :value="huHeight" @blur="validateDimension('height', $event)"
|
||||
@keydown.enter="handleEnter('heightInput', $event)" class="input-field"
|
||||
:placeholder="fromMassEdit ? '<keep>' : ''"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -47,7 +44,6 @@
|
|||
<div class="text-container">
|
||||
<input ref="weightInput" :value="huWeight" @blur="validateWeight('weight', $event)"
|
||||
@keydown.enter="handleEnter('weightInput', $event)" class="input-field"
|
||||
:placeholder="fromMassEdit ? '<keep>' : ''"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -61,7 +57,6 @@
|
|||
<div class="text-container">
|
||||
<input ref="unitCountInput" :value="huUnitCount" @blur="validateCount"
|
||||
@keydown.enter="handleEnter('unitCountInput', $event)" class="input-field"
|
||||
:placeholder="fromMassEdit ? '<keep>' : ''"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -89,7 +84,7 @@ import {parseNumberFromString} from "@/common.js";
|
|||
export default {
|
||||
name: "PackagingEdit",
|
||||
components: {Tooltip, Dropdown, Checkbox},
|
||||
emits: ['update:stackable', 'update:mixable', 'update:length', 'update:width', 'update:height', 'update:weight', 'update:unitCount', 'update:weightUnit', 'update:dimensionUnit', 'save', 'accept'],
|
||||
emits: ['update:stackable', 'update:mixable', 'update:length', 'update:width', 'update:height', 'update:weight', 'update:unitCount', 'update:weightUnit', 'update:dimensionUnit', 'save'],
|
||||
props: {
|
||||
length: {
|
||||
required: true,
|
||||
|
|
@ -130,10 +125,6 @@ export default {
|
|||
responsive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
fromMassEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -213,13 +204,6 @@ export default {
|
|||
const inputOrder = ['lengthInput', 'widthInput', 'heightInput', 'weightInput', 'unitCountInput'];
|
||||
|
||||
const currentIndex = inputOrder.indexOf(currentRef);
|
||||
|
||||
if(currentIndex >= inputOrder.length - 1) {
|
||||
this.validateCount(event);
|
||||
this.$emit('accept');
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentIndex !== -1 && currentIndex < inputOrder.length - 1) {
|
||||
const nextRef = inputOrder[currentIndex + 1];
|
||||
this.$nextTick(() => {
|
||||
|
|
|
|||
|
|
@ -5,19 +5,17 @@
|
|||
<div class="caption-column">MEK_A [EUR]</div>
|
||||
<div class="input-column">
|
||||
<div class="text-container">
|
||||
<input ref="priceInput" @keydown.enter="handleEnter('priceInput', $event)" :value="priceFormatted" @blur="validatePrice" class="input-field"
|
||||
:placeholder="fromMassEdit ? '<keep>' : ''"
|
||||
<input :value="priceFormatted" @blur="validatePrice" class="input-field"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<div class="caption-column">Overseas share [%]</div>
|
||||
<div class="caption-column">Oversea share [%]</div>
|
||||
<div class="input-column">
|
||||
<div class="text-container">
|
||||
<input ref="overseaShareInput" @keydown.enter="handleEnter('overseaShareInput', $event)" :value="overSeaSharePercent" @blur="validateOverSeaShare" class="input-field"
|
||||
:placeholder="fromMassEdit ? '<keep>' : ''"
|
||||
<input :value="overSeaSharePercent" @blur="validateOverSeaShare" class="input-field"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -27,7 +25,7 @@
|
|||
<div class="caption-column">Include FCA Fee</div>
|
||||
<div class="input-column">
|
||||
<tooltip text="Select if a additional FCA has to be added during calculation">
|
||||
<checkbox ref="fcaInput" @enter="handleEnter('fcaInput', $event)" @keydown.enter="handleEnter('fcaInput', $event)" :checked="includeFcaFee" @checkbox-changed="updateIncludeFcaFee"></checkbox>
|
||||
<checkbox :checked="includeFcaFee" @checkbox-changed="updateIncludeFcaFee"></checkbox>
|
||||
</tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -64,10 +62,6 @@ export default {
|
|||
responsive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
fromMassEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -79,29 +73,6 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
handleEnter(currentRef, event) {
|
||||
event.preventDefault();
|
||||
|
||||
// Define the navigation order
|
||||
const inputOrder = ['priceInput', 'overseaShareInput', 'fcaInput'];
|
||||
|
||||
const currentIndex = inputOrder.indexOf(currentRef);
|
||||
|
||||
if(currentIndex >= inputOrder.length - 1) {
|
||||
this.$emit('accept');
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentIndex !== -1 && currentIndex < inputOrder.length - 1) {
|
||||
const nextRef = inputOrder[currentIndex + 1];
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs[nextRef]) {
|
||||
this.$refs[nextRef].focus();
|
||||
// this.$refs[nextRef].select();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
focusLost(event) {
|
||||
if (!this.$el.contains(event.relatedTarget)) {
|
||||
this.$emit('save', 'price');
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export default {
|
|||
},
|
||||
repackaging: {
|
||||
get() {
|
||||
return this.destination?.repackaging_costs?.toFixed(2) ?? '';
|
||||
return this.destination?.repackaging_costs?.toFixed(2) ?? '0.00';
|
||||
},
|
||||
set(value) {
|
||||
return this.destination && (this.destination.repackaging_costs = value);
|
||||
|
|
@ -66,7 +66,7 @@ export default {
|
|||
},
|
||||
handling: {
|
||||
get() {
|
||||
return this.destination?.handling_costs?.toFixed(2) ?? '';
|
||||
return this.destination?.handling_costs?.toFixed(2) ?? '0.00';
|
||||
},
|
||||
set(value) {
|
||||
return this.destination && (this.destination.handling_costs = value);
|
||||
|
|
@ -74,7 +74,7 @@ export default {
|
|||
},
|
||||
disposal: {
|
||||
get() {
|
||||
return this.destination?.disposal_costs?.toFixed(2) ?? '';
|
||||
return this.destination?.disposal_costs?.toFixed(2) ?? '0.00';
|
||||
},
|
||||
set(value) {
|
||||
return this.destination && (this.destination.disposal_costs = value);
|
||||
|
|
@ -118,7 +118,6 @@ export default {
|
|||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
align-items: flex-start;
|
||||
margin: 1.6rem;
|
||||
}
|
||||
|
||||
.destination-edit-handling-cost-info {
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ export default {
|
|||
},
|
||||
rateD2d: {
|
||||
get() {
|
||||
return this.destination.rate_d2d?.toFixed(2) ?? null;
|
||||
return this.destination.rate_d2d?.toFixed(2) ?? '0.00';
|
||||
},
|
||||
set(value) {
|
||||
this.destination && (this.destination.rate_d2d = value);
|
||||
|
|
@ -172,7 +172,7 @@ export default {
|
|||
},
|
||||
leadtimeD2d: {
|
||||
get() {
|
||||
return this.destination.lead_time_d2d === 0 ? null : (this.destination.lead_time_d2d?.toFixed() ?? null);
|
||||
return this.destination.lead_time_d2d?.toFixed() ?? '0';
|
||||
},
|
||||
set(value) {
|
||||
this.destination && (this.destination.lead_time_d2d = value);
|
||||
|
|
@ -197,7 +197,6 @@ export default {
|
|||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0; /* Important for flexbox shrinking */
|
||||
margin: 1.6rem;
|
||||
}
|
||||
|
||||
.destination-edit-route-warning {
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export default {
|
|||
return this.route.is_fastest;
|
||||
},
|
||||
routeElements() {
|
||||
const routeElem = this.route.transit_nodes.map(n => n.external_mapping_id.replace("_", " "));
|
||||
const routeElem = this.route.transit_nodes.map(n => n.external_mapping_id);
|
||||
return routeElem;
|
||||
},
|
||||
isSea() {
|
||||
|
|
|
|||
|
|
@ -1,224 +0,0 @@
|
|||
<template>
|
||||
<div class="dest-mass-create-container">
|
||||
<autosuggest-searchbar @selected="selectedNode" placeholder="Search and add destination ..."
|
||||
no-results-text='No destination found for "{query}".' :fetch-suggestions="fetch"
|
||||
variant="flags" :reset-on-select="true"
|
||||
:flag-resolver="resolveFlag" title-resolver="name"
|
||||
subtitle-resolver="address"></autosuggest-searchbar>
|
||||
|
||||
<div class="dest-mass-create-table-wrapper">
|
||||
<div class="dest-mass-create-table-header">
|
||||
<div class="dest-mass-create-table-header-material">Material</div>
|
||||
<div class="dest-mass-create-table-header-supplier">Supplier</div>
|
||||
<div class="dest-mass-create-table-header-dest"
|
||||
:key="`${dest.id}-${dest.overallCheck}-${dest.overallIndeterminate}`"
|
||||
v-for="dest in destPool">
|
||||
<checkbox :checked="dest.overallCheck" :indeterminate="dest.overallIndeterminate"
|
||||
@checkbox-changed="setOverallCheck($event, dest.id)"></checkbox>
|
||||
{{ toNode(dest, 6) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="dest-mass-create-table">
|
||||
<dest-mass-create-row @update-selected="updateCheck" :row="row" :key="row.id"
|
||||
v-for="row in destMatrix"></dest-mass-create-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AutosuggestSearchbar from "@/components/UI/AutoSuggestSearchBar.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {useNodeStore} from "@/store/node.js";
|
||||
import DestMassCreateRow from "@/components/layout/edit/destination/mass/DestMassCreateRow.vue";
|
||||
import {useDestinationEditStore} from "@/store/destinationEdit.js";
|
||||
import {usePremiseEditStore} from "@/store/premiseEdit.js";
|
||||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
|
||||
export default {
|
||||
name: "DestinationMassCreate",
|
||||
components: {Checkbox, DestMassCreateRow, AutosuggestSearchbar},
|
||||
computed: {
|
||||
...mapStores(useNodeStore, useDestinationEditStore, usePremiseEditStore),
|
||||
premises() {
|
||||
return this.premiseEditStore.getPremisses;
|
||||
},
|
||||
|
||||
},
|
||||
created() {
|
||||
this.buildMatrix();
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
destPool: [],
|
||||
destMatrix: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getDestinationChanges() {
|
||||
return this.destMatrix;
|
||||
},
|
||||
buildMatrix() {
|
||||
this.destPool = [];
|
||||
const destIds = new Set();
|
||||
|
||||
this.premises.forEach(p => {
|
||||
this.destinationEditStore.getByPremiseId(p.id)?.forEach(d => {
|
||||
const destId = d.destination_node.id;
|
||||
if (!destIds.has(destId)) {
|
||||
destIds.add(destId);
|
||||
this.destPool.push({...d.destination_node, overallCheck: false, overallIndeterminate: false});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Build matrix
|
||||
this.destMatrix = this.premises
|
||||
.filter(p => p)
|
||||
.map(premise => {
|
||||
const existingDestIds = new Set(
|
||||
this.destinationEditStore.getByPremiseId(premise.id)
|
||||
?.map(d => d.destination_node.id) ?? []
|
||||
);
|
||||
|
||||
return {
|
||||
id: premise.id,
|
||||
material: premise.material.part_number,
|
||||
supplier: this.toNode(premise.supplier, 30),
|
||||
destinations: this.destPool.map(dest => ({
|
||||
...dest,
|
||||
selected: existingDestIds.has(dest.id)
|
||||
}))
|
||||
};
|
||||
});
|
||||
|
||||
// set overall checkboxes
|
||||
this.destPool.forEach(dest => {
|
||||
const selectedCount = this.destMatrix.filter(r =>
|
||||
r.destinations.some(d => d.id === dest.id && d.selected)
|
||||
).length;
|
||||
|
||||
const totalCount = this.destMatrix.length;
|
||||
|
||||
dest.overallCheck = selectedCount === totalCount;
|
||||
dest.overallIndeterminate = selectedCount > 0 && selectedCount < totalCount;
|
||||
});
|
||||
},
|
||||
updateCheck(data) {
|
||||
const dest = this.destPool.find(d => d.id === data.dest);
|
||||
|
||||
const selectedCount = this.destMatrix.filter(r =>
|
||||
r.destinations.some(d => d.id === data.dest && d.selected)
|
||||
).length;
|
||||
|
||||
const totalCount = this.destMatrix.length;
|
||||
|
||||
dest.overallCheck = selectedCount === totalCount;
|
||||
dest.overallIndeterminate = selectedCount > 0 && !dest.overallCheck;
|
||||
this.$forceUpdate();
|
||||
|
||||
},
|
||||
selectedNode(destination) {
|
||||
if (destination && !this.destPool.find(d => d.id === destination.id)) {
|
||||
this.destPool.push({
|
||||
...destination,
|
||||
overallCheck: false,
|
||||
overallIndeterminate: false
|
||||
});
|
||||
|
||||
this.destMatrix.forEach(p => p.destinations.push({...destination, selected: false}));
|
||||
}
|
||||
},
|
||||
setOverallCheck(newValue, id) {
|
||||
const header = this.destPool.find(d => d.id === id);
|
||||
header.overallCheck = newValue;
|
||||
header.overallIndeterminate = false;
|
||||
|
||||
this.destMatrix.forEach(r => r.destinations.find(d => d.id === id).selected = newValue);
|
||||
this.$forceUpdate();
|
||||
},
|
||||
async fetch(query) {
|
||||
const supplierQuery = {searchTerm: query, includeUserNode: true, nodeType: "DESTINATION"};
|
||||
await this.nodeStore.setSearch(supplierQuery);
|
||||
return this.nodeStore.nodes;
|
||||
},
|
||||
resolveFlag(node) {
|
||||
return node.country.iso_code;
|
||||
},
|
||||
toNode(node, limit = 15) {
|
||||
if (!node)
|
||||
return 'N/A';
|
||||
|
||||
const name = node.name;
|
||||
const mappingId = node.external_mapping_id;
|
||||
const needsShortName = name.length > limit;
|
||||
const useMappingId = ((mappingId ?? null) !== null) && ((name ?? null) === null || needsShortName);
|
||||
const shortName = name?.substring(0, limit).concat("...") ?? 'N/A';
|
||||
return `${useMappingId ? mappingId.replace("_", " ") : (needsShortName ? shortName : name)}`;
|
||||
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.dest-mass-create-container {
|
||||
min-width: 100rem;
|
||||
width: 90vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 80vh; /* Begrenzt die Gesamthöhe */
|
||||
}
|
||||
|
||||
.dest-mass-create-table-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
min-height: 0; /* Wichtig für Firefox */
|
||||
}
|
||||
|
||||
.dest-mass-create-table-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1.6rem;
|
||||
padding: 2.4rem;
|
||||
justify-content: flex-start;
|
||||
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid rgba(107, 134, 156, 0.2);
|
||||
font-weight: 500;
|
||||
font-size: 1.4rem;
|
||||
color: #6B869C;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08rem;
|
||||
|
||||
flex-shrink: 0; /* Header bleibt fixiert */
|
||||
}
|
||||
|
||||
.dest-mass-create-table {
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
padding-bottom: 2.4rem;
|
||||
}
|
||||
|
||||
.dest-mass-create-table-header-material {
|
||||
width: 14rem;
|
||||
}
|
||||
|
||||
.dest-mass-create-table-header-supplier {
|
||||
width: 24rem;
|
||||
}
|
||||
|
||||
.dest-mass-create-table-header-dest {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
width: 15rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
<template>
|
||||
<div class="dest-mass-create-row-container">
|
||||
<div class="dest-mass-create-row-material"><ph-package size="24"/> {{ row.material }}</div>
|
||||
<div class="dest-mass-create-row-supplier">
|
||||
<ph-factory size="24"/>{{ row.supplier }}
|
||||
</div>
|
||||
<div v-for="dest in row.destinations" class="dest-mass-create-row-dest" :key="dest.id">
|
||||
<checkbox :checked="dest.selected" @checkbox-changed="updateCheckbox($event, dest.id)">
|
||||
</checkbox>
|
||||
<!-- <basic-badge variant="secondary">{{ toNode(dest, 15) }}</basic-badge>-->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
import {PhFactory, PhPackage} from "@phosphor-icons/vue";
|
||||
import BasicBadge from "@/components/UI/BasicBadge.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {useDestinationEditStore} from "@/store/destinationEdit.js";
|
||||
|
||||
export default {
|
||||
name: "DestMassCreateRow",
|
||||
components: {PhFactory, BasicBadge, PhPackage, Checkbox},
|
||||
emits: ['update-selected'],
|
||||
props: {
|
||||
row: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useDestinationEditStore)
|
||||
|
||||
},
|
||||
methods: {
|
||||
updateCheckbox(value, destId) {
|
||||
this.row.destinations.find(d => d.id === destId).selected = value;
|
||||
this.$emit('update-selected',{id: this.row.id, dest: destId, selected: value});
|
||||
},
|
||||
toNode(node, limit = 5) {
|
||||
if (!node)
|
||||
return 'N/A';
|
||||
|
||||
const name = node.name;
|
||||
const mappingId = node.external_mapping_id;
|
||||
const needsShortName = name.length > limit;
|
||||
const useMappingId = ((mappingId ?? null) !== null) && ((name ?? null) === null || needsShortName);
|
||||
const shortName = name?.substring(0, limit).concat("...") ?? 'N/A';
|
||||
return `${useMappingId ? mappingId.replace("_", " ") : (needsShortName ? shortName : name)}`;
|
||||
|
||||
},
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.dest-mass-create-row-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1.6rem;
|
||||
padding: 1.6rem 2.4rem;
|
||||
justify-content: flex-start;
|
||||
border-bottom: 0.16rem solid #f3f4f6;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.dest-mass-create-row-container:hover {
|
||||
background-color: rgba(107, 134, 156, 0.05);
|
||||
}
|
||||
|
||||
.dest-mass-create-row-material {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.8rem;
|
||||
width: 14rem;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.dest-mass-create-row-supplier {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.8rem;
|
||||
width: 24rem;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.dest-mass-create-row-dest {
|
||||
width: 15rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -1,137 +0,0 @@
|
|||
<template>
|
||||
<div class="destination-edit-container">
|
||||
<tab-container :default-tab="defaultTab" :tabs="tabsConfig" class="tab-container">
|
||||
</tab-container>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import TabContainer from "@/components/UI/TabContainer.vue";
|
||||
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||
import {markRaw} from "vue";
|
||||
import DestinationMassQuantity from "@/components/layout/edit/destination/mass/DestinationMassQuantity.vue";
|
||||
import DestinationMassRoute from "@/components/layout/edit/destination/mass/DestinationMassRoute.vue";
|
||||
import DestinationMassHandlingCost from "@/components/layout/edit/destination/mass/DestinationMassHandlingCost.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {useNotificationStore} from "@/store/notification.js";
|
||||
|
||||
export default {
|
||||
name: "DestinationMassEdit",
|
||||
components: {BasicButton, TabContainer},
|
||||
props: {
|
||||
premiseIds: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentTab: null,
|
||||
isLoading: [false, false, false],
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.updateSpinner();
|
||||
},
|
||||
methods: {
|
||||
handleTabLoadingQuantity(loading) {
|
||||
this.isLoading[0] = loading;
|
||||
this.updateSpinner();
|
||||
},
|
||||
handleTabLoadingHandling(loading) {
|
||||
this.isLoading[1] = loading;
|
||||
this.updateSpinner();
|
||||
},
|
||||
handleTabLoadingRoutes(loading) {
|
||||
this.isLoading[2] = loading;
|
||||
this.updateSpinner();
|
||||
},
|
||||
updateSpinner() {
|
||||
if (this.isLoading[0] || this.isLoading[1] || this.isLoading[2]) {
|
||||
this.notificationStore.setSpinner("Processing ...");
|
||||
|
||||
}
|
||||
else {
|
||||
this.notificationStore.clearSpinner();
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useNotificationStore),
|
||||
defaultTab() {
|
||||
return this.tabsConfig.indexOf(this.tabsConfig.find(t => t.matchType === this.type)) ?? 0;
|
||||
},
|
||||
tabsConfig() {
|
||||
return [
|
||||
{
|
||||
title: 'Annual quantity',
|
||||
component: markRaw(DestinationMassQuantity),
|
||||
props: {premiseIds: this.premiseIds, onLoadingChange: this.handleTabLoadingQuantity},
|
||||
matchType: 'amount',
|
||||
|
||||
},
|
||||
{
|
||||
title: 'Handling & Repackaging',
|
||||
component: markRaw(DestinationMassHandlingCost),
|
||||
props: {premiseIds: this.premiseIds, onLoadingChange: this.handleTabLoadingHandling},
|
||||
matchType: 'handling',
|
||||
|
||||
},
|
||||
{
|
||||
title: 'Routes',
|
||||
component: markRaw(DestinationMassRoute),
|
||||
props: {premiseIds: this.premiseIds, onLoadingChange: this.handleTabLoadingRoutes},
|
||||
matchType: 'routes',
|
||||
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.tab-container {
|
||||
flex: 1;
|
||||
min-height: 0; /* Critical: allows flex child to shrink below content size */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.destination-edit-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
min-width: 100rem;
|
||||
width: 90vw;
|
||||
height: 70vh; /* Feste Höhe statt max-height */
|
||||
min-height: 50rem; /* Mindesthöhe für kleine Bildschirme */
|
||||
max-height: 90rem; /* Maximale Höhe für große Bildschirme */
|
||||
}
|
||||
|
||||
|
||||
.destination-edit-modal-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
flex: 1 0 min(60vw, 120rem);
|
||||
height: min(60vh, 120rem);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.destination-edit-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1.6rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
|
@ -1,466 +0,0 @@
|
|||
<template>
|
||||
<div class="dest-mass-handling-container"
|
||||
:class="{ 'has-selection': hasSelection, 'apply-filter': applyFilter, 'add-all': addAll }">
|
||||
<div>
|
||||
<div class="destination-mass-handling-cost-info">
|
||||
<ph-warning size="18px"></ph-warning>
|
||||
Handling and repackaging costs are calculated automatically.
|
||||
If needed, you can overwrite these values here.
|
||||
</div>
|
||||
</div>
|
||||
<div class="destination-mass-handling-checkbox">
|
||||
<checkbox :checked="handlingCostActive" @checkbox-changed="activateInputFields">I want to enter handling and
|
||||
repackaging costs manually.
|
||||
</checkbox>
|
||||
</div>
|
||||
<div class="dest-mass-handling-table-wrapper" v-if="handlingCostActive">
|
||||
<div class="dest-mass-handling-table-header">
|
||||
<div class="dest-mass-handling-table-header-checkbox">
|
||||
<checkbox @checkbox-changed="updateCheckBoxes" :checked="overallCheck"
|
||||
:indeterminate="overallIndeterminate"></checkbox>
|
||||
</div>
|
||||
<div class="dest-mass-handling-table-header-material">Material</div>
|
||||
<div class="dest-mass-handling-table-header-supplier">Supplier</div>
|
||||
<div class="dest-mass-handling-table-header-destination">Destination</div>
|
||||
<div class="dest-mass-handling-table-header-applier">
|
||||
<icon-button icon="check" :disabled="!someChecked" @click="updateOverallValue"></icon-button>
|
||||
<icon-button icon="x" :disabled="!someChecked" @click="dismissChecked"></icon-button>
|
||||
</div>
|
||||
<div class="dest-mass-handling-table-header-costs">
|
||||
<div>Handling costs</div>
|
||||
<div class="text-container" :class="{disabled: !someChecked}">
|
||||
<input class="input-field"
|
||||
v-model="overallHandlingCostValue"
|
||||
autocomplete="off"
|
||||
@blur="validateHandlingCost($event, 'handling')"
|
||||
:disabled="!someChecked"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dest-mass-handling-table-header-costs">
|
||||
<div>Repackaging cost</div>
|
||||
<div class="text-container" :class="{disabled: !someChecked}">
|
||||
<input class="input-field"
|
||||
v-model="overallRepackagingCostValue"
|
||||
autocomplete="off"
|
||||
@blur="validateHandlingCost($event, 'repackaging')"
|
||||
:disabled="!someChecked"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dest-mass-handling-table-header-costs">
|
||||
<div>Disposal costs</div>
|
||||
<div class="text-container" :class="{disabled: !someChecked}">
|
||||
<input class="input-field"
|
||||
v-model="overallDisposalCostValue"
|
||||
autocomplete="off"
|
||||
@blur="validateHandlingCost($event, 'disposal')"
|
||||
:disabled="!someChecked"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dest-mass-handling-table">
|
||||
<destination-mass-handling-cost-row @action="onClickAction" @update-selected="updateCheckBox" :row="row"
|
||||
:key="row.id"
|
||||
:disabled="someChecked"
|
||||
v-for="row in rows"></destination-mass-handling-cost-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapStores} from "pinia";
|
||||
import {useDestinationEditStore} from "@/store/destinationEdit.js";
|
||||
import {usePremiseEditStore} from "@/store/premiseEdit.js";
|
||||
import DestinationMassQuantityRow from "@/components/layout/edit/destination/mass/DestinationMassQuantityRow.vue";
|
||||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import DestinationMassHandlingCostRow
|
||||
from "@/components/layout/edit/destination/mass/DestinationMassHandlingCostRow.vue";
|
||||
import {parseNumberFromString} from "@/common.js";
|
||||
|
||||
export default {
|
||||
name: "DestinationMassHandlingCost",
|
||||
components: {DestinationMassHandlingCostRow, IconButton, Checkbox, DestinationMassQuantityRow},
|
||||
props: {
|
||||
premiseIds: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
onLoadingChange: {
|
||||
type: Function,
|
||||
default: () => {
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
handlingCostActive: false,
|
||||
overallDisposalCostValue: null,
|
||||
overallRepackagingCostValue: null,
|
||||
overallHandlingCostValue: null,
|
||||
overallCheck: false,
|
||||
overallIndeterminate: false,
|
||||
isCtrlPressed: false,
|
||||
isShiftPressed: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useDestinationEditStore, usePremiseEditStore),
|
||||
rows() {
|
||||
return this.destinationEditStore.getHandlingCostMatrix ?? [];
|
||||
},
|
||||
allChecked() {
|
||||
return this.rows.every(r => r.selected);
|
||||
},
|
||||
someChecked() {
|
||||
return this.rows.some(r => r.selected);
|
||||
},
|
||||
hasSelection() {
|
||||
return !this.addAll && !this.applyFilter && this.someChecked;
|
||||
},
|
||||
applyFilter() {
|
||||
return this.isCtrlPressed && this.isShiftPressed;
|
||||
},
|
||||
addAll() {
|
||||
return this.isCtrlPressed && !this.isShiftPressed;
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.onLoadingChange(true);
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(() => {
|
||||
this.buildMatrix();
|
||||
resolve();
|
||||
}, 10));
|
||||
} finally {
|
||||
this.onLoadingChange(false);
|
||||
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('keydown', this.handleKeyDown);
|
||||
window.addEventListener('keyup', this.handleKeyUp);
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener('keydown', this.handleKeyDown);
|
||||
window.removeEventListener('keyup', this.handleKeyUp);
|
||||
},
|
||||
methods: {
|
||||
|
||||
activateInputFields(value) {
|
||||
this.handlingCostActive = value;
|
||||
},
|
||||
validateHandlingCost(event, type) {
|
||||
const value = event.target.value == null ? null : parseNumberFromString(event.target.value, 2);
|
||||
const validatedValue = value == null ? null : Math.max(0, value);
|
||||
const stringified = validatedValue === null ? '' : validatedValue.toFixed();
|
||||
|
||||
|
||||
if (type === 'handling')
|
||||
this.overallHandlingCostValue = validatedValue;
|
||||
else if (type === 'repackaging')
|
||||
this.overallRepackagingCostValue = validatedValue;
|
||||
else if (type === 'disposal')
|
||||
this.overallDisposalCostValue = validatedValue;
|
||||
|
||||
event.target.value = stringified;
|
||||
},
|
||||
onClickAction(data) {
|
||||
|
||||
this.rows.forEach(d => {
|
||||
d.selected = ((data.column === 'material' && d.material === data.row.material)
|
||||
|| (data.column === 'supplier' && d.supplierId === data.row.supplierId)
|
||||
|| (data.column === 'destination' && d.destinationNodeId === data.row.destinationNodeId)
|
||||
|| (data.action === 'append' && d.selected));
|
||||
});
|
||||
|
||||
this.updateOverallCheckBox();
|
||||
},
|
||||
|
||||
|
||||
/* key down/up handler */
|
||||
handleKeyDown(event) {
|
||||
if (event.key === 'Control') {
|
||||
this.isCtrlPressed = true;
|
||||
} else if (event.key === 'Shift') {
|
||||
this.isShiftPressed = true;
|
||||
}
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
this.fillData(this.modalType);
|
||||
this.modalType = null;
|
||||
}
|
||||
|
||||
},
|
||||
handleKeyUp(event) {
|
||||
if (event.key === 'Control') {
|
||||
this.isCtrlPressed = false;
|
||||
} else if (event.key === 'Shift') {
|
||||
this.isShiftPressed = false;
|
||||
}
|
||||
},
|
||||
|
||||
updateOverallValue() {
|
||||
|
||||
if (this.overallHandlingCostValue !== null || this.overallDisposalCostValue !== null || this.overallRepackagingCostValue !== null) {
|
||||
|
||||
this.rows
|
||||
.filter(row => row.selected)
|
||||
.forEach(row => {
|
||||
row.handling_costs = this.overallHandlingCostValue ?? row.handling_costs;
|
||||
row.disposal_costs = this.overallDisposalCostValue ?? row.disposal_costs;
|
||||
row.repackaging_costs = this.overallRepackagingCostValue ?? row.repackaging_costs;
|
||||
});
|
||||
|
||||
this.overallHandlingCostValue = null;
|
||||
this.overallRepackagingCostValue = null;
|
||||
this.overallDisposalCostValue = null;
|
||||
|
||||
this.$forceUpdate();
|
||||
}
|
||||
|
||||
this.dismissChecked();
|
||||
},
|
||||
dismissChecked() {
|
||||
this.rows.forEach(row => row.selected = false);
|
||||
this.updateOverallCheckBox();
|
||||
},
|
||||
|
||||
/* checkbox handling */
|
||||
|
||||
updateCheckBox(data) { // data = {id: this.row.id, selected: value}
|
||||
// update global (rest is done in row)
|
||||
this.updateOverallCheckBox();
|
||||
},
|
||||
updateCheckBoxes(value) {
|
||||
this.rows?.forEach(r => r.selected = value);
|
||||
this.updateOverallCheckBox();
|
||||
|
||||
},
|
||||
updateOverallCheckBox() {
|
||||
this.overallCheck = this.rows.every(r => r.selected);
|
||||
|
||||
if (!this.overallCheck)
|
||||
this.overallIndeterminate = this.rows.some(r => r.selected);
|
||||
|
||||
},
|
||||
toNode(node, limit = 5) {
|
||||
if (!node)
|
||||
return 'N/A';
|
||||
|
||||
const name = node.name;
|
||||
const mappingId = node.external_mapping_id;
|
||||
const needsShortName = name.length > limit;
|
||||
const useMappingId = ((mappingId ?? null) !== null) && ((name ?? null) === null || needsShortName);
|
||||
const shortName = name?.substring(0, limit).concat("...") ?? 'N/A';
|
||||
return `${useMappingId ? mappingId.replace("_", " ") : (needsShortName ? shortName : name)}`;
|
||||
|
||||
},
|
||||
async buildMatrix() {
|
||||
const handlingCostMatrix = [];
|
||||
|
||||
for (const pId of this.premiseIds) {
|
||||
const premise = this.premiseEditStore.getById(pId);
|
||||
const destinations = this.destinationEditStore.getByPremiseId(pId);
|
||||
if (!destinations) continue;
|
||||
|
||||
for (const d of destinations) {
|
||||
handlingCostMatrix.push({
|
||||
id: premise.id,
|
||||
material: premise.material.part_number,
|
||||
supplier: this.toNode(premise.supplier, 15),
|
||||
supplierId: premise.supplier.id,
|
||||
supplierIso: premise.supplier.country.iso_code,
|
||||
destinationId: d.id,
|
||||
destinationNodeId: d.destination_node.id,
|
||||
destination: this.toNode(d.destination_node, 15),
|
||||
repackaging_costs: d.repackaging_costs,
|
||||
handling_costs: d.handling_costs,
|
||||
disposal_costs: d.disposal_costs,
|
||||
selected: false
|
||||
});
|
||||
|
||||
this.handlingCostActive = ((d.handling_costs !== null) || d.repackaging_costs !== null || d.disposal_costs !== null) || this.handlingCostActive;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
this.destinationEditStore.setHandlingCostMatrix(handlingCostMatrix);
|
||||
await this.$nextTick();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.dest-mass-handling-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
gap: 2.4rem;
|
||||
}
|
||||
|
||||
.destination-mass-handling-cost-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.4rem;
|
||||
gap: 1.6rem;
|
||||
background-color: #c3cfdf;
|
||||
color: #002F54;
|
||||
border-radius: 0.8rem;
|
||||
padding: 1.6rem;
|
||||
margin: 1.6rem 1.6rem 0 1.6rem;
|
||||
}
|
||||
|
||||
.destination-mass-handling-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.6rem;
|
||||
padding: 0 1.6rem;
|
||||
}
|
||||
|
||||
|
||||
/* Global style für copy-mode cursor */
|
||||
.dest-mass-handling-container.has-selection :deep(.dest-mass-handling-row__cell--copyable:hover) {
|
||||
cursor: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDEyOC41MSAxMzQuMDUiPjxkZWZzPjxzdHlsZT4uY3tmaWxsOm5vbmU7fS5jLC5ke3N0cm9rZTojMDEwMTAxO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2Utd2lkdGg6NXB4O30uZHtmaWxsOiNmZmY7fTwvc3R5bGU+PC9kZWZzPjxnIGlkPSJhIj48cGF0aCBjbGFzcz0iYyIgZD0ibTU0Ljg5LDExMi41MWgtMi4yNGMtMS4yNCwwLTIuMjQtMS0yLjI0LTIuMjR2LTIuMjQiLz48bGluZSBjbGFzcz0iYyIgeDE9IjcwLjU3IiB5MT0iNzYuNjciIHgyPSI2My44NSIgeTI9Ijc2LjY3Ii8+PGxpbmUgY2xhc3M9ImMiIHgxPSI3MC41NyIgeTE9IjExMi41MSIgeDI9IjY2LjA5IiB5Mj0iMTEyLjUxIi8+PGxpbmUgY2xhc3M9ImMiIHgxPSI4Ni4yNSIgeTE9Ijk5LjA3IiB4Mj0iODYuMjUiIHkyPSI5Mi4zNSIvPjxsaW5lIGNsYXNzPSJjIiB4MT0iNTAuNDEiIHkxPSI5Ni44MyIgeDI9IjUwLjQxIiB5Mj0iOTIuMzUiLz48cGF0aCBjbGFzcz0iYyIgZD0ibTgxLjc3LDExMi41MWgyLjI0YzEuMjQsMCwyLjI0LTEsMi4yNC0yLjI0di0yLjI0Ii8+PHBhdGggY2xhc3M9ImMiIGQ9Im04MS43Nyw3Ni42N2gyLjI0YzEuMjQsMCwyLjI0LDEsMi4yNCwyLjI0djIuMjQiLz48cGF0aCBjbGFzcz0iYyIgZD0ibTU0Ljg5LDc2LjY3aC0yLjI0Yy0xLjI0LDAtMi4yNCwxLTIuMjQsMi4yNHYyLjI0Ii8+PHBhdGggY2xhc3M9ImMiIGQ9Im04Ni4yNSw5OS4wN2gxMS4yYzEuMjQsMCwyLjI0LTEsMi4yNC0yLjI0di0zMS4zNmMwLTEuMjQtMS0yLjI0LTIuMjQtMi4yNGgtMzEuMzZjLTEuMjQsMC0yLjI0LDEtMi4yNCwyLjI0djExLjIiLz48L2c+PGcgaWQ9ImIiPjxwYXRoIGNsYXNzPSJkIiBkPSJtNDQuMDgsNDQuMDdsMzIuOTQtOS4yYzEuNjktLjUyLDIuNjQtMi4zMSwyLjEyLTQtLjMtLjk4LTEuMDUtMS43NS0yLjAxLTIuMDlMNi43MywyLjY3Yy0xLjY3LS41Ny0zLjQ5LjMzLTQuMDYsMi0uMjMuNjYtLjIzLDEuMzgsMCwyLjA1bDI2LjExLDcwLjRjLjU4LDEuNjcsMi40LDIuNTYsNC4wNywxLjk4Ljk3LS4zMywxLjcxLTEuMTEsMi4wMS0yLjA5bDkuMjItMzIuOTRaIi8+PC9nPjwvc3ZnPg==") 12 12, pointer;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
||||
/* Global style für filter-mode cursor */
|
||||
.dest-mass-handling-container.add-all :deep(.dest-mass-handling-row__cell--filterable:hover) {
|
||||
cursor: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDEyOC41MSAxMzQuMDUiPjxkZWZzPjxzdHlsZT4uY3tmaWxsOm5vbmU7fS5jLC5ke3N0cm9rZTojMDEwMTAxO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2Utd2lkdGg6NXB4O30uZHtmaWxsOiNmZmY7fTwvc3R5bGU+PC9kZWZzPjxnIGlkPSJhIj48bGluZSBjbGFzcz0iYyIgeDE9IjczLjAzIiB5MT0iNzUuNTUiIHgyPSI2Ni4zOCIgeTI9Ijc1LjU1Ii8+PGxpbmUgY2xhc3M9ImMiIHgxPSI2Ni4zOCIgeTE9IjExMi4xNiIgeDI9IjczLjAzIiB5Mj0iMTEyLjE2Ii8+PHBhdGggY2xhc3M9ImMiIGQ9Im04MS4zNSw3NS41NWg0Ljk5Yy45MiwwLDEuNjYuNzUsMS42NiwxLjY2djQuOTkiLz48bGluZSBjbGFzcz0iYyIgeDE9Ijg4LjAxIiB5MT0iOTcuMTgiIHgyPSI4OC4wMSIgeTI9IjkwLjUzIi8+PHBhdGggY2xhc3M9ImMiIGQ9Im04MS4zNSwxMTIuMTZoNC45OWMuOTIsMCwxLjY2LS43NSwxLjY2LTEuNjZ2LTQuOTkiLz48bGluZSBjbGFzcz0iYyIgeDE9IjUxLjQiIHkxPSI5MC41MyIgeDI9IjUxLjQiIHkyPSI5Ny4xOCIvPjxwYXRoIGNsYXNzPSJjIiBkPSJtNTguMDUsMTEyLjE2aC00Ljk5Yy0uOTIsMC0xLjY2LS43NS0xLjY2LTEuNjZ2LTQuOTkiLz48cGF0aCBjbGFzcz0iYyIgZD0ibTU4LjA1LDc1LjU1aC00Ljk5Yy0uOTIsMC0xLjY2Ljc1LTEuNjYsMS42NnY0Ljk5Ii8+PC9nPjxnIGlkPSJiIj48cGF0aCBjbGFzcz0iZCIgZD0ibTQ0LjA4LDQ0LjA3bDMyLjk0LTkuMmMxLjY5LS41MiwyLjY0LTIuMzEsMi4xMi00LS4zLS45OC0xLjA1LTEuNzUtMi4wMS0yLjA5TDYuNzMsMi42N2MtMS42Ny0uNTctMy40OS4zMy00LjA2LDItLjIzLjY2LS4yMywxLjM4LDAsMi4wNWwyNi4xMSw3MC40Yy41OCwxLjY3LDIuNCwyLjU2LDQuMDcsMS45OC45Ny0uMzMsMS43MS0xLjExLDIuMDEtMi4wOWw5LjIyLTMyLjk0WiIvPjwvZz48L3N2Zz4=") 12 12, pointer;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
||||
/* Global style für filter-mode cursor */
|
||||
.dest-mass-handling-container.apply-filter :deep(.dest-mass-handling-row__cell--filterable:hover) {
|
||||
cursor: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDEyOC41MSAxMzQuMDUiPjxkZWZzPjxzdHlsZT4uZHtzdHJva2U6IzAwMDt9LmQsLmV7ZmlsbDpub25lO30uZCwuZSwuZntzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLXdpZHRoOjVweDt9LmUsLmZ7c3Ryb2tlOiMwMTAxMDE7fS5me2ZpbGw6I2ZmZjt9PC9zdHlsZT48L2RlZnM+PGcgaWQ9ImEiPjxsaW5lIGNsYXNzPSJlIiB4MT0iNzMuMDMiIHkxPSI3NS41NSIgeDI9IjY2LjM4IiB5Mj0iNzUuNTUiLz48bGluZSBjbGFzcz0iZSIgeDE9IjY2LjM4IiB5MT0iMTEyLjE2IiB4Mj0iNzMuMDMiIHkyPSIxMTIuMTYiLz48cGF0aCBjbGFzcz0iZSIgZD0ibTgxLjM1LDc1LjU1aDQuOTljLjkyLDAsMS42Ni43NSwxLjY2LDEuNjZ2NC45OSIvPjxsaW5lIGNsYXNzPSJlIiB4MT0iODguMDEiIHkxPSI5Ny4xOCIgeDI9Ijg4LjAxIiB5Mj0iOTAuNTMiLz48cGF0aCBjbGFzcz0iZSIgZD0ibTgxLjM1LDExMi4xNmg0Ljk5Yy45MiwwLDEuNjYtLjc1LDEuNjYtMS42NnYtNC45OSIvPjxsaW5lIGNsYXNzPSJlIiB4MT0iNTEuNCIgeTE9IjkwLjUzIiB4Mj0iNTEuNCIgeTI9Ijk3LjE4Ii8+PHBhdGggY2xhc3M9ImUiIGQ9Im01OC4wNSwxMTIuMTZoLTQuOTljLS45MiwwLTEuNjYtLjc1LTEuNjYtMS42NnYtNC45OSIvPjxwYXRoIGNsYXNzPSJlIiBkPSJtNTguMDUsNzUuNTVoLTQuOTljLS45MiwwLTEuNjYuNzUtMS42NiwxLjY2djQuOTkiLz48L2c+PGcgaWQ9ImIiPjxwYXRoIGNsYXNzPSJmIiBkPSJtNDQuMDgsNDQuMDdsMzIuOTQtOS4yYzEuNjktLjUyLDIuNjQtMi4zMSwyLjEyLTQtLjMtLjk4LTEuMDUtMS43NS0yLjAxLTIuMDlMNi43MywyLjY3Yy0xLjY3LS41Ny0zLjQ5LjMzLTQuMDYsMi0uMjMuNjYtLjIzLDEuMzgsMCwyLjA1bDI2LjExLDcwLjRjLjU4LDEuNjcsMi40LDIuNTYsNC4wNywxLjk4Ljk3LS4zMywxLjcxLTEuMTEsMi4wMS0yLjA5bDkuMjItMzIuOTRaIi8+PC9nPjxnIGlkPSJjIj48bGluZSBjbGFzcz0iZCIgeDE9Ijk5LjM4IiB5MT0iOTQuMTkiIHgyPSIxMjYuMDEiIHkyPSI5NC4xOSIvPjxsaW5lIGNsYXNzPSJkIiB4MT0iMTEyLjY5IiB5MT0iODAuODciIHgyPSIxMTIuNjkiIHkyPSIxMDcuNTEiLz48L2c+PC9zdmc+") 12 12, pointer;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
||||
.text-container.disabled {
|
||||
background-color: #f3f4f6;
|
||||
cursor: not-allowed;
|
||||
border-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.text-container.disabled input {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
||||
.text-container:hover:not(.disabled) {
|
||||
background: #EEF4FF;
|
||||
border: 0.2rem solid #8DB3FE;
|
||||
transform: scale(1.01);
|
||||
|
||||
}
|
||||
|
||||
|
||||
.input-field {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: none;
|
||||
resize: none;
|
||||
font-family: inherit;
|
||||
font-size: 1.4rem;
|
||||
color: #002F54;
|
||||
|
||||
max-width: 6rem;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);*/
|
||||
border: 0.2rem solid #E3EDFF;
|
||||
transition: all 0.1s ease;
|
||||
flex: 1 0 auto;
|
||||
|
||||
max-width: 8rem;
|
||||
|
||||
}
|
||||
|
||||
.dest-mass-handling-table-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
min-height: 0; /* Wichtig für Firefox */
|
||||
}
|
||||
|
||||
.dest-mass-handling-table-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1.6rem;
|
||||
padding: 1.6rem 2.4rem;
|
||||
justify-content: flex-start;
|
||||
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid rgba(107, 134, 156, 0.2);
|
||||
font-weight: 500;
|
||||
font-size: 1.4rem;
|
||||
color: #6B869C;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08rem;
|
||||
|
||||
flex-shrink: 0; /* Header bleibt fixiert */
|
||||
}
|
||||
|
||||
.dest-mass-handling-table {
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
padding-bottom: 2.4rem;
|
||||
}
|
||||
|
||||
.dest-mass-handling-table-header-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
|
||||
.dest-mass-handling-table-header-material {
|
||||
width: 14rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dest-mass-handling-table-header-supplier {
|
||||
width: 18rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dest-mass-handling-table-header-destination {
|
||||
width: 18rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dest-mass-handling-table-header-applier {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 7rem;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.dest-mass-handling-table-header-costs {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
width: 25rem;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -1,294 +0,0 @@
|
|||
<template>
|
||||
<div class="dest-mass-handling-row-container" @wheel="handleWheel">
|
||||
<div class="dest-mass-handling-row-checkbox">
|
||||
<checkbox :checked="row.selected" @checkbox-changed="updateCheckbox">
|
||||
</checkbox>
|
||||
</div>
|
||||
<div class="dest-mass-handling-row-material dest-mass-handling-row__cell--filterable"
|
||||
@click="action($event,'material')"
|
||||
@mousedown="handleMouseDown">
|
||||
<ph-package size="24"/>
|
||||
{{ row.material }}
|
||||
</div>
|
||||
<div class="dest-mass-handling-row-supplier dest-mass-handling-row__cell--filterable"
|
||||
@click="action($event,'supplier')"
|
||||
@mousedown="handleMouseDown">
|
||||
<flag :iso="row.supplierIso" />
|
||||
{{ row.supplier }}
|
||||
</div>
|
||||
<div class="dest-mass-handling-row-destination dest-mass-handling-row__cell--filterable"
|
||||
@click="action($event,'destination')"
|
||||
@mousedown="handleMouseDown">
|
||||
<ph-map-pin size="24"/>
|
||||
{{ row.destination }}
|
||||
</div>
|
||||
<div class="dest-mass-handling-row-applier"></div>
|
||||
<div class="dest-mass-handling-row-costs">
|
||||
|
||||
<div class="text-container" :class="{disabled: disabled}">
|
||||
<input class="input-field"
|
||||
v-model="handling"
|
||||
@blur="validateHandlingCost($event, 'handling')"
|
||||
autocomplete="off"
|
||||
:disabled="disabled"/>
|
||||
</div>
|
||||
<div>[EUR/HU]</div>
|
||||
</div>
|
||||
<div class="dest-mass-handling-row-costs">
|
||||
|
||||
<div class="text-container" :class="{disabled: disabled}">
|
||||
<input class="input-field"
|
||||
v-model="repackaging"
|
||||
@blur="validateHandlingCost($event, 'repackaging')"
|
||||
autocomplete="off"
|
||||
:disabled="disabled"/>
|
||||
</div>
|
||||
<div>[EUR/HU]</div>
|
||||
</div>
|
||||
<div class="dest-mass-handling-row-costs">
|
||||
|
||||
<div class="text-container" :class="{disabled: disabled}">
|
||||
<input class="input-field"
|
||||
v-model="disposal"
|
||||
@blur="validateHandlingCost($event, 'disposal')"
|
||||
autocomplete="off"
|
||||
:disabled="disabled"/>
|
||||
</div>
|
||||
<div>[EUR/HU]</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
import {PhFactory, PhMapPin} from "@phosphor-icons/vue";
|
||||
import {parseNumberFromString} from "@/common.js";
|
||||
import Flag from "@/components/UI/Flag.vue";
|
||||
|
||||
export default {
|
||||
name: "DestinationMassHandlingCostRow",
|
||||
components: {Flag, PhMapPin, PhFactory, Checkbox},
|
||||
emits: ['action', 'update-selected'],
|
||||
props: {
|
||||
row: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
repackaging: {
|
||||
get() {
|
||||
return this.row.repackaging_costs?.toFixed(2) ?? '';
|
||||
},
|
||||
set(value) {
|
||||
this.row.repackaging_costs && (this.row.repackaging_costs = value);
|
||||
},
|
||||
},
|
||||
handling: {
|
||||
get() {
|
||||
return this.row.handling_costs?.toFixed(2) ?? '';
|
||||
},
|
||||
set(value) {
|
||||
this.row.handling_costs && (this.row.handling_costs = value);
|
||||
},
|
||||
},
|
||||
disposal: {
|
||||
get() {
|
||||
return this.row.disposal_costs?.toFixed(2) ?? '';
|
||||
},
|
||||
set(value) {
|
||||
this.row.disposal_costs && (this.row.disposal_costs = value);
|
||||
},
|
||||
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleMouseDown(event) {
|
||||
if (event.shiftKey || event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
handleWheel(event) {
|
||||
if (event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
window.scrollBy(0, event.deltaY);
|
||||
}
|
||||
},
|
||||
action(event, column) {
|
||||
|
||||
if (event.ctrlKey && !event.shiftKey && (column === 'material' || column === 'supplier' || column === 'destination')) {
|
||||
this.$emit('action', {row: this.row, column: column, action: 'filter'});
|
||||
} else if (event.ctrlKey && event.shiftKey && (column === 'material' || column === 'supplier' || column === 'destination')) {
|
||||
this.$emit('action', {row: this.row, column: column, action: 'append'});
|
||||
}
|
||||
|
||||
},
|
||||
updateCheckbox(value) {
|
||||
this.row.selected = value;
|
||||
this.$emit('update-selected', {id: this.row.id, selected: value});
|
||||
},
|
||||
validateHandlingCost(event, type) {
|
||||
const value = event.target.value == null ? null : parseNumberFromString(event.target.value, 2);
|
||||
const validatedValue = value == null ? null : Math.max(0, value);
|
||||
const stringified = validatedValue === null ? '' : validatedValue.toFixed();
|
||||
|
||||
if(type === 'handling')
|
||||
this.row.handling_costs = validatedValue;
|
||||
else if(type === 'repackaging')
|
||||
this.row.repackaging_costs = validatedValue;
|
||||
else if(type === 'disposal')
|
||||
this.row = validatedValue;
|
||||
|
||||
event.target.value = stringified;
|
||||
},
|
||||
|
||||
toNode(node, limit = 5) {
|
||||
if (!node)
|
||||
return 'N/A';
|
||||
|
||||
const name = node.name;
|
||||
const mappingId = node.external_mapping_id;
|
||||
const needsShortName = name.length > limit;
|
||||
const useMappingId = ((mappingId ?? null) !== null) && ((name ?? null) === null || needsShortName);
|
||||
const shortName = name?.substring(0, limit).concat("...") ?? 'N/A';
|
||||
return `${useMappingId ? mappingId.replace("_", " ") : (needsShortName ? shortName : name)}`;
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.input-field {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: none;
|
||||
resize: none;
|
||||
font-family: inherit;
|
||||
font-size: 1.4rem;
|
||||
color: #002F54;
|
||||
|
||||
max-width: 6rem;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);*/
|
||||
border: 0.2rem solid #E3EDFF;
|
||||
transition: all 0.1s ease;
|
||||
flex: 1 0 auto;
|
||||
|
||||
max-width: 8rem;
|
||||
|
||||
}
|
||||
|
||||
.text-container:hover {
|
||||
background: #EEF4FF;
|
||||
border: 0.2rem solid #8DB3FE;
|
||||
/*transform: translateY(2px);*/
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
|
||||
.text-container.disabled {
|
||||
background-color: #f3f4f6;
|
||||
cursor: not-allowed;
|
||||
border-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.text-container.disabled input {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
||||
.text-container:hover:not(.disabled) {
|
||||
background: #EEF4FF;
|
||||
border: 0.2rem solid #8DB3FE;
|
||||
transform: scale(1.01);
|
||||
|
||||
}
|
||||
|
||||
|
||||
.dest-mass-handling-row-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1.6rem;
|
||||
padding: 1.2rem 2.4rem;
|
||||
justify-content: flex-start;
|
||||
border-bottom: 0.16rem solid #f3f4f6;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.dest-mass-handling-row-container:hover {
|
||||
background-color: rgba(107, 134, 156, 0.05);
|
||||
}
|
||||
|
||||
.dest-mass-handling-row-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.dest-mass-handling-row-material {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.8rem;
|
||||
width: 14rem;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.dest-mass-handling-row-applier {
|
||||
width: 7rem;
|
||||
}
|
||||
|
||||
.dest-mass-handling-row-supplier {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.8rem;
|
||||
width: 18rem;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.dest-mass-handling-row-destination {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.8rem;
|
||||
width: 18rem;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.dest-mass-handling-row-costs {
|
||||
width: 25rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.8rem;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -1,428 +0,0 @@
|
|||
<template>
|
||||
<div class="dest-mass-quantity-container"
|
||||
:class="{ 'has-selection': hasSelection, 'apply-filter': applyFilter, 'add-all': addAll }">
|
||||
<div class="dest-mass-quantity-table-wrapper">
|
||||
<div class="dest-mass-quantity-table-header">
|
||||
<div class="dest-mass-quantity-table-header-checkbox">
|
||||
<checkbox @checkbox-changed="updateCheckBoxes" :checked="overallCheck"
|
||||
:indeterminate="overallIndeterminate"></checkbox>
|
||||
</div>
|
||||
<div class="dest-mass-quantity-table-header-material">Material</div>
|
||||
<div class="dest-mass-quantity-table-header-supplier">Supplier</div>
|
||||
<div class="dest-mass-quantity-table-header-applier">
|
||||
<icon-button icon="check" :disabled="!someChecked" @click="updateOverallValue"></icon-button>
|
||||
<icon-button icon="x" :disabled="!someChecked" @click="dismissChecked"></icon-button>
|
||||
</div>
|
||||
<div class="dest-mass-quantity-table-header-dest"
|
||||
:key="`${dest.id}`"
|
||||
v-for="dest in destPool">
|
||||
<div>{{ toNode(dest, 6) }}</div>
|
||||
<div class="text-container" :class="{disabled: !someChecked}">
|
||||
<input class="input-field"
|
||||
v-model="dest.overallValue"
|
||||
autocomplete="off"
|
||||
@blur="validateAnnualAmount($event, dest)"
|
||||
:disabled="!someChecked"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dest-mass-quantity-table">
|
||||
<destination-mass-quantity-row @action="onClickAction" @update-selected="updateCheckBox" :row="row"
|
||||
:key="row.id"
|
||||
:disabled="someChecked"
|
||||
v-for="row in rows"></destination-mass-quantity-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {useDestinationEditStore} from "@/store/destinationEdit.js";
|
||||
import {mapStores} from "pinia";
|
||||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
import DestMassCreateRow from "@/components/layout/edit/destination/mass/DestMassCreateRow.vue";
|
||||
import DestinationMassQuantityRow from "@/components/layout/edit/destination/mass/DestinationMassQuantityRow.vue";
|
||||
import {usePremiseEditStore} from "@/store/premiseEdit.js";
|
||||
import BulkEditRow from "@/components/layout/bulkedit/BulkEditRow.vue";
|
||||
import {toRaw} from "vue";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||
import {parseNumberFromString} from "@/common.js";
|
||||
|
||||
export default {
|
||||
name: "DestinationMassQuantity",
|
||||
components: {BasicButton, IconButton, BulkEditRow, DestinationMassQuantityRow, DestMassCreateRow, Checkbox},
|
||||
props: {
|
||||
premiseIds: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
onLoadingChange: {
|
||||
type: Function,
|
||||
default: () => {
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
destPool: null,
|
||||
overallCheck: false,
|
||||
overallIndeterminate: false,
|
||||
isCtrlPressed: false,
|
||||
isShiftPressed: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useDestinationEditStore, usePremiseEditStore),
|
||||
rows() {
|
||||
return this.destinationEditStore.getQuantityMatrix ?? [];
|
||||
},
|
||||
allChecked() {
|
||||
return this.rows.every(r => r.selected);
|
||||
},
|
||||
someChecked() {
|
||||
return this.rows.some(r => r.selected);
|
||||
},
|
||||
hasSelection() {
|
||||
return !this.addAll && !this.applyFilter && this.someChecked;
|
||||
},
|
||||
applyFilter() {
|
||||
return this.isCtrlPressed && this.isShiftPressed;
|
||||
},
|
||||
addAll() {
|
||||
return this.isCtrlPressed && !this.isShiftPressed;
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
this.onLoadingChange(true);
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(() => {
|
||||
this.buildMatrix();
|
||||
resolve();
|
||||
}, 10));
|
||||
} finally {
|
||||
this.onLoadingChange(false);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('keydown', this.handleKeyDown);
|
||||
window.addEventListener('keyup', this.handleKeyUp);
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener('keydown', this.handleKeyDown);
|
||||
window.removeEventListener('keyup', this.handleKeyUp);
|
||||
},
|
||||
methods: {
|
||||
/* key down/up handler */
|
||||
handleKeyDown(event) {
|
||||
if (event.key === 'Control') {
|
||||
this.isCtrlPressed = true;
|
||||
} else if (event.key === 'Shift') {
|
||||
this.isShiftPressed = true;
|
||||
}
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
this.fillData(this.modalType);
|
||||
this.modalType = null;
|
||||
}
|
||||
|
||||
},
|
||||
handleKeyUp(event) {
|
||||
if (event.key === 'Control') {
|
||||
this.isCtrlPressed = false;
|
||||
} else if (event.key === 'Shift') {
|
||||
this.isShiftPressed = false;
|
||||
}
|
||||
},
|
||||
onClickAction(data) {
|
||||
|
||||
this.rows.forEach(d => {
|
||||
d.selected = ((data.column === 'material' && d.material === data.row.material)
|
||||
|| (data.column === 'supplier' && d.supplier === data.row.supplier)
|
||||
|| (data.action === 'append' && d.selected));
|
||||
});
|
||||
|
||||
this.updateOverallCheckBox();
|
||||
},
|
||||
validateAnnualAmount(event, dest) {
|
||||
const value = event.target.value == null ? null : parseNumberFromString(event.target.value, 0);
|
||||
const validatedValue = value == null ? null : Math.max(0, value);
|
||||
const stringified = validatedValue === null ? '' : validatedValue.toFixed();
|
||||
|
||||
dest.overallValue = validatedValue;
|
||||
event.target.value = stringified;
|
||||
},
|
||||
updateOverallValue() {
|
||||
|
||||
const updates = this.destPool
|
||||
.filter(d => d.overallValue !== null && d.overallValue !== '')
|
||||
.map(d => ({
|
||||
nodeId: d.id,
|
||||
value: d.overallValue
|
||||
}));
|
||||
|
||||
if (updates.length > 0) {
|
||||
this.rows
|
||||
.filter(row => row.selected)
|
||||
.forEach(row => {
|
||||
updates.forEach(update => {
|
||||
const d = row.destinations.find(rd => rd.nodeId === update.nodeId);
|
||||
if (d && d.id !== null) {
|
||||
d.annual_amount = update.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.destPool.forEach(d => d.overallValue = null);
|
||||
|
||||
this.$forceUpdate();
|
||||
}
|
||||
|
||||
this.dismissChecked();
|
||||
},
|
||||
dismissChecked() {
|
||||
this.rows.forEach(row => row.selected = false);
|
||||
this.updateOverallCheckBox();
|
||||
},
|
||||
|
||||
/* checkbox handling */
|
||||
|
||||
updateCheckBox(data) { // data = {id: this.row.id, selected: value}
|
||||
// update global (rest is done in row)
|
||||
this.updateOverallCheckBox();
|
||||
},
|
||||
updateCheckBoxes(value) {
|
||||
this.rows?.forEach(r => r.selected = value);
|
||||
this.updateOverallCheckBox();
|
||||
},
|
||||
updateOverallCheckBox() {
|
||||
this.overallCheck = this.rows.every(r => r.selected);
|
||||
|
||||
if (!this.overallCheck)
|
||||
this.overallIndeterminate = this.rows.some(r => r.selected);
|
||||
},
|
||||
toNode(node, limit = 5) {
|
||||
if (!node)
|
||||
return 'N/A';
|
||||
|
||||
const name = node.name;
|
||||
const mappingId = node.external_mapping_id;
|
||||
const needsShortName = name.length > limit;
|
||||
const useMappingId = ((mappingId ?? null) !== null) && ((name ?? null) === null || needsShortName);
|
||||
const shortName = name?.substring(0, limit).concat("...") ?? 'N/A';
|
||||
return `${useMappingId ? mappingId.replace("_", " ") : (needsShortName ? shortName : name)}`;
|
||||
|
||||
},
|
||||
async buildMatrix() {
|
||||
// destPool aufbauen
|
||||
const destMap = new Map();
|
||||
|
||||
for (const pId of this.premiseIds) {
|
||||
const destinations = this.destinationEditStore.getByPremiseId(pId);
|
||||
if (!destinations) continue;
|
||||
|
||||
for (const d of destinations) {
|
||||
const destId = d.destination_node.id;
|
||||
if (!destMap.has(destId)) {
|
||||
destMap.set(destId, {
|
||||
...d.destination_node,
|
||||
overallValue: null,
|
||||
overallCheck: false,
|
||||
overallIndeterminate: false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.destPool = Array.from(destMap.values());
|
||||
|
||||
|
||||
const quantityMatrix = this.premiseIds
|
||||
.filter(p => p)
|
||||
.map(p => this.premiseEditStore.getById(p))
|
||||
.map(premise => {
|
||||
const destRaw = this.destinationEditStore.getByPremiseId(premise.id);
|
||||
|
||||
// Map für schnelleren Lookup erstellen
|
||||
const destLookup = new Map();
|
||||
if (destRaw) {
|
||||
for (const dr of destRaw) {
|
||||
destLookup.set(dr.destination_node.id, dr);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: premise.id,
|
||||
material: premise.material.part_number,
|
||||
supplier: this.toNode(premise.supplier, 30),
|
||||
destinations: this.destPool.map(dest => {
|
||||
const match = destLookup.get(dest.id);
|
||||
return {
|
||||
annual_amount: match?.annual_amount ?? null,
|
||||
id: match?.id ?? null,
|
||||
nodeId: dest.id,
|
||||
};
|
||||
}),
|
||||
selected: false
|
||||
};
|
||||
});
|
||||
|
||||
this.destinationEditStore.setQuantityMatrix(quantityMatrix);
|
||||
await this.$nextTick();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
|
||||
/* Global style für copy-mode cursor */
|
||||
.dest-mass-quantity-container.has-selection :deep(.dest-mass-quantity-row__cell--copyable:hover) {
|
||||
cursor: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDEyOC41MSAxMzQuMDUiPjxkZWZzPjxzdHlsZT4uY3tmaWxsOm5vbmU7fS5jLC5ke3N0cm9rZTojMDEwMTAxO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2Utd2lkdGg6NXB4O30uZHtmaWxsOiNmZmY7fTwvc3R5bGU+PC9kZWZzPjxnIGlkPSJhIj48cGF0aCBjbGFzcz0iYyIgZD0ibTU0Ljg5LDExMi41MWgtMi4yNGMtMS4yNCwwLTIuMjQtMS0yLjI0LTIuMjR2LTIuMjQiLz48bGluZSBjbGFzcz0iYyIgeDE9IjcwLjU3IiB5MT0iNzYuNjciIHgyPSI2My44NSIgeTI9Ijc2LjY3Ii8+PGxpbmUgY2xhc3M9ImMiIHgxPSI3MC41NyIgeTE9IjExMi41MSIgeDI9IjY2LjA5IiB5Mj0iMTEyLjUxIi8+PGxpbmUgY2xhc3M9ImMiIHgxPSI4Ni4yNSIgeTE9Ijk5LjA3IiB4Mj0iODYuMjUiIHkyPSI5Mi4zNSIvPjxsaW5lIGNsYXNzPSJjIiB4MT0iNTAuNDEiIHkxPSI5Ni44MyIgeDI9IjUwLjQxIiB5Mj0iOTIuMzUiLz48cGF0aCBjbGFzcz0iYyIgZD0ibTgxLjc3LDExMi41MWgyLjI0YzEuMjQsMCwyLjI0LTEsMi4yNC0yLjI0di0yLjI0Ii8+PHBhdGggY2xhc3M9ImMiIGQ9Im04MS43Nyw3Ni42N2gyLjI0YzEuMjQsMCwyLjI0LDEsMi4yNCwyLjI0djIuMjQiLz48cGF0aCBjbGFzcz0iYyIgZD0ibTU0Ljg5LDc2LjY3aC0yLjI0Yy0xLjI0LDAtMi4yNCwxLTIuMjQsMi4yNHYyLjI0Ii8+PHBhdGggY2xhc3M9ImMiIGQ9Im04Ni4yNSw5OS4wN2gxMS4yYzEuMjQsMCwyLjI0LTEsMi4yNC0yLjI0di0zMS4zNmMwLTEuMjQtMS0yLjI0LTIuMjQtMi4yNGgtMzEuMzZjLTEuMjQsMC0yLjI0LDEtMi4yNCwyLjI0djExLjIiLz48L2c+PGcgaWQ9ImIiPjxwYXRoIGNsYXNzPSJkIiBkPSJtNDQuMDgsNDQuMDdsMzIuOTQtOS4yYzEuNjktLjUyLDIuNjQtMi4zMSwyLjEyLTQtLjMtLjk4LTEuMDUtMS43NS0yLjAxLTIuMDlMNi43MywyLjY3Yy0xLjY3LS41Ny0zLjQ5LjMzLTQuMDYsMi0uMjMuNjYtLjIzLDEuMzgsMCwyLjA1bDI2LjExLDcwLjRjLjU4LDEuNjcsMi40LDIuNTYsNC4wNywxLjk4Ljk3LS4zMywxLjcxLTEuMTEsMi4wMS0yLjA5bDkuMjItMzIuOTRaIi8+PC9nPjwvc3ZnPg==") 12 12, pointer;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
||||
/* Global style für filter-mode cursor */
|
||||
.dest-mass-quantity-container.add-all :deep(.dest-mass-quantity-row__cell--filterable:hover) {
|
||||
cursor: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDEyOC41MSAxMzQuMDUiPjxkZWZzPjxzdHlsZT4uY3tmaWxsOm5vbmU7fS5jLC5ke3N0cm9rZTojMDEwMTAxO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2Utd2lkdGg6NXB4O30uZHtmaWxsOiNmZmY7fTwvc3R5bGU+PC9kZWZzPjxnIGlkPSJhIj48bGluZSBjbGFzcz0iYyIgeDE9IjczLjAzIiB5MT0iNzUuNTUiIHgyPSI2Ni4zOCIgeTI9Ijc1LjU1Ii8+PGxpbmUgY2xhc3M9ImMiIHgxPSI2Ni4zOCIgeTE9IjExMi4xNiIgeDI9IjczLjAzIiB5Mj0iMTEyLjE2Ii8+PHBhdGggY2xhc3M9ImMiIGQ9Im04MS4zNSw3NS41NWg0Ljk5Yy45MiwwLDEuNjYuNzUsMS42NiwxLjY2djQuOTkiLz48bGluZSBjbGFzcz0iYyIgeDE9Ijg4LjAxIiB5MT0iOTcuMTgiIHgyPSI4OC4wMSIgeTI9IjkwLjUzIi8+PHBhdGggY2xhc3M9ImMiIGQ9Im04MS4zNSwxMTIuMTZoNC45OWMuOTIsMCwxLjY2LS43NSwxLjY2LTEuNjZ2LTQuOTkiLz48bGluZSBjbGFzcz0iYyIgeDE9IjUxLjQiIHkxPSI5MC41MyIgeDI9IjUxLjQiIHkyPSI5Ny4xOCIvPjxwYXRoIGNsYXNzPSJjIiBkPSJtNTguMDUsMTEyLjE2aC00Ljk5Yy0uOTIsMC0xLjY2LS43NS0xLjY2LTEuNjZ2LTQuOTkiLz48cGF0aCBjbGFzcz0iYyIgZD0ibTU4LjA1LDc1LjU1aC00Ljk5Yy0uOTIsMC0xLjY2Ljc1LTEuNjYsMS42NnY0Ljk5Ii8+PC9nPjxnIGlkPSJiIj48cGF0aCBjbGFzcz0iZCIgZD0ibTQ0LjA4LDQ0LjA3bDMyLjk0LTkuMmMxLjY5LS41MiwyLjY0LTIuMzEsMi4xMi00LS4zLS45OC0xLjA1LTEuNzUtMi4wMS0yLjA5TDYuNzMsMi42N2MtMS42Ny0uNTctMy40OS4zMy00LjA2LDItLjIzLjY2LS4yMywxLjM4LDAsMi4wNWwyNi4xMSw3MC40Yy41OCwxLjY3LDIuNCwyLjU2LDQuMDcsMS45OC45Ny0uMzMsMS43MS0xLjExLDIuMDEtMi4wOWw5LjIyLTMyLjk0WiIvPjwvZz48L3N2Zz4=") 12 12, pointer;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
||||
/* Global style für filter-mode cursor */
|
||||
.dest-mass-quantity-container.apply-filter :deep(.dest-mass-quantity-row__cell--filterable:hover) {
|
||||
cursor: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDEyOC41MSAxMzQuMDUiPjxkZWZzPjxzdHlsZT4uZHtzdHJva2U6IzAwMDt9LmQsLmV7ZmlsbDpub25lO30uZCwuZSwuZntzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLXdpZHRoOjVweDt9LmUsLmZ7c3Ryb2tlOiMwMTAxMDE7fS5me2ZpbGw6I2ZmZjt9PC9zdHlsZT48L2RlZnM+PGcgaWQ9ImEiPjxsaW5lIGNsYXNzPSJlIiB4MT0iNzMuMDMiIHkxPSI3NS41NSIgeDI9IjY2LjM4IiB5Mj0iNzUuNTUiLz48bGluZSBjbGFzcz0iZSIgeDE9IjY2LjM4IiB5MT0iMTEyLjE2IiB4Mj0iNzMuMDMiIHkyPSIxMTIuMTYiLz48cGF0aCBjbGFzcz0iZSIgZD0ibTgxLjM1LDc1LjU1aDQuOTljLjkyLDAsMS42Ni43NSwxLjY2LDEuNjZ2NC45OSIvPjxsaW5lIGNsYXNzPSJlIiB4MT0iODguMDEiIHkxPSI5Ny4xOCIgeDI9Ijg4LjAxIiB5Mj0iOTAuNTMiLz48cGF0aCBjbGFzcz0iZSIgZD0ibTgxLjM1LDExMi4xNmg0Ljk5Yy45MiwwLDEuNjYtLjc1LDEuNjYtMS42NnYtNC45OSIvPjxsaW5lIGNsYXNzPSJlIiB4MT0iNTEuNCIgeTE9IjkwLjUzIiB4Mj0iNTEuNCIgeTI9Ijk3LjE4Ii8+PHBhdGggY2xhc3M9ImUiIGQ9Im01OC4wNSwxMTIuMTZoLTQuOTljLS45MiwwLTEuNjYtLjc1LTEuNjYtMS42NnYtNC45OSIvPjxwYXRoIGNsYXNzPSJlIiBkPSJtNTguMDUsNzUuNTVoLTQuOTljLS45MiwwLTEuNjYuNzUtMS42NiwxLjY2djQuOTkiLz48L2c+PGcgaWQ9ImIiPjxwYXRoIGNsYXNzPSJmIiBkPSJtNDQuMDgsNDQuMDdsMzIuOTQtOS4yYzEuNjktLjUyLDIuNjQtMi4zMSwyLjEyLTQtLjMtLjk4LTEuMDUtMS43NS0yLjAxLTIuMDlMNi43MywyLjY3Yy0xLjY3LS41Ny0zLjQ5LjMzLTQuMDYsMi0uMjMuNjYtLjIzLDEuMzgsMCwyLjA1bDI2LjExLDcwLjRjLjU4LDEuNjcsMi40LDIuNTYsNC4wNywxLjk4Ljk3LS4zMywxLjcxLTEuMTEsMi4wMS0yLjA5bDkuMjItMzIuOTRaIi8+PC9nPjxnIGlkPSJjIj48bGluZSBjbGFzcz0iZCIgeDE9Ijk5LjM4IiB5MT0iOTQuMTkiIHgyPSIxMjYuMDEiIHkyPSI5NC4xOSIvPjxsaW5lIGNsYXNzPSJkIiB4MT0iMTEyLjY5IiB5MT0iODAuODciIHgyPSIxMTIuNjkiIHkyPSIxMDcuNTEiLz48L2c+PC9zdmc+") 12 12, pointer;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
||||
.text-container.disabled {
|
||||
background-color: #f3f4f6;
|
||||
cursor: not-allowed;
|
||||
border-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.text-container.disabled input {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
||||
.text-container:hover:not(.disabled) {
|
||||
background: #EEF4FF;
|
||||
border: 0.2rem solid #8DB3FE;
|
||||
transform: scale(1.01);
|
||||
|
||||
}
|
||||
|
||||
|
||||
.input-field {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: none;
|
||||
resize: none;
|
||||
font-family: inherit;
|
||||
font-size: 1.4rem;
|
||||
color: #002F54;
|
||||
|
||||
max-width: 6rem;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);*/
|
||||
border: 0.2rem solid #E3EDFF;
|
||||
transition: all 0.1s ease;
|
||||
flex: 1 0 auto;
|
||||
|
||||
max-width: 8rem;
|
||||
|
||||
}
|
||||
|
||||
.dest-mass-quantity-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dest-mass-quantity-table-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.dest-mass-quantity-table-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1.6rem;
|
||||
padding: 1.6rem 2.4rem;
|
||||
justify-content: flex-start;
|
||||
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid rgba(107, 134, 156, 0.2);
|
||||
font-weight: 500;
|
||||
font-size: 1.4rem;
|
||||
color: #6B869C;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08rem;
|
||||
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dest-mass-quantity-table {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
padding-bottom: 2.4rem;
|
||||
}
|
||||
|
||||
.dest-mass-quantity-table-header-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
|
||||
.dest-mass-quantity-table-header-material {
|
||||
width: 14rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dest-mass-quantity-table-header-supplier {
|
||||
width: 24rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dest-mass-quantity-table-header-applier {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 7rem;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.dest-mass-quantity-table-header-dest {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
width: 15rem;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -1,216 +0,0 @@
|
|||
<template>
|
||||
<div class="dest-mass-quantity-row-container" @wheel="handleWheel">
|
||||
<div class="dest-mass-quantity-row-checkbox">
|
||||
<checkbox :checked="row.selected" @checkbox-changed="updateCheckbox">
|
||||
</checkbox>
|
||||
</div>
|
||||
<div class="dest-mass-quantity-row-material dest-mass-quantity-row__cell--filterable" @click="action($event,'material')"
|
||||
@mousedown="handleMouseDown">
|
||||
<ph-package size="24"/>
|
||||
{{ row.material }}
|
||||
</div>
|
||||
<div class="dest-mass-quantity-row-supplier dest-mass-quantity-row__cell--filterable" @click="action($event,'supplier')"
|
||||
@mousedown="handleMouseDown">
|
||||
<ph-factory size="24"/>
|
||||
{{ row.supplier }}
|
||||
</div>
|
||||
<div class="dest-mass-quantity-row-applier"></div>
|
||||
<div v-for="dest in row.destinations" class="dest-mass-quantity-row-dest" :key="dest.id">
|
||||
<ph-stack size="24"></ph-stack>
|
||||
<div class="text-container" :class="{disabled: disabled || dest.id === null}">
|
||||
<input class="input-field"
|
||||
v-model="dest.annual_amount"
|
||||
@blur="validateAnnualAmount($event, dest)"
|
||||
autocomplete="off"
|
||||
:disabled="disabled || dest.id === null"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
import {PhFactory, PhPackage, PhStack} from "@phosphor-icons/vue";
|
||||
import BasicBadge from "@/components/UI/BasicBadge.vue";
|
||||
import InputField from "@/components/UI/InputField.vue";
|
||||
import {parseNumberFromString} from "@/common.js";
|
||||
|
||||
export default {
|
||||
name: "DestinationMassQuantityRow",
|
||||
components: {PhStack, InputField, PhFactory, Checkbox},
|
||||
emits: ['update-selected', 'action'],
|
||||
props: {
|
||||
row: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
validateAnnualAmount(event, dest) {
|
||||
const value = event.target.value == null ? null : parseNumberFromString(event.target.value, 0);
|
||||
const validatedValue = value == null ? null : Math.max(0, value);
|
||||
const stringified = validatedValue === null ? '' : validatedValue.toFixed();
|
||||
|
||||
dest.annual_amount = validatedValue;
|
||||
event.target.value = stringified;
|
||||
},
|
||||
handleMouseDown(event) {
|
||||
if (event.shiftKey || event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
handleWheel(event) {
|
||||
if (event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
window.scrollBy(0, event.deltaY);
|
||||
}
|
||||
},
|
||||
action(event, column) {
|
||||
|
||||
if (event.ctrlKey && !event.shiftKey && (column === 'material' || column === 'supplier')) {
|
||||
this.$emit('action', {row: this.row, column: column, action: 'filter'});
|
||||
} else if (event.ctrlKey && event.shiftKey && (column === 'material' || column === 'supplier')) {
|
||||
this.$emit('action', {row: this.row, column: column, action: 'append'});
|
||||
}
|
||||
|
||||
},
|
||||
updateCheckbox(value) {
|
||||
this.row.selected = value;
|
||||
this.$emit('update-selected', {id: this.row.id, selected: value});
|
||||
},
|
||||
toNode(node, limit = 5) {
|
||||
if (!node)
|
||||
return 'N/A';
|
||||
|
||||
const name = node.name;
|
||||
const mappingId = node.external_mapping_id;
|
||||
const needsShortName = name.length > limit;
|
||||
const useMappingId = ((mappingId ?? null) !== null) && ((name ?? null) === null || needsShortName);
|
||||
const shortName = name?.substring(0, limit).concat("...") ?? 'N/A';
|
||||
return `${useMappingId ? mappingId.replace("_", " ") : (needsShortName ? shortName : name)}`;
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.input-field {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: none;
|
||||
resize: none;
|
||||
font-family: inherit;
|
||||
font-size: 1.4rem;
|
||||
color: #002F54;
|
||||
|
||||
max-width: 6rem;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);*/
|
||||
border: 0.2rem solid #E3EDFF;
|
||||
transition: all 0.1s ease;
|
||||
flex: 1 0 auto;
|
||||
|
||||
max-width: 8rem;
|
||||
|
||||
}
|
||||
|
||||
.text-container:hover {
|
||||
background: #EEF4FF;
|
||||
border: 0.2rem solid #8DB3FE;
|
||||
/*transform: translateY(2px);*/
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
|
||||
.text-container.disabled {
|
||||
background-color: #f3f4f6;
|
||||
cursor: not-allowed;
|
||||
border-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.text-container.disabled input {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
||||
.text-container:hover:not(.disabled) {
|
||||
background: #EEF4FF;
|
||||
border: 0.2rem solid #8DB3FE;
|
||||
transform: scale(1.01);
|
||||
|
||||
}
|
||||
|
||||
|
||||
.dest-mass-quantity-row-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1.6rem;
|
||||
padding: 1.2rem 2.4rem;
|
||||
justify-content: flex-start;
|
||||
border-bottom: 0.16rem solid #f3f4f6;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.dest-mass-quantity-row-container:hover {
|
||||
background-color: rgba(107, 134, 156, 0.05);
|
||||
}
|
||||
|
||||
.dest-mass-quantity-row-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.dest-mass-quantity-row-material {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.8rem;
|
||||
width: 14rem;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.dest-mass-quantity-row-applier {
|
||||
width: 7rem;
|
||||
}
|
||||
|
||||
.dest-mass-quantity-row-supplier {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.8rem;
|
||||
width: 24rem;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.dest-mass-quantity-row-dest {
|
||||
width: 15rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -1,476 +0,0 @@
|
|||
<template>
|
||||
<div class="dest-mass-route-container">
|
||||
<div v-if="generalError">
|
||||
<div class="destination-mass-route-info">
|
||||
<ph-warning size="18px"></ph-warning>
|
||||
The routing data is faulty. Please contact support.
|
||||
You can try to solve the problem by first deleting all destinations and then creating them again.
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="dest-mass-route-table-wrapper">
|
||||
<div class="dest-mass-route-table-header-wrapper"
|
||||
ref="headerWrapper"
|
||||
@scroll="syncScroll('header')">
|
||||
<div class="dest-mass-route-table-header">
|
||||
<div class="dest-mass-route-table-header-supplier"></div>
|
||||
<div class="dest-mass-route-table-header-dest"
|
||||
:key="`${dest.id}`"
|
||||
v-for="dest in destPool">
|
||||
<div></div>
|
||||
<div>{{ toNode(dest.destination_node, 6) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dest-mass-route-table"
|
||||
ref="tableBody"
|
||||
@scroll="syncScroll('body')">
|
||||
<destination-mass-route-row :row="row"
|
||||
:key="row.id"
|
||||
v-for="row in rows"></destination-mass-route-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
import DestinationMassQuantityRow from "@/components/layout/edit/destination/mass/DestinationMassQuantityRow.vue";
|
||||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {useDestinationEditStore} from "@/store/destinationEdit.js";
|
||||
import {usePremiseEditStore} from "@/store/premiseEdit.js";
|
||||
import {toRaw} from "vue";
|
||||
import DestinationMassRouteRow from "@/components/layout/edit/destination/mass/DestinationMassRouteRow.vue";
|
||||
import Flag from "@/components/UI/Flag.vue";
|
||||
import logger from "@/logger.js";
|
||||
|
||||
export default {
|
||||
name: "DestinationMassRoute",
|
||||
components: {Flag, DestinationMassRouteRow, IconButton, Checkbox, DestinationMassQuantityRow},
|
||||
props: {
|
||||
premiseIds: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
onLoadingChange: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useDestinationEditStore, usePremiseEditStore),
|
||||
rows() {
|
||||
return this.destinationEditStore.getRouteMatrix ?? [];
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
destPool: null,
|
||||
generalError: false,
|
||||
isScrollingSyncronized: false,
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.onLoadingChange(true);
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(() => {
|
||||
this.buildMatrix();
|
||||
resolve();
|
||||
}, 10));
|
||||
} finally {
|
||||
this.onLoadingChange(false);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
syncScroll(source) {
|
||||
if (this.isScrollingSyncronized) {
|
||||
this.isScrollingSyncronized = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.isScrollingSyncronized = true;
|
||||
|
||||
if (source === 'body') {
|
||||
this.$refs.headerWrapper.scrollLeft = this.$refs.tableBody.scrollLeft;
|
||||
} else if (source === 'header') {
|
||||
this.$refs.tableBody.scrollLeft = this.$refs.headerWrapper.scrollLeft;
|
||||
}
|
||||
},
|
||||
toNode(node, limit = 5) {
|
||||
if (!node)
|
||||
return 'N/A';
|
||||
|
||||
const name = node.name;
|
||||
const mappingId = node.external_mapping_id;
|
||||
const needsShortName = name.length > limit;
|
||||
const useMappingId = ((mappingId ?? null) !== null) && ((name ?? null) === null || needsShortName);
|
||||
const shortName = name?.substring(0, limit).concat("...") ?? 'N/A';
|
||||
return `${useMappingId ? mappingId.replace("_", " ") : (needsShortName ? shortName : name)}`;
|
||||
|
||||
},
|
||||
async buildMatrix() {
|
||||
const columnHeadersMap = new Map();
|
||||
const supplierToDestinationsMap = new Map();
|
||||
|
||||
for (const pId of this.premiseIds) {
|
||||
const curPremise = this.premiseEditStore.getById(pId);
|
||||
const destOfCurPremise = this.destinationEditStore.getByPremiseId(pId);
|
||||
if (!destOfCurPremise) continue;
|
||||
|
||||
/* supplier map collects all destinations for one supplier.
|
||||
* if there is more than one instance of a destination for one supplier
|
||||
* (more than one part number), a destination instance is chosen by the following priority list:
|
||||
* 1. instances with d2d rate
|
||||
* 2. instances with selected routes
|
||||
* 3. all other instances.
|
||||
*/
|
||||
if (!supplierToDestinationsMap.has(curPremise.supplier.id)) {
|
||||
supplierToDestinationsMap.set(curPremise.supplier.id, {
|
||||
destinations: [...destOfCurPremise]
|
||||
});
|
||||
} else {
|
||||
const mapEntry = supplierToDestinationsMap.get(curPremise.supplier.id);
|
||||
const exDs = mapEntry.destinations;
|
||||
|
||||
destOfCurPremise.forEach(d => {
|
||||
const exD = exDs.find(ex => ex.destination_node.id === d.destination_node.id) ?? null;
|
||||
|
||||
if (!exD) {
|
||||
exDs.push(d);
|
||||
} else {
|
||||
|
||||
if ((!exD.routes?.some(r => r.is_selected) && d.routes?.some(r => r.is_selected) && !exD.is_d2d) || (!exD.is_d2d && d.is_d2d)) {
|
||||
const idx = exDs.indexOf(exD);
|
||||
exDs.splice(idx, 1);
|
||||
exDs.push(d);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* Collects all destinations over all
|
||||
* suppliers and part numbers for the table headers
|
||||
*/
|
||||
for (const d of destOfCurPremise) {
|
||||
const destId = d.destination_node.id;
|
||||
if (!columnHeadersMap.has(destId)) {
|
||||
columnHeadersMap.set(destId, {
|
||||
destination_node: {...d.destination_node},
|
||||
destinationNodeId: d.destination_node.id,
|
||||
destinationNodeName: d.destination_node.name
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.destPool = Array.from(columnHeadersMap.values());
|
||||
|
||||
|
||||
|
||||
const premiseMap = new Map();
|
||||
|
||||
this.premiseIds.forEach(pId => {
|
||||
const curPremise = this.premiseEditStore.getById(pId);
|
||||
const destOfCurPremise = this.destinationEditStore.getByPremiseId(pId);
|
||||
|
||||
if (!premiseMap.has(curPremise.supplier.id)) {
|
||||
premiseMap.set(curPremise.supplier.id, {
|
||||
ids: [],
|
||||
supplierNodeId: curPremise.supplier.id,
|
||||
supplier: curPremise.supplier,
|
||||
destinations: this.buildDestinations(columnHeadersMap, supplierToDestinationsMap.get(curPremise.supplier.id)?.destinations ?? [])
|
||||
});
|
||||
}
|
||||
|
||||
const row = premiseMap.get(curPremise.supplier.id);
|
||||
|
||||
if (row) {
|
||||
row.ids.push(curPremise.id);
|
||||
this.addDestinationsToRow(row.destinations, destOfCurPremise)
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
const destMatrix = Array.from(premiseMap.values());
|
||||
this.generalError = destMatrix.some(r => !r.destinations.every(d => d.valid));
|
||||
|
||||
this.destinationEditStore.setRouteMatrix(destMatrix)
|
||||
await this.$nextTick();
|
||||
},
|
||||
buildDestinations(allDestinationsMap, assignedDestinations) {
|
||||
return Array.from(allDestinationsMap.values()).map(d => {
|
||||
|
||||
const assignedDest = assignedDestinations.find(dest => dest.destination_node.id === d.destinationNodeId);
|
||||
|
||||
const builtRoutes = this.buildRoutes(assignedDest?.routes);
|
||||
const selectedBuildRoute = builtRoutes?.find(r => r.selected)?.routeCompareString ?? null;
|
||||
|
||||
return {
|
||||
ids: [],
|
||||
disabled: true,
|
||||
valid: true,
|
||||
destinationNodeId: d.destinationNodeId,
|
||||
destinationName: d.destinationNodeName,
|
||||
routes: builtRoutes,
|
||||
isD2d: assignedDest?.is_d2d ?? false,
|
||||
rateD2d: assignedDest?.rate_d2d ?? null,
|
||||
leadTimeD2d: assignedDest?.lead_time_d2d === 0 ? null : (assignedDest?.lead_time_d2d ?? null),
|
||||
selectedRoute: selectedBuildRoute
|
||||
}
|
||||
});
|
||||
},
|
||||
addDestinationsToRow(rowDestinations, destOfPremises) {
|
||||
destOfPremises.forEach(curDestOfPremise => {
|
||||
|
||||
/* rowDestinations contains all here known destinations that are shown
|
||||
*
|
||||
*
|
||||
*/
|
||||
let existingDest = rowDestinations.find(rowD => rowD.destinationNodeId === curDestOfPremise.destination_node.id) ?? null;
|
||||
|
||||
if (existingDest) {
|
||||
existingDest.disabled = false;
|
||||
|
||||
if(existingDest.ids.includes(curDestOfPremise.id))
|
||||
logger.log("Duplicate id: ", curDestOfPremise.id);
|
||||
|
||||
existingDest.ids.push(curDestOfPremise.id);
|
||||
|
||||
/* add route ids to routes */
|
||||
this.verifyRoutes(existingDest, curDestOfPremise)
|
||||
}
|
||||
});
|
||||
},
|
||||
verifyRoutes(rowDest, premiseDest) {
|
||||
|
||||
const premiseRoutes = premiseDest.routes;
|
||||
|
||||
if (rowDest.routes.length !== premiseRoutes.length) {
|
||||
logger.log("length mismatch ", toRaw(rowDest), toRaw(premiseDest));
|
||||
rowDest.valid = false;
|
||||
return
|
||||
}
|
||||
|
||||
premiseRoutes.forEach(route => {
|
||||
const routeString = JSON.stringify(route.transit_nodes.map(n => n.external_mapping_id)); //.join(" > ").replace("_", " ");
|
||||
|
||||
const rowRoute = rowDest.routes.find(r => r.routeCompareString === routeString && r.type === route.type);
|
||||
|
||||
if (!rowRoute) {
|
||||
logger.log("no matching route ", routeString, rowDest);
|
||||
rowDest.valid = false;
|
||||
} else {
|
||||
rowRoute.ids.push(route.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
buildRoutes(routes) {
|
||||
|
||||
return routes?.map(r => {
|
||||
return {
|
||||
ids: [],
|
||||
type: r.type,
|
||||
selected: r.is_selected,
|
||||
transitNodes: r.transit_nodes.map(n => n.external_mapping_id),
|
||||
routeCompareString: JSON.stringify(r.transit_nodes.map(n => n.external_mapping_id)), //.join(" > ").replace("_", " ")
|
||||
routeDisplayString: this.toRoute(r)
|
||||
}
|
||||
}) ?? [];
|
||||
},
|
||||
toRoute(route, limit = 48) {
|
||||
|
||||
if (!route)
|
||||
return 'N/A';
|
||||
|
||||
const nodes = route.transit_nodes?.map((node) => this.toNode(node)) ?? [];
|
||||
|
||||
if (nodes.length === 0)
|
||||
return 'N/A';
|
||||
|
||||
const separator = " > ";
|
||||
let fullString = nodes.join(separator);
|
||||
|
||||
if (fullString.length <= limit)
|
||||
return fullString;
|
||||
|
||||
const front = nodes[0].concat(separator).concat("...").concat(separator);
|
||||
let back = [];
|
||||
|
||||
for (const node of nodes.slice().reverse()) {
|
||||
|
||||
back.unshift(node);
|
||||
const temp = front.concat(back.join(separator));
|
||||
|
||||
if (temp.length > limit) {
|
||||
return front.concat(back.slice(1).join(separator));
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.destination-mass-route-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.4rem;
|
||||
gap: 1.6rem;
|
||||
background-color: #BC2B72;
|
||||
color: #ffffff;
|
||||
border-radius: 0.8rem;
|
||||
padding: 1.6rem;
|
||||
margin: 1.6rem 1.6rem 0 1.6rem;
|
||||
}
|
||||
|
||||
|
||||
/* Global style für copy-mode cursor */
|
||||
.dest-mass-route-container.has-selection :deep(.dest-mass-route-row__cell--copyable:hover) {
|
||||
cursor: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDEyOC41MSAxMzQuMDUiPjxkZWZzPjxzdHlsZT4uY3tmaWxsOm5vbmU7fS5jLC5ke3N0cm9rZTojMDEwMTAxO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2Utd2lkdGg6NXB4O30uZHtmaWxsOiNmZmY7fTwvc3R5bGU+PC9kZWZzPjxnIGlkPSJhIj48cGF0aCBjbGFzcz0iYyIgZD0ibTU0Ljg5LDExMi41MWgtMi4yNGMtMS4yNCwwLTIuMjQtMS0yLjI0LTIuMjR2LTIuMjQiLz48bGluZSBjbGFzcz0iYyIgeDE9IjcwLjU3IiB5MT0iNzYuNjciIHgyPSI2My44NSIgeTI9Ijc2LjY3Ii8+PGxpbmUgY2xhc3M9ImMiIHgxPSI3MC41NyIgeTE9IjExMi41MSIgeDI9IjY2LjA5IiB5Mj0iMTEyLjUxIi8+PGxpbmUgY2xhc3M9ImMiIHgxPSI4Ni4yNSIgeTE9Ijk5LjA3IiB4Mj0iODYuMjUiIHkyPSI5Mi4zNSIvPjxsaW5lIGNsYXNzPSJjIiB4MT0iNTAuNDEiIHkxPSI5Ni44MyIgeDI9IjUwLjQxIiB5Mj0iOTIuMzUiLz48cGF0aCBjbGFzcz0iYyIgZD0ibTgxLjc3LDExMi41MWgyLjI0YzEuMjQsMCwyLjI0LTEsMi4yNC0yLjI0di0yLjI0Ii8+PHBhdGggY2xhc3M9ImMiIGQ9Im04MS43Nyw3Ni42N2gyLjI0YzEuMjQsMCwyLjI0LDEsMi4yNCwyLjI0djIuMjQiLz48cGF0aCBjbGFzcz0iYyIgZD0ibTU0Ljg5LDc2LjY3aC0yLjI0Yy0xLjI0LDAtMi4yNCwxLTIuMjQsMi4yNHYyLjI0Ii8+PHBhdGggY2xhc3M9ImMiIGQ9Im04Ni4yNSw5OS4wN2gxMS4yYzEuMjQsMCwyLjI0LTEsMi4yNC0yLjI0di0zMS4zNmMwLTEuMjQtMS0yLjI0LTIuMjQtMi4yNGgtMzEuMzZjLTEuMjQsMC0yLjI0LDEtMi4yNCwyLjI0djExLjIiLz48L2c+PGcgaWQ9ImIiPjxwYXRoIGNsYXNzPSJkIiBkPSJtNDQuMDgsNDQuMDdsMzIuOTQtOS4yYzEuNjktLjUyLDIuNjQtMi4zMSwyLjEyLTQtLjMtLjk4LTEuMDUtMS43NS0yLjAxLTIuMDlMNi43MywyLjY3Yy0xLjY3LS41Ny0zLjQ5LjMzLTQuMDYsMi0uMjMuNjYtLjIzLDEuMzgsMCwyLjA1bDI2LjExLDcwLjRjLjU4LDEuNjcsMi40LDIuNTYsNC4wNywxLjk4Ljk3LS4zMywxLjcxLTEuMTEsMi4wMS0yLjA5bDkuMjItMzIuOTRaIi8+PC9nPjwvc3ZnPg==") 12 12, pointer;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
||||
/* Global style für filter-mode cursor */
|
||||
.dest-mass-route-container.add-all :deep(.dest-mass-route-row__cell--filterable:hover) {
|
||||
cursor: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDEyOC41MSAxMzQuMDUiPjxkZWZzPjxzdHlsZT4uY3tmaWxsOm5vbmU7fS5jLC5ke3N0cm9rZTojMDEwMTAxO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2Utd2lkdGg6NXB4O30uZHtmaWxsOiNmZmY7fTwvc3R5bGU+PC9kZWZzPjxnIGlkPSJhIj48bGluZSBjbGFzcz0iYyIgeDE9IjczLjAzIiB5MT0iNzUuNTUiIHgyPSI2Ni4zOCIgeTI9Ijc1LjU1Ii8+PGxpbmUgY2xhc3M9ImMiIHgxPSI2Ni4zOCIgeTE9IjExMi4xNiIgeDI9IjczLjAzIiB5Mj0iMTEyLjE2Ii8+PHBhdGggY2xhc3M9ImMiIGQ9Im04MS4zNSw3NS41NWg0Ljk5Yy45MiwwLDEuNjYuNzUsMS42NiwxLjY2djQuOTkiLz48bGluZSBjbGFzcz0iYyIgeDE9Ijg4LjAxIiB5MT0iOTcuMTgiIHgyPSI4OC4wMSIgeTI9IjkwLjUzIi8+PHBhdGggY2xhc3M9ImMiIGQ9Im04MS4zNSwxMTIuMTZoNC45OWMuOTIsMCwxLjY2LS43NSwxLjY2LTEuNjZ2LTQuOTkiLz48bGluZSBjbGFzcz0iYyIgeDE9IjUxLjQiIHkxPSI5MC41MyIgeDI9IjUxLjQiIHkyPSI5Ny4xOCIvPjxwYXRoIGNsYXNzPSJjIiBkPSJtNTguMDUsMTEyLjE2aC00Ljk5Yy0uOTIsMC0xLjY2LS43NS0xLjY2LTEuNjZ2LTQuOTkiLz48cGF0aCBjbGFzcz0iYyIgZD0ibTU4LjA1LDc1LjU1aC00Ljk5Yy0uOTIsMC0xLjY2Ljc1LTEuNjYsMS42NnY0Ljk5Ii8+PC9nPjxnIGlkPSJiIj48cGF0aCBjbGFzcz0iZCIgZD0ibTQ0LjA4LDQ0LjA3bDMyLjk0LTkuMmMxLjY5LS41MiwyLjY0LTIuMzEsMi4xMi00LS4zLS45OC0xLjA1LTEuNzUtMi4wMS0yLjA5TDYuNzMsMi42N2MtMS42Ny0uNTctMy40OS4zMy00LjA2LDItLjIzLjY2LS4yMywxLjM4LDAsMi4wNWwyNi4xMSw3MC40Yy41OCwxLjY3LDIuNCwyLjU2LDQuMDcsMS45OC45Ny0uMzMsMS43MS0xLjExLDIuMDEtMi4wOWw5LjIyLTMyLjk0WiIvPjwvZz48L3N2Zz4=") 12 12, pointer;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
||||
/* Global style für filter-mode cursor */
|
||||
.dest-mass-route-container.apply-filter :deep(.dest-mass-route-row__cell--filterable:hover) {
|
||||
cursor: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDEyOC41MSAxMzQuMDUiPjxkZWZzPjxzdHlsZT4uZHtzdHJva2U6IzAwMDt9LmQsLmV7ZmlsbDpub25lO30uZCwuZSwuZntzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLXdpZHRoOjVweDt9LmUsLmZ7c3Ryb2tlOiMwMTAxMDE7fS5me2ZpbGw6I2ZmZjt9PC9zdHlsZT48L2RlZnM+PGcgaWQ9ImEiPjxsaW5lIGNsYXNzPSJlIiB4MT0iNzMuMDMiIHkxPSI3NS41NSIgeDI9IjY2LjM4IiB5Mj0iNzUuNTUiLz48bGluZSBjbGFzcz0iZSIgeDE9IjY2LjM4IiB5MT0iMTEyLjE2IiB4Mj0iNzMuMDMiIHkyPSIxMTIuMTYiLz48cGF0aCBjbGFzcz0iZSIgZD0ibTgxLjM1LDc1LjU1aDQuOTljLjkyLDAsMS42Ni43NSwxLjY2LDEuNjZ2NC45OSIvPjxsaW5lIGNsYXNzPSJlIiB4MT0iODguMDEiIHkxPSI5Ny4xOCIgeDI9Ijg4LjAxIiB5Mj0iOTAuNTMiLz48cGF0aCBjbGFzcz0iZSIgZD0ibTgxLjM1LDExMi4xNmg0Ljk5Yy45MiwwLDEuNjYtLjc1LDEuNjYtMS42NnYtNC45OSIvPjxsaW5lIGNsYXNzPSJlIiB4MT0iNTEuNCIgeTE9IjkwLjUzIiB4Mj0iNTEuNCIgeTI9Ijk3LjE4Ii8+PHBhdGggY2xhc3M9ImUiIGQ9Im01OC4wNSwxMTIuMTZoLTQuOTljLS45MiwwLTEuNjYtLjc1LTEuNjYtMS42NnYtNC45OSIvPjxwYXRoIGNsYXNzPSJlIiBkPSJtNTguMDUsNzUuNTVoLTQuOTljLS45MiwwLTEuNjYuNzUtMS42NiwxLjY2djQuOTkiLz48L2c+PGcgaWQ9ImIiPjxwYXRoIGNsYXNzPSJmIiBkPSJtNDQuMDgsNDQuMDdsMzIuOTQtOS4yYzEuNjktLjUyLDIuNjQtMi4zMSwyLjEyLTQtLjMtLjk4LTEuMDUtMS43NS0yLjAxLTIuMDlMNi43MywyLjY3Yy0xLjY3LS41Ny0zLjQ5LjMzLTQuMDYsMi0uMjMuNjYtLjIzLDEuMzgsMCwyLjA1bDI2LjExLDcwLjRjLjU4LDEuNjcsMi40LDIuNTYsNC4wNywxLjk4Ljk3LS4zMywxLjcxLTEuMTEsMi4wMS0yLjA5bDkuMjItMzIuOTRaIi8+PC9nPjxnIGlkPSJjIj48bGluZSBjbGFzcz0iZCIgeDE9Ijk5LjM4IiB5MT0iOTQuMTkiIHgyPSIxMjYuMDEiIHkyPSI5NC4xOSIvPjxsaW5lIGNsYXNzPSJkIiB4MT0iMTEyLjY5IiB5MT0iODAuODciIHgyPSIxMTIuNjkiIHkyPSIxMDcuNTEiLz48L2c+PC9zdmc+") 12 12, pointer;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
||||
.text-container.disabled {
|
||||
background-color: #f3f4f6;
|
||||
cursor: not-allowed;
|
||||
border-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.text-container.disabled input {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
||||
.text-container:hover:not(.disabled) {
|
||||
background: #EEF4FF;
|
||||
border: 0.2rem solid #8DB3FE;
|
||||
transform: scale(1.01);
|
||||
|
||||
}
|
||||
|
||||
|
||||
.input-field {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: none;
|
||||
resize: none;
|
||||
font-family: inherit;
|
||||
font-size: 1.4rem;
|
||||
color: #002F54;
|
||||
|
||||
max-width: 6rem;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);*/
|
||||
border: 0.2rem solid #E3EDFF;
|
||||
transition: all 0.1s ease;
|
||||
flex: 1 0 auto;
|
||||
|
||||
max-width: 8rem;
|
||||
|
||||
}
|
||||
|
||||
.dest-mass-route-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dest-mass-route-table-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.dest-mass-route-table-header-wrapper {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
flex-shrink: 0;
|
||||
|
||||
/* Scrollbar verstecken aber Funktionalität behalten */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE/Edge */
|
||||
}
|
||||
|
||||
.dest-mass-route-table-header-wrapper::-webkit-scrollbar {
|
||||
display: none; /* Chrome/Safari/Opera */
|
||||
}
|
||||
|
||||
|
||||
|
||||
.dest-mass-route-table {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
padding-bottom: 2.4rem;
|
||||
}
|
||||
|
||||
.dest-mass-route-table-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1.6rem;
|
||||
padding: 1.6rem 2.4rem;
|
||||
justify-content: flex-start;
|
||||
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid rgba(107, 134, 156, 0.2);
|
||||
font-weight: 500;
|
||||
font-size: 1.4rem;
|
||||
color: #6B869C;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08rem;
|
||||
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.dest-mass-route-table-header-supplier {
|
||||
width: 24rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.dest-mass-route-table-header-dest {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
width: 35rem;
|
||||
gap: 0.8rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,229 +0,0 @@
|
|||
<template>
|
||||
<div class="dest-mass-route-cell">
|
||||
<div class="dest-mass-route-dropdown">
|
||||
<route-dropdown
|
||||
placeholder="No route selected"
|
||||
empty-text="No routes"
|
||||
:disabled="destination.disabled"
|
||||
:show-d2d-warn="showD2DWarn"
|
||||
v-model:model-value="this.selectedRoute"
|
||||
:options="destination.routes"
|
||||
display-key="routeDisplayString"
|
||||
value-key="routeCompareString"
|
||||
@update:modelValue="updateSelectedRoute"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<icon-button :disabled="destination.disabled || this.selectedRoute !== 'D2D_ROUTING'" icon="pencilSimple" @click="openD2DModal">icon</icon-button>
|
||||
</div>
|
||||
|
||||
<modal :state="modalState" @close="modalState = false">
|
||||
<div class="destination-route-modal">
|
||||
<div>
|
||||
<div>D2D Rate [EUR]</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-container">
|
||||
<input :value="this.rateD2d" @blur="validateRateD2d" class="input-field" ref="rate" @keydown.enter="handleEnter('rate', $event)"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div> Lead time [days]</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-container">
|
||||
<input :value="this.leadTimeD2d" @blur="validateLeadTimeD2d" class="input-field" ref="leadTime" @keydown.enter="handleEnter('leadTime', $event)"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
<div></div>
|
||||
<div class="destination-route-modal-footer">
|
||||
<basic-button :show-icon="false" @click="applyD2D">OK</basic-button>
|
||||
<basic-button variant="secondary" :show-icon="false" @click="dismissD2D">Cancel</basic-button>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import RouteDropdown from "@/components/UI/RouteDropdown.vue";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import Modal from "@/components/UI/Modal.vue";
|
||||
import DestinationRoute from "@/components/layout/edit/destination/DestinationRoute.vue";
|
||||
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||
import {parseNumberFromString} from "@/common.js";
|
||||
|
||||
export default {
|
||||
name: "DestinationMassRouteCell",
|
||||
components: {BasicButton, DestinationRoute, Modal, IconButton, RouteDropdown},
|
||||
props: {
|
||||
destination: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedRoute: null,
|
||||
modalState: false,
|
||||
rateD2d: null,
|
||||
leadTimeD2d: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showD2DWarn() {
|
||||
return (this.destination.rateD2d === null || this.destination.leadTimeD2d === null || this.destination.rateD2d === 0 || this.destination.leadTimeD2d === 0);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if(this.destination.isD2d) {
|
||||
this.selectedRoute = 'D2D_ROUTING'
|
||||
} else {
|
||||
this.selectedRoute = this.destination.selectedRoute;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleEnter(currentRef, event) {
|
||||
event.preventDefault();
|
||||
|
||||
// Define the navigation order
|
||||
const inputOrder = ['rate', 'leadTime'];
|
||||
|
||||
const currentIndex = inputOrder.indexOf(currentRef);
|
||||
|
||||
if(currentIndex >= inputOrder.length - 1) {
|
||||
this.validateLeadTimeD2d(event);
|
||||
this.applyD2D();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentIndex !== -1 && currentIndex < inputOrder.length - 1) {
|
||||
const nextRef = inputOrder[currentIndex + 1];
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs[nextRef]) {
|
||||
this.$refs[nextRef].focus();
|
||||
this.$refs[nextRef].select();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
updateSelectedRoute(route) {
|
||||
if(route === 'D2D_ROUTING') {
|
||||
this.destination.selectedRoute = null;
|
||||
this.destination.isD2d = true;
|
||||
} else {
|
||||
this.destination.selectedRoute = route;
|
||||
this.destination.isD2d = false;
|
||||
}
|
||||
},
|
||||
applyD2D() {
|
||||
this.destination.rateD2d = this.rateD2d;
|
||||
this.destination.leadTimeD2d = this.leadTimeD2d;
|
||||
this.dismissD2D()
|
||||
},
|
||||
dismissD2D() {
|
||||
this.modalState = false;
|
||||
},
|
||||
openD2DModal() {
|
||||
this.rateD2d = this.destination.rateD2d;
|
||||
this.leadTimeD2d = this.destination.leadTimeD2d;
|
||||
this.modalState = true;
|
||||
},
|
||||
validateRateD2d(event) {
|
||||
|
||||
const value = parseNumberFromString(event.target.value, 2);
|
||||
const validatedValue = Math.max(0, value);
|
||||
const stringified = validatedValue.toFixed(2);
|
||||
|
||||
this.rateD2d = validatedValue === 0 ? null : validatedValue;
|
||||
event.target.value = stringified;
|
||||
},
|
||||
validateLeadTimeD2d(event) {
|
||||
|
||||
const value = parseNumberFromString(event.target.value, 0);
|
||||
const validatedValue = Math.max(0, value);
|
||||
const stringified = validatedValue.toFixed();
|
||||
|
||||
this.leadTimeD2d = validatedValue === 0 ? null : validatedValue;
|
||||
event.target.value = stringified;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.destination-route-modal {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 1.6rem;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.destination-route-modal-footer {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dest-mass-route-cell {
|
||||
width: 35rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 400;
|
||||
gap: 0.8rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dest-mass-route-dropdown {
|
||||
width: 30rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
gap: 0.8rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: none;
|
||||
resize: none;
|
||||
font-family: inherit;
|
||||
font-size: 1.4rem;
|
||||
color: #002F54;
|
||||
width: 100%;
|
||||
min-width: 5rem;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
border: 0.2rem solid #E3EDFF;
|
||||
transition: all 0.1s ease;
|
||||
flex: 1 1 fit-content(80rem);
|
||||
}
|
||||
|
||||
.text-container:hover {
|
||||
background: #EEF4FF;
|
||||
border: 0.2rem solid #8DB3FE;
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
<template>
|
||||
<div class="dest-mass-route-row-container">
|
||||
<div class="dest-mass-route-row-supplier">
|
||||
<flag :iso="row.supplier.country.iso_code"/>
|
||||
{{ row.supplier.name }}
|
||||
</div>
|
||||
<destination-mass-route-cell :destination="dest" v-for="dest in row.destinations" class="dest-mass-route-row-dest"
|
||||
:key="dest.id">
|
||||
</destination-mass-route-cell>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
import Flag from "@/components/UI/Flag.vue";
|
||||
import RouteDropdown from "@/components/UI/RouteDropdown.vue";
|
||||
import DestinationMassRouteCell from "@/components/layout/edit/destination/mass/DestinationMassRouteCell.vue";
|
||||
|
||||
export default {
|
||||
name: "DestinationMassRouteRow",
|
||||
components: {DestinationMassRouteCell, RouteDropdown, Flag, Checkbox},
|
||||
props: {
|
||||
row: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
routes() {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleDropdown() {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
|
||||
.dest-mass-route-row-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1.6rem;
|
||||
padding: 1.2rem 2.4rem;
|
||||
justify-content: flex-start;
|
||||
border-bottom: 0.16rem solid #f3f4f6;
|
||||
transition: background-color 0.2s ease;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.dest-mass-route-row-container:hover {
|
||||
background-color: rgba(107, 134, 156, 0.05);
|
||||
}
|
||||
|
||||
|
||||
.dest-mass-route-row-supplier {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.8rem;
|
||||
width: 24rem;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
<template>
|
||||
<teleport to="body">
|
||||
<modal :z-index="9001" :state="showHelp">
|
||||
<div class="help-modal-container">
|
||||
<div class="help-modal-header">
|
||||
<icon-button icon="x" @click="helpStore.closeHelp()"/>
|
||||
</div>
|
||||
<div class="help-container">
|
||||
<div class="help-menu-container">
|
||||
<the-help-menu @changePage="updatePage" :currentPage="currentPage" :pages="pages"/>
|
||||
</div>
|
||||
<div class="help-content-container">
|
||||
<h2 class="page-header">{{ title }}</h2>
|
||||
<tab-container :tabs="tabsConfig"></tab-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from "@/components/UI/Modal.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import Help from "@/components/layout/help/Help.vue";
|
||||
import {useHelpStore} from "@/store/help.js";
|
||||
import TheHelpMenu from "@/components/layout/help/TheHelpMenu.vue";
|
||||
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import TabContainer from "@/components/UI/TabContainer.vue";
|
||||
import {markRaw} from "vue";
|
||||
import Nodes from "@/components/layout/config/Nodes.vue";
|
||||
import HelpVideo from "@/components/layout/help/HelpVideo.vue";
|
||||
import HelpText from "@/components/layout/help/HelpText.vue";
|
||||
|
||||
export default {
|
||||
name: "TheHelpSystem",
|
||||
components: {TabContainer, IconButton, BasicButton, TheHelpMenu, Help, Modal},
|
||||
data() {
|
||||
return {
|
||||
tabsConfig: [
|
||||
{
|
||||
title: 'Video',
|
||||
component: markRaw(HelpVideo),
|
||||
props: {isSelected: false},
|
||||
},
|
||||
{
|
||||
title: 'Text',
|
||||
component: markRaw(HelpText),
|
||||
props: {isSelected: false},
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useHelpStore),
|
||||
showHelp() {
|
||||
return this.helpStore.showHelp;
|
||||
},
|
||||
currentPage() {
|
||||
return this.helpStore.currentPage;
|
||||
},
|
||||
title() {
|
||||
return this.helpStore.title;
|
||||
},
|
||||
pages() {
|
||||
return this.helpStore.pages;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async updatePage(page) {
|
||||
await this.helpStore.getContent(page);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.helpStore.loadPages();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
|
||||
.help-modal-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
width: min(80vw, 180rem);
|
||||
height: min(80vh, 120rem);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.help-modal-header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.help-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 2.4rem;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.help-menu-container {
|
||||
flex: 0 0 auto;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.help-content-container {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -7,53 +7,42 @@
|
|||
<report-chart
|
||||
title=""
|
||||
:mek_a="report.costs.mek_a.total"
|
||||
:logistics_costs="report.overview.mek_b.total-report.costs.mek_a.total"
|
||||
:chance_cost="report.overview.opportunity_scenario.total"
|
||||
:risk_cost="report.overview.risk_scenario.total"
|
||||
:logistics_costs="report.risk.mek_b.total-report.costs.mek_a.total"
|
||||
:chance_cost="report.risk.opportunity_scenario.total"
|
||||
:risk_cost="report.risk.risk_scenario.total"
|
||||
:scale="chartScale"
|
||||
></report-chart>
|
||||
</div>
|
||||
|
||||
<!-- summary -->
|
||||
<div class="box-gap">
|
||||
<collapsible-box :is-collapsable="false" variant="border" title="Summary" size="m" :stretch-content="true">
|
||||
<collapsible-box :is-collapsable="false" variant="border" title="Overview" size="m" :stretch-content="true">
|
||||
<div class="report-content-container--3-col">
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>MEK A</div>
|
||||
<div class="report-content-data-cell">{{ report.overview.mek_a.total.toFixed(2) }} €</div>
|
||||
<div class="report-content-data-cell">{{
|
||||
`${(report.overview.mek_a.percentage * 100).toFixed(2)} %`
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Logistics cost</div>
|
||||
<div class="report-content-data-cell">{{ report.overview.logistics.total.toFixed(2) }} €</div>
|
||||
<div class="report-content-data-cell">{{
|
||||
`${(report.overview.logistics.percentage * 100).toFixed(2)} %`
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div class="report-content-row-highlight">MEK B</div>
|
||||
<div class="report-content-data-cell report-content-row-highlight">{{
|
||||
report.overview.mek_b.total.toFixed(2)
|
||||
}} €
|
||||
</div>
|
||||
<div class="report-content-data-cell report-content-row-highlight">{{
|
||||
`${(report.overview.mek_b.percentage * 100).toFixed(2)} %`
|
||||
<div class="report-content-data-cell report-content-row-highlight">{{ report.risk.mek_b.total.toFixed(2) }} €</div>
|
||||
<div class="report-content-data-cell"></div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Opportunity scenario</div>
|
||||
<div class="report-content-data-cell">{{ report.risk.opportunity_scenario.total.toFixed(2) }} €</div>
|
||||
<div class="report-content-data-cell">{{
|
||||
`${(report.risk.opportunity_scenario.percentage * 100).toFixed(2)} %`
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Risk scenario</div>
|
||||
<div class="report-content-data-cell">{{ report.risk.risk_scenario.total.toFixed(2) }} €</div>
|
||||
<div class="report-content-data-cell">{{ `${(report.risk.risk_scenario.percentage * 100).toFixed(2)} %`}}</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</collapsible-box>
|
||||
</div>
|
||||
<!-- weighted cost breakdown-->
|
||||
<div class="box-gap">
|
||||
<collapsible-box :is-collapsable="false" variant="border" title="Weighted cost breakdown" size="m"
|
||||
:stretch-content="true">
|
||||
|
|
@ -153,155 +142,18 @@
|
|||
<div class="report-content-data-cell">{{ (report.costs.capital.percentage * 100).toFixed(2) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div class="report-content-row-highlight">Total</div>
|
||||
<div class="report-content-data-cell report-content-row-highlight">{{
|
||||
report.costs.total.total.toFixed(2)
|
||||
}}
|
||||
</div>
|
||||
<div class="report-content-data-cell report-content-row-highlight">
|
||||
{{ (report.costs.total.percentage * 100).toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</collapsible-box>
|
||||
</div>
|
||||
|
||||
<!-- all time high/low container rate-->
|
||||
<div class="box-gap">
|
||||
<collapsible-box :is-collapsable="true" variant="border" title="Transport costs fluctuations"
|
||||
:initially-collapsed="true"
|
||||
:stretch-content="true">
|
||||
|
||||
<div class="report-content-container--3-col-2">
|
||||
<div class="box-gap" :key="premise.id" v-for="(premise, idx) in report.premises">
|
||||
|
||||
<div class="report-content-row">
|
||||
<div></div>
|
||||
<div class="report-content-data-header-cell">total [€]</div>
|
||||
<div class="report-content-data-header-cell">of MEK B [%]</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div class="">Current scenario</div>
|
||||
<div class="report-content-data-cell">{{
|
||||
report.overview.mek_b.total.toFixed(2)
|
||||
}} €
|
||||
</div>
|
||||
<div class="report-content-data-cell">{{
|
||||
`${(report.overview.mek_b.percentage * 100).toFixed(2)}`
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Opportunity scenario</div>
|
||||
<div class="report-content-data-cell">{{ report.overview.opportunity_scenario.total.toFixed(2) }} €</div>
|
||||
<div class="report-content-data-cell">{{
|
||||
`${(report.overview.opportunity_scenario.percentage * 100).toFixed(2)}`
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Risk scenario</div>
|
||||
<div class="report-content-data-cell">{{ report.overview.risk_scenario.total.toFixed(2) }} €</div>
|
||||
<div class="report-content-data-cell">
|
||||
{{ `${(report.overview.risk_scenario.percentage * 100).toFixed(2)}` }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</collapsible-box>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- material and handling unit-->
|
||||
<div class="box-gap">
|
||||
<collapsible-box :is-collapsable="false" variant="border" title="Material" size="m"
|
||||
:stretch-content="true">
|
||||
|
||||
<div class="report-content-container--2-col">
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Part number</div>
|
||||
<div class="report-content-data-cell"> {{ report.material.part_number }}</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>HS code</div>
|
||||
<div class="report-content-data-cell"> {{ report.premises.hs_code }}</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Tariff rate</div>
|
||||
<div class="report-content-data-cell"> {{ (report.premises.tariff_rate * 100).toFixed(2) }}%</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Oversea share</div>
|
||||
<div class="report-content-data-cell">{{ (report.premises.oversea_share * 100).toFixed(2) }}%</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row" v-if="(report.premises.air_freight_share ?? null) !== null">
|
||||
<div>Airfreight share</div>
|
||||
<div class="report-content-data-cell">{{ (report.premises.air_freight_share * 100).toFixed(2) }}%</div>
|
||||
</div>
|
||||
<div class="report-content-row">
|
||||
<div>Safety stock [w-days]</div>
|
||||
<div class="report-content-data-cell">{{ report.premises.safety_stock }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="report-sub-header">Handling unit</div>
|
||||
|
||||
<div class="report-content-container--2-col">
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Dimensions [{{ report.premises.dimension_unit }}]</div>
|
||||
<div class="report-content-data-cell">{{
|
||||
toFixedDimension(report.premises.length, report.premises.dimension_unit)
|
||||
}} x
|
||||
{{ toFixedDimension(report.premises.width, report.premises.dimension_unit) }} x
|
||||
{{ toFixedDimension(report.premises.height, report.premises.dimension_unit) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Weight [{{ report.premises.weight_unit }}]</div>
|
||||
<div class="report-content-data-cell">{{
|
||||
toFixedWeight(report.premises.weight, report.premises.weight_unit)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Unit count</div>
|
||||
<div class="report-content-data-cell">{{ report.premises.hu_unit_count }}</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Mixed transport</div>
|
||||
<div class="report-content-data-cell">{{ report.premises.mixable ? 'Yes' : 'No' }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</collapsible-box>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- destinations -->
|
||||
<div class="box-gap" :key="destination.id" v-for="(destination, idx) in report.destinations">
|
||||
|
||||
<collapsible-box class="report-content-container" variant="border" :title="destination.destination.name"
|
||||
<collapsible-box class="report-content-container" variant="border" :title="premise.destination.name"
|
||||
:stretch-content="true" :initially-collapsed="true">
|
||||
<div>
|
||||
<report-route :sections="destination.sections" :destination="destination.destination"
|
||||
<report-route :sections="premise.sections" :destination="premise.destination"
|
||||
:route-section-scale="routeSectionScale[idx]"></report-route>
|
||||
|
||||
<div class="report-sub-header">General</div>
|
||||
|
|
@ -310,19 +162,69 @@
|
|||
|
||||
<div class="report-content-row">
|
||||
<div>Annual Quantity</div>
|
||||
<div class="report-content-data-cell">{{ destination.annual_quantity }}</div>
|
||||
<div class="report-content-data-cell">{{ premise.annual_quantity }}</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>HS code</div>
|
||||
<div class="report-content-data-cell">{{ premise.hs_code }}</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Tariff rate</div>
|
||||
<div class="report-content-data-cell">{{ (premise.tariff_rate * 100).toFixed(2) }}%</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Oversea share</div>
|
||||
<div class="report-content-data-cell">{{ (premise.oversea_share * 100).toFixed(2) }}%</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row" v-if="(premise.air_freight_share ?? null) !== null">
|
||||
<div>Airfreight share</div>
|
||||
<div class="report-content-data-cell">{{ (premise.air_freight_share * 100).toFixed(2) }}%</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Transit time [days]</div>
|
||||
<div class="report-content-data-cell">{{ destination.transport_time }}</div>
|
||||
<div class="report-content-data-cell">{{ premise.transport_time }}</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Safety stock [w-days]</div>
|
||||
<div class="report-content-data-cell">{{ premise.safety_stock }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="report-sub-header">Handling unit</div>
|
||||
|
||||
<div class="report-content-container--2-col">
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Dimensions [{{ premise.dimension_unit }}]</div>
|
||||
<div class="report-content-data-cell">{{ toFixedDimension(premise.length, premise.dimension_unit) }} x
|
||||
{{ toFixedDimension(premise.width, premise.dimension_unit) }} x
|
||||
{{ toFixedDimension(premise.height, premise.dimension_unit) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Weight [{{ premise.weight_unit }}]</div>
|
||||
<div class="report-content-data-cell">{{ toFixedWeight(premise.weight, premise.weight_unit) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Unit count</div>
|
||||
<div class="report-content-data-cell">{{ premise.hu_unit_count }}</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Mixed transport</div>
|
||||
<div class="report-content-data-cell">{{ premise.mixed ? 'Yes' : 'No' }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="report-sub-header">Container</div>
|
||||
|
||||
|
|
@ -330,33 +232,29 @@
|
|||
|
||||
<div class="report-content-row">
|
||||
<div>Stacked layers</div>
|
||||
<div class="report-content-data-cell">{{
|
||||
hasMainRunOrD2D(destination.sections) ? destination.layer : '-'
|
||||
}}
|
||||
</div>
|
||||
<div class="report-content-data-cell">{{ hasMainRun(premise.sections) ? premise.layer : '-' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Container unit count</div>
|
||||
<div class="report-content-data-cell">
|
||||
{{
|
||||
hasMainRunOrD2D(destination.sections) ? (destination.unit_count * report.premises.hu_unit_count) : '-'
|
||||
}}
|
||||
{{ hasMainRun(premise.sections) ? (premise.unit_count * premise.hu_unit_count) : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Container type</div>
|
||||
<div class="report-content-data-cell">
|
||||
{{ hasMainRunOrD2D(destination.sections) ? getContainerTypeName(destination.container_type) : '-' }}
|
||||
{{ hasMainRun(premise.sections) ? getContainerTypeName(premise.container_type) : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Limiting factor</div>
|
||||
<div class="report-content-data-cell">
|
||||
{{ hasMainRunOrD2D(destination.sections) ? destination.weight_exceeded ? 'Weight' : 'Volume' : '-' }}
|
||||
{{ hasMainRun(premise.sections) ? premise.weight_exceeded ? 'Weight' : 'Volume' : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -395,8 +293,8 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
hasMainRunOrD2D(sections) {
|
||||
return sections.some(section => section.transport_type === 'SEA' || section.transport_type === 'RAIL' || section.rate_type === 'D2D');
|
||||
hasMainRun(sections) {
|
||||
return sections.some(section => section.transport_type === 'SEA' || section.transport_type === 'RAIL');
|
||||
},
|
||||
shorten(text, length) {
|
||||
if (text !== null && text !== undefined && text.length > length) {
|
||||
|
|
@ -492,14 +390,6 @@ export default {
|
|||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.report-content-container--3-col-2 {
|
||||
display: grid;
|
||||
grid-template-columns: 5fr 3fr 3fr;
|
||||
gap: 1rem;
|
||||
margin-top: 1.6rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.report-content-row {
|
||||
display: contents;
|
||||
color: #6B869C;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import log from 'loglevel'
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
log.setLevel('silent')
|
||||
log.setLevel('debug') //TODO change back to 'silent'
|
||||
} else {
|
||||
log.setLevel('debug')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import router from './router.js';
|
||||
//import store from './store/index.js';
|
||||
import {setupErrorBuffer} from './store/notification.js'
|
||||
import {createApp} from 'vue'
|
||||
import {createPinia} from 'pinia';
|
||||
|
|
@ -33,8 +34,7 @@ import {
|
|||
PhTruckTrailer,
|
||||
PhUpload,
|
||||
PhWarning,
|
||||
PhX,
|
||||
PhExclamationMark, PhMapPin, PhEmpty, PhShippingContainer, PhPackage, PhVectorThree, PhTag, PhInfo
|
||||
PhX
|
||||
} from "@phosphor-icons/vue";
|
||||
import {setupSessionRefresh} from "@/store/activeuser.js";
|
||||
|
||||
|
|
@ -61,8 +61,6 @@ app.component('PhTruckTrailer', PhTruckTrailer);
|
|||
app.component('PhTruck', PhTruck);
|
||||
app.component('PhBoat', PhBoat);
|
||||
app.component('PhTrain', PhTrain);
|
||||
app.component('PhEmpty', PhEmpty);
|
||||
app.component('PhShippingContainer', PhShippingContainer);
|
||||
app.component('PhPencilSimple', PhPencilSimple);
|
||||
app.component('PhX', PhX);
|
||||
app.component('PhCloudArrowUp', PhCloudArrowUp);
|
||||
|
|
@ -76,13 +74,6 @@ app.component('PhFile', PhFile);
|
|||
app.component("PhDesktop", PhDesktop );
|
||||
app.component("PhHardDrives", PhHardDrives );
|
||||
app.component("PhClipboard", PhClipboard );
|
||||
app.component("PhExclamationMark", PhExclamationMark );
|
||||
app.component("PhMapPin", PhMapPin);
|
||||
app.component("PhPackage", PhPackage);
|
||||
app.component("PhVectorThree", PhVectorThree);
|
||||
app.component("PhTag", PhTag);
|
||||
app.component("PhInfo", PhInfo);
|
||||
|
||||
|
||||
app.use(router);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,6 @@
|
|||
<template>
|
||||
<div class="start-calculation-container">
|
||||
<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>
|
||||
<h2 class="page-header">Create Calculation</h2>
|
||||
|
||||
|
||||
<div class="part-numbers-headers">
|
||||
|
|
@ -32,15 +23,8 @@
|
|||
<textarea v-model="partNumberField" name="partNumbers" cols="140" rows="15"></textarea>
|
||||
</div>
|
||||
<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="closeModal('partNumber')" :show-icon="false" variant="secondary">Cancel</basic-button>
|
||||
|
||||
</div>
|
||||
<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>
|
||||
</modal>
|
||||
|
|
@ -104,22 +88,11 @@ import CreateNewNode from "@/components/layout/node/CreateNewNode.vue";
|
|||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
import {UrlSafeBase64} from "@/common.js";
|
||||
import {useNotificationStore} from "@/store/notification.js";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import {useHelpStore} from "@/store/help.js";
|
||||
|
||||
|
||||
export default {
|
||||
name: "CalculationAssistant",
|
||||
components: {
|
||||
IconButton,
|
||||
Checkbox,
|
||||
CreateNewNode,
|
||||
Modal,
|
||||
SupplierItem,
|
||||
MaterialItem,
|
||||
BasicButton,
|
||||
AutosuggestSearchbar
|
||||
},
|
||||
components: {Checkbox, CreateNewNode, Modal, SupplierItem, MaterialItem, BasicButton, AutosuggestSearchbar},
|
||||
computed: {
|
||||
...mapStores(useNodeStore, useAssistantStore, useNotificationStore),
|
||||
showPartNumberModal() {
|
||||
|
|
@ -135,7 +108,6 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
useHelpStore,
|
||||
setUseExisting(useExisting) {
|
||||
this.assistantStore.setCreateEmpty(!useExisting);
|
||||
},
|
||||
|
|
@ -188,7 +160,7 @@ export default {
|
|||
parsePartNumbers() {
|
||||
this.closeModal('partNumber');
|
||||
|
||||
if (this.partNumberField.trim().length !== 0)
|
||||
if(this.partNumberField.trim().length !== 0)
|
||||
this.assistantStore.getMaterialsAndSuppliers(this.partNumberField);
|
||||
|
||||
this.partNumberField = '';
|
||||
|
|
@ -207,22 +179,6 @@ export default {
|
|||
|
||||
<style scoped>
|
||||
|
||||
.start-calculation-help {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 1.6rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.start-calculation-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.start-calculation-footer-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -276,17 +232,10 @@ textarea {
|
|||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.part-number-modal-action-help {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.part-number-modal-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-end;
|
||||
gap: 1.6rem
|
||||
}
|
||||
|
||||
|
|
@ -297,13 +246,6 @@ textarea {
|
|||
margin-bottom: 1.6rem;
|
||||
}
|
||||
|
||||
.part-number-modal-action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 1.6rem
|
||||
}
|
||||
|
||||
.item-list {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
|
|
|
|||
|
|
@ -1,157 +1,92 @@
|
|||
<template>
|
||||
<div class="edit-calculation-container"
|
||||
:class="{ 'has-selection': hasSelection, 'apply-filter': applyFilter, 'add-all': addAll }">
|
||||
<div class="edit-calculation-container" :class="{ 'has-selection': hasSelection }">
|
||||
<div class="header-container">
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
<h2 class="page-header">Mass edit calculation</h2>
|
||||
<div class="header-controls">
|
||||
<basic-button :show-icon="true"
|
||||
:disabled="disableButtons"
|
||||
icon="MapPin" variant="primary"
|
||||
@click="destMgmt"
|
||||
>Destination manager
|
||||
</basic-button>
|
||||
<basic-button :show-icon="true"
|
||||
:disabled="disableButtons"
|
||||
icon="Calculator" variant="primary"
|
||||
@click="calculate"
|
||||
>Calculate & close
|
||||
</basic-button>
|
||||
<basic-button :show-icon="false"
|
||||
:disabled="disableButtons"
|
||||
:disabled="premiseEditStore.selectedLoading"
|
||||
variant="secondary"
|
||||
@click="close"
|
||||
@click="closeMassEdit"
|
||||
>Close
|
||||
</basic-button>
|
||||
<basic-button :show-icon="true"
|
||||
:disabled="premiseEditStore.selectedLoading"
|
||||
icon="Calculator" variant="primary"
|
||||
@click="startCalculation"
|
||||
>Calculate & close
|
||||
</basic-button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="edit-calculation-list-container">
|
||||
<div class="edit-calculation-list-header">
|
||||
<div>
|
||||
<checkbox @checkbox-changed="updateCheckBoxes" :checked="overallCheck"
|
||||
:indeterminate="overallIndeterminate" :disabled="!showData"></checkbox>
|
||||
</div>
|
||||
<div class="edit-calculation-list-header-cell edit-calculation-list-header-cell--clickable"
|
||||
:class="{'edit-calculation-list-header-cell--selected': premiseEditStore.activeSort === 'material'}"
|
||||
@click="premiseEditStore.sort('material')">Material
|
||||
<sort-button :active="premiseEditStore.activeSort === 'material'"
|
||||
:direction="premiseEditStore.directionSort('material')"/>
|
||||
</div>
|
||||
<div class="edit-calculation-list-header-cell">Price</div>
|
||||
<div class="edit-calculation-list-header-cell">Packaging</div>
|
||||
<div class="edit-calculation-list-header-cell edit-calculation-list-header-cell--clickable"
|
||||
:class="{'edit-calculation-list-header-cell--selected': premiseEditStore.activeSort === 'supplier'}"
|
||||
@click="premiseEditStore.sort('supplier')">Supplier
|
||||
<sort-button :active="premiseEditStore.activeSort === 'supplier'"
|
||||
:direction="premiseEditStore.directionSort('supplier')"/>
|
||||
</div>
|
||||
<div class="edit-calculation-list-header-cell">Annual Quantity</div>
|
||||
<div class="edit-calculation-list-header-cell">Routes</div>
|
||||
<div class="edit-calculation-list-header-cell">Actions</div>
|
||||
</div>
|
||||
<transition name="list-edit-container" tag="div">
|
||||
<transition-group name="list-edit" mode="out-in" class="edit-calculation-list-container" tag="div">
|
||||
|
||||
<!-- Loading Spinner - außerhalb der TransitionGroup -->
|
||||
<div v-if="showLoading" class="spinner-container">
|
||||
<spinner class="space-around"></spinner>
|
||||
</div>
|
||||
<div class="edit-calculation-list-header" key="header">
|
||||
<div>
|
||||
<checkbox @checkbox-changed="updateCheckBoxes" :checked="overallCheck"></checkbox>
|
||||
</div>
|
||||
<div>Material</div>
|
||||
<div>Price</div>
|
||||
<div>Packaging</div>
|
||||
<div>Supplier</div>
|
||||
<div>Destinations & routes</div>
|
||||
<div>Actions</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State - außerhalb der TransitionGroup -->
|
||||
<div v-else-if="showEmpty" class="empty-container">
|
||||
<span class="space-around">No Calculations found.</span>
|
||||
</div>
|
||||
|
||||
<!-- Rows mit Sort-Animation -->
|
||||
<transition-group
|
||||
v-else
|
||||
name="sort-list"
|
||||
tag="div"
|
||||
class="edit-calculation-list-body"
|
||||
@before-enter="onBeforeEnter"
|
||||
@enter="onEnter"
|
||||
>
|
||||
<bulk-edit-row
|
||||
v-for="(premise, index) of premises"
|
||||
:key="premise.id"
|
||||
:id="premise.id"
|
||||
:premise="premise"
|
||||
:data-index="index"
|
||||
class="edit-calculation-list-item"
|
||||
@action="onClickAction"
|
||||
@select="updateCheckBox"
|
||||
@remove="updateUrl">
|
||||
<div v-if="showLoading" class="spinner-container" key="spinner">
|
||||
<spinner class="space-around"></spinner>
|
||||
</div>
|
||||
|
||||
<div v-else-if="showEmpty" class="empty-container" key="empty">
|
||||
<span class="space-around">No Calculations found.</span>
|
||||
</div>
|
||||
|
||||
<bulk-edit-row v-else class="edit-calculation-list-item" v-for="premise of this.premiseEditStore.getPremisses"
|
||||
:key="premise.id" :id="premise.id" :premise="premise" @action="onClickAction"
|
||||
@remove="updateUrl">
|
||||
</bulk-edit-row>
|
||||
</transition-group>
|
||||
</div>
|
||||
|
||||
<mass-edit-dialog v-if="showData" :show="showMultiselectAction" @action="onToolbarAction"
|
||||
|
||||
</transition-group>
|
||||
</transition>
|
||||
|
||||
<mass-edit-dialog v-if="showData" :show="showMultiselectAction" @action="multiselectAction"
|
||||
:select-count="selectCount"></mass-edit-dialog>
|
||||
|
||||
|
||||
<modal-dialog title="Missing destinations" :state="modalDialogShow"
|
||||
message="Some of the selected calculations have no destinations set. Would you like to edit the destinations first?"
|
||||
accept-text="Yes" :deny-text="denyText" @click="modalDialogClick"></modal-dialog>
|
||||
|
||||
<modal :z-index="2000" :state="modalShow">
|
||||
<modal :z-index="2000" :state="showEditModal">
|
||||
<div class="modal-content-container">
|
||||
<h3 class="sub-header">{{ modalTitle }}</h3>
|
||||
<component
|
||||
:is="modalComponentType"
|
||||
ref="modalComponent"
|
||||
|
||||
|
||||
v-model:partNumber="modalProps.partNumber"
|
||||
v-model:hsCode="modalProps.hsCode"
|
||||
v-model:tariffRate="modalProps.tariffRate"
|
||||
v-model:tariffUnlocked="modalProps.tariffUnlocked"
|
||||
v-model:description="modalProps.description"
|
||||
|
||||
v-model:price="modalProps.price"
|
||||
v-model:overSeaShare="modalProps.overSeaShare"
|
||||
v-model:includeFcaFee="modalProps.includeFcaFee"
|
||||
|
||||
v-model:length="modalProps.length"
|
||||
v-model:width="modalProps.width"
|
||||
v-model:height="modalProps.height"
|
||||
v-model:weight="modalProps.weight"
|
||||
v-model:weightUnit="modalProps.weightUnit"
|
||||
v-model:dimensionUnit="modalProps.dimensionUnit"
|
||||
v-model:unitCount="modalProps.unitCount"
|
||||
v-model:mixable="modalProps.mixable"
|
||||
v-model:stackable="modalProps.stackable"
|
||||
|
||||
v-model:hideDescription="modalProps.hideDescription"
|
||||
|
||||
:type="modalType"
|
||||
:premiseIds="editIds"
|
||||
|
||||
:fromMassEdit="true"
|
||||
:is="componentType"
|
||||
v-model:partNumber="componentProps.partNumber"
|
||||
v-model:hsCode="componentProps.hsCode"
|
||||
v-model:tariffRate="componentProps.tariffRate"
|
||||
v-model:tariffUnlocked="componentProps.tariffUnlocked"
|
||||
v-model:description="componentProps.description"
|
||||
v-model:price="componentProps.price"
|
||||
v-model:overSeaShare="componentProps.overSeaShare"
|
||||
v-model:includeFcaFee="componentProps.includeFcaFee"
|
||||
v-model:length="componentProps.length"
|
||||
v-model:width="componentProps.width"
|
||||
v-model:height="componentProps.height"
|
||||
v-model:weight="componentProps.weight"
|
||||
v-model:weightUnit="componentProps.weightUnit"
|
||||
v-model:dimensionUnit="componentProps.dimensionUnit"
|
||||
v-model:unitCount="componentProps.unitCount"
|
||||
v-model:mixable="componentProps.mixable"
|
||||
v-model:stackable="componentProps.stackable"
|
||||
v-model:hideDescription="componentProps.hideDescription"
|
||||
:countryId=null
|
||||
:responsive="false"
|
||||
|
||||
@close="closeEditModalAction('cancel')"
|
||||
@accept="closeEditModalAction('accept')"
|
||||
|
||||
>
|
||||
</component>
|
||||
|
||||
<div class="modal-content-footer" @keydown="handleKeyDown($event)">
|
||||
<basic-button v-if="!modalCloseOnly" :show-icon="false" @click="closeEditModalAction('accept')">OK
|
||||
</basic-button>
|
||||
<basic-button variant="secondary" :show-icon="false" @click="closeEditModalAction('cancel')">
|
||||
{{ modalCloseOnly ? "Close" : "Cancel" }}
|
||||
<div class="modal-content-footer" >
|
||||
<basic-button v-if="!modalCloseOnly" :show-icon="false" @click="closeEditModalAction('accept')">OK</basic-button>
|
||||
<basic-button variant="secondary" :show-icon="false" @click="closeEditModalAction('cancel')"> {{ modalCloseOnly ? "Close" : "Cancel" }}
|
||||
</basic-button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -176,34 +111,21 @@ import Modal from "@/components/UI/Modal.vue";
|
|||
import PriceEdit from "@/components/layout/edit/PriceEdit.vue";
|
||||
import MaterialEdit from "@/components/layout/edit/MaterialEdit.vue";
|
||||
import PackagingEdit from "@/components/layout/edit/PackagingEdit.vue";
|
||||
|
||||
import {useNotificationStore} from "@/store/notification.js";
|
||||
import {useDestinationEditStore} from "@/store/destinationEdit.js";
|
||||
import SortButton from "@/components/UI/SortButton.vue";
|
||||
import DestinationMassEdit from "@/components/layout/edit/destination/mass/DestinationMassEdit.vue";
|
||||
import DestMassCreate from "@/components/layout/edit/destination/mass/DestMassCreate.vue";
|
||||
import ModalDialog from "@/components/UI/ModalDialog.vue";
|
||||
import destinationEdit from "@/components/layout/edit/destination/DestinationEdit.vue";
|
||||
import DestinationListView from "@/components/layout/edit/DestinationListView.vue";
|
||||
import logger from "@/logger.js";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import {useHelpStore} from "@/store/help.js";
|
||||
import {useNotificationStore} from "@/store/notification.js";
|
||||
|
||||
|
||||
const COMPONENT_TYPES = {
|
||||
price: PriceEdit,
|
||||
material: MaterialEdit,
|
||||
packaging: PackagingEdit,
|
||||
destinations: DestMassCreate,
|
||||
routes: DestinationMassEdit,
|
||||
amount: DestinationMassEdit
|
||||
destinations: DestinationListView,
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "MassEdit",
|
||||
components: {
|
||||
IconButton,
|
||||
ModalDialog,
|
||||
SortButton,
|
||||
Modal,
|
||||
MassEditDialog,
|
||||
ListEdit,
|
||||
|
|
@ -213,51 +135,19 @@ export default {
|
|||
BulkEditRow,
|
||||
BasicButton
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ids: [],
|
||||
isCtrlPressed: false,
|
||||
isShiftPressed: false,
|
||||
overallCheck: false,
|
||||
overallIndeterminate: false,
|
||||
bulkQuery: null,
|
||||
modalTitle: null,
|
||||
modalType: null,
|
||||
modalProps: null,
|
||||
editIds: null,
|
||||
processingMessage: "Please wait. Processing ...",
|
||||
showCalculationModal: false,
|
||||
isInitialLoad: true,
|
||||
modalDialogShow: false,
|
||||
modalStash: null,
|
||||
denyText: 'No'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(usePremiseEditStore, useNotificationStore, useDestinationEditStore),
|
||||
disableButtons() {
|
||||
return this.premiseEditStore.selectedLoading;
|
||||
},
|
||||
premises() {
|
||||
return this.premiseEditStore.getPremisses;
|
||||
},
|
||||
...mapStores(usePremiseEditStore, useNotificationStore),
|
||||
hasSelection() {
|
||||
if (this.premiseEditStore.isLoading || this.premiseEditStore.selectedLoading) {
|
||||
return false;
|
||||
}
|
||||
return !this.addAll && !this.applyFilter && this.premiseEditStore.someChecked;
|
||||
},
|
||||
applyFilter() {
|
||||
return this.isCtrlPressed && this.isShiftPressed;
|
||||
},
|
||||
addAll() {
|
||||
return this.isCtrlPressed && !this.isShiftPressed;
|
||||
},
|
||||
showMultiselectAction() {
|
||||
return this.selectCount > 0;
|
||||
return this.premiseEditStore.getSelectedPremissesIds?.length > 0;
|
||||
},
|
||||
selectCount() {
|
||||
return this.premiseEditStore.getSelectedPremiseIds?.length ?? 0;
|
||||
return this.selectedPremisses?.length ?? 0;
|
||||
},
|
||||
selectedPremisses() {
|
||||
return this.premiseEditStore.getSelectedPremisses;
|
||||
},
|
||||
showEmpty() {
|
||||
return this.premiseEditStore.showEmpty;
|
||||
|
|
@ -268,73 +158,82 @@ export default {
|
|||
showData() {
|
||||
return this.premiseEditStore.showData;
|
||||
},
|
||||
modalCloseOnly() {
|
||||
//TODO: fix material editing.
|
||||
return this.modalType === 'material' && !this.modalProps.tariffUnlocked; //TODO: check all selected.
|
||||
overallCheck() {
|
||||
return this.premiseEditStore.isLoading ? false : this.premiseEditStore.getPremisses?.every(p => p.selected === true) ?? false;
|
||||
},
|
||||
modalShow() {
|
||||
showMultiselectAction() {
|
||||
return this.selectCount > 0;
|
||||
},
|
||||
modalCloseOnly() {
|
||||
return this.modalType === 'material' && !this.componentProps.tariffUnlocked;
|
||||
},
|
||||
showEditModal() {
|
||||
return ((this.modalType ?? null) !== null);
|
||||
},
|
||||
modalComponentType() {
|
||||
componentProps() {
|
||||
return this.componentData?.props ?? null;
|
||||
},
|
||||
componentType() {
|
||||
return this.modalType ? COMPONENT_TYPES[this.modalType] : null;
|
||||
},
|
||||
componentData() {
|
||||
return this.modalType ? this.componentsData[this.modalType] : null;
|
||||
},
|
||||
showProcessingModal() {
|
||||
return this.premiseEditStore.showProcessingModal || this.destinationEditStore.showProcessingModal;
|
||||
return this.premiseEditStore.showProcessingModal || this.showCalculationModal;
|
||||
},
|
||||
shownProcessingMessage() {
|
||||
if (this.premiseEditStore.showProcessingModal)
|
||||
return "Please wait. Prepare calculation ..."
|
||||
|
||||
return this.processingMessage;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
showProcessingModal(newState, _) {
|
||||
if (newState) {
|
||||
if(newState) {
|
||||
this.notificationStore.setSpinner(this.shownProcessingMessage);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
this.notificationStore.clearSpinner();
|
||||
}
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
created() {
|
||||
this.bulkQuery = this.$route.params.ids;
|
||||
this.ids = new UrlSafeBase64().decodeIds(this.$route.params.ids);
|
||||
const premisses = await this.premiseEditStore.load(this.ids);
|
||||
this.destinationEditStore.setupDestinations(premisses);
|
||||
this.premiseEditStore.loadPremissesForced(this.ids);
|
||||
},
|
||||
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('keydown', this.handleKeyDown);
|
||||
window.addEventListener('keyup', this.handleKeyUp);
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener('keydown', this.handleKeyDown);
|
||||
window.removeEventListener('keyup', this.handleKeyUp);
|
||||
data() {
|
||||
return {
|
||||
ids: [],
|
||||
bulkQuery: null,
|
||||
modalType: null,
|
||||
componentsData: {
|
||||
price: {props: {price: 0, overSeaShare: 0, includeFcaFee: false}},
|
||||
material: {props: {partNumber: "", hsCode: null, tariffRate: null, tariffUnlocked: false, description: "", hideDescription: false}},
|
||||
packaging: {
|
||||
props: {
|
||||
length: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
weight: 0,
|
||||
weightUnit: "KG",
|
||||
dimensionUnit: "MM",
|
||||
unitCount: 1,
|
||||
mixable: true,
|
||||
stackable: true
|
||||
}
|
||||
},
|
||||
destinations: {props: {}},
|
||||
},
|
||||
editIds: null,
|
||||
dataSourceId: null,
|
||||
processingMessage: "Please wait. Calculating ...",
|
||||
showCalculationModal: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
useHelpStore,
|
||||
handleKeyDown(event) {
|
||||
if (event.key === 'Control') {
|
||||
this.isCtrlPressed = true;
|
||||
} else if (event.key === 'Shift') {
|
||||
this.isShiftPressed = true;
|
||||
}
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
this.fillData(this.modalType);
|
||||
this.modalType = null;
|
||||
}
|
||||
|
||||
},
|
||||
handleKeyUp(event) {
|
||||
if (event.key === 'Control') {
|
||||
this.isCtrlPressed = false;
|
||||
} else if (event.key === 'Shift') {
|
||||
this.isShiftPressed = false;
|
||||
}
|
||||
},
|
||||
updateUrl(id) {
|
||||
|
||||
const idx = this.ids.findIndex(curId => curId === id);
|
||||
|
||||
if (idx > -1) {
|
||||
|
|
@ -348,215 +247,113 @@ export default {
|
|||
});
|
||||
}
|
||||
},
|
||||
destMgmt() {
|
||||
this.fillData('destinations');
|
||||
this.editIds = null;
|
||||
this.modalTitle = 'Destination Manager'
|
||||
this.modalType = 'destinations';
|
||||
},
|
||||
async calculate() {
|
||||
async startCalculation() {
|
||||
this.showCalculationModal = true;
|
||||
const error = await this.premiseEditStore.startCalculation();
|
||||
|
||||
if (error === null) {
|
||||
this.close()
|
||||
this.closeMassEdit()
|
||||
}
|
||||
this.showCalculationModal = false;
|
||||
},
|
||||
close() {
|
||||
closeMassEdit() {
|
||||
this.$router.push({name: "calculation-list"});
|
||||
},
|
||||
|
||||
/* checkbox handling */
|
||||
|
||||
updateCheckBox(data) {
|
||||
this.premiseEditStore.setChecked(data.id, data.checked);
|
||||
this.updateOverallCheckBox();
|
||||
},
|
||||
updateCheckBoxes(value) {
|
||||
this.premiseEditStore.setAll(value);
|
||||
this.updateOverallCheckBox();
|
||||
},
|
||||
updateOverallCheckBox() {
|
||||
this.overallCheck = this.premiseEditStore.allChecked;
|
||||
|
||||
if (!this.overallCheck)
|
||||
this.overallIndeterminate = this.premiseEditStore.someChecked;
|
||||
},
|
||||
|
||||
|
||||
/* click listeners */
|
||||
|
||||
onToolbarAction(action) {
|
||||
if (action === 'deselect') {
|
||||
this.updateCheckBoxes(false);
|
||||
} else
|
||||
this.openModal(action, this.premiseEditStore.getSelectedPremiseIds);
|
||||
multiselectAction(action) {
|
||||
this.openModal(action, this.selectedPremisses.map(p => p.id));
|
||||
},
|
||||
onClickAction(data) {
|
||||
|
||||
const actions = data.action.split("-");
|
||||
|
||||
if (actions.length === 1) {
|
||||
const massEdit = 0 !== this.selectCount;
|
||||
this.openModal(data.action, massEdit ? this.premiseEditStore.getSelectedPremiseIds : [data.id], data.id, massEdit);
|
||||
} else if (actions.length === 2) { /* ctrl or ctrl + shift */
|
||||
this.premiseEditStore.setBy(actions[0], actions[1], data.id);
|
||||
this.updateOverallCheckBox();
|
||||
}
|
||||
const massEdit = 0 !== this.selectCount
|
||||
this.openModal(data.action, massEdit ? this.premiseEditStore.getSelectedPremissesIds : [data.id], data.id, massEdit);
|
||||
},
|
||||
|
||||
/* modal handling */
|
||||
|
||||
modalDialogClick(action) {
|
||||
|
||||
this.modalDialogShow = false;
|
||||
|
||||
if (action === 'dismiss') {
|
||||
this.modalStash = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'deny') {
|
||||
this.openModal(this.modalStash.type, this.modalStash.ids, this.modalStash.dataSource, this.modalStash.massEdit)
|
||||
this.modalStash = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.destMgmt();
|
||||
|
||||
|
||||
},
|
||||
|
||||
openModal(type, ids, dataSource = -1, massEdit = true) {
|
||||
|
||||
logger.log("open modal", type, ids, dataSource, massEdit, this.modalStash, this.modalDialogShow)
|
||||
|
||||
if ((type === 'amount' || type === 'routes') && this.modalStash === null) {
|
||||
|
||||
const state = this.destinationEditStore.checkDestinationAssignment(ids);
|
||||
|
||||
if (state === 'some' || state === 'none') {
|
||||
this.denyText = state === 'none' ? null : 'No'
|
||||
this.modalDialogShow = true;
|
||||
// stash for later.
|
||||
this.modalStash = {type: type, ids: ids, dataSource: dataSource, massEdit: massEdit};
|
||||
}
|
||||
if (type !== 'destinations')
|
||||
this.fillData(type, dataSource, massEdit)
|
||||
else {
|
||||
this.premiseEditStore.prepareDestinations(dataSource, ids, massEdit, true);
|
||||
}
|
||||
|
||||
if ((type === 'amount' || type === 'routes')) {
|
||||
if (dataSource !== -1)
|
||||
ids = [dataSource];
|
||||
}
|
||||
this.dataSourceId = dataSource !== -1 ? dataSource : null;
|
||||
this.editIds = ids;
|
||||
this.modalType = type;
|
||||
|
||||
if (!this.modalDialogShow) {
|
||||
logger.log("open modal (actual)", type, ids, dataSource, massEdit, this.modalStash, this.modalDialogShow)
|
||||
this.fillData(type, dataSource, massEdit);
|
||||
this.editIds = ids;
|
||||
this.modalType = type;
|
||||
}
|
||||
logger.info("open modal", massEdit, this.modalType, this.editIds, this.dataSourceId)
|
||||
|
||||
},
|
||||
async closeEditModalAction(action) {
|
||||
|
||||
let massUpdate = false;
|
||||
let success = true;
|
||||
|
||||
if (this.modalType === 'amount' || this.modalType === 'routes' || this.modalType === "destinations") {
|
||||
|
||||
if (action === 'accept') {
|
||||
|
||||
if (this.modalType === "destinations") {
|
||||
const setMatrix = this.$refs.modalComponent?.destMatrix;
|
||||
|
||||
if (setMatrix) {
|
||||
success = await this.destinationEditStore.massSetDestinations(setMatrix);
|
||||
}
|
||||
} else {
|
||||
massUpdate = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (success) {
|
||||
// Clear data
|
||||
this.fillData(this.modalType);
|
||||
this.modalType = null;
|
||||
} else return;
|
||||
|
||||
if (massUpdate) {
|
||||
await this.destinationEditStore.massUpdateDestinations(this.editIds);
|
||||
}
|
||||
|
||||
if (this.modalStash && action === 'accept') {
|
||||
setTimeout(() => {
|
||||
this.openModal(this.modalStash.type, this.modalStash.ids, this.modalStash.dataSource, this.modalStash.massEdit);
|
||||
this.modalStash = null;
|
||||
}, 300);
|
||||
if (this.modalType === "destinations") {
|
||||
if (action === "accept") {
|
||||
await this.premiseEditStore.executeDestinationsMassEdit();
|
||||
} else {
|
||||
this.modalStash = null;
|
||||
this.premiseEditStore.cancelMassEdit();
|
||||
}
|
||||
|
||||
} else if (action === "accept") {
|
||||
await this.premiseEditStore.batchUpdate(this.modalType, this.editIds, this.modalProps);
|
||||
// Clear data
|
||||
this.fillData(this.modalType);
|
||||
this.modalType = null;
|
||||
} else if (action === "cancel") {
|
||||
// Clear data
|
||||
this.fillData(this.modalType);
|
||||
this.modalType = null;
|
||||
const props = this.componentsData[this.modalType].props;
|
||||
|
||||
switch (this.modalType) {
|
||||
case "price":
|
||||
await this.premiseEditStore.batchUpdatePrice(this.editIds, props);
|
||||
break;
|
||||
case "material":
|
||||
await this.premiseEditStore.batchUpdateMaterial(this.editIds, props);
|
||||
break;
|
||||
case "packaging":
|
||||
await this.premiseEditStore.batchUpdatePackaging(this.editIds, props);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear data
|
||||
this.fillData(this.modalType);
|
||||
this.modalType = null;
|
||||
},
|
||||
fillData(type, id = -1, hideDescription = false) {
|
||||
|
||||
this.modalTitle = "Edit ".concat(type);
|
||||
|
||||
if (id === -1) {
|
||||
|
||||
if (type === 'price')
|
||||
this.modalProps = {price: null, overSeaShare: null, includeFcaFee: null};
|
||||
|
||||
if (type === 'material')
|
||||
this.modalProps = {
|
||||
partNumber: "",
|
||||
hsCode: null,
|
||||
tariffRate: null,
|
||||
tariffUnlocked: false,
|
||||
description: null,
|
||||
hideDescription: hideDescription
|
||||
};
|
||||
|
||||
if (type === 'packaging')
|
||||
this.modalProps = {
|
||||
length: null,
|
||||
width: null,
|
||||
height: null,
|
||||
weight: null,
|
||||
weightUnit: "KG",
|
||||
dimensionUnit: "MM",
|
||||
unitCount: null,
|
||||
mixable: true,
|
||||
stackable: true
|
||||
};
|
||||
|
||||
if (type === 'amount' || type === 'routes' || type === 'destinations') {
|
||||
this.modalTitle = "Edit destinations";
|
||||
this.modalProps = {};
|
||||
}
|
||||
|
||||
// clear
|
||||
this.componentsData = {
|
||||
price: {props: {price: null, overSeaShare: null, includeFcaFee: null}},
|
||||
material: {
|
||||
props: {
|
||||
partNumber: "",
|
||||
hsCode: null,
|
||||
tariffRate: null,
|
||||
tariffUnlocked: false,
|
||||
description: null,
|
||||
hideDescription: hideDescription
|
||||
}
|
||||
},
|
||||
packaging: {
|
||||
props: {
|
||||
length: null,
|
||||
width: null,
|
||||
height: null,
|
||||
weight: null,
|
||||
weightUnit: "KG",
|
||||
dimensionUnit: "MM",
|
||||
unitCount: null,
|
||||
mixable: true,
|
||||
stackable: true
|
||||
}
|
||||
},
|
||||
destinations: {props: {}},
|
||||
};
|
||||
} else {
|
||||
const premise = this.premiseEditStore.getById(id);
|
||||
|
||||
if (type === "price") {
|
||||
this.modalProps = {
|
||||
this.componentsData.price.props = {
|
||||
price: premise.material_cost,
|
||||
overSeaShare: premise.oversea_share,
|
||||
includeFcaFee: premise.is_fca_enabled
|
||||
}
|
||||
} else if (type === "material") {
|
||||
|
||||
this.modalProps = {
|
||||
this.componentsData.material.props = {
|
||||
partNumber: premise.material.part_number,
|
||||
hsCode: premise.hs_code,
|
||||
tariffRate: premise.tariff_rate ?? null,
|
||||
|
|
@ -566,7 +363,7 @@ export default {
|
|||
}
|
||||
|
||||
} else if (type === "packaging") {
|
||||
this.modalProps = {
|
||||
this.componentsData.packaging.props = {
|
||||
length: premise.handling_unit.length,
|
||||
width: premise.handling_unit.width,
|
||||
height: premise.handling_unit.height,
|
||||
|
|
@ -577,76 +374,17 @@ export default {
|
|||
mixable: premise.is_mixable ?? true,
|
||||
stackable: premise.is_stackable ?? true
|
||||
}
|
||||
} else if (type === 'amount' || type === 'routes' || type === 'destinations') {
|
||||
this.modalTitle = "Edit destinations";
|
||||
this.modalProps = {type: type};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/* Animation hooks */
|
||||
|
||||
onBeforeEnter(el) {
|
||||
if (this.isInitialLoad) {
|
||||
el.style.opacity = 0;
|
||||
el.style.transform = 'translateY(2rem)';
|
||||
}
|
||||
}
|
||||
,
|
||||
onEnter(el, done) {
|
||||
if (this.isInitialLoad) {
|
||||
const index = parseInt(el.dataset.index) || 0;
|
||||
const delay = index * 50; // 50ms Verzögerung pro Element
|
||||
|
||||
setTimeout(() => {
|
||||
el.style.transition = 'opacity 0.4s ease, transform 0.4s ease';
|
||||
el.style.opacity = 1;
|
||||
el.style.transform = 'translateY(0)';
|
||||
|
||||
// Cleanup nach Animation
|
||||
setTimeout(() => {
|
||||
el.style.transition = '';
|
||||
el.style.opacity = '';
|
||||
el.style.transform = '';
|
||||
done();
|
||||
|
||||
// Nach dem letzten Element isInitialLoad deaktivieren
|
||||
if (index === this.premises.length - 1) {
|
||||
this.isInitialLoad = false;
|
||||
}
|
||||
}, 400);
|
||||
}, delay);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
.sub-header {
|
||||
flex-shrink: 0; /* Prevent header from shrinking */
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
|
||||
/* Global style für copy-mode cursor */
|
||||
.edit-calculation-container.has-selection :deep(.edit-calculation-list-header-cell--copyable:hover) {
|
||||
cursor: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDEyOC41MSAxMzQuMDUiPjxkZWZzPjxzdHlsZT4uY3tmaWxsOm5vbmU7fS5jLC5ke3N0cm9rZTojMDEwMTAxO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2Utd2lkdGg6NXB4O30uZHtmaWxsOiNmZmY7fTwvc3R5bGU+PC9kZWZzPjxnIGlkPSJhIj48cGF0aCBjbGFzcz0iYyIgZD0ibTU0Ljg5LDExMi41MWgtMi4yNGMtMS4yNCwwLTIuMjQtMS0yLjI0LTIuMjR2LTIuMjQiLz48bGluZSBjbGFzcz0iYyIgeDE9IjcwLjU3IiB5MT0iNzYuNjciIHgyPSI2My44NSIgeTI9Ijc2LjY3Ii8+PGxpbmUgY2xhc3M9ImMiIHgxPSI3MC41NyIgeTE9IjExMi41MSIgeDI9IjY2LjA5IiB5Mj0iMTEyLjUxIi8+PGxpbmUgY2xhc3M9ImMiIHgxPSI4Ni4yNSIgeTE9Ijk5LjA3IiB4Mj0iODYuMjUiIHkyPSI5Mi4zNSIvPjxsaW5lIGNsYXNzPSJjIiB4MT0iNTAuNDEiIHkxPSI5Ni44MyIgeDI9IjUwLjQxIiB5Mj0iOTIuMzUiLz48cGF0aCBjbGFzcz0iYyIgZD0ibTgxLjc3LDExMi41MWgyLjI0YzEuMjQsMCwyLjI0LTEsMi4yNC0yLjI0di0yLjI0Ii8+PHBhdGggY2xhc3M9ImMiIGQ9Im04MS43Nyw3Ni42N2gyLjI0YzEuMjQsMCwyLjI0LDEsMi4yNCwyLjI0djIuMjQiLz48cGF0aCBjbGFzcz0iYyIgZD0ibTU0Ljg5LDc2LjY3aC0yLjI0Yy0xLjI0LDAtMi4yNCwxLTIuMjQsMi4yNHYyLjI0Ii8+PHBhdGggY2xhc3M9ImMiIGQ9Im04Ni4yNSw5OS4wN2gxMS4yYzEuMjQsMCwyLjI0LTEsMi4yNC0yLjI0di0zMS4zNmMwLTEuMjQtMS0yLjI0LTIuMjQtMi4yNGgtMzEuMzZjLTEuMjQsMC0yLjI0LDEtMi4yNCwyLjI0djExLjIiLz48L2c+PGcgaWQ9ImIiPjxwYXRoIGNsYXNzPSJkIiBkPSJtNDQuMDgsNDQuMDdsMzIuOTQtOS4yYzEuNjktLjUyLDIuNjQtMi4zMSwyLjEyLTQtLjMtLjk4LTEuMDUtMS43NS0yLjAxLTIuMDlMNi43MywyLjY3Yy0xLjY3LS41Ny0zLjQ5LjMzLTQuMDYsMi0uMjMuNjYtLjIzLDEuMzgsMCwyLjA1bDI2LjExLDcwLjRjLjU4LDEuNjcsMi40LDIuNTYsNC4wNywxLjk4Ljk3LS4zMywxLjcxLTEuMTEsMi4wMS0yLjA5bDkuMjItMzIuOTRaIi8+PC9nPjwvc3ZnPg==") 12 12, pointer;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
||||
/* Global style für filter-mode cursor */
|
||||
.edit-calculation-container.add-all :deep(.bulk-edit-row__cell--filterable:hover) {
|
||||
cursor: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDEyOC41MSAxMzQuMDUiPjxkZWZzPjxzdHlsZT4uY3tmaWxsOm5vbmU7fS5jLC5ke3N0cm9rZTojMDEwMTAxO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2Utd2lkdGg6NXB4O30uZHtmaWxsOiNmZmY7fTwvc3R5bGU+PC9kZWZzPjxnIGlkPSJhIj48bGluZSBjbGFzcz0iYyIgeDE9IjczLjAzIiB5MT0iNzUuNTUiIHgyPSI2Ni4zOCIgeTI9Ijc1LjU1Ii8+PGxpbmUgY2xhc3M9ImMiIHgxPSI2Ni4zOCIgeTE9IjExMi4xNiIgeDI9IjczLjAzIiB5Mj0iMTEyLjE2Ii8+PHBhdGggY2xhc3M9ImMiIGQ9Im04MS4zNSw3NS41NWg0Ljk5Yy45MiwwLDEuNjYuNzUsMS42NiwxLjY2djQuOTkiLz48bGluZSBjbGFzcz0iYyIgeDE9Ijg4LjAxIiB5MT0iOTcuMTgiIHgyPSI4OC4wMSIgeTI9IjkwLjUzIi8+PHBhdGggY2xhc3M9ImMiIGQ9Im04MS4zNSwxMTIuMTZoNC45OWMuOTIsMCwxLjY2LS43NSwxLjY2LTEuNjZ2LTQuOTkiLz48bGluZSBjbGFzcz0iYyIgeDE9IjUxLjQiIHkxPSI5MC41MyIgeDI9IjUxLjQiIHkyPSI5Ny4xOCIvPjxwYXRoIGNsYXNzPSJjIiBkPSJtNTguMDUsMTEyLjE2aC00Ljk5Yy0uOTIsMC0xLjY2LS43NS0xLjY2LTEuNjZ2LTQuOTkiLz48cGF0aCBjbGFzcz0iYyIgZD0ibTU4LjA1LDc1LjU1aC00Ljk5Yy0uOTIsMC0xLjY2Ljc1LTEuNjYsMS42NnY0Ljk5Ii8+PC9nPjxnIGlkPSJiIj48cGF0aCBjbGFzcz0iZCIgZD0ibTQ0LjA4LDQ0LjA3bDMyLjk0LTkuMmMxLjY5LS41MiwyLjY0LTIuMzEsMi4xMi00LS4zLS45OC0xLjA1LTEuNzUtMi4wMS0yLjA5TDYuNzMsMi42N2MtMS42Ny0uNTctMy40OS4zMy00LjA2LDItLjIzLjY2LS4yMywxLjM4LDAsMi4wNWwyNi4xMSw3MC40Yy41OCwxLjY3LDIuNCwyLjU2LDQuMDcsMS45OC45Ny0uMzMsMS43MS0xLjExLDIuMDEtMi4wOWw5LjIyLTMyLjk0WiIvPjwvZz48L3N2Zz4=") 12 12, pointer;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
||||
/* Global style für filter-mode cursor */
|
||||
.edit-calculation-container.apply-filter :deep(.bulk-edit-row__cell--filterable:hover) {
|
||||
cursor: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDEyOC41MSAxMzQuMDUiPjxkZWZzPjxzdHlsZT4uZHtzdHJva2U6IzAwMDt9LmQsLmV7ZmlsbDpub25lO30uZCwuZSwuZntzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLXdpZHRoOjVweDt9LmUsLmZ7c3Ryb2tlOiMwMTAxMDE7fS5me2ZpbGw6I2ZmZjt9PC9zdHlsZT48L2RlZnM+PGcgaWQ9ImEiPjxsaW5lIGNsYXNzPSJlIiB4MT0iNzMuMDMiIHkxPSI3NS41NSIgeDI9IjY2LjM4IiB5Mj0iNzUuNTUiLz48bGluZSBjbGFzcz0iZSIgeDE9IjY2LjM4IiB5MT0iMTEyLjE2IiB4Mj0iNzMuMDMiIHkyPSIxMTIuMTYiLz48cGF0aCBjbGFzcz0iZSIgZD0ibTgxLjM1LDc1LjU1aDQuOTljLjkyLDAsMS42Ni43NSwxLjY2LDEuNjZ2NC45OSIvPjxsaW5lIGNsYXNzPSJlIiB4MT0iODguMDEiIHkxPSI5Ny4xOCIgeDI9Ijg4LjAxIiB5Mj0iOTAuNTMiLz48cGF0aCBjbGFzcz0iZSIgZD0ibTgxLjM1LDExMi4xNmg0Ljk5Yy45MiwwLDEuNjYtLjc1LDEuNjYtMS42NnYtNC45OSIvPjxsaW5lIGNsYXNzPSJlIiB4MT0iNTEuNCIgeTE9IjkwLjUzIiB4Mj0iNTEuNCIgeTI9Ijk3LjE4Ii8+PHBhdGggY2xhc3M9ImUiIGQ9Im01OC4wNSwxMTIuMTZoLTQuOTljLS45MiwwLTEuNjYtLjc1LTEuNjYtMS42NnYtNC45OSIvPjxwYXRoIGNsYXNzPSJlIiBkPSJtNTguMDUsNzUuNTVoLTQuOTljLS45MiwwLTEuNjYuNzUtMS42NiwxLjY2djQuOTkiLz48L2c+PGcgaWQ9ImIiPjxwYXRoIGNsYXNzPSJmIiBkPSJtNDQuMDgsNDQuMDdsMzIuOTQtOS4yYzEuNjktLjUyLDIuNjQtMi4zMSwyLjEyLTQtLjMtLjk4LTEuMDUtMS43NS0yLjAxLTIuMDlMNi43MywyLjY3Yy0xLjY3LS41Ny0zLjQ5LjMzLTQuMDYsMi0uMjMuNjYtLjIzLDEuMzgsMCwyLjA1bDI2LjExLDcwLjRjLjU4LDEuNjcsMi40LDIuNTYsNC4wNywxLjk4Ljk3LS4zMywxLjcxLTEuMTEsMi4wMS0yLjA5bDkuMjItMzIuOTRaIi8+PC9nPjxnIGlkPSJjIj48bGluZSBjbGFzcz0iZCIgeDE9Ijk5LjM4IiB5MT0iOTQuMTkiIHgyPSIxMjYuMDEiIHkyPSI5NC4xOSIvPjxsaW5lIGNsYXNzPSJkIiB4MT0iMTEyLjY5IiB5MT0iODAuODciIHgyPSIxMTIuNjkiIHkyPSIxMDcuNTEiLz48L2c+PC9zdmc+") 12 12, pointer;
|
||||
.edit-calculation-container.has-selection :deep(.copyable-cell:hover) {
|
||||
cursor: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOCIgaGVpZ2h0PSIyOCIgdmlld0JveD0iMCAwIDI1NiAyNTYiPgogIDxyZWN0IHg9Ijg0IiB5PSIzMiIgd2lkdGg9IjEzNiIgaGVpZ2h0PSIxMzYiIGZpbGw9IndoaXRlIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iOCIgcng9IjQiLz4KICA8cmVjdCB4PSIzNiIgeT0iODQiIHdpZHRoPSIxMzYiIGhlaWdodD0iMTM2IiBmaWxsPSJ3aGl0ZSIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjgiIHJ4PSI0Ii8+Cjwvc3ZnPg==") 12 12, pointer;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
|
@ -660,6 +398,8 @@ export default {
|
|||
.modal-content-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
margin-top: 1.6rem;
|
||||
min-width: 50rem;
|
||||
}
|
||||
|
||||
|
|
@ -669,19 +409,25 @@ export default {
|
|||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
/* Sort Animation für Rows */
|
||||
.sort-list-move {
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
/* Container Animation */
|
||||
|
||||
/* Verhindere Animation während des Entfernens */
|
||||
.sort-list-leave-active {
|
||||
position: absolute;
|
||||
.list-edit-enter-from {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
transform: translateY(-20px);
|
||||
max-height: 0;
|
||||
}
|
||||
|
||||
/* Enter-Animation wird via JavaScript gesteuert für staggered effect */
|
||||
.list-edit-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
max-height: 0;
|
||||
}
|
||||
|
||||
.list-edit-enter-active,
|
||||
.list-edit-leave-active {
|
||||
transition: all 0.4s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
.spinner-container {
|
||||
|
|
@ -704,15 +450,13 @@ export default {
|
|||
overflow: hidden;
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 3rem;
|
||||
|
||||
}
|
||||
|
||||
.edit-calculation-list-body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.edit-calculation-list-header {
|
||||
display: grid;
|
||||
grid-template-columns: 6rem 0.8fr 0.7fr 1fr 1fr 1.2fr 2fr 10rem;
|
||||
grid-template-columns: 6rem 1fr 1fr 1.5fr 1.5fr 1.5fr 10rem;
|
||||
gap: 1.6rem;
|
||||
padding: 2.4rem;
|
||||
background-color: #ffffff;
|
||||
|
|
@ -724,24 +468,6 @@ export default {
|
|||
letter-spacing: 0.08rem;
|
||||
}
|
||||
|
||||
.edit-calculation-list-header-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.edit-calculation-list-header-cell--copyable {
|
||||
}
|
||||
|
||||
.edit-calculation-list-header-cell--clickable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.edit-calculation-list-header-cell--selected {
|
||||
color: #002F54;
|
||||
}
|
||||
|
||||
.edit-calculation-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -758,20 +484,4 @@ export default {
|
|||
display: flex;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.header-help-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 1.6rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.header-caption-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -1,22 +1,7 @@
|
|||
<template>
|
||||
<div class="edit-calculation-container">
|
||||
<div class="header-container">
|
||||
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
|
||||
|
||||
<h2 class="page-header">Edit calculation</h2>
|
||||
<div class="header-controls">
|
||||
<basic-button @click="close" :show-icon="false" :disabled="premiseSingleEditStore.showLoadingSpinner"
|
||||
variant="secondary"> {{ fromMassEdit ? 'Back' : 'Close' }}
|
||||
|
|
@ -122,7 +107,6 @@ import {UrlSafeBase64} from "@/common.js";
|
|||
import {usePremiseSingleEditStore} from "@/store/premiseSingleEdit.js";
|
||||
import {useNotificationStore} from "@/store/notification.js";
|
||||
import Spinner from "@/components/UI/Spinner.vue";
|
||||
import {useHelpStore} from "@/store/help.js";
|
||||
|
||||
export default {
|
||||
name: "SingleEdit",
|
||||
|
|
@ -163,7 +147,7 @@ export default {
|
|||
if (this.premiseSingleEditStore.routing)
|
||||
return "Please wait. Routing ..."
|
||||
|
||||
return "Please wait. Prepare calculation ...";
|
||||
return "Please wait. Calculating ...";
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -177,7 +161,6 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
useHelpStore,
|
||||
|
||||
async startCalculation() {
|
||||
this.showCalculationModal = true;
|
||||
|
|
@ -191,10 +174,11 @@ export default {
|
|||
},
|
||||
close() {
|
||||
if (this.bulkEditQuery) {
|
||||
//TODO: deselect element and save
|
||||
//TODO: deselect and save
|
||||
// this.premiseEditStore.deselectPremise();
|
||||
this.$router.push({name: 'bulk', params: {ids: this.bulkEditQuery}});
|
||||
} else {
|
||||
//TODO: deselect element and save
|
||||
//TODO: deselect and save
|
||||
this.$router.push({name: 'home'});
|
||||
}
|
||||
},
|
||||
|
|
@ -226,35 +210,11 @@ export default {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.header-help-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 1.6rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.edit-calculation-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.edit-calculation-spinner-container
|
||||
{
|
||||
display: flex;
|
||||
padding-top: 10rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
|
|||
|
|
@ -1,25 +1,9 @@
|
|||
<template>
|
||||
<div>
|
||||
|
||||
<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-header">My calculations</h2>
|
||||
|
||||
|
||||
<h3 class="sub-header">Status</h3>
|
||||
|
||||
<the-dashboard></the-dashboard>
|
||||
|
||||
<h3 class="sub-header">Drafts</h3>
|
||||
<div class="calculation-list-container">
|
||||
|
||||
<the-calculation-search @execute-search="updateFilter"/>
|
||||
|
|
@ -33,7 +17,6 @@
|
|||
<div>Material</div>
|
||||
<div>Supplier</div>
|
||||
<div>Status</div>
|
||||
<div>Created at</div>
|
||||
<div>Action</div>
|
||||
</div>
|
||||
<transition name="list-container" mode="out-in">
|
||||
|
|
@ -61,6 +44,7 @@
|
|||
<list-edit :show="showListEdit" :select-count="premiseStore.selectedIds.length"
|
||||
@action="handleMultiselectAction"></list-edit>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
|
@ -84,16 +68,10 @@ import {UrlSafeBase64} from "@/common.js";
|
|||
import Pagination from "@/components/UI/Pagination.vue";
|
||||
import ModalDialog from "@/components/UI/ModalDialog.vue";
|
||||
import modal from "@/components/UI/Modal.vue";
|
||||
import {useActiveUserStore} from "@/store/activeuser.js";
|
||||
import TheDashboard from "@/components/layout/calculation/TheDashboard.vue";
|
||||
import Box from "@/components/UI/Box.vue";
|
||||
import {useHelpStore} from "@/store/help.js";
|
||||
|
||||
export default {
|
||||
name: "Calculation",
|
||||
components: {
|
||||
Box,
|
||||
TheDashboard,
|
||||
ModalDialog,
|
||||
Pagination,
|
||||
ListEdit,
|
||||
|
|
@ -101,64 +79,9 @@ export default {
|
|||
CalculationListItem, Checkbox, NotificationBar, IconButton, BasicBadge, TheCalculationSearch, Flag
|
||||
},
|
||||
computed: {
|
||||
...mapStores(usePremiseStore, useActiveUserStore),
|
||||
...mapStores(usePremiseStore),
|
||||
showListEdit() {
|
||||
return this.premiseStore.globallySomeChecked;
|
||||
},
|
||||
greeting() {
|
||||
const now = new Date();
|
||||
const hour = now.getHours();
|
||||
|
||||
// Get day of year as seed for consistent random selection throughout the day
|
||||
const dayOfYear = Math.floor((now - new Date(now.getFullYear(), 0, 0)) / 86400000);
|
||||
|
||||
let greetings = [];
|
||||
|
||||
// Morning: 5-12
|
||||
if (hour >= 5 && hour < 12) {
|
||||
greetings = [
|
||||
`Good morning, ${this.username}`,
|
||||
`Morning, ${this.username}!`,
|
||||
`Good morning, ${this.username}. Ready to calculate?`,
|
||||
`Morning ${this.username}, what's on the agenda today?`
|
||||
];
|
||||
}
|
||||
// Afternoon: 12-18
|
||||
else if (hour >= 12 && hour < 18) {
|
||||
greetings = [
|
||||
`Good afternoon, ${this.username}`,
|
||||
`Hi ${this.username}, welcome back`,
|
||||
`Afternoon, ${this.username}!`,
|
||||
`Hello ${this.username}, let's continue`,
|
||||
`Hi ${this.username}, ready to work?`
|
||||
];
|
||||
}
|
||||
// Evening: 18-22
|
||||
else if (hour >= 18 && hour < 22) {
|
||||
greetings = [
|
||||
`Good evening, ${this.username}`,
|
||||
`Evening, ${this.username}!`,
|
||||
`Hi ${this.username}, still working hard?`,
|
||||
`Good evening, ${this.username}, almost done for today?`
|
||||
];
|
||||
}
|
||||
// Night: 22-5
|
||||
else {
|
||||
greetings = [
|
||||
`Working late, ${this.username}?`,
|
||||
`Hi ${this.username}, burning the midnight oil?`,
|
||||
`Hello ${this.username}`,
|
||||
`Still here, ${this.username}?`,
|
||||
`Hi ${this.username}, don't stay up too late`
|
||||
];
|
||||
}
|
||||
|
||||
// Use day of year as seed for consistent selection
|
||||
const index = dayOfYear % greetings.length;
|
||||
return greetings[index];
|
||||
},
|
||||
username() {
|
||||
return this.activeUserStore.username;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
|
@ -195,7 +118,6 @@ export default {
|
|||
await this.executeSearch();
|
||||
},
|
||||
methods: {
|
||||
useHelpStore,
|
||||
async handleModalAction(action) {
|
||||
if (action === 'dismiss') {
|
||||
this.modal.state = false;
|
||||
|
|
@ -318,7 +240,7 @@ export default {
|
|||
}
|
||||
},
|
||||
updatePagination(resetPage = true) {
|
||||
if (resetPage) {
|
||||
if(resetPage) {
|
||||
this.updatePage(1);
|
||||
}
|
||||
this.pagination = this.premiseStore.pagination;
|
||||
|
|
@ -371,36 +293,6 @@ export default {
|
|||
|
||||
<style scoped>
|
||||
|
||||
.header-help-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
font-weight: normal;
|
||||
margin-bottom: 0;
|
||||
font-size: 2.4rem;
|
||||
color: #002F54;
|
||||
}
|
||||
|
||||
.page-sub-header {
|
||||
font-weight: normal;
|
||||
font-size: 1.8rem;
|
||||
color: #6B869C;
|
||||
margin-bottom: 2.4rem;
|
||||
}
|
||||
|
||||
|
||||
.space-around {
|
||||
margin: 3rem;
|
||||
}
|
||||
|
|
@ -480,7 +372,7 @@ export default {
|
|||
|
||||
.calculation-list-header {
|
||||
display: grid;
|
||||
grid-template-columns: 6rem 1fr 2fr 14rem 20rem 10rem;
|
||||
grid-template-columns: 6rem 1fr 2fr 14rem 10rem;
|
||||
gap: 1.6rem;
|
||||
padding: 1.6rem;
|
||||
background-color: #ffffff;
|
||||
|
|
|
|||
|
|
@ -29,8 +29,6 @@ import {mapStores} from "pinia";
|
|||
import {useActiveUserStore} from "@/store/activeuser.js";
|
||||
import Apps from "@/components/layout/config/Apps.vue";
|
||||
import Users from "@/components/layout/config/Users.vue";
|
||||
import CalculationDumpList from "@/components/layout/dev/CalculationDumpList.vue";
|
||||
import CalculationDumps from "@/components/layout/config/CalculationDumps.vue";
|
||||
|
||||
export default {
|
||||
name: "Config",
|
||||
|
|
@ -58,11 +56,6 @@ export default {
|
|||
component: markRaw(ErrorLog),
|
||||
props: {isSelected: false},
|
||||
},
|
||||
calculationDump: {
|
||||
title: 'Calculation dump',
|
||||
component: markRaw(CalculationDumps),
|
||||
props: {isSelected: false},
|
||||
},
|
||||
materialsTab: {
|
||||
title: 'Materials',
|
||||
component: markRaw(Materials),
|
||||
|
|
@ -96,7 +89,6 @@ export default {
|
|||
}
|
||||
|
||||
if (this.activeUserStore.isService) {
|
||||
tabs.push(this.calculationDump);
|
||||
tabs.push(this.appsTab);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +1,12 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="header-container">
|
||||
|
||||
<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>
|
||||
<h2 class="page-header page-header-align">Reporting
|
||||
<div class="page-header-badges">
|
||||
<basic-badge variant="primary" v-if="period">{{ period }}</basic-badge>
|
||||
<basic-badge variant="secondary" v-if="partNumber">{{ partNumber }}</basic-badge>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</h2>
|
||||
<div class="header-controls">
|
||||
<basic-button @click="createReport" icon="file">Create report</basic-button>
|
||||
<basic-button :disabled="!hasReport" variant="secondary" @click="downloadReport" icon="Download">Export
|
||||
|
|
@ -70,12 +56,10 @@ import ReportChart from "@/components/UI/ReportChart.vue";
|
|||
import Report from "@/components/layout/report/Report.vue";
|
||||
import BasicBadge from "@/components/UI/BasicBadge.vue";
|
||||
import {buildDate} from "@/common.js";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import {useHelpStore} from "@/store/help.js";
|
||||
|
||||
export default {
|
||||
name: "Reporting",
|
||||
components: {IconButton, BasicBadge, Report, ReportChart, Spinner, Box, SelectForReport, BasicButton, Modal},
|
||||
components: {BasicBadge, Report, ReportChart, Spinner, Box, SelectForReport, BasicButton, Modal},
|
||||
data() {
|
||||
return {
|
||||
showModal: false,
|
||||
|
|
@ -91,17 +75,17 @@ export default {
|
|||
},
|
||||
routeSectionScale() {
|
||||
const reports = this.reportsStore.reports;
|
||||
const scale = new Array(reports.map(r => r.destinations.length).reduce((max, n) => Math.max(n, max), 0)).fill(0);
|
||||
const scale = new Array(reports.map(r => r.premises.length).reduce((max, n) => Math.max(n, max), 0)).fill(0);
|
||||
|
||||
for (let i = 0; i < scale.length; i++) {
|
||||
for (const report of reports) {
|
||||
if (report.destinations.length > i) {
|
||||
scale[i] = Math.max(scale[i], report.destinations[i].sections.length);
|
||||
if(report.premises.length > i) {
|
||||
scale[i] = Math.max(scale[i], report.premises[i].sections.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scale.map(s => (s + 1) * 4);
|
||||
return scale.map(s => (s+1)*4);
|
||||
},
|
||||
reports() {
|
||||
return this.reportsStore.reports
|
||||
|
|
@ -129,7 +113,6 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
useHelpStore,
|
||||
downloadReport() {
|
||||
this.reportsStore.downloadReport();
|
||||
},
|
||||
|
|
@ -157,22 +140,6 @@ export default {
|
|||
|
||||
<style scoped>
|
||||
|
||||
.header-help-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 1.6rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.header-caption-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.page-header-align {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -183,7 +150,6 @@ export default {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.space-around {
|
||||
|
|
|
|||
|
|
@ -109,7 +109,6 @@ const router = createRouter({
|
|||
},
|
||||
{
|
||||
path: '/config',
|
||||
name: 'config',
|
||||
component: Config,
|
||||
beforeEnter: async (to, from) => {
|
||||
const userStore = useActiveUserStore();
|
||||
|
|
@ -122,7 +121,7 @@ const router = createRouter({
|
|||
},
|
||||
},
|
||||
{
|
||||
path: '/dumps/dump/:id',
|
||||
path: '/dev/dump/:id',
|
||||
component: CalculationDump,
|
||||
name: 'dev-calculation-dump'
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export const useActiveUserStore = defineStore('activeUser', {
|
|||
allowReporting(state) {
|
||||
if (state.user === null)
|
||||
return false;
|
||||
return state.user.groups?.includes("super") || state.user.groups?.includes("basic") || state.user.groups?.includes("calculation");
|
||||
return state.user.groups?.includes("super") || state.user.groups?.includes("freight") || state.user.groups?.includes("packaging") || state.user.groups?.includes("material") || state.user.groups?.includes("basic") || state.user.groups?.includes("calculation");
|
||||
},
|
||||
isSuper(state) {
|
||||
if (state.user === null)
|
||||
|
|
@ -62,11 +62,6 @@ export const useActiveUserStore = defineStore('activeUser', {
|
|||
if (state.user === null)
|
||||
return false;
|
||||
return state.user.groups?.includes("super") || state.user.groups?.includes("freight");
|
||||
},
|
||||
username(state) {
|
||||
if (state.user === null)
|
||||
return null;
|
||||
return state.user.firstname; //+ ' ' + state.user.lastname;
|
||||
}
|
||||
|
||||
},
|
||||
|
|
|
|||
|
|
@ -24,18 +24,6 @@ export const useAppsStore = defineStore('apps', {
|
|||
this.apps = resp.data;
|
||||
this.loading = false;
|
||||
},
|
||||
async exportApp(id) {
|
||||
const url = `${config.backendUrl}/apps/export/${id}`;
|
||||
const resp = await performRequest(this, 'GET', url, null);
|
||||
return resp.data;
|
||||
},
|
||||
async importApp(app) {
|
||||
const url = `${config.backendUrl}/apps/import`;
|
||||
const resp = await performRequest(this, 'POST', url, { data: app },true);
|
||||
|
||||
if(resp.data)
|
||||
await this.loadApps();
|
||||
},
|
||||
async addApp(appName, appGroups) {
|
||||
const url = `${config.backendUrl}/apps`;
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export const useBulkOperationStore = defineStore('bulkOperation', {
|
|||
useNotificationStore().addNotification({
|
||||
title: 'Bulk operation',
|
||||
message: 'All your bulk operations have been completed.',
|
||||
variant: 'info',
|
||||
type: 'success',
|
||||
icon: 'stack',
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,74 +0,0 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import performRequest from "@/backend.js";
|
||||
import {config} from '@/config'
|
||||
import {useNotificationStore} from "@/store/notification.js";
|
||||
|
||||
export const useDashboardStore = defineStore('dashboard', {
|
||||
state: () => ({
|
||||
stats: null,
|
||||
pullInterval: 3000,
|
||||
pullTimer: null,
|
||||
}),
|
||||
getters: {
|
||||
completed(state) {
|
||||
if (state.stats)
|
||||
return state.stats.completed;
|
||||
|
||||
return null;
|
||||
},
|
||||
running(state) {
|
||||
if (state.stats)
|
||||
return state.stats.running;
|
||||
|
||||
return null;
|
||||
},
|
||||
drafts(state) {
|
||||
if (state.stats)
|
||||
return state.stats.drafts;
|
||||
|
||||
return null;
|
||||
},
|
||||
failed(state) {
|
||||
if (state.stats)
|
||||
return state.stats.failed;
|
||||
|
||||
return null;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async load() {
|
||||
const url = `${config.backendUrl}/dashboard`;
|
||||
const resp = await performRequest(this, 'GET', url, null);
|
||||
|
||||
if(this.stats?.running && this.stats.running > 0 && resp.data.running === 0) {
|
||||
useNotificationStore().addNotification({
|
||||
title: 'Calculation',
|
||||
message: 'All your calculations have been completed.',
|
||||
variant: 'info',
|
||||
icon: 'calculator',
|
||||
})
|
||||
}
|
||||
|
||||
this.stats = resp.data;
|
||||
},
|
||||
startPulling() {
|
||||
if (this.pullTimer) return
|
||||
|
||||
this.pullTimer = setTimeout(async () => {
|
||||
await this.pull()
|
||||
}, this.pullInterval)
|
||||
},
|
||||
stopPulling() {
|
||||
if (this.pullTimer) {
|
||||
clearTimeout(this.pullTimer)
|
||||
this.pullTimer = null
|
||||
}
|
||||
},
|
||||
async pull() {
|
||||
await this.load();
|
||||
this.stopPulling();
|
||||
this.startPulling();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
|
@ -1,241 +0,0 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import performRequest from "@/backend.js";
|
||||
import {config} from '@/config'
|
||||
import logger from "@/logger.js";
|
||||
import {useNotificationStore} from "@/store/notification.js";
|
||||
|
||||
export const useDestinationEditStore = defineStore('destinationEdit', {
|
||||
state: () => ({
|
||||
destinations: null,
|
||||
loading: false,
|
||||
initialized: false,
|
||||
handlingCostMatrix: null,
|
||||
quantityMatrix: null,
|
||||
routeMatrix: null
|
||||
}),
|
||||
getters: {
|
||||
checkDestinationAssignment(state) {
|
||||
return (ids) => {
|
||||
let some = false;
|
||||
let all = true;
|
||||
|
||||
ids.forEach(id => {
|
||||
const dest = state.destinations?.get(id);
|
||||
|
||||
if ((dest ?? null) === null || dest.length === 0)
|
||||
all = false;
|
||||
else
|
||||
some = true;
|
||||
|
||||
});
|
||||
|
||||
if (all)
|
||||
return "all";
|
||||
else if (some)
|
||||
return "some";
|
||||
else
|
||||
return "none";
|
||||
}
|
||||
},
|
||||
getByPremiseId(state) {
|
||||
return (id) => {
|
||||
return state.destinations?.get(id);
|
||||
}
|
||||
},
|
||||
getByPremiseIds(state) {
|
||||
return (ids) => {
|
||||
return new Map(
|
||||
[...state.destinations].filter(([premiseId, destinations]) => ids.includes(premiseId))
|
||||
);
|
||||
}
|
||||
},
|
||||
showProcessingModal(state) {
|
||||
return state.loading;
|
||||
},
|
||||
getHandlingCostMatrix(state) {
|
||||
return state.handlingCostMatrix;
|
||||
},
|
||||
getQuantityMatrix(state) {
|
||||
return state.quantityMatrix;
|
||||
},
|
||||
getRouteMatrix(state) {
|
||||
return state.routeMatrix;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setHandlingCostMatrix(handlingCostMatrix) {
|
||||
this.handlingCostMatrix = handlingCostMatrix;
|
||||
},
|
||||
setQuantityMatrix(quantityMatrix) {
|
||||
this.quantityMatrix = quantityMatrix;
|
||||
},
|
||||
setRouteMatrix(routeMatrix) {
|
||||
this.routeMatrix = routeMatrix;
|
||||
},
|
||||
setupDestinations(premisses) {
|
||||
this.loading = true;
|
||||
|
||||
const temp = new Map();
|
||||
premisses.forEach(p => temp.set(p.id, p.destinations));
|
||||
this.destinations = temp;
|
||||
|
||||
this.initialized = true;
|
||||
this.loading = false;
|
||||
},
|
||||
async massSetDestinations(updateMatrix) {
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
|
||||
const toBeAdded = {};
|
||||
const toBeDeletedMap = new Map();
|
||||
|
||||
updateMatrix.forEach(row => {
|
||||
toBeAdded[row.id] = row.destinations.filter(d => d.selected).map(d => d.id);
|
||||
toBeDeletedMap.set(row.id, row.destinations.filter(d => !d.selected).map(d => d.id));
|
||||
});
|
||||
|
||||
const url = `${config.backendUrl}/calculation/destination`;
|
||||
const {
|
||||
data: data,
|
||||
headers: headers
|
||||
} = await performRequest(this, 'POST', url, {'destination_node_ids': toBeAdded});
|
||||
|
||||
this.destinations.forEach((destinations, premiseId) => {
|
||||
const toBeDeleted = toBeDeletedMap.get(premiseId);
|
||||
|
||||
const filtered = destinations !== null ? destinations.filter(d => !toBeDeleted?.includes(d.destination_node.id)) : [];
|
||||
const dataForPremiseId = (data[premiseId] ?? null) === null ? [] : data[premiseId];
|
||||
|
||||
this.destinations.set(premiseId, [...filtered, ...dataForPremiseId]);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error in massSetDestinations:', error);
|
||||
useNotificationStore().addNotification({
|
||||
title: 'Unable to set destinations',
|
||||
message: error.message ?? error.toString(),
|
||||
variant: 'exception',
|
||||
icon: 'bug',
|
||||
})
|
||||
return false;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
async massUpdateDestinations(premiseIds) {
|
||||
this.loading = true;
|
||||
|
||||
|
||||
await new Promise(resolve => setTimeout(() => {
|
||||
this.updateQuantity();
|
||||
resolve();
|
||||
}, 10));
|
||||
|
||||
await new Promise(resolve => setTimeout(() => {
|
||||
this.updateHandlingCosts();
|
||||
resolve();
|
||||
}, 10));
|
||||
|
||||
await new Promise(resolve => setTimeout(() => {
|
||||
this.updateRoutes();
|
||||
resolve();
|
||||
}, 10));
|
||||
|
||||
const destinationMap = new Map();
|
||||
|
||||
premiseIds.forEach(premiseId => {
|
||||
this.destinations.get(premiseId)?.forEach(toUpdate => {
|
||||
|
||||
destinationMap.set(toUpdate.id,{
|
||||
annual_amount: toUpdate.annual_amount,
|
||||
repackaging_costs: toUpdate.repackaging_costs,
|
||||
handling_costs: toUpdate.handling_costs,
|
||||
disposal_costs: toUpdate.disposal_costs,
|
||||
is_d2d: toUpdate.is_d2d,
|
||||
rate_d2d: toUpdate.rate_d2d,
|
||||
lead_time_d2d: toUpdate.lead_time_d2d,
|
||||
route_selected_id: toUpdate.routes.find(r => r.is_selected)?.id ?? null,
|
||||
})
|
||||
} )
|
||||
});
|
||||
|
||||
await performRequest(this, 'PUT', `${config.backendUrl}/calculation/destination/all`, {destinations: Object.fromEntries(destinationMap)}, false);
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
updateRoutes() {
|
||||
this.routeMatrix.forEach(row => {
|
||||
|
||||
row.ids.forEach(premiseId => {
|
||||
row.destinations.forEach(destinationUpdateInfo => {
|
||||
const destOfCurPremisses = this.destinations.get(premiseId);
|
||||
|
||||
if ((destOfCurPremisses ?? null) !== null) {
|
||||
const destOfCurPremise = destOfCurPremisses.find(d => destinationUpdateInfo.destinationNodeId === d.destination_node.id);
|
||||
|
||||
if(destOfCurPremisses && destinationUpdateInfo.ids.includes(destOfCurPremise?.id)) {
|
||||
/* set d2d stuff */
|
||||
destOfCurPremise.is_d2d = destinationUpdateInfo.isD2d;
|
||||
destOfCurPremise.rate_d2d = destinationUpdateInfo.rateD2d;
|
||||
destOfCurPremise.lead_time_d2d = destinationUpdateInfo.leadTimeD2d;
|
||||
|
||||
/* set selected route */
|
||||
const selectedRoute = destinationUpdateInfo.routes?.find(r => r.routeCompareString === destinationUpdateInfo.selectedRoute);
|
||||
|
||||
destOfCurPremise.routes.forEach(r => r.is_selected = false);
|
||||
|
||||
if(selectedRoute) {
|
||||
const routeOfCurPremise = destOfCurPremise.routes.find(r => selectedRoute.ids.includes(r.id));
|
||||
|
||||
if(routeOfCurPremise) {
|
||||
routeOfCurPremise.is_selected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
updateHandlingCosts() {
|
||||
this.handlingCostMatrix.forEach(row => {
|
||||
const destinations = this.destinations.get(row.id);
|
||||
|
||||
if ((destinations ?? null) !== null) {
|
||||
|
||||
const destination = destinations.find(dest => dest.id === row.destinationId);
|
||||
|
||||
if ((destination ?? null) !== null) {
|
||||
destination.disposal_costs = row.disposal_costs;
|
||||
destination.repackaging_costs = row.repackaging_costs;
|
||||
destination.handling_costs = row.handling_costs;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
updateQuantity() {
|
||||
this.quantityMatrix.forEach(row => {
|
||||
const destinations = this.destinations.get(row.id);
|
||||
|
||||
|
||||
if ((destinations ?? null) !== null) {
|
||||
row.destinations
|
||||
.filter(newDest => newDest.id !== null)
|
||||
.forEach(newDest => {
|
||||
|
||||
const found = destinations.find(dest => dest.id === newDest.id);
|
||||
if (found)
|
||||
found.annual_amount = newDest.annual_amount;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
import {defineStore} from "pinia";
|
||||
import {config} from "@/config.js";
|
||||
import performRequest from "@/backend.js";
|
||||
|
||||
export const useHelpStore = defineStore('help', {
|
||||
state() {
|
||||
return {
|
||||
currentPage: null,
|
||||
pages: null,
|
||||
content: null,
|
||||
showHelp: false,
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
title(state) {
|
||||
return state.pages?.find(p => p.page === state.currentPage)?.title;
|
||||
},
|
||||
videoUrl(state) {
|
||||
return state.content?.video;
|
||||
},
|
||||
text(state) {
|
||||
return state.content?.content;
|
||||
},
|
||||
showHelpVideo() {
|
||||
return this.baseUrl && this.baseUrl !== "";
|
||||
},
|
||||
enableHelp() {
|
||||
return this.pages !== null && this.pages.length > 0;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async getContent(pageId) {
|
||||
const url = `${config.backendUrl}/help/content/${pageId}`;
|
||||
const {data: data, headers: headers} = await performRequest(this, 'GET', url, null);
|
||||
console.log("help system", data);
|
||||
this.content = data;
|
||||
this.currentPage = pageId;
|
||||
},
|
||||
async loadPages() {
|
||||
const url = `${config.backendUrl}/help/content`;
|
||||
const {data: data, headers: headers} = await performRequest(this, 'GET', url, null);
|
||||
this.pages = data;
|
||||
},
|
||||
async activateHelp(pageId) {
|
||||
await this.getContent(pageId);
|
||||
this.showHelp = true;
|
||||
|
||||
},
|
||||
closeHelp() {
|
||||
this.showHelp = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -33,9 +33,6 @@ export const useNotificationStore = defineStore('notification', {
|
|||
return this.notifications.pop();
|
||||
},
|
||||
addNotification(notification) {
|
||||
|
||||
console.log("add notification", notification, this.notifications.length)
|
||||
|
||||
this.notifications.push({
|
||||
icon: notification.icon ?? null,
|
||||
message: notification.message ?? 'Unknown notification',
|
||||
|
|
@ -98,8 +95,8 @@ export const useNotificationStore = defineStore('notification', {
|
|||
this.stopAutoSubmitTimer()
|
||||
this.sendCache = [];
|
||||
} else {
|
||||
logger.error("Error transmitting errors: " + url, params);
|
||||
logger.error(response, await response?.text());
|
||||
console.error("Error transmitting errors: " + url, params);
|
||||
console.error(response, await response?.text());
|
||||
this.startAutoSubmitTimer();
|
||||
}
|
||||
},
|
||||
|
|
@ -131,7 +128,7 @@ export const useNotificationStore = defineStore('notification', {
|
|||
const pinia = this.$pinia || getActivePinia()
|
||||
if (pinia && pinia._s) {
|
||||
pinia._s.forEach((store, storeId) => {
|
||||
if (storeId !== 'notification' && storeId !== 'errorLog' && store.$state) {
|
||||
if (storeId !== 'error' && storeId !== 'errorLog' && store.$state) {
|
||||
storeState[storeId] = {
|
||||
...toRaw(store.$state)
|
||||
}
|
||||
|
|
@ -140,7 +137,7 @@ export const useNotificationStore = defineStore('notification', {
|
|||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn('Failed to capture store state:', err);
|
||||
console.warn('Failed to capture store state:', err);
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import {config} from '@/config'
|
||||
import {toRaw} from "vue";
|
||||
import {useNotificationStore} from "@/store/notification.js";
|
||||
import logger from "@/logger.js"
|
||||
import performRequest from '@/backend.js'
|
||||
|
||||
|
|
@ -7,22 +9,67 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
state() {
|
||||
return {
|
||||
premisses: null,
|
||||
selectedIds: [],
|
||||
sortedBy: 'id',
|
||||
order: new Map([['id', 'desc'], ['material', 'desc'], ['supplier', 'desc']]),
|
||||
|
||||
/**
|
||||
* set to true while the store is loading the premises.
|
||||
*/
|
||||
loading: false,
|
||||
|
||||
processing: false,
|
||||
/**
|
||||
* set to true while the store sets the selected/deselected field in the premises.
|
||||
*/
|
||||
selectedLoading: false,
|
||||
|
||||
|
||||
destinations: null,
|
||||
processDestinationMassEdit: false,
|
||||
|
||||
selectedDestination: null,
|
||||
|
||||
throwsException: true,
|
||||
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
|
||||
getCountryIdByPremiseIds(state) {
|
||||
return function (ids) {
|
||||
if (state.loading) {
|
||||
if (state.throwsException)
|
||||
throw new Error("Premises are accessed while still loading.");
|
||||
return null;
|
||||
}
|
||||
|
||||
const premiss = state.premisses?.filter(p => ids.some(id => id === p.id));
|
||||
|
||||
const premiseCountryMap = new Map();
|
||||
premiss?.forEach(premise => {
|
||||
premiseCountryMap.set(premise.id, premise.supplier?.country?.id);
|
||||
});
|
||||
|
||||
return premiseCountryMap;
|
||||
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the ids of all premises.
|
||||
* @param state
|
||||
* @returns {*}
|
||||
*/
|
||||
getPremiseIds(state) {
|
||||
if (state.loading) {
|
||||
if (state.throwsException)
|
||||
throw new Error("Premises are accessed while still loading.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return state.premisses?.map(p => p.id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the premises.
|
||||
* @param state
|
||||
|
|
@ -66,7 +113,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
* @param state
|
||||
* @returns {T[]}
|
||||
*/
|
||||
getSelectedPremiseIds(state) {
|
||||
getSelectedPremisses(state) {
|
||||
if (state.loading || state.selectedLoading) {
|
||||
if (state.throwsException)
|
||||
throw new Error("Premises are accessed while still loading.");
|
||||
|
|
@ -74,7 +121,24 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
return null;
|
||||
}
|
||||
|
||||
return state.selectedIds;
|
||||
return state.premisses.filter(p => p.selected);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Returns all premise ids that are selected.
|
||||
* @param state
|
||||
* @returns {T[]}
|
||||
*/
|
||||
getSelectedPremissesIds(state) {
|
||||
if (state.loading || state.selectedLoading) {
|
||||
if (state.throwsException)
|
||||
throw new Error("Premises are accessed while still loading.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return state.premisses.filter(p => p.selected).map(p => p.id);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -92,7 +156,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
},
|
||||
|
||||
/**
|
||||
* Returns true if the premises are loaded and not empty. The frontend can show the loaded premisses.
|
||||
* Returns true if the premises are loaded and not empty. The frontend can show a the loaded premisses.
|
||||
* @param state
|
||||
* @returns {boolean}
|
||||
*/
|
||||
|
|
@ -110,115 +174,80 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
},
|
||||
|
||||
showProcessingModal(state) {
|
||||
return state.processing;
|
||||
return state.processDestinationMassEdit;
|
||||
},
|
||||
|
||||
/**
|
||||
* Getters for controlling getters
|
||||
* ======================================
|
||||
* Getters for single edit view
|
||||
* ============================
|
||||
*/
|
||||
|
||||
allChecked(state) {
|
||||
if (state.premisses.length > state.selectedIds.length)
|
||||
return false;
|
||||
/**
|
||||
* Returns true if only one premise is selected.
|
||||
* @param state
|
||||
* @returns {boolean|null}
|
||||
*/
|
||||
isSingleSelect(state) {
|
||||
if (state.loading || state.selectedLoading) {
|
||||
if (state.throwsException)
|
||||
throw new Error("Premises are accessed while still loading.");
|
||||
|
||||
for (const premise of state.premisses) {
|
||||
if (!state.selectedIds.includes(premise.id))
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
return state.premisses.length !== 0;
|
||||
return state.premisses.filter(p => p.selected).length === 1;
|
||||
},
|
||||
someChecked(state) {
|
||||
for (const premise of state.premisses) {
|
||||
if (state.selectedIds.includes(premise.id))
|
||||
return true;
|
||||
|
||||
/**
|
||||
* Returns the id of the single selected premise.
|
||||
* @param state
|
||||
* @returns {*}
|
||||
*/
|
||||
singleSelectId(state) {
|
||||
if (state.loading || state.selectedLoading) {
|
||||
if (state.throwsException)
|
||||
throw new Error("Premises are accessed while still loading.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
isChecked(state) {
|
||||
return (id) => {
|
||||
return state.selectedIds.includes(id);
|
||||
if (!state.isSingleSelect) {
|
||||
return null;
|
||||
// throw new Error("Single selected premise accessed, but not in single select mode");
|
||||
}
|
||||
|
||||
|
||||
return state.premisses.find(p => p.selected)?.id;
|
||||
},
|
||||
activeSort(state) {
|
||||
return state.sortedBy;
|
||||
},
|
||||
directionSort(state) {
|
||||
return (sort) => {
|
||||
return state.order.get(sort);
|
||||
|
||||
|
||||
/**
|
||||
* Returns the single selected premise.
|
||||
* @param state
|
||||
* @returns {*}
|
||||
*/
|
||||
singleSelectedPremise(state) {
|
||||
if (state.loading || state.selectedLoading) {
|
||||
if (state.throwsException)
|
||||
throw new Error("Premises are accessed while still loading.");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!state.isSingleSelect) {
|
||||
return null;
|
||||
// throw new Error("Single selected premise accessed, but not in single select mode");
|
||||
}
|
||||
|
||||
return state.premisses?.find(p => p.selected);
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
},
|
||||
actions: {
|
||||
|
||||
sort(type) {
|
||||
|
||||
|
||||
this.loading = true;
|
||||
|
||||
const direction = (type !== this.sortedBy) ? this.order.get(type) : (this.order.get(type) === 'asc' ? 'desc' : 'asc');
|
||||
|
||||
const temp = this.premisses.slice();
|
||||
temp.sort((a, b) => {
|
||||
if (type === 'material')
|
||||
return direction === 'asc' ?
|
||||
a.material.part_number.localeCompare(b.material.part_number) :
|
||||
b.material.part_number.localeCompare(a.material.part_number);
|
||||
else if (type === 'supplier')
|
||||
return direction === 'asc' ?
|
||||
a.supplier.name.localeCompare(b.supplier.name) :
|
||||
b.supplier.name.localeCompare(a.supplier.name);
|
||||
else return a.id - b.id;
|
||||
});
|
||||
|
||||
|
||||
this.premisses = temp;
|
||||
this.sortedBy = type;
|
||||
this.order.set(type, direction);
|
||||
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
async startCalculation() {
|
||||
|
||||
this.processing = true;
|
||||
|
||||
const body = {premise_ids: this.premisses.map(p => p.id)};
|
||||
const url = `${config.backendUrl}/calculation/start/`;
|
||||
let error = null;
|
||||
|
||||
await performRequest(this, 'PUT', url, body, false, ['Premiss validation error', 'Internal Server Error']).catch(e => {
|
||||
logger.log("startCalculation exception", e.errorObj);
|
||||
error = e.errorObj;
|
||||
})
|
||||
|
||||
this.processing = false;
|
||||
return error;
|
||||
},
|
||||
|
||||
/**
|
||||
* Save
|
||||
*/
|
||||
|
||||
async batchUpdate(type, ids, data) {
|
||||
switch (type) {
|
||||
case 'price':
|
||||
this.batchUpdatePrice(ids, data);
|
||||
break;
|
||||
case 'material':
|
||||
this.batchUpdateMaterial(ids, data);
|
||||
break;
|
||||
case 'packaging':
|
||||
this.batchUpdatePackaging(ids, data);
|
||||
break;
|
||||
}
|
||||
|
||||
},
|
||||
async batchUpdatePrice(ids, priceData) {
|
||||
|
||||
const updatedPremises = this.premisses.map(p => {
|
||||
if (ids.includes(p.id)) {
|
||||
return {
|
||||
|
|
@ -250,10 +279,13 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
});
|
||||
this.premisses = updatedPremises;
|
||||
|
||||
|
||||
|
||||
return await this.saveMaterial(ids, materialData);
|
||||
},
|
||||
async batchUpdatePackaging(ids, packagingData) {
|
||||
|
||||
|
||||
const updatedPremises = this.premisses.map(p => {
|
||||
if (ids.includes(p.id)) {
|
||||
return {
|
||||
|
|
@ -276,13 +308,354 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
});
|
||||
this.premisses = updatedPremises;
|
||||
|
||||
logger.info("packaging data:", toRaw(packagingData), "update result", toRaw(updatedPremises));
|
||||
|
||||
return await this.savePackaging(ids, packagingData);
|
||||
},
|
||||
async startCalculation() {
|
||||
|
||||
const body = this.premisses.map(p => p.id);
|
||||
const url = `${config.backendUrl}/calculation/start/`;
|
||||
let error = null;
|
||||
|
||||
await performRequest(this, 'PUT', url, body, false, ['Premiss validation error', 'Internal Server Error']).catch(e => {
|
||||
logger.log("startCalculation exception", e.errorObj);
|
||||
error = e.errorObj;
|
||||
})
|
||||
|
||||
return error;
|
||||
},
|
||||
|
||||
/**
|
||||
* DESTINATION stuff
|
||||
* =================
|
||||
*/
|
||||
|
||||
prepareDestinations(dataSourcePremiseId, editedPremiseIds, massEdit = false, fromMassEditView = false) {
|
||||
if (this.premisses === null) return;
|
||||
if (!editedPremiseIds || !dataSourcePremiseId || editedPremiseIds.length === 0) return;
|
||||
|
||||
this.destinations = {
|
||||
premise_ids: editedPremiseIds,
|
||||
massEdit: massEdit,
|
||||
fromMassEditView: fromMassEditView,
|
||||
destinations: this.premisses.find(p => String(p.id) === String(dataSourcePremiseId))?.destinations.map(d => this.copyAllFromPremises(d, !massEdit)) ?? [],
|
||||
};
|
||||
|
||||
this.selectedDestination = null;
|
||||
|
||||
},
|
||||
async executeDestinationsMassEdit() {
|
||||
|
||||
if (!this.destinations.massEdit) {
|
||||
|
||||
this.destinations.premise_ids.forEach(premiseId => {
|
||||
const toPremise = this.getById(premiseId);
|
||||
|
||||
this.destinations.destinations.forEach(fromDest => {
|
||||
const toDest = toPremise.destinations.find(to => fromDest.id.substring(1) === String(to.id));
|
||||
|
||||
if ((toDest ?? null) === null) {
|
||||
throw new Error("Destination not found in premise: " + premiseId + " -> " + d.id);
|
||||
}
|
||||
|
||||
this.copyAllToPremise(fromDest, toDest);
|
||||
|
||||
const body = {
|
||||
annual_amount: toDest.annual_amount,
|
||||
repackaging_costs: toDest.repackaging_costs,
|
||||
handling_costs: toDest.handling_costs,
|
||||
disposal_costs: toDest.disposal_costs,
|
||||
is_d2d: toDest.is_d2d,
|
||||
rate_d2d: toDest.rate_d2d,
|
||||
lead_time_d2d: toDest.lead_time_d2d,
|
||||
route_selected_id: toDest.routes.find(r => r.is_selected)?.id ?? null,
|
||||
};
|
||||
|
||||
const url = `${config.backendUrl}/calculation/destination/${toDest.id}`;
|
||||
performRequest(this, 'PUT', url, body, false);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
} else {
|
||||
this.processDestinationMassEdit = true;
|
||||
|
||||
const destinations = [];
|
||||
|
||||
this.destinations.destinations.forEach(d => {
|
||||
const dest = {
|
||||
destination_node_id: d.destination_node.id,
|
||||
annual_amount: d.annual_amount,
|
||||
disposal_costs: d.userDefinedHandlingCosts ? d.disposal_costs : null,
|
||||
repackaging_costs: d.userDefinedHandlingCosts ? d.repackaging_costs : null,
|
||||
handling_costs: d.userDefinedHandlingCosts ? d.handling_costs : null,
|
||||
}
|
||||
destinations.push(dest);
|
||||
})
|
||||
|
||||
const body = {destinations: destinations, premise_id: this.destinations.premise_ids};
|
||||
const url = `${config.backendUrl}/calculation/destination/`;
|
||||
|
||||
const {data: data, headers: headers} = await performRequest(this, 'PUT', url, body).catch(e => {
|
||||
this.destinations = null;
|
||||
this.processDestinationMassEdit = false;
|
||||
});
|
||||
|
||||
if (data) {
|
||||
for (const id of Object.keys(data)) {
|
||||
this.premisses.find(p => String(p.id) === id).destinations = data[id];
|
||||
}
|
||||
}
|
||||
|
||||
this.destinations = null;
|
||||
this.processDestinationMassEdit = false;
|
||||
}
|
||||
},
|
||||
cancelMassEdit() {
|
||||
this.destinations = null;
|
||||
},
|
||||
|
||||
copyAllFromPremises(from, fullCopy = true) {
|
||||
|
||||
const d = {};
|
||||
|
||||
d.id = `e${from.id}`;
|
||||
d.destination_node = structuredClone(toRaw(from.destination_node));
|
||||
d.routes = fullCopy ? structuredClone(toRaw(from.routes)) : null;
|
||||
|
||||
d.annual_amount = from.annual_amount;
|
||||
d.is_d2d = from.is_d2d;
|
||||
d.rate_d2d = from.is_d2d ? from.rate_d2d : null;
|
||||
d.lead_time_d2d = from.is_d2d ? from.lead_time_d2d : null;
|
||||
d.handling_costs = from.handling_costs;
|
||||
d.disposal_costs = from.disposal_costs;
|
||||
d.repackaging_costs = from.repackaging_costs;
|
||||
d.userDefinedHandlingCosts = from.handling_costs !== null || from.disposal_costs !== null || from.repackaging_costs !== null;
|
||||
|
||||
return d;
|
||||
},
|
||||
copyAllToPremise(from, to, fullCopy = true) {
|
||||
|
||||
const d = to ?? {};
|
||||
|
||||
d.annual_amount = from.annual_amount;
|
||||
d.is_d2d = from.is_d2d;
|
||||
d.rate_d2d = from.is_d2d ? from.rate_d2d : null;
|
||||
d.lead_time_d2d = from.is_d2d ? from.lead_time_d2d : null;
|
||||
|
||||
if (from.userDefinedHandlingCosts) {
|
||||
d.disposal_costs = from.disposal_costs;
|
||||
d.repackaging_costs = from.repackaging_costs;
|
||||
d.handling_costs = from.handling_costs;
|
||||
} else {
|
||||
d.disposal_costs = null;
|
||||
d.repackaging_costs = null;
|
||||
d.handling_costs = null;
|
||||
}
|
||||
|
||||
if (fullCopy && (from.routes ?? null) !== null) {
|
||||
to.routes.forEach(route => route.is_selected = from.routes.find(r => r.id === route.id)?.is_selected ?? false);
|
||||
}
|
||||
|
||||
return d;
|
||||
},
|
||||
|
||||
/**
|
||||
* Selects all destinations for the given "ids" for editing.
|
||||
* This creates a copy of the destination with id "id".
|
||||
* They are written back as soon as the user closes the dialog.
|
||||
*/
|
||||
selectDestination(id) {
|
||||
if (this.premisses === null) return;
|
||||
|
||||
logger.info("selectDestination:", id)
|
||||
|
||||
const dest = this.destinations.destinations.find(d => d.id === id);
|
||||
|
||||
|
||||
if ((dest ?? null) == null) {
|
||||
const error = {
|
||||
code: 'Frontend error.',
|
||||
message: `Destination not found: ${id}. Please contact support.`,
|
||||
trace: null
|
||||
}
|
||||
throw new Error("Internal frontend error: Destination not found: " + id);
|
||||
}
|
||||
|
||||
this.selectedDestination = structuredClone(toRaw(dest));
|
||||
},
|
||||
async deselectDestinations(save = false) {
|
||||
if (this.premisses === null) return;
|
||||
|
||||
|
||||
if (save) {
|
||||
const idx = this.destinations.destinations.findIndex(d => d.id === this.selectedDestination.id);
|
||||
this.destinations.destinations.splice(idx, 1, this.selectedDestination);
|
||||
|
||||
if (!this.destinations.fromMassEditView) {
|
||||
//TODO write trough backend if no massEdit
|
||||
|
||||
const toDest = this.singleSelectedPremise.destinations.find(to => this.selectedDestination.id.substring(1) === String(to.id));
|
||||
this.copyAllToPremise(this.selectedDestination, toDest);
|
||||
|
||||
const body = {
|
||||
annual_amount: toDest.annual_amount,
|
||||
repackaging_costs: toDest.repackaging_costs,
|
||||
handling_costs: toDest.handling_costs,
|
||||
disposal_costs: toDest.disposal_costs,
|
||||
is_d2d: toDest.is_d2d,
|
||||
rate_d2d: toDest.rate_d2d,
|
||||
lead_time_d2d: toDest.lead_time_d2d,
|
||||
route_selected_id: toDest.routes.find(r => r.is_selected)?.id ?? null,
|
||||
};
|
||||
|
||||
logger.info(body)
|
||||
|
||||
const url = `${config.backendUrl}/calculation/destination/${toDest.id}`;
|
||||
await performRequest(this, 'PUT', url, body, false);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.selectedDestination = null;
|
||||
},
|
||||
async deleteDestination(id) {
|
||||
|
||||
|
||||
/*
|
||||
* 1. delete from destinations copy
|
||||
*/
|
||||
const idx = this.destinations.destinations.findIndex(d => d.id === id);
|
||||
|
||||
if (idx === -1) {
|
||||
logger.info("Destination not found in mass edit: , id)");
|
||||
return;
|
||||
}
|
||||
|
||||
this.destinations.destinations.splice(idx, 1);
|
||||
|
||||
/*
|
||||
* 2. delete from backend if not mass edit
|
||||
*/
|
||||
|
||||
if (!this.destinations.massEdit && id.startsWith('e')) { /* 'v'-ids cannot be deleted because they only exist in the frontend */
|
||||
if (this.premisses === null) return;
|
||||
|
||||
const origId = id.substring(1);
|
||||
|
||||
const url = `${config.backendUrl}/calculation/destination/${origId}`;
|
||||
await performRequest(this, 'DELETE', url, null, false).catch(async e => {
|
||||
logger.error("Unable to delete destination: " + origId + "");
|
||||
logger.error(e);
|
||||
await this.loadPremissesIfNeeded(this.premisses.map(p => p.id));
|
||||
});
|
||||
|
||||
for (const p of this.premisses) {
|
||||
const toBeDeleted = p.destinations.findIndex(d => String(d.id) === String(origId))
|
||||
|
||||
logger.info(toBeDeleted)
|
||||
|
||||
if (toBeDeleted !== -1) {
|
||||
p.destinations.splice(toBeDeleted, 1)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async addDestination(node) {
|
||||
|
||||
if (this.destinations.massEdit) {
|
||||
|
||||
const existing = this.destinations.destinations.find(d => d.destination_node.id === node.id);
|
||||
logger.info(existing)
|
||||
|
||||
if ((existing ?? null) !== null) {
|
||||
logger.info("Destination already exists", node.id);
|
||||
return [existing.id];
|
||||
}
|
||||
|
||||
const destination = {
|
||||
id: `v${node.id}`,
|
||||
destination_node: structuredClone(toRaw(node)),
|
||||
massEdit: true,
|
||||
annual_amount: 0,
|
||||
is_d2d: false,
|
||||
rate_d2d: null,
|
||||
lead_time_d2d: null,
|
||||
disposal_costs: null,
|
||||
repackaging_costs: null,
|
||||
handling_costs: null,
|
||||
userDefinedHandlingCosts: false,
|
||||
};
|
||||
|
||||
this.destinations.destinations.push(destination);
|
||||
|
||||
return [destination.id];
|
||||
|
||||
} else {
|
||||
const id = node.id;
|
||||
|
||||
this.processDestinationMassEdit = true;
|
||||
|
||||
|
||||
const toBeUpdated = this.destinations.fromMassEditView ? this.destinations.premise_ids : this.premisses?.filter(p => p.selected).map(p => p.id);
|
||||
|
||||
if (toBeUpdated === null || toBeUpdated.length === 0) return;
|
||||
|
||||
const body = {destination_node_id: id, premise_id: toBeUpdated};
|
||||
const url = `${config.backendUrl}/calculation/destination/`;
|
||||
|
||||
|
||||
const {data: destinations} = await performRequest(this, 'POST', url, body).catch(e => {
|
||||
this.loading = false;
|
||||
this.selectedLoading = false;
|
||||
this.processDestinationMassEdit = false;
|
||||
throw e;
|
||||
});
|
||||
|
||||
const mappedIds = []
|
||||
|
||||
for (const id of Object.keys(destinations)) {
|
||||
const premise = this.premisses.find(p => String(p.id) === id)
|
||||
premise.destinations.push(destinations[id]);
|
||||
const mappedDestination = this.copyAllFromPremises(destinations[id], true);
|
||||
mappedIds.push(mappedDestination.id);
|
||||
this.destinations.destinations.push(mappedDestination);
|
||||
}
|
||||
|
||||
this.processDestinationMassEdit = false;
|
||||
|
||||
return mappedIds;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Replace the premisses with the loaded ones by id.
|
||||
* This is used to update the premisses after a "Set" change.
|
||||
* @param premisses
|
||||
* @param loadedData
|
||||
* @returns {*}
|
||||
*/
|
||||
replacePremissesById(premisses, loadedData) {
|
||||
const replacementMap = new Map(loadedData.map(obj => [obj.id, obj]));
|
||||
const replaced = premisses.map(obj => replacementMap.get(obj.id) || obj);
|
||||
logger.info("Replaced", replaced);
|
||||
return replaced;
|
||||
},
|
||||
|
||||
/**
|
||||
* Save
|
||||
*/
|
||||
|
||||
async savePrice(ids = null, priceData = null) {
|
||||
let success = true;
|
||||
const toBeUpdated = this.premisses ? (ids ? (ids.map(id => this.premisses.find(p => String(p.id) === String(id)))) : (this.selectedIds.map(id => this.premisses.find(p => String(p.id) === String(id))))) : null;
|
||||
|
||||
|
||||
const toBeUpdated = this.premisses ? (ids ? (ids.map(id => this.premisses.find(p => String(p.id) === String(id)))) : (this.premisses.filter(p => p.selected))) : null;
|
||||
|
||||
if (!toBeUpdated?.length) return;
|
||||
|
||||
|
|
@ -302,7 +675,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
async savePackaging(ids = null, packagingData = null) {
|
||||
let success = true;
|
||||
|
||||
const toBeUpdated = this.premisses ? (ids ? (ids.map(id => this.premisses.find(p => String(p.id) === String(id)))) : (this.premisses.filter(p => this.selectedIds.includes(p.id)))) : null;
|
||||
const toBeUpdated = this.premisses ? (ids ? (ids.map(id => this.premisses.find(p => String(p.id) === String(id)))) : (this.premisses.filter(p => p.selected))) : null;
|
||||
|
||||
if (!toBeUpdated?.length) return;
|
||||
|
||||
|
|
@ -335,7 +708,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
},
|
||||
async saveMaterial(ids = null, materialData = null) {
|
||||
let success = true;
|
||||
const toBeUpdated = this.premisses ? (ids ? (ids.map(id => this.premisses.find(p => String(p.id) === String(id)))) : (this.premisses.filter(p => this.selectedIds.includes(p.id)))) : null;
|
||||
const toBeUpdated = this.premisses ? (ids ? (ids.map(id => this.premisses.find(p => String(p.id) === String(id)))) : (this.premisses.filter(p => p.selected))) : null;
|
||||
|
||||
|
||||
if (!toBeUpdated?.length) return;
|
||||
|
|
@ -360,56 +733,80 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
* PREMISE stuff
|
||||
* =================
|
||||
*/
|
||||
deselectPremise() {
|
||||
this.selectedLoading = true;
|
||||
|
||||
this.premisses.forEach(p => p.selected = false);
|
||||
|
||||
this.destinations = null;
|
||||
this.selectedDestination = null;
|
||||
this.selectedLoading = false;
|
||||
},
|
||||
setAll(value) {
|
||||
|
||||
this.selectedLoading = true;
|
||||
|
||||
const temp = [];
|
||||
|
||||
if (value)
|
||||
this.premisses.forEach(p => temp.push(p.id));
|
||||
|
||||
this.selectedIds = temp;
|
||||
const updatedPremises = this.premisses.map(p => ({
|
||||
...p,
|
||||
selected: value
|
||||
}));
|
||||
this.premisses = updatedPremises;
|
||||
|
||||
this.selectedLoading = false;
|
||||
|
||||
},
|
||||
setChecked(premiseId, checked) {
|
||||
setSelectTo(ids, value) {
|
||||
this.selectedLoading = true;
|
||||
|
||||
if (checked) {
|
||||
if (!this.selectedIds.includes(premiseId)) {
|
||||
this.selectedIds.push(premiseId);
|
||||
}
|
||||
} else {
|
||||
const idx = this.selectedIds.indexOf(premiseId);
|
||||
if (idx !== -1)
|
||||
this.selectedIds.splice(idx, 1);
|
||||
}
|
||||
const idsSet = new Set(ids);
|
||||
const updatedPremises = this.premisses.map(p => ({
|
||||
...p,
|
||||
selected: idsSet.has(p.id) ? value : p.selected
|
||||
}));
|
||||
this.premisses = updatedPremises;
|
||||
|
||||
this.selectedLoading = false;
|
||||
},
|
||||
setBy(type, action, ofId) {
|
||||
async selectSinglePremise(id, ids) {
|
||||
this.selectedLoading = true;
|
||||
const premise = this.premisses.find(p => p.id === ofId);
|
||||
|
||||
const temp = [];
|
||||
await this.loadPremissesIfNeeded(ids);
|
||||
|
||||
if (action === 'append')
|
||||
temp.push(...this.selectedIds);
|
||||
this.premisses.forEach(p => p.selected = String(id) === String(p.id));
|
||||
|
||||
this.premisses.forEach(p => {
|
||||
if (type === 'supplier' && p.supplier.id === premise.supplier.id) {
|
||||
temp.push(p.id);
|
||||
} else if (type === 'material' && p.material.id === premise.material.id) {
|
||||
temp.push(p.id);
|
||||
}
|
||||
this.prepareDestinations(id, [id]);
|
||||
|
||||
this.selectedLoading = false;
|
||||
},
|
||||
async loadAndSelectSinglePremise(id) {
|
||||
|
||||
this.loading = true;
|
||||
this.selectedLoading = true;
|
||||
this.premises = [];
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('premissIds', `${[id]}`);
|
||||
const url = `${config.backendUrl}/calculation/edit/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||
|
||||
const {data: data, headers: headers} = await performRequest(this, 'GET', url, null).catch(e => {
|
||||
this.selectedLoading = false;
|
||||
this.loading = false;
|
||||
});
|
||||
this.premisses = data;
|
||||
|
||||
this.selectedIds = temp;
|
||||
|
||||
this.premisses.forEach(p => p.selected = true);
|
||||
this.prepareDestinations(id, [id]);
|
||||
this.selectedLoading = false;
|
||||
this.loading = false;
|
||||
},
|
||||
async load(ids) {
|
||||
async loadPremissesIfNeeded(ids, exact = false) {
|
||||
const reload = this.premisses ? !ids.every((id) => this.premisses.find(d => d.id === id) && (!exact || ids.length === this.premisses.length)) : true;
|
||||
|
||||
if (reload) {
|
||||
await this.loadPremissesForced(ids);
|
||||
}
|
||||
},
|
||||
async loadPremissesForced(ids) {
|
||||
|
||||
this.loading = true;
|
||||
this.premises = [];
|
||||
|
|
@ -423,11 +820,10 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
});
|
||||
this.premisses = data;
|
||||
|
||||
this.selectedIds = [];
|
||||
|
||||
this.premisses.forEach(p => p.selected = false);
|
||||
this.loading = false;
|
||||
|
||||
return this.premisses;
|
||||
|
||||
},
|
||||
removePremise(id) {
|
||||
const idx = this.premisses.findIndex(p => p.id === id);
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ export const usePremiseSingleEditStore = defineStore('premiseSingleEdit', {
|
|||
async startCalculation() {
|
||||
|
||||
this.calculating = true;
|
||||
const body = {premise_ids:[this.premise?.id]};
|
||||
const body = [this.premise?.id];
|
||||
const url = `${config.backendUrl}/calculation/start/`;
|
||||
let error = null;
|
||||
|
||||
|
|
@ -90,13 +90,9 @@ export const usePremiseSingleEditStore = defineStore('premiseSingleEdit', {
|
|||
if (this.premise === null) return;
|
||||
this.routing = true;
|
||||
|
||||
const destinationNodeIds = {};
|
||||
destinationNodeIds[this.premise.id] = [node.id, ...this.premise.destinations.map(d => d.destination_node.id)];
|
||||
|
||||
const body = {destination_node_ids: destinationNodeIds};
|
||||
const body = {destination_node_id: node.id, premise_id: [this.premise.id]};
|
||||
const url = `${config.backendUrl}/calculation/destination/`;
|
||||
|
||||
logger.info("addDestination", body, url);
|
||||
|
||||
const {data: destinations} = await performRequest(this, 'POST', url, body).catch(e => {
|
||||
this.routing = false;
|
||||
|
|
@ -105,11 +101,10 @@ export const usePremiseSingleEditStore = defineStore('premiseSingleEdit', {
|
|||
|
||||
const ids = []
|
||||
|
||||
if (destinations[this.premise.id]?.length !== 0)
|
||||
for (const destId of Object.keys(destinations[this.premise.id])) {
|
||||
this.premise.destinations.push(destinations[this.premise.id][destId]);
|
||||
ids.push(destinations[this.premise.id][destId].id);
|
||||
}
|
||||
for (const destId of Object.keys(destinations)) {
|
||||
this.premise.destinations.push(destinations[destId]);
|
||||
ids.push(destinations[destId].id);
|
||||
}
|
||||
|
||||
this.routing = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ export const useReportsStore = defineStore('reports', {
|
|||
let max = 0;
|
||||
|
||||
state.reports.forEach(report => {
|
||||
max = Math.max(report.overview.mek_b.total, max);
|
||||
max = Math.max(report.overview.risk_scenario.total, max);
|
||||
max = Math.max(report.risk.mek_b.total, max);
|
||||
max = Math.max(report.risk.risk_scenario.total, max);
|
||||
})
|
||||
|
||||
const magnitude = Math.pow(10, Math.floor(Math.log10(max)));
|
||||
|
|
@ -37,7 +37,7 @@ export const useReportsStore = defineStore('reports', {
|
|||
return;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('materials', [this.materialId]);
|
||||
params.append('material', this.materialId);
|
||||
params.append('sources', this.supplierIds);
|
||||
params.append('userSources', this.userSupplierIds);
|
||||
|
||||
|
|
@ -74,14 +74,14 @@ export const useReportsStore = defineStore('reports', {
|
|||
this.showComparableWarning = false;
|
||||
for (const [idx, report] of this.reports.entries()) {
|
||||
for (const otherReport of this.reports.slice(idx + 1)) {
|
||||
if (report.destinations.length !== otherReport.destinations.length) {
|
||||
if (report.premises.length !== otherReport.premises.length) {
|
||||
this.showComparableWarning = true;
|
||||
break;
|
||||
}
|
||||
|
||||
for (const premise of report.destinations) {
|
||||
for (const premise of report.premises) {
|
||||
|
||||
const otherPremise = otherReport.destinations.find(otherPremise => otherPremise.destination.external_mapping_id === premise.destination.external_mapping_id);
|
||||
const otherPremise = otherReport.premises.find(otherPremise => otherPremise.destination.external_mapping_id === premise.destination.external_mapping_id);
|
||||
|
||||
if((otherPremise ?? null) == null) {
|
||||
this.showComparableWarning = true;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export default defineConfig({
|
|||
src: 'assets/map.json',
|
||||
dest: 'assets/'
|
||||
}
|
||||
|
||||
]
|
||||
})
|
||||
],
|
||||
|
|
@ -34,18 +35,4 @@ export default defineConfig({
|
|||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
secure: false
|
||||
},
|
||||
'/help': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
secure: false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@ public class LccApplication {
|
|||
|
||||
Runtime runtime = Runtime.getRuntime();
|
||||
long usedMemory = (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024;
|
||||
logger.info("LCC Launcher (start) - Memory: {} used, {} total, {} free, {} max ", usedMemory, runtime.totalMemory() / 1024 / 1024, runtime.freeMemory() / 1024 / 1024, runtime.maxMemory() / 1024 / 1024);
|
||||
logger.info("LCC Start - Memory: {} used, {} total, {} free, {} max ", usedMemory, runtime.totalMemory() / 1024 / 1024, runtime.freeMemory() / 1024 / 1024, runtime.maxMemory() / 1024 / 1024);
|
||||
|
||||
SpringApplication.run(LccApplication.class, args);
|
||||
|
||||
usedMemory = (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024;
|
||||
logger.info("LCC Launcher (finished) - Memory: {} used, {} total, {} free, {} max ", usedMemory, runtime.totalMemory() / 1024 / 1024, runtime.freeMemory() / 1024 / 1024, runtime.maxMemory() / 1024 / 1024);
|
||||
logger.info("LCC End - Memory: {} used, {} total, {} free, {} max ", usedMemory, runtime.totalMemory() / 1024 / 1024, runtime.freeMemory() / 1024 / 1024, runtime.maxMemory() / 1024 / 1024);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,41 +1,33 @@
|
|||
package de.avatic.lcc.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.task.TaskExecutor;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
public class AsyncConfig {
|
||||
|
||||
@Bean(name = "calculationJobScheduler")
|
||||
public ThreadPoolTaskScheduler calculationJobScheduler(
|
||||
@Value("${calculation.job.processor.pool-size:1}") int poolSize,
|
||||
@Value("${calculation.job.processor.thread-name-prefix:calc-job-}") String threadNamePrefix) {
|
||||
|
||||
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||
scheduler.setPoolSize(poolSize);
|
||||
scheduler.setThreadNamePrefix(threadNamePrefix);
|
||||
scheduler.setWaitForTasksToCompleteOnShutdown(true);
|
||||
scheduler.setAwaitTerminationSeconds(60);
|
||||
scheduler.initialize();
|
||||
|
||||
return scheduler;
|
||||
@Bean(name = "calculationExecutor")
|
||||
public Executor taskExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(16);
|
||||
executor.setMaxPoolSize(32);
|
||||
executor.setQueueCapacity(500);
|
||||
executor.setThreadNamePrefix("calc-");
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
|
||||
@Bean(name = "customLookupExecutor")
|
||||
public Executor customLookupExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(2);
|
||||
executor.setCorePoolSize(16);
|
||||
executor.setMaxPoolSize(32);
|
||||
executor.setQueueCapacity(500);
|
||||
executor.setThreadNamePrefix("lookup-");
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
|
|||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
|
||||
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
|
|
@ -111,7 +111,7 @@ public class SecurityConfig {
|
|||
.exceptionHandling(ex -> ex
|
||||
.defaultAuthenticationEntryPointFor(
|
||||
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
|
||||
PathPatternRequestMatcher.withDefaults().matcher("/api/**")
|
||||
new AntPathRequestMatcher("/api/**")
|
||||
)
|
||||
)
|
||||
.csrf(csrf -> csrf
|
||||
|
|
|
|||
|
|
@ -18,5 +18,11 @@ public class ShutdownListener {
|
|||
Runtime runtime = Runtime.getRuntime();
|
||||
long usedMemory = (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024;
|
||||
log.info("Memory: {} used, {} total, {} free, {} max ", usedMemory, runtime.totalMemory() / 1024 / 1024, runtime.freeMemory() / 1024 / 1024, runtime.maxMemory() / 1024 / 1024);
|
||||
|
||||
|
||||
|
||||
log.error("Application shutdown. Context: {}, Thread: {}", event.getApplicationContext(), Thread.currentThread());
|
||||
log.error("Thread stack dump:");
|
||||
Thread.dumpStack();
|
||||
}
|
||||
}
|
||||
|
|
@ -133,7 +133,7 @@ public class GlobalExceptionHandler {
|
|||
public ResponseEntity<ErrorResponseDTO> handlePremiseValidationException(PremiseValidationError exception) {
|
||||
ErrorDTO error = new ErrorDTO(
|
||||
exception.getClass().getName(),
|
||||
"Almost there - just need to fix a few things",
|
||||
"Premiss validation error",
|
||||
exception.getMessage(),
|
||||
Arrays.asList(exception.getStackTrace())
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
package de.avatic.lcc.controller.calculation;
|
||||
|
||||
import de.avatic.lcc.dto.calculation.execution.CalculationProcessingOverviewDTO;
|
||||
import de.avatic.lcc.service.calculation.execution.CalculationJobProcessorManagementService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/dashboard")
|
||||
public class DashboardController {
|
||||
|
||||
private final CalculationJobProcessorManagementService calculationJobProcessorManagementService;
|
||||
|
||||
public DashboardController(CalculationJobProcessorManagementService calculationJobProcessorManagementService) {
|
||||
this.calculationJobProcessorManagementService = calculationJobProcessorManagementService;
|
||||
}
|
||||
|
||||
@GetMapping({"/", ""})
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
|
||||
public ResponseEntity<CalculationProcessingOverviewDTO> getDashboardData() {
|
||||
return ResponseEntity.ok(calculationJobProcessorManagementService.getCalculationOverview());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package de.avatic.lcc.controller.calculation;
|
||||
|
||||
|
||||
import de.avatic.lcc.dto.calculation.CalculationStatus;
|
||||
import de.avatic.lcc.dto.calculation.DestinationDTO;
|
||||
import de.avatic.lcc.dto.calculation.PremiseDTO;
|
||||
import de.avatic.lcc.dto.calculation.ResolvePremiseDTO;
|
||||
|
|
@ -8,21 +9,15 @@ import de.avatic.lcc.dto.calculation.create.CreatePremiseDTO;
|
|||
import de.avatic.lcc.dto.calculation.create.PremiseSearchResultDTO;
|
||||
import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO;
|
||||
import de.avatic.lcc.dto.calculation.edit.destination.DestinationCreateDTO;
|
||||
import de.avatic.lcc.dto.calculation.edit.destination.DestinationMassUpdateDTO;
|
||||
import de.avatic.lcc.dto.calculation.edit.destination.DestinationSetDTO;
|
||||
import de.avatic.lcc.dto.calculation.edit.destination.DestinationUpdateDTO;
|
||||
import de.avatic.lcc.dto.calculation.edit.masterData.MaterialUpdateDTO;
|
||||
import de.avatic.lcc.dto.calculation.edit.masterData.PackagingUpdateDTO;
|
||||
import de.avatic.lcc.dto.calculation.edit.masterData.PriceUpdateDTO;
|
||||
import de.avatic.lcc.dto.calculation.execution.CalculationProcessingStateRequestDTO;
|
||||
import de.avatic.lcc.dto.calculation.execution.CalculationProcessingStateResponseDTO;
|
||||
import de.avatic.lcc.dto.calculation.execution.CalculationStartRequestDTO;
|
||||
import de.avatic.lcc.dto.calculation.execution.CalculationStartResponseDTO;
|
||||
import de.avatic.lcc.service.access.DestinationService;
|
||||
import de.avatic.lcc.service.access.PremisesService;
|
||||
import de.avatic.lcc.service.calculation.PremiseCreationService;
|
||||
import de.avatic.lcc.service.calculation.PremiseSearchStringAnalyzerService;
|
||||
import de.avatic.lcc.service.calculation.execution.CalculationJobProcessorManagementService;
|
||||
import de.avatic.lcc.util.exception.badrequest.InvalidArgumentException;
|
||||
import de.avatic.lcc.util.exception.base.BadRequestException;
|
||||
import jakarta.validation.Valid;
|
||||
|
|
@ -34,6 +29,8 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
|||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
|
@ -49,14 +46,12 @@ public class PremiseController {
|
|||
private final PremisesService premisesServices;
|
||||
private final PremiseCreationService premiseCreationService;
|
||||
private final DestinationService destinationService;
|
||||
private final CalculationJobProcessorManagementService calculationJobProcessorManagementService;
|
||||
|
||||
public PremiseController(PremiseSearchStringAnalyzerService premiseSearchStringAnalyzerService, PremisesService premisesServices, PremiseCreationService premiseCreationService, DestinationService destinationService, CalculationJobProcessorManagementService calculationJobProcessorManagementService) {
|
||||
public PremiseController(PremiseSearchStringAnalyzerService premiseSearchStringAnalyzerService, PremisesService premisesServices, PremiseCreationService premiseCreationService, DestinationService destinationService) {
|
||||
this.premiseSearchStringAnalyzerService = premiseSearchStringAnalyzerService;
|
||||
this.premisesServices = premisesServices;
|
||||
this.premiseCreationService = premiseCreationService;
|
||||
this.destinationService = destinationService;
|
||||
this.calculationJobProcessorManagementService = calculationJobProcessorManagementService;
|
||||
}
|
||||
|
||||
@GetMapping({"/view", "/view/"})
|
||||
|
|
@ -83,13 +78,13 @@ public class PremiseController {
|
|||
public ResponseEntity<PremiseSearchResultDTO> findMaterialsAndSuppliers(@RequestParam String search) {
|
||||
|
||||
try {
|
||||
// String decodedValue = URLDecoder.decode(search, StandardCharsets.UTF_8);
|
||||
return ResponseEntity.ok(premiseSearchStringAnalyzerService.findMaterialAndSuppliers(search));
|
||||
} catch (Exception e) {
|
||||
throw new BadRequestException("Bad string encoding", "Unable to decode request", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@PostMapping({"/create", "/create/"})
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
|
||||
public ResponseEntity<List<PremiseDetailDTO>> createPremises(@RequestBody @Valid CreatePremiseDTO dto) {
|
||||
|
|
@ -135,22 +130,26 @@ public class PremiseController {
|
|||
@GetMapping({"/edit", "/edit/"})
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
|
||||
public ResponseEntity<List<PremiseDetailDTO>> getPremises(@RequestParam List<Integer> premissIds) {
|
||||
var premisses = premisesServices.getPremises(premissIds);
|
||||
return ResponseEntity.ok(premisses);
|
||||
return ResponseEntity.ok(premisesServices.getPremises(premissIds));
|
||||
}
|
||||
|
||||
@PutMapping({"/start", "/start/"})
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
|
||||
public ResponseEntity<CalculationStartResponseDTO> startCalculation(@RequestBody @Valid CalculationStartRequestDTO requestDTO) {
|
||||
var response = calculationJobProcessorManagementService.startCalculation(requestDTO);
|
||||
return ResponseEntity.ok(response);
|
||||
public ResponseEntity<Void> startCalculation(@RequestBody List<Integer> premiseIds) {
|
||||
premisesServices.startCalculation(premiseIds);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
|
||||
@GetMapping({"/status/", "/status"})
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION', 'BASIC')")
|
||||
public ResponseEntity<CalculationProcessingStateResponseDTO> getCalculationStatus(@RequestBody CalculationProcessingStateRequestDTO requestDTO) {
|
||||
return ResponseEntity.ok(calculationJobProcessorManagementService.getCalculationStatus(requestDTO));
|
||||
/**
|
||||
* Retrieves the current status of a specific calculation processing operation.
|
||||
*
|
||||
* @param id The unique identifier of the operation (processing_id) to check its status.
|
||||
* @return A ResponseEntity with the bulk processing status payload.
|
||||
*/
|
||||
@GetMapping({"/status/{processing_id}", "/status/{processing_id}/"})
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
|
||||
public ResponseEntity<CalculationStatus> getCalculationStatus(@PathVariable("processing_id") Integer id) {
|
||||
return ResponseEntity.ok(premisesServices.getCalculationStatus(id));
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -177,14 +176,14 @@ public class PremiseController {
|
|||
|
||||
@PostMapping({"/destination", "/destination/"})
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
|
||||
public ResponseEntity<Map<Integer, List<DestinationDTO>>> createDestination(@RequestBody @Valid DestinationCreateDTO destinationCreateDTO) {
|
||||
return ResponseEntity.ok(destinationService.massSetDestinations(destinationCreateDTO));
|
||||
public ResponseEntity<Map<Integer, DestinationDTO>> createDestination(@RequestBody @Valid DestinationCreateDTO destinationCreateDTO) {
|
||||
return ResponseEntity.ok(destinationService.createDestination(destinationCreateDTO));
|
||||
}
|
||||
|
||||
@PutMapping({"/destination", "/destination/"})
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
|
||||
public ResponseEntity<Map<Integer, List<DestinationDTO>>> setDestination(@RequestBody DestinationSetDTO destinationSetDTO) {
|
||||
return ResponseEntity.ok(destinationService.massSetDestinationProperties(destinationSetDTO));
|
||||
return ResponseEntity.ok(destinationService.setDestination(destinationSetDTO));
|
||||
}
|
||||
|
||||
@GetMapping({"/destination/{id}", "/destination/{id}/"})
|
||||
|
|
@ -201,13 +200,6 @@ public class PremiseController {
|
|||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PutMapping({"/destination/all", "/destination/all/"})
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
|
||||
public ResponseEntity<Void> updateAllDestination(@RequestBody @Valid DestinationMassUpdateDTO destinationUpdateDTO) {
|
||||
destinationUpdateDTO.getDestinations().forEach(destinationService::updateDestination);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@DeleteMapping({"/destination/{id}", "/destination/{id}/"})
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
|
||||
public ResponseEntity<Void> deleteDestination(@PathVariable Integer id) {
|
||||
|
|
@ -216,4 +208,5 @@ public class PremiseController {
|
|||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,8 @@ package de.avatic.lcc.controller.configuration;
|
|||
|
||||
import com.azure.core.annotation.BodyParam;
|
||||
import de.avatic.lcc.dto.configuration.apps.AppDTO;
|
||||
import de.avatic.lcc.dto.configuration.apps.AppExchangeDTO;
|
||||
import de.avatic.lcc.service.apps.AppsService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
|
@ -18,35 +16,21 @@ public class AppsController {
|
|||
private final AppsService appsService;
|
||||
|
||||
public AppsController(AppsService appsService) {
|
||||
|
||||
this.appsService = appsService;
|
||||
}
|
||||
|
||||
@GetMapping({"", "/"})
|
||||
@PreAuthorize("hasRole('SERVICE')")
|
||||
public ResponseEntity<List<AppDTO>> listApps() {
|
||||
return ResponseEntity.ok(appsService.listApps());
|
||||
}
|
||||
|
||||
@PostMapping({"", "/"})
|
||||
@PreAuthorize("hasRole('SERVICE')")
|
||||
public ResponseEntity<AppDTO> updateApp(@RequestBody AppDTO dto) {
|
||||
return ResponseEntity.ok(appsService.updateApp(dto));
|
||||
}
|
||||
|
||||
@GetMapping({"/export/{id}", "/export/{id}/"})
|
||||
@PreAuthorize("hasRole('SERVICE')")
|
||||
public ResponseEntity<AppExchangeDTO> exportApp(@PathVariable Integer id) {
|
||||
return ResponseEntity.ok(appsService.exportApp(id));
|
||||
}
|
||||
|
||||
@PostMapping({"/import/", "/import"})
|
||||
@PreAuthorize("hasRole('SERVICE')")
|
||||
public ResponseEntity<Boolean> importApp(@RequestBody AppExchangeDTO dto) {
|
||||
return ResponseEntity.ok(appsService.importApp(dto));
|
||||
}
|
||||
|
||||
@DeleteMapping({"/{id}", "/{id}/"})
|
||||
@PreAuthorize("hasRole('SERVICE')")
|
||||
public ResponseEntity<Void> deleteApp(@PathVariable Integer id) {
|
||||
appsService.deleteApp(id);
|
||||
return ResponseEntity.ok().build();
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
package de.avatic.lcc.controller.configuration;
|
||||
|
||||
import de.avatic.lcc.dto.error.CalculationJobDumpDTO;
|
||||
import de.avatic.lcc.repositories.error.DumpRepository;
|
||||
import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/dumps")
|
||||
public class CalculationDumpController {
|
||||
|
||||
private final DumpRepository dumpRepository;
|
||||
|
||||
public CalculationDumpController(DumpRepository dumpRepository) {
|
||||
this.dumpRepository = dumpRepository;
|
||||
}
|
||||
|
||||
@GetMapping({"/dump/{id}", "/dump/{id}/"})
|
||||
@PreAuthorize("hasRole('SERVICE')")
|
||||
public ResponseEntity<CalculationJobDumpDTO> getDump(@PathVariable Integer id) {
|
||||
return ResponseEntity.ok(dumpRepository.getDump(id));
|
||||
}
|
||||
|
||||
@GetMapping({"/dump/", "/dump"})
|
||||
@PreAuthorize("hasRole('SERVICE')")
|
||||
public ResponseEntity<List<CalculationJobDumpDTO>> listDumps(
|
||||
@RequestParam(defaultValue = "20") @Min(1) int limit,
|
||||
@RequestParam(defaultValue = "1") @Min(1) int page) {
|
||||
|
||||
var dump = dumpRepository.listDumps(new SearchQueryPagination(page, limit));
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header("X-Total-Count", String.valueOf(dump.getTotalElements()))
|
||||
.header("X-Page-Count", String.valueOf(dump.getTotalPages()))
|
||||
.header("X-Current-Page", String.valueOf(page))
|
||||
.body(dump.toList());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package de.avatic.lcc.controller.dev;
|
||||
|
||||
import de.avatic.lcc.config.filter.DevUserEmulationFilter;
|
||||
import de.avatic.lcc.dto.error.CalculationJobDumpDTO;
|
||||
import de.avatic.lcc.dto.users.UserDTO;
|
||||
import de.avatic.lcc.repositories.error.DumpRepository;
|
||||
import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
|
||||
|
|
@ -20,15 +21,36 @@ import java.util.List;
|
|||
@RequestMapping({"/api/dev", "/api/dev/"})
|
||||
public class DevController {
|
||||
|
||||
|
||||
private final DumpRepository dumpRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final UserTransformer userTransformer;
|
||||
|
||||
public DevController(UserRepository userRepository, UserTransformer userTransformer) {
|
||||
public DevController(DumpRepository dumpRepository, UserRepository userRepository, UserTransformer userTransformer) {
|
||||
|
||||
this.dumpRepository = dumpRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.userTransformer = userTransformer;
|
||||
}
|
||||
|
||||
@GetMapping({"/dump/{id}", "/dump/{id}/"})
|
||||
public ResponseEntity<CalculationJobDumpDTO> getDump(@PathVariable Integer id) {
|
||||
return ResponseEntity.ok(dumpRepository.getDump(id));
|
||||
}
|
||||
|
||||
@GetMapping({"/dump/", "/dump"})
|
||||
public ResponseEntity<List<CalculationJobDumpDTO>> listDumps(
|
||||
@RequestParam(defaultValue = "20") @Min(1) int limit,
|
||||
@RequestParam(defaultValue = "1") @Min(1) int page) {
|
||||
|
||||
var dump = dumpRepository.listDumps(new SearchQueryPagination(page, limit));
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header("X-Total-Count", String.valueOf(dump.getTotalElements()))
|
||||
.header("X-Page-Count", String.valueOf(dump.getTotalPages()))
|
||||
.header("X-Current-Page", String.valueOf(page))
|
||||
.body(dump.toList());
|
||||
}
|
||||
|
||||
@GetMapping({"/user"})
|
||||
public ResponseEntity<List<UserDTO>> listUser(@RequestParam(defaultValue = "20") @Min(1) int limit,
|
||||
@RequestParam(defaultValue = "1") @Min(1) int page) {
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
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;
|
||||
|
||||
}
|
||||
}
|
||||