Intermediate commit
|
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<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="SvgjsSvg1016" data-name="Ebene 1 Kopie" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 297.43 345.13">
|
<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>
|
<defs>
|
||||||
<style>
|
<style>
|
||||||
.cls-1 {
|
.cls-1 {
|
||||||
|
|
@ -9,10 +9,8 @@
|
||||||
fill: #002f54;
|
fill: #002f54;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</defs>
|
<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-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="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>
|
<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><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
|
</svg></svg></g></g></svg>
|
||||||
@media (prefers-color-scheme: dark) { :root { filter: none; } }
|
|
||||||
</style></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.6 KiB |
|
|
@ -29,6 +29,14 @@ export default {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
html.modal-open {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
margin-bottom: 3rem;
|
margin-bottom: 3rem;
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,24 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="checkbox-container">
|
<div class="checkbox-container">
|
||||||
<label class="checkbox-item" :class="{ disabled: disabled }" @change="setFilter">
|
<label class="checkbox-item" :class="{ disabled: disabled }">
|
||||||
<input
|
<input
|
||||||
@keydown.enter="$emit('enter', $event)"
|
@keydown.enter="$emit('enter', $event)"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="isChecked"
|
:checked="internalChecked"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:indeterminate.prop="isIndeterminate"
|
:indeterminate.prop="internalIndeterminate"
|
||||||
v-model="isChecked"
|
@change="handleChange"
|
||||||
ref="checkboxInput"
|
ref="checkboxInput"
|
||||||
>
|
>
|
||||||
<span class="checkmark" :class="{ indeterminate: isIndeterminate }"></span>
|
<span class="checkmark" :class="{ indeterminate: internalIndeterminate }"></span>
|
||||||
<span class="checkbox-label"><slot></slot></span>
|
<span class="checkbox-label"><slot></slot></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default{
|
export default {
|
||||||
emits:["checkbox-changed"],
|
emits: ["checkbox-changed", "enter"],
|
||||||
props: {
|
props: {
|
||||||
checked: {
|
checked: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|
@ -40,45 +40,49 @@ export default{
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
internalChecked: this.checked,
|
internalChecked: this.checked,
|
||||||
internalIndeterminate: this.indeterminate,
|
internalIndeterminate: this.indeterminate && !this.checked,
|
||||||
}
|
|
||||||
},
|
|
||||||
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: {
|
watch: {
|
||||||
checked(newVal) {
|
checked(newVal) {
|
||||||
this.internalChecked = newVal;
|
this.internalChecked = newVal;
|
||||||
this.updateIndeterminateState(this.internalIndeterminate);
|
// Wenn checked true ist, dann indeterminate deaktivieren
|
||||||
|
if (newVal) {
|
||||||
|
this.internalIndeterminate = false;
|
||||||
|
this.updateIndeterminateState(false);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
indeterminate(newVal) {
|
indeterminate(newVal) {
|
||||||
this.internalIndeterminate = newVal;
|
// Indeterminate nur setzen, wenn checked false ist
|
||||||
this.updateIndeterminateState(newVal);
|
this.internalIndeterminate = newVal && !this.internalChecked;
|
||||||
|
this.updateIndeterminateState(this.internalIndeterminate);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.updateIndeterminateState(this.isIndeterminate);
|
// Beim Mount: checked hat Priorität über indeterminate
|
||||||
|
if (this.internalChecked) {
|
||||||
|
this.internalIndeterminate = false;
|
||||||
|
}
|
||||||
|
this.updateIndeterminateState(this.internalIndeterminate);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
focus() {
|
focus() {
|
||||||
this.$refs.checkboxInput?.focus();
|
this.$refs.checkboxInput?.focus();
|
||||||
},
|
},
|
||||||
setFilter(event) {
|
handleChange(event) {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
this.isChecked = event.target.checked;
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
updateIndeterminateState(value) {
|
updateIndeterminateState(value) {
|
||||||
if (this.$refs.checkboxInput) {
|
if (this.$refs.checkboxInput) {
|
||||||
|
|
@ -88,8 +92,8 @@ export default{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style>
|
|
||||||
|
|
||||||
|
<style>
|
||||||
.checkbox-container {
|
.checkbox-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,12 @@
|
||||||
:style="modalAddStyle"
|
:style="modalAddStyle"
|
||||||
>
|
>
|
||||||
<div class="modal-container">
|
<div class="modal-container">
|
||||||
<box @click.stop class="modal-box">
|
<box
|
||||||
|
@click.stop
|
||||||
|
class="modal-box"
|
||||||
|
@mouseenter="onModalMouseEnter"
|
||||||
|
@mouseleave="onModalMouseLeave"
|
||||||
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</box>
|
</box>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -70,6 +75,11 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
preventScroll: null
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.isVisible) {
|
if (this.isVisible) {
|
||||||
this.handleOpen();
|
this.handleOpen();
|
||||||
|
|
@ -83,6 +93,20 @@ export default {
|
||||||
this.$emit('close');
|
this.$emit('close');
|
||||||
},
|
},
|
||||||
handleOpen() {
|
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(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.modalOverlay) {
|
if (this.$refs.modalOverlay) {
|
||||||
this.$refs.modalOverlay.focus();
|
this.$refs.modalOverlay.focus();
|
||||||
|
|
@ -90,6 +114,31 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleClose() {
|
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, page 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -100,8 +149,8 @@ export default {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
width: 100vw; /* Statt right: 0 */
|
||||||
bottom: 0;
|
height: 100vh; /* Statt bottom: 0 */
|
||||||
background: rgba(0, 0, 0, 0.5);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(4px);
|
||||||
-webkit-backdrop-filter: blur(4px);
|
-webkit-backdrop-filter: blur(4px);
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
class="sort-button"
|
class="sort-button"
|
||||||
:class="{ 'active': active }"
|
:class="{ 'active': active }"
|
||||||
>
|
>
|
||||||
<PhArrowCircleUp weight="fill"
|
<PhCaretUp weight="fill"
|
||||||
:size="24"
|
:size="16"
|
||||||
class="sort-icon"
|
class="sort-icon"
|
||||||
:class="{ 'rotate': direction === 'asc' }"
|
:class="{ 'rotate': direction === 'asc' }"
|
||||||
/>
|
/>
|
||||||
|
|
@ -13,11 +13,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {PhArrowCircleUp, PhFunnelSimple} from '@phosphor-icons/vue';
|
import {PhArrowCircleUp, PhCaretUp, PhFunnelSimple} from '@phosphor-icons/vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "SortButton",
|
name: "SortButton",
|
||||||
components: {
|
components: {
|
||||||
|
PhCaretUp,
|
||||||
PhArrowCircleUp,
|
PhArrowCircleUp,
|
||||||
PhFunnelSimple
|
PhFunnelSimple
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -138,17 +138,18 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-content {
|
.tab-content {
|
||||||
padding: 20px;
|
flex: 1;
|
||||||
background-color: white;
|
display: flex;
|
||||||
flex: 1; /* Take remaining space */
|
flex-direction: column;
|
||||||
min-height: 0; /* Allow shrinking */
|
min-height: 0;
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-pane {
|
.tab-pane {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(20px);
|
transform: translateY(20px);
|
||||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
<div class="bulk-edit-row__cell-container">
|
<div class="bulk-edit-row__cell-container">
|
||||||
<div
|
<div
|
||||||
class="bulk-edit-row__cell bulk-edit-row__cell--status bulk-edit-row__cell--clickable bulk-edit-row__cell--filterable"
|
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')"
|
@click="action($event,'material')"
|
||||||
@mousedown="handleMouseDown">
|
@mousedown="handleMouseDown">
|
||||||
<div class="bulk-edit-row__data">
|
<div class="bulk-edit-row__data">
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bulk-edit-row__cell-container">
|
<div class="bulk-edit-row__cell-container">
|
||||||
<div class="bulk-edit-row__cell bulk-edit-row__cell--status bulk-edit-row__cell--clickable"
|
<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')">
|
@click="action($event,'price')">
|
||||||
<div class="bulk-edit-row__data">
|
<div class="bulk-edit-row__data">
|
||||||
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
||||||
|
|
@ -59,7 +59,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bulk-edit-row__cell-container">
|
<div class="bulk-edit-row__cell-container">
|
||||||
<div class="bulk-edit-row__cell bulk-edit-row__cell--status bulk-edit-row__cell--clickable"
|
<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')">
|
@click="action($event,'packaging')">
|
||||||
<div class="bulk-edit-row__data">
|
<div class="bulk-edit-row__data">
|
||||||
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
||||||
|
|
@ -111,7 +111,7 @@
|
||||||
@click="action($event,'amount')">
|
@click="action($event,'amount')">
|
||||||
<div class="bulk-edit-row__data bulk-edit-row__data--destinations">
|
<div class="bulk-edit-row__data bulk-edit-row__data--destinations">
|
||||||
<div class="bulk-edit-row__dest-line"
|
<div class="bulk-edit-row__dest-line"
|
||||||
v-for="(destination, index) in premise.destinations.slice(0, 3)"
|
v-for="(destination, index) in destinations.slice(0, 3)"
|
||||||
:key="index">
|
:key="index">
|
||||||
<div>
|
<div>
|
||||||
<ph-stack size="16"/>
|
<ph-stack size="16"/>
|
||||||
|
|
@ -121,7 +121,7 @@
|
||||||
<basic-badge size="compact" variant="secondary">{{ toDestination(destination) }}</basic-badge>
|
<basic-badge size="compact" variant="secondary">{{ toDestination(destination) }}</basic-badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bulk-edit-row__dest-line" v-if="premise.destinations.length > 3">
|
<div class="bulk-edit-row__dest-line" v-if="destinations.length > 3">
|
||||||
<div></div>
|
<div></div>
|
||||||
<div> more ...</div>
|
<div> more ...</div>
|
||||||
<div></div>
|
<div></div>
|
||||||
|
|
@ -129,9 +129,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Expanded destinations overlay -->
|
<!-- Expanded destinations overlay -->
|
||||||
<div class="bulk-edit-row__destinations-expanded" v-if="premise.destinations.length > 3">
|
<div class="bulk-edit-row__destinations-expanded" v-if="destinations.length > 3">
|
||||||
<div class="bulk-edit-row__dest-line"
|
<div class="bulk-edit-row__dest-line"
|
||||||
v-for="(destination, index) in premise.destinations"
|
v-for="(destination, index) in destinations"
|
||||||
:key="index">
|
:key="index">
|
||||||
<div>
|
<div>
|
||||||
<ph-stack size="16"/>
|
<ph-stack size="16"/>
|
||||||
|
|
@ -159,7 +159,7 @@
|
||||||
@click="action($event,'routes')">
|
@click="action($event,'routes')">
|
||||||
<div class="bulk-edit-row__data">
|
<div class="bulk-edit-row__data">
|
||||||
<div class="bulk-edit-row__route-line"
|
<div class="bulk-edit-row__route-line"
|
||||||
v-for="(destination, index) in premise.destinations.slice(0, 3)"
|
v-for="(destination, index) in destinations.slice(0, 3)"
|
||||||
:key="index">
|
:key="index">
|
||||||
<div>
|
<div>
|
||||||
<component :is="toRouteIcon(destination)" size="16"></component>
|
<component :is="toRouteIcon(destination)" size="16"></component>
|
||||||
|
|
@ -169,7 +169,7 @@
|
||||||
<basic-badge size="compact" variant="secondary">{{ toDestination(destination) }}</basic-badge>
|
<basic-badge size="compact" variant="secondary">{{ toDestination(destination) }}</basic-badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bulk-edit-row__route-line" v-if="premise.destinations.length > 3">
|
<div class="bulk-edit-row__route-line" v-if="destinations.length > 3">
|
||||||
<div></div>
|
<div></div>
|
||||||
<div> more ...</div>
|
<div> more ...</div>
|
||||||
<div></div>
|
<div></div>
|
||||||
|
|
@ -177,9 +177,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Expanded destinations overlay -->
|
<!-- Expanded destinations overlay -->
|
||||||
<div class="bulk-edit-row__destinations-expanded" v-if="premise.destinations.length > 3">
|
<div class="bulk-edit-row__destinations-expanded" v-if="destinations.length > 3">
|
||||||
<div class="bulk-edit-row__route-line"
|
<div class="bulk-edit-row__route-line"
|
||||||
v-for="(destination, index) in premise.destinations"
|
v-for="(destination, index) in destinations"
|
||||||
:key="index">
|
:key="index">
|
||||||
<div>
|
<div>
|
||||||
<component :is="toRouteIcon(destination)" size="16"></component>
|
<component :is="toRouteIcon(destination)" size="16"></component>
|
||||||
|
|
@ -193,9 +193,9 @@
|
||||||
|
|
||||||
<div class="bulk-edit-row__status">
|
<div class="bulk-edit-row__status">
|
||||||
<transition name="badge-transition" mode="out-in">
|
<transition name="badge-transition" mode="out-in">
|
||||||
<circle-badge v-if="destinationCheck && showRouteCheck" :key="'check-route-' + id" variant="primary"
|
<circle-badge v-if="routeCheck && showRouteCheck" :key="'check-route-' + id" variant="primary"
|
||||||
icon="check" class="badge--check"></circle-badge>
|
icon="check" class="badge--check"></circle-badge>
|
||||||
<circle-badge v-else-if="!destinationCheck" :key="'error-route-' + id" variant="exception" icon="exclamation-mark"></circle-badge>
|
<circle-badge v-else-if="!routeCheck" :key="'error-route-' + id" variant="exception" icon="exclamation-mark"></circle-badge>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -228,6 +228,7 @@ import {
|
||||||
} from "@phosphor-icons/vue";
|
} from "@phosphor-icons/vue";
|
||||||
import {UrlSafeBase64} from "@/common.js";
|
import {UrlSafeBase64} from "@/common.js";
|
||||||
import CircleBadge from "@/components/UI/CircleBadge.vue";
|
import CircleBadge from "@/components/UI/CircleBadge.vue";
|
||||||
|
import {useDestinationEditStore} from "@/store/destinationEdit.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "BulkEditRow",
|
name: "BulkEditRow",
|
||||||
|
|
@ -260,11 +261,6 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// Track previous states to detect error->ok transitions
|
|
||||||
prevMaterialCheck: null,
|
|
||||||
prevPriceCheck: null,
|
|
||||||
prevPackagingCheck: null,
|
|
||||||
prevDestinationCheck: null,
|
|
||||||
// Flags to show check badges only on transition
|
// Flags to show check badges only on transition
|
||||||
showMaterialCheck: false,
|
showMaterialCheck: false,
|
||||||
showPriceCheck: false,
|
showPriceCheck: false,
|
||||||
|
|
@ -273,6 +269,8 @@ export default {
|
||||||
showRouteCheck: false,
|
showRouteCheck: false,
|
||||||
// Flag to track if component has been initialized
|
// Flag to track if component has been initialized
|
||||||
isInitialized: false,
|
isInitialized: false,
|
||||||
|
// Store the initial state to prevent false triggers on mount
|
||||||
|
initialCheckStates: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -286,57 +284,110 @@ export default {
|
||||||
return this.premise.handling_unit;
|
return this.premise.handling_unit;
|
||||||
},
|
},
|
||||||
packagingCheck() {
|
packagingCheck() {
|
||||||
return this.hu?.length != null && this.hu?.width != null && this.hu?.height != null && this.hu?.weight != null && this.hu?.content_unit_count;
|
return this.hu?.length != null
|
||||||
|
&& this.hu?.width != null
|
||||||
|
&& this.hu?.height != null
|
||||||
|
&& this.hu?.weight != null
|
||||||
|
&& this.hu?.content_unit_count != null;
|
||||||
},
|
},
|
||||||
destinationCheck() {
|
destinationCheck() {
|
||||||
|
|
||||||
if (((this.premise?.destinations ?? null) === null) || this.premise?.destinations.length === 0)
|
if (((this.destinations ?? null) === null) || this.destinations.length === 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return !this.premise?.destinations?.some(d => d.annual_amount == null);
|
return !this.destinations?.some(d => d.annual_amount == null);
|
||||||
|
|
||||||
},
|
},
|
||||||
|
routeCheck() {
|
||||||
|
|
||||||
|
if (((this.destinations ?? null) === null) || this.destinations.length === 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return this.destinations?.every(d => d.routes?.some((route) => route.is_selected));
|
||||||
|
},
|
||||||
isSelected() {
|
isSelected() {
|
||||||
return this.premiseEditStore.isChecked(this.id);
|
return this.premiseEditStore.isChecked(this.id);
|
||||||
},
|
},
|
||||||
...mapStores(usePremiseEditStore),
|
destinations() {
|
||||||
|
return this.destinationEditStore.getByPremiseId(this.id) ?? [];
|
||||||
|
},
|
||||||
|
...mapStores(usePremiseEditStore, useDestinationEditStore),
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
materialCheck(newVal, oldVal) {
|
materialCheck(newVal, oldVal) {
|
||||||
if (this.isInitialized && oldVal === false && newVal === true) {
|
if (this.isInitialized
|
||||||
|
&& oldVal === false
|
||||||
|
&& newVal === true
|
||||||
|
&& this.initialCheckStates?.material !== true) {
|
||||||
this.showMaterialCheck = true;
|
this.showMaterialCheck = true;
|
||||||
|
// Reset initial state after first valid transition
|
||||||
|
if (this.initialCheckStates) {
|
||||||
|
this.initialCheckStates.material = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
priceCheck(newVal, oldVal) {
|
priceCheck(newVal, oldVal) {
|
||||||
if (this.isInitialized && oldVal === false && newVal === true) {
|
if (this.isInitialized
|
||||||
|
&& oldVal === false
|
||||||
|
&& newVal === true
|
||||||
|
&& this.initialCheckStates?.price !== true) {
|
||||||
this.showPriceCheck = true;
|
this.showPriceCheck = true;
|
||||||
|
if (this.initialCheckStates) {
|
||||||
|
this.initialCheckStates.price = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
packagingCheck(newVal, oldVal) {
|
packagingCheck(newVal, oldVal) {
|
||||||
if (this.isInitialized && oldVal === false && newVal === true) {
|
if (this.isInitialized
|
||||||
|
&& oldVal === false
|
||||||
|
&& newVal === true
|
||||||
|
&& this.initialCheckStates?.packaging !== true) {
|
||||||
this.showPackagingCheck = true;
|
this.showPackagingCheck = true;
|
||||||
|
if (this.initialCheckStates) {
|
||||||
|
this.initialCheckStates.packaging = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
destinationCheck(newVal, oldVal) {
|
destinationCheck(newVal, oldVal) {
|
||||||
if (this.isInitialized && oldVal === false && newVal === true) {
|
if (this.isInitialized
|
||||||
|
&& oldVal === false
|
||||||
|
&& newVal === true
|
||||||
|
&& this.initialCheckStates?.destination !== true) {
|
||||||
this.showDestinationCheck = true;
|
this.showDestinationCheck = true;
|
||||||
|
if (this.initialCheckStates) {
|
||||||
|
this.initialCheckStates.destination = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
routeCheck(newVal, oldVal) {
|
||||||
|
if (this.isInitialized
|
||||||
|
&& oldVal === false
|
||||||
|
&& newVal === true
|
||||||
|
&& this.initialCheckStates?.route !== true) {
|
||||||
this.showRouteCheck = true;
|
this.showRouteCheck = true;
|
||||||
|
if (this.initialCheckStates) {
|
||||||
|
this.initialCheckStates.route = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
// Initialize previous states after first render
|
// Capture initial states BEFORE setting isInitialized
|
||||||
// Use nextTick to ensure computed properties are evaluated
|
// 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.$nextTick(() => {
|
||||||
this.prevMaterialCheck = this.materialCheck;
|
|
||||||
this.prevPriceCheck = this.priceCheck;
|
|
||||||
this.prevPackagingCheck = this.packagingCheck;
|
|
||||||
this.prevDestinationCheck = this.destinationCheck;
|
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toDestination(destination, limit = 10) {
|
toDestination(destination, limit = 15) {
|
||||||
return this.toNode(destination.destination_node, limit);
|
return this.toNode(destination.destination_node, limit);
|
||||||
},
|
},
|
||||||
toNode(node, limit = 5) {
|
toNode(node, limit = 5) {
|
||||||
|
|
|
||||||
|
|
@ -98,8 +98,6 @@ export default {
|
||||||
},
|
},
|
||||||
editDestination(id) {
|
editDestination(id) {
|
||||||
|
|
||||||
logger.log(id);
|
|
||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
const destination = this.premiseSingleEditStore.getDestinationById(id);
|
const destination = this.premiseSingleEditStore.getDestinationById(id);
|
||||||
logger.log(destination);
|
logger.log(destination);
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ export default {
|
||||||
return this.route.is_fastest;
|
return this.route.is_fastest;
|
||||||
},
|
},
|
||||||
routeElements() {
|
routeElements() {
|
||||||
const routeElem = this.route.transit_nodes.map(n => n.external_mapping_id);
|
const routeElem = this.route.transit_nodes.map(n => n.external_mapping_id.replace("_", " "));
|
||||||
return routeElem;
|
return routeElem;
|
||||||
},
|
},
|
||||||
isSea() {
|
isSea() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
<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="updateOverallCheck($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 && selectedCount < totalCount;
|
||||||
|
|
||||||
|
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}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateOverallCheck(newValue, id) {
|
||||||
|
this.destPool.find(d => d.id === id).overallCheck = newValue;
|
||||||
|
this.destMatrix.forEach(r => r.destinations.find(d => d.id === id).selected = newValue);
|
||||||
|
},
|
||||||
|
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>
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
<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,13 +0,0 @@
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: "DestinationMassCreate"
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,14 +1,104 @@
|
||||||
<script>
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "DestinationMassEdit"
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div class="destination-edit-container">
|
||||||
|
<tab-container :default-tab="defaultTab" :tabs="tabsConfig" class="tab-container">
|
||||||
|
</tab-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
</template>
|
</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";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "DestinationMassEdit",
|
||||||
|
components: {BasicButton, TabContainer},
|
||||||
|
props: {
|
||||||
|
premiseIds: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentTab: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
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},
|
||||||
|
matchType: 'amount'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Handling & Repackaging',
|
||||||
|
component: markRaw(DestinationMassHandlingCost),
|
||||||
|
props: {premiseIds: this.premiseIds},
|
||||||
|
matchType: 'handling'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Routes',
|
||||||
|
component: markRaw(DestinationMassRoute),
|
||||||
|
props: {premiseIds: this.premiseIds},
|
||||||
|
matchType: 'routes'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<style scoped>
|
<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>
|
</style>
|
||||||
|
|
@ -1,15 +1,446 @@
|
||||||
<script lang="ts">
|
<template>
|
||||||
import {defineComponent} from 'vue'
|
<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>
|
||||||
|
</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>
|
||||||
|
|
||||||
export default defineComponent({
|
<script>
|
||||||
name: "DestinationMassHandlingCost"
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
handlingCostMatrix: null,
|
||||||
|
handlingCostActive: false,
|
||||||
|
overallDisposalCostValue: null,
|
||||||
|
overallRepackagingCostValue: null,
|
||||||
|
overallHandlingCostValue: null,
|
||||||
|
overallCheck: false,
|
||||||
|
overallIndeterminate: false,
|
||||||
|
isCtrlPressed: false,
|
||||||
|
isShiftPressed: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapStores(useDestinationEditStore, usePremiseEditStore),
|
||||||
|
rows() {
|
||||||
|
return this.handlingCostMatrix;
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.buildMatrix();
|
||||||
|
},
|
||||||
|
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, 0);
|
||||||
|
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.handlingCostMatrix.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.handlingCostMatrix
|
||||||
|
.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.handlingCostMatrix.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)}`;
|
||||||
|
|
||||||
|
},
|
||||||
|
buildMatrix() {
|
||||||
|
this.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) {
|
||||||
|
this.handlingCostMatrix.push({
|
||||||
|
id: premise.id,
|
||||||
|
material: premise.material.part_number,
|
||||||
|
supplier: this.toNode(premise.supplier, 15),
|
||||||
|
supplierId: premise.supplier.id,
|
||||||
|
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);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
<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: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dest-mass-handling-table-header-costs {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
width: 25rem;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -0,0 +1,266 @@
|
||||||
|
<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">
|
||||||
|
<ph-factory size="24"/>
|
||||||
|
{{ 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="row.handling_costs"
|
||||||
|
@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="row.repackaging_costs"
|
||||||
|
@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="row.disposal_costs"
|
||||||
|
@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";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "DestinationMassHandlingCostRow",
|
||||||
|
components: {PhMapPin, PhFactory, Checkbox},
|
||||||
|
emits: ['action', 'update-selected'],
|
||||||
|
props: {
|
||||||
|
row: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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,15 +1,414 @@
|
||||||
<script lang="ts">
|
<template>
|
||||||
import {defineComponent} from 'vue'
|
<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></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>
|
||||||
|
|
||||||
export default defineComponent({
|
<script>
|
||||||
name: "DestinationMassQuantity"
|
|
||||||
})
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
destPool: null,
|
||||||
|
destMatrix: null,
|
||||||
|
overallCheck: false,
|
||||||
|
overallIndeterminate: false,
|
||||||
|
isCtrlPressed: false,
|
||||||
|
isShiftPressed: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapStores(useDestinationEditStore, usePremiseEditStore),
|
||||||
|
rows() {
|
||||||
|
return this.destMatrix;
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// watch: {
|
||||||
|
// someChecked(newVal, oldVal) {
|
||||||
|
// if(newVal === true && oldVal === false) {
|
||||||
|
// //reset overall inputs
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
created() {
|
||||||
|
this.buildMatrix();
|
||||||
|
},
|
||||||
|
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.destMatrix.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.destMatrix
|
||||||
|
.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.destMatrix.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)}`;
|
||||||
|
|
||||||
|
},
|
||||||
|
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());
|
||||||
|
|
||||||
|
// destMatrix aufbauen
|
||||||
|
this.destMatrix = 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
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
<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: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dest-mass-quantity-table-header-dest {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
width: 15rem;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -0,0 +1,216 @@
|
||||||
|
<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: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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,14 +1,192 @@
|
||||||
<script lang="ts">
|
<template>
|
||||||
import {defineComponent} from 'vue'
|
<div class="dest-mass-route-container">
|
||||||
|
<div class="dest-mass-route-table-wrapper">
|
||||||
|
<div class="dest-mass-route-table-header">
|
||||||
|
<div class="dest-mass-route-table-header-checkbox">
|
||||||
|
<checkbox @checkbox-changed="updateCheckBoxes" :checked="overallCheck"
|
||||||
|
:indeterminate="overallIndeterminate"></checkbox>
|
||||||
|
</div>
|
||||||
|
<div class="dest-mass-route-table-header-supplier">Supplier</div>
|
||||||
|
<div class="dest-mass-route-table-header-dest"
|
||||||
|
:key="`${dest.id}`"
|
||||||
|
v-for="dest in destPool">
|
||||||
|
<div>{{ toNode(dest.destination_node, 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-route-table">
|
||||||
|
|
||||||
export default defineComponent({
|
</div>
|
||||||
name: "DestinationMassRoute"
|
</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";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "DestinationMassRoute",
|
||||||
|
components: {IconButton, Checkbox, DestinationMassQuantityRow},
|
||||||
|
props: {
|
||||||
|
premiseIds: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapStores(useDestinationEditStore, usePremiseEditStore)
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
destPool: null,
|
||||||
|
destMatrix: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.buildMatrix();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
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)}`;
|
||||||
|
|
||||||
|
},
|
||||||
|
buildMatrix() {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.destPool = Array.from(destMap.values());
|
||||||
|
|
||||||
|
// destMatrix aufbauen
|
||||||
|
const premiseMap = new Map();
|
||||||
|
|
||||||
|
this.premiseIds.forEach(pId => {
|
||||||
|
const premise = this.premiseEditStore.getById(pId);
|
||||||
|
const destinations = this.destinationEditStore.getByPremiseId(pId);
|
||||||
|
|
||||||
|
if (!premiseMap.has(premise.supplier.id)) {
|
||||||
|
premiseMap.set(premise.supplier.id, {
|
||||||
|
ids: [],
|
||||||
|
supplierNodeId: premise.supplier.id,
|
||||||
|
supplier: premise.supplier.name,
|
||||||
|
destinations: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = premiseMap.get(premise.supplier.id);
|
||||||
|
|
||||||
|
if (row) {
|
||||||
|
row.ids.push(premise.id);
|
||||||
|
this.addDestinationsToRow(row.destinations, destinations)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
this.destMatrix = [];
|
||||||
|
// .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);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// console.log("prem", toRaw( premise), destRaw)
|
||||||
|
//
|
||||||
|
// return {
|
||||||
|
// // id: premise.id,
|
||||||
|
// // 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
|
||||||
|
// };
|
||||||
|
// });
|
||||||
|
|
||||||
|
},
|
||||||
|
addDestinationsToRow(rowDestinations, premiseDestinations) {
|
||||||
|
premiseDestinations.forEach(premD => {
|
||||||
|
|
||||||
|
let existingDest = rowDestinations.find(rowD => rowD.destinationNodeId === premD.destination_node.id) ?? null;
|
||||||
|
|
||||||
|
/* create destination, if it does not exist for supplier. */
|
||||||
|
if (!existingDest) {
|
||||||
|
|
||||||
|
existingDest = {
|
||||||
|
ids: [],
|
||||||
|
destinationNodeId: premD.destination_node.id,
|
||||||
|
destinationName: premD.destination_node.name,
|
||||||
|
routes: this.buildRoutes(premD.routes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// premdD.routes.push
|
||||||
|
rowDestinations.push(existingDest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* add orig Destination id to destination */
|
||||||
|
existingDest.ids.push(premD.id);
|
||||||
|
|
||||||
|
/* add route ids to routes */
|
||||||
|
this.addRoutesToDestination(existingDest.routes, premD.routes)
|
||||||
|
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addRoutesToDestination(rowRoutes, premiseRoutes) {
|
||||||
|
// premiseRoutes
|
||||||
|
|
||||||
|
},
|
||||||
|
buildRoutes(routes) {
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<h2 class="page-header">Mass edit calculation</h2>
|
<h2 class="page-header">Mass edit calculation</h2>
|
||||||
<div class="header-controls">
|
<div class="header-controls">
|
||||||
<basic-button :show-icon="false"
|
<basic-button :show-icon="true"
|
||||||
:disabled="disableButtons"
|
:disabled="disableButtons"
|
||||||
variant="secondary"
|
icon="MapPin" variant="primary"
|
||||||
@click="close"
|
@click="destMgmt"
|
||||||
>Close
|
>Destination manager
|
||||||
</basic-button>
|
</basic-button>
|
||||||
<basic-button :show-icon="true"
|
<basic-button :show-icon="true"
|
||||||
:disabled="disableButtons"
|
:disabled="disableButtons"
|
||||||
|
|
@ -16,6 +16,12 @@
|
||||||
@click="calculate"
|
@click="calculate"
|
||||||
>Calculate & close
|
>Calculate & close
|
||||||
</basic-button>
|
</basic-button>
|
||||||
|
<basic-button :show-icon="false"
|
||||||
|
:disabled="disableButtons"
|
||||||
|
variant="secondary"
|
||||||
|
@click="close"
|
||||||
|
>Close
|
||||||
|
</basic-button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -24,17 +30,21 @@
|
||||||
<div class="edit-calculation-list-header">
|
<div class="edit-calculation-list-header">
|
||||||
<div>
|
<div>
|
||||||
<checkbox @checkbox-changed="updateCheckBoxes" :checked="overallCheck"
|
<checkbox @checkbox-changed="updateCheckBoxes" :checked="overallCheck"
|
||||||
:indeterminate="overallIndeterminate"></checkbox>
|
:indeterminate="overallIndeterminate" :disabled="!showData"></checkbox>
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-calculation-list-header-cell">Material
|
<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'"
|
<sort-button :active="premiseEditStore.activeSort === 'material'"
|
||||||
:direction="premiseEditStore.directionSort('material')" @click="premiseEditStore.sort('material')"/>
|
:direction="premiseEditStore.directionSort('material')"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-calculation-list-header-cell">Price</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">Packaging</div>
|
||||||
<div class="edit-calculation-list-header-cell">Supplier
|
<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'"
|
<sort-button :active="premiseEditStore.activeSort === 'supplier'"
|
||||||
:direction="premiseEditStore.directionSort('supplier')" @click="premiseEditStore.sort('supplier')"/>
|
:direction="premiseEditStore.directionSort('supplier')"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-calculation-list-header-cell">Annual Quantity</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">Routes</div>
|
||||||
|
|
@ -78,10 +88,18 @@
|
||||||
:select-count="selectCount"></mass-edit-dialog>
|
: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="modalShow">
|
||||||
<div class="modal-content-container">
|
<div class="modal-content-container">
|
||||||
|
<h3 class="sub-header">{{ modalTitle }}</h3>
|
||||||
<component
|
<component
|
||||||
:is="modalComponentType"
|
:is="modalComponentType"
|
||||||
|
ref="modalComponent"
|
||||||
|
|
||||||
|
|
||||||
v-model:partNumber="modalProps.partNumber"
|
v-model:partNumber="modalProps.partNumber"
|
||||||
v-model:hsCode="modalProps.hsCode"
|
v-model:hsCode="modalProps.hsCode"
|
||||||
v-model:tariffRate="modalProps.tariffRate"
|
v-model:tariffRate="modalProps.tariffRate"
|
||||||
|
|
@ -104,6 +122,9 @@
|
||||||
|
|
||||||
v-model:hideDescription="modalProps.hideDescription"
|
v-model:hideDescription="modalProps.hideDescription"
|
||||||
|
|
||||||
|
:type="modalType"
|
||||||
|
:premiseIds="editIds"
|
||||||
|
|
||||||
:fromMassEdit="true"
|
:fromMassEdit="true"
|
||||||
:countryId=null
|
:countryId=null
|
||||||
:responsive="false"
|
:responsive="false"
|
||||||
|
|
@ -147,18 +168,25 @@ import PackagingEdit from "@/components/layout/edit/PackagingEdit.vue";
|
||||||
import {useNotificationStore} from "@/store/notification.js";
|
import {useNotificationStore} from "@/store/notification.js";
|
||||||
import {useDestinationEditStore} from "@/store/destinationEdit.js";
|
import {useDestinationEditStore} from "@/store/destinationEdit.js";
|
||||||
import SortButton from "@/components/UI/SortButton.vue";
|
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";
|
||||||
|
|
||||||
|
|
||||||
const COMPONENT_TYPES = {
|
const COMPONENT_TYPES = {
|
||||||
price: PriceEdit,
|
price: PriceEdit,
|
||||||
material: MaterialEdit,
|
material: MaterialEdit,
|
||||||
packaging: PackagingEdit,
|
packaging: PackagingEdit,
|
||||||
destinations: null,
|
destinations: DestMassCreate,
|
||||||
|
routes: DestinationMassEdit,
|
||||||
|
amount: DestinationMassEdit
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "MassEdit",
|
name: "MassEdit",
|
||||||
components: {
|
components: {
|
||||||
|
ModalDialog,
|
||||||
SortButton,
|
SortButton,
|
||||||
Modal,
|
Modal,
|
||||||
MassEditDialog,
|
MassEditDialog,
|
||||||
|
|
@ -169,6 +197,26 @@ export default {
|
||||||
BulkEditRow,
|
BulkEditRow,
|
||||||
BasicButton
|
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. Calculating ...",
|
||||||
|
showCalculationModal: false,
|
||||||
|
isInitialLoad: true,
|
||||||
|
modalDialogShow: false,
|
||||||
|
modalStash: null,
|
||||||
|
denyText: 'No'
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(usePremiseEditStore, useNotificationStore, useDestinationEditStore),
|
...mapStores(usePremiseEditStore, useNotificationStore, useDestinationEditStore),
|
||||||
disableButtons() {
|
disableButtons() {
|
||||||
|
|
@ -215,7 +263,7 @@ export default {
|
||||||
return this.modalType ? COMPONENT_TYPES[this.modalType] : null;
|
return this.modalType ? COMPONENT_TYPES[this.modalType] : null;
|
||||||
},
|
},
|
||||||
showProcessingModal() {
|
showProcessingModal() {
|
||||||
return this.premiseEditStore.showProcessingModal || this.showCalculationModal;
|
return this.premiseEditStore.showProcessingModal || this.destinationEditStore.showProcessingModal ;
|
||||||
},
|
},
|
||||||
shownProcessingMessage() {
|
shownProcessingMessage() {
|
||||||
return this.processingMessage;
|
return this.processingMessage;
|
||||||
|
|
@ -237,30 +285,11 @@ export default {
|
||||||
this.destinationEditStore.setupDestinations(premisses);
|
this.destinationEditStore.setupDestinations(premisses);
|
||||||
|
|
||||||
},
|
},
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
ids: [],
|
|
||||||
isCtrlPressed: false,
|
|
||||||
isShiftPressed: false,
|
|
||||||
overallCheck: false,
|
|
||||||
overallIndeterminate: false,
|
|
||||||
bulkQuery: null,
|
|
||||||
modalType: null,
|
|
||||||
modalProps: null,
|
|
||||||
editIds: null,
|
|
||||||
dataSourceId: null,
|
|
||||||
processingMessage: "Please wait. Calculating ...",
|
|
||||||
showCalculationModal: false,
|
|
||||||
isInitialLoad: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
mounted() {
|
||||||
console.log("add listener")
|
|
||||||
window.addEventListener('keydown', this.handleKeyDown);
|
window.addEventListener('keydown', this.handleKeyDown);
|
||||||
window.addEventListener('keyup', this.handleKeyUp);
|
window.addEventListener('keyup', this.handleKeyUp);
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
console.log("remove listener")
|
|
||||||
window.removeEventListener('keydown', this.handleKeyDown);
|
window.removeEventListener('keydown', this.handleKeyDown);
|
||||||
window.removeEventListener('keyup', this.handleKeyUp);
|
window.removeEventListener('keyup', this.handleKeyUp);
|
||||||
},
|
},
|
||||||
|
|
@ -299,6 +328,12 @@ export default {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
destMgmt() {
|
||||||
|
this.fillData('destinations');
|
||||||
|
this.editIds = null;
|
||||||
|
this.modalTitle = 'Destination Manager'
|
||||||
|
this.modalType = 'destinations';
|
||||||
|
},
|
||||||
async calculate() {
|
async calculate() {
|
||||||
this.showCalculationModal = true;
|
this.showCalculationModal = true;
|
||||||
const error = await this.premiseEditStore.startCalculation();
|
const error = await this.premiseEditStore.startCalculation();
|
||||||
|
|
@ -345,7 +380,7 @@ export default {
|
||||||
if (actions.length === 1) {
|
if (actions.length === 1) {
|
||||||
const massEdit = 0 !== this.selectCount;
|
const massEdit = 0 !== this.selectCount;
|
||||||
this.openModal(data.action, massEdit ? this.premiseEditStore.getSelectedPremiseIds : [data.id], data.id, massEdit);
|
this.openModal(data.action, massEdit ? this.premiseEditStore.getSelectedPremiseIds : [data.id], data.id, massEdit);
|
||||||
} else if (actions.length === 2) {
|
} else if (actions.length === 2) { /* ctrl or ctrl + shift */
|
||||||
this.premiseEditStore.setBy(actions[0], actions[1], data.id);
|
this.premiseEditStore.setBy(actions[0], actions[1], data.id);
|
||||||
this.updateOverallCheckBox();
|
this.updateOverallCheckBox();
|
||||||
}
|
}
|
||||||
|
|
@ -353,37 +388,90 @@ export default {
|
||||||
|
|
||||||
/* modal handling */
|
/* modal handling */
|
||||||
|
|
||||||
openModal(type, ids, dataSource = -1, massEdit = true) {
|
modalDialogClick(action) {
|
||||||
|
|
||||||
if (type !== 'amount' && type !== 'route')
|
|
||||||
this.fillData(type, dataSource, massEdit)
|
|
||||||
else {
|
|
||||||
//TODO new destination handling
|
|
||||||
|
|
||||||
// 1. all unset -> goto destination create
|
|
||||||
|
|
||||||
// 2. some unset -> ask if goto destination create
|
|
||||||
|
|
||||||
// 3. all set -> goto amount/route
|
|
||||||
|
|
||||||
|
this.modalDialogShow = false;
|
||||||
|
|
||||||
|
if (action === 'dismiss') {
|
||||||
|
this.modalStash = null;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dataSourceId = dataSource !== -1 ? dataSource : null;
|
if (action === 'deny') {
|
||||||
this.editIds = ids;
|
this.openModal(this.modalStash.type, this.modalStash.ids, this.modalStash.dataSource, this.modalStash.massEdit)
|
||||||
this.modalType = type;
|
this.modalStash = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.destMgmt();
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
openModal(type, ids, dataSource = -1, massEdit = true) {
|
||||||
|
|
||||||
|
console.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 === 'amount' || type === 'routes')) {
|
||||||
|
if(dataSource !== -1)
|
||||||
|
ids = [dataSource];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.modalDialogShow) {
|
||||||
|
console.log("open modal (actual)", type, ids, dataSource, massEdit, this.modalStash, this.modalDialogShow)
|
||||||
|
this.fillData(type, dataSource, massEdit);
|
||||||
|
this.editIds = ids;
|
||||||
|
this.modalType = type;
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
async closeEditModalAction(action) {
|
async closeEditModalAction(action) {
|
||||||
|
|
||||||
if (this.modalType === "destinations") {
|
if (this.modalType === "destinations") {
|
||||||
//TODO new destination handling
|
|
||||||
|
if (action === 'accept') {
|
||||||
|
const destMatrix = this.$refs.modalComponent?.destMatrix;
|
||||||
|
|
||||||
|
if (destMatrix) {
|
||||||
|
await this.destinationEditStore.massSetDestinations(destMatrix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear data
|
||||||
|
this.fillData(this.modalType);
|
||||||
|
this.modalType = null;
|
||||||
|
|
||||||
|
if (this.modalStash && action === 'accept') {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.openModal(this.modalStash.type, this.modalStash.ids, this.modalStash.dataSource, this.modalStash.massEdit);
|
||||||
|
this.modalStash = null;
|
||||||
|
}, 300);
|
||||||
|
} else {
|
||||||
|
this.modalStash = null;
|
||||||
|
}
|
||||||
|
|
||||||
} else if (action === "accept") {
|
} else if (action === "accept") {
|
||||||
await this.premiseEditStore.batchUpdate(this.modalType, this.editIds, this.modalProps);
|
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;
|
||||||
}
|
}
|
||||||
// Clear data
|
|
||||||
this.fillData(this.modalType);
|
|
||||||
this.modalType = null;
|
|
||||||
},
|
},
|
||||||
fillData(type, id = -1, hideDescription = false) {
|
fillData(type, id = -1, hideDescription = false) {
|
||||||
|
|
||||||
|
|
@ -415,9 +503,15 @@ export default {
|
||||||
stackable: true
|
stackable: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (type === 'amount' || type === 'routes' || type === 'destinations')
|
||||||
|
this.modalProps = {};
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
const premise = this.premiseEditStore.getById(id);
|
const premise = this.premiseEditStore.getById(id);
|
||||||
|
|
||||||
|
this.modalTitle = "Edit ".concat(type);
|
||||||
|
|
||||||
if (type === "price") {
|
if (type === "price") {
|
||||||
this.modalProps = {
|
this.modalProps = {
|
||||||
price: premise.material_cost,
|
price: premise.material_cost,
|
||||||
|
|
@ -447,9 +541,14 @@ export default {
|
||||||
mixable: premise.is_mixable ?? true,
|
mixable: premise.is_mixable ?? true,
|
||||||
stackable: premise.is_stackable ?? true
|
stackable: premise.is_stackable ?? true
|
||||||
}
|
}
|
||||||
|
} else if (type === 'amount' || type === 'routes' || type === 'destinations') {
|
||||||
|
this.modalTitle = "Edit destinations";
|
||||||
|
this.modalProps = {type: type};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
,
|
||||||
|
|
||||||
/* Animation hooks */
|
/* Animation hooks */
|
||||||
|
|
||||||
|
|
@ -458,7 +557,8 @@ export default {
|
||||||
el.style.opacity = 0;
|
el.style.opacity = 0;
|
||||||
el.style.transform = 'translateY(2rem)';
|
el.style.transform = 'translateY(2rem)';
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
,
|
||||||
onEnter(el, done) {
|
onEnter(el, done) {
|
||||||
if (this.isInitialLoad) {
|
if (this.isInitialLoad) {
|
||||||
const index = parseInt(el.dataset.index) || 0;
|
const index = parseInt(el.dataset.index) || 0;
|
||||||
|
|
@ -491,8 +591,13 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
|
.sub-header {
|
||||||
|
flex-shrink: 0; /* Prevent header from shrinking */
|
||||||
|
margin-bottom: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Global style für copy-mode cursor */
|
/* Global style für copy-mode cursor */
|
||||||
.edit-calculation-container.has-selection :deep(.bulk-edit-row__cell--clickable:hover) {
|
.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;
|
cursor: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDEyOC41MSAxMzQuMDUiPjxkZWZzPjxzdHlsZT4uY3tmaWxsOm5vbmU7fS5jLC5ke3N0cm9rZTojMDEwMTAxO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2Utd2lkdGg6NXB4O30uZHtmaWxsOiNmZmY7fTwvc3R5bGU+PC9kZWZzPjxnIGlkPSJhIj48cGF0aCBjbGFzcz0iYyIgZD0ibTU0Ljg5LDExMi41MWgtMi4yNGMtMS4yNCwwLTIuMjQtMS0yLjI0LTIuMjR2LTIuMjQiLz48bGluZSBjbGFzcz0iYyIgeDE9IjcwLjU3IiB5MT0iNzYuNjciIHgyPSI2My44NSIgeTI9Ijc2LjY3Ii8+PGxpbmUgY2xhc3M9ImMiIHgxPSI3MC41NyIgeTE9IjExMi41MSIgeDI9IjY2LjA5IiB5Mj0iMTEyLjUxIi8+PGxpbmUgY2xhc3M9ImMiIHgxPSI4Ni4yNSIgeTE9Ijk5LjA3IiB4Mj0iODYuMjUiIHkyPSI5Mi4zNSIvPjxsaW5lIGNsYXNzPSJjIiB4MT0iNTAuNDEiIHkxPSI5Ni44MyIgeDI9IjUwLjQxIiB5Mj0iOTIuMzUiLz48cGF0aCBjbGFzcz0iYyIgZD0ibTgxLjc3LDExMi41MWgyLjI0YzEuMjQsMCwyLjI0LTEsMi4yNC0yLjI0di0yLjI0Ii8+PHBhdGggY2xhc3M9ImMiIGQ9Im04MS43Nyw3Ni42N2gyLjI0YzEuMjQsMCwyLjI0LDEsMi4yNCwyLjI0djIuMjQiLz48cGF0aCBjbGFzcz0iYyIgZD0ibTU0Ljg5LDc2LjY3aC0yLjI0Yy0xLjI0LDAtMi4yNCwxLTIuMjQsMi4yNHYyLjI0Ii8+PHBhdGggY2xhc3M9ImMiIGQ9Im04Ni4yNSw5OS4wN2gxMS4yYzEuMjQsMCwyLjI0LTEsMi4yNC0yLjI0di0zMS4zNmMwLTEuMjQtMS0yLjI0LTIuMjQtMi4yNGgtMzEuMzZjLTEuMjQsMC0yLjI0LDEtMi4yNCwyLjI0djExLjIiLz48L2c+PGcgaWQ9ImIiPjxwYXRoIGNsYXNzPSJkIiBkPSJtNDQuMDgsNDQuMDdsMzIuOTQtOS4yYzEuNjktLjUyLDIuNjQtMi4zMSwyLjEyLTQtLjMtLjk4LTEuMDUtMS43NS0yLjAxLTIuMDlMNi43MywyLjY3Yy0xLjY3LS41Ny0zLjQ5LjMzLTQuMDYsMi0uMjMuNjYtLjIzLDEuMzgsMCwyLjA1bDI2LjExLDcwLjRjLjU4LDEuNjcsMi40LDIuNTYsNC4wNywxLjk4Ljk3LS4zMywxLjcxLTEuMTEsMi4wMS0yLjA5bDkuMjItMzIuOTRaIi8+PC9nPjwvc3ZnPg==") 12 12, pointer;
|
||||||
background-color: #f8fafc;
|
background-color: #f8fafc;
|
||||||
border-radius: 0.8rem;
|
border-radius: 0.8rem;
|
||||||
|
|
@ -521,8 +626,6 @@ export default {
|
||||||
.modal-content-container {
|
.modal-content-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.6rem;
|
|
||||||
margin-top: 1.6rem;
|
|
||||||
min-width: 50rem;
|
min-width: 50rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -593,6 +696,18 @@ export default {
|
||||||
gap: 0.8rem;
|
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 {
|
.edit-calculation-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -174,11 +174,10 @@ export default {
|
||||||
},
|
},
|
||||||
close() {
|
close() {
|
||||||
if (this.bulkEditQuery) {
|
if (this.bulkEditQuery) {
|
||||||
//TODO: deselect and save
|
//TODO: deselect element and save
|
||||||
// this.premiseEditStore.deselectPremise();
|
|
||||||
this.$router.push({name: 'bulk', params: {ids: this.bulkEditQuery}});
|
this.$router.push({name: 'bulk', params: {ids: this.bulkEditQuery}});
|
||||||
} else {
|
} else {
|
||||||
//TODO: deselect and save
|
//TODO: deselect element and save
|
||||||
this.$router.push({name: 'home'});
|
this.$router.push({name: 'home'});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,53 @@
|
||||||
import {defineStore} from 'pinia'
|
import {defineStore} from 'pinia'
|
||||||
import {toRaw} from "vue";
|
import performRequest from "@/backend.js";
|
||||||
|
import {config} from '@/config'
|
||||||
|
|
||||||
export const useDestinationEditStore = defineStore('destinationEdit', {
|
export const useDestinationEditStore = defineStore('destinationEdit', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
destinations: null,
|
destinations: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
}),
|
}),
|
||||||
getters: {},
|
getters: {
|
||||||
actions: {
|
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
setupDestinations(premisses) {
|
setupDestinations(premisses) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
|
|
@ -18,312 +57,39 @@ export const useDestinationEditStore = defineStore('destinationEdit', {
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
},
|
},
|
||||||
|
async massSetDestinations(updateMatrix) {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
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)) : [];
|
||||||
|
|
||||||
|
this.destinations.set(premiseId, [...filtered, ...data[premiseId]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
this.loading = false;
|
||||||
* 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() {
|
async massUpdateDestinations(updateMatrix) {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
if (!this.destinations.massEdit) {
|
this.loading = false;
|
||||||
|
|
||||||
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 => this.selectedIds.includes(p.id)).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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import {defineStore} from 'pinia'
|
import {defineStore} from 'pinia'
|
||||||
import {config} from '@/config'
|
import {config} from '@/config'
|
||||||
import {toRaw} from "vue";
|
|
||||||
import {useNotificationStore} from "@/store/notification.js";
|
|
||||||
import logger from "@/logger.js"
|
import logger from "@/logger.js"
|
||||||
import performRequest from '@/backend.js'
|
import performRequest from '@/backend.js'
|
||||||
|
|
||||||
|
|
@ -164,7 +162,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
const direction = (type !== this.sortedBy) ? 'desc' : (this.order.get(type) === 'asc' ? 'desc' : 'asc');
|
const direction = (type !== this.sortedBy) ? this.order.get(type) : (this.order.get(type) === 'asc' ? 'desc' : 'asc');
|
||||||
|
|
||||||
const temp = this.premisses.slice();
|
const temp = this.premisses.slice();
|
||||||
temp.sort((a, b) => {
|
temp.sort((a, b) => {
|
||||||
|
|
@ -179,7 +177,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
||||||
else return a.id - b.id;
|
else return a.id - b.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("sort", this.sortedBy, direction, type);
|
|
||||||
this.premisses = temp;
|
this.premisses = temp;
|
||||||
this.sortedBy = type;
|
this.sortedBy = type;
|
||||||
this.order.set(type, direction);
|
this.order.set(type, direction);
|
||||||
|
|
|
||||||
|
|
@ -90,9 +90,13 @@ export const usePremiseSingleEditStore = defineStore('premiseSingleEdit', {
|
||||||
if (this.premise === null) return;
|
if (this.premise === null) return;
|
||||||
this.routing = true;
|
this.routing = true;
|
||||||
|
|
||||||
const body = {destination_node_id: node.id, premise_id: [this.premise.id]};
|
const destinationNodeIds = {};
|
||||||
|
destinationNodeIds[this.premise.id] = [node.id, ...this.premise.destinations.map(d => d.destination_node.id)];
|
||||||
|
|
||||||
|
const body = {destination_node_ids: destinationNodeIds};
|
||||||
const url = `${config.backendUrl}/calculation/destination/`;
|
const url = `${config.backendUrl}/calculation/destination/`;
|
||||||
|
|
||||||
|
logger.info("addDestination", body, url);
|
||||||
|
|
||||||
const {data: destinations} = await performRequest(this, 'POST', url, body).catch(e => {
|
const {data: destinations} = await performRequest(this, 'POST', url, body).catch(e => {
|
||||||
this.routing = false;
|
this.routing = false;
|
||||||
|
|
@ -101,10 +105,11 @@ export const usePremiseSingleEditStore = defineStore('premiseSingleEdit', {
|
||||||
|
|
||||||
const ids = []
|
const ids = []
|
||||||
|
|
||||||
for (const destId of Object.keys(destinations)) {
|
if (destinations[this.premise.id]?.length !== 0)
|
||||||
this.premise.destinations.push(destinations[destId]);
|
for (const destId of Object.keys(destinations[this.premise.id])) {
|
||||||
ids.push(destinations[destId].id);
|
this.premise.destinations.push(destinations[this.premise.id][destId]);
|
||||||
}
|
ids.push(destinations[this.premise.id][destId].id);
|
||||||
|
}
|
||||||
|
|
||||||
this.routing = false;
|
this.routing = false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,6 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.net.URLDecoder;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
@ -176,14 +174,14 @@ public class PremiseController {
|
||||||
|
|
||||||
@PostMapping({"/destination", "/destination/"})
|
@PostMapping({"/destination", "/destination/"})
|
||||||
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
|
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
|
||||||
public ResponseEntity<Map<Integer, DestinationDTO>> createDestination(@RequestBody @Valid DestinationCreateDTO destinationCreateDTO) {
|
public ResponseEntity<Map<Integer, List<DestinationDTO>>> createDestination(@RequestBody @Valid DestinationCreateDTO destinationCreateDTO) {
|
||||||
return ResponseEntity.ok(destinationService.createDestination(destinationCreateDTO));
|
return ResponseEntity.ok(destinationService.massSetDestinations(destinationCreateDTO));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping({"/destination", "/destination/"})
|
@PutMapping({"/destination", "/destination/"})
|
||||||
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
|
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
|
||||||
public ResponseEntity<Map<Integer, List<DestinationDTO>>> setDestination(@RequestBody DestinationSetDTO destinationSetDTO) {
|
public ResponseEntity<Map<Integer, List<DestinationDTO>>> setDestination(@RequestBody DestinationSetDTO destinationSetDTO) {
|
||||||
return ResponseEntity.ok(destinationService.setDestination(destinationSetDTO));
|
return ResponseEntity.ok(destinationService.massSetDestinationProperties(destinationSetDTO));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping({"/destination/{id}", "/destination/{id}/"})
|
@GetMapping({"/destination/{id}", "/destination/{id}/"})
|
||||||
|
|
|
||||||
|
|
@ -6,32 +6,20 @@ import jakarta.validation.constraints.NotEmpty;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class DestinationCreateDTO {
|
public class DestinationCreateDTO {
|
||||||
|
|
||||||
@NotEmpty (message = "At least one premise must be selected")
|
@NotEmpty(message = "At least one premise must be selected")
|
||||||
@NotNull (message = "At least one premise must be selected")
|
@NotNull(message = "At least one premise must be selected")
|
||||||
@JsonProperty("premise_id")
|
@JsonProperty("destination_node_ids")
|
||||||
List<@Min(value = 1, message = "Invalid premise id") Integer> premiseId;
|
Map<Integer, List<@Min(value = 1, message = "Missing destination ids") Integer>> destinationNodeIds;
|
||||||
|
|
||||||
@Min(value = 1, message = "Invalid destination node id")
|
public Map<Integer, List<Integer>> getDestinationNodeIds() {
|
||||||
@NotNull (message = "Destination node id must be provided")
|
return destinationNodeIds;
|
||||||
@JsonProperty("destination_node_id")
|
|
||||||
Integer destinationNodeId;
|
|
||||||
|
|
||||||
public List<Integer> getPremiseId() {
|
|
||||||
return premiseId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPremiseId(List<Integer> premiseId) {
|
public void setDestinationNodeIds(Map<Integer, List<Integer>> destinationNodeIds) {
|
||||||
this.premiseId = premiseId;
|
this.destinationNodeIds = destinationNodeIds;
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getDestinationNodeId() {
|
|
||||||
return destinationNodeId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDestinationNodeId(Integer destinationNodeId) {
|
|
||||||
this.destinationNodeId = destinationNodeId;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
package de.avatic.lcc.dto.calculation.edit.destination;
|
package de.avatic.lcc.dto.calculation.edit.destination;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import jakarta.validation.constraints.DecimalMin;
|
import jakarta.validation.constraints.*;
|
||||||
import jakarta.validation.constraints.Digits;
|
|
||||||
import jakarta.validation.constraints.Min;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class DestinationSetDTO {
|
public class DestinationSetDTO {
|
||||||
|
|
||||||
|
|
@ -15,6 +14,7 @@ public class DestinationSetDTO {
|
||||||
@JsonProperty("destinations")
|
@JsonProperty("destinations")
|
||||||
List<DestinationSetListItemDTO> destinations;
|
List<DestinationSetListItemDTO> destinations;
|
||||||
|
|
||||||
|
|
||||||
public List<Integer> getPremiseId() {
|
public List<Integer> getPremiseId() {
|
||||||
return premiseId;
|
return premiseId;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.sql.Statement;
|
import java.sql.Statement;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class DestinationRepository {
|
public class DestinationRepository {
|
||||||
|
|
@ -50,6 +51,21 @@ public class DestinationRepository {
|
||||||
return jdbcTemplate.query(query, new DestinationMapper(), id);
|
return jdbcTemplate.query(query, new DestinationMapper(), id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public List<Destination> getByPremiseIdAndUserId(Integer premiseId, Integer userId) {
|
||||||
|
|
||||||
|
String premiseCheckQuery = "SELECT COUNT(*) FROM premise WHERE id = ? AND user_id = ?";
|
||||||
|
|
||||||
|
Integer count = jdbcTemplate.queryForObject(premiseCheckQuery, Integer.class, premiseId, userId);
|
||||||
|
|
||||||
|
if (count == null || count == 0) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
String query = "SELECT * FROM premise_destination WHERE premise_id = ?";
|
||||||
|
return jdbcTemplate.query(query, new DestinationMapper(), premiseId);
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void update(Integer id, Integer annualAmount, BigDecimal repackingCost, BigDecimal disposalCost, BigDecimal handlingCost, Boolean isD2d, BigDecimal d2dRate, BigDecimal d2dLeadTime) {
|
public void update(Integer id, Integer annualAmount, BigDecimal repackingCost, BigDecimal disposalCost, BigDecimal handlingCost, Boolean isD2d, BigDecimal d2dRate, BigDecimal d2dLeadTime) {
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
|
|
@ -73,7 +89,7 @@ public class DestinationRepository {
|
||||||
setClauses.add("handling_cost = :handlingCost");
|
setClauses.add("handling_cost = :handlingCost");
|
||||||
parameters.put("handlingCost", handlingCost);
|
parameters.put("handlingCost", handlingCost);
|
||||||
|
|
||||||
var setD2d = isD2d != null ? isD2d : false;
|
var setD2d = isD2d != null ? isD2d : false;
|
||||||
setClauses.add("is_d2d = :isD2d");
|
setClauses.add("is_d2d = :isD2d");
|
||||||
parameters.put("isD2d", setD2d);
|
parameters.put("isD2d", setD2d);
|
||||||
|
|
||||||
|
|
@ -143,10 +159,10 @@ public class DestinationRepository {
|
||||||
|
|
||||||
String placeholders = String.join(",", Collections.nCopies(ids.size(), "?"));
|
String placeholders = String.join(",", Collections.nCopies(ids.size(), "?"));
|
||||||
String query = String.format("""
|
String query = String.format("""
|
||||||
SELECT pd.id AS pd_id, p.user_id AS user_id
|
SELECT pd.id AS pd_id, p.user_id AS user_id
|
||||||
FROM premise_destination pd
|
FROM premise_destination pd
|
||||||
JOIN premise p ON pd.premise_id = p.id
|
JOIN premise p ON pd.premise_id = p.id
|
||||||
WHERE pd.id IN (%s)""", placeholders);
|
WHERE pd.id IN (%s)""", placeholders);
|
||||||
|
|
||||||
return jdbcTemplate.query(query, rs -> {
|
return jdbcTemplate.query(query, rs -> {
|
||||||
Map<Integer, Integer> result = new HashMap<>();
|
Map<Integer, Integer> result = new HashMap<>();
|
||||||
|
|
@ -157,20 +173,73 @@ public class DestinationRepository {
|
||||||
}, ids.toArray());
|
}, ids.toArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Transactional
|
||||||
|
// public List<Destination> getByPremiseIdsAndNodeId(List<Integer> premiseId, Integer nodeId, Integer userId) {
|
||||||
|
// String placeholder = String.join(",", Collections.nCopies(premiseId.size(), "?"));
|
||||||
|
// String query = "SELECT * FROM premise_destination JOIN premise ON premise_destination.premise_id = premise.id WHERE premise_destination.premise_id IN (" + placeholder + ") AND premise_destination.destination_node_id = ? AND premise.user_id = ?";
|
||||||
|
//
|
||||||
|
// // Create array with all parameters
|
||||||
|
// Object[] params = new Object[premiseId.size() + 2];
|
||||||
|
// for (int i = 0; i < premiseId.size(); i++) {
|
||||||
|
// params[i] = premiseId.get(i);
|
||||||
|
// }
|
||||||
|
// params[premiseId.size()] = nodeId;
|
||||||
|
// params[premiseId.size() + 1] = userId;
|
||||||
|
//
|
||||||
|
// return jdbcTemplate.query(query, new DestinationMapper(), params);
|
||||||
|
// }
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public List<Destination> getByPremiseIdsAndNodeId(List<Integer> premiseId, Integer nodeId, Integer userId) {
|
public Map<Integer, List<Destination>> getByPremiseIdsAndNodeIds(Map<Integer, List<Integer>> premiseToNodes, Integer userId) {
|
||||||
String placeholder = String.join(",", Collections.nCopies(premiseId.size(), "?"));
|
if (premiseToNodes.isEmpty()) {
|
||||||
String query = "SELECT * FROM premise_destination JOIN premise ON premise_destination.premise_id = premise.id WHERE premise_destination.premise_id IN (" + placeholder + ") AND premise_destination.destination_node_id = ? AND premise.user_id = ?";
|
return new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flatten all premise IDs and node IDs for the query
|
||||||
|
List<Integer> allPremiseIds = new ArrayList<>(premiseToNodes.keySet());
|
||||||
|
Set<Integer> allNodeIds = premiseToNodes.values().stream()
|
||||||
|
.flatMap(List::stream)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
String premisePlaceholder = String.join(",", Collections.nCopies(allPremiseIds.size(), "?"));
|
||||||
|
String nodePlaceholder = String.join(",", Collections.nCopies(allNodeIds.size(), "?"));
|
||||||
|
|
||||||
|
String query = "SELECT * FROM premise_destination " +
|
||||||
|
"JOIN premise ON premise_destination.premise_id = premise.id " +
|
||||||
|
"WHERE premise_destination.premise_id IN (" + premisePlaceholder + ") " +
|
||||||
|
"AND premise_destination.destination_node_id IN (" + nodePlaceholder + ") " +
|
||||||
|
"AND premise.user_id = ?";
|
||||||
|
|
||||||
// Create array with all parameters
|
// Create array with all parameters
|
||||||
Object[] params = new Object[premiseId.size() + 2];
|
Object[] params = new Object[allPremiseIds.size() + allNodeIds.size() + 1];
|
||||||
for (int i = 0; i < premiseId.size(); i++) {
|
int index = 0;
|
||||||
params[i] = premiseId.get(i);
|
|
||||||
}
|
|
||||||
params[premiseId.size()] = nodeId;
|
|
||||||
params[premiseId.size() + 1] = userId;
|
|
||||||
|
|
||||||
return jdbcTemplate.query(query, new DestinationMapper(), params);
|
for (Integer premiseId : allPremiseIds) {
|
||||||
|
params[index++] = premiseId;
|
||||||
|
}
|
||||||
|
for (Integer nodeId : allNodeIds) {
|
||||||
|
params[index++] = nodeId;
|
||||||
|
}
|
||||||
|
params[index] = userId;
|
||||||
|
|
||||||
|
List<Destination> allDestinations = jdbcTemplate.query(query, new DestinationMapper(), params);
|
||||||
|
|
||||||
|
// Group destinations by premise ID and filter by the requested node IDs
|
||||||
|
Map<Integer, List<Destination>> result = new HashMap<>();
|
||||||
|
|
||||||
|
for (Map.Entry<Integer, List<Integer>> entry : premiseToNodes.entrySet()) {
|
||||||
|
Integer premiseId = entry.getKey();
|
||||||
|
Set<Integer> requestedNodeIds = new HashSet<>(entry.getValue());
|
||||||
|
|
||||||
|
List<Destination> filteredDestinations = allDestinations.stream()
|
||||||
|
.filter(d -> d.getPremiseId().equals(premiseId) &&
|
||||||
|
requestedNodeIds.contains(d.getDestinationNodeId()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
result.put(premiseId, filteredDestinations);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,12 @@ package de.avatic.lcc.service.access;
|
||||||
import de.avatic.lcc.dto.calculation.DestinationDTO;
|
import de.avatic.lcc.dto.calculation.DestinationDTO;
|
||||||
import de.avatic.lcc.dto.calculation.edit.destination.DestinationCreateDTO;
|
import de.avatic.lcc.dto.calculation.edit.destination.DestinationCreateDTO;
|
||||||
import de.avatic.lcc.dto.calculation.edit.destination.DestinationSetDTO;
|
import de.avatic.lcc.dto.calculation.edit.destination.DestinationSetDTO;
|
||||||
import de.avatic.lcc.dto.calculation.edit.destination.DestinationSetListItemDTO;
|
|
||||||
import de.avatic.lcc.dto.calculation.edit.destination.DestinationUpdateDTO;
|
import de.avatic.lcc.dto.calculation.edit.destination.DestinationUpdateDTO;
|
||||||
import de.avatic.lcc.model.db.nodes.Node;
|
import de.avatic.lcc.model.db.nodes.Node;
|
||||||
import de.avatic.lcc.model.db.premises.Premise;
|
import de.avatic.lcc.model.db.premises.Premise;
|
||||||
import de.avatic.lcc.model.db.premises.route.*;
|
import de.avatic.lcc.model.db.premises.route.*;
|
||||||
import de.avatic.lcc.repositories.NodeRepository;
|
import de.avatic.lcc.repositories.NodeRepository;
|
||||||
import de.avatic.lcc.repositories.premise.*;
|
import de.avatic.lcc.repositories.premise.*;
|
||||||
import de.avatic.lcc.repositories.properties.PropertyRepository;
|
|
||||||
import de.avatic.lcc.repositories.users.UserNodeRepository;
|
import de.avatic.lcc.repositories.users.UserNodeRepository;
|
||||||
import de.avatic.lcc.service.calculation.RoutingService;
|
import de.avatic.lcc.service.calculation.RoutingService;
|
||||||
import de.avatic.lcc.service.transformer.premise.DestinationTransformer;
|
import de.avatic.lcc.service.transformer.premise.DestinationTransformer;
|
||||||
|
|
@ -51,8 +49,52 @@ public class DestinationService {
|
||||||
this.authorizationService = authorizationService;
|
this.authorizationService = authorizationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Map<Integer, List<Destination>> processDestinations(List<Premise> premisesToProcess, Map<Integer, List<Integer>> destinationNodeIds, Integer annualAmount, Number repackingCost, Number disposalCost, Number handlingCost, Map<RouteIds, List<RouteInformation>> routes) {
|
||||||
|
|
||||||
|
var destMap = new HashMap<Integer, List<Destination>>();
|
||||||
|
|
||||||
|
for (var premise : premisesToProcess) {
|
||||||
|
|
||||||
|
var destinations = new ArrayList<Destination>();
|
||||||
|
|
||||||
|
for (var destinationNodeId : destinationNodeIds.get(premise.getId())) {
|
||||||
|
|
||||||
|
Node destinationNode = nodeRepository.getById(destinationNodeId).orElseThrow();
|
||||||
|
|
||||||
|
var destination = new Destination();
|
||||||
|
destination.setDestinationNodeId(destinationNodeId);
|
||||||
|
destination.setPremiseId(premise.getId());
|
||||||
|
destination.setAnnualAmount(annualAmount);
|
||||||
|
destination.setD2d(false);
|
||||||
|
destination.setLeadTimeD2d(null);
|
||||||
|
destination.setRateD2d(null);
|
||||||
|
destination.setDisposalCost(disposalCost == null ? null : BigDecimal.valueOf(disposalCost.doubleValue()));
|
||||||
|
destination.setHandlingCost(handlingCost == null ? null : BigDecimal.valueOf(handlingCost.doubleValue()));
|
||||||
|
destination.setRepackingCost(repackingCost == null ? null : BigDecimal.valueOf(repackingCost.doubleValue()));
|
||||||
|
destination.setCountryId(destinationNode.getCountryId());
|
||||||
|
destination.setGeoLat(destinationNode.getGeoLat());
|
||||||
|
destination.setGeoLng(destinationNode.getGeoLng());
|
||||||
|
destination.setId(destinationRepository.insert(destination));
|
||||||
|
|
||||||
|
Node source = premise.getSupplierNodeId() == null ? userNodeRepository.getById(premise.getUserSupplierNodeId()).orElseThrow() : nodeRepository.getById(premise.getSupplierNodeId()).orElseThrow();
|
||||||
|
|
||||||
|
if (routes != null)
|
||||||
|
//noinspection SpringTransactionalMethodCallsInspection
|
||||||
|
saveRoute(routes.get(new RouteIds(source.getId(), destinationNodeId, premise.getSupplierNodeId() == null)), destination.getId());
|
||||||
|
|
||||||
|
|
||||||
|
destinations.add(destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
destMap.put(premise.getId(), destinations);
|
||||||
|
}
|
||||||
|
|
||||||
|
return destMap;
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public Map<Integer, List<DestinationDTO>> setDestination(DestinationSetDTO dto) {
|
public Map<Integer, List<DestinationDTO>> massSetDestinationProperties(DestinationSetDTO dto) {
|
||||||
var admin = authorizationService.isSuper();
|
var admin = authorizationService.isSuper();
|
||||||
Integer userId = authorizationService.getUserId();
|
Integer userId = authorizationService.getUserId();
|
||||||
|
|
||||||
|
|
@ -62,77 +104,69 @@ public class DestinationService {
|
||||||
deleteAllDestinationsByPremiseId(dto.getPremiseId(), false);
|
deleteAllDestinationsByPremiseId(dto.getPremiseId(), false);
|
||||||
|
|
||||||
var premisses = premiseRepository.getPremisesById(dto.getPremiseId());
|
var premisses = premiseRepository.getPremisesById(dto.getPremiseId());
|
||||||
Map<RouteIds, List<RouteInformation>> routes = findRoutes(premisses, dto.getDestinations().stream().map(DestinationSetListItemDTO::getDestinationNodeId).toList());
|
// TODO no routing in set ... only props.
|
||||||
|
// Map<RouteIds, List<RouteInformation>> routes = findRoutes(premisses, dto.getDestinations().stream().map(DestinationSetListItemDTO::getDestinationNodeId).toList());
|
||||||
|
|
||||||
Map<Integer, List<Destination>> destinations = dto.getDestinations().stream()
|
// Map<Integer, List<Destination>> destinations = dto.getDestinations().stream()
|
||||||
.flatMap(destination -> createDestination(premisses, destination.getDestinationNodeId(), destination.getAnnualAmount(), destination.getRepackingCost(), destination.getDisposalCost(), destination.getHandlingCost(), routes).stream())
|
// .flatMap(destination -> processDestinations(premisses, destination.getDestinationNodeId(), destination.getAnnualAmount(), destination.getRepackingCost(), destination.getDisposalCost(), destination.getHandlingCost(), null).stream())
|
||||||
.collect(Collectors.groupingBy(Destination::getPremiseId));
|
// .collect(Collectors.groupingBy(Destination::getPremiseId));
|
||||||
|
|
||||||
|
|
||||||
return destinations.entrySet().stream()
|
// return destinations.entrySet().stream()
|
||||||
.collect(Collectors.toMap(
|
// .collect(Collectors.toMap(
|
||||||
Map.Entry::getKey,
|
// Map.Entry::getKey,
|
||||||
entry -> entry.getValue().stream().map(destinationTransformer::toDestinationDTO).toList()));
|
// entry -> entry.getValue().stream().map(destinationTransformer::toDestinationDTO).toList()));
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<Integer> getDestinationToRemove(List<Destination> oldDestinations, List<Integer> newIds) {
|
||||||
|
return oldDestinations.stream().filter(dest -> !newIds.contains(dest.getDestinationNodeId())).map(Destination::getId).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
private List<Destination> createDestination(List<Premise> premisesToProcess, Integer destinationNodeId, Integer annualAmount, Number repackingCost, Number disposalCost, Number handlingCost, Map<RouteIds, List<RouteInformation>> routes) {
|
private List<Integer> getNodeIdsToAdd(List<Destination> oldDestinations, List<Integer> newIds) {
|
||||||
|
var oldIds = oldDestinations.stream().map(Destination::getDestinationNodeId).toList();
|
||||||
Node destinationNode = nodeRepository.getById(destinationNodeId).orElseThrow();
|
return newIds.stream().filter(id -> !oldIds.contains(id)).collect(Collectors.toList());
|
||||||
|
|
||||||
var destinations = new ArrayList<Destination>();
|
|
||||||
|
|
||||||
for (var premise : premisesToProcess) {
|
|
||||||
var destination = new Destination();
|
|
||||||
destination.setDestinationNodeId(destinationNodeId);
|
|
||||||
destination.setPremiseId(premise.getId());
|
|
||||||
destination.setAnnualAmount(annualAmount);
|
|
||||||
destination.setD2d(false);
|
|
||||||
destination.setLeadTimeD2d(null);
|
|
||||||
destination.setRateD2d(null);
|
|
||||||
destination.setDisposalCost(disposalCost == null ? null : BigDecimal.valueOf(disposalCost.doubleValue()));
|
|
||||||
destination.setHandlingCost(handlingCost == null ? null : BigDecimal.valueOf(handlingCost.doubleValue()));
|
|
||||||
destination.setRepackingCost(repackingCost == null ? null : BigDecimal.valueOf(repackingCost.doubleValue()));
|
|
||||||
destination.setCountryId(destinationNode.getCountryId());
|
|
||||||
destination.setGeoLat(destinationNode.getGeoLat());
|
|
||||||
destination.setGeoLng(destinationNode.getGeoLng());
|
|
||||||
destination.setId(destinationRepository.insert(destination));
|
|
||||||
|
|
||||||
Node source = premise.getSupplierNodeId() == null ? userNodeRepository.getById(premise.getUserSupplierNodeId()).orElseThrow() : nodeRepository.getById(premise.getSupplierNodeId()).orElseThrow();
|
|
||||||
|
|
||||||
//noinspection SpringTransactionalMethodCallsInspection
|
|
||||||
saveRoute(routes.get(new RouteIds(source.getId(), destinationNodeId, premise.getSupplierNodeId() == null)), destination.getId());
|
|
||||||
|
|
||||||
destinations.add(destination);
|
|
||||||
}
|
|
||||||
|
|
||||||
return destinations;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public Map<Integer, DestinationDTO> createDestination(DestinationCreateDTO dto) {
|
public Map<Integer, List<DestinationDTO>> massSetDestinations(DestinationCreateDTO dto) {
|
||||||
|
|
||||||
Integer userId = authorizationService.getUserId();
|
Integer userId = authorizationService.getUserId();
|
||||||
|
|
||||||
var existingDestinations = destinationRepository.getByPremiseIdsAndNodeId(dto.getPremiseId(), dto.getDestinationNodeId(), userId);
|
Map<Integer, List<Integer>> destinationsToProcess = new HashMap<>();
|
||||||
|
|
||||||
var premisesIdsToProcess = new ArrayList<Integer>();
|
var requestedDestMap = dto.getDestinationNodeIds();
|
||||||
for (var premiseId : dto.getPremiseId()) {
|
var existingDestMap = dto.getDestinationNodeIds().keySet().stream().collect(Collectors.toMap(id -> id, id -> destinationRepository.getByPremiseIdAndUserId(id, userId)));
|
||||||
if (existingDestinations.stream().map(Destination::getPremiseId).noneMatch(id -> id.equals(premiseId))) {
|
|
||||||
premisesIdsToProcess.add(premiseId);
|
for (Integer premiseId : requestedDestMap.keySet()) {
|
||||||
}
|
var requestedDestinations = requestedDestMap.get(premiseId);
|
||||||
|
var existingDestinations = existingDestMap.getOrDefault(premiseId, Collections.emptyList());
|
||||||
|
|
||||||
|
/* remove deselected */
|
||||||
|
var toRemove = getDestinationToRemove(existingDestinations, requestedDestinations);
|
||||||
|
deleteDestinationsById(toRemove, false);
|
||||||
|
|
||||||
|
/* find new selected */
|
||||||
|
var toAdd = getNodeIdsToAdd(existingDestinations, requestedDestinations);
|
||||||
|
destinationsToProcess.put(premiseId, toAdd);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (premisesIdsToProcess.isEmpty())
|
if (destinationsToProcess.isEmpty())
|
||||||
return new HashMap<>();
|
return new HashMap<>();
|
||||||
|
|
||||||
var premisses = premiseRepository.getPremisesById(premisesIdsToProcess);
|
var premisses = premiseRepository.getPremisesById(new ArrayList<>(destinationsToProcess.keySet()));
|
||||||
Map<RouteIds, List<RouteInformation>> routes = findRoutes(premisses, Collections.singletonList(dto.getDestinationNodeId()));
|
Map<RouteIds, List<RouteInformation>> routes = findRoutes(premisses, destinationsToProcess);
|
||||||
|
|
||||||
var destinations = createDestination(premisses, dto.getDestinationNodeId(), null, null, null, null, routes);
|
|
||||||
return destinations.stream().collect(Collectors.toMap(Destination::getPremiseId, destinationTransformer::toDestinationDTO));
|
|
||||||
|
|
||||||
|
var destinations = processDestinations(premisses, destinationsToProcess, null, null, null, null, routes);
|
||||||
|
return destinations.entrySet().stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
Map.Entry::getKey,
|
||||||
|
entry -> entry.getValue().stream()
|
||||||
|
.map(destinationTransformer::toDestinationDTO)
|
||||||
|
.toList()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
public DestinationDTO getDestination(Integer id) {
|
public DestinationDTO getDestination(Integer id) {
|
||||||
|
|
@ -170,14 +204,16 @@ public class DestinationService {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<RouteIds, List<RouteInformation>> findRoutes(List<Premise> premisses, List<Integer> destinationIds) {
|
private Map<RouteIds, List<RouteInformation>> findRoutes(List<Premise> premisses, Map<Integer, List<Integer>> routingRequest) {
|
||||||
|
|
||||||
Map<RouteIds, List<RouteInformation>> routes = new HashMap<>();
|
Map<RouteIds, List<RouteInformation>> routes = new HashMap<>();
|
||||||
Map<Integer, Node> nodes = new HashMap<>();
|
Map<Integer, Node> nodes = new HashMap<>();
|
||||||
Map<Integer, Node> userNodes = new HashMap<>();
|
Map<Integer, Node> userNodes = new HashMap<>();
|
||||||
|
|
||||||
for (var premise : premisses) {
|
for (var premise : premisses) {
|
||||||
for (var destinationId : destinationIds) {
|
|
||||||
|
for (var destinationId : routingRequest.get(premise.getId())) {
|
||||||
|
|
||||||
boolean isUserSupplierNode = (premise.getSupplierNodeId() == null);
|
boolean isUserSupplierNode = (premise.getSupplierNodeId() == null);
|
||||||
var ids = new RouteIds(isUserSupplierNode ? premise.getUserSupplierNodeId() : premise.getSupplierNodeId(), destinationId, isUserSupplierNode);
|
var ids = new RouteIds(isUserSupplierNode ? premise.getUserSupplierNodeId() : premise.getSupplierNodeId(), destinationId, isUserSupplierNode);
|
||||||
if (routes.containsKey(ids)) continue;
|
if (routes.containsKey(ids)) continue;
|
||||||
|
|
@ -194,6 +230,8 @@ public class DestinationService {
|
||||||
userNodes.put(premise.getUserSupplierNodeId(), userNodeRepository.getById(premise.getUserSupplierNodeId()).orElseThrow());
|
userNodes.put(premise.getUserSupplierNodeId(), userNodeRepository.getById(premise.getUserSupplierNodeId()).orElseThrow());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//TODO in parallel
|
||||||
routes.put(ids, routingService.findRoutes(nodes.get(destinationId), isUserSupplierNode ? userNodes.get(premise.getUserSupplierNodeId()) : nodes.get(premise.getSupplierNodeId()), isUserSupplierNode));
|
routes.put(ids, routingService.findRoutes(nodes.get(destinationId), isUserSupplierNode ? userNodes.get(premise.getUserSupplierNodeId()) : nodes.get(premise.getSupplierNodeId()), isUserSupplierNode));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -255,6 +293,11 @@ public class DestinationService {
|
||||||
destinations.forEach(destination -> deleteDestinationById(destination.getId(), deleteRoutesOnly));
|
destinations.forEach(destination -> deleteDestinationById(destination.getId(), deleteRoutesOnly));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deleteDestinationsById(List<Integer> ids, boolean deleteRoutesOnly) {
|
||||||
|
ids.forEach(id -> deleteDestinationById(id, deleteRoutesOnly));
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void deleteDestinationById(Integer id, boolean deleteRoutesOnly) {
|
public void deleteDestinationById(Integer id, boolean deleteRoutesOnly) {
|
||||||
var admin = authorizationService.isSuper();
|
var admin = authorizationService.isSuper();
|
||||||
|
|
|
||||||
|
|
@ -334,7 +334,7 @@ public class RoutingService {
|
||||||
}
|
}
|
||||||
|
|
||||||
finalSection.setRate(matrixRate);
|
finalSection.setRate(matrixRate);
|
||||||
finalSection.setApproxDistance(distanceService.getDistance(container.getSourceNode(), toNode, false));
|
finalSection.setApproxDistance(distanceService.getDistance(container.getSourceNode(), toNode, true));
|
||||||
rates.add(finalSection);
|
rates.add(finalSection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -699,7 +699,7 @@ public class RoutingService {
|
||||||
|
|
||||||
if (matrixRate.isPresent()) {
|
if (matrixRate.isPresent()) {
|
||||||
matrixRateObj.setRate(matrixRate.get());
|
matrixRateObj.setRate(matrixRate.get());
|
||||||
matrixRateObj.setApproxDistance(distanceService.getDistance(startNode, endNode, false));
|
matrixRateObj.setApproxDistance(distanceService.getDistance(startNode, endNode, true));
|
||||||
container.getRates().add(matrixRateObj);
|
container.getRates().add(matrixRateObj);
|
||||||
return matrixRateObj;
|
return matrixRateObj;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,7 @@ import org.springframework.test.context.jdbc.Sql;
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.*;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
|
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
|
||||||
|
|
@ -62,8 +60,11 @@ public class CalculationIntegrationTests {
|
||||||
var premise3 = premisesBeforeUpdate.stream().filter(p -> p.getHuUnitCount() == 3).findFirst().orElseThrow();
|
var premise3 = premisesBeforeUpdate.stream().filter(p -> p.getHuUnitCount() == 3).findFirst().orElseThrow();
|
||||||
|
|
||||||
var createDto = new DestinationCreateDTO();
|
var createDto = new DestinationCreateDTO();
|
||||||
createDto.setPremiseId(Collections.singletonList(premise1.getId()));
|
|
||||||
createDto.setDestinationNodeId(nodeId);
|
var map = new HashMap<Integer, List<Integer>>();
|
||||||
|
map.put(premise1.getId(), List.of(nodeId));
|
||||||
|
createDto.setDestinationNodeIds(map);
|
||||||
|
|
||||||
|
|
||||||
var response = mockMvc.perform(post("/api/calculation/destination")
|
var response = mockMvc.perform(post("/api/calculation/destination")
|
||||||
.content(objectMapper.writeValueAsString(createDto))
|
.content(objectMapper.writeValueAsString(createDto))
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||||
|
|
@ -61,8 +62,11 @@ public class DestinationIntegrationTest {
|
||||||
var premise3 = premisesBeforeUpdate.stream().filter(p -> p.getHuUnitCount() == 3).findFirst().orElseThrow();
|
var premise3 = premisesBeforeUpdate.stream().filter(p -> p.getHuUnitCount() == 3).findFirst().orElseThrow();
|
||||||
|
|
||||||
var dto = new DestinationCreateDTO();
|
var dto = new DestinationCreateDTO();
|
||||||
dto.setPremiseId(Arrays.asList(premise1.getId(), premise3.getId()));
|
|
||||||
dto.setDestinationNodeId(nodeId);
|
var map = new HashMap<Integer, List<Integer>>();
|
||||||
|
map.put(premise1.getId(), List.of(nodeId));
|
||||||
|
map.put(premise3.getId(), List.of(nodeId));
|
||||||
|
dto.setDestinationNodeIds(map);
|
||||||
|
|
||||||
mockMvc.perform(post("/api/calculation/destination")
|
mockMvc.perform(post("/api/calculation/destination")
|
||||||
.content(objectMapper.writeValueAsString(dto))
|
.content(objectMapper.writeValueAsString(dto))
|
||||||
|
|
|
||||||