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>
|
||||
<style>
|
||||
.cls-1 {
|
||||
|
|
@ -9,10 +9,8 @@
|
|||
fill: #002f54;
|
||||
}
|
||||
</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-2" points="289.44 82.78 289.44 82.79 241.65 110.38 193.85 137.97 146.06 165.57 98.27 137.97 50.47 110.38 2.68 82.79 2.68 82.78 50.47 55.19 98.26 27.6 98.27 27.6 98.27 27.59 146.06 0 193.85 27.59 193.85 27.6 146.06 55.19 98.27 82.78 98.27 82.79 146.06 110.38 193.85 82.79 193.86 82.79 241.65 55.19 241.66 55.19 289.44 82.78"></polygon>
|
||||
<polygon class="cls-2" points="143.38 289.94 143.38 345.13 95.59 317.54 47.79 289.94 0 262.35 0 96.79 47.79 124.38 47.79 234.76 95.58 262.35 95.59 262.35 143.38 289.94"></polygon>
|
||||
</svg><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
|
||||
@media (prefers-color-scheme: dark) { :root { filter: none; } }
|
||||
</style></svg>
|
||||
</svg></svg></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.6 KiB |
|
|
@ -29,6 +29,14 @@ export default {
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
html.modal-open {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
font-weight: normal;
|
||||
margin-bottom: 3rem;
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
<template>
|
||||
<div class="checkbox-container">
|
||||
<label class="checkbox-item" :class="{ disabled: disabled }" @change="setFilter">
|
||||
<label class="checkbox-item" :class="{ disabled: disabled }">
|
||||
<input
|
||||
@keydown.enter="$emit('enter', $event)"
|
||||
type="checkbox"
|
||||
:checked="isChecked"
|
||||
:checked="internalChecked"
|
||||
:disabled="disabled"
|
||||
:indeterminate.prop="isIndeterminate"
|
||||
v-model="isChecked"
|
||||
:indeterminate.prop="internalIndeterminate"
|
||||
@change="handleChange"
|
||||
ref="checkboxInput"
|
||||
>
|
||||
<span class="checkmark" :class="{ indeterminate: isIndeterminate }"></span>
|
||||
<span class="checkmark" :class="{ indeterminate: internalIndeterminate }"></span>
|
||||
<span class="checkbox-label"><slot></slot></span>
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
emits:["checkbox-changed"],
|
||||
emits: ["checkbox-changed", "enter"],
|
||||
props: {
|
||||
checked: {
|
||||
type: Boolean,
|
||||
|
|
@ -40,45 +40,49 @@ export default{
|
|||
data() {
|
||||
return {
|
||||
internalChecked: this.checked,
|
||||
internalIndeterminate: this.indeterminate,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isChecked: {
|
||||
get() {
|
||||
return this.internalChecked;
|
||||
},
|
||||
set(value) {
|
||||
if (this.disabled) return; // Prevent changes when disabled
|
||||
this.internalChecked = value;
|
||||
this.internalIndeterminate = false;
|
||||
this.$emit('checkbox-changed', value);
|
||||
}
|
||||
},
|
||||
isIndeterminate() {
|
||||
return this.internalIndeterminate && !this.internalChecked;
|
||||
internalIndeterminate: this.indeterminate && !this.checked,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
checked(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) {
|
||||
this.internalIndeterminate = newVal;
|
||||
this.updateIndeterminateState(newVal);
|
||||
// Indeterminate nur setzen, wenn checked false ist
|
||||
this.internalIndeterminate = newVal && !this.internalChecked;
|
||||
this.updateIndeterminateState(this.internalIndeterminate);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateIndeterminateState(this.isIndeterminate);
|
||||
// Beim Mount: checked hat Priorität über indeterminate
|
||||
if (this.internalChecked) {
|
||||
this.internalIndeterminate = false;
|
||||
}
|
||||
this.updateIndeterminateState(this.internalIndeterminate);
|
||||
},
|
||||
methods: {
|
||||
focus() {
|
||||
this.$refs.checkboxInput?.focus();
|
||||
},
|
||||
setFilter(event) {
|
||||
handleChange(event) {
|
||||
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) {
|
||||
if (this.$refs.checkboxInput) {
|
||||
|
|
@ -88,8 +92,8 @@ export default{
|
|||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
|
||||
<style>
|
||||
.checkbox-container {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,12 @@
|
|||
:style="modalAddStyle"
|
||||
>
|
||||
<div class="modal-container">
|
||||
<box @click.stop class="modal-box">
|
||||
<box
|
||||
@click.stop
|
||||
class="modal-box"
|
||||
@mouseenter="onModalMouseEnter"
|
||||
@mouseleave="onModalMouseLeave"
|
||||
>
|
||||
<slot></slot>
|
||||
</box>
|
||||
</div>
|
||||
|
|
@ -70,6 +75,11 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
preventScroll: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.isVisible) {
|
||||
this.handleOpen();
|
||||
|
|
@ -83,6 +93,20 @@ export default {
|
|||
this.$emit('close');
|
||||
},
|
||||
handleOpen() {
|
||||
// Prevent scroll via event listener
|
||||
this.preventScroll = (e) => {
|
||||
// Allow scrolling when mouse is over modal
|
||||
if (this.isMouseOverModal) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
// Prevent scroll, touch events and keyboard scrolling
|
||||
document.addEventListener('wheel', this.preventScroll, { passive: false });
|
||||
document.addEventListener('touchmove', this.preventScroll, { passive: false });
|
||||
document.addEventListener('keydown', this.preventScrollKeys, { passive: false });
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.modalOverlay) {
|
||||
this.$refs.modalOverlay.focus();
|
||||
|
|
@ -90,6 +114,31 @@ export default {
|
|||
});
|
||||
},
|
||||
handleClose() {
|
||||
// Re-enable scrolling
|
||||
if (this.preventScroll) {
|
||||
document.removeEventListener('wheel', this.preventScroll);
|
||||
document.removeEventListener('touchmove', this.preventScroll);
|
||||
document.removeEventListener('keydown', this.preventScrollKeys);
|
||||
}
|
||||
this.isMouseOverModal = false;
|
||||
},
|
||||
preventScrollKeys(e) {
|
||||
// Check if focus is inside modal
|
||||
if (this.$refs.modalOverlay && this.$refs.modalOverlay.contains(document.activeElement)) {
|
||||
return; // Allow keyboard scrolling inside modal
|
||||
}
|
||||
|
||||
// Prevent scrolling via keyboard (arrow keys, space, 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;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100vw; /* Statt right: 0 */
|
||||
height: 100vh; /* Statt bottom: 0 */
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
class="sort-button"
|
||||
:class="{ 'active': active }"
|
||||
>
|
||||
<PhArrowCircleUp weight="fill"
|
||||
:size="24"
|
||||
<PhCaretUp weight="fill"
|
||||
:size="16"
|
||||
class="sort-icon"
|
||||
:class="{ 'rotate': direction === 'asc' }"
|
||||
/>
|
||||
|
|
@ -13,11 +13,12 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import {PhArrowCircleUp, PhFunnelSimple} from '@phosphor-icons/vue';
|
||||
import {PhArrowCircleUp, PhCaretUp, PhFunnelSimple} from '@phosphor-icons/vue';
|
||||
|
||||
export default {
|
||||
name: "SortButton",
|
||||
components: {
|
||||
PhCaretUp,
|
||||
PhArrowCircleUp,
|
||||
PhFunnelSimple
|
||||
},
|
||||
|
|
|
|||
|
|
@ -138,17 +138,18 @@ export default {
|
|||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
flex: 1; /* Take remaining space */
|
||||
min-height: 0; /* Allow shrinking */
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab-pane {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
<div class="bulk-edit-row__cell-container">
|
||||
<div
|
||||
class="bulk-edit-row__cell bulk-edit-row__cell--status bulk-edit-row__cell--clickable bulk-edit-row__cell--filterable"
|
||||
class="bulk-edit-row__cell bulk-edit-row__cell--status bulk-edit-row__cell--clickable bulk-edit-row__cell--filterable edit-calculation-list-header-cell--copyable"
|
||||
@click="action($event,'material')"
|
||||
@mousedown="handleMouseDown">
|
||||
<div class="bulk-edit-row__data">
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
</div>
|
||||
|
||||
<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')">
|
||||
<div class="bulk-edit-row__data">
|
||||
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
</div>
|
||||
|
||||
<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')">
|
||||
<div class="bulk-edit-row__data">
|
||||
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
||||
|
|
@ -111,7 +111,7 @@
|
|||
@click="action($event,'amount')">
|
||||
<div class="bulk-edit-row__data bulk-edit-row__data--destinations">
|
||||
<div class="bulk-edit-row__dest-line"
|
||||
v-for="(destination, index) in premise.destinations.slice(0, 3)"
|
||||
v-for="(destination, index) in destinations.slice(0, 3)"
|
||||
:key="index">
|
||||
<div>
|
||||
<ph-stack size="16"/>
|
||||
|
|
@ -121,7 +121,7 @@
|
|||
<basic-badge size="compact" variant="secondary">{{ toDestination(destination) }}</basic-badge>
|
||||
</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> more ...</div>
|
||||
<div></div>
|
||||
|
|
@ -129,9 +129,9 @@
|
|||
</div>
|
||||
|
||||
<!-- 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"
|
||||
v-for="(destination, index) in premise.destinations"
|
||||
v-for="(destination, index) in destinations"
|
||||
:key="index">
|
||||
<div>
|
||||
<ph-stack size="16"/>
|
||||
|
|
@ -159,7 +159,7 @@
|
|||
@click="action($event,'routes')">
|
||||
<div class="bulk-edit-row__data">
|
||||
<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">
|
||||
<div>
|
||||
<component :is="toRouteIcon(destination)" size="16"></component>
|
||||
|
|
@ -169,7 +169,7 @@
|
|||
<basic-badge size="compact" variant="secondary">{{ toDestination(destination) }}</basic-badge>
|
||||
</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> more ...</div>
|
||||
<div></div>
|
||||
|
|
@ -177,9 +177,9 @@
|
|||
</div>
|
||||
|
||||
<!-- 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"
|
||||
v-for="(destination, index) in premise.destinations"
|
||||
v-for="(destination, index) in destinations"
|
||||
:key="index">
|
||||
<div>
|
||||
<component :is="toRouteIcon(destination)" size="16"></component>
|
||||
|
|
@ -193,9 +193,9 @@
|
|||
|
||||
<div class="bulk-edit-row__status">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -228,6 +228,7 @@ import {
|
|||
} from "@phosphor-icons/vue";
|
||||
import {UrlSafeBase64} from "@/common.js";
|
||||
import CircleBadge from "@/components/UI/CircleBadge.vue";
|
||||
import {useDestinationEditStore} from "@/store/destinationEdit.js";
|
||||
|
||||
export default {
|
||||
name: "BulkEditRow",
|
||||
|
|
@ -260,11 +261,6 @@ export default {
|
|||
},
|
||||
data() {
|
||||
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
|
||||
showMaterialCheck: false,
|
||||
showPriceCheck: false,
|
||||
|
|
@ -273,6 +269,8 @@ export default {
|
|||
showRouteCheck: false,
|
||||
// Flag to track if component has been initialized
|
||||
isInitialized: false,
|
||||
// Store the initial state to prevent false triggers on mount
|
||||
initialCheckStates: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -286,57 +284,110 @@ export default {
|
|||
return this.premise.handling_unit;
|
||||
},
|
||||
packagingCheck() {
|
||||
return this.hu?.length != null && this.hu?.width != null && this.hu?.height != null && this.hu?.weight != null && this.hu?.content_unit_count;
|
||||
return this.hu?.length != null
|
||||
&& this.hu?.width != null
|
||||
&& this.hu?.height != null
|
||||
&& this.hu?.weight != null
|
||||
&& this.hu?.content_unit_count != null;
|
||||
},
|
||||
destinationCheck() {
|
||||
|
||||
if (((this.premise?.destinations ?? null) === null) || this.premise?.destinations.length === 0)
|
||||
if (((this.destinations ?? null) === null) || this.destinations.length === 0)
|
||||
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() {
|
||||
return this.premiseEditStore.isChecked(this.id);
|
||||
},
|
||||
...mapStores(usePremiseEditStore),
|
||||
destinations() {
|
||||
return this.destinationEditStore.getByPremiseId(this.id) ?? [];
|
||||
},
|
||||
...mapStores(usePremiseEditStore, useDestinationEditStore),
|
||||
},
|
||||
watch: {
|
||||
materialCheck(newVal, oldVal) {
|
||||
if (this.isInitialized && oldVal === false && newVal === true) {
|
||||
if (this.isInitialized
|
||||
&& oldVal === false
|
||||
&& newVal === true
|
||||
&& this.initialCheckStates?.material !== true) {
|
||||
this.showMaterialCheck = true;
|
||||
// Reset initial state after first valid transition
|
||||
if (this.initialCheckStates) {
|
||||
this.initialCheckStates.material = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
priceCheck(newVal, oldVal) {
|
||||
if (this.isInitialized && oldVal === false && newVal === true) {
|
||||
if (this.isInitialized
|
||||
&& oldVal === false
|
||||
&& newVal === true
|
||||
&& this.initialCheckStates?.price !== true) {
|
||||
this.showPriceCheck = true;
|
||||
if (this.initialCheckStates) {
|
||||
this.initialCheckStates.price = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
packagingCheck(newVal, oldVal) {
|
||||
if (this.isInitialized && oldVal === false && newVal === true) {
|
||||
if (this.isInitialized
|
||||
&& oldVal === false
|
||||
&& newVal === true
|
||||
&& this.initialCheckStates?.packaging !== true) {
|
||||
this.showPackagingCheck = true;
|
||||
if (this.initialCheckStates) {
|
||||
this.initialCheckStates.packaging = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
destinationCheck(newVal, oldVal) {
|
||||
if (this.isInitialized && oldVal === false && newVal === true) {
|
||||
if (this.isInitialized
|
||||
&& oldVal === false
|
||||
&& newVal === true
|
||||
&& this.initialCheckStates?.destination !== 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;
|
||||
if (this.initialCheckStates) {
|
||||
this.initialCheckStates.route = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// Initialize previous states after first render
|
||||
// Use nextTick to ensure computed properties are evaluated
|
||||
// Capture initial states BEFORE setting isInitialized
|
||||
// This prevents the watchers from triggering on mount
|
||||
this.initialCheckStates = {
|
||||
material: this.materialCheck,
|
||||
price: this.priceCheck,
|
||||
packaging: this.packagingCheck,
|
||||
destination: this.destinationCheck,
|
||||
route: this.routeCheck,
|
||||
};
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.prevMaterialCheck = this.materialCheck;
|
||||
this.prevPriceCheck = this.priceCheck;
|
||||
this.prevPackagingCheck = this.packagingCheck;
|
||||
this.prevDestinationCheck = this.destinationCheck;
|
||||
this.isInitialized = true;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
toDestination(destination, limit = 10) {
|
||||
toDestination(destination, limit = 15) {
|
||||
return this.toNode(destination.destination_node, limit);
|
||||
},
|
||||
toNode(node, limit = 5) {
|
||||
|
|
|
|||
|
|
@ -98,8 +98,6 @@ export default {
|
|||
},
|
||||
editDestination(id) {
|
||||
|
||||
logger.log(id);
|
||||
|
||||
if (id) {
|
||||
const destination = this.premiseSingleEditStore.getDestinationById(id);
|
||||
logger.log(destination);
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export default {
|
|||
return this.route.is_fastest;
|
||||
},
|
||||
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;
|
||||
},
|
||||
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>
|
||||
<div class="destination-edit-container">
|
||||
<tab-container :default-tab="defaultTab" :tabs="tabsConfig" class="tab-container">
|
||||
</tab-container>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import TabContainer from "@/components/UI/TabContainer.vue";
|
||||
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||
import {markRaw} from "vue";
|
||||
import DestinationMassQuantity from "@/components/layout/edit/destination/mass/DestinationMassQuantity.vue";
|
||||
import DestinationMassRoute from "@/components/layout/edit/destination/mass/DestinationMassRoute.vue";
|
||||
import DestinationMassHandlingCost from "@/components/layout/edit/destination/mass/DestinationMassHandlingCost.vue";
|
||||
|
||||
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>
|
||||
.tab-container {
|
||||
flex: 1;
|
||||
min-height: 0; /* Critical: allows flex child to shrink below content size */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.destination-edit-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
min-width: 100rem;
|
||||
width: 90vw;
|
||||
height: 70vh; /* Feste Höhe statt max-height */
|
||||
min-height: 50rem; /* Mindesthöhe für kleine Bildschirme */
|
||||
max-height: 90rem; /* Maximale Höhe für große Bildschirme */
|
||||
}
|
||||
|
||||
|
||||
.destination-edit-modal-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
flex: 1 0 min(60vw, 120rem);
|
||||
height: min(60vh, 120rem);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.destination-edit-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1.6rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
|
@ -1,15 +1,446 @@
|
|||
<script lang="ts">
|
||||
import {defineComponent} from 'vue'
|
||||
<template>
|
||||
<div class="dest-mass-handling-container"
|
||||
:class="{ 'has-selection': hasSelection, 'apply-filter': applyFilter, 'add-all': addAll }">
|
||||
<div>
|
||||
<div class="destination-mass-handling-cost-info">
|
||||
<ph-warning size="18px"></ph-warning>
|
||||
Handling and repackaging costs are calculated automatically.
|
||||
If needed, you can overwrite these values here.
|
||||
</div>
|
||||
</div>
|
||||
<div class="destination-mass-handling-checkbox">
|
||||
<checkbox :checked="handlingCostActive" @checkbox-changed="activateInputFields">I want to enter handling and
|
||||
repackaging costs manually.
|
||||
</checkbox>
|
||||
</div>
|
||||
<div class="dest-mass-handling-table-wrapper" v-if="handlingCostActive">
|
||||
<div class="dest-mass-handling-table-header">
|
||||
<div class="dest-mass-handling-table-header-checkbox">
|
||||
<checkbox @checkbox-changed="updateCheckBoxes" :checked="overallCheck"
|
||||
:indeterminate="overallIndeterminate"></checkbox>
|
||||
</div>
|
||||
<div class="dest-mass-handling-table-header-material">Material</div>
|
||||
<div class="dest-mass-handling-table-header-supplier">Supplier</div>
|
||||
<div class="dest-mass-handling-table-header-destination">Destination</div>
|
||||
<div class="dest-mass-handling-table-header-applier">
|
||||
<icon-button icon="check" :disabled="!someChecked" @click="updateOverallValue"></icon-button>
|
||||
</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({
|
||||
name: "DestinationMassHandlingCost"
|
||||
})
|
||||
<script>
|
||||
import {mapStores} from "pinia";
|
||||
import {useDestinationEditStore} from "@/store/destinationEdit.js";
|
||||
import {usePremiseEditStore} from "@/store/premiseEdit.js";
|
||||
import DestinationMassQuantityRow from "@/components/layout/edit/destination/mass/DestinationMassQuantityRow.vue";
|
||||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import DestinationMassHandlingCostRow
|
||||
from "@/components/layout/edit/destination/mass/DestinationMassHandlingCostRow.vue";
|
||||
import {parseNumberFromString} from "@/common.js";
|
||||
|
||||
export default {
|
||||
name: "DestinationMassHandlingCost",
|
||||
components: {DestinationMassHandlingCostRow, IconButton, Checkbox, DestinationMassQuantityRow},
|
||||
props: {
|
||||
premiseIds: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<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>
|
||||
|
|
@ -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">
|
||||
import {defineComponent} from 'vue'
|
||||
<template>
|
||||
<div class="dest-mass-quantity-container"
|
||||
:class="{ 'has-selection': hasSelection, 'apply-filter': applyFilter, 'add-all': addAll }">
|
||||
<div class="dest-mass-quantity-table-wrapper">
|
||||
<div class="dest-mass-quantity-table-header">
|
||||
<div class="dest-mass-quantity-table-header-checkbox">
|
||||
<checkbox @checkbox-changed="updateCheckBoxes" :checked="overallCheck"
|
||||
:indeterminate="overallIndeterminate"></checkbox>
|
||||
</div>
|
||||
<div class="dest-mass-quantity-table-header-material">Material</div>
|
||||
<div class="dest-mass-quantity-table-header-supplier">Supplier</div>
|
||||
<div class="dest-mass-quantity-table-header-applier">
|
||||
<icon-button icon="check" :disabled="!someChecked" @click="updateOverallValue"></icon-button></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({
|
||||
name: "DestinationMassQuantity"
|
||||
})
|
||||
<script>
|
||||
|
||||
import {useDestinationEditStore} from "@/store/destinationEdit.js";
|
||||
import {mapStores} from "pinia";
|
||||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
import DestMassCreateRow from "@/components/layout/edit/destination/mass/DestMassCreateRow.vue";
|
||||
import DestinationMassQuantityRow from "@/components/layout/edit/destination/mass/DestinationMassQuantityRow.vue";
|
||||
import {usePremiseEditStore} from "@/store/premiseEdit.js";
|
||||
import BulkEditRow from "@/components/layout/bulkedit/BulkEditRow.vue";
|
||||
import {toRaw} from "vue";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||
import {parseNumberFromString} from "@/common.js";
|
||||
|
||||
export default {
|
||||
name: "DestinationMassQuantity",
|
||||
components: {BasicButton, IconButton, BulkEditRow, DestinationMassQuantityRow, DestMassCreateRow, Checkbox},
|
||||
props: {
|
||||
premiseIds: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<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>
|
||||
|
|
@ -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">
|
||||
import {defineComponent} from 'vue'
|
||||
<template>
|
||||
<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({
|
||||
name: "DestinationMassRoute"
|
||||
})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import DestinationMassQuantityRow from "@/components/layout/edit/destination/mass/DestinationMassQuantityRow.vue";
|
||||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {useDestinationEditStore} from "@/store/destinationEdit.js";
|
||||
import {usePremiseEditStore} from "@/store/premiseEdit.js";
|
||||
import {toRaw} from "vue";
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@
|
|||
<div class="header-container">
|
||||
<h2 class="page-header">Mass edit calculation</h2>
|
||||
<div class="header-controls">
|
||||
<basic-button :show-icon="false"
|
||||
<basic-button :show-icon="true"
|
||||
:disabled="disableButtons"
|
||||
variant="secondary"
|
||||
@click="close"
|
||||
>Close
|
||||
icon="MapPin" variant="primary"
|
||||
@click="destMgmt"
|
||||
>Destination manager
|
||||
</basic-button>
|
||||
<basic-button :show-icon="true"
|
||||
:disabled="disableButtons"
|
||||
|
|
@ -16,6 +16,12 @@
|
|||
@click="calculate"
|
||||
>Calculate & close
|
||||
</basic-button>
|
||||
<basic-button :show-icon="false"
|
||||
:disabled="disableButtons"
|
||||
variant="secondary"
|
||||
@click="close"
|
||||
>Close
|
||||
</basic-button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -24,17 +30,21 @@
|
|||
<div class="edit-calculation-list-header">
|
||||
<div>
|
||||
<checkbox @checkbox-changed="updateCheckBoxes" :checked="overallCheck"
|
||||
:indeterminate="overallIndeterminate"></checkbox>
|
||||
:indeterminate="overallIndeterminate" :disabled="!showData"></checkbox>
|
||||
</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'"
|
||||
:direction="premiseEditStore.directionSort('material')" @click="premiseEditStore.sort('material')"/>
|
||||
:direction="premiseEditStore.directionSort('material')"/>
|
||||
</div>
|
||||
<div class="edit-calculation-list-header-cell">Price</div>
|
||||
<div class="edit-calculation-list-header-cell">Packaging</div>
|
||||
<div class="edit-calculation-list-header-cell">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'"
|
||||
:direction="premiseEditStore.directionSort('supplier')" @click="premiseEditStore.sort('supplier')"/>
|
||||
:direction="premiseEditStore.directionSort('supplier')"/>
|
||||
</div>
|
||||
<div class="edit-calculation-list-header-cell">Annual Quantity</div>
|
||||
<div class="edit-calculation-list-header-cell">Routes</div>
|
||||
|
|
@ -78,10 +88,18 @@
|
|||
: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">
|
||||
<div class="modal-content-container">
|
||||
<h3 class="sub-header">{{ modalTitle }}</h3>
|
||||
<component
|
||||
:is="modalComponentType"
|
||||
ref="modalComponent"
|
||||
|
||||
|
||||
v-model:partNumber="modalProps.partNumber"
|
||||
v-model:hsCode="modalProps.hsCode"
|
||||
v-model:tariffRate="modalProps.tariffRate"
|
||||
|
|
@ -104,6 +122,9 @@
|
|||
|
||||
v-model:hideDescription="modalProps.hideDescription"
|
||||
|
||||
:type="modalType"
|
||||
:premiseIds="editIds"
|
||||
|
||||
:fromMassEdit="true"
|
||||
:countryId=null
|
||||
:responsive="false"
|
||||
|
|
@ -147,18 +168,25 @@ import PackagingEdit from "@/components/layout/edit/PackagingEdit.vue";
|
|||
import {useNotificationStore} from "@/store/notification.js";
|
||||
import {useDestinationEditStore} from "@/store/destinationEdit.js";
|
||||
import SortButton from "@/components/UI/SortButton.vue";
|
||||
import DestinationMassEdit from "@/components/layout/edit/destination/mass/DestinationMassEdit.vue";
|
||||
import DestMassCreate from "@/components/layout/edit/destination/mass/DestMassCreate.vue";
|
||||
import ModalDialog from "@/components/UI/ModalDialog.vue";
|
||||
import destinationEdit from "@/components/layout/edit/destination/DestinationEdit.vue";
|
||||
|
||||
|
||||
const COMPONENT_TYPES = {
|
||||
price: PriceEdit,
|
||||
material: MaterialEdit,
|
||||
packaging: PackagingEdit,
|
||||
destinations: null,
|
||||
destinations: DestMassCreate,
|
||||
routes: DestinationMassEdit,
|
||||
amount: DestinationMassEdit
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "MassEdit",
|
||||
components: {
|
||||
ModalDialog,
|
||||
SortButton,
|
||||
Modal,
|
||||
MassEditDialog,
|
||||
|
|
@ -169,6 +197,26 @@ export default {
|
|||
BulkEditRow,
|
||||
BasicButton
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ids: [],
|
||||
isCtrlPressed: false,
|
||||
isShiftPressed: false,
|
||||
overallCheck: false,
|
||||
overallIndeterminate: false,
|
||||
bulkQuery: null,
|
||||
modalTitle: null,
|
||||
modalType: null,
|
||||
modalProps: null,
|
||||
editIds: null,
|
||||
processingMessage: "Please wait. Calculating ...",
|
||||
showCalculationModal: false,
|
||||
isInitialLoad: true,
|
||||
modalDialogShow: false,
|
||||
modalStash: null,
|
||||
denyText: 'No'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(usePremiseEditStore, useNotificationStore, useDestinationEditStore),
|
||||
disableButtons() {
|
||||
|
|
@ -215,7 +263,7 @@ export default {
|
|||
return this.modalType ? COMPONENT_TYPES[this.modalType] : null;
|
||||
},
|
||||
showProcessingModal() {
|
||||
return this.premiseEditStore.showProcessingModal || this.showCalculationModal;
|
||||
return this.premiseEditStore.showProcessingModal || this.destinationEditStore.showProcessingModal ;
|
||||
},
|
||||
shownProcessingMessage() {
|
||||
return this.processingMessage;
|
||||
|
|
@ -237,30 +285,11 @@ export default {
|
|||
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() {
|
||||
console.log("add listener")
|
||||
window.addEventListener('keydown', this.handleKeyDown);
|
||||
window.addEventListener('keyup', this.handleKeyUp);
|
||||
},
|
||||
beforeUnmount() {
|
||||
console.log("remove listener")
|
||||
window.removeEventListener('keydown', this.handleKeyDown);
|
||||
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() {
|
||||
this.showCalculationModal = true;
|
||||
const error = await this.premiseEditStore.startCalculation();
|
||||
|
|
@ -345,7 +380,7 @@ export default {
|
|||
if (actions.length === 1) {
|
||||
const massEdit = 0 !== this.selectCount;
|
||||
this.openModal(data.action, massEdit ? this.premiseEditStore.getSelectedPremiseIds : [data.id], data.id, massEdit);
|
||||
} else if (actions.length === 2) {
|
||||
} else if (actions.length === 2) { /* ctrl or ctrl + shift */
|
||||
this.premiseEditStore.setBy(actions[0], actions[1], data.id);
|
||||
this.updateOverallCheckBox();
|
||||
}
|
||||
|
|
@ -353,37 +388,90 @@ export default {
|
|||
|
||||
/* modal handling */
|
||||
|
||||
openModal(type, ids, dataSource = -1, massEdit = true) {
|
||||
|
||||
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
|
||||
modalDialogClick(action) {
|
||||
|
||||
this.modalDialogShow = false;
|
||||
|
||||
if (action === 'dismiss') {
|
||||
this.modalStash = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.dataSourceId = dataSource !== -1 ? dataSource : null;
|
||||
if (action === 'deny') {
|
||||
this.openModal(this.modalStash.type, this.modalStash.ids, this.modalStash.dataSource, this.modalStash.massEdit)
|
||||
this.modalStash = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.destMgmt();
|
||||
|
||||
|
||||
},
|
||||
|
||||
openModal(type, ids, dataSource = -1, massEdit = true) {
|
||||
|
||||
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) {
|
||||
if (this.modalType === "destinations") {
|
||||
//TODO new destination handling
|
||||
|
||||
} else if (action === "accept") {
|
||||
await this.premiseEditStore.batchUpdate(this.modalType, this.editIds, this.modalProps);
|
||||
if (this.modalType === "destinations") {
|
||||
|
||||
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") {
|
||||
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;
|
||||
}
|
||||
},
|
||||
fillData(type, id = -1, hideDescription = false) {
|
||||
|
||||
|
|
@ -415,9 +503,15 @@ export default {
|
|||
stackable: true
|
||||
};
|
||||
|
||||
if (type === 'amount' || type === 'routes' || type === 'destinations')
|
||||
this.modalProps = {};
|
||||
|
||||
|
||||
} else {
|
||||
const premise = this.premiseEditStore.getById(id);
|
||||
|
||||
this.modalTitle = "Edit ".concat(type);
|
||||
|
||||
if (type === "price") {
|
||||
this.modalProps = {
|
||||
price: premise.material_cost,
|
||||
|
|
@ -447,9 +541,14 @@ export default {
|
|||
mixable: premise.is_mixable ?? true,
|
||||
stackable: premise.is_stackable ?? true
|
||||
}
|
||||
} else if (type === 'amount' || type === 'routes' || type === 'destinations') {
|
||||
this.modalTitle = "Edit destinations";
|
||||
this.modalProps = {type: type};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
,
|
||||
|
||||
/* Animation hooks */
|
||||
|
||||
|
|
@ -458,7 +557,8 @@ export default {
|
|||
el.style.opacity = 0;
|
||||
el.style.transform = 'translateY(2rem)';
|
||||
}
|
||||
},
|
||||
}
|
||||
,
|
||||
onEnter(el, done) {
|
||||
if (this.isInitialLoad) {
|
||||
const index = parseInt(el.dataset.index) || 0;
|
||||
|
|
@ -491,8 +591,13 @@ export default {
|
|||
</script>
|
||||
<style scoped>
|
||||
|
||||
.sub-header {
|
||||
flex-shrink: 0; /* Prevent header from shrinking */
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
|
||||
/* Global style für copy-mode cursor */
|
||||
.edit-calculation-container.has-selection :deep(.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;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 0.8rem;
|
||||
|
|
@ -521,8 +626,6 @@ export default {
|
|||
.modal-content-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
margin-top: 1.6rem;
|
||||
min-width: 50rem;
|
||||
}
|
||||
|
||||
|
|
@ -593,6 +696,18 @@ export default {
|
|||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.edit-calculation-list-header-cell--copyable {
|
||||
}
|
||||
|
||||
.edit-calculation-list-header-cell--clickable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.edit-calculation-list-header-cell--selected {
|
||||
color: #002F54;
|
||||
}
|
||||
|
||||
.edit-calculation-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -174,11 +174,10 @@ export default {
|
|||
},
|
||||
close() {
|
||||
if (this.bulkEditQuery) {
|
||||
//TODO: deselect and save
|
||||
// this.premiseEditStore.deselectPremise();
|
||||
//TODO: deselect element and save
|
||||
this.$router.push({name: 'bulk', params: {ids: this.bulkEditQuery}});
|
||||
} else {
|
||||
//TODO: deselect and save
|
||||
//TODO: deselect element and save
|
||||
this.$router.push({name: 'home'});
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,14 +1,53 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import {toRaw} from "vue";
|
||||
import performRequest from "@/backend.js";
|
||||
import {config} from '@/config'
|
||||
|
||||
export const useDestinationEditStore = defineStore('destinationEdit', {
|
||||
state: () => ({
|
||||
destinations: null,
|
||||
loading: false,
|
||||
}),
|
||||
getters: {},
|
||||
actions: {
|
||||
getters: {
|
||||
checkDestinationAssignment(state) {
|
||||
return (ids) => {
|
||||
let some = false;
|
||||
let all = true;
|
||||
|
||||
ids.forEach(id => {
|
||||
const dest = state.destinations?.get(id);
|
||||
|
||||
if ((dest ?? null) === null || dest.length === 0)
|
||||
all = false;
|
||||
else
|
||||
some = true;
|
||||
|
||||
});
|
||||
|
||||
if (all)
|
||||
return "all";
|
||||
else if (some)
|
||||
return "some";
|
||||
else
|
||||
return "none";
|
||||
}
|
||||
},
|
||||
getByPremiseId(state) {
|
||||
return (id) => {
|
||||
return state.destinations?.get(id);
|
||||
}
|
||||
},
|
||||
getByPremiseIds(state) {
|
||||
return (ids) => {
|
||||
return new Map(
|
||||
[...state.destinations].filter(([premiseId, destinations]) => ids.includes(premiseId))
|
||||
);
|
||||
}
|
||||
},
|
||||
showProcessingModal(state) {
|
||||
return state.loading;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setupDestinations(premisses) {
|
||||
this.loading = true;
|
||||
|
||||
|
|
@ -18,312 +57,39 @@ export const useDestinationEditStore = defineStore('destinationEdit', {
|
|||
|
||||
this.loading = false;
|
||||
},
|
||||
async massSetDestinations(updateMatrix) {
|
||||
this.loading = true;
|
||||
|
||||
const toBeAdded = {};
|
||||
const toBeDeletedMap = new Map();
|
||||
|
||||
/**
|
||||
* DESTINATION stuff
|
||||
* =================
|
||||
*/
|
||||
|
||||
prepareDestinations(dataSourcePremiseId, editedPremiseIds, massEdit = false, fromMassEditView = false) {
|
||||
if (this.premisses === null) return;
|
||||
if (!editedPremiseIds || !dataSourcePremiseId || editedPremiseIds.length === 0) return;
|
||||
|
||||
this.destinations = {
|
||||
premise_ids: editedPremiseIds,
|
||||
massEdit: massEdit,
|
||||
fromMassEditView: fromMassEditView,
|
||||
destinations: this.premisses.find(p => String(p.id) === String(dataSourcePremiseId))?.destinations.map(d => this.copyAllFromPremises(d, !massEdit)) ?? [],
|
||||
};
|
||||
|
||||
this.selectedDestination = null;
|
||||
|
||||
},
|
||||
async executeDestinationsMassEdit() {
|
||||
|
||||
if (!this.destinations.massEdit) {
|
||||
|
||||
this.destinations.premise_ids.forEach(premiseId => {
|
||||
const toPremise = this.getById(premiseId);
|
||||
|
||||
this.destinations.destinations.forEach(fromDest => {
|
||||
const toDest = toPremise.destinations.find(to => fromDest.id.substring(1) === String(to.id));
|
||||
|
||||
if ((toDest ?? null) === null) {
|
||||
throw new Error("Destination not found in premise: " + premiseId + " -> " + d.id);
|
||||
}
|
||||
|
||||
this.copyAllToPremise(fromDest, toDest);
|
||||
|
||||
const body = {
|
||||
annual_amount: toDest.annual_amount,
|
||||
repackaging_costs: toDest.repackaging_costs,
|
||||
handling_costs: toDest.handling_costs,
|
||||
disposal_costs: toDest.disposal_costs,
|
||||
is_d2d: toDest.is_d2d,
|
||||
rate_d2d: toDest.rate_d2d,
|
||||
lead_time_d2d: toDest.lead_time_d2d,
|
||||
route_selected_id: toDest.routes.find(r => r.is_selected)?.id ?? null,
|
||||
};
|
||||
|
||||
const url = `${config.backendUrl}/calculation/destination/${toDest.id}`;
|
||||
performRequest(this, 'PUT', url, body, false);
|
||||
|
||||
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]]);
|
||||
});
|
||||
|
||||
|
||||
} 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;
|
||||
}
|
||||
},
|
||||
async massUpdateDestinations(updateMatrix) {
|
||||
this.loading = true;
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
}
|
||||
});
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import {config} from '@/config'
|
||||
import {toRaw} from "vue";
|
||||
import {useNotificationStore} from "@/store/notification.js";
|
||||
import logger from "@/logger.js"
|
||||
import performRequest from '@/backend.js'
|
||||
|
||||
|
|
@ -164,7 +162,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
|
||||
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();
|
||||
temp.sort((a, b) => {
|
||||
|
|
@ -179,7 +177,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
else return a.id - b.id;
|
||||
});
|
||||
|
||||
console.log("sort", this.sortedBy, direction, type);
|
||||
|
||||
this.premisses = temp;
|
||||
this.sortedBy = type;
|
||||
this.order.set(type, direction);
|
||||
|
|
|
|||
|
|
@ -90,9 +90,13 @@ export const usePremiseSingleEditStore = defineStore('premiseSingleEdit', {
|
|||
if (this.premise === null) return;
|
||||
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/`;
|
||||
|
||||
logger.info("addDestination", body, url);
|
||||
|
||||
const {data: destinations} = await performRequest(this, 'POST', url, body).catch(e => {
|
||||
this.routing = false;
|
||||
|
|
@ -101,9 +105,10 @@ export const usePremiseSingleEditStore = defineStore('premiseSingleEdit', {
|
|||
|
||||
const ids = []
|
||||
|
||||
for (const destId of Object.keys(destinations)) {
|
||||
this.premise.destinations.push(destinations[destId]);
|
||||
ids.push(destinations[destId].id);
|
||||
if (destinations[this.premise.id]?.length !== 0)
|
||||
for (const destId of Object.keys(destinations[this.premise.id])) {
|
||||
this.premise.destinations.push(destinations[this.premise.id][destId]);
|
||||
ids.push(destinations[this.premise.id][destId].id);
|
||||
}
|
||||
|
||||
this.routing = false;
|
||||
|
|
|
|||
|
|
@ -29,8 +29,6 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
|||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
|
@ -176,14 +174,14 @@ public class PremiseController {
|
|||
|
||||
@PostMapping({"/destination", "/destination/"})
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
|
||||
public ResponseEntity<Map<Integer, DestinationDTO>> createDestination(@RequestBody @Valid DestinationCreateDTO destinationCreateDTO) {
|
||||
return ResponseEntity.ok(destinationService.createDestination(destinationCreateDTO));
|
||||
public ResponseEntity<Map<Integer, List<DestinationDTO>>> createDestination(@RequestBody @Valid DestinationCreateDTO destinationCreateDTO) {
|
||||
return ResponseEntity.ok(destinationService.massSetDestinations(destinationCreateDTO));
|
||||
}
|
||||
|
||||
@PutMapping({"/destination", "/destination/"})
|
||||
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
|
||||
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}/"})
|
||||
|
|
|
|||
|
|
@ -6,32 +6,20 @@ import jakarta.validation.constraints.NotEmpty;
|
|||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class DestinationCreateDTO {
|
||||
|
||||
@NotEmpty(message = "At least one premise must be selected")
|
||||
@NotNull(message = "At least one premise must be selected")
|
||||
@JsonProperty("premise_id")
|
||||
List<@Min(value = 1, message = "Invalid premise id") Integer> premiseId;
|
||||
@JsonProperty("destination_node_ids")
|
||||
Map<Integer, List<@Min(value = 1, message = "Missing destination ids") Integer>> destinationNodeIds;
|
||||
|
||||
@Min(value = 1, message = "Invalid destination node id")
|
||||
@NotNull (message = "Destination node id must be provided")
|
||||
@JsonProperty("destination_node_id")
|
||||
Integer destinationNodeId;
|
||||
|
||||
public List<Integer> getPremiseId() {
|
||||
return premiseId;
|
||||
public Map<Integer, List<Integer>> getDestinationNodeIds() {
|
||||
return destinationNodeIds;
|
||||
}
|
||||
|
||||
public void setPremiseId(List<Integer> premiseId) {
|
||||
this.premiseId = premiseId;
|
||||
}
|
||||
|
||||
public Integer getDestinationNodeId() {
|
||||
return destinationNodeId;
|
||||
}
|
||||
|
||||
public void setDestinationNodeId(Integer destinationNodeId) {
|
||||
this.destinationNodeId = destinationNodeId;
|
||||
public void setDestinationNodeIds(Map<Integer, List<Integer>> destinationNodeIds) {
|
||||
this.destinationNodeIds = destinationNodeIds;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
package de.avatic.lcc.dto.calculation.edit.destination;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.Digits;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class DestinationSetDTO {
|
||||
|
||||
|
|
@ -15,6 +14,7 @@ public class DestinationSetDTO {
|
|||
@JsonProperty("destinations")
|
||||
List<DestinationSetListItemDTO> destinations;
|
||||
|
||||
|
||||
public List<Integer> getPremiseId() {
|
||||
return premiseId;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import java.sql.ResultSet;
|
|||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class DestinationRepository {
|
||||
|
|
@ -50,6 +51,21 @@ public class DestinationRepository {
|
|||
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
|
||||
public void update(Integer id, Integer annualAmount, BigDecimal repackingCost, BigDecimal disposalCost, BigDecimal handlingCost, Boolean isD2d, BigDecimal d2dRate, BigDecimal d2dLeadTime) {
|
||||
if (id == null) {
|
||||
|
|
@ -157,20 +173,73 @@ public class DestinationRepository {
|
|||
}, 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
|
||||
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 = ?";
|
||||
public Map<Integer, List<Destination>> getByPremiseIdsAndNodeIds(Map<Integer, List<Integer>> premiseToNodes, Integer userId) {
|
||||
if (premiseToNodes.isEmpty()) {
|
||||
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
|
||||
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;
|
||||
Object[] params = new Object[allPremiseIds.size() + allNodeIds.size() + 1];
|
||||
int index = 0;
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -3,14 +3,12 @@ package de.avatic.lcc.service.access;
|
|||
import de.avatic.lcc.dto.calculation.DestinationDTO;
|
||||
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.DestinationSetListItemDTO;
|
||||
import de.avatic.lcc.dto.calculation.edit.destination.DestinationUpdateDTO;
|
||||
import de.avatic.lcc.model.db.nodes.Node;
|
||||
import de.avatic.lcc.model.db.premises.Premise;
|
||||
import de.avatic.lcc.model.db.premises.route.*;
|
||||
import de.avatic.lcc.repositories.NodeRepository;
|
||||
import de.avatic.lcc.repositories.premise.*;
|
||||
import de.avatic.lcc.repositories.properties.PropertyRepository;
|
||||
import de.avatic.lcc.repositories.users.UserNodeRepository;
|
||||
import de.avatic.lcc.service.calculation.RoutingService;
|
||||
import de.avatic.lcc.service.transformer.premise.DestinationTransformer;
|
||||
|
|
@ -51,38 +49,19 @@ public class DestinationService {
|
|||
this.authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Map<Integer, List<DestinationDTO>> setDestination(DestinationSetDTO dto) {
|
||||
var admin = authorizationService.isSuper();
|
||||
Integer userId = authorizationService.getUserId();
|
||||
|
||||
if (!admin)
|
||||
destinationRepository.checkOwner(dto.getPremiseId(), userId);
|
||||
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) {
|
||||
|
||||
deleteAllDestinationsByPremiseId(dto.getPremiseId(), false);
|
||||
var destMap = new HashMap<Integer, List<Destination>>();
|
||||
|
||||
var premisses = premiseRepository.getPremisesById(dto.getPremiseId());
|
||||
Map<RouteIds, List<RouteInformation>> routes = findRoutes(premisses, dto.getDestinations().stream().map(DestinationSetListItemDTO::getDestinationNodeId).toList());
|
||||
|
||||
Map<Integer, List<Destination>> destinations = dto.getDestinations().stream()
|
||||
.flatMap(destination -> createDestination(premisses, destination.getDestinationNodeId(), destination.getAnnualAmount(), destination.getRepackingCost(), destination.getDisposalCost(), destination.getHandlingCost(), routes).stream())
|
||||
.collect(Collectors.groupingBy(Destination::getPremiseId));
|
||||
|
||||
|
||||
return destinations.entrySet().stream()
|
||||
.collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
entry -> entry.getValue().stream().map(destinationTransformer::toDestinationDTO).toList()));
|
||||
}
|
||||
|
||||
|
||||
private List<Destination> createDestination(List<Premise> premisesToProcess, Integer destinationNodeId, Integer annualAmount, Number repackingCost, Number disposalCost, Number handlingCost, Map<RouteIds, List<RouteInformation>> routes) {
|
||||
|
||||
Node destinationNode = nodeRepository.getById(destinationNodeId).orElseThrow();
|
||||
for (var premise : premisesToProcess) {
|
||||
|
||||
var destinations = new ArrayList<Destination>();
|
||||
|
||||
for (var premise : premisesToProcess) {
|
||||
for (var destinationNodeId : destinationNodeIds.get(premise.getId())) {
|
||||
|
||||
Node destinationNode = nodeRepository.getById(destinationNodeId).orElseThrow();
|
||||
|
||||
var destination = new Destination();
|
||||
destination.setDestinationNodeId(destinationNodeId);
|
||||
destination.setPremiseId(premise.getId());
|
||||
|
|
@ -100,39 +79,94 @@ public class DestinationService {
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
return destinations;
|
||||
destMap.put(premise.getId(), destinations);
|
||||
}
|
||||
|
||||
return destMap;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Map<Integer, DestinationDTO> createDestination(DestinationCreateDTO dto) {
|
||||
public Map<Integer, List<DestinationDTO>> massSetDestinationProperties(DestinationSetDTO dto) {
|
||||
var admin = authorizationService.isSuper();
|
||||
Integer userId = authorizationService.getUserId();
|
||||
|
||||
if (!admin)
|
||||
destinationRepository.checkOwner(dto.getPremiseId(), userId);
|
||||
|
||||
deleteAllDestinationsByPremiseId(dto.getPremiseId(), false);
|
||||
|
||||
var premisses = premiseRepository.getPremisesById(dto.getPremiseId());
|
||||
// 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()
|
||||
// .flatMap(destination -> processDestinations(premisses, destination.getDestinationNodeId(), destination.getAnnualAmount(), destination.getRepackingCost(), destination.getDisposalCost(), destination.getHandlingCost(), null).stream())
|
||||
// .collect(Collectors.groupingBy(Destination::getPremiseId));
|
||||
|
||||
|
||||
// return destinations.entrySet().stream()
|
||||
// .collect(Collectors.toMap(
|
||||
// Map.Entry::getKey,
|
||||
// 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<Integer> getNodeIdsToAdd(List<Destination> oldDestinations, List<Integer> newIds) {
|
||||
var oldIds = oldDestinations.stream().map(Destination::getDestinationNodeId).toList();
|
||||
return newIds.stream().filter(id -> !oldIds.contains(id)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Map<Integer, List<DestinationDTO>> massSetDestinations(DestinationCreateDTO dto) {
|
||||
|
||||
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>();
|
||||
for (var premiseId : dto.getPremiseId()) {
|
||||
if (existingDestinations.stream().map(Destination::getPremiseId).noneMatch(id -> id.equals(premiseId))) {
|
||||
premisesIdsToProcess.add(premiseId);
|
||||
}
|
||||
var requestedDestMap = dto.getDestinationNodeIds();
|
||||
var existingDestMap = dto.getDestinationNodeIds().keySet().stream().collect(Collectors.toMap(id -> id, id -> destinationRepository.getByPremiseIdAndUserId(id, userId)));
|
||||
|
||||
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<>();
|
||||
|
||||
var premisses = premiseRepository.getPremisesById(premisesIdsToProcess);
|
||||
Map<RouteIds, List<RouteInformation>> routes = findRoutes(premisses, Collections.singletonList(dto.getDestinationNodeId()));
|
||||
var premisses = premiseRepository.getPremisesById(new ArrayList<>(destinationsToProcess.keySet()));
|
||||
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) {
|
||||
|
|
@ -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<Integer, Node> nodes = new HashMap<>();
|
||||
Map<Integer, Node> userNodes = new HashMap<>();
|
||||
|
||||
for (var premise : premisses) {
|
||||
for (var destinationId : destinationIds) {
|
||||
|
||||
for (var destinationId : routingRequest.get(premise.getId())) {
|
||||
|
||||
boolean isUserSupplierNode = (premise.getSupplierNodeId() == null);
|
||||
var ids = new RouteIds(isUserSupplierNode ? premise.getUserSupplierNodeId() : premise.getSupplierNodeId(), destinationId, isUserSupplierNode);
|
||||
if (routes.containsKey(ids)) continue;
|
||||
|
|
@ -194,6 +230,8 @@ public class DestinationService {
|
|||
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));
|
||||
}
|
||||
}
|
||||
|
|
@ -255,6 +293,11 @@ public class DestinationService {
|
|||
destinations.forEach(destination -> deleteDestinationById(destination.getId(), deleteRoutesOnly));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteDestinationsById(List<Integer> ids, boolean deleteRoutesOnly) {
|
||||
ids.forEach(id -> deleteDestinationById(id, deleteRoutesOnly));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteDestinationById(Integer id, boolean deleteRoutesOnly) {
|
||||
var admin = authorizationService.isSuper();
|
||||
|
|
|
|||
|
|
@ -334,7 +334,7 @@ public class RoutingService {
|
|||
}
|
||||
|
||||
finalSection.setRate(matrixRate);
|
||||
finalSection.setApproxDistance(distanceService.getDistance(container.getSourceNode(), toNode, false));
|
||||
finalSection.setApproxDistance(distanceService.getDistance(container.getSourceNode(), toNode, true));
|
||||
rates.add(finalSection);
|
||||
}
|
||||
|
||||
|
|
@ -699,7 +699,7 @@ public class RoutingService {
|
|||
|
||||
if (matrixRate.isPresent()) {
|
||||
matrixRateObj.setRate(matrixRate.get());
|
||||
matrixRateObj.setApproxDistance(distanceService.getDistance(startNode, endNode, false));
|
||||
matrixRateObj.setApproxDistance(distanceService.getDistance(startNode, endNode, true));
|
||||
container.getRates().add(matrixRateObj);
|
||||
return matrixRateObj;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -17,9 +17,7 @@ import org.springframework.test.context.jdbc.Sql;
|
|||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
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 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")
|
||||
.content(objectMapper.writeValueAsString(createDto))
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
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 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")
|
||||
.content(objectMapper.writeValueAsString(dto))
|
||||
|
|
|
|||