FRONTEND/BACKEND: Refactor and extend error-handling capabilities; update ErrorStore with new actions like clearErrors and expanded error fields (traceCombined); rework PremiseEditStore for enhanced destination and premise state management; introduce mass destination editing functionality; modify UI components (CalculationListItem.vue) for improved styling and base64-encoded route handling; backend adjustments include adding support for externalMappingId in NodeTransformer and RouteNodeRepository.
This commit is contained in:
parent
6eaf3d4abc
commit
1690d869d6
40 changed files with 2321 additions and 446 deletions
|
|
@ -1,4 +1,5 @@
|
|||
<template>
|
||||
<error-notification></error-notification>
|
||||
<the-header></the-header>
|
||||
<router-view v-slot="slotProps">
|
||||
<transition name="route" mode="out-in">
|
||||
|
|
@ -11,9 +12,11 @@
|
|||
|
||||
|
||||
import TheHeader from "@/components/layout/TheHeader.vue";
|
||||
import ErrorNotification from "@/components/UI/ErrorNotifcation.vue";
|
||||
|
||||
export default {
|
||||
components: {TheHeader}
|
||||
components: {ErrorNotification, TheHeader},
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -75,4 +78,8 @@ body {
|
|||
transform: translateX(0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export default{
|
|||
variant: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
validator: (value) => ['primary', 'secondary', 'grey', 'exception'].includes(value)
|
||||
validator: (value) => ['primary', 'secondary', 'grey', 'exception', 'skeleton'].includes(value)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -89,6 +89,18 @@ export default{
|
|||
border-color: transparent;
|
||||
}
|
||||
|
||||
.batch--skeleton {
|
||||
border: 0.1rem solid #002F54;
|
||||
background-color: transparent;
|
||||
color: #002F54;
|
||||
}
|
||||
|
||||
.batch--skeleton-grey {
|
||||
border: 0.1rem solid #6b7280;
|
||||
background-color: transparent;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.batch-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
v-if="!link"
|
||||
:class="buttonClasses"
|
||||
class="btn"
|
||||
@click="$emit('click', $event)"
|
||||
@click="handleClick"
|
||||
>
|
||||
<component
|
||||
v-if="showIcon"
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
<span class="btn-text"><slot></slot></span>
|
||||
</button>
|
||||
<router-link v-else :to="to" :class="buttonClasses"
|
||||
class="btn">
|
||||
class="btn" :disabled="disabled">
|
||||
<component
|
||||
v-if="showIcon"
|
||||
:is="iconComponent"
|
||||
|
|
@ -78,6 +78,12 @@ export default defineComponent({
|
|||
const iconName = this.icon.charAt(0).toUpperCase() + this.icon.slice(1)
|
||||
return `Ph${iconName}`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick(event) {
|
||||
if(this.disabled) return;
|
||||
this.$emit('click', event);
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
165
src/frontend/src/components/UI/ErrorNotifcation.vue
Normal file
165
src/frontend/src/components/UI/ErrorNotifcation.vue
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
<template>
|
||||
<teleport to="body">
|
||||
<transition
|
||||
name="error-container"
|
||||
tag="div"
|
||||
class="error-notification-container">
|
||||
<div class="error-notification" v-if="error">
|
||||
<div>
|
||||
<ph-warning size="24"></ph-warning>
|
||||
</div>
|
||||
<div class="error-message">
|
||||
<div class="error-message-title">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="error-message-content">
|
||||
{{ message }}
|
||||
</div>
|
||||
<div class="error-view-trace" v-if="trace" @click="activateTrace">
|
||||
View trace
|
||||
</div>
|
||||
</div>
|
||||
<div class="icon-error-notification">
|
||||
<ph-x size="24" @click="close"></ph-x>
|
||||
</div>
|
||||
<modal :z-index="9001" :state="showTrace"><trace-view :error="error" @close="deactivateTrace"></trace-view></modal>
|
||||
</div>
|
||||
</transition>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {useErrorStore} from "@/store/error.js";
|
||||
import {mapStores} from "pinia";
|
||||
import TraceView from "@/components/layout/TraceView.vue";
|
||||
import Modal from "@/components/UI/Modal.vue";
|
||||
|
||||
export default {
|
||||
name: "ErrorNotification",
|
||||
components: {Modal, TraceView},
|
||||
data() {
|
||||
return {
|
||||
showTrace: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useErrorStore),
|
||||
title() {
|
||||
return this.error?.title || 'Error';
|
||||
},
|
||||
message() {
|
||||
return this.error?.message || 'An unexpected error occurred';
|
||||
},
|
||||
trace() {
|
||||
return !((this.error?.trace ?? null) === null) || !((this.error?.traceCombined ?? null) === null);
|
||||
},
|
||||
error() {
|
||||
return this.errorStore.lastError;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.errorStore.clearErrors();
|
||||
},
|
||||
activateTrace() {
|
||||
this.showTrace = true;
|
||||
},
|
||||
deactivateTrace() {
|
||||
this.showTrace = false;
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.error-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.error-message-title {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.error-message-content {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.error-view-trace {
|
||||
font-size: 1.4rem;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.error-notification-container {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
z-index: 9000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.error-notification {
|
||||
min-width: 30rem;
|
||||
max-width: 100rem;
|
||||
padding: 1.6rem;
|
||||
border-radius: 0.8rem;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1.6rem;
|
||||
|
||||
background-color: #BC2B72;
|
||||
color: #ffffff;
|
||||
|
||||
font-size: 1.4rem;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.icon-error-notification {
|
||||
cursor: pointer;
|
||||
transition: all 0.1s ease-in-out;
|
||||
background-color: #BC2B72;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.icon-error-notification:hover, .icon-error-notification:active {
|
||||
cursor: pointer;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
|
||||
/* Transition animations */
|
||||
.error-container-enter-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.error-container-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.error-container-enter-from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, 100%);
|
||||
}
|
||||
|
||||
.error-container-leave-to {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, 100%);
|
||||
}
|
||||
|
||||
.error-container-move {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
</style>
|
||||
131
src/frontend/src/components/UI/MassEditDialog.vue
Normal file
131
src/frontend/src/components/UI/MassEditDialog.vue
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
<template>
|
||||
<transition
|
||||
name="list-edit-transition"
|
||||
tag="div"
|
||||
class="list-edit-container"
|
||||
|
||||
>
|
||||
<div v-if="show" class="list-edit">
|
||||
<div class="icon-container"><ph-pencil-simple size="24" /><span class="number-circle">{{selectCount}}</span></div>
|
||||
<div class="list-edit-button" @click="handleAction('material')">Material</div>
|
||||
<div class="list-edit-button" @click="handleAction('price')">Price</div>
|
||||
<div class="list-edit-button" @click="handleAction('packaging')">Packaging</div>
|
||||
<div class="list-edit-button" @click="handleAction('supplier')">Supplier</div>
|
||||
<div class="list-edit-button" @click="handleAction('destinations')">Destinations & Routes</div>
|
||||
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import {PhPencilSimple} from "@phosphor-icons/vue";
|
||||
|
||||
export default{
|
||||
name: "MassEditDialog",
|
||||
components: {PhPencilSimple, IconButton},
|
||||
emits: ['action'],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selectCount: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleAction(action) {
|
||||
this.$emit('action', action);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.list-edit-container {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
|
||||
.list-edit-button {
|
||||
padding: 0.8rem;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
.list-edit-button:hover {
|
||||
color: #FFFFFF;
|
||||
background-color: #002F54;
|
||||
border-radius: 0.8rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.list-edit {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 3.6rem;
|
||||
background-color: #5AF0B4;
|
||||
border-radius: 0.8rem;
|
||||
flex: 0 0 auto;
|
||||
padding: 0.8rem 1.6rem;
|
||||
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
|
||||
font-size: 1.4rem;
|
||||
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
|
||||
.number-circle {
|
||||
position: relative;
|
||||
top: 0.8rem;
|
||||
left: -1.8rem;
|
||||
display: inline-block;
|
||||
width: 1.8rem;
|
||||
height: 1.8rem;
|
||||
border-radius: 50%;
|
||||
/*background-color: #002F54;
|
||||
color: white;*/
|
||||
color: #002F54;
|
||||
text-align: center;
|
||||
line-height: 1.6rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
/* Transition animations */
|
||||
.list-edit-transition-enter-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.list-edit-transition-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.list-edit-transition-enter-from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, 100%); /* beide Transforms kombinieren */
|
||||
}
|
||||
|
||||
.list-edit-transition-leave-to {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, 100%); /* beide Transforms kombinieren */
|
||||
}
|
||||
|
||||
.list-edit-transition-move {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
@keydown.esc="closeOnEscape && close()"
|
||||
tabindex="-1"
|
||||
ref="modalOverlay"
|
||||
:style="modalAddStyle"
|
||||
>
|
||||
<box @click.stop>
|
||||
<slot></slot>
|
||||
|
|
@ -36,9 +37,19 @@ export default {
|
|||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
zIndex: {
|
||||
type: Number,
|
||||
validators: (value) => value >= 0,
|
||||
default: 5000,
|
||||
},
|
||||
},
|
||||
emits: ['close'],
|
||||
computed: {
|
||||
modalAddStyle() {
|
||||
return {
|
||||
zIndex: this.zIndex,
|
||||
}
|
||||
},
|
||||
isVisible() {
|
||||
return this.state;
|
||||
}
|
||||
|
|
@ -90,7 +101,7 @@ export default {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
z-index: 5000;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div>
|
||||
<modal :state="state">
|
||||
<modal :state="state" :z-index="zIndex">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-dialog-title sub-header">{{ title }}</div>
|
||||
<div class="modal-dialog-message">{{ message }}</div>
|
||||
|
|
@ -25,6 +25,12 @@ export default {
|
|||
components: {Modal, BasicButton},
|
||||
emits: ['click'],
|
||||
props: {
|
||||
zIndex: {
|
||||
type: Number,
|
||||
required: false,
|
||||
validators: (value) => value >= 0,
|
||||
default: 5000
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
'slide-in': activeTab === index && hasChanged,
|
||||
'slide-out': activeTab !== index && hasChanged
|
||||
}]"
|
||||
v-show="activeTab === index"
|
||||
>
|
||||
<!-- Render component if provided -->
|
||||
<component
|
||||
|
|
@ -94,6 +95,10 @@ export default {
|
|||
<style scoped>
|
||||
.tab-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.tab-headers {
|
||||
|
|
@ -101,9 +106,9 @@ export default {
|
|||
border-bottom: 1px solid rgba(107, 134, 156, 0.2);
|
||||
background-color: transparent;
|
||||
position: relative;
|
||||
flex-shrink: 0; /* Don't let headers shrink */
|
||||
}
|
||||
|
||||
|
||||
.tab-header {
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
|
|
@ -119,13 +124,11 @@ export default {
|
|||
z-index: 2;
|
||||
}
|
||||
|
||||
|
||||
.tab-header:hover {
|
||||
background-color: rgba(107, 134, 156, 0.05);
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
|
||||
.tab-header.active {
|
||||
color: #002F54;
|
||||
border-bottom-color: #5AF0B4;
|
||||
|
|
@ -135,17 +138,15 @@ export default {
|
|||
.tab-content {
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
min-height: 200px;
|
||||
flex: 1; /* Take remaining space */
|
||||
min-height: 0; /* Allow shrinking */
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab-pane {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
|
@ -155,10 +156,6 @@ export default {
|
|||
.tab-pane.active {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
position: relative;
|
||||
top: auto;
|
||||
left: auto;
|
||||
right: auto;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
|
|
@ -182,7 +179,6 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
/* Keyframe Animations */
|
||||
@keyframes slideOutUp {
|
||||
0% {
|
||||
opacity: 1;
|
||||
|
|
@ -193,5 +189,4 @@ export default {
|
|||
transform: translateX(30px);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -1,16 +1,22 @@
|
|||
<template>
|
||||
<div class="trace-view-container" :class="trace ? '' : 'trace-view-container--no-trace'">
|
||||
<div class="trace-view-header">
|
||||
<h3 class="sub-header">{{ code }}</h3>
|
||||
<h3 class="sub-header">{{ title }} <br><span class="trace-view-message" v-if="showMessage">{{ message }}</span>
|
||||
</h3>
|
||||
<icon-button icon="x" @click="$emit('close')"></icon-button>
|
||||
</div>
|
||||
<div v-if="showMessage" class="trace-view-message">{{ message }}</div>
|
||||
<div v-if="trace" class="trace-view">
|
||||
<div class="trace-view-exception">{{ exception }}</div>
|
||||
<div :class="highlightClasses(traceItem)" v-for="traceItem of trace">at {{ fullClassName(traceItem) }} (<span
|
||||
class="trace-view-file">{{ fileName(traceItem) }}</span>)
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="traceCombined" class="trace-view">
|
||||
<div class="trace-view-exception">{{ traceCombinedException }}</div>
|
||||
<div class="trace-item" v-for="traceItem of traceCombinedItems">
|
||||
<div v-html="highlightFilenames(traceItem)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -18,10 +24,11 @@
|
|||
|
||||
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import {PhWarning} from "@phosphor-icons/vue";
|
||||
|
||||
export default {
|
||||
name: "TraceView",
|
||||
components: {IconButton, BasicButton},
|
||||
components: {PhWarning, IconButton, BasicButton},
|
||||
emits: ['close'],
|
||||
props: {
|
||||
error: {
|
||||
|
|
@ -30,11 +37,23 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
traceCombined() {
|
||||
return this.error.traceCombined;
|
||||
},
|
||||
traceCombinedException() {
|
||||
return this.error.traceCombined?.split('\n')[0];
|
||||
},
|
||||
traceCombinedItems() {
|
||||
return this.error.traceCombined?.split('\n').slice(1);
|
||||
},
|
||||
trace() {
|
||||
return this.error.trace?.stackTrace;
|
||||
return this.error.trace;
|
||||
},
|
||||
exception() {
|
||||
return this.error.trace?.errorMessage;
|
||||
return this.error.code;
|
||||
},
|
||||
title() {
|
||||
return this.error.title;
|
||||
},
|
||||
code() {
|
||||
return this.error.code;
|
||||
|
|
@ -47,15 +66,35 @@ export default {
|
|||
},
|
||||
|
||||
},
|
||||
created() {
|
||||
console.log(this.error);
|
||||
console.log(this.error.trace);
|
||||
},
|
||||
methods: {
|
||||
highlightClasses(traceItem) {
|
||||
return traceItem.className.includes('de.avatic.lcc') ? 'highlight-trace-item' : 'trace-item';
|
||||
return traceItem.className?.includes('de.avatic.lcc') ? 'highlight-trace-item' : 'trace-item';
|
||||
},
|
||||
fullClassName(traceItem) {
|
||||
return `${traceItem.className}.${traceItem.methodName}`;
|
||||
},
|
||||
fileName(traceItem) {
|
||||
return `${traceItem.fileName}:${traceItem.lineNumber}`;
|
||||
},
|
||||
highlightFilenames(text) {
|
||||
if (!text) return text;
|
||||
|
||||
// Regex to match file paths ending with .vue or .js
|
||||
// This will match paths like http://localhost:5173/src/pages/CalculationSingleEdit.vue
|
||||
const filePathRegex = /(https?:\/\/[^\s]+\/)?([^\/\s]+\.(vue|js))/g;
|
||||
|
||||
return text.replace(filePathRegex, (match, path, filename) => {
|
||||
// If there's a path, keep it but only bold the filename
|
||||
if (path) {
|
||||
return `${path}<strong class="highlighted-filename">${filename}</strong>`;
|
||||
}
|
||||
// If it's just a filename, bold it
|
||||
return `<strong class="highlighted-filename">${filename}</strong>`;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -66,12 +105,14 @@ export default {
|
|||
@import url('https://fonts.googleapis.com/css2?family=Cal+Sans&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap');
|
||||
|
||||
.highlight-trace-item {
|
||||
background-color: #c3cfdf;
|
||||
|
||||
color: #002F54;
|
||||
background-color: #F2F2F2;
|
||||
padding-left: 1.6rem;
|
||||
}
|
||||
|
||||
.trace-item:hover {
|
||||
background-color: rgba(107, 134, 156, 0.05);
|
||||
.trace-item {
|
||||
padding-left: 1.6rem;
|
||||
}
|
||||
|
||||
.trace-view {
|
||||
|
|
@ -79,7 +120,7 @@ export default {
|
|||
font-family: Roboto Mono, monospace;
|
||||
font-size: 1.2rem;
|
||||
flex: 1;
|
||||
padding: 1.6rem;
|
||||
padding: 0 1.6rem;
|
||||
}
|
||||
|
||||
.trace-view-header {
|
||||
|
|
@ -89,8 +130,7 @@ export default {
|
|||
}
|
||||
|
||||
.trace-view-message {
|
||||
font-size: 1.4rem;
|
||||
color: #002F54;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.trace-view-file {
|
||||
|
|
@ -99,8 +139,8 @@ export default {
|
|||
}
|
||||
|
||||
.trace-view-exception {
|
||||
font-family: inherit;
|
||||
font-weight: 500;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.trace-view-container {
|
||||
|
|
|
|||
328
src/frontend/src/components/layout/bulkedit/BulkEditRow.vue
Normal file
328
src/frontend/src/components/layout/bulkedit/BulkEditRow.vue
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
<template>
|
||||
<div class="bulk-edit-row">
|
||||
<div class="edit-calculation-checkbox-cell">
|
||||
<checkbox :checked="isSelected" @checkbox-changed="updateSelected"></checkbox>
|
||||
</div>
|
||||
<div class="edit-calculation-cell--material" :class="copyModeClass"
|
||||
@click="action('material')">
|
||||
<div class="edit-calculation-cell-line">{{ premise.material.part_number }}</div>
|
||||
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="premise.material.name">
|
||||
{{ premise.material.name }}
|
||||
</div>
|
||||
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="premise.hs_code">
|
||||
<PhBarcode/>
|
||||
{{ premise.material.hs_code }}
|
||||
</div>
|
||||
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="premise.tariff_rate">
|
||||
<PhPercent/>
|
||||
{{ premise.tariff_rate }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="edit-calculation-cell--price" :class="copyModeClass" v-if="showPrice"
|
||||
@click="action('price')">
|
||||
<div class="edit-calculation-cell-line">{{ premise.material_cost }} EUR</div>
|
||||
<div class="edit-calculation-cell-line edit-calculation-cell-subline" >
|
||||
<basic-badge icon="plus" variant="primary">FCA FEE</basic-badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="edit-calculation-empty" :class="copyModeClass" v-else @click="action('price')">
|
||||
<basic-badge variant="exception" icon="warning">MISSING</basic-badge>
|
||||
</div>
|
||||
<div v-if="showHu" class="edit-calculation-cell edit-calculation-cell--packaging" :class="copyModeClass"
|
||||
@click="action('packaging')">
|
||||
<div class="edit-calculation-cell-line">
|
||||
<PhVectorThree/>
|
||||
{{ premise.handling_unit.length }} x
|
||||
{{ premise.handling_unit.width }} x
|
||||
{{ premise.handling_unit.height }} {{ premise.handling_unit.dimension_unit }}
|
||||
</div>
|
||||
<div class="edit-calculation-cell-line">
|
||||
<PhBarbell/>
|
||||
<span>{{ premise.handling_unit.weight }} {{ premise.handling_unit.weight_unit }}</span>
|
||||
</div>
|
||||
<div class="edit-calculation-cell-line">
|
||||
<PhHash/>
|
||||
{{ premise.handling_unit.content_unit_count }} pcs.
|
||||
</div>
|
||||
<div class="edit-calculation-packaging-badges">
|
||||
<basic-badge v-if="premise.is_stackable" variant="primary" icon="stack">STACKABLE</basic-badge>
|
||||
<basic-badge v-if="premise.is_mixable" variant="skeleton" icon="shuffle">MIXABLE</basic-badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="edit-calculation-empty" :class="copyModeClass" v-else
|
||||
@click="action('packaging')">
|
||||
<basic-badge variant="exception" icon="warning">MISSING</basic-badge>
|
||||
</div>
|
||||
<div class="edit-calculation-cell--supplier" :class="copyModeClass"
|
||||
@click="action('supplier')">
|
||||
<div class="edit-calculation-cell--supplier-container" v-if="premise.supplier">
|
||||
<!-- <div class="edit-calculation-cell--supplier-flag">-->
|
||||
<!-- <flag :iso="premise.supplier.country.iso_code" size="m"></flag>-->
|
||||
<!-- </div>-->
|
||||
<div class="calculation-list-supplier-data">
|
||||
<div class="edit-calculation-cell-line">{{ premise.supplier.name }}</div>
|
||||
<!-- <div class="edit-calculation-cell-subline"> {{ premise.supplier.address }}</div>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="edit-calculation-cell--destination" :class="copyModeClass" v-if="showDestinations"
|
||||
@click="action('destinations')">
|
||||
<div class="edit-calculation-cell-line">
|
||||
<span class="number-circle"> {{ destinationsCount }} </span> Destinations
|
||||
</div>
|
||||
<div class="edit-calculation-cell-subline" v-for="name in destinationNames"> {{ name }}</div>
|
||||
</div>
|
||||
<div class="edit-calculation-empty" :class="copyModeClass" v-else
|
||||
@click="action('destinations')">
|
||||
<basic-badge variant="exception" icon="warning">MISSING</basic-badge>
|
||||
</div>
|
||||
|
||||
<div class="edit-calculation-actions-cell">
|
||||
<icon-button icon="pencil-simple" help-text="Edit this calculation" help-text-position="left"
|
||||
@click="editSingle"></icon-button>
|
||||
<icon-button icon="x" help-text="Remove from mass edit" help-text-position="left" @click="remove"></icon-button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import Flag from "@/components/UI/Flag.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {usePremiseEditStore} from "@/store/premiseEdit.js";
|
||||
import BasicBadge from "@/components/UI/BasicBadge.vue";
|
||||
import {
|
||||
PhBarbell,
|
||||
PhBarcode,
|
||||
PhEmpty,
|
||||
PhFactory,
|
||||
PhHash, PhMapPin,
|
||||
PhPercent,
|
||||
PhVectorThree,
|
||||
PhVectorTwo
|
||||
} from "@phosphor-icons/vue";
|
||||
import {UrlSafeBase64} from "@/common.js";
|
||||
|
||||
|
||||
export default {
|
||||
name: "BulkEditRow",
|
||||
emits: ['remove', 'action'],
|
||||
components: {
|
||||
PhMapPin,
|
||||
PhFactory,
|
||||
PhPercent,
|
||||
PhBarcode, PhBarbell, PhHash, PhVectorThree, PhVectorTwo, PhEmpty, BasicBadge, Flag, IconButton, Checkbox
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
copyMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
destinationsCount() {
|
||||
return this.premise.destinations.length;
|
||||
},
|
||||
destinationsText() {
|
||||
return this.premise.destinations.map(d => d.destination_node.name).join(', ');
|
||||
},
|
||||
destinationNames() {
|
||||
const spliceCnt = ((this.premise.destinations.length === 4) ? 4 : 3);
|
||||
const names = this.premise.destinations.map(d => d.destination_node.name).slice(0, spliceCnt);
|
||||
if (this.premise.destinations.length > 4) {
|
||||
names.push('and more ...');
|
||||
}
|
||||
|
||||
return names;
|
||||
},
|
||||
showDestinations() {
|
||||
return (this.destinationsCount > 0);
|
||||
},
|
||||
showHu() {
|
||||
return (this.hu.width && this.hu.length && this.hu.height && this.hu.weight && this.hu.content_unit_count)
|
||||
},
|
||||
showPrice() {
|
||||
return (this.premise.material_cost)
|
||||
},
|
||||
isSelected() {
|
||||
return this.premise.selected;
|
||||
},
|
||||
hu() {
|
||||
return this.premise.handling_unit;
|
||||
},
|
||||
...mapStores(usePremiseEditStore),
|
||||
premise() {
|
||||
const data = this.premiseEditStore.getById(this.id);
|
||||
return data;
|
||||
},
|
||||
copyModeClass() {
|
||||
if (this.copyMode) {
|
||||
return 'edit-calculation-cell--copy-mode';
|
||||
}
|
||||
return 'edit-calculation-cell';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
updateSelected(value) {
|
||||
this.premiseEditStore.setSelectTo([this.id], value);
|
||||
},
|
||||
editSingle() {
|
||||
const bulkQuery = this.$route.params.ids;
|
||||
const urlStr = new UrlSafeBase64().encodeIds([this.id]);
|
||||
this.$router.push({name: 'bulk-single-edit', params: {id: urlStr, ids: bulkQuery}});
|
||||
},
|
||||
action(action) {
|
||||
this.$emit('action', {id: this.id, action: action});
|
||||
},
|
||||
remove() {
|
||||
this.premiseEditStore.removePremise(this.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
|
||||
.bulk-edit-row {
|
||||
display: grid;
|
||||
grid-template-columns: 6rem 1fr 1fr 1.5fr 1.5fr 1.5fr 10rem;
|
||||
gap: 1.6rem;
|
||||
padding: 0 2.4rem;
|
||||
border-bottom: 0.16rem solid #f3f4f6;
|
||||
align-items: center;
|
||||
transition: background-color 0.2s ease;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
height: 14rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/*.bulk-edit-row:hover {
|
||||
// background-color: rgba(107, 134, 156, 0.05);
|
||||
//} */
|
||||
|
||||
.bulk-edit-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.edit-calculation-checkbox-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.edit-calculation-cell {
|
||||
padding: 0.8rem;
|
||||
border-radius: 0.8rem;
|
||||
height: 90%;
|
||||
}
|
||||
|
||||
.edit-calculation-cell:hover {
|
||||
cursor: pointer;
|
||||
background-color: rgba(107, 134, 156, 0.05);
|
||||
border-radius: 0.8rem;
|
||||
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.edit-calculation-cell--copy-mode {
|
||||
padding: 0.8rem;
|
||||
border-radius: 0.8rem;
|
||||
height: 90%;
|
||||
}
|
||||
|
||||
.edit-calculation-cell--copy-mode:hover {
|
||||
cursor: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOCIgaGVpZ2h0PSIyOCIgdmlld0JveD0iMCAwIDI1NiAyNTYiPgogIDxyZWN0IHg9Ijg0IiB5PSIzMiIgd2lkdGg9IjEzNiIgaGVpZ2h0PSIxMzYiIGZpbGw9IndoaXRlIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iOCIgcng9IjQiLz4KICA8cmVjdCB4PSIzNiIgeT0iODQiIHdpZHRoPSIxMzYiIGhlaWdodD0iMTM2IiBmaWxsPSJ3aGl0ZSIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjgiIHJ4PSI0Ii8+Cjwvc3ZnPg==") 12 12, pointer;
|
||||
|
||||
/*cursor: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0iI2ZmZmZmZiIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjQiIHZpZXdCb3g9IjAgMCAyNTYgMjU2Ij4KICA8cGF0aCBkPSJNMjAwLDMySDE2My43NGE0Ny45Miw0Ny45MiwwLDAsMC03MS40OCwwSDU2QTE2LDE2LDAsMCwwLDQwLDQ4VjIxNmExNiwxNiwwLDAsMCwxNiwxNkgyMDBhMTYsMTYsMCwwLDAsMTYtMTZWNDhBMTYsMTYsMCwwLDAsMjAwLDMyWm0tNzIsMGEzMiwzMiwwLDAsMSwzMiwzMkg5NkEzMiwzMiwwLDAsMSwxMjgsMzJaIj48L3BhdGg+Cjwvc3ZnPg==") 12 12, pointer;
|
||||
*/
|
||||
|
||||
|
||||
background-color: rgba(107, 134, 156, 0.05);
|
||||
border-radius: 0.8rem;
|
||||
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.edit-calculation-cell--price {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.edit-calculation-cell--supplier {
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
|
||||
.edit-calculation-cell--supplier-container {
|
||||
display: flex;
|
||||
height: fit-content;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.edit-calculation-cell--supplier-flag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.edit-calculation-cell--packaging, .edit-calculation-cell--material, .edit-calculation-cell--destination {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.edit-calculation-cell-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.edit-calculation-cell-subline {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.edit-calculation-packaging-badges {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.edit-calculation-empty {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.edit-calculation-actions-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.number-circle {
|
||||
display: inline-block;
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
border-radius: 50%;
|
||||
background-color: #002F54;
|
||||
color: white;
|
||||
text-align: center;
|
||||
line-height: 1.6rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
|
||||
<div class="calculation-list-row">
|
||||
<div class="calculation-list-checkbox-cell">
|
||||
<div class="edit-calculation-checkbox-cell">
|
||||
<checkbox :checked="checked" @checkbox-changed="updateCheckBox"></checkbox>
|
||||
</div>
|
||||
<div class="calculation-list-material-cell">
|
||||
<div class="edit-calculation-material-cell">
|
||||
<div class="material-name">{{ premise.material.name }}</div>
|
||||
<div class="material-code">{{ premise.material.part_number }}</div>
|
||||
</div>
|
||||
|
|
@ -39,6 +39,7 @@ import Checkbox from "@/components/UI/Checkbox.vue";
|
|||
import {mapStores} from "pinia";
|
||||
import {usePremiseStore} from "@/store/premise.js";
|
||||
import Flag from "@/components/UI/Flag.vue";
|
||||
import {UrlSafeBase64} from "@/common.js";
|
||||
|
||||
export default {
|
||||
name: "CalculationListItem",
|
||||
|
|
@ -94,7 +95,8 @@ export default {
|
|||
this.premise.checked = checked;
|
||||
},
|
||||
editClick() {
|
||||
this.$router.push({name: 'edit', params: {id: this.id}});
|
||||
const urlStr = new UrlSafeBase64().encodeIds([this.id]);
|
||||
this.$router.push({name: 'edit', params: {id: urlStr}});
|
||||
},
|
||||
deleteClick() {
|
||||
|
||||
|
|
@ -126,13 +128,13 @@ export default {
|
|||
border-bottom: none;
|
||||
}
|
||||
|
||||
.calculation-list-checkbox-cell {
|
||||
.edit-calculation-checkbox-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.calculation-list-material-cell {
|
||||
.edit-calculation-material-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
|
|
@ -159,6 +161,9 @@ export default {
|
|||
.calculation-list-supplier-flag {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.calculation-list-supplier-data {
|
||||
|
|
@ -205,12 +210,12 @@ export default {
|
|||
"actions";
|
||||
}
|
||||
|
||||
.calculation-list-checkbox-cell {
|
||||
.edit-calculation-checkbox-cell {
|
||||
grid-area: checkbox;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.calculation-list-material-cell {
|
||||
.edit-calculation-material-cell {
|
||||
grid-area: material;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,25 +12,30 @@
|
|||
:reset-on-select="true"></autosuggest-searchbar>
|
||||
</div>
|
||||
|
||||
<div class="list-container">
|
||||
<!-- <div class="list-container">-->
|
||||
<transition-group name="list-item" class="list-container" tag="div" mode="out-in">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="destination-list-header">
|
||||
<div class="destination-list-header" key="header">
|
||||
<div>Destination</div>
|
||||
<div>Annual quantity</div>
|
||||
<div>Selected route</div>
|
||||
<div>Action</div>
|
||||
</div>
|
||||
|
||||
<div v-if="premiseEditStore.selectedPremise.destinations.length !== 0">
|
||||
<destination-item v-for="destination in premiseEditStore.selectedPremise.destinations" :key="destination.id"
|
||||
|
||||
<!-- TODO: straight this up -->
|
||||
<div v-if="showDestinationsList">
|
||||
<destination-item v-for="destination in destinations" :key="destination.id"
|
||||
:id="destination.id" :destination="destination" @delete="deleteDestination"
|
||||
@edit="editDestination"></destination-item>
|
||||
@edit="editDestination" class="list-item-wrapper"></destination-item>
|
||||
</div>
|
||||
<div v-else class="empty-container">
|
||||
<div v-else class="empty-container" key="empty">
|
||||
<span class="space-around">No Destinations found.</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</transition-group>
|
||||
<!-- </div>-->
|
||||
<modal :state="editDestinationModalState" >
|
||||
<destination-edit @accept="deselectDestination(true)" @discard="deselectDestination(false)"></destination-edit>
|
||||
</modal>
|
||||
|
|
@ -61,19 +66,19 @@ export default {
|
|||
Modal,
|
||||
DestinationItem, CalculationListItem, Checkbox, Spinner, BasicButton, AutosuggestSearchbar, IconButton
|
||||
},
|
||||
props: {
|
||||
destinations: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editDestinationModalState: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(usePremiseEditStore, useNodeStore)
|
||||
...mapStores(usePremiseEditStore, useNodeStore),
|
||||
destinations() {
|
||||
return this.premiseEditStore.getDestinationsView;
|
||||
},
|
||||
showDestinationsList() {
|
||||
return this.destinations !== null && this.destinations.length > 0;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchDestinations(query) {
|
||||
|
|
@ -85,17 +90,18 @@ export default {
|
|||
},
|
||||
async addDestination(node) {
|
||||
console.log(node)
|
||||
const [id] = await this.premiseEditStore.addDestination(node.id);
|
||||
console.log(id);
|
||||
// todo add to massEdit copy only
|
||||
const [id] = await this.premiseEditStore.addDestination(node);
|
||||
this.editDestination(id);
|
||||
},
|
||||
deleteDestination(id) {
|
||||
// todo delete from to massEdit copy only
|
||||
this.premiseEditStore.deleteDestination(id);
|
||||
},
|
||||
|
||||
editDestination(id) {
|
||||
if (id && this.destinations.some(d => d.id === id)) {
|
||||
this.premiseEditStore.selectDestinations([id]);
|
||||
// TODO refactor.
|
||||
if (id && this.premiseEditStore.getDestinationById(id) !== null) {
|
||||
this.premiseEditStore.selectDestination(id);
|
||||
this.editDestinationModalState = true;
|
||||
}
|
||||
},
|
||||
|
|
@ -164,6 +170,119 @@ export default {
|
|||
letter-spacing: 0.08rem;
|
||||
}
|
||||
|
||||
|
||||
/* List Animation Styles
|
||||
|
||||
.list-item-wrapper {
|
||||
transition: all 0.3s ease;
|
||||
}*/
|
||||
|
||||
|
||||
/* Enter animations */
|
||||
|
||||
.list-item-enter-active {
|
||||
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
|
||||
.list-item-leave-active {
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
|
||||
.list-item-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px) scale(0.95);
|
||||
}
|
||||
|
||||
|
||||
.list-item-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(20px) scale(0.95);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Move animation for when items shift position */
|
||||
|
||||
.list-item-move {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
|
||||
/* Optional: Add a subtle hover animation for list items
|
||||
|
||||
.list-item-wrapper:hover {
|
||||
|
||||
transform: translateY(-2px);
|
||||
|
||||
box-shadow: 0 0.6rem 1.2rem -0.2rem rgba(0, 0, 0, 0.15);
|
||||
|
||||
}*/
|
||||
|
||||
|
||||
/* Stagger animation for initial load */
|
||||
|
||||
.list-item-enter-active:nth-child(1) {
|
||||
transition-delay: 0ms;
|
||||
}
|
||||
|
||||
.list-item-enter-active:nth-child(2) {
|
||||
transition-delay: 50ms;
|
||||
}
|
||||
|
||||
.list-item-enter-active:nth-child(3) {
|
||||
transition-delay: 100ms;
|
||||
}
|
||||
|
||||
.list-item-enter-active:nth-child(4) {
|
||||
transition-delay: 150ms;
|
||||
}
|
||||
|
||||
.list-item-enter-active:nth-child(5) {
|
||||
transition-delay: 200ms;
|
||||
}
|
||||
|
||||
.list-item-enter-active:nth-child(6) {
|
||||
transition-delay: 250ms;
|
||||
}
|
||||
|
||||
.list-item-enter-active:nth-child(7) {
|
||||
transition-delay: 300ms;
|
||||
}
|
||||
|
||||
.list-item-enter-active:nth-child(8) {
|
||||
transition-delay: 350ms;
|
||||
}
|
||||
|
||||
|
||||
/* Empty state animation */
|
||||
|
||||
.empty-container {
|
||||
animation: fadeInUp 0.5s ease-out;
|
||||
}
|
||||
|
||||
|
||||
@keyframes fadeInUp {
|
||||
|
||||
from {
|
||||
|
||||
opacity: 0;
|
||||
|
||||
transform: translateY(20px);
|
||||
|
||||
}
|
||||
|
||||
to {
|
||||
|
||||
opacity: 1;
|
||||
|
||||
transform: translateY(0);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.destination-list-header {
|
||||
display: none;
|
||||
|
|
|
|||
|
|
@ -1,24 +1,16 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<div class="container" @focusout="focusLost">
|
||||
|
||||
<div class="caption-column">Part number</div>
|
||||
|
||||
<div class="input-column">
|
||||
<autosuggest-searchbar v-if="editMode"
|
||||
@selected="partNumberSelected"
|
||||
:fetch-suggestions="fetchPartNumbers"
|
||||
:initial-value="partNumber"
|
||||
title-resolver="part_number"
|
||||
placeholder="Find part number"
|
||||
:activate-watcher="true"></autosuggest-searchbar>
|
||||
<span v-else>{{ partNumber }}</span>
|
||||
<modal-dialog :state="modalDialogPartNumberState"
|
||||
accept-text="Yes"
|
||||
deny-text="No"
|
||||
@click="modalDialogClick"
|
||||
message="You have changed the part number. Your current master data (like packaging dimensions) might be outdated. Do you want to reload master data?"
|
||||
title="Update master data"></modal-dialog>
|
||||
<icon-button :icon="editIconPartNumber" @click="toggleEditMode"></icon-button>
|
||||
|
||||
<span>{{ partNumber }}</span>
|
||||
|
||||
<modal :state="modalSelectMaterial" @close="closeEditModal">
|
||||
<select-material :part-number="partNumber" @close="modalEditClick"/>
|
||||
</modal>
|
||||
<icon-button icon="pencil-simple" @click="activateEditMode"></icon-button>
|
||||
</div>
|
||||
|
||||
|
||||
|
|
@ -34,21 +26,19 @@
|
|||
|
||||
|
||||
<div class="input-column">
|
||||
<div class="hs-code-container" v-if="!editMode">
|
||||
<div class="hs-code-container">
|
||||
<autosuggest-searchbar :fetch-suggestions="fetchHsCode" :initial-value="hsCode"
|
||||
placeholder="Find hs code"></autosuggest-searchbar>
|
||||
<icon-button icon="ArrowCounterClockwise"></icon-button>
|
||||
</div>
|
||||
<div v-else>{{ hsCode }}</div>
|
||||
|
||||
|
||||
<div class="caption-column">Tariff rate [%]</div>
|
||||
<div v-if="!editMode" class="input-field-container input-field-tariffrate">
|
||||
<div class="input-field-container input-field-tariffrate">
|
||||
<input ref="tariffRateInput" :value="tariffRatePercent" @blur="validateInput('tariffRate',$event)"
|
||||
class="input-field"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
<span v-else>{{ tariffRatePercent }}</span>
|
||||
</div>
|
||||
|
||||
|
||||
|
|
@ -67,10 +57,15 @@ import {PhArrowCounterClockwise} from "@phosphor-icons/vue";
|
|||
import {useMaterialStore} from "@/store/material.js";
|
||||
import {mapStores} from "pinia";
|
||||
import {parseNumberFromString} from "@/common.js";
|
||||
import Modal from "@/components/UI/Modal.vue";
|
||||
import SelectMaterial from "@/components/layout/material/SelectMaterial.vue";
|
||||
|
||||
export default {
|
||||
name: "MaterialEdit",
|
||||
components: {PhArrowCounterClockwise, ModalDialog, AutosuggestSearchbar, InputField, Flag, IconButton},
|
||||
components: {
|
||||
SelectMaterial,
|
||||
Modal, PhArrowCounterClockwise, ModalDialog, AutosuggestSearchbar, InputField, Flag, IconButton
|
||||
},
|
||||
emits: ["update:tariffRate", "updateMaterial", "update:partNumber", "update:hsCode"],
|
||||
props: {
|
||||
description: {
|
||||
|
|
@ -89,41 +84,32 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useMaterialStore),
|
||||
editIconPartNumber() {
|
||||
return this.editMode ? "check" : "pencil-simple";
|
||||
},
|
||||
tariffRatePercent() {
|
||||
return this.tariffRate ? (this.tariffRate * 100).toFixed(2) : '';
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editMode: false,
|
||||
modalDialogPartNumberState: false,
|
||||
selectedMaterial: null,
|
||||
modalSelectMaterial: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
partNumberSelected(material) {
|
||||
this.selectedMaterial = material;
|
||||
focusLost(event) {
|
||||
if (!this.$el.contains(event.relatedTarget)) {
|
||||
this.$emit('save', 'material');
|
||||
}
|
||||
},
|
||||
modalDialogClick(action) {
|
||||
this.modalDialogPartNumberState = false;
|
||||
if (action === 'accept') {
|
||||
this.$emit('updateMaterial', this.selectedMaterial.id, 'updateMasterData');
|
||||
this.editMode = false;
|
||||
this.selectedMaterial = null;
|
||||
} else if(action === 'deny') {
|
||||
this.$emit('updateMaterial', this.selectedMaterial.id, 'keepMasterData');
|
||||
this.editMode = false;
|
||||
this.selectedMaterial = null;
|
||||
closeEditModal() {
|
||||
this.modalSelectMaterial = false;
|
||||
},
|
||||
modalEditClick(data) {
|
||||
this.closeEditModal();
|
||||
if (data.action === 'accept') {
|
||||
this.selectedMaterial = data.material;
|
||||
this.$emit('updateMaterial', data.material.id, data.updateMasterData ? 'updateMasterData' : 'keepMasterData');
|
||||
}
|
||||
},
|
||||
updateInputValue(inputRef, formattedValue) {
|
||||
|
|
@ -145,21 +131,8 @@ export default {
|
|||
const inputRef = `${type}Input`;
|
||||
this.updateInputValue(inputRef, formattedValue);
|
||||
},
|
||||
toggleEditMode() {
|
||||
if (this.editMode) {
|
||||
if (this.selectedMaterial != null && this.selectedMaterial !== this.partNumber) {
|
||||
this.modalDialogPartNumberState = true;
|
||||
} else {
|
||||
this.editMode = false;
|
||||
}
|
||||
} else {
|
||||
this.editMode = true;
|
||||
}
|
||||
},
|
||||
async fetchPartNumbers(query) {
|
||||
const materialQuery = {searchTerm: query};
|
||||
await this.materialStore.setQuery(materialQuery);
|
||||
return this.materialStore.materials;
|
||||
activateEditMode() {
|
||||
this.modalSelectMaterial = true;
|
||||
},
|
||||
async fetchHsCode(query) {
|
||||
const hsCodeQuery = {searchTerm: query};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<div class="container" @focusout="focusLost">
|
||||
<div class="caption-column">MEK_A [EUR]</div>
|
||||
<div class="input-column">
|
||||
<div class="input-field-container">
|
||||
|
|
@ -58,6 +58,11 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
focusLost(event) {
|
||||
if (!this.$el.contains(event.relatedTarget)) {
|
||||
this.$emit('save', 'price');
|
||||
}
|
||||
},
|
||||
validatePrice(event) {
|
||||
const value = parseNumberFromString(event.target.value, 2);
|
||||
const validatedValue = Math.max(0, value);
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
src="https://www.galerie-braunbehrens.de/wp-content/uploads/2020/06/placeholder-google-maps.jpg" alt="map">
|
||||
</div>
|
||||
<div class="footer">
|
||||
<modal :state="selectSupplierModalState">
|
||||
<modal :state="selectSupplierModalState" @close="closeEditModal">
|
||||
<select-node @close="modalDialogClose"></select-node>
|
||||
</modal>
|
||||
<icon-button icon="plus" @click="openModal"></icon-button>
|
||||
|
|
@ -80,6 +80,9 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
closeEditModal() {
|
||||
this.selectSupplierModalState = false;
|
||||
},
|
||||
modalDialogClose(data) {
|
||||
this.selectSupplierModalState = false;
|
||||
if (data.action === 'accept') {
|
||||
|
|
|
|||
|
|
@ -13,16 +13,12 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
import Tab from "@/components/UI/Tab.vue";
|
||||
import TabContainer from "@/components/UI/TabContainer.vue";
|
||||
import DestinationEditRoutes from "@/components/layout/edit/destination/DestinationEditRoutes.vue";
|
||||
import DestinationEditHandlingCost from "@/components/layout/edit/destination/DestinationEditHandlingCost.vue";
|
||||
import {markRaw} from "vue";
|
||||
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {usePremiseEditStore} from "@/store/premiseEdit.js";
|
||||
|
||||
export default {
|
||||
name: "DestinationEdit",
|
||||
|
|
@ -51,32 +47,36 @@ export default {
|
|||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.tab-container {
|
||||
flex: 1;
|
||||
overflow: auto; /* In case content overflows */
|
||||
min-height: 0; /* Critical: allows flex child to shrink below content size */
|
||||
}
|
||||
|
||||
.destination-edit-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0; /* Critical: allows flex child to shrink below content size */
|
||||
}
|
||||
|
||||
.destination-edit-modal-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
flex: 1 0 max(80vw, 80rem);
|
||||
flex: 1 0 min(60vw, 120rem);
|
||||
height: min(70vh, 50rem);
|
||||
min-height: 0; /* Critical: allows flex child to shrink below content size */
|
||||
}
|
||||
|
||||
.destination-edit-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1.6rem;
|
||||
flex-shrink: 0; /* Prevent buttons from shrinking */
|
||||
}
|
||||
|
||||
.sub-header {
|
||||
flex-shrink: 0; /* Prevent header from shrinking */
|
||||
}
|
||||
</style>
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
<div class="destination-edit-handling-cost">
|
||||
<div class="destination-edit-handling-cost-info">
|
||||
<ph-warning size="18px"></ph-warning>
|
||||
Handling and repackaging costs can be calculated automatically.
|
||||
Handling and repackaging costs are calculated automatically.
|
||||
If needed, you can overwrite these values here.
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -54,8 +54,7 @@ export default {
|
|||
computed: {
|
||||
...mapStores(usePremiseEditStore),
|
||||
destination() {
|
||||
const [dest] = this.premiseEditStore.getSelectedDestinations;
|
||||
return dest;
|
||||
return this.premiseEditStore.getSelectedDestinationsData;
|
||||
},
|
||||
repackaging: {
|
||||
get() {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
<template>
|
||||
<div class="destination-edit-routes-container">
|
||||
<div v-if="showMassEditWarning" class="destination-edit-route-warning">
|
||||
<ph-warning size="18px"></ph-warning>
|
||||
Routing is deactivated during mass edit. Please select routes for each calculation separately.
|
||||
</div>
|
||||
|
||||
<div class="destination-edit-routes">
|
||||
<div class="destination-edit-column-caption">Destination</div>
|
||||
<div class="destination-edit-column-data">{{ destination.destination_node.name }}</div>
|
||||
|
||||
<div class="destination-edit-column-caption"><tooltip :text="tooltipAnnualAmount" position="right">Annual quantity</tooltip></div>
|
||||
<div class="destination-edit-column-caption">
|
||||
<tooltip :text="tooltipAnnualAmount" position="right">Annual quantity</tooltip>
|
||||
</div>
|
||||
<div class="destination-edit-column-data">
|
||||
<div class="input-field-container">
|
||||
<input :value="annualAmount" @blur="validateAnnualAmount" class="input-field"
|
||||
|
|
@ -12,26 +19,26 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="destination-edit-column-caption">Transport mode</div>
|
||||
<div class="destination-edit-column-data destination-edit-cell-routing">
|
||||
<div v-if="!showMassEditWarning" class="destination-edit-column-caption">Calculation model</div>
|
||||
<div v-if="!showMassEditWarning" class="destination-edit-column-data destination-edit-cell-routing">
|
||||
<radio-option name="model" value="routing" v-model="calculationModel">standard routing</radio-option>
|
||||
<radio-option name="model" value="d2d" v-model="calculationModel">individual rate</radio-option>
|
||||
</div>
|
||||
|
||||
<!-- Single grid cell for caption that transitions content -->
|
||||
<div class="destination-edit-column-caption">
|
||||
<div v-if="!showMassEditWarning" class="destination-edit-column-caption destination-edit-caption-top">
|
||||
<transition name="fade" mode="out-in">
|
||||
<div v-if="showRoutes || showRouteWarning" key="routes">Routes</div>
|
||||
<div v-else key="rate">D2D Rate [EUR]</div>
|
||||
<div v-if="showRoutes || showRouteWarning" key="routes" class="destination-edit-caption-top-elem">Routes</div>
|
||||
<div v-else key="rate" class="destination-edit-caption-top-elem">D2D Rate [EUR]</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<!-- Single grid cell for data that transitions content -->
|
||||
<div class="destination-edit-column-data">
|
||||
<div v-if="!showMassEditWarning" class="destination-edit-column-data destination-edit-routes-cell">
|
||||
<transition name="fade" mode="out-in">
|
||||
<div v-if="showRoutes" key="routes" class="destination-edit-cell-routes">
|
||||
<destination-route v-for="route in destination.routes" :key="route.id" :route="route"
|
||||
:selected="route.selected" @click="selectRoute(route.id)"></destination-route>
|
||||
:selected="route.is_selected" @click="selectRoute(route.id)"></destination-route>
|
||||
</div>
|
||||
<div v-else-if="showRouteWarning">
|
||||
<div class="destination-edit-route-warning">
|
||||
|
|
@ -62,7 +69,7 @@ export default {
|
|||
components: {Tooltip, DestinationRoute, RadioOption},
|
||||
methods: {
|
||||
selectRoute(id) {
|
||||
// Your route selection logic
|
||||
this.destination.routes.forEach(route => route.is_selected = route.id === id);
|
||||
},
|
||||
validateAnnualAmount(event) {
|
||||
const value = parseNumberFromString(event.target.value, 0);
|
||||
|
|
@ -94,8 +101,11 @@ export default {
|
|||
tooltipAnnualAmount() {
|
||||
return `Annual quantity that "${this.destination.destination_node.name}" will source from the supplier`
|
||||
},
|
||||
showMassEditWarning() {
|
||||
return (this.destination.massEdit ?? false);
|
||||
},
|
||||
showRoutes() {
|
||||
return !this.destination.is_d2d && this.destination.routes.length > 0;
|
||||
return !this.destination.is_d2d && this.destination.routes?.length > 0 | false;
|
||||
},
|
||||
showRouteWarning() {
|
||||
return !this.destination.is_d2d && this.destination.routes.length === 0;
|
||||
|
|
@ -114,8 +124,8 @@ export default {
|
|||
},
|
||||
...mapStores(usePremiseEditStore),
|
||||
destination() {
|
||||
const [dest] = this.premiseEditStore.getSelectedDestinations;
|
||||
return dest;
|
||||
//TODO handle multiselect (here?)
|
||||
return this.premiseEditStore.getSelectedDestinationsData;
|
||||
},
|
||||
annualAmount: {
|
||||
get() {
|
||||
|
|
@ -147,14 +157,24 @@ export default {
|
|||
opacity: 0;
|
||||
}
|
||||
|
||||
.destination-edit-routes-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0; /* Important for flexbox shrinking */
|
||||
}
|
||||
|
||||
.destination-edit-route-warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.4rem;
|
||||
gap: 1.6rem;
|
||||
|
||||
background-color: #c3cfdf;
|
||||
color: #002F54;
|
||||
border-radius: 0.8rem;
|
||||
padding: 1.6rem;
|
||||
margin-bottom: 1.6rem;
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
|
||||
.destination-edit-cell-routing {
|
||||
|
|
@ -166,9 +186,10 @@ export default {
|
|||
.destination-edit-routes {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: repeat(4, auto);
|
||||
grid-template-rows: auto auto auto 1fr; /* Last row takes remaining space */
|
||||
gap: 1.6rem;
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
min-height: 0; /* Important for grid to shrink */
|
||||
}
|
||||
|
||||
.destination-edit-column-data {
|
||||
|
|
@ -178,6 +199,12 @@ export default {
|
|||
color: #6b7280;
|
||||
}
|
||||
|
||||
.destination-edit-routes-cell {
|
||||
/* This cell needs to handle overflow */
|
||||
min-height: 0;
|
||||
align-self: stretch; /* Take full height of grid row */
|
||||
}
|
||||
|
||||
.destination-edit-column-caption {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
|
|
@ -187,6 +214,16 @@ export default {
|
|||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
.destination-edit-caption-top {
|
||||
align-self: start; /* Align to top instead of center */
|
||||
display: flex;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.destination-edit-caption-top-elem {
|
||||
margin-top: 0.8rem;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
border: none;
|
||||
outline: none;
|
||||
|
|
@ -220,5 +257,9 @@ export default {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
overflow-y: auto; /* Enable scrolling */
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding-right: 0.8rem; /* Space for scrollbar */
|
||||
}
|
||||
</style>
|
||||
|
|
@ -2,8 +2,8 @@
|
|||
<div class="destination-item-row">
|
||||
<div class="destination-item-name"><flag :iso="destinationIsoCode" />{{ destination.destination_node.name }}</div>
|
||||
<div class="destination-item-annual">{{ destination.annual_amount }}</div>
|
||||
<div class="destination-item-route" v-if="hasRoute"><destination-route :showBorder="false" :route="selectedRoute"></destination-route></div>
|
||||
<div class="destination-item-route" v-else-if="isD2d"><div class="d2d-routing-container"><PhShippingContainer /> D2D routing</div></div>
|
||||
<div class="destination-item-route" v-if="isD2d"><div class="d2d-routing-container"><PhShippingContainer /> D2D routing</div></div>
|
||||
<div class="destination-item-route" v-else-if="hasRoute"><destination-route :showBorder="false" :route="selectedRoute"></destination-route></div>
|
||||
<div class="destination-item-route" v-else><div class="d2d-routing-container"><PhEmpty /> No route selected</div></div>
|
||||
<div class="destination-item-action"><icon-button icon="pencil-simple" @click="$emit('edit', destination.id)"></icon-button><icon-button icon="trash" @click="$emit('delete',destination.id)"></icon-button></div>
|
||||
</div>
|
||||
|
|
@ -34,7 +34,7 @@ export default {
|
|||
return this.destination.destination_node.country.iso_code;
|
||||
},
|
||||
hasRoute() {
|
||||
return this.destination.routes.some(route => route.is_selected);
|
||||
return this.destination.routes?.some(route => route.is_selected) | false;
|
||||
},
|
||||
isD2d() {
|
||||
return this.destination.is_d2d;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
<template>
|
||||
<div class="destination-route-container">
|
||||
<div class="destination-route-container" @click="$emit('click')">
|
||||
<div :class="containerClass">
|
||||
<ph-boat :size="18" v-if="isSea" class="destination-route-icon"></ph-boat>
|
||||
<ph-train :size="18" v-else-if="isRail" class="destination-route-icon"></ph-train>
|
||||
<ph-truck-trailer :size="18" v-else-if="isRoad" class="destination-route-icon"></ph-truck-trailer>
|
||||
<ph-navigation-arrow :size="18" v-else class="destination-route-icon"></ph-navigation-arrow>
|
||||
<span v-for="element in routeElements" class="destination-route-element"> {{ element }} </span>
|
||||
<div><span v-for="element in routeElements" class="destination-route-element"> {{ element }} </span></div>
|
||||
<basic-badge v-if="cheapest" variant="secondary">CHEAPEST</basic-badge>
|
||||
<basic-badge v-if="fastest" variant="primary">FASTEST</basic-badge>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -14,11 +16,12 @@
|
|||
<script>
|
||||
|
||||
|
||||
import {PhBoat, PhNavigationArrow, PhTrain, PhTruckTrailer} from "@phosphor-icons/vue";
|
||||
import {PhBoat, PhHandCoins, PhLightning, PhNavigationArrow, PhTrain, PhTruckTrailer} from "@phosphor-icons/vue";
|
||||
import BasicBadge from "@/components/UI/BasicBadge.vue";
|
||||
|
||||
export default {
|
||||
name: "DestinationRoute",
|
||||
components: {PhNavigationArrow, PhTrain, PhTruckTrailer, PhBoat},
|
||||
components: {BasicBadge, PhLightning, PhHandCoins, PhNavigationArrow, PhTrain, PhTruckTrailer, PhBoat},
|
||||
props: {
|
||||
route: {
|
||||
type: Object,
|
||||
|
|
@ -36,18 +39,24 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
cheapest() {
|
||||
return this.route.is_cheapest;
|
||||
},
|
||||
fastest() {
|
||||
return this.route.is_fastest;
|
||||
},
|
||||
routeElements() {
|
||||
const routeElem = this.route.transit_nodes.map(n => n.external_mapping_id);
|
||||
return routeElem;
|
||||
},
|
||||
isSea() {
|
||||
return this.route.variant === "SEA";
|
||||
return this.route.type === "SEA";
|
||||
},
|
||||
isRoad() {
|
||||
return this.route.variant === "ROAD";
|
||||
return this.route.type === "ROAD";
|
||||
},
|
||||
isRail() {
|
||||
return this.route.variant === "RAIL";
|
||||
return this.route.type === "RAIL";
|
||||
},
|
||||
containerClass() {
|
||||
|
||||
|
|
@ -76,6 +85,8 @@ export default {
|
|||
padding: 0.6rem 1.2rem;
|
||||
transition: all 0.1s ease;
|
||||
flex: 0 1 fit-content;
|
||||
gap: 0.8rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.destination-route-container {
|
||||
|
|
@ -86,17 +97,19 @@ export default {
|
|||
|
||||
.destination-route-inner-container--bordered {
|
||||
border: 0.2rem solid #E3EDFF;
|
||||
color: #6B869C;
|
||||
}
|
||||
|
||||
.destination-route-inner-container--selected {
|
||||
background: #EEF4FF;
|
||||
background: #ffffff;
|
||||
border: 0.2rem solid #8DB3FE;
|
||||
}
|
||||
|
||||
.destination-route-inner-container--bordered:hover {
|
||||
background: #EEF4FF;
|
||||
color: #6B869C;
|
||||
border: 0.2rem solid #8DB3FE;
|
||||
transform: scale(1.01);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
173
src/frontend/src/components/layout/material/SelectMaterial.vue
Normal file
173
src/frontend/src/components/layout/material/SelectMaterial.vue
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
<template>
|
||||
<div class="select-material-modal-container">
|
||||
<h3 class="sub-header">Select material</h3>
|
||||
<div class="select-material-container">
|
||||
<div class="select-material-caption-column">Part number</div>
|
||||
<div class="select-material-input-column select-material-input-field-suppliername">
|
||||
<div class="select-material-input-field-suppliername-searchbar">
|
||||
<autosuggest-searchbar
|
||||
@selected="actionPartNumberSelect"
|
||||
:fetch-suggestions="fetchPartNumbers"
|
||||
:initial-value="partNumber"
|
||||
title-resolver="part_number"
|
||||
placeholder="Find part number"
|
||||
:activate-watcher="true"></autosuggest-searchbar>
|
||||
</div>
|
||||
</div>
|
||||
<div class="select-material-caption-column" v-if="materialSelected">Description</div>
|
||||
<div class="select-material-input-column select-material-input-field-address" v-if="materialSelected">
|
||||
{{ description }}
|
||||
</div>
|
||||
<div class="select-material-caption-column" v-if="materialSelected">HS Code</div>
|
||||
<div class="select-material-input-column" v-if="materialSelected">{{ hsCode }}</div>
|
||||
<div class="select-material-checkbox" v-if="materialSelected">
|
||||
<tooltip
|
||||
text="Your current master data (like packaging dimensions) might be outdated after material change. Tick to reload master data">
|
||||
<checkbox :checked="updateMasterData" @checkbox-changed="checkboxChanged">update master data</checkbox>
|
||||
</tooltip>
|
||||
</div>
|
||||
<div class="select-material-footer">
|
||||
<basic-button :show-icon="false" :disabled="disableAcceptButton" @click="action('accept')">OK</basic-button>
|
||||
<basic-button variant="secondary" :show-icon="false" @click="action('cancel')">Cancel</basic-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AutosuggestSearchbar from "@/components/UI/AutoSuggestSearchBar.vue";
|
||||
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
import Flag from "@/components/UI/Flag.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {useMaterialStore} from "@/store/material.js";
|
||||
import Tooltip from "@/components/UI/Tooltip.vue";
|
||||
|
||||
export default {
|
||||
name: "SelectMaterial",
|
||||
emits: ['close'],
|
||||
components: {Tooltip, Flag, Checkbox, BasicButton, AutosuggestSearchbar},
|
||||
props: {
|
||||
partNumber: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useMaterialStore),
|
||||
disableAcceptButton() {
|
||||
return (this.selectedPartNumber === this.partNumber);
|
||||
},
|
||||
materialSelected() {
|
||||
return this.selectedMaterial != null;
|
||||
},
|
||||
description() {
|
||||
return this.selectedMaterial?.name ?? '';
|
||||
},
|
||||
hsCode() {
|
||||
return this.selectedMaterial?.hs_code ?? '';
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.selectedPartNumber = this.partNumber;
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedMaterial: null,
|
||||
selectedPartNumber: null,
|
||||
updateMasterData: true,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
checkboxChanged(value) {
|
||||
this.updateMasterData = value;
|
||||
},
|
||||
action(action) {
|
||||
this.$emit('close', {action: action, material: this.selectedMaterial, updateMasterData: this.updateMasterData});
|
||||
},
|
||||
actionPartNumberSelect(material) {
|
||||
this.selectedMaterial = material;
|
||||
this.selectedPartNumber = material.part_number;
|
||||
},
|
||||
async fetchPartNumbers(query) {
|
||||
const materialQuery = {searchTerm: query};
|
||||
await this.materialStore.setQuery(materialQuery);
|
||||
return this.materialStore.materials;
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.select-material-input-column {
|
||||
font-size: 1.4rem;
|
||||
color: #6b7280;
|
||||
max-width: 30rem;
|
||||
}
|
||||
|
||||
.select-material-input-field-suppliername {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.select-material-input-field-suppliername-searchbar {
|
||||
flex: 1 1 auto;
|
||||
min-width: 50rem;
|
||||
}
|
||||
|
||||
.select-material-modal-container {
|
||||
width: 60rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.select-material-container {
|
||||
flex: 1 1 auto;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: repeat(3, fit-content(0));
|
||||
align-content: center;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.select-material-caption-column {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
align-self: center;
|
||||
justify-self: end;
|
||||
color: #001D33
|
||||
}
|
||||
|
||||
.select-material-footer {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.select-material-checkbox {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.select-material-input-field-address {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.supplier-address {
|
||||
font-size: 1.4rem;
|
||||
color: #6b7280;
|
||||
max-width: 30rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import router from './router.js';
|
||||
//import store from './store/index.js';
|
||||
import { setupErrorBuffer } from './store/error.js'
|
||||
import {createApp} from 'vue'
|
||||
import {createPinia} from 'pinia';
|
||||
import App from './App.vue'
|
||||
import { setupErrorBuffer } from './store/error.js'
|
||||
|
||||
import {
|
||||
PhStar,
|
||||
|
|
@ -26,15 +26,15 @@ import {
|
|||
PhArchive,
|
||||
PhFloppyDisk,
|
||||
PhArrowCounterClockwise,
|
||||
PhCheck, PhBug
|
||||
PhCheck, PhBug, PhShuffle, PhStack
|
||||
} from "@phosphor-icons/vue";
|
||||
|
||||
const app = createApp(App);
|
||||
const pinia = createPinia();
|
||||
app.use(pinia);
|
||||
|
||||
app.config.globalProperties.backendUrl = 'http://localhost:8080/api/';
|
||||
|
||||
|
||||
app.component('PhPlus', PhPlus);
|
||||
app.component('PhDownload', PhDownload);
|
||||
app.component('PhUpload', PhUpload);
|
||||
|
|
@ -56,11 +56,12 @@ app.component('PhCloudArrowUp', PhCloudArrowUp);
|
|||
app.component('PhSealCheck', PhSealCheck);
|
||||
app.component('PhCalculator', PhCalculator);
|
||||
app.component('PhStar', PhStar);
|
||||
app.component('PhBug', PhBug)
|
||||
|
||||
app.component('PhBug', PhBug);
|
||||
app.component('PhShuffle', PhShuffle);
|
||||
app.component('PhStack', PhStack );
|
||||
|
||||
app.use(router);
|
||||
app.use(pinia);
|
||||
|
||||
|
||||
//app.component('base-button', () => import('./components/UI/BasicButton.vue'));
|
||||
//app.component('base-badge', () => import('./components/UI/BasicBadge.vue'));
|
||||
|
|
|
|||
|
|
@ -1,14 +1,435 @@
|
|||
<template>
|
||||
<div class="edit-calculation-container">
|
||||
<div class="header-container">
|
||||
<h2 class="page-header">Mass edit calculation</h2>
|
||||
<div class="header-controls">
|
||||
<basic-button :show-icon="false" :disabled="premiseEditStore.selectedLoading"
|
||||
variant="secondary">Close
|
||||
</basic-button>
|
||||
<basic-button :show-icon="true"
|
||||
:disabled="premiseEditStore.selectedLoading"
|
||||
icon="Calculator" variant="primary">Calculate & close
|
||||
</basic-button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<transition name="list-edit-container" tag="div">
|
||||
<transition-group name="list-edit" mode="out-in" class="edit-calculation-list-container" tag="div">
|
||||
|
||||
<div class="edit-calculation-list-header" key="header">
|
||||
<div>
|
||||
<checkbox @checkbox-changed="updateCheckBoxes" :checked="overallCheck"></checkbox>
|
||||
</div>
|
||||
<div>Material</div>
|
||||
<div>Price</div>
|
||||
<div>Packaging</div>
|
||||
<div>Supplier</div>
|
||||
<div>Destinations & routes</div>
|
||||
<div>Actions</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="showLoading" class="spinner-container" key="spinner">
|
||||
<spinner class="space-around"></spinner>
|
||||
</div>
|
||||
|
||||
<div v-else-if="showEmpty" class="empty-container" key="empty">
|
||||
<span class="space-around">No Calculations found.</span>
|
||||
</div>
|
||||
|
||||
<bulk-edit-row v-else class="edit-calculation-list-item" v-for="id in this.premiseEditStore.getPremiseIds"
|
||||
:key="id" :id="id" @action="onClickAction" :copy-mode="selectCount !== 0">
|
||||
</bulk-edit-row>
|
||||
|
||||
|
||||
</transition-group>
|
||||
</transition>
|
||||
|
||||
|
||||
<modal :z-index="3000" :state="showProcessingModal" @close="closeEditModal">
|
||||
<div class="edit-calculation-spinner-container space-around">
|
||||
<spinner></spinner>
|
||||
<span>{{ processingMessage }}</span>
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
<mass-edit-dialog v-if="showData" :show="showMultiselectAction" @action="multiselectAction"
|
||||
:select-count="selectCount"></mass-edit-dialog>
|
||||
|
||||
|
||||
<modal :z-index="2000" :state="showEditModal" @close="closeEditModal">
|
||||
<div class="modal-content-container">
|
||||
<component
|
||||
:is="componentType"
|
||||
v-bind="componentProps"
|
||||
>
|
||||
</component>
|
||||
<div class="modal-content-footer">
|
||||
<basic-button :show-icon="false" @click="closeEditModalAction('accept')">OK</basic-button>
|
||||
<basic-button variant="secondary" :show-icon="false" @click="closeEditModalAction('cancel')">Cancel
|
||||
</basic-button>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {UrlSafeBase64} from "@/common.js";
|
||||
import {mapStores} from "pinia";
|
||||
import {usePremiseEditStore} from "@/store/premiseEdit.js";
|
||||
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||
import BulkEditRow from "@/components/layout/bulkedit/BulkEditRow.vue";
|
||||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
import CalculationListItem from "@/components/layout/calculation/CalculationListItem.vue";
|
||||
import Spinner from "@/components/UI/Spinner.vue";
|
||||
import ListEdit from "@/components/UI/ListEdit.vue";
|
||||
import MassEditDialog from "@/components/UI/MassEditDialog.vue";
|
||||
import Modal from "@/components/UI/Modal.vue";
|
||||
import PriceEdit from "@/components/layout/edit/PriceEdit.vue";
|
||||
import MaterialEdit from "@/components/layout/edit/MaterialEdit.vue";
|
||||
import PackagingEdit from "@/components/layout/edit/PackagingEdit.vue";
|
||||
import SupplierView from "@/components/layout/edit/SupplierView.vue";
|
||||
import DestinationListView from "@/components/layout/edit/DestinationListView.vue";
|
||||
|
||||
|
||||
const COMPONENT_TYPES = {
|
||||
price: PriceEdit,
|
||||
material: MaterialEdit,
|
||||
packaging: PackagingEdit,
|
||||
supplier: SupplierView,
|
||||
destinations: DestinationListView,
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "MassEdit"
|
||||
name: "MassEdit",
|
||||
components: {Modal, MassEditDialog, ListEdit, Spinner, CalculationListItem, Checkbox, BulkEditRow, BasicButton},
|
||||
computed: {
|
||||
...mapStores(usePremiseEditStore),
|
||||
selectCount() {
|
||||
return this.selectedPremisses?.length ?? 0;
|
||||
},
|
||||
selectedPremisses() {
|
||||
return this.premiseEditStore.getSelectedPremisses;
|
||||
},
|
||||
showEmpty() {
|
||||
return this.premiseEditStore.showEmpty;
|
||||
},
|
||||
showLoading() {
|
||||
return this.premiseEditStore.isLoading;
|
||||
},
|
||||
showData() {
|
||||
return this.premiseEditStore.showData;
|
||||
},
|
||||
overallCheck() {
|
||||
return this.premiseEditStore.isLoading ? false : this.premiseEditStore.getPremisses?.every(p => p.selected === true) ?? false;
|
||||
},
|
||||
showMultiselectAction() {
|
||||
return this.premiseEditStore.getPremisses?.some(p => p.selected === true) ?? false;
|
||||
},
|
||||
showEditModal() {
|
||||
return ((this.modalType ?? null) !== null);
|
||||
},
|
||||
componentProps() {
|
||||
return this.componentData?.props ?? null;
|
||||
},
|
||||
componentType() {
|
||||
return this.modalType ? COMPONENT_TYPES[this.modalType] : null;
|
||||
},
|
||||
componentData() {
|
||||
return this.modalType ? this.componentsData[this.modalType] : null;
|
||||
},
|
||||
showProcessingModal() {
|
||||
return this.premiseEditStore.showProcessingModal;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.bulkQuery = this.$route.params.ids;
|
||||
this.ids = new UrlSafeBase64().decodeIds(this.$route.params.ids);
|
||||
this.premiseEditStore.loadPremissesForced(this.ids);
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ids: [],
|
||||
bulkQuery: null,
|
||||
modalType: null,
|
||||
componentsData: {
|
||||
price: {props: {price: 0, overSeaShare: 0, includeFcaFee: true}},
|
||||
material: {props: {partNumber: "", hsCode: "", tariffRate: 0.00, description: ""}},
|
||||
packaging: {
|
||||
props: {
|
||||
length: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
weight: 0,
|
||||
weightUnit: "KG",
|
||||
dimensionUnit: "MM",
|
||||
unitCount: 1,
|
||||
mixable: true,
|
||||
stackable: false
|
||||
}
|
||||
},
|
||||
supplier: {
|
||||
props: {
|
||||
supplierName: "",
|
||||
supplierAddress: "",
|
||||
supplierCoordinates: {latitude: 1, longitude: 2},
|
||||
isoCode: "DE"
|
||||
}
|
||||
},
|
||||
destinations: {props: {}},
|
||||
},
|
||||
editIds: null,
|
||||
processingMessage: "Please wait. Calculating routes ...",
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateCheckBoxes(value) {
|
||||
this.premiseEditStore.setSelectTo(this.ids, value);
|
||||
},
|
||||
multiselectAction(action) {
|
||||
this.openModal(action, this.selectedPremisses.map(p => p.id));
|
||||
},
|
||||
onClickAction(data) {
|
||||
if (0 !== this.premiseEditStore.selectCount) {
|
||||
this.openModal(data.action, this.premiseEditStore.getSelectedPremissesIds, data.id);
|
||||
} else {
|
||||
this.openModal(data.action, [data.id], data.id);
|
||||
}
|
||||
},
|
||||
openModal(type, ids, dataSource = -1) {
|
||||
|
||||
if (type !== 'destinations')
|
||||
this.fillData(type, dataSource)
|
||||
else {
|
||||
this.premiseEditStore.prepareMassEdit(dataSource, ids);
|
||||
}
|
||||
|
||||
this.editIds = ids;
|
||||
this.modalType = type;
|
||||
},
|
||||
closeEditModalAction(action) {
|
||||
|
||||
if (this.modalType === "destinations") {
|
||||
if(action === "accept") {
|
||||
this.premiseEditStore.finishMassEdit();
|
||||
}
|
||||
} else {
|
||||
if (action === "accept") {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// clear data.
|
||||
this.fillData(this.modalType);
|
||||
this.closeEditModal();
|
||||
},
|
||||
triggerProcessingModal(text) {
|
||||
if(text) {
|
||||
this.processingMessage = text;
|
||||
this.processingModal = true;
|
||||
}
|
||||
else
|
||||
this.processingModal = false
|
||||
},
|
||||
closeEditModal() {
|
||||
this.modalType = null;
|
||||
},
|
||||
fillData(type, id = -1) {
|
||||
if (id === -1) {
|
||||
// clear
|
||||
this.componentsData = {
|
||||
price: {props: {price: 0, overSeaShare: 0.0, includeFcaFee: true}},
|
||||
material: {props: {partNumber: "", hsCode: "", tariffRate: 0.00, description: ""}},
|
||||
packaging: {
|
||||
props: {
|
||||
length: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
weight: 0,
|
||||
weightUnit: "KG",
|
||||
dimensionUnit: "MM",
|
||||
unitCount: 1,
|
||||
mixable: true,
|
||||
stackable: false
|
||||
}
|
||||
},
|
||||
supplier: {
|
||||
props: {
|
||||
supplierName: "",
|
||||
supplierAddress: "",
|
||||
supplierCoordinates: {latitude: 1, longitude: 2},
|
||||
isoCode: "DE"
|
||||
}
|
||||
},
|
||||
destinations: {props: {}},
|
||||
};
|
||||
} else {
|
||||
const premise = this.premiseEditStore.getById(id);
|
||||
|
||||
if (type === "price") {
|
||||
this.componentsData.price.props = {
|
||||
price: premise.material_cost,
|
||||
overSeaShare: premise.oversea_share,
|
||||
includeFcaFee: premise.is_fca_enabled
|
||||
}
|
||||
} else if (type === "material") {
|
||||
this.componentsData.material.props = {
|
||||
partNumber: premise.material.part_number,
|
||||
hsCode: premise.material.hs_code ?? "",
|
||||
tariffRate: premise.tariff_rate ?? 0.00,
|
||||
description: premise.material.name ?? ""
|
||||
}
|
||||
} else if (type === "packaging") {
|
||||
this.componentsData.packaging.props = {
|
||||
length: premise.handling_unit.length ?? 0,
|
||||
width: premise.handling_unit.width ?? 0,
|
||||
height: premise.handling_unit.height ?? 0,
|
||||
weight: premise.handling_unit.weight ?? 0,
|
||||
weightUnit: premise.handling_unit.weightUnit ?? "KG",
|
||||
dimensionUnit: premise.handling_unit.dimensionUnit ?? "MM",
|
||||
unitCount: premise.handling_unit.content_unit_count ?? 1,
|
||||
mixable: premise.is_mixable ?? true,
|
||||
stackable: premise.is_stackable ?? true
|
||||
}
|
||||
} else if (type === "supplier") {
|
||||
this.componentsData.supplier.props = {
|
||||
supplierName: premise.supplier.name ?? "",
|
||||
supplierAddress: premise.supplier.address ?? "",
|
||||
supplierCoordinates: {
|
||||
latitude: premise.supplier.location.latitude,
|
||||
longitude: premise.supplier.location.longitude
|
||||
},
|
||||
isoCode: premise.supplier.country.iso_code
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2>Edit Calculations</h2>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.space-around {
|
||||
margin: 3rem;
|
||||
}
|
||||
|
||||
.edit-calculation-spinner-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 3.6rem;
|
||||
flex: 1 1 auto;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.modal-content-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.modal-content-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
/* Container Animation */
|
||||
|
||||
.list-edit-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
max-height: 0;
|
||||
}
|
||||
|
||||
.list-edit-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
max-height: 0;
|
||||
}
|
||||
|
||||
.list-edit-enter-active,
|
||||
.list-edit-leave-active {
|
||||
transition: all 0.4s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
.spinner-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.empty-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.edit-calculation-list-container {
|
||||
background: white;
|
||||
border-radius: 1.2rem;
|
||||
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 3rem;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.edit-calculation-list-header {
|
||||
display: grid;
|
||||
grid-template-columns: 6rem 1fr 1fr 1.5fr 1.5fr 1.5fr 10rem;
|
||||
gap: 1.6rem;
|
||||
padding: 2.4rem;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 0.1rem solid rgba(107, 134, 156, 0.2);
|
||||
font-weight: 500;
|
||||
font-size: 1.4rem;
|
||||
color: #6B869C;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08rem;
|
||||
}
|
||||
|
||||
|
||||
.edit-calculation-spinner-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1 1 30rem
|
||||
}
|
||||
|
||||
.edit-calculation-spinner {
|
||||
font-size: 1.6rem;
|
||||
width: 24rem;
|
||||
height: 12rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.edit-calculation-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
display: flex;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,30 +1,24 @@
|
|||
<template>
|
||||
<div class="edit-calculation-container">
|
||||
<div class="header-container">
|
||||
<h2 class="page-header">Edit Calculation</h2>
|
||||
<h2 class="page-header">Edit calculation</h2>
|
||||
<div class="header-controls">
|
||||
<basic-button @click="showCustomToast" :show-icon="false" :disabled="premiseEditStore.selectedLoading" variant="secondary">Close
|
||||
<basic-button @click="close" :show-icon="false" :disabled="premiseEditStore.selectedLoading" variant="secondary"> {{ fromMassEdit ? 'Back' : 'Close' }}
|
||||
</basic-button>
|
||||
<basic-button :show-icon="true" :disabled="premiseEditStore.selectedLoading || premiseEditStore.singleSelectEmpty"
|
||||
<basic-button v-if="!fromMassEdit" :show-icon="true" :disabled="premiseEditStore.selectedLoading || !premiseEditStore.isSingleSelect"
|
||||
icon="Calculator" variant="primary">Calculate & close
|
||||
</basic-button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<Toast ref="toast" />
|
||||
<modal :state="traceModal"><trace-view :error="premiseEditStore.error" @close="traceModal = false;"></trace-view></modal>
|
||||
<notification-bar v-if="premiseEditStore.error != null" variant="exception" icon="x"
|
||||
@icon-clicked="premiseEditStore.error = null">
|
||||
<div class="errorCode">{{ premiseEditStore.error.code }}</div>
|
||||
{{ premiseEditStore.error.message }} <span class="trace-link" @click="trace">View Trace</span>
|
||||
</notification-bar>
|
||||
|
||||
<div v-if="premiseEditStore.selectedLoading" class="edit-calculation-spinner-container">
|
||||
<box class="edit-calculation-spinner">
|
||||
<spinner></spinner>
|
||||
</box>
|
||||
</div>
|
||||
<div v-else-if="premiseEditStore.singleSelectEmpty" class="edit-calculation-spinner-container">
|
||||
<div v-else-if="!premiseEditStore.isSingleSelect" class="edit-calculation-spinner-container">
|
||||
<box class="edit-calculation-spinner">No calculation found.</box>
|
||||
</div>
|
||||
<div v-else>
|
||||
|
|
@ -66,7 +60,7 @@
|
|||
</div>
|
||||
|
||||
<h3 class="sub-header">Destinations & routes</h3>
|
||||
<destination-list-view :destinations="premise.destinations"></destination-list-view>
|
||||
<destination-list-view></destination-list-view>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -89,6 +83,7 @@ import Modal from "@/components/UI/Modal.vue";
|
|||
import TraceView from "@/components/layout/TraceView.vue";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import Toast from "@/components/UI/Toast.vue";
|
||||
import {UrlSafeBase64} from "@/common.js";
|
||||
|
||||
export default {
|
||||
name: "SingleEdit",
|
||||
|
|
@ -109,16 +104,31 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
traceModal: false
|
||||
traceModal: false,
|
||||
bulkEditQuery: null,
|
||||
id: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(usePremiseEditStore),
|
||||
premise() {
|
||||
return this.premiseEditStore.selectedPremise;
|
||||
return this.premiseEditStore.singleSelectedPremise;
|
||||
},
|
||||
fromMassEdit() {
|
||||
return this.bulkEditQuery !== null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
if(this.bulkEditQuery) {
|
||||
//TODO: deselect and save
|
||||
this.$router.push({name: 'bulk', params: {ids: this.bulkEditQuery}});
|
||||
}
|
||||
else {
|
||||
//TODO: deselect and save
|
||||
this.$router.push({name: 'home'});
|
||||
}
|
||||
},
|
||||
save(type) {
|
||||
console.log(type);
|
||||
},
|
||||
|
|
@ -143,7 +153,14 @@ export default {
|
|||
}
|
||||
},
|
||||
created() {
|
||||
this.premiseEditStore.selectPremise(parseInt(this.$route.params.id));
|
||||
this.id = new UrlSafeBase64().decodeIds(this.$route.params.id);
|
||||
|
||||
if(this.$route.params.ids) {
|
||||
this.bulkEditQuery = this.$route.params.ids;
|
||||
this.premiseEditStore.selectPremises(this.id, new UrlSafeBase64().decodeIds(this.$route.params.ids));
|
||||
} else {
|
||||
this.premiseEditStore.loadAndSelectSinglePremise(this.id)
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,6 @@
|
|||
|
||||
<h2 class="page-header">My calculations</h2>
|
||||
|
||||
<notification-bar v-if="premiseStore.error != null" variant="exception" icon="x"
|
||||
@icon-clicked="premiseStore.error = null">
|
||||
<div class="errorCode">{{ premiseStore.error.code }}</div>
|
||||
{{ premiseStore.error.message }}
|
||||
</notification-bar>
|
||||
|
||||
|
||||
|
||||
<div class="calculation-list-container">
|
||||
|
||||
|
|
@ -44,7 +37,6 @@
|
|||
<list-edit :show="showListEdit" @action="handleMultiselectAction"></list-edit>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
|
@ -64,6 +56,7 @@ import {usePremiseStore} from "@/store/premise.js";
|
|||
import {mapStores} from "pinia";
|
||||
import Spinner from "@/components/UI/Spinner.vue";
|
||||
import ListEdit from "@/components/UI/ListEdit.vue";
|
||||
import {UrlSafeBase64} from "@/common.js";
|
||||
|
||||
export default {
|
||||
name: "Calculation",
|
||||
|
|
@ -88,7 +81,20 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
handleMultiselectAction(action) {
|
||||
this.premiseStore.selected
|
||||
if (action === "delete") {
|
||||
|
||||
} else if (action === "edit") {
|
||||
const ids = this.premiseStore.premises.filter(p => p.checked === true).map(p => p.id);
|
||||
|
||||
if (ids.length === 1) {
|
||||
this.$router.push({name: "edit", params: {id: ids[0]}});
|
||||
} else {
|
||||
this.$router.push({name: "bulk", params: {ids: new UrlSafeBase64().encodeIds(ids)}});
|
||||
}
|
||||
|
||||
} else if (action === "archive") {
|
||||
|
||||
}
|
||||
},
|
||||
updateCheckBoxes(checked) {
|
||||
this.overallCheck = checked;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ const router = createRouter({
|
|||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/calculations'
|
||||
redirect: '/calculations',
|
||||
|
||||
},
|
||||
{
|
||||
path: '/assistant',
|
||||
|
|
@ -21,6 +22,7 @@ const router = createRouter({
|
|||
{
|
||||
path: '/calculations',
|
||||
component: Calculations,
|
||||
name: 'home',
|
||||
},
|
||||
{
|
||||
path: '/edit/:id',
|
||||
|
|
@ -28,8 +30,14 @@ const router = createRouter({
|
|||
name: 'edit',
|
||||
},
|
||||
{
|
||||
path: '/bulk',
|
||||
path: '/bulk/:ids',
|
||||
component: CalculationMassEdit,
|
||||
name: 'bulk',
|
||||
},
|
||||
{
|
||||
path: '/bulk/:ids/edit/:id',
|
||||
component: CalculationSingleEdit,
|
||||
name: 'bulk-single-edit',
|
||||
},
|
||||
{
|
||||
path: '/reports',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import {config} from '@/config'
|
||||
import {useErrorStore} from "@/store/error.js";
|
||||
|
||||
|
||||
export const useAssistantStore = defineStore('assistant', {
|
||||
|
|
@ -31,16 +32,24 @@ export const useAssistantStore = defineStore('assistant', {
|
|||
|
||||
console.log(`Creation body: ${jsonBody}`);
|
||||
|
||||
const response = await fetch(url, {
|
||||
const params = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: jsonBody
|
||||
}).catch(e => {
|
||||
body: jsonBody};
|
||||
|
||||
const request = { url: url, params: params};
|
||||
|
||||
const response = await fetch(url, params).catch(e => {
|
||||
this.error = {code: 'Network error.', message: "Please check your internet connection.", trace: null}
|
||||
console.error(this.error);
|
||||
this.loading = false;
|
||||
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(this.error, { store: this, request: request});
|
||||
|
||||
throw e;
|
||||
});
|
||||
|
||||
|
|
@ -52,13 +61,22 @@ export const useAssistantStore = defineStore('assistant', {
|
|||
}
|
||||
console.error(this.error);
|
||||
this.loading = false;
|
||||
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(this.error, { store: this, request: request});
|
||||
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
this.error = {code: data.error.title, message: data.error.message, trace: data.error.details}
|
||||
this.error = {code: data.error.code, title: data.error.title, message: data.error.message, trace: data.error.trace };
|
||||
this.loading = false;
|
||||
console.error(data);
|
||||
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(this.error, { store: this, request: request});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -111,6 +129,7 @@ export const useAssistantStore = defineStore('assistant', {
|
|||
const headers = new Headers();
|
||||
headers.append('search', encodeURIComponent(query));
|
||||
|
||||
const request = { url: url, params: {method: 'GET'}};
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
|
|
@ -118,6 +137,11 @@ export const useAssistantStore = defineStore('assistant', {
|
|||
}).catch(e => {
|
||||
this.error = {code: 'Network error.', message: "Please check your internet connection.", trace: null}
|
||||
this.loading = false;
|
||||
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(this.error, { store: this, request: request});
|
||||
|
||||
throw e;
|
||||
});
|
||||
|
||||
|
|
@ -128,13 +152,22 @@ export const useAssistantStore = defineStore('assistant', {
|
|||
trace: null
|
||||
}
|
||||
this.loading = false;
|
||||
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(this.error, { store: this, request: request});
|
||||
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
this.error = {code: data.error.title, message: data.error.message, trace: data.error.details}
|
||||
this.error = {code: data.error.code, title: data.error.title, message: data.error.message, trace: data.error.trace };
|
||||
this.loading = false;
|
||||
console.log(data);
|
||||
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(this.error, { store: this, request: request});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,15 +15,21 @@ export const useErrorStore = defineStore('error', {
|
|||
lastError: (state) => state.errors.length > 0 ? state.errors[state.errors.length - 1].error : null,
|
||||
},
|
||||
actions: {
|
||||
clearErrors() {
|
||||
this.errors = [];
|
||||
console.log("Cleared errors");
|
||||
},
|
||||
async addError(errorDto, options = {}) {
|
||||
const {request = null, store = null, global = false} = options;
|
||||
const state = this.captureStoreState(store, global);
|
||||
|
||||
const error = {
|
||||
error: {
|
||||
code: errorDto.code ?? 'Unknown error',
|
||||
code: errorDto.code ?? 'Unknown error code',
|
||||
title: errorDto.title ?? 'Unknown error',
|
||||
message: errorDto.message ?? 'Unknown message',
|
||||
trace: errorDto.trace ?? null,
|
||||
traceCombined: errorDto.traceCombined ?? null,
|
||||
},
|
||||
request: request ? JSON.stringify(request) : null,
|
||||
state: state ? JSON.stringify(state) : null,
|
||||
|
|
@ -119,27 +125,29 @@ export const useErrorStore = defineStore('error', {
|
|||
export function setupErrorBuffer() {
|
||||
const errorStore = useErrorStore()
|
||||
|
||||
// Unhandled Promise Rejections
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
|
||||
const error = {
|
||||
code: "Frontend error",
|
||||
message: event.reason?.message || 'Unhandled Promise Rejection',
|
||||
trace: event.reason?.stack,
|
||||
};
|
||||
|
||||
errorStore.addError(error, {global: true}).then(r => {} );
|
||||
})
|
||||
|
||||
// JavaScript Errors
|
||||
window.addEventListener('error', (event) => {
|
||||
const error = {
|
||||
code: "Frontend error",
|
||||
message: event.reason?.message || 'Unhandled Promise Rejection',
|
||||
trace: event.reason?.stack,
|
||||
};
|
||||
errorStore.addError(error, {global: true}).then(r => {} );
|
||||
})
|
||||
// // Unhandled Promise Rejections
|
||||
// window.addEventListener('unhandledrejection', (event) => {
|
||||
//
|
||||
// const error = {
|
||||
// code: "Unhandled rejection",
|
||||
// title: "Frontend error",
|
||||
// message: event.reason?.message || 'Unhandled Promise Rejection',
|
||||
// traceCombined: event.reason?.stack,
|
||||
// };
|
||||
//
|
||||
// errorStore.addError(error, {global: true}).then(r => {} );
|
||||
// })
|
||||
//
|
||||
// // JavaScript Errors
|
||||
// window.addEventListener('error', (event) => {
|
||||
// const error = {
|
||||
// code: "General error",
|
||||
// title: "Frontend error",
|
||||
// message: event.reason?.message || 'Unhandled Promise Rejection',
|
||||
// traceCombined: event.reason?.stack,
|
||||
// };
|
||||
// errorStore.addError(error, {global: true}).then(r => {} );
|
||||
// })
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
errorStore.submitOnBeforeUnload();
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export const useMaterialStore = defineStore('material', {
|
|||
});
|
||||
|
||||
if (!response.ok) {
|
||||
this.error = {code: data.error.title, message: data.error.message, trace: data.error.details}
|
||||
this.error = {code: data.error.code, title: data.error.title, message: data.error.message, trace: data.error.trace };
|
||||
this.loading = false;
|
||||
|
||||
console.error(this.error);
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export const useNodeStore = defineStore('node', {
|
|||
});
|
||||
|
||||
if (!response.ok) {
|
||||
this.error = {code: data.error.title, message: data.error.message, trace: data.error.details}
|
||||
this.error = {code: data.error.code, title: data.error.title, message: data.error.message, trace: data.error.trace };
|
||||
this.loading = false;
|
||||
|
||||
console.error(this.error);
|
||||
|
|
|
|||
|
|
@ -43,41 +43,39 @@ export const usePremiseStore = defineStore('premise', {
|
|||
if (this.query.deleted)
|
||||
params.append('deleted', this.query.deleted);
|
||||
|
||||
const that = this;
|
||||
|
||||
const url = `${config.backendUrl}/calculation/view/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||
|
||||
const request = { url: url, params: {method: 'GET'}};
|
||||
|
||||
const response = await fetch(url).catch(e => {
|
||||
that.error = { code: 'Network error.', message: "Please check your internet connection.", trace: null }
|
||||
that.loading = false;
|
||||
this.error = { title: 'Network error.', message: "Please check your internet connection.", trace: null }
|
||||
this.loading = false;
|
||||
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(that.error, { store: that, request: request});
|
||||
void errorStore.addError(this.error, { store: this, request: request});
|
||||
|
||||
throw e;
|
||||
});
|
||||
|
||||
const data = await response.json().catch(e => {
|
||||
that.error = { code: 'Malformed response', message: "Malformed server response. Please contact support.", trace: null }
|
||||
that.loading = false;
|
||||
this.error = { title: 'Malformed response', message: "Malformed server response. Please contact support.", trace: null }
|
||||
this.loading = false;
|
||||
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(that.error, { store: that, request: request});
|
||||
void errorStore.addError(this.error, { store: this, request: request});
|
||||
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
that.error = { code: data.error.title, message: data.error.message, trace: data.error.details }
|
||||
that.loading = false;
|
||||
this.error = { code: data.error.code, title: data.error.title, message: data.error.message, trace: data.error.trace }
|
||||
this.loading = false;
|
||||
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(that.error, { store: that, request: request});
|
||||
void errorStore.addError(this.error, { store: this, request: request});
|
||||
|
||||
console.log(data);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -7,36 +7,327 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
state() {
|
||||
return {
|
||||
premisses: null,
|
||||
singleSelectId: null,
|
||||
|
||||
/**
|
||||
* set to true while the store is loading the premises.
|
||||
*/
|
||||
loading: false,
|
||||
|
||||
/**
|
||||
* set to true while the store sets the selected/deselected field in the premises.
|
||||
*/
|
||||
selectedLoading: false,
|
||||
empty: false,
|
||||
error: null,
|
||||
selectedDestinations: [],
|
||||
|
||||
|
||||
massEditDestinations: null,
|
||||
processMassEdit: false,
|
||||
|
||||
selectedDestination: null,
|
||||
|
||||
throwsException: true,
|
||||
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
selectedPremise: (state) => state.premisses.find(p => p.id === state.singleSelectId),
|
||||
singleSelectEmpty: (state) => state.selectedLoading === false && state.singleSelectId === null,
|
||||
getDestinationById: (state) => (id) => state.premisses.find(p => p.id === state.singleSelectId)?.destinations.find(d => d.id === id),
|
||||
getSelectedDestinations: (state) => state.selectedDestinations,
|
||||
getSelectedDestinationsById: (state) => (id) => state.selectedDestinations?.find(d => d.id === id),
|
||||
/**
|
||||
* Returns the ids of all premises.
|
||||
* @param state
|
||||
* @returns {*}
|
||||
*/
|
||||
getPremiseIds(state) {
|
||||
if (state.loading) {
|
||||
if (state.throwsException)
|
||||
throw new Error("Premises are accessed while still loading.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return state.premisses?.map(p => p.id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the premises.
|
||||
* @param state
|
||||
* @returns {null|*[]|*}
|
||||
*/
|
||||
getPremisses(state) {
|
||||
if (state.loading) {
|
||||
if (state.throwsException)
|
||||
throw new Error("Premises are accessed while still loading.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return state.premisses;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the premise with the given id.
|
||||
* @param state
|
||||
* @returns {function(*): *}
|
||||
*/
|
||||
getById(state) {
|
||||
return function (id) {
|
||||
if (state.loading) {
|
||||
if (state.throwsException)
|
||||
throw new Error("Premises are accessed while still loading.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if((id ?? null) === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return state.premisses.find(p => p.id === id);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns all premises that are selected.
|
||||
* @param state
|
||||
* @returns {T[]}
|
||||
*/
|
||||
getSelectedPremisses(state) {
|
||||
if (state.loading || state.selectedLoading) {
|
||||
if (state.throwsException)
|
||||
throw new Error("Premises are accessed while still loading.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return state.premisses.filter(p => p.selected);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Returns all premise ids that are selected.
|
||||
* @param state
|
||||
* @returns {T[]}
|
||||
*/
|
||||
getSelectedPremissesIds(state) {
|
||||
if (state.loading || state.selectedLoading) {
|
||||
if (state.throwsException)
|
||||
throw new Error("Premises are accessed while still loading.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return state.premisses.filter(p => p.selected).map(p => p.id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Getters for controlling frontend views
|
||||
* ======================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns true if no premises are loaded. The frontend can show a message that no premises are found.
|
||||
* @param state
|
||||
* @returns {*|boolean}
|
||||
*/
|
||||
showEmpty(state) {
|
||||
return (!state.loading && (((state.premisses ?? null) === null) || state.premisses.length === 0))
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if the premises are loaded and not empty. The frontend can show a the loaded premisses.
|
||||
* @param state
|
||||
* @returns {boolean}
|
||||
*/
|
||||
showData(state) {
|
||||
return (!state.loading && !((state.premisses ?? null) === null) && state.premisses.length !== 0)
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if the premises are loaded and not empty. The frontend can show a the loaded premisses.
|
||||
* @param state
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isLoading(state) {
|
||||
return (state.loading);
|
||||
},
|
||||
|
||||
showProcessingModal(state) {
|
||||
return state.processMassEdit;
|
||||
},
|
||||
|
||||
/**
|
||||
* Getters for single edit view
|
||||
* ============================
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns true if only one premise is selected.
|
||||
* @param state
|
||||
* @returns {boolean|null}
|
||||
*/
|
||||
isSingleSelect(state) {
|
||||
if (state.loading || state.selectedLoading) {
|
||||
if (state.throwsException)
|
||||
throw new Error("Premises are accessed while still loading.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return state.premisses.filter(p => p.selected).length === 1;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the id of the single selected premise.
|
||||
* @param state
|
||||
* @returns {*}
|
||||
*/
|
||||
singleSelectId(state) {
|
||||
if (state.loading || state.selectedLoading) {
|
||||
if (state.throwsException)
|
||||
throw new Error("Premises are accessed while still loading.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!state.isSingleSelect) {
|
||||
return null;
|
||||
// throw new Error("Single selected premise accessed, but not in single select mode");
|
||||
}
|
||||
|
||||
|
||||
return state.premisses.find(p => p.selected)?.id;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Returns the single selected premise.
|
||||
* @param state
|
||||
* @returns {*}
|
||||
*/
|
||||
singleSelectedPremise(state) {
|
||||
if (state.loading || state.selectedLoading) {
|
||||
if (state.throwsException)
|
||||
throw new Error("Premises are accessed while still loading.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!state.isSingleSelect) {
|
||||
return null;
|
||||
// throw new Error("Single selected premise accessed, but not in single select mode");
|
||||
}
|
||||
|
||||
return state.premisses?.find(p => p.selected);
|
||||
},
|
||||
|
||||
/**
|
||||
* Getters for destination editing
|
||||
* ===============================
|
||||
*/
|
||||
|
||||
getDestinationsView(state) {
|
||||
if(state.massEditDestinations !== null) {
|
||||
return state.massEditDestinations.destinations;
|
||||
}
|
||||
|
||||
return state.singleSelectedPremise?.destinations ?? [];
|
||||
},
|
||||
|
||||
getDestinationById(state) {
|
||||
return function (id) {
|
||||
if (state.loading || state.selectedLoading) {
|
||||
if (state.throwsException)
|
||||
throw new Error("Premises are accessed while still loading.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const p of state.premisses) {
|
||||
const d = p.destinations.find(d => d.id === id);
|
||||
if (d !== null) return d;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getSelectedDestinationsData: (state) => state.selectedDestination,
|
||||
|
||||
/**
|
||||
* Mass editing stuff
|
||||
* ===============================
|
||||
*/
|
||||
|
||||
isMassEdit(state) {
|
||||
return state.massEditDestinations !== null;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
|
||||
/**
|
||||
* Selects all destinations for the given ids for editing.
|
||||
* This creates a copy of the destinations. They are written back as soon as the user closes the dialog.
|
||||
* DESTINATION stuff
|
||||
* =================
|
||||
*/
|
||||
selectDestinations(ids) {
|
||||
|
||||
prepareMassEdit(dataSourcePremiseId, editedPremiseIds) {
|
||||
if (this.premisses === null) return;
|
||||
|
||||
if (!editedPremiseIds || !dataSourcePremiseId || editedPremiseIds.length === 0) return;
|
||||
|
||||
this.massEditDestinations = {
|
||||
premise_ids: editedPremiseIds,
|
||||
destinations: this.getById(dataSourcePremiseId)?.destinations.map(d => this.copyAllButRouting(d)) ?? [],
|
||||
toBeAdded: null,
|
||||
toBeRemoved: null,
|
||||
};
|
||||
|
||||
this.selectedDestination = null;
|
||||
|
||||
},
|
||||
async finishMassEdit() {
|
||||
this.processMassEdit = true;
|
||||
|
||||
|
||||
|
||||
this.processMassEdit = false;
|
||||
},
|
||||
|
||||
copyAllButRouting(from, to = null) {
|
||||
|
||||
const fromIsOrig = (to === null);
|
||||
const d = to ?? {};
|
||||
|
||||
if(fromIsOrig) {
|
||||
d.id = `e${from.id}`;
|
||||
d.destination_node = structuredClone(toRaw(from.destination_node));
|
||||
d.massEdit = true;
|
||||
}
|
||||
|
||||
d.annual_amount = from.annual_amount;
|
||||
d.is_d2d = from.is_d2d;
|
||||
d.rate_d2d = from.is_d2d ? from.rate_d2d : null;
|
||||
|
||||
if (fromIsOrig || from.userDefinedHandlingCosts) {
|
||||
d.disposal_costs = from.disposal_costs;
|
||||
d.repackaging_costs = from.repackaging_costs;
|
||||
d.handling_costs = from.handling_costs;
|
||||
} else {
|
||||
d.disposal_costs = null;
|
||||
d.repackaging_costs = null;
|
||||
d.handling_costs = null;
|
||||
}
|
||||
|
||||
return d;
|
||||
},
|
||||
|
||||
/**
|
||||
* Selects all destinations for the given "ids" for editing.
|
||||
* This creates a copy of the destination with id "id".
|
||||
* They are written back as soon as the user closes the dialog.
|
||||
*/
|
||||
selectDestination(id) {
|
||||
if (this.premisses === null) return;
|
||||
|
||||
const destination = !this.isMassEdit ? this.getDestinationById(id) : this.massEditDestinations.destinations.find(d => d.id === id);
|
||||
|
||||
const selected = [];
|
||||
ids.forEach(id => {
|
||||
const destination = this.getDestinationById(id);
|
||||
|
||||
if ((destination ?? null) == null) {
|
||||
this.error = {
|
||||
const error = {
|
||||
code: 'Frontend error.',
|
||||
message: `Destination not found: ${id}. Please contact support.`,
|
||||
trace: null
|
||||
|
|
@ -44,53 +335,50 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
throw new Error("Internal frontend error: Destination not found: " + id);
|
||||
}
|
||||
|
||||
const selectedDestination = structuredClone(toRaw(destination));
|
||||
selectedDestination.userDefinedHandlingCosts = selectedDestination.handling_costs !== null || selectedDestination.disposal_costs !== null || selectedDestination.repackaging_costs !== null;
|
||||
selected.push(selectedDestination);
|
||||
});
|
||||
|
||||
this.selectedDestinations = selected;
|
||||
const data = structuredClone(toRaw(destination));
|
||||
data.userDefinedHandlingCosts = data.handling_costs !== null || data.disposal_costs !== null || data.repackaging_costs !== null;
|
||||
this.selectedDestination = data;
|
||||
},
|
||||
deselectDestinations(save = false) {
|
||||
if (this.premisses === null) return;
|
||||
|
||||
if (save) {
|
||||
const orig = this.isMassEdit ?
|
||||
this.massEditDestinations.destinations.find(d => d.id === this.selectedDestination.id) :
|
||||
this.getDestinationById(this.selectedDestination.id)
|
||||
|
||||
this.selectedDestinations.forEach(destination => {
|
||||
const orig = this.getDestinationById(destination.id);
|
||||
this.copyAllButRouting(this.selectedDestination, orig);
|
||||
|
||||
orig.annual_amount = destination.annual_amount;
|
||||
orig.is_d2d = destination.is_d2d;
|
||||
orig.rate_d2d = destination.is_d2d ? destination.rate_d2d : null;
|
||||
|
||||
if(destination.userDefinedHandlingCosts) {
|
||||
orig.disposal_costs = destination.disposal_costs;
|
||||
orig.repackaging_costs = destination.repackaging_costs;
|
||||
orig.handling_costs = destination.handling_costs;
|
||||
} else {
|
||||
orig.disposal_costs = null;
|
||||
orig.repackaging_costs = null;
|
||||
orig.handling_costs = null;
|
||||
}
|
||||
|
||||
destination.routes.forEach(route => {
|
||||
if (!this.isMassEdit) {
|
||||
this.selectedDestination.routes.forEach(route => {
|
||||
const origRoute = orig.routes.find(r => r.id === route.id);
|
||||
origRoute.is_selected = route.is_selected;
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.selectedDestinations = [];
|
||||
this.selectedDestination = null;
|
||||
},
|
||||
async deleteDestination(id) {
|
||||
|
||||
if(this.isMassEdit) {
|
||||
|
||||
const idx = this.massEditDestinations.destinations.findIndex(d => d.id === id);
|
||||
|
||||
if(idx === -1) {
|
||||
console.info("Destination not found in mass edit: , id)");
|
||||
return;
|
||||
}
|
||||
|
||||
this.massEditDestinations.destinations.splice(idx, 1);
|
||||
} else {
|
||||
if (this.premisses === null) return;
|
||||
|
||||
const url = `${config.backendUrl}/calculation/destination/${id}`;
|
||||
await this.performRequest('DELETE', url, null, false).catch(async e => {
|
||||
console.error("Unable to delete destination: " + id + "");
|
||||
console.error(e);
|
||||
await this.reloadPremisses(this.premisses.map(p => p.id));
|
||||
throw e;
|
||||
await this.loadPremissesIfNeeded(this.premisses.map(p => p.id));
|
||||
});
|
||||
|
||||
for (const p of this.premisses) {
|
||||
|
|
@ -100,9 +388,40 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
async addDestination(id) {
|
||||
async addDestination(node) {
|
||||
|
||||
if(this.isMassEdit) {
|
||||
|
||||
const existing = this.massEditDestinations.destinations.find(d => d.destination_node.id === node.id);
|
||||
console.log(existing)
|
||||
|
||||
if((existing ?? null) !== null) {
|
||||
console.info("Destination already exists", node.id);
|
||||
return [existing.id];
|
||||
}
|
||||
|
||||
const destination = {
|
||||
id: `v${node.id}`,
|
||||
destination_node: structuredClone(toRaw(node)),
|
||||
massEdit: true,
|
||||
annual_amount: 0,
|
||||
is_d2d: false,
|
||||
rate_d2d: null,
|
||||
disposal_costs: null,
|
||||
repackaging_costs: null,
|
||||
handling_costs: null,
|
||||
userDefinedHandlingCosts: false,
|
||||
};
|
||||
|
||||
this.massEditDestinations.destinations.push(destination);
|
||||
|
||||
return [destination.id];
|
||||
|
||||
} else {
|
||||
const id = node.id;
|
||||
|
||||
const toBeUpdated = this.premisses?.filter(p => p.selected).map(p => p.id);
|
||||
|
||||
if (toBeUpdated === null) return;
|
||||
|
|
@ -111,10 +430,6 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
const url = `${config.backendUrl}/calculation/destination/`;
|
||||
|
||||
|
||||
|
||||
this.loading = true;
|
||||
this.selectedLoading = true;
|
||||
|
||||
const destinations = await this.performRequest('POST', url, body).catch(e => {
|
||||
this.loading = false;
|
||||
this.selectedLoading = false;
|
||||
|
|
@ -122,15 +437,19 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
});
|
||||
|
||||
for (const id of Object.keys(destinations)) {
|
||||
this.premisses.find(p => p.id === parseInt(id)).destinations.push(destinations[id]);
|
||||
this.premisses.find(p => String(p.id) === id).destinations.push(destinations[id]);
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
this.selectedLoading = false;
|
||||
|
||||
return destinations.map(d => d.id);
|
||||
|
||||
return Object.values(destinations).map(d => d.id);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Set methods
|
||||
* (these are more extensive changes. The edited premises are replaced by the one returned by the backend)
|
||||
*/
|
||||
|
||||
async setSupplier(id, updateMasterData) {
|
||||
console.log("setSupplier");
|
||||
const body = {supplier_node_id: id, update_master_data: updateMasterData};
|
||||
|
|
@ -156,67 +475,100 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
|
||||
const data = await this.performRequest('PUT', url, body).catch(e => {
|
||||
this.loading = false;
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (data) {
|
||||
data.forEach(p => p.selected = true);
|
||||
this.replacePremissesById(this.premisses, data);
|
||||
this.premisses = this.replacePremissesById(this.premisses, data);
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
this.selectedLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Replace the premisses with the loaded ones by id.
|
||||
* This is used to update the premisses after a "Set" change.
|
||||
* @param premisses
|
||||
* @param loadedData
|
||||
* @returns {*}
|
||||
*/
|
||||
replacePremissesById(premisses, loadedData) {
|
||||
const replacementMap = new Map(loadedData.map(obj => [obj.id, obj]));
|
||||
return premisses.map(obj => replacementMap.get(obj.id) || obj);
|
||||
const replaced = premisses.map(obj => replacementMap.get(obj.id) || obj);
|
||||
console.log("Replaced", replaced);
|
||||
return replaced;
|
||||
},
|
||||
async selectPremise(id) {
|
||||
|
||||
|
||||
/**
|
||||
* PREMISE stuff
|
||||
* =================
|
||||
*/
|
||||
|
||||
setSelectTo(ids, value) {
|
||||
this.selectedLoading = true;
|
||||
|
||||
const toSelect = Array.isArray(id) ? id : [id];
|
||||
const reload = this.premisses ? !toSelect.every((id) => this.premisses.find(d => d.id === id)) : true;
|
||||
|
||||
if (reload) {
|
||||
try {
|
||||
await this.reloadPremisses(toSelect);
|
||||
} catch (e) {
|
||||
this.premisses.forEach(p => p.selected = ids.includes(p.id) ? value : p.selected);
|
||||
this.selectedLoading = false;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.premisses.forEach(p => p.selected = toSelect.includes(p.id));
|
||||
this.singleSelectId = Array.isArray(id) ? null : id;
|
||||
},
|
||||
async selectPremises(id, ids) {
|
||||
this.selectedLoading = true;
|
||||
await this.loadPremissesIfNeeded(ids);
|
||||
const toSelect = String(id);
|
||||
this.premisses.forEach(p => p.selected = toSelect.includes(String(p.id)));
|
||||
this.selectedLoading = false;
|
||||
},
|
||||
async loadAndSelectSinglePremise(id) {
|
||||
|
||||
}
|
||||
,
|
||||
async reloadPremisses(ids) {
|
||||
this.loading = true;
|
||||
this.selectedLoading = true;
|
||||
this.premises = [];
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('premissIds', `${id}`);
|
||||
const url = `${config.backendUrl}/calculation/edit/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||
|
||||
this.premisses = await this.performRequest('GET', url, null).catch(e => {
|
||||
this.selectedLoading = false;
|
||||
this.loading = false;
|
||||
});
|
||||
|
||||
this.premisses.forEach(p => p.selected = true);
|
||||
this.selectedLoading = false;
|
||||
this.loading = false;
|
||||
},
|
||||
async loadPremissesIfNeeded(ids) {
|
||||
const reload = this.premisses ? !ids.every((id) => this.premisses.find(d => d.id === id)) : true;
|
||||
|
||||
if (reload) {
|
||||
await this.loadPremissesForced(ids);
|
||||
}
|
||||
},
|
||||
async loadPremissesForced(ids) {
|
||||
|
||||
|
||||
this.loading = true;
|
||||
this.empty = true;
|
||||
this.premises = [];
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('premissIds', ids.join(', '));
|
||||
const url = `${config.backendUrl}/calculation/edit/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||
|
||||
const data = await this.performRequest('GET', url, null).catch(e => {
|
||||
this.premisses = await this.performRequest('GET', url, null).catch(e => {
|
||||
this.loading = false;
|
||||
throw e;
|
||||
})
|
||||
});
|
||||
|
||||
this.premisses = data;
|
||||
this.empty = data.length === 0;
|
||||
this.premisses.forEach(p => p.selected = false);
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
removePremise(id) {
|
||||
const idx = this.premisses.findIndex(p => p.id === id);
|
||||
this.premisses.splice(idx, 1);
|
||||
},
|
||||
|
||||
async performRequest(method, url, body, expectResponse = true) {
|
||||
|
||||
const params = {
|
||||
|
|
@ -235,47 +587,57 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
|
||||
const response = await fetch(url, params
|
||||
).catch(e => {
|
||||
this.error = {
|
||||
const error = {
|
||||
code: 'Network error.',
|
||||
message: "Please check your internet connection.",
|
||||
trace: null
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(error, {store: this, request: request});
|
||||
|
||||
throw e;
|
||||
});
|
||||
|
||||
let data = null;
|
||||
if (expectResponse) {
|
||||
data = await response.json().catch(e => {
|
||||
this.error = {
|
||||
const error = {
|
||||
code: 'Malformed response',
|
||||
message: "Malformed server response. Please contact support.",
|
||||
trace: null
|
||||
}
|
||||
|
||||
console.error(this.error);
|
||||
console.error(error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(this.error, { store: this, request: request});
|
||||
void errorStore.addError(error, {store: this, request: request});
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
this.error = {code: data.error.title, message: data.error.message, trace: data.error.details}
|
||||
const error = {
|
||||
code: data.error.code,
|
||||
title: data.error.title,
|
||||
message: data.error.message,
|
||||
trace: data.error.trace
|
||||
}
|
||||
|
||||
console.error(this.error);
|
||||
console.error(error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(this.error, {store: this, request: request});
|
||||
void errorStore.addError(error, {store: this, request: request});
|
||||
throw new Error('Internal backend error');
|
||||
}
|
||||
} else {
|
||||
if (!response.ok) {
|
||||
this.error = {
|
||||
const error = {
|
||||
code: "Return code error " + response.status,
|
||||
message: "Server returned wrong response code",
|
||||
trace: null
|
||||
}
|
||||
console.error(this.error);
|
||||
console.error(error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(this.error, { store: this, request: request});
|
||||
void errorStore.addError(error, {store: this, request: request});
|
||||
throw new Error('Internal backend error');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
|
|||
import org.springframework.web.method.annotation.HandlerMethodValidationException;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
|
@ -27,13 +28,10 @@ public class GlobalExceptionHandler {
|
|||
HttpMessageNotReadableException exception) {
|
||||
|
||||
ErrorDTO error = new ErrorDTO(
|
||||
exception.getClass().getSimpleName(),
|
||||
exception.getClass().getName(),
|
||||
"Malformed Request",
|
||||
exception.getMessage(),
|
||||
new HashMap<>() {{
|
||||
put("errorMessage", exception.getMessage());
|
||||
put("stackTrace", exception.getStackTrace());
|
||||
}}
|
||||
Arrays.asList(exception.getStackTrace())
|
||||
);
|
||||
|
||||
return new ResponseEntity<>(new ErrorResponseDTO(error), HttpStatus.BAD_REQUEST);
|
||||
|
|
@ -45,13 +43,10 @@ public class GlobalExceptionHandler {
|
|||
MethodArgumentNotValidException exception) {
|
||||
|
||||
ErrorDTO error = new ErrorDTO(
|
||||
exception.getClass().getSimpleName(),
|
||||
exception.getClass().getName(),
|
||||
"Constraint Violation",
|
||||
exception.getMessage(),
|
||||
new HashMap<>() {{
|
||||
put("errorMessage", exception.getMessage());
|
||||
put("stackTrace", exception.getStackTrace());
|
||||
}}
|
||||
Arrays.asList(exception.getStackTrace())
|
||||
);
|
||||
|
||||
return new ResponseEntity<>(new ErrorResponseDTO(error), HttpStatus.BAD_REQUEST);
|
||||
|
|
@ -79,13 +74,10 @@ public class GlobalExceptionHandler {
|
|||
public ResponseEntity<ErrorResponseDTO> handleMethodArgumentNotValid(BadRequestException exception) {
|
||||
|
||||
ErrorDTO error = new ErrorDTO(
|
||||
exception.getClass().getSimpleName(),
|
||||
exception.getClass().getName(),
|
||||
exception.getTitle(),
|
||||
exception.getMessage(),
|
||||
new HashMap<>() {{
|
||||
put("errorMessage", exception.getMessage());
|
||||
put("stackTrace", exception.getStackTrace());
|
||||
}}
|
||||
Arrays.asList(exception.getStackTrace())
|
||||
);
|
||||
|
||||
return new ResponseEntity<>(new ErrorResponseDTO(error), HttpStatus.BAD_REQUEST);
|
||||
|
|
@ -98,13 +90,10 @@ public class GlobalExceptionHandler {
|
|||
public ResponseEntity<ErrorResponseDTO> handleConstraintViolation(MethodArgumentTypeMismatchException exception) { //
|
||||
|
||||
ErrorDTO error = new ErrorDTO(
|
||||
exception.getClass().getSimpleName(),
|
||||
exception.getClass().getName(),
|
||||
"Wrong Datatype",
|
||||
exception.getMessage(),
|
||||
new HashMap<>() {{
|
||||
put("errorMessage", exception.getMessage());
|
||||
put("stackTrace", exception.getStackTrace());
|
||||
}}
|
||||
Arrays.asList(exception.getStackTrace())
|
||||
);
|
||||
|
||||
return new ResponseEntity<>(new ErrorResponseDTO(error), HttpStatus.BAD_REQUEST);
|
||||
|
|
@ -115,13 +104,10 @@ public class GlobalExceptionHandler {
|
|||
public ResponseEntity<ErrorResponseDTO> handleForbiddenException(ForbiddenException exception) { //
|
||||
|
||||
ErrorDTO error = new ErrorDTO(
|
||||
exception.getClass().getSimpleName(),
|
||||
exception.getClass().getName(),
|
||||
"Forbidden Error",
|
||||
exception.getMessage(),
|
||||
new HashMap<>() {{
|
||||
put("errorMessage", exception.getMessage());
|
||||
put("stackTrace", exception.getStackTrace());
|
||||
}}
|
||||
Arrays.asList(exception.getStackTrace())
|
||||
);
|
||||
|
||||
return new ResponseEntity<>(new ErrorResponseDTO(error), HttpStatus.FORBIDDEN);
|
||||
|
|
@ -132,13 +118,10 @@ public class GlobalExceptionHandler {
|
|||
public ResponseEntity<ErrorResponseDTO> handleConstraintViolation(ConstraintViolationException exception) { //
|
||||
|
||||
ErrorDTO error = new ErrorDTO(
|
||||
exception.getClass().getSimpleName(),
|
||||
exception.getClass().getName(),
|
||||
"Constraint Violation",
|
||||
exception.getMessage(),
|
||||
new HashMap<>() {{
|
||||
put("errorMessage", exception.getMessage());
|
||||
put("stackTrace", exception.getStackTrace());
|
||||
}}
|
||||
Arrays.asList(exception.getStackTrace())
|
||||
);
|
||||
|
||||
return new ResponseEntity<>(new ErrorResponseDTO(error), HttpStatus.BAD_REQUEST);
|
||||
|
|
@ -147,13 +130,10 @@ public class GlobalExceptionHandler {
|
|||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ErrorResponseDTO> handleGenericException(Exception exception) {
|
||||
ErrorDTO error = new ErrorDTO(
|
||||
exception.getClass().getSimpleName(),
|
||||
exception.getClass().getName(),
|
||||
"Internal Server Error",
|
||||
exception.getMessage(),
|
||||
new HashMap<>() {{
|
||||
put("errorMessage", exception.getMessage());
|
||||
put("stackTrace", exception.getStackTrace());
|
||||
}}
|
||||
Arrays.asList(exception.getStackTrace())
|
||||
);
|
||||
|
||||
return new ResponseEntity<>(new ErrorResponseDTO(error), HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ public class PremiseController {
|
|||
|
||||
@GetMapping({"/search", "/search/"})
|
||||
public ResponseEntity<PremiseSearchResultDTO> findMaterialsAndSuppliers(@RequestHeader String search) {
|
||||
|
||||
try {
|
||||
// Decode the header value
|
||||
String decodedValue = URLDecoder.decode(search, StandardCharsets.UTF_8);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ public class DestinationDTO {
|
|||
@JsonProperty("rate_d2d")
|
||||
private BigDecimal rateD2d;
|
||||
|
||||
private List<RouteDTO> routes;
|
||||
|
||||
public Boolean getD2d() {
|
||||
return d2d;
|
||||
}
|
||||
|
|
@ -47,7 +49,6 @@ public class DestinationDTO {
|
|||
this.annualAmount = annualAmount;
|
||||
}
|
||||
|
||||
private List<RouteDTO> routes;
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package de.avatic.lcc.dto.error;
|
||||
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
|
|
@ -8,16 +10,16 @@ public class ErrorDTO {
|
|||
private String code;
|
||||
private String title;
|
||||
private String message;
|
||||
private Map<String, Object> details;
|
||||
private List<StackTraceElement> trace;
|
||||
|
||||
public ErrorDTO() {
|
||||
}
|
||||
|
||||
public ErrorDTO(String code, String title, String message, Map<String, Object> details) {
|
||||
public ErrorDTO(String code, String title, String message, List<StackTraceElement> trace) {
|
||||
this.code = code;
|
||||
this.title = title;
|
||||
this.message = message;
|
||||
this.details = details;
|
||||
this.trace = trace;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
|
|
@ -44,12 +46,13 @@ public class ErrorDTO {
|
|||
this.title = title;
|
||||
}
|
||||
|
||||
public Map<String, Object> getDetails() {
|
||||
return details;
|
||||
|
||||
public List<StackTraceElement> getTrace() {
|
||||
return trace;
|
||||
}
|
||||
|
||||
public void setDetails(Map<String, Object> details) {
|
||||
this.details = details;
|
||||
public void setTrace(List<StackTraceElement> trace) {
|
||||
this.trace = trace;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -57,7 +60,7 @@ public class ErrorDTO {
|
|||
return "ErrorDTO{" +
|
||||
"code='" + code + '\'' +
|
||||
", message='" + message + '\'' +
|
||||
", details=" + details +
|
||||
", trace=" + trace +
|
||||
'}';
|
||||
}
|
||||
|
||||
|
|
@ -68,12 +71,12 @@ public class ErrorDTO {
|
|||
ErrorDTO errorDTO = (ErrorDTO) o;
|
||||
return Objects.equals(code, errorDTO.code) &&
|
||||
Objects.equals(message, errorDTO.message) &&
|
||||
Objects.equals(details, errorDTO.details);
|
||||
Objects.equals(trace, errorDTO.trace);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(code, message, details);
|
||||
return Objects.hash(code, message, trace);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,6 +122,8 @@ public class RouteNodeRepository {
|
|||
|
||||
entity.setOutdated(rs.getBoolean("is_outdated"));
|
||||
|
||||
entity.setExternalMappingId(rs.getString("external_mapping_id"));
|
||||
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -352,6 +352,7 @@ public class RoutingService {
|
|||
// if the chain is routable -> add the final rate and save the route.
|
||||
if (routable) {
|
||||
routeObj.addSection(finalSection);
|
||||
routeObj.setQuality(ChainQuality.SUPERIOR);
|
||||
container.addRoute(routeObj);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ public class NodeTransformer {
|
|||
dto.setDeprecated(entity.getOutdated());
|
||||
dto.setLocation(locationTransformer.toLocationDTO(entity));
|
||||
dto.setUserNode(false);
|
||||
dto.setExternalMappingId(entity.getExternalMappingId());
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -448,7 +448,7 @@ CREATE TABLE IF NOT EXISTS premise_route_section
|
|||
CONSTRAINT fk_premise_route_section_premise_route_id FOREIGN KEY (premise_route_id) REFERENCES premise_route (id),
|
||||
FOREIGN KEY (from_route_node_id) REFERENCES premise_route_node (id),
|
||||
FOREIGN KEY (to_route_node_id) REFERENCES premise_route_node (id),
|
||||
CONSTRAINT chk_main_run CHECK ((transport_type != 'RAIL' AND transport_type != 'SEA') OR is_main_run IS TRUE),
|
||||
CONSTRAINT chk_main_run CHECK (transport_type = 'ROAD' OR transport_type = 'POST_RUN' OR is_main_run IS TRUE),
|
||||
INDEX idx_premise_route_id (premise_route_id),
|
||||
INDEX idx_from_route_node_id (from_route_node_id),
|
||||
INDEX idx_to_route_node_id (to_route_node_id)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue