Intermediate commit
This commit is contained in:
parent
27b56bc92d
commit
a40a8c6bb4
22 changed files with 1400 additions and 167 deletions
|
|
@ -4,7 +4,7 @@
|
|||
ref="trigger"
|
||||
class="dropdown-trigger"
|
||||
:class="{ 'dropdown-trigger--open': isOpen}"
|
||||
@click="toggleDropdown"
|
||||
@click.stop="toggleDropdown"
|
||||
@keydown="handleTriggerKeydown"
|
||||
:disabled="disabled"
|
||||
>
|
||||
|
|
@ -143,6 +143,9 @@ export default {
|
|||
return this.modelValue === option[this.valueKey]
|
||||
},
|
||||
handleClickOutside(event) {
|
||||
|
||||
console.log("HANDLE click outside")
|
||||
|
||||
if (!this.$refs.dropdown?.contains(event.target)) {
|
||||
this.closeDropdown()
|
||||
}
|
||||
|
|
|
|||
507
src/frontend/src/components/UI/RouteDropdown.vue
Normal file
507
src/frontend/src/components/UI/RouteDropdown.vue
Normal file
|
|
@ -0,0 +1,507 @@
|
|||
<template>
|
||||
<div class="route-dropdown" ref="dropdown">
|
||||
<button
|
||||
ref="trigger"
|
||||
class="route-dropdown-trigger"
|
||||
:class="{ 'route-dropdown-trigger--open': isOpen}"
|
||||
@click="toggleDropdown"
|
||||
@keydown="handleTriggerKeydown"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<span class="route-dropdown-trigger-content">
|
||||
<component
|
||||
v-if="selectedIcon"
|
||||
:is="selectedIcon"
|
||||
:size="16"
|
||||
class="route-icon"
|
||||
/>
|
||||
<span class="route-dropdown-trigger-text">
|
||||
{{ selectedDisplayText }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="route-dropdown-trigger-warning"><ph-warning-circle v-if="warnD2D" weight="fill"
|
||||
size="16"></ph-warning-circle></span>
|
||||
<svg
|
||||
class="route-dropdown-trigger-icon"
|
||||
:class="{ 'route-dropdown-trigger-icon--rotated': isOpen }"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="6,9 12,15 18,9"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<transition name="route-dropdown-fade">
|
||||
<ul
|
||||
v-if="isOpen"
|
||||
ref="menu"
|
||||
class="route-dropdown-menu"
|
||||
@keydown="handleMenuKeydown"
|
||||
tabindex="-1"
|
||||
>
|
||||
<!-- D2D Routing Option -->
|
||||
<li
|
||||
class="route-dropdown-option"
|
||||
:class="{
|
||||
'route-dropdown-option--selected': isD2DSelected,
|
||||
'route-dropdown-option--focused': focusedIndex === -1
|
||||
}"
|
||||
@click="selectD2D"
|
||||
@mouseenter="focusedIndex = -1"
|
||||
>
|
||||
<ph-shipping-container :size="16" class="route-icon"/>
|
||||
<span>D2D routing</span>
|
||||
</li>
|
||||
|
||||
<!-- Divider -->
|
||||
<li v-if="options.length > 0" class="route-dropdown-divider"></li>
|
||||
|
||||
<!-- Regular Route Options -->
|
||||
<li
|
||||
v-for="(option, index) in options"
|
||||
:key="option[valueKey]"
|
||||
class="route-dropdown-option"
|
||||
:class="{
|
||||
'route-dropdown-option--selected': isSelected(option),
|
||||
'route-dropdown-option--focused': focusedIndex === index
|
||||
}"
|
||||
@click="selectOption(option, $event)"
|
||||
@mouseenter="focusedIndex = index"
|
||||
>
|
||||
<component
|
||||
:is="getIconForType(option.type)"
|
||||
:size="16"
|
||||
class="route-icon"
|
||||
/>
|
||||
<span>{{ option[displayKey] }}</span>
|
||||
</li>
|
||||
|
||||
<li v-if="options.length === 0" class="route-dropdown-option route-dropdown-option--empty">
|
||||
{{ emptyText }}
|
||||
</li>
|
||||
</ul>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {PhTruck, PhBoat, PhTrain, PhShippingContainer, PhWarningCircle} from '@phosphor-icons/vue'
|
||||
|
||||
export default {
|
||||
name: 'RouteDropdown',
|
||||
components: {
|
||||
PhWarningCircle,
|
||||
PhTruck,
|
||||
PhBoat,
|
||||
PhTrain,
|
||||
PhShippingContainer
|
||||
},
|
||||
props: {
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number, Object],
|
||||
default: null
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Select a route'
|
||||
},
|
||||
displayKey: {
|
||||
type: String,
|
||||
default: 'routeDisplayString'
|
||||
},
|
||||
valueKey: {
|
||||
type: String,
|
||||
default: 'routeCompareString'
|
||||
},
|
||||
emptyText: {
|
||||
type: String,
|
||||
default: 'No routes available'
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showD2dWarn: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'change'],
|
||||
data() {
|
||||
return {
|
||||
isOpen: false,
|
||||
focusedIndex: -1,
|
||||
D2D_VALUE: 'D2D_ROUTING'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
warnD2D() {
|
||||
return this.isD2DSelected && this.showD2dWarn;
|
||||
},
|
||||
selectedOption() {
|
||||
if (!this.modelValue || this.modelValue === this.D2D_VALUE) return null
|
||||
return this.options?.find(option =>
|
||||
option[this.valueKey] === this.modelValue
|
||||
) ?? null
|
||||
},
|
||||
isD2DSelected() {
|
||||
return this.modelValue === this.D2D_VALUE
|
||||
},
|
||||
selectedDisplayText() {
|
||||
if (this.isD2DSelected) return 'D2D routing'
|
||||
if (this.selectedOption) return this.selectedOption[this.displayKey]
|
||||
return this.placeholder
|
||||
},
|
||||
selectedIcon() {
|
||||
if (this.isD2DSelected) return 'ph-shipping-container'
|
||||
if (this.selectedOption) return this.getIconForType(this.selectedOption.type)
|
||||
return null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isOpen(newVal) {
|
||||
if (newVal) {
|
||||
// Warte einen Tick, damit das Menü gerendert ist
|
||||
this.$nextTick(() => {
|
||||
document.addEventListener('click', this.handleClickOutside, true)
|
||||
})
|
||||
} else {
|
||||
document.removeEventListener('click', this.handleClickOutside, true)
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
document.removeEventListener('click', this.handleClickOutside, true)
|
||||
},
|
||||
methods: {
|
||||
getIconForType(type) {
|
||||
const iconMap = {
|
||||
'ROAD': 'ph-truck',
|
||||
'SEA': 'ph-boat',
|
||||
'RAIL': 'ph-train'
|
||||
}
|
||||
return iconMap[type] || null
|
||||
},
|
||||
toggleDropdown(event) {
|
||||
if (this.disabled) return
|
||||
|
||||
event.stopPropagation()
|
||||
this.isOpen = !this.isOpen
|
||||
|
||||
if (this.isOpen) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.menu?.focus()
|
||||
|
||||
if (this.isD2DSelected) {
|
||||
this.focusedIndex = -1
|
||||
} else if (this.selectedOption) {
|
||||
this.focusedIndex = this.options.findIndex(option =>
|
||||
option[this.valueKey] === this.modelValue
|
||||
)
|
||||
} else {
|
||||
this.focusedIndex = -1
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.focusedIndex = -1
|
||||
}
|
||||
},
|
||||
selectD2D(event) {
|
||||
event.stopPropagation()
|
||||
this.$emit('update:modelValue', this.D2D_VALUE)
|
||||
this.$emit('change', {type: 'D2D', value: this.D2D_VALUE})
|
||||
this.closeDropdown()
|
||||
this.$refs.trigger.focus()
|
||||
},
|
||||
selectOption(option, event) {
|
||||
event.stopPropagation()
|
||||
this.$emit('update:modelValue', option[this.valueKey])
|
||||
this.$emit('change', option)
|
||||
this.closeDropdown()
|
||||
this.$refs.trigger.focus()
|
||||
},
|
||||
closeDropdown() {
|
||||
this.isOpen = false
|
||||
this.focusedIndex = -1
|
||||
},
|
||||
isSelected(option) {
|
||||
return this.modelValue === option[this.valueKey]
|
||||
},
|
||||
handleClickOutside(event) {
|
||||
|
||||
if (!this.$refs.dropdown?.contains(event.target)) {
|
||||
this.closeDropdown()
|
||||
}
|
||||
},
|
||||
handleTriggerKeydown(event) {
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
case 'ArrowDown':
|
||||
event.preventDefault()
|
||||
if (!this.isOpen) {
|
||||
this.toggleDropdown(event)
|
||||
}
|
||||
break
|
||||
case 'ArrowUp':
|
||||
event.preventDefault()
|
||||
if (!this.isOpen) {
|
||||
this.toggleDropdown(event)
|
||||
}
|
||||
break
|
||||
case 'Escape':
|
||||
if (this.isOpen) {
|
||||
event.preventDefault()
|
||||
this.closeDropdown()
|
||||
}
|
||||
break
|
||||
}
|
||||
},
|
||||
handleMenuKeydown(event) {
|
||||
const totalOptions = this.options.length
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
event.preventDefault()
|
||||
if (this.focusedIndex === -1) {
|
||||
this.focusedIndex = totalOptions > 0 ? 0 : -1
|
||||
} else {
|
||||
this.focusedIndex = Math.min(this.focusedIndex + 1, totalOptions - 1)
|
||||
}
|
||||
break
|
||||
case 'ArrowUp':
|
||||
event.preventDefault()
|
||||
if (this.focusedIndex === 0) {
|
||||
this.focusedIndex = -1
|
||||
} else if (this.focusedIndex === -1) {
|
||||
this.focusedIndex = -1
|
||||
} else {
|
||||
this.focusedIndex = Math.max(this.focusedIndex - 1, 0)
|
||||
}
|
||||
break
|
||||
case 'Enter':
|
||||
event.preventDefault()
|
||||
if (this.focusedIndex === -1) {
|
||||
this.selectD2D(event)
|
||||
} else if (this.focusedIndex >= 0 && this.options[this.focusedIndex]) {
|
||||
this.selectOption(this.options[this.focusedIndex], event)
|
||||
}
|
||||
break
|
||||
case 'Escape':
|
||||
event.preventDefault()
|
||||
this.closeDropdown()
|
||||
this.$refs.trigger.focus()
|
||||
break
|
||||
case 'Tab':
|
||||
this.closeDropdown()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.route-dropdown {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.route-dropdown-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: white;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
border: 0.2rem solid #E3EDFF;
|
||||
transition: all 0.1s ease;
|
||||
flex: 1 0 auto;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.route-dropdown-trigger:hover {
|
||||
background: #EEF4FF;
|
||||
border: 0.2rem solid #8DB3FE;
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
.route-dropdown-trigger--open {
|
||||
border: 0.2rem solid #8DB3FE;
|
||||
background: #EEF4FF;
|
||||
}
|
||||
|
||||
.route-dropdown-trigger-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.route-dropdown-trigger-text {
|
||||
color: #2d3748;
|
||||
font: inherit;
|
||||
letter-spacing: -0.05em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.route-dropdown-trigger-warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #BC2B72;
|
||||
}
|
||||
|
||||
.route-icon {
|
||||
color: #6b7280;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.route-dropdown-trigger-icon {
|
||||
transition: transform 0.2s ease;
|
||||
color: #718096;
|
||||
flex-shrink: 0;
|
||||
margin-left: 0.8rem;
|
||||
}
|
||||
|
||||
.route-dropdown-trigger-icon--rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.route-dropdown-menu {
|
||||
font: inherit;
|
||||
outline: none;
|
||||
list-style: none;
|
||||
color: #2d3748;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 0;
|
||||
background: white;
|
||||
border: 0.1rem solid #E3EDFF;
|
||||
border-radius: 0.8rem;
|
||||
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
z-index: 1000;
|
||||
max-height: 50rem;
|
||||
overflow-y: auto;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
.route-dropdown-option {
|
||||
font: inherit;
|
||||
padding: 1.2rem 1.6rem;
|
||||
cursor: pointer;
|
||||
border-bottom: 0.16rem solid #f3f4f6;
|
||||
transition: background-color 0.2s ease;
|
||||
color: #2d3748;
|
||||
background: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
|
||||
|
||||
.route-dropdown-option:hover,
|
||||
.route-dropdown-option--focused {
|
||||
background-color: rgba(107, 134, 156, 0.05);
|
||||
}
|
||||
|
||||
.route-dropdown-option--selected {
|
||||
color: #2d3748;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.route-dropdown-option--selected:hover,
|
||||
.route-dropdown-option--selected.route-dropdown-option--focused {
|
||||
color: #2d3748;
|
||||
background-color: rgba(107, 134, 156, 0.05);
|
||||
}
|
||||
|
||||
.route-dropdown-option--empty {
|
||||
color: #001D33;
|
||||
cursor: default;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.route-dropdown-option--empty:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.route-dropdown-divider {
|
||||
height: 0.1rem;
|
||||
background-color: #e5e7eb;
|
||||
|
||||
border-bottom: none; /* Diese Zeile hinzufügen */
|
||||
}
|
||||
|
||||
.route-dropdown-option:has(+ .route-dropdown-divider) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Transition animations */
|
||||
.route-dropdown-fade-enter-active,
|
||||
.route-dropdown-fade-leave-active {
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.route-dropdown-fade-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
.route-dropdown-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
/* Disabled state */
|
||||
.route-dropdown-trigger:disabled {
|
||||
background: white;
|
||||
cursor: not-allowed;
|
||||
border: 0.2rem solid rgba(227, 237, 255, 0.5);
|
||||
color: rgba(0, 47, 84, 0.3);
|
||||
}
|
||||
|
||||
.route-dropdown-trigger:disabled:hover {
|
||||
border: 0.2rem solid rgba(227, 237, 255, 0.5);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.route-dropdown-trigger:disabled .route-dropdown-trigger-text {
|
||||
color: rgba(0, 47, 84, 0.5);
|
||||
}
|
||||
|
||||
.route-dropdown-trigger:disabled .route-dropdown-trigger-icon,
|
||||
.route-dropdown-trigger:disabled .route-icon {
|
||||
color: rgba(113, 128, 150, 0.5);
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 640px) {
|
||||
.route-dropdown-trigger {
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.route-dropdown-option {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -293,7 +293,7 @@ export default {
|
|||
destinationCheck() {
|
||||
|
||||
if (((this.destinations ?? null) === null) || this.destinations.length === 0)
|
||||
return false;
|
||||
return true;
|
||||
|
||||
return !this.destinations?.some(d => d.annual_amount == null);
|
||||
|
||||
|
|
@ -301,7 +301,7 @@ export default {
|
|||
routeCheck() {
|
||||
|
||||
if (((this.destinations ?? null) === null) || this.destinations.length === 0)
|
||||
return false;
|
||||
return true;
|
||||
|
||||
return this.destinations?.every(d => d.routes?.some((route) => route.is_selected));
|
||||
},
|
||||
|
|
@ -352,7 +352,7 @@ export default {
|
|||
if (this.isInitialized
|
||||
&& oldVal === false
|
||||
&& newVal === true
|
||||
&& this.initialCheckStates?.destination !== true) {
|
||||
&& this.initialCheckStates?.destination === false) { // Hier war !== true, sollte === false sein
|
||||
this.showDestinationCheck = true;
|
||||
if (this.initialCheckStates) {
|
||||
this.initialCheckStates.destination = true;
|
||||
|
|
@ -363,7 +363,7 @@ export default {
|
|||
if (this.isInitialized
|
||||
&& oldVal === false
|
||||
&& newVal === true
|
||||
&& this.initialCheckStates?.route !== true) {
|
||||
&& this.initialCheckStates?.route === false) { // Hier war !== true, sollte === false sein
|
||||
this.showRouteCheck = true;
|
||||
if (this.initialCheckStates) {
|
||||
this.initialCheckStates.route = true;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
|
||||
<div class="apps-container">
|
||||
|
||||
<div class="app-list-header">
|
||||
<div>App</div>
|
||||
|
|
@ -15,6 +15,8 @@
|
|||
<add-app @close="closeModal"></add-app>
|
||||
</modal>
|
||||
<basic-button icon="Plus" @click="modalState = true">New App</basic-button>
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
|
|
@ -65,6 +67,12 @@ export default {
|
|||
|
||||
<style scoped>
|
||||
|
||||
.apps-container {
|
||||
padding: 2.4rem;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.app-list-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr 0.5fr;
|
||||
|
|
|
|||
|
|
@ -295,6 +295,7 @@ export default {
|
|||
}
|
||||
|
||||
.bulk-operations-container {
|
||||
margin: 2.4rem;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="materials-container">
|
||||
<table-view ref="tableViewRef" :data-source="fetch" :columns="materialColumns" :page="pagination.page"
|
||||
:page-size="pageSize" :page-count="pagination.pageCount"
|
||||
:total-count="pagination.totalCount"></table-view>
|
||||
|
|
@ -74,5 +74,8 @@ export default {
|
|||
|
||||
|
||||
<style scoped>
|
||||
.materials-container {
|
||||
padding: 2.4rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="nodes-container">
|
||||
<table-view ref="tableViewRef" :data-source="fetch" :columns="nodeColumns" :page="pagination.page"
|
||||
:page-size="pageSize" :page-count="pagination.pageCount"
|
||||
:total-count="pagination.totalCount"></table-view>
|
||||
|
|
@ -91,4 +91,8 @@ export default {
|
|||
|
||||
<style scoped>
|
||||
|
||||
.nodes-container {
|
||||
padding: 2.4rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -166,6 +166,9 @@ export default {
|
|||
width: fit-content;
|
||||
}
|
||||
|
||||
.properties-container {
|
||||
padding: 2.4rem;
|
||||
}
|
||||
|
||||
.property-item-enter-from {
|
||||
opacity: 0;
|
||||
|
|
|
|||
|
|
@ -236,6 +236,7 @@ export default {
|
|||
.container-rate-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.6rem;
|
||||
}
|
||||
|
||||
.container-rate-header {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="users-container">
|
||||
<div class="user-list">
|
||||
<table-view ref="tableViewRef" :searchbar="false" :columns="columns" :data-source="fetch" @row-click="selectUser"
|
||||
:mouse-over="true"></table-view>
|
||||
|
|
@ -119,9 +119,9 @@ export default {
|
|||
badgeResolver: (value) => {
|
||||
|
||||
const formattedValues = []
|
||||
value.slice(0,5).forEach(v => formattedValues.push({text: v, variant: "secondary"}));
|
||||
value.slice(0, 5).forEach(v => formattedValues.push({text: v, variant: "secondary"}));
|
||||
|
||||
if(value.length > 5)
|
||||
if (value.length > 5)
|
||||
formattedValues.push({text: "...", variant: "secondary"});
|
||||
|
||||
return formattedValues;
|
||||
|
|
@ -141,6 +141,11 @@ export default {
|
|||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.users-container {
|
||||
padding: 2.4rem;
|
||||
}
|
||||
|
||||
.user-list {
|
||||
margin-bottom: 2.4rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ export default {
|
|||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
align-items: flex-start;
|
||||
margin: 1.6rem;
|
||||
}
|
||||
|
||||
.destination-edit-handling-cost-info {
|
||||
|
|
|
|||
|
|
@ -197,6 +197,7 @@ export default {
|
|||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0; /* Important for flexbox shrinking */
|
||||
margin: 1.6rem;
|
||||
}
|
||||
|
||||
.destination-edit-route-warning {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import {markRaw} from "vue";
|
|||
import DestinationMassQuantity from "@/components/layout/edit/destination/mass/DestinationMassQuantity.vue";
|
||||
import DestinationMassRoute from "@/components/layout/edit/destination/mass/DestinationMassRoute.vue";
|
||||
import DestinationMassHandlingCost from "@/components/layout/edit/destination/mass/DestinationMassHandlingCost.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {useNotificationStore} from "@/store/notification.js";
|
||||
|
||||
export default {
|
||||
name: "DestinationMassEdit",
|
||||
|
|
@ -31,9 +33,37 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
currentTab: null,
|
||||
isLoading: [false, false, false],
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.updateSpinner();
|
||||
},
|
||||
methods: {
|
||||
handleTabLoadingQuantity(loading) {
|
||||
this.isLoading[0] = loading;
|
||||
this.updateSpinner();
|
||||
},
|
||||
handleTabLoadingHandling(loading) {
|
||||
this.isLoading[1] = loading;
|
||||
this.updateSpinner();
|
||||
},
|
||||
handleTabLoadingRoutes(loading) {
|
||||
this.isLoading[2] = loading;
|
||||
this.updateSpinner();
|
||||
},
|
||||
updateSpinner() {
|
||||
if (this.isLoading[0] || this.isLoading[1] || this.isLoading[2]) {
|
||||
this.notificationStore.setSpinner("Processing ...");
|
||||
|
||||
}
|
||||
else {
|
||||
this.notificationStore.clearSpinner();
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useNotificationStore),
|
||||
defaultTab() {
|
||||
return this.tabsConfig.indexOf(this.tabsConfig.find(t => t.matchType === this.type)) ?? 0;
|
||||
},
|
||||
|
|
@ -42,20 +72,23 @@ export default {
|
|||
{
|
||||
title: 'Annual quantity',
|
||||
component: markRaw(DestinationMassQuantity),
|
||||
props: {premiseIds: this.premiseIds},
|
||||
matchType: 'amount'
|
||||
props: {premiseIds: this.premiseIds, onLoadingChange: this.handleTabLoadingQuantity},
|
||||
matchType: 'amount',
|
||||
|
||||
},
|
||||
{
|
||||
title: 'Handling & Repackaging',
|
||||
component: markRaw(DestinationMassHandlingCost),
|
||||
props: {premiseIds: this.premiseIds},
|
||||
matchType: 'handling'
|
||||
props: {premiseIds: this.premiseIds, onLoadingChange: this.handleTabLoadingHandling},
|
||||
matchType: 'handling',
|
||||
|
||||
},
|
||||
{
|
||||
title: 'Routes',
|
||||
component: markRaw(DestinationMassRoute),
|
||||
props: {premiseIds: this.premiseIds},
|
||||
matchType: 'routes'
|
||||
props: {premiseIds: this.premiseIds, onLoadingChange: this.handleTabLoadingRoutes},
|
||||
matchType: 'routes',
|
||||
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
<div class="dest-mass-handling-table-header-destination">Destination</div>
|
||||
<div class="dest-mass-handling-table-header-applier">
|
||||
<icon-button icon="check" :disabled="!someChecked" @click="updateOverallValue"></icon-button>
|
||||
<icon-button icon="x" :disabled="!someChecked" @click="dismissChecked"></icon-button>
|
||||
</div>
|
||||
<div class="dest-mass-handling-table-header-costs">
|
||||
<div>Handling costs</div>
|
||||
|
|
@ -84,11 +85,15 @@ export default {
|
|||
premiseIds: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
onLoadingChange: {
|
||||
type: Function,
|
||||
default: () => {
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
handlingCostMatrix: null,
|
||||
handlingCostActive: false,
|
||||
overallDisposalCostValue: null,
|
||||
overallRepackagingCostValue: null,
|
||||
|
|
@ -102,7 +107,7 @@ export default {
|
|||
computed: {
|
||||
...mapStores(useDestinationEditStore, usePremiseEditStore),
|
||||
rows() {
|
||||
return this.handlingCostMatrix;
|
||||
return this.destinationEditStore.getHandlingCostMatrix
|
||||
},
|
||||
allChecked() {
|
||||
return this.rows.every(r => r.selected);
|
||||
|
|
@ -120,8 +125,14 @@ export default {
|
|||
return this.isCtrlPressed && !this.isShiftPressed;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.buildMatrix();
|
||||
async created() {
|
||||
this.onLoadingChange(true);
|
||||
try {
|
||||
await this.buildMatrix();
|
||||
} finally {
|
||||
this.onLoadingChange(false);
|
||||
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('keydown', this.handleKeyDown);
|
||||
|
|
@ -153,7 +164,7 @@ export default {
|
|||
},
|
||||
onClickAction(data) {
|
||||
|
||||
this.handlingCostMatrix.forEach(d => {
|
||||
this.rows.forEach(d => {
|
||||
d.selected = ((data.column === 'material' && d.material === data.row.material)
|
||||
|| (data.column === 'supplier' && d.supplierId === data.row.supplierId)
|
||||
|| (data.column === 'destination' && d.destinationNodeId === data.row.destinationNodeId)
|
||||
|
|
@ -190,7 +201,7 @@ export default {
|
|||
|
||||
if (this.overallHandlingCostValue !== null || this.overallDisposalCostValue !== null || this.overallRepackagingCostValue !== null) {
|
||||
|
||||
this.handlingCostMatrix
|
||||
this.rows
|
||||
.filter(row => row.selected)
|
||||
.forEach(row => {
|
||||
row.handling_costs = this.overallHandlingCostValue ?? row.handling_costs;
|
||||
|
|
@ -205,7 +216,10 @@ export default {
|
|||
this.$forceUpdate();
|
||||
}
|
||||
|
||||
this.handlingCostMatrix.forEach(row => row.selected = false);
|
||||
this.dismissChecked();
|
||||
},
|
||||
dismissChecked() {
|
||||
this.rows.forEach(row => row.selected = false);
|
||||
this.updateOverallCheckBox();
|
||||
},
|
||||
|
||||
|
|
@ -218,12 +232,14 @@ export default {
|
|||
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)
|
||||
|
|
@ -237,8 +253,8 @@ export default {
|
|||
return `${useMappingId ? mappingId.replace("_", " ") : (needsShortName ? shortName : name)}`;
|
||||
|
||||
},
|
||||
buildMatrix() {
|
||||
this.handlingCostMatrix = [];
|
||||
async buildMatrix() {
|
||||
const handlingCostMatrix = [];
|
||||
|
||||
for (const pId of this.premiseIds) {
|
||||
const premise = this.premiseEditStore.getById(pId);
|
||||
|
|
@ -246,11 +262,12 @@ export default {
|
|||
if (!destinations) continue;
|
||||
|
||||
for (const d of destinations) {
|
||||
this.handlingCostMatrix.push({
|
||||
handlingCostMatrix.push({
|
||||
id: premise.id,
|
||||
material: premise.material.part_number,
|
||||
supplier: this.toNode(premise.supplier, 15),
|
||||
supplierId: premise.supplier.id,
|
||||
supplierIso: premise.supplier.country.iso_code,
|
||||
destinationId: d.id,
|
||||
destinationNodeId: d.destination_node.id,
|
||||
destination: this.toNode(d.destination_node, 15),
|
||||
|
|
@ -260,14 +277,13 @@ export default {
|
|||
selected: false
|
||||
});
|
||||
|
||||
this.handlingCostActive |= ((d.handling_costs !==null) || d.repackaging_costs !== null || d.disposal_costs !== null);
|
||||
this.handlingCostActive = ((d.handling_costs !== null) || d.repackaging_costs !== null || d.disposal_costs !== null) || this.handlingCostActive;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
this.destinationEditStore.setHandlingCostMatrix(handlingCostMatrix);
|
||||
await this.$nextTick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -432,7 +448,8 @@ export default {
|
|||
.dest-mass-handling-table-header-applier {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 5rem;
|
||||
width: 7rem;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.dest-mass-handling-table-header-costs {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
<div class="dest-mass-handling-row-supplier dest-mass-handling-row__cell--filterable"
|
||||
@click="action($event,'supplier')"
|
||||
@mousedown="handleMouseDown">
|
||||
<ph-factory size="24"/>
|
||||
<flag :iso="row.supplierIso" />
|
||||
{{ row.supplier }}
|
||||
</div>
|
||||
<div class="dest-mass-handling-row-destination dest-mass-handling-row__cell--filterable"
|
||||
|
|
@ -63,10 +63,11 @@
|
|||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
import {PhFactory, PhMapPin} from "@phosphor-icons/vue";
|
||||
import {parseNumberFromString} from "@/common.js";
|
||||
import Flag from "@/components/UI/Flag.vue";
|
||||
|
||||
export default {
|
||||
name: "DestinationMassHandlingCostRow",
|
||||
components: {PhMapPin, PhFactory, Checkbox},
|
||||
components: {Flag, PhMapPin, PhFactory, Checkbox},
|
||||
emits: ['action', 'update-selected'],
|
||||
props: {
|
||||
row: {
|
||||
|
|
@ -225,7 +226,7 @@ export default {
|
|||
}
|
||||
|
||||
.dest-mass-handling-row-applier {
|
||||
width: 5rem;
|
||||
width: 7rem;
|
||||
}
|
||||
|
||||
.dest-mass-handling-row-supplier {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@
|
|||
<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>
|
||||
<icon-button icon="check" :disabled="!someChecked" @click="updateOverallValue"></icon-button>
|
||||
<icon-button icon="x" :disabled="!someChecked" @click="dismissChecked"></icon-button>
|
||||
</div>
|
||||
<div class="dest-mass-quantity-table-header-dest"
|
||||
:key="`${dest.id}`"
|
||||
v-for="dest in destPool">
|
||||
|
|
@ -55,13 +57,17 @@ export default {
|
|||
premiseIds: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
onLoadingChange: {
|
||||
type: Function,
|
||||
default: () => {
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
destPool: null,
|
||||
destMatrix: null,
|
||||
overallCheck: false,
|
||||
overallIndeterminate: false,
|
||||
isCtrlPressed: false,
|
||||
|
|
@ -71,7 +77,7 @@ export default {
|
|||
computed: {
|
||||
...mapStores(useDestinationEditStore, usePremiseEditStore),
|
||||
rows() {
|
||||
return this.destMatrix;
|
||||
return this.destinationEditStore.getQuantityMatrix;
|
||||
},
|
||||
allChecked() {
|
||||
return this.rows.every(r => r.selected);
|
||||
|
|
@ -89,15 +95,13 @@ export default {
|
|||
return this.isCtrlPressed && !this.isShiftPressed;
|
||||
},
|
||||
},
|
||||
// watch: {
|
||||
// someChecked(newVal, oldVal) {
|
||||
// if(newVal === true && oldVal === false) {
|
||||
// //reset overall inputs
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
created() {
|
||||
this.buildMatrix();
|
||||
async created() {
|
||||
this.onLoadingChange(true);
|
||||
try {
|
||||
await this.buildMatrix();
|
||||
} finally {
|
||||
this.onLoadingChange(false);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('keydown', this.handleKeyDown);
|
||||
|
|
@ -131,7 +135,7 @@ export default {
|
|||
},
|
||||
onClickAction(data) {
|
||||
|
||||
this.destMatrix.forEach(d => {
|
||||
this.rows.forEach(d => {
|
||||
d.selected = ((data.column === 'material' && d.material === data.row.material)
|
||||
|| (data.column === 'supplier' && d.supplier === data.row.supplier)
|
||||
|| (data.action === 'append' && d.selected));
|
||||
|
|
@ -157,7 +161,7 @@ export default {
|
|||
}));
|
||||
|
||||
if (updates.length > 0) {
|
||||
this.destMatrix
|
||||
this.rows
|
||||
.filter(row => row.selected)
|
||||
.forEach(row => {
|
||||
updates.forEach(update => {
|
||||
|
|
@ -173,7 +177,10 @@ export default {
|
|||
this.$forceUpdate();
|
||||
}
|
||||
|
||||
this.destMatrix.forEach(row => row.selected = false);
|
||||
this.dismissChecked();
|
||||
},
|
||||
dismissChecked() {
|
||||
this.rows.forEach(row => row.selected = false);
|
||||
this.updateOverallCheckBox();
|
||||
},
|
||||
|
||||
|
|
@ -205,7 +212,7 @@ export default {
|
|||
return `${useMappingId ? mappingId.replace("_", " ") : (needsShortName ? shortName : name)}`;
|
||||
|
||||
},
|
||||
buildMatrix() {
|
||||
async buildMatrix() {
|
||||
// destPool aufbauen
|
||||
const destMap = new Map();
|
||||
|
||||
|
|
@ -228,8 +235,8 @@ export default {
|
|||
|
||||
this.destPool = Array.from(destMap.values());
|
||||
|
||||
// destMatrix aufbauen
|
||||
this.destMatrix = this.premiseIds
|
||||
|
||||
const quantityMatrix = this.premiseIds
|
||||
.filter(p => p)
|
||||
.map(p => this.premiseEditStore.getById(p))
|
||||
.map(premise => {
|
||||
|
|
@ -258,6 +265,9 @@ export default {
|
|||
selected: false
|
||||
};
|
||||
});
|
||||
|
||||
this.destinationEditStore.setQuantityMatrix(quantityMatrix);
|
||||
await this.$nextTick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -340,7 +350,7 @@ export default {
|
|||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.dest-mass-quantity-table-wrapper {
|
||||
display: flex;
|
||||
|
|
@ -400,7 +410,8 @@ export default {
|
|||
.dest-mass-quantity-table-header-applier {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 5rem;
|
||||
width: 7rem;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.dest-mass-quantity-table-header-dest {
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ export default {
|
|||
}
|
||||
|
||||
.dest-mass-quantity-row-applier {
|
||||
width: 5rem;
|
||||
width: 7rem;
|
||||
}
|
||||
|
||||
.dest-mass-quantity-row-supplier {
|
||||
|
|
|
|||
|
|
@ -1,32 +1,39 @@
|
|||
<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 v-if="generalError">
|
||||
<div class="destination-mass-route-info">
|
||||
<ph-warning size="18px"></ph-warning>
|
||||
The routing data is faulty. Please contact support.
|
||||
You can try to solve the problem by first deleting all destinations and then creating them again.
|
||||
</div>
|
||||
<div class="dest-mass-route-table-header-supplier">Supplier</div>
|
||||
</div>
|
||||
<div v-else class="dest-mass-route-table-wrapper">
|
||||
<div class="dest-mass-route-table-header-wrapper"
|
||||
ref="headerWrapper"
|
||||
@scroll="syncScroll('header')">
|
||||
<div class="dest-mass-route-table-header">
|
||||
<div class="dest-mass-route-table-header-supplier"></div>
|
||||
<div class="dest-mass-route-table-header-dest"
|
||||
:key="`${dest.id}`"
|
||||
v-for="dest in destPool">
|
||||
<div></div>
|
||||
<div>{{ toNode(dest.destination_node, 6) }}</div>
|
||||
<div 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">
|
||||
|
||||
<div class="dest-mass-route-table"
|
||||
ref="tableBody"
|
||||
@scroll="syncScroll('body')">
|
||||
<destination-mass-route-row :row="row"
|
||||
:key="row.id"
|
||||
v-for="row in rows"></destination-mass-route-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
import DestinationMassQuantityRow from "@/components/layout/edit/destination/mass/DestinationMassQuantityRow.vue";
|
||||
|
|
@ -36,30 +43,58 @@ import {mapStores} from "pinia";
|
|||
import {useDestinationEditStore} from "@/store/destinationEdit.js";
|
||||
import {usePremiseEditStore} from "@/store/premiseEdit.js";
|
||||
import {toRaw} from "vue";
|
||||
import DestinationMassRouteRow from "@/components/layout/edit/destination/mass/DestinationMassRouteRow.vue";
|
||||
import Flag from "@/components/UI/Flag.vue";
|
||||
|
||||
export default {
|
||||
name: "DestinationMassRoute",
|
||||
components: {IconButton, Checkbox, DestinationMassQuantityRow},
|
||||
components: {Flag, DestinationMassRouteRow, IconButton, Checkbox, DestinationMassQuantityRow},
|
||||
props: {
|
||||
premiseIds: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
onLoadingChange: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
}
|
||||
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useDestinationEditStore, usePremiseEditStore)
|
||||
...mapStores(useDestinationEditStore, usePremiseEditStore),
|
||||
rows() {
|
||||
return this.destinationEditStore.getRouteMatrix;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
destPool: null,
|
||||
destMatrix: null,
|
||||
generalError: false,
|
||||
isScrollingSyncronized: false,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.buildMatrix();
|
||||
async created() {
|
||||
this.onLoadingChange(true);
|
||||
try {
|
||||
await this.buildMatrix();
|
||||
} finally {
|
||||
this.onLoadingChange(false);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
syncScroll(source) {
|
||||
if (this.isScrollingSyncronized) {
|
||||
this.isScrollingSyncronized = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.isScrollingSyncronized = true;
|
||||
|
||||
if (source === 'body') {
|
||||
this.$refs.headerWrapper.scrollLeft = this.$refs.tableBody.scrollLeft;
|
||||
} else if (source === 'header') {
|
||||
this.$refs.tableBody.scrollLeft = this.$refs.headerWrapper.scrollLeft;
|
||||
}
|
||||
},
|
||||
toNode(node, limit = 5) {
|
||||
if (!node)
|
||||
return 'N/A';
|
||||
|
|
@ -72,26 +107,62 @@ export default {
|
|||
return `${useMappingId ? mappingId.replace("_", " ") : (needsShortName ? shortName : name)}`;
|
||||
|
||||
},
|
||||
buildMatrix() {
|
||||
const destMap = new Map();
|
||||
async buildMatrix() {
|
||||
const columnHeadersMap = new Map();
|
||||
const supplierToDestinationsMap = new Map();
|
||||
|
||||
for (const pId of this.premiseIds) {
|
||||
const destinations = this.destinationEditStore.getByPremiseId(pId);
|
||||
if (!destinations) continue;
|
||||
const curPremise = this.premiseEditStore.getById(pId);
|
||||
const destOfCurPremise = this.destinationEditStore.getByPremiseId(pId);
|
||||
if (!destOfCurPremise) continue;
|
||||
|
||||
for (const d of destinations) {
|
||||
/* supplier map collects all destinations for one supplier
|
||||
* and replaces the destination if the same destination is found
|
||||
* that already has a selected route.
|
||||
*/
|
||||
if (!supplierToDestinationsMap.has(curPremise.supplier.id)) {
|
||||
supplierToDestinationsMap.set(curPremise.supplier.id, {
|
||||
destinations: destOfCurPremise ?? []
|
||||
});
|
||||
} else {
|
||||
const mapEntry = supplierToDestinationsMap.get(curPremise.supplier.id);
|
||||
const exDs = mapEntry.destinations;
|
||||
|
||||
destOfCurPremise.forEach(d => {
|
||||
const exD = exDs.find(ex => ex.destination_node.id === d.destination_node.id) ?? null;
|
||||
|
||||
if (!exD) {
|
||||
exDs.push(d);
|
||||
} else {
|
||||
|
||||
if ((!exD.routes?.some(r => r.is_selected) && d.routes?.some(r => r.is_selected) && !exD.is_d2d) || (!exD.is_d2d && d.is_d2d)) {
|
||||
const idx = exDs.indexOf(exD);
|
||||
exDs.splice(idx, 1);
|
||||
exDs.push(d);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* destination map collects all destinations over all
|
||||
* suppliers for table headers
|
||||
*/
|
||||
for (const d of destOfCurPremise) {
|
||||
const destId = d.destination_node.id;
|
||||
if (!destMap.has(destId)) {
|
||||
destMap.set(destId, {
|
||||
...d,
|
||||
if (!columnHeadersMap.has(destId)) {
|
||||
columnHeadersMap.set(destId, {
|
||||
destination_node: {...d.destination_node},
|
||||
destinationNodeId: d.destination_node.id,
|
||||
destinationNodeName: d.destination_node.name
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.destPool = Array.from(destMap.values());
|
||||
this.destPool = Array.from(columnHeadersMap.values());
|
||||
|
||||
|
||||
|
||||
// destMatrix aufbauen
|
||||
const premiseMap = new Map();
|
||||
|
||||
this.premiseIds.forEach(pId => {
|
||||
|
|
@ -102,8 +173,8 @@ export default {
|
|||
premiseMap.set(premise.supplier.id, {
|
||||
ids: [],
|
||||
supplierNodeId: premise.supplier.id,
|
||||
supplier: premise.supplier.name,
|
||||
destinations: []
|
||||
supplier: premise.supplier,
|
||||
destinations: this.buildDestinations(columnHeadersMap, supplierToDestinationsMap.get(premise.supplier.id)?.destinations ?? [])
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -114,74 +185,110 @@ export default {
|
|||
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
|
||||
// };
|
||||
// });
|
||||
const destMatrix = Array.from(premiseMap.values());
|
||||
this.generalError = destMatrix.some(r => !r.destinations.every(d => d.valid));
|
||||
|
||||
this.destinationEditStore.setRouteMatrix(destMatrix)
|
||||
await this.$nextTick();
|
||||
},
|
||||
buildDestinations(allDestinationsMap, assignedDestinations) {
|
||||
return Array.from(allDestinationsMap.values()).map(d => {
|
||||
|
||||
const assignedDest = assignedDestinations.find(dest => dest.destination_node.id === d.destinationNodeId);
|
||||
|
||||
const builtRoutes = this.buildRoutes(assignedDest?.routes);
|
||||
const selectedBuildRoute = builtRoutes?.find(r => r.selected)?.routeCompareString ?? null;
|
||||
|
||||
return {
|
||||
ids: [],
|
||||
disabled: true,
|
||||
valid: true,
|
||||
destinationNodeId: d.destinationNodeId,
|
||||
destinationName: d.destinationNodeName,
|
||||
routes: builtRoutes,
|
||||
isD2d: assignedDest?.is_d2d ?? false,
|
||||
rateD2d: assignedDest?.rate_d2d ?? null,
|
||||
leadTimeD2d: assignedDest?.lead_time_d2d === 0 ? null : (assignedDest?.lead_time_d2d ?? null),
|
||||
selectedRoute: selectedBuildRoute
|
||||
}
|
||||
});
|
||||
},
|
||||
addDestinationsToRow(rowDestinations, 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 */
|
||||
if (existingDest) {
|
||||
existingDest.disabled = false;
|
||||
existingDest.ids.push(premD.id);
|
||||
|
||||
/* add route ids to routes */
|
||||
this.addRoutesToDestination(existingDest.routes, premD.routes)
|
||||
|
||||
this.verifyRoutes(existingDest, premD)
|
||||
}
|
||||
});
|
||||
},
|
||||
addRoutesToDestination(rowRoutes, premiseRoutes) {
|
||||
// premiseRoutes
|
||||
verifyRoutes(rowDest, premiseDest) {
|
||||
|
||||
const premiseRoutes = premiseDest.routes;
|
||||
|
||||
if (rowDest.routes.length !== premiseRoutes.length) {
|
||||
console.log("length mismatch ", toRaw(rowDest), toRaw(premiseDest));
|
||||
rowDest.valid = false;
|
||||
return
|
||||
}
|
||||
|
||||
premiseRoutes.forEach(route => {
|
||||
const routeString = JSON.stringify(route.transit_nodes.map(n => n.external_mapping_id)); //.join(" > ").replace("_", " ");
|
||||
|
||||
if (!(rowDest.routes.some(r => r.routeCompareString === routeString && r.type === route.type))) {
|
||||
console.log("no matching route ", routeString, rowDest);
|
||||
rowDest.valid = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
buildRoutes(routes) {
|
||||
|
||||
return routes?.map(r => {
|
||||
return {
|
||||
type: r.type,
|
||||
selected: r.is_selected,
|
||||
transitNodes: r.transit_nodes.map(n => n.external_mapping_id),
|
||||
routeCompareString: JSON.stringify(r.transit_nodes.map(n => n.external_mapping_id)), //.join(" > ").replace("_", " ")
|
||||
routeDisplayString: this.toRoute(r)
|
||||
}
|
||||
}) ?? [];
|
||||
},
|
||||
toRoute(route, limit = 48) {
|
||||
|
||||
if (!route)
|
||||
return 'N/A';
|
||||
|
||||
const nodes = route.transit_nodes?.map((node) => this.toNode(node)) ?? [];
|
||||
|
||||
if (nodes.length === 0)
|
||||
return 'N/A';
|
||||
|
||||
const separator = " > ";
|
||||
let fullString = nodes.join(separator);
|
||||
|
||||
if (fullString.length <= limit)
|
||||
return fullString;
|
||||
|
||||
const front = nodes[0].concat(separator).concat("...").concat(separator);
|
||||
let back = [];
|
||||
|
||||
for (const node of nodes.slice().reverse()) {
|
||||
|
||||
back.unshift(node);
|
||||
const temp = front.concat(back.join(separator));
|
||||
|
||||
if (temp.length > limit) {
|
||||
return front.concat(back.slice(1).join(separator));
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -190,4 +297,160 @@ export default {
|
|||
|
||||
<style scoped>
|
||||
|
||||
.destination-mass-route-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.4rem;
|
||||
gap: 1.6rem;
|
||||
background-color: #BC2B72;
|
||||
color: #ffffff;
|
||||
border-radius: 0.8rem;
|
||||
padding: 1.6rem;
|
||||
margin: 1.6rem 1.6rem 0 1.6rem;
|
||||
}
|
||||
|
||||
|
||||
/* Global style für copy-mode cursor */
|
||||
.dest-mass-route-container.has-selection :deep(.dest-mass-route-row__cell--copyable:hover) {
|
||||
cursor: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDEyOC41MSAxMzQuMDUiPjxkZWZzPjxzdHlsZT4uY3tmaWxsOm5vbmU7fS5jLC5ke3N0cm9rZTojMDEwMTAxO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2Utd2lkdGg6NXB4O30uZHtmaWxsOiNmZmY7fTwvc3R5bGU+PC9kZWZzPjxnIGlkPSJhIj48cGF0aCBjbGFzcz0iYyIgZD0ibTU0Ljg5LDExMi41MWgtMi4yNGMtMS4yNCwwLTIuMjQtMS0yLjI0LTIuMjR2LTIuMjQiLz48bGluZSBjbGFzcz0iYyIgeDE9IjcwLjU3IiB5MT0iNzYuNjciIHgyPSI2My44NSIgeTI9Ijc2LjY3Ii8+PGxpbmUgY2xhc3M9ImMiIHgxPSI3MC41NyIgeTE9IjExMi41MSIgeDI9IjY2LjA5IiB5Mj0iMTEyLjUxIi8+PGxpbmUgY2xhc3M9ImMiIHgxPSI4Ni4yNSIgeTE9Ijk5LjA3IiB4Mj0iODYuMjUiIHkyPSI5Mi4zNSIvPjxsaW5lIGNsYXNzPSJjIiB4MT0iNTAuNDEiIHkxPSI5Ni44MyIgeDI9IjUwLjQxIiB5Mj0iOTIuMzUiLz48cGF0aCBjbGFzcz0iYyIgZD0ibTgxLjc3LDExMi41MWgyLjI0YzEuMjQsMCwyLjI0LTEsMi4yNC0yLjI0di0yLjI0Ii8+PHBhdGggY2xhc3M9ImMiIGQ9Im04MS43Nyw3Ni42N2gyLjI0YzEuMjQsMCwyLjI0LDEsMi4yNCwyLjI0djIuMjQiLz48cGF0aCBjbGFzcz0iYyIgZD0ibTU0Ljg5LDc2LjY3aC0yLjI0Yy0xLjI0LDAtMi4yNCwxLTIuMjQsMi4yNHYyLjI0Ii8+PHBhdGggY2xhc3M9ImMiIGQ9Im04Ni4yNSw5OS4wN2gxMS4yYzEuMjQsMCwyLjI0LTEsMi4yNC0yLjI0di0zMS4zNmMwLTEuMjQtMS0yLjI0LTIuMjQtMi4yNGgtMzEuMzZjLTEuMjQsMC0yLjI0LDEtMi4yNCwyLjI0djExLjIiLz48L2c+PGcgaWQ9ImIiPjxwYXRoIGNsYXNzPSJkIiBkPSJtNDQuMDgsNDQuMDdsMzIuOTQtOS4yYzEuNjktLjUyLDIuNjQtMi4zMSwyLjEyLTQtLjMtLjk4LTEuMDUtMS43NS0yLjAxLTIuMDlMNi43MywyLjY3Yy0xLjY3LS41Ny0zLjQ5LjMzLTQuMDYsMi0uMjMuNjYtLjIzLDEuMzgsMCwyLjA1bDI2LjExLDcwLjRjLjU4LDEuNjcsMi40LDIuNTYsNC4wNywxLjk4Ljk3LS4zMywxLjcxLTEuMTEsMi4wMS0yLjA5bDkuMjItMzIuOTRaIi8+PC9nPjwvc3ZnPg==") 12 12, pointer;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
||||
/* Global style für filter-mode cursor */
|
||||
.dest-mass-route-container.add-all :deep(.dest-mass-route-row__cell--filterable:hover) {
|
||||
cursor: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDEyOC41MSAxMzQuMDUiPjxkZWZzPjxzdHlsZT4uY3tmaWxsOm5vbmU7fS5jLC5ke3N0cm9rZTojMDEwMTAxO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2Utd2lkdGg6NXB4O30uZHtmaWxsOiNmZmY7fTwvc3R5bGU+PC9kZWZzPjxnIGlkPSJhIj48bGluZSBjbGFzcz0iYyIgeDE9IjczLjAzIiB5MT0iNzUuNTUiIHgyPSI2Ni4zOCIgeTI9Ijc1LjU1Ii8+PGxpbmUgY2xhc3M9ImMiIHgxPSI2Ni4zOCIgeTE9IjExMi4xNiIgeDI9IjczLjAzIiB5Mj0iMTEyLjE2Ii8+PHBhdGggY2xhc3M9ImMiIGQ9Im04MS4zNSw3NS41NWg0Ljk5Yy45MiwwLDEuNjYuNzUsMS42NiwxLjY2djQuOTkiLz48bGluZSBjbGFzcz0iYyIgeDE9Ijg4LjAxIiB5MT0iOTcuMTgiIHgyPSI4OC4wMSIgeTI9IjkwLjUzIi8+PHBhdGggY2xhc3M9ImMiIGQ9Im04MS4zNSwxMTIuMTZoNC45OWMuOTIsMCwxLjY2LS43NSwxLjY2LTEuNjZ2LTQuOTkiLz48bGluZSBjbGFzcz0iYyIgeDE9IjUxLjQiIHkxPSI5MC41MyIgeDI9IjUxLjQiIHkyPSI5Ny4xOCIvPjxwYXRoIGNsYXNzPSJjIiBkPSJtNTguMDUsMTEyLjE2aC00Ljk5Yy0uOTIsMC0xLjY2LS43NS0xLjY2LTEuNjZ2LTQuOTkiLz48cGF0aCBjbGFzcz0iYyIgZD0ibTU4LjA1LDc1LjU1aC00Ljk5Yy0uOTIsMC0xLjY2Ljc1LTEuNjYsMS42NnY0Ljk5Ii8+PC9nPjxnIGlkPSJiIj48cGF0aCBjbGFzcz0iZCIgZD0ibTQ0LjA4LDQ0LjA3bDMyLjk0LTkuMmMxLjY5LS41MiwyLjY0LTIuMzEsMi4xMi00LS4zLS45OC0xLjA1LTEuNzUtMi4wMS0yLjA5TDYuNzMsMi42N2MtMS42Ny0uNTctMy40OS4zMy00LjA2LDItLjIzLjY2LS4yMywxLjM4LDAsMi4wNWwyNi4xMSw3MC40Yy41OCwxLjY3LDIuNCwyLjU2LDQuMDcsMS45OC45Ny0uMzMsMS43MS0xLjExLDIuMDEtMi4wOWw5LjIyLTMyLjk0WiIvPjwvZz48L3N2Zz4=") 12 12, pointer;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
||||
/* Global style für filter-mode cursor */
|
||||
.dest-mass-route-container.apply-filter :deep(.dest-mass-route-row__cell--filterable:hover) {
|
||||
cursor: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDEyOC41MSAxMzQuMDUiPjxkZWZzPjxzdHlsZT4uZHtzdHJva2U6IzAwMDt9LmQsLmV7ZmlsbDpub25lO30uZCwuZSwuZntzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLXdpZHRoOjVweDt9LmUsLmZ7c3Ryb2tlOiMwMTAxMDE7fS5me2ZpbGw6I2ZmZjt9PC9zdHlsZT48L2RlZnM+PGcgaWQ9ImEiPjxsaW5lIGNsYXNzPSJlIiB4MT0iNzMuMDMiIHkxPSI3NS41NSIgeDI9IjY2LjM4IiB5Mj0iNzUuNTUiLz48bGluZSBjbGFzcz0iZSIgeDE9IjY2LjM4IiB5MT0iMTEyLjE2IiB4Mj0iNzMuMDMiIHkyPSIxMTIuMTYiLz48cGF0aCBjbGFzcz0iZSIgZD0ibTgxLjM1LDc1LjU1aDQuOTljLjkyLDAsMS42Ni43NSwxLjY2LDEuNjZ2NC45OSIvPjxsaW5lIGNsYXNzPSJlIiB4MT0iODguMDEiIHkxPSI5Ny4xOCIgeDI9Ijg4LjAxIiB5Mj0iOTAuNTMiLz48cGF0aCBjbGFzcz0iZSIgZD0ibTgxLjM1LDExMi4xNmg0Ljk5Yy45MiwwLDEuNjYtLjc1LDEuNjYtMS42NnYtNC45OSIvPjxsaW5lIGNsYXNzPSJlIiB4MT0iNTEuNCIgeTE9IjkwLjUzIiB4Mj0iNTEuNCIgeTI9Ijk3LjE4Ii8+PHBhdGggY2xhc3M9ImUiIGQ9Im01OC4wNSwxMTIuMTZoLTQuOTljLS45MiwwLTEuNjYtLjc1LTEuNjYtMS42NnYtNC45OSIvPjxwYXRoIGNsYXNzPSJlIiBkPSJtNTguMDUsNzUuNTVoLTQuOTljLS45MiwwLTEuNjYuNzUtMS42NiwxLjY2djQuOTkiLz48L2c+PGcgaWQ9ImIiPjxwYXRoIGNsYXNzPSJmIiBkPSJtNDQuMDgsNDQuMDdsMzIuOTQtOS4yYzEuNjktLjUyLDIuNjQtMi4zMSwyLjEyLTQtLjMtLjk4LTEuMDUtMS43NS0yLjAxLTIuMDlMNi43MywyLjY3Yy0xLjY3LS41Ny0zLjQ5LjMzLTQuMDYsMi0uMjMuNjYtLjIzLDEuMzgsMCwyLjA1bDI2LjExLDcwLjRjLjU4LDEuNjcsMi40LDIuNTYsNC4wNywxLjk4Ljk3LS4zMywxLjcxLTEuMTEsMi4wMS0yLjA5bDkuMjItMzIuOTRaIi8+PC9nPjxnIGlkPSJjIj48bGluZSBjbGFzcz0iZCIgeDE9Ijk5LjM4IiB5MT0iOTQuMTkiIHgyPSIxMjYuMDEiIHkyPSI5NC4xOSIvPjxsaW5lIGNsYXNzPSJkIiB4MT0iMTEyLjY5IiB5MT0iODAuODciIHgyPSIxMTIuNjkiIHkyPSIxMDcuNTEiLz48L2c+PC9zdmc+") 12 12, pointer;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
||||
.text-container.disabled {
|
||||
background-color: #f3f4f6;
|
||||
cursor: not-allowed;
|
||||
border-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.text-container.disabled input {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
||||
.text-container:hover:not(.disabled) {
|
||||
background: #EEF4FF;
|
||||
border: 0.2rem solid #8DB3FE;
|
||||
transform: scale(1.01);
|
||||
|
||||
}
|
||||
|
||||
|
||||
.input-field {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: none;
|
||||
resize: none;
|
||||
font-family: inherit;
|
||||
font-size: 1.4rem;
|
||||
color: #002F54;
|
||||
|
||||
max-width: 6rem;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);*/
|
||||
border: 0.2rem solid #E3EDFF;
|
||||
transition: all 0.1s ease;
|
||||
flex: 1 0 auto;
|
||||
|
||||
max-width: 8rem;
|
||||
|
||||
}
|
||||
|
||||
.dest-mass-route-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dest-mass-route-table-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.dest-mass-route-table-header-wrapper {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
flex-shrink: 0;
|
||||
|
||||
/* Scrollbar verstecken aber Funktionalität behalten */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE/Edge */
|
||||
}
|
||||
|
||||
.dest-mass-route-table-header-wrapper::-webkit-scrollbar {
|
||||
display: none; /* Chrome/Safari/Opera */
|
||||
}
|
||||
|
||||
|
||||
|
||||
.dest-mass-route-table {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
padding-bottom: 2.4rem;
|
||||
}
|
||||
|
||||
.dest-mass-route-table-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1.6rem;
|
||||
padding: 1.6rem 2.4rem;
|
||||
justify-content: flex-start;
|
||||
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid rgba(107, 134, 156, 0.2);
|
||||
font-weight: 500;
|
||||
font-size: 1.4rem;
|
||||
color: #6B869C;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08rem;
|
||||
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.dest-mass-route-table-header-supplier {
|
||||
width: 24rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.dest-mass-route-table-header-dest {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
width: 35rem;
|
||||
gap: 0.8rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
<template>
|
||||
<div class="dest-mass-route-cell">
|
||||
<div class="dest-mass-route-dropdown">
|
||||
<route-dropdown
|
||||
placeholder="No route selected"
|
||||
empty-text="No routes"
|
||||
:disabled="destination.disabled"
|
||||
:show-d2d-warn="showD2DWarn"
|
||||
v-model:model-value="this.selectedRoute"
|
||||
:options="destination.routes"
|
||||
display-key="routeDisplayString"
|
||||
value-key="routeCompareString"
|
||||
@update:modelValue="updateSelectedRoute"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<icon-button :disabled="destination.disabled || this.selectedRoute !== 'D2D_ROUTING'" icon="pencilSimple" @click="openD2DModal">icon</icon-button>
|
||||
</div>
|
||||
|
||||
<modal :state="modalState" @close="modalState = false">
|
||||
<div class="destination-route-modal">
|
||||
<div>
|
||||
<div>D2D Rate [EUR]</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-container">
|
||||
<input :value="this.rateD2d" @blur="validateRateD2d" class="input-field" ref="rate" @keydown.enter="handleEnter('rate', $event)"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div> Lead time [days]</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-container">
|
||||
<input :value="this.leadTimeD2d" @blur="validateLeadTimeD2d" class="input-field" ref="leadTime" @keydown.enter="handleEnter('leadTime', $event)"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
<div></div>
|
||||
<div class="destination-route-modal-footer">
|
||||
<basic-button :show-icon="false" @click="applyD2D">OK</basic-button>
|
||||
<basic-button variant="secondary" :show-icon="false" @click="dismissD2D">Cancel</basic-button>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import RouteDropdown from "@/components/UI/RouteDropdown.vue";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import Modal from "@/components/UI/Modal.vue";
|
||||
import DestinationRoute from "@/components/layout/edit/destination/DestinationRoute.vue";
|
||||
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||
import {parseNumberFromString} from "@/common.js";
|
||||
|
||||
export default {
|
||||
name: "DestinationMassRouteCell",
|
||||
components: {BasicButton, DestinationRoute, Modal, IconButton, RouteDropdown},
|
||||
props: {
|
||||
destination: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedRoute: null,
|
||||
modalState: false,
|
||||
rateD2d: null,
|
||||
leadTimeD2d: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showD2DWarn() {
|
||||
return (this.destination.rateD2d === null || this.destination.leadTimeD2d === null || this.destination.rateD2d === 0 || this.destination.leadTimeD2d === 0);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if(this.destination.isD2d) {
|
||||
this.selectedRoute = 'D2D_ROUTING'
|
||||
} else {
|
||||
this.selectedRoute = this.destination.selectedRoute;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleEnter(currentRef, event) {
|
||||
event.preventDefault();
|
||||
|
||||
// Define the navigation order
|
||||
const inputOrder = ['rate', 'leadTime'];
|
||||
|
||||
const currentIndex = inputOrder.indexOf(currentRef);
|
||||
|
||||
if(currentIndex >= inputOrder.length - 1) {
|
||||
this.validateLeadTimeD2d(event);
|
||||
this.applyD2D();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentIndex !== -1 && currentIndex < inputOrder.length - 1) {
|
||||
const nextRef = inputOrder[currentIndex + 1];
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs[nextRef]) {
|
||||
this.$refs[nextRef].focus();
|
||||
this.$refs[nextRef].select();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
updateSelectedRoute(route) {
|
||||
if(route === 'D2D_ROUTING') {
|
||||
this.destination.selectedRoute = null;
|
||||
this.destination.isD2d = true;
|
||||
} else {
|
||||
this.destination.selectedRoute = route;
|
||||
this.destination.isD2d = false;
|
||||
}
|
||||
},
|
||||
applyD2D() {
|
||||
this.destination.rateD2d = this.rateD2d;
|
||||
this.destination.leadTimeD2d = this.leadTimeD2d;
|
||||
this.dismissD2D()
|
||||
},
|
||||
dismissD2D() {
|
||||
this.modalState = false;
|
||||
},
|
||||
openD2DModal() {
|
||||
this.rateD2d = this.destination.rateD2d;
|
||||
this.leadTimeD2d = this.destination.leadTimeD2d;
|
||||
this.modalState = true;
|
||||
},
|
||||
validateRateD2d(event) {
|
||||
|
||||
const value = parseNumberFromString(event.target.value, 2);
|
||||
const validatedValue = Math.max(0, value);
|
||||
const stringified = validatedValue.toFixed(2);
|
||||
|
||||
this.rateD2d = validatedValue === 0 ? null : validatedValue;
|
||||
event.target.value = stringified;
|
||||
},
|
||||
validateLeadTimeD2d(event) {
|
||||
|
||||
const value = parseNumberFromString(event.target.value, 0);
|
||||
const validatedValue = Math.max(0, value);
|
||||
const stringified = validatedValue.toFixed();
|
||||
|
||||
this.leadTimeD2d = validatedValue === 0 ? null : validatedValue;
|
||||
event.target.value = stringified;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.destination-route-modal {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 1.6rem;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.destination-route-modal-footer {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dest-mass-route-cell {
|
||||
width: 35rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 400;
|
||||
gap: 0.8rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dest-mass-route-dropdown {
|
||||
width: 30rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
gap: 0.8rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: none;
|
||||
resize: none;
|
||||
font-family: inherit;
|
||||
font-size: 1.4rem;
|
||||
color: #002F54;
|
||||
width: 100%;
|
||||
min-width: 5rem;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
border: 0.2rem solid #E3EDFF;
|
||||
transition: all 0.1s ease;
|
||||
flex: 1 1 fit-content(80rem);
|
||||
}
|
||||
|
||||
.text-container:hover {
|
||||
background: #EEF4FF;
|
||||
border: 0.2rem solid #8DB3FE;
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
<template>
|
||||
<div class="dest-mass-route-row-container">
|
||||
<div class="dest-mass-route-row-supplier">
|
||||
<flag :iso="row.supplier.country.iso_code"/>
|
||||
{{ row.supplier.name }}
|
||||
</div>
|
||||
<destination-mass-route-cell :destination="dest" v-for="dest in row.destinations" class="dest-mass-route-row-dest"
|
||||
:key="dest.id">
|
||||
</destination-mass-route-cell>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
import Flag from "@/components/UI/Flag.vue";
|
||||
import RouteDropdown from "@/components/UI/RouteDropdown.vue";
|
||||
import DestinationMassRouteCell from "@/components/layout/edit/destination/mass/DestinationMassRouteCell.vue";
|
||||
|
||||
export default {
|
||||
name: "DestinationMassRouteRow",
|
||||
components: {DestinationMassRouteCell, RouteDropdown, Flag, Checkbox},
|
||||
props: {
|
||||
row: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
routes() {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleDropdown() {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
|
||||
.dest-mass-route-row-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1.6rem;
|
||||
padding: 1.2rem 2.4rem;
|
||||
justify-content: flex-start;
|
||||
border-bottom: 0.16rem solid #f3f4f6;
|
||||
transition: background-color 0.2s ease;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.dest-mass-route-row-container:hover {
|
||||
background-color: rgba(107, 134, 156, 0.05);
|
||||
}
|
||||
|
||||
|
||||
.dest-mass-route-row-supplier {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.8rem;
|
||||
width: 24rem;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
|
@ -263,7 +263,7 @@ export default {
|
|||
return this.modalType ? COMPONENT_TYPES[this.modalType] : null;
|
||||
},
|
||||
showProcessingModal() {
|
||||
return this.premiseEditStore.showProcessingModal || this.destinationEditStore.showProcessingModal ;
|
||||
return this.premiseEditStore.showProcessingModal || this.destinationEditStore.showProcessingModal;
|
||||
},
|
||||
shownProcessingMessage() {
|
||||
return this.processingMessage;
|
||||
|
|
@ -425,7 +425,7 @@ export default {
|
|||
}
|
||||
|
||||
if ((type === 'amount' || type === 'routes')) {
|
||||
if(dataSource !== -1)
|
||||
if (dataSource !== -1)
|
||||
ids = [dataSource];
|
||||
}
|
||||
|
||||
|
|
@ -439,13 +439,18 @@ export default {
|
|||
},
|
||||
async closeEditModalAction(action) {
|
||||
|
||||
if (this.modalType === "destinations") {
|
||||
if (this.modalType === 'amount' || this.modalType === 'routes' || this.modalType === "destinations") {
|
||||
|
||||
if (action === 'accept') {
|
||||
const destMatrix = this.$refs.modalComponent?.destMatrix;
|
||||
|
||||
if (destMatrix) {
|
||||
await this.destinationEditStore.massSetDestinations(destMatrix);
|
||||
if (this.modalType === "destinations") {
|
||||
const setMatrix = this.$refs.modalComponent?.destMatrix;
|
||||
|
||||
if (setMatrix) {
|
||||
await this.destinationEditStore.massSetDestinations(setMatrix);
|
||||
}
|
||||
} else {
|
||||
await this.destinationEditStore.massUpdateDestinations();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -503,9 +508,10 @@ export default {
|
|||
stackable: true
|
||||
};
|
||||
|
||||
if (type === 'amount' || type === 'routes' || type === 'destinations')
|
||||
if (type === 'amount' || type === 'routes' || type === 'destinations') {
|
||||
this.modalTitle = "Edit destinations";
|
||||
this.modalProps = {};
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
const premise = this.premiseEditStore.getById(id);
|
||||
|
|
@ -545,10 +551,8 @@ export default {
|
|||
this.modalTitle = "Edit destinations";
|
||||
this.modalProps = {type: type};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
,
|
||||
},
|
||||
|
||||
/* Animation hooks */
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ export const useDestinationEditStore = defineStore('destinationEdit', {
|
|||
state: () => ({
|
||||
destinations: null,
|
||||
loading: false,
|
||||
initialized: false,
|
||||
handlingCostMatrix: null,
|
||||
quantityMatrix: null,
|
||||
routeMatrix: null
|
||||
}),
|
||||
getters: {
|
||||
checkDestinationAssignment(state) {
|
||||
|
|
@ -45,9 +49,27 @@ export const useDestinationEditStore = defineStore('destinationEdit', {
|
|||
},
|
||||
showProcessingModal(state) {
|
||||
return state.loading;
|
||||
},
|
||||
getHandlingCostMatrix(state) {
|
||||
return state.handlingCostMatrix;
|
||||
},
|
||||
getQuantityMatrix(state) {
|
||||
return state.quantityMatrix;
|
||||
},
|
||||
getRouteMatrix(state) {
|
||||
return state.routeMatrix;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setHandlingCostMatrix(handlingCostMatrix) {
|
||||
this.handlingCostMatrix = handlingCostMatrix;
|
||||
},
|
||||
setQuantityMatrix(quantityMatrix) {
|
||||
this.quantityMatrix = quantityMatrix;
|
||||
},
|
||||
setRouteMatrix(routeMatrix) {
|
||||
this.routeMatrix = routeMatrix;
|
||||
},
|
||||
setupDestinations(premisses) {
|
||||
this.loading = true;
|
||||
|
||||
|
|
@ -55,6 +77,7 @@ export const useDestinationEditStore = defineStore('destinationEdit', {
|
|||
premisses.forEach(p => temp.set(p.id, p.destinations));
|
||||
this.destinations = temp;
|
||||
|
||||
this.initialized = true;
|
||||
this.loading = false;
|
||||
},
|
||||
async massSetDestinations(updateMatrix) {
|
||||
|
|
@ -85,11 +108,51 @@ export const useDestinationEditStore = defineStore('destinationEdit', {
|
|||
|
||||
this.loading = false;
|
||||
},
|
||||
async massUpdateDestinations(updateMatrix) {
|
||||
async massUpdateDestinations() {
|
||||
this.loading = true;
|
||||
|
||||
this.updateQuantity();
|
||||
this.updateHandlingCosts();
|
||||
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
updateHandlingCosts() {
|
||||
this.handlingCostMatrix.forEach(row => {
|
||||
const destinations = this.destinations.get(row.id);
|
||||
|
||||
if ((destinations ?? null) !== null) {
|
||||
|
||||
const destination = destinations.find(dest => dest.id === row.destinationId);
|
||||
|
||||
if((destination ?? null) !== null) {
|
||||
destination.disposal_costs = row.disposal_costs;
|
||||
destination.repackaging_costs = row.repackaging_costs;
|
||||
destination.handling_costs = row.handling_costs;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
updateQuantity() {
|
||||
this.quantityMatrix.forEach(row => {
|
||||
const destinations = this.destinations.get(row.id);
|
||||
|
||||
|
||||
if ((destinations ?? null) !== null) {
|
||||
row.destinations
|
||||
.filter(newDest => newDest.id !== null)
|
||||
.forEach(newDest => {
|
||||
|
||||
const found = destinations.find(dest => dest.id === newDest.id);
|
||||
if (found)
|
||||
found.annual_amount = newDest.annual_amount;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue