Intermediate commit

This commit is contained in:
Jan 2025-12-04 22:43:23 +01:00
parent 27b56bc92d
commit a40a8c6bb4
22 changed files with 1400 additions and 167 deletions

View file

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

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

View file

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

View file

@ -1,20 +1,22 @@
<template>
<div class="apps-container">
<div class="app-list-header">
<div>App</div>
<div>Groups</div>
<div>Action</div>
</div>
<div class="app-list">
<div class="app-list-header">
<div>App</div>
<div>Groups</div>
<div>Action</div>
</div>
<div class="app-list">
<app-list-item v-for="app in apps" :app="app" @delete-app="deleteApp"></app-list-item>
</div>
<app-list-item v-for="app in apps" :app="app" @delete-app="deleteApp"></app-list-item>
<modal :state="modalState">
<add-app @close="closeModal"></add-app>
</modal>
<basic-button icon="Plus" @click="modalState = true">New App</basic-button>
</div>
<modal :state="modalState">
<add-app @close="closeModal"></add-app>
</modal>
<basic-button icon="Plus" @click="modalState = true">New App</basic-button>
</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;

View file

@ -295,6 +295,7 @@ export default {
}
.bulk-operations-container {
margin: 2.4rem;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;

View file

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

View file

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

View file

@ -166,6 +166,9 @@ export default {
width: fit-content;
}
.properties-container {
padding: 2.4rem;
}
.property-item-enter-from {
opacity: 0;

View file

@ -236,6 +236,7 @@ export default {
.container-rate-container {
display: flex;
flex-direction: column;
padding: 1.6rem;
}
.container-rate-header {

View file

@ -1,8 +1,8 @@
<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>
<table-view ref="tableViewRef" :searchbar="false" :columns="columns" :data-source="fetch" @row-click="selectUser"
:mouse-over="true"></table-view>
</div>
<modal :state="showModal">
<edit-user @close="closeModal" v-model:user="selectedUser" :is-new-user="isNewUser"></edit-user>
@ -119,9 +119,9 @@ export default {
badgeResolver: (value) => {
const formattedValues = []
value.slice(0,5).forEach(v => formattedValues.push({text: v, variant: "secondary"}));
value.slice(0, 5).forEach(v => formattedValues.push({text: v, variant: "secondary"}));
if(value.length > 5)
if (value.length > 5)
formattedValues.push({text: "...", variant: "secondary"});
return formattedValues;
@ -141,6 +141,11 @@ export default {
<style scoped>
.users-container {
padding: 2.4rem;
}
.user-list {
margin-bottom: 2.4rem;
}

View file

@ -118,6 +118,7 @@ export default {
flex-direction: column;
gap: 1.6rem;
align-items: flex-start;
margin: 1.6rem;
}
.destination-edit-handling-cost-info {

View file

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

View file

@ -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',
},
]
}

View file

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

View file

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

View file

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

View file

@ -189,7 +189,7 @@ export default {
}
.dest-mass-quantity-row-applier {
width: 5rem;
width: 7rem;
}
.dest-mass-quantity-row-supplier {

View file

@ -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>
<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 v-if="generalError">
<div class="destination-mass-route-info">
<ph-warning size="18px"></ph-warning>
The routing data is faulty. Please contact support.
You can try to solve the problem by first deleting all destinations and then creating them again.
</div>
</div>
<div v-else class="dest-mass-route-table-wrapper">
<div class="dest-mass-route-table-header-wrapper"
ref="headerWrapper"
@scroll="syncScroll('header')">
<div class="dest-mass-route-table-header">
<div class="dest-mass-route-table-header-supplier"></div>
<div class="dest-mass-route-table-header-dest"
:key="`${dest.id}`"
v-for="dest in destPool">
<div></div>
<div>{{ toNode(dest.destination_node, 6) }}</div>
</div>
</div>
</div>
<div class="dest-mass-route-table">
<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) {
if (existingDest) {
existingDest.disabled = false;
existingDest.ids.push(premD.id);
existingDest = {
ids: [],
destinationNodeId: premD.destination_node.id,
destinationName: premD.destination_node.name,
routes: this.buildRoutes(premD.routes)
}
// premdD.routes.push
rowDestinations.push(existingDest);
/* add route ids to routes */
this.verifyRoutes(existingDest, premD)
}
/* 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
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>

View file

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

View file

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

View file

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

View file

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