FRONTEND/BACKEND: Add comprehensive error capturing and reporting functionality; implement ErrorController and FrontendErrorDTO for backend handling; introduce useErrorStore in frontend for collecting and transmitting errors; integrate error handling into PremiseEditStore, MaterialStore, and other related components; enhance DestinationListView and destination editing workflow with new modal behavior and user-defined cost handling.
This commit is contained in:
parent
b13f5db876
commit
6eaf3d4abc
31 changed files with 1686 additions and 347 deletions
156
src/frontend/src/common.js
Normal file
156
src/frontend/src/common.js
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
export const parseNumberFromString = (value, decimals = 2) => {
|
||||
|
||||
if (typeof value === 'number') return value;
|
||||
if (!value || typeof value !== 'string') return 0;
|
||||
|
||||
/* determine decimal separator */
|
||||
const lastDot = value.lastIndexOf('.');
|
||||
const lastComma = value.lastIndexOf(',');
|
||||
const decimalSeparator = (lastDot > lastComma) ? '.' : ',';
|
||||
const hasSeperator = !(lastDot === -1 && lastComma === -1);
|
||||
|
||||
if (hasSeperator) {
|
||||
/* remove all digit grouping */
|
||||
value = (decimalSeparator === '.') ? value.replace(',', '') : value.replace('.', '');
|
||||
|
||||
/* remove artificial decimal separators */
|
||||
const parts = value.split(decimalSeparator);
|
||||
value = parts.slice(0, -1).join('') + decimalSeparator + parts[parts.length - 1];
|
||||
}
|
||||
|
||||
/* replace ',' with '.' and remove all non-digit characters */
|
||||
const normalizedValue = value.replace(',', '.').replace(/[^0-9.]/g, '');
|
||||
const parsed = parseFloat(normalizedValue);
|
||||
|
||||
if (isNaN(parsed)) return 0;
|
||||
return Math.round(parsed * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
||||
}
|
||||
|
||||
export class UrlSafeBase64 {
|
||||
constructor() {
|
||||
// Standard Base64 alphabet
|
||||
this.base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
// URL-safe Base64 alphabet (replacing + with - and / with _)
|
||||
this.urlSafeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
||||
|
||||
// Create reverse lookup table for decoding
|
||||
this.decodeTable = {};
|
||||
for (let i = 0; i < this.urlSafeChars.length; i++) {
|
||||
this.decodeTable[this.urlSafeChars[i]] = i;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a string to URL-safe Base64
|
||||
* @param {string} input - The string to encode
|
||||
* @returns {string} URL-safe Base64 encoded string
|
||||
*/
|
||||
encode(input) {
|
||||
if (!input) return '';
|
||||
|
||||
// Convert string to bytes
|
||||
const bytes = new TextEncoder().encode(input);
|
||||
let result = '';
|
||||
let i = 0;
|
||||
|
||||
// Process 3 bytes at a time
|
||||
while (i < bytes.length) {
|
||||
const byte1 = bytes[i++];
|
||||
const byte2 = i < bytes.length ? bytes[i++] : 0;
|
||||
const byte3 = i < bytes.length ? bytes[i++] : 0;
|
||||
|
||||
const bitmap = (byte1 << 16) | (byte2 << 8) | byte3;
|
||||
|
||||
// Extract 6-bit segments
|
||||
const char1 = (bitmap >> 18) & 63;
|
||||
const char2 = (bitmap >> 12) & 63;
|
||||
const char3 = (bitmap >> 6) & 63;
|
||||
const char4 = bitmap & 63;
|
||||
|
||||
// Add characters based on how many bytes we had
|
||||
result += this.urlSafeChars[char1];
|
||||
result += this.urlSafeChars[char2];
|
||||
result += (i - 2) < bytes.length ? this.urlSafeChars[char3] : '=';
|
||||
result += (i - 1) < bytes.length ? this.urlSafeChars[char4] : '=';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a URL-safe Base64 string
|
||||
* @param {string} input - The URL-safe Base64 string to decode
|
||||
* @returns {string} Decoded string
|
||||
*/
|
||||
decode(input) {
|
||||
if (!input) return '';
|
||||
|
||||
// Remove padding if present
|
||||
input = input.replace(/=/g, '');
|
||||
|
||||
// Validate input
|
||||
if (!/^[A-Za-z0-9\-_]*$/.test(input)) {
|
||||
throw new Error('Invalid URL-safe Base64 string');
|
||||
}
|
||||
|
||||
const bytes = [];
|
||||
let i = 0;
|
||||
|
||||
// Process 4 characters at a time
|
||||
while (i < input.length) {
|
||||
const char1 = this.decodeTable[input[i++]] || 0;
|
||||
const char2 = i < input.length ? this.decodeTable[input[i++]] || 0 : 0;
|
||||
const char3 = i < input.length ? this.decodeTable[input[i++]] || 0 : 0;
|
||||
const char4 = i < input.length ? this.decodeTable[input[i++]] || 0 : 0;
|
||||
|
||||
const bitmap = (char1 << 18) | (char2 << 12) | (char3 << 6) | char4;
|
||||
|
||||
// Extract original bytes
|
||||
const byte1 = (bitmap >> 16) & 255;
|
||||
const byte2 = (bitmap >> 8) & 255;
|
||||
const byte3 = bitmap & 255;
|
||||
|
||||
bytes.push(byte1);
|
||||
if (i - 3 < input.length) bytes.push(byte2);
|
||||
if (i - 2 < input.length) bytes.push(byte3);
|
||||
}
|
||||
|
||||
// Convert bytes back to string
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode without padding (optional for URLs)
|
||||
* @param {string} input - The string to encode
|
||||
* @returns {string} URL-safe Base64 encoded string without padding
|
||||
*/
|
||||
encodeNoPadding(input) {
|
||||
return this.encode(input).replace(/=/g, '');
|
||||
}
|
||||
|
||||
encodeIds(ids) {
|
||||
return this.encodeNoPadding(ids.join(','));
|
||||
}
|
||||
|
||||
decodeIds(encodedIds) {
|
||||
return this.decode(encodedIds).split(',').map(id => parseInt(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert standard Base64 to URL-safe Base64
|
||||
* @param {string} base64 - Standard Base64 string
|
||||
* @returns {string} URL-safe Base64 string
|
||||
*/
|
||||
fromStandardBase64(base64) {
|
||||
return base64.replace(/\+/g, '-').replace(/\//g, '_');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert URL-safe Base64 to standard Base64
|
||||
* @param {string} urlSafeBase64 - URL-safe Base64 string
|
||||
* @returns {string} Standard Base64 string
|
||||
*/
|
||||
toStandardBase64(urlSafeBase64) {
|
||||
return urlSafeBase64.replace(/-/g, '+').replace(/_/g, '/');
|
||||
}
|
||||
}
|
||||
|
|
@ -20,4 +20,35 @@ export default {
|
|||
position: relative;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Fade transition for Vue.js */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-enter-to,
|
||||
.fade-leave-from {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Optional: Add a slight delay to prevent content jumping */
|
||||
.fade-enter-active {
|
||||
transition-delay: 0.15s;
|
||||
}
|
||||
|
||||
.fade-leave-active {
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
/* Ensure the transition container maintains consistent height */
|
||||
.destination-edit-routes {
|
||||
min-height: fit-content;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -1,12 +1,19 @@
|
|||
<template>
|
||||
<div class="list-edit-container">
|
||||
<icon-button variant="blue" icon="pencil-simple" help-text="Edit all selected calculations"></icon-button>
|
||||
<icon-button variant="blue" icon="trash" help-text="Delete all selected calculations"></icon-button>
|
||||
<icon-button variant="blue" icon="archive" help-text="Archive all selected calculations"></icon-button>
|
||||
<transition
|
||||
name="list-edit-transition"
|
||||
tag="div"
|
||||
class="list-edit-container"
|
||||
|
||||
>
|
||||
<div v-if="show" class="list-edit">
|
||||
<icon-button variant="blue" icon="pencil-simple" help-text="Edit all selected calculations" @click="handleAction('edit')"></icon-button>
|
||||
<icon-button variant="blue" icon="trash" help-text="Delete all selected calculations" @click="handleAction('delete')"></icon-button>
|
||||
<icon-button variant="blue" icon="archive" help-text="Archive all selected calculations" @click="handleAction('archive')"></icon-button>
|
||||
<!-- <icon-button variant="blue" icon="pencil-simple" ></icon-button>-->
|
||||
<!-- <icon-button variant="blue" icon="trash" ></icon-button>-->
|
||||
<!-- <icon-button variant="blue" icon="archive" ></icon-button>-->
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
</template>
|
||||
|
||||
|
|
@ -15,12 +22,33 @@ import IconButton from "@/components/UI/IconButton.vue";
|
|||
|
||||
export default{
|
||||
name: "ListEdit",
|
||||
components: {IconButton}
|
||||
components: {IconButton},
|
||||
emits: ['action'],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
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: 9999;
|
||||
}
|
||||
|
||||
.list-edit {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
|
@ -33,4 +61,28 @@ export default{
|
|||
}
|
||||
|
||||
|
||||
|
||||
/* 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>
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
type="radio"
|
||||
:name="name"
|
||||
:value="value"
|
||||
v-model="selectedValue"
|
||||
:checked="isSelected"
|
||||
>
|
||||
<span class="radiomark"></span>
|
||||
<span class="radio-label"><slot></slot></span>
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
<script>
|
||||
export default{
|
||||
emits:["option-changed"],
|
||||
emits:["update:modelValue", "option-changed"],
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number, Boolean],
|
||||
|
|
@ -32,31 +32,17 @@ export default{
|
|||
}
|
||||
},
|
||||
name: "RadioOption",
|
||||
data() {
|
||||
return {
|
||||
internalValue: this.modelValue,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedValue: {
|
||||
get() {
|
||||
return this.internalValue;
|
||||
},
|
||||
set(value) {
|
||||
this.internalValue = value;
|
||||
this.$emit('option-changed', value);
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modelValue(newVal) {
|
||||
this.internalValue = newVal;
|
||||
isSelected() {
|
||||
return this.modelValue === this.value;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setOption(event) {
|
||||
// The computed setter will handle the emit
|
||||
this.selectedValue = event.target.value;
|
||||
if (event.target.checked) {
|
||||
this.$emit('update:modelValue', this.value);
|
||||
this.$emit('option-changed', this.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,40 +1,42 @@
|
|||
// TabContainer.vue - Main tabs component
|
||||
<template>
|
||||
<div class="tab-container">
|
||||
<!-- Tab Headers -->
|
||||
<div class="tab-headers" :class="{ 'tab-headers-vertical': vertical }">
|
||||
<div class="tab-headers">
|
||||
<button
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="tab.id || index"
|
||||
:class="[
|
||||
'tab-header',
|
||||
{ 'tab-header-active': activeTab === index },
|
||||
{ 'tab-header-disabled': tab.disabled }
|
||||
]"
|
||||
@click="selectTab(index)"
|
||||
:disabled="tab.disabled"
|
||||
:aria-selected="activeTab === index"
|
||||
role="tab"
|
||||
:key="index"
|
||||
:class="['tab-header', { active: activeTab === index }]"
|
||||
@click="setActiveTab(index)"
|
||||
>
|
||||
<component :is="tab.icon"></component>
|
||||
{{ tab.title }}
|
||||
<span v-if="tab.badge" class="tab-badge">{{ tab.badge }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="tab-content" :class="{ 'tab-content-vertical': vertical }">
|
||||
<!-- Tab Content with CSS Animations -->
|
||||
<div class="tab-content">
|
||||
<div
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="tab.id || index"
|
||||
v-show="activeTab === index"
|
||||
class="tab-panel"
|
||||
role="tabpanel"
|
||||
:aria-hidden="activeTab !== index"
|
||||
:key="index"
|
||||
:class="['tab-pane', {
|
||||
active: activeTab === index,
|
||||
'slide-in': activeTab === index && hasChanged,
|
||||
'slide-out': activeTab !== index && hasChanged
|
||||
}]"
|
||||
>
|
||||
<slot :name="tab.slot || `tab-${index}`" :tab="tab" :index="index">
|
||||
<div v-html="tab.content"></div>
|
||||
</slot>
|
||||
<!-- Render component if provided -->
|
||||
<component
|
||||
v-if="tab.component"
|
||||
:is="tab.component"
|
||||
v-bind="tab.props || {}"
|
||||
/>
|
||||
<!-- Render slot content if provided -->
|
||||
<div v-else-if="tab.slot">
|
||||
<slot :name="tab.slot"></slot>
|
||||
</div>
|
||||
<!-- Render HTML content if provided -->
|
||||
<div v-else-if="tab.content" v-html="tab.content"></div>
|
||||
<!-- Fallback text content -->
|
||||
<div v-else>{{ tab.text || 'No content provided' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -48,205 +50,148 @@ export default {
|
|||
type: Array,
|
||||
required: true,
|
||||
validator(tabs) {
|
||||
return tabs.every(tab => tab.title)
|
||||
return tabs.every(tab => tab.title && typeof tab.title === 'string');
|
||||
}
|
||||
},
|
||||
defaultTab: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
vertical: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
animated: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTab: this.defaultTab
|
||||
}
|
||||
activeTab: this.defaultTab,
|
||||
hasChanged: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
selectTab(index) {
|
||||
if (this.tabs[index] && !this.tabs[index].disabled) {
|
||||
const previousTab = this.activeTab
|
||||
this.activeTab = index
|
||||
|
||||
this.$emit('tab-changed', {
|
||||
activeTab: index,
|
||||
previousTab,
|
||||
tab: this.tabs[index]
|
||||
})
|
||||
}
|
||||
},
|
||||
setActiveTab(index) {
|
||||
this.selectTab(index)
|
||||
},
|
||||
getActiveTab() {
|
||||
return {
|
||||
index: this.activeTab,
|
||||
tab: this.tabs[this.activeTab]
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
defaultTab: {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
this.activeTab = newVal
|
||||
if (index >= 0 && index < this.tabs.length) {
|
||||
this.hasChanged = true;
|
||||
this.activeTab = index;
|
||||
this.$emit('tab-changed', {
|
||||
index,
|
||||
tab: this.tabs[index]
|
||||
});
|
||||
|
||||
// Reset animation flag
|
||||
setTimeout(() => {
|
||||
this.hasChanged = false;
|
||||
}, 400);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// Emit initial tab
|
||||
this.$emit('tab-changed', {
|
||||
activeTab: this.activeTab,
|
||||
previousTab: null,
|
||||
index: this.activeTab,
|
||||
tab: this.tabs[this.activeTab]
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tab-container {
|
||||
width: 100%;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* Horizontal Layout (default) */
|
||||
.tab-headers {
|
||||
display: flex;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
background-color: #f8fafc;
|
||||
border-bottom: 1px solid rgba(107, 134, 156, 0.2);
|
||||
background-color: transparent;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
.tab-header {
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
color: #6b7280;
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
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;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 1rem;
|
||||
background-color: #ffffff;
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
min-height: 200px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Vertical Layout */
|
||||
.tab-container:has(.tab-headers-vertical) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tab-headers-vertical {
|
||||
flex-direction: column;
|
||||
border-bottom: none;
|
||||
border-right: 2px solid #e2e8f0;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.tab-content-vertical {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Tab Header Styles */
|
||||
.tab-header {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.4rem;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab-headers-vertical .tab-header {
|
||||
border-bottom: none;
|
||||
border-right: 2px solid transparent;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.tab-header:hover:not(.tab-header-disabled) {
|
||||
color: #3b82f6;
|
||||
background-color: #f1f5f9;
|
||||
}
|
||||
|
||||
.tab-header-active {
|
||||
color: #3b82f6;
|
||||
border-bottom-color: #3b82f6;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.tab-headers-vertical .tab-header-active {
|
||||
border-bottom-color: transparent;
|
||||
border-right-color: #3b82f6;
|
||||
}
|
||||
|
||||
.tab-header-disabled {
|
||||
color: #cbd5e1;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.tab-header-disabled:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Tab Icon and Badge */
|
||||
.tab-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.tab-badge {
|
||||
background-color: #ef4444;
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.75rem;
|
||||
min-width: 1.25rem;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tab-header-active .tab-badge {
|
||||
background-color: #3b82f6;
|
||||
}
|
||||
|
||||
/* Tab Panel */
|
||||
.tab-panel {
|
||||
animation: fadeIn 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
.tab-pane {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
opacity: 0;
|
||||
transform: translateY(0.5rem);
|
||||
transform: translateY(20px);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
pointer-events: none;
|
||||
}
|
||||
to {
|
||||
|
||||
.tab-pane.active {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
position: relative;
|
||||
top: auto;
|
||||
left: auto;
|
||||
right: auto;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.tab-pane.slide-in {
|
||||
animation: slideInUp 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
.tab-pane.slide-out {
|
||||
animation: slideOutUp 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
/* Keyframe Animations */
|
||||
@keyframes slideInUp {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.tab-headers {
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
/* Keyframe Animations */
|
||||
@keyframes slideOutUp {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-headers::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-header {
|
||||
flex-shrink: 0;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
239
src/frontend/src/components/UI/Toast.vue
Normal file
239
src/frontend/src/components/UI/Toast.vue
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
<template>
|
||||
<teleport to="body">
|
||||
<transition-group
|
||||
name="toast"
|
||||
tag="div"
|
||||
class="toast-container"
|
||||
>
|
||||
<div
|
||||
v-for="toast in toasts"
|
||||
:key="toast.id"
|
||||
:class="['toast', `toast--${toast.variant}`]"
|
||||
@click="removeToast(toast.id)"
|
||||
>
|
||||
<div v-if="toast.icon" class="toast__icon">
|
||||
<component
|
||||
:is="toast.icon"
|
||||
weight="regular"
|
||||
size="24"
|
||||
class="icon-btn"
|
||||
:class="toast__icon_sizing"
|
||||
/>
|
||||
</div>
|
||||
<div class="toast__content">
|
||||
<div v-if="toast.title" class="toast__title">{{ toast.title }}</div>
|
||||
<div class="toast__message">{{ toast.message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {PhCheck, PhFloppyDisk} from "@phosphor-icons/vue";
|
||||
|
||||
export default {
|
||||
name: 'Toast',
|
||||
components: {PhFloppyDisk, PhCheck},
|
||||
data() {
|
||||
return {
|
||||
toasts: [],
|
||||
nextId: 1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Add a new toast notification
|
||||
* @param {Object} options - Toast configuration
|
||||
* @param {string} options.message - Toast message (required)
|
||||
* @param {string} options.title - Toast title (optional)
|
||||
* @param {string} options.variant - Toast variant: 'success', 'error', 'warning', 'info'
|
||||
* @param {number} options.duration - Auto-dismiss duration in ms (0 = no auto-dismiss)
|
||||
* @param {string} options.icon - Icon name (optional)
|
||||
*/
|
||||
addToast(options = {}) {
|
||||
const toast = {
|
||||
id: this.nextId++,
|
||||
message: options.message || 'Notification',
|
||||
title: options.title || null,
|
||||
variant: options.variant || 'primary',
|
||||
duration: options.duration !== undefined ? options.duration : 5000,
|
||||
icon: options.icon ? `Ph${options.icon.charAt(0).toUpperCase() + options.icon.slice(1)}` : null,
|
||||
};
|
||||
|
||||
this.toasts.push(toast)
|
||||
|
||||
// Auto-dismiss if duration is set
|
||||
if (toast.duration > 0) {
|
||||
setTimeout(() => {
|
||||
this.removeToast(toast.id)
|
||||
}, toast.duration)
|
||||
}
|
||||
|
||||
return toast.id
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a toast by ID
|
||||
* @param {number} id - Toast ID
|
||||
*/
|
||||
removeToast(id) {
|
||||
const index = this.toasts.findIndex(toast => toast.id === id)
|
||||
if (index > -1) {
|
||||
this.toasts.splice(index, 1)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove all toasts
|
||||
*/
|
||||
clearToasts() {
|
||||
this.toasts = []
|
||||
},
|
||||
|
||||
// Convenience methods
|
||||
success(message, options = {}) {
|
||||
return this.addToast({ ...options, message, variant: 'primary' })
|
||||
},
|
||||
|
||||
error(message, options = {}) {
|
||||
return this.addToast({ ...options, message, variant: 'exception' })
|
||||
},
|
||||
|
||||
warning(message, options = {}) {
|
||||
return this.addToast({ ...options, message, variant: 'secondary' })
|
||||
},
|
||||
|
||||
info(message, options = {}) {
|
||||
return this.addToast({ ...options, message, variant: 'secondary' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.toast {
|
||||
background-color: #5AF0B4;
|
||||
color: #002F54;
|
||||
min-width: 30rem;
|
||||
max-width: 40rem;
|
||||
margin-bottom: 1.2rem;
|
||||
padding: 1.6rem;
|
||||
border-radius: 0.8rem;
|
||||
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.6rem;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
|
||||
}
|
||||
|
||||
.toast:hover {
|
||||
transform: translateY(-2px);
|
||||
|
||||
box-shadow: 0 0.5rem 0.9rem rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.toast__icon {
|
||||
flex-shrink: 0;
|
||||
width: 3.6rem;
|
||||
height: 3.6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.toast__icon_sizing {
|
||||
width: 3.6rem;
|
||||
height: 3.6rem;
|
||||
}
|
||||
|
||||
.toast__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toast__title {
|
||||
font-weight: 600;
|
||||
font-size: 1.6rem;
|
||||
margin-bottom: 0.2rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.toast__message {
|
||||
font-size: 1.4rem;
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
|
||||
.toast-primary {
|
||||
background-color: #5AF0B4;
|
||||
color: #002F54;
|
||||
}
|
||||
|
||||
.toast--secondary {
|
||||
background-color: #002F54;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.toast--exception{
|
||||
background-color: #BC2B72;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
|
||||
/* Transition animations */
|
||||
.toast-enter-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.toast-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.toast-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.toast-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.toast-move {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 480px) {
|
||||
.toast-container {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
min-width: auto;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -180,6 +180,7 @@ export default {
|
|||
.tooltip {
|
||||
opacity: 0;
|
||||
animation: tooltipFadeIn 0.2s ease-out forwards;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@keyframes tooltipFadeIn {
|
||||
|
|
|
|||
119
src/frontend/src/components/layout/TraceView.vue
Normal file
119
src/frontend/src/components/layout/TraceView.vue
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
<template>
|
||||
<div class="trace-view-container" :class="trace ? '' : 'trace-view-container--no-trace'">
|
||||
<div class="trace-view-header">
|
||||
<h3 class="sub-header">{{ code }}</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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
|
||||
export default {
|
||||
name: "TraceView",
|
||||
components: {IconButton, BasicButton},
|
||||
emits: ['close'],
|
||||
props: {
|
||||
error: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
trace() {
|
||||
return this.error.trace?.stackTrace;
|
||||
},
|
||||
exception() {
|
||||
return this.error.trace?.errorMessage;
|
||||
},
|
||||
code() {
|
||||
return this.error.code;
|
||||
},
|
||||
showMessage() {
|
||||
return this.error.message !== this.error.trace?.errorMessage;
|
||||
},
|
||||
message() {
|
||||
return this.error.message;
|
||||
},
|
||||
|
||||
},
|
||||
methods: {
|
||||
highlightClasses(traceItem) {
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
.trace-item:hover {
|
||||
background-color: rgba(107, 134, 156, 0.05);
|
||||
}
|
||||
|
||||
.trace-view {
|
||||
overflow: auto;
|
||||
font-family: Roboto Mono, monospace;
|
||||
font-size: 1.2rem;
|
||||
flex: 1;
|
||||
padding: 1.6rem;
|
||||
}
|
||||
|
||||
.trace-view-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trace-view-message {
|
||||
font-size: 1.4rem;
|
||||
color: #002F54;
|
||||
}
|
||||
|
||||
.trace-view-file {
|
||||
font-weight: 500;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.trace-view-exception {
|
||||
font-family: inherit;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.trace-view-container {
|
||||
height: 90vh;
|
||||
width: 80vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.trace-view-container--no-trace {
|
||||
height: auto;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -94,7 +94,7 @@ export default {
|
|||
this.premise.checked = checked;
|
||||
},
|
||||
editClick() {
|
||||
|
||||
this.$router.push({name: 'edit', params: {id: this.id}});
|
||||
},
|
||||
deleteClick() {
|
||||
|
||||
|
|
|
|||
|
|
@ -16,14 +16,15 @@
|
|||
<!-- Header -->
|
||||
<div class="destination-list-header">
|
||||
<div>Destination</div>
|
||||
<div>Annual purchase volume</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"
|
||||
:id="destination.id" :destination="destination" @delete="deleteDestination" @edit="editDestination"></destination-item>
|
||||
:id="destination.id" :destination="destination" @delete="deleteDestination"
|
||||
@edit="editDestination"></destination-item>
|
||||
</div>
|
||||
<div v-else class="empty-container">
|
||||
<span class="space-around">No Destinations found.</span>
|
||||
|
|
@ -31,7 +32,7 @@
|
|||
|
||||
</div>
|
||||
<modal :state="editDestinationModalState">
|
||||
<destination-edit></destination-edit>
|
||||
<destination-edit @accept="deselectDestination(true)" @discard="deselectDestination(false)"></destination-edit>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -51,13 +52,15 @@ import {usePremiseEditStore} from "@/store/premiseEdit.js";
|
|||
import {useNodeStore} from "@/store/node.js";
|
||||
import Modal from "@/components/UI/Modal.vue";
|
||||
import DestinationEdit from "@/components/layout/edit/destination/DestinationEdit.vue";
|
||||
import {UrlSafeBase64} from "@/common.js";
|
||||
|
||||
export default {
|
||||
name: "DestinationListView",
|
||||
components: {
|
||||
DestinationEdit,
|
||||
Modal,
|
||||
DestinationItem, CalculationListItem, Checkbox, Spinner, BasicButton, AutosuggestSearchbar, IconButton},
|
||||
DestinationItem, CalculationListItem, Checkbox, Spinner, BasicButton, AutosuggestSearchbar, IconButton
|
||||
},
|
||||
props: {
|
||||
destinations: {
|
||||
type: Object,
|
||||
|
|
@ -66,7 +69,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
editDestinationModalState: true
|
||||
editDestinationModalState: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -80,16 +83,25 @@ export default {
|
|||
resolveFlag(node) {
|
||||
return node.country.iso_code;
|
||||
},
|
||||
addDestination(node) {
|
||||
async addDestination(node) {
|
||||
console.log(node)
|
||||
this.premiseEditStore.addDestination(node.id);
|
||||
const [id] = await this.premiseEditStore.addDestination(node.id);
|
||||
console.log(id);
|
||||
this.editDestination(id);
|
||||
},
|
||||
deleteDestination(id) {
|
||||
this.premiseEditStore.deleteDestination(id);
|
||||
},
|
||||
editDestination(id) {
|
||||
console.log("edit id: " + id + "")
|
||||
|
||||
editDestination(id) {
|
||||
if (id && this.destinations.some(d => d.id === id)) {
|
||||
this.premiseEditStore.selectDestinations([id]);
|
||||
this.editDestinationModalState = true;
|
||||
}
|
||||
},
|
||||
deselectDestination(save) {
|
||||
this.premiseEditStore.deselectDestinations(save);
|
||||
this.editDestinationModalState = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ import ModalDialog from "@/components/UI/ModalDialog.vue";
|
|||
import {PhArrowCounterClockwise} from "@phosphor-icons/vue";
|
||||
import {useMaterialStore} from "@/store/material.js";
|
||||
import {mapStores} from "pinia";
|
||||
import {parseNumberFromString} from "@/common.js";
|
||||
|
||||
export default {
|
||||
name: "MaterialEdit",
|
||||
|
|
@ -125,17 +126,6 @@ export default {
|
|||
this.selectedMaterial = null;
|
||||
}
|
||||
},
|
||||
parseNumberFromString(value, decimals = 2) {
|
||||
if (typeof value === 'number') return value;
|
||||
if (!value || typeof value !== 'string') return 0;
|
||||
|
||||
const normalizedValue = value.replace(',', '.').replace(/[^0-9.]/g, '');
|
||||
|
||||
const parsed = parseFloat(normalizedValue);
|
||||
|
||||
if (isNaN(parsed)) return 0;
|
||||
return Math.round(parsed * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
||||
},
|
||||
updateInputValue(inputRef, formattedValue) {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs[inputRef] && this.$refs[inputRef].value !== formattedValue) {
|
||||
|
|
@ -145,7 +135,7 @@ export default {
|
|||
},
|
||||
validateInput(type, event) {
|
||||
const decimals = 2
|
||||
const parsed = this.parseNumberFromString(event.target.value, decimals);
|
||||
const parsed = parseNumberFromString(event.target.value, decimals);
|
||||
console.log('validateInput', type, event.target.value, parsed);
|
||||
|
||||
this.$emit(`update:${type}`, parsed);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<div class="container" @focusout="focusLost">
|
||||
<div class="caption-column">Length</div>
|
||||
<div class="input-column">
|
||||
<div class="input-field-container">
|
||||
|
|
@ -64,7 +64,6 @@
|
|||
<checkbox :checked="mixable" @checkbox-changed="updateMixable">mixable</checkbox>
|
||||
</tooltip>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -74,11 +73,12 @@
|
|||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
import Dropdown from "@/components/UI/Dropdown.vue";
|
||||
import Tooltip from "@/components/UI/Tooltip.vue";
|
||||
import {parseNumberFromString} from "@/common.js";
|
||||
|
||||
export default {
|
||||
name: "PackagingEdit",
|
||||
components: {Tooltip, Dropdown, Checkbox},
|
||||
emits: ['update:stackable', 'update:mixable', 'update:length', 'update:width', 'update:height', 'update:weight', 'update:unitCount', 'update:weightUnit', 'update:dimensionUnit'],
|
||||
emits: ['update:stackable', 'update:mixable', 'update:length', 'update:width', 'update:height', 'update:weight', 'update:unitCount', 'update:weightUnit', 'update:dimensionUnit', 'save'],
|
||||
props: {
|
||||
length: {
|
||||
required: true,
|
||||
|
|
@ -192,16 +192,10 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
parseNumberFromString(value, decimals = 2) {
|
||||
if (typeof value === 'number') return value;
|
||||
if (!value || typeof value !== 'string') return 0;
|
||||
|
||||
const normalizedValue = value.replace(',', '.').replace(/[^0-9.]/g, '');
|
||||
|
||||
const parsed = parseFloat(normalizedValue);
|
||||
|
||||
if (isNaN(parsed)) return 0;
|
||||
return Math.round(parsed * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
||||
focusLost(event) {
|
||||
if (!this.$el.contains(event.relatedTarget)) {
|
||||
this.$emit('save', 'packaging');
|
||||
}
|
||||
},
|
||||
updateInputValue(inputRef, formattedValue) {
|
||||
this.$nextTick(() => {
|
||||
|
|
@ -212,7 +206,7 @@ export default {
|
|||
},
|
||||
validateDimension(type, event) {
|
||||
const decimals = (this.huDimensionUnitSelected === 2) ? 2 : ((this.huDimensionUnitSelected === 3) ? 4 : 0);
|
||||
const parsed = this.parseNumberFromString(event.target.value, decimals);
|
||||
const parsed = parseNumberFromString(event.target.value, decimals);
|
||||
|
||||
|
||||
this.$emit(`update:${type}`, parsed);
|
||||
|
|
@ -224,7 +218,7 @@ export default {
|
|||
},
|
||||
validateWeight(type, event) {
|
||||
const decimals = (this.huWeightUnitSelected === 2) ? 4 : ((this.huWeightUnitSelected === 3) ? 8 : 0);
|
||||
const parsed = this.parseNumberFromString(event.target.value, decimals);
|
||||
const parsed = parseNumberFromString(event.target.value, decimals);
|
||||
|
||||
this.$emit('update:weight', parsed);
|
||||
|
||||
|
|
@ -233,7 +227,7 @@ export default {
|
|||
this.updateInputValue('weightInput', formattedValue);
|
||||
},
|
||||
validateCount(event) {
|
||||
const parsed = this.parseNumberFromString(event.target.value, 0);
|
||||
const parsed = parseNumberFromString(event.target.value, 0);
|
||||
this.$emit('update:unitCount', parsed);
|
||||
|
||||
// Force update the input field with the correctly formatted value
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
|
||||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
import Tooltip from "@/components/UI/Tooltip.vue";
|
||||
import {parseNumberFromString} from "@/common.js";
|
||||
|
||||
export default {
|
||||
name: "PriceEdit",
|
||||
|
|
@ -57,18 +58,8 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
parseNumberFromString(value, decimals = 2) {
|
||||
if (typeof value === 'number') return value;
|
||||
if (!value || typeof value !== 'string') return 0;
|
||||
|
||||
const normalizedValue = value.replace(',', '.').replace(/[^0-9.]/g, '');
|
||||
const parsed = parseFloat(normalizedValue);
|
||||
|
||||
if (isNaN(parsed)) return 0;
|
||||
return Math.round(parsed * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
||||
},
|
||||
validatePrice(event) {
|
||||
const value = this.parseNumberFromString(event.target.value, 2);
|
||||
const value = parseNumberFromString(event.target.value, 2);
|
||||
const validatedValue = Math.max(0, value);
|
||||
|
||||
if (validatedValue !== this.price) {
|
||||
|
|
@ -81,7 +72,7 @@ export default {
|
|||
this.$emit('update:includeFcaFee', value);
|
||||
},
|
||||
validateOverSeaShare(event) {
|
||||
const percentValue = this.parseNumberFromString(event.target.value, 4);
|
||||
const percentValue = parseNumberFromString(event.target.value, 4);
|
||||
|
||||
const validatedPercent = Math.max(0, Math.min(100, percentValue));
|
||||
const validatedDecimal = validatedPercent / 100;
|
||||
|
|
@ -107,17 +98,6 @@ export default {
|
|||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: none;
|
||||
resize: none;
|
||||
font-family: inherit;
|
||||
font-size: 1.4rem;
|
||||
color: #002F54;
|
||||
width: 100%;
|
||||
min-width: 5rem;
|
||||
}
|
||||
|
||||
|
||||
.input-column {
|
||||
|
|
@ -130,6 +110,18 @@ export default {
|
|||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: none;
|
||||
resize: none;
|
||||
font-family: inherit;
|
||||
font-size: 1.4rem;
|
||||
color: #002F54;
|
||||
width: 100%;
|
||||
min-width: 5rem;
|
||||
}
|
||||
|
||||
.input-field-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -2,22 +2,12 @@
|
|||
<div class="destination-edit-modal-container">
|
||||
<h3 class="sub-header">Edit Destination</h3>
|
||||
<div class="destination-edit-container">
|
||||
<tab-container :tabs="basicTabs">
|
||||
<template #tab-0>
|
||||
<h3>Dashboard Content</h3>
|
||||
<p>This is the dashboard panel with charts and metrics.</p>
|
||||
</template>
|
||||
|
||||
<template #tab-1>
|
||||
<h3>User Profile</h3>
|
||||
<p>User settings and profile information go here.</p>
|
||||
</template>
|
||||
|
||||
<template #tab-2>
|
||||
<h3>Settings Panel</h3>
|
||||
<p>Application settings and preferences.</p>
|
||||
</template>
|
||||
<tab-container :tabs="tabsConfig" class="tab-container">
|
||||
</tab-container >
|
||||
<div class="destination-edit-actions">
|
||||
<basic-button variant="primary" :show-icon="false" @click="close('accept')">OK</basic-button>
|
||||
<basic-button variant="secondary" :show-icon="false" @click="close('discard')">Cancel</basic-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -27,56 +17,66 @@
|
|||
|
||||
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",
|
||||
components: {TabContainer, Tab},
|
||||
components: {BasicButton, TabContainer, Tab},
|
||||
emits: ['accept', 'discard'],
|
||||
data() {
|
||||
return {
|
||||
basicTabs: [
|
||||
currentTab: null,
|
||||
tabsConfig: [
|
||||
{
|
||||
id: 'dashboard',
|
||||
title: 'Routes',
|
||||
component: markRaw(DestinationEditRoutes),
|
||||
},
|
||||
{
|
||||
id: 'profile',
|
||||
title: 'Packaging & Handling',
|
||||
}
|
||||
],
|
||||
advancedTabs: [
|
||||
{
|
||||
id: 'notifications',
|
||||
title: 'Notifications',
|
||||
icon: 'PhPlus',
|
||||
badge: '5',
|
||||
slot: 'notifications'
|
||||
},
|
||||
{
|
||||
id: 'messages',
|
||||
title: 'Messages',
|
||||
icon: 'PhCheck',
|
||||
badge: '12',
|
||||
slot: 'messages'
|
||||
},
|
||||
{
|
||||
id: 'analytics',
|
||||
title: 'Analytics',
|
||||
icon: 'PhTrash',
|
||||
slot: 'analytics'
|
||||
title: 'Handling & Repackaging',
|
||||
component: markRaw(DestinationEditHandlingCost),
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close(action) {
|
||||
this.$emit(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.tab-container {
|
||||
flex: 1;
|
||||
overflow: auto; /* In case content overflows */
|
||||
}
|
||||
|
||||
.destination-edit-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.destination-edit-modal-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
flex: 0 0 max(80vw, 80rem);
|
||||
flex: 1 0 max(80vw, 80rem);
|
||||
height: min(70vh, 50rem);
|
||||
}
|
||||
|
||||
.destination-edit-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
<template>
|
||||
<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.
|
||||
If needed, you can overwrite these values here.
|
||||
</div>
|
||||
<div>
|
||||
<checkbox :checked="inputFieldsActive" @checkbox-changed="activateInputFields">I want to enter handling and
|
||||
repackaging costs.
|
||||
</checkbox>
|
||||
</div>
|
||||
<div class="destination-edit-handling-cost-container" v-show="inputFieldsActive">
|
||||
<div class="destination-edit-column-caption">Repackaging cost [EUR]</div>
|
||||
<div class="destination-edit-column-data">
|
||||
<div class="input-field-container">
|
||||
<input :value="repackaging" @blur="validate('repackaging', $event)" class="input-field"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="destination-edit-column-caption">Handling cost [EUR]</div>
|
||||
<div class="destination-edit-column-data">
|
||||
<div class="input-field-container">
|
||||
<input :value="handling" @blur="validate('handling', $event)" class="input-field"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="destination-edit-column-caption">Disposal cost [EUR]</div>
|
||||
<div class="destination-edit-column-data">
|
||||
<div class="input-field-container">
|
||||
<input :value="disposal" @blur="validate('disposal', $event)" class="input-field"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {usePremiseEditStore} from "@/store/premiseEdit.js";
|
||||
import {set} from "@vueuse/core";
|
||||
import {parseNumberFromString} from "@/common.js";
|
||||
|
||||
export default {
|
||||
name: "DestinationEditHandlingCost",
|
||||
components: {Checkbox},
|
||||
created() {
|
||||
console.log("Destination:", this.destination)
|
||||
},
|
||||
computed: {
|
||||
...mapStores(usePremiseEditStore),
|
||||
destination() {
|
||||
const [dest] = this.premiseEditStore.getSelectedDestinations;
|
||||
return dest;
|
||||
},
|
||||
repackaging: {
|
||||
get() {
|
||||
return this.destination?.repackaging_costs?.toFixed(2) ?? '0.00';
|
||||
},
|
||||
set(value) {
|
||||
return this.destination && (this.destination.repackaging_costs = value);
|
||||
}
|
||||
},
|
||||
handling: {
|
||||
get() {
|
||||
return this.destination?.handling_costs?.toFixed(2) ?? '0.00';
|
||||
},
|
||||
set(value) {
|
||||
return this.destination && (this.destination.handling_costs = value);
|
||||
}
|
||||
},
|
||||
disposal: {
|
||||
get() {
|
||||
return this.destination?.disposal_costs?.toFixed(2) ?? '0.00';
|
||||
},
|
||||
set(value) {
|
||||
return this.destination && (this.destination.disposal_costs = value);
|
||||
}
|
||||
},
|
||||
inputFieldsActive: {
|
||||
get() {
|
||||
return this.destination?.userDefinedHandlingCosts ?? false;
|
||||
},
|
||||
set(value) {
|
||||
this.destination && (this.destination.userDefinedHandlingCosts = value);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
activateInputFields(value) {
|
||||
if (value !== this.inputFieldsActive) {
|
||||
this.inputFieldsActive = value;
|
||||
this.destination.userDefinedHandlingCosts = value;
|
||||
}
|
||||
},
|
||||
validate(field, event) {
|
||||
const value = parseNumberFromString(event.target.value, 2);
|
||||
const validatedValue = Math.max(0, value);
|
||||
const stringified = validatedValue.toFixed(2);
|
||||
|
||||
if (stringified !== this[field]) {
|
||||
this.destination[`${field}_costs`] = validatedValue;
|
||||
}
|
||||
event.target.value = stringified;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.destination-edit-handling-cost {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.destination-edit-handling-cost-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.4rem;
|
||||
gap: 1.6rem;
|
||||
background-color: #c3cfdf;
|
||||
color: #002F54;
|
||||
border-radius: 0.8rem;
|
||||
padding: 1.6rem;
|
||||
}
|
||||
|
||||
|
||||
.destination-edit-column-caption {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
align-self: center;
|
||||
justify-self: end;
|
||||
color: #001D33;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
.destination-edit-handling-cost-container {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: repeat(4, auto);
|
||||
gap: 1.6rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: none;
|
||||
resize: none;
|
||||
font-family: inherit;
|
||||
font-size: 1.4rem;
|
||||
color: #002F54;
|
||||
width: 100%;
|
||||
min-width: 5rem;
|
||||
}
|
||||
|
||||
.input-field-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
border: 0.2rem solid #E3EDFF;
|
||||
transition: all 0.1s ease;
|
||||
flex: 1 1 fit-content(80rem);
|
||||
}
|
||||
|
||||
.input-field-container:hover {
|
||||
background: #EEF4FF;
|
||||
border: 0.2rem solid #8DB3FE;
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
<template>
|
||||
<div class="destination-edit-routes-container">
|
||||
<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-data">
|
||||
<div class="input-field-container">
|
||||
<input :value="annualAmount" @blur="validateAnnualAmount" class="input-field"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="destination-edit-column-caption">Transport mode</div>
|
||||
<div 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">
|
||||
<transition name="fade" mode="out-in">
|
||||
<div v-if="showRoutes || showRouteWarning" key="routes">Routes</div>
|
||||
<div v-else key="rate">D2D Rate [EUR]</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<!-- Single grid cell for data that transitions content -->
|
||||
<div class="destination-edit-column-data">
|
||||
<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>
|
||||
</div>
|
||||
<div v-else-if="showRouteWarning">
|
||||
<div class="destination-edit-route-warning">
|
||||
<ph-warning size="18px"></ph-warning>
|
||||
Unable to route from supplier to {{this.destination.destination_node.name}}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else key="rate" class="input-field-container">
|
||||
<input :value="rateD2d" @blur="validateRateD2d" class="input-field"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RadioOption from "@/components/UI/RadioOption.vue";
|
||||
import DestinationRoute from "@/components/layout/edit/destination/DestinationRoute.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {usePremiseEditStore} from "@/store/premiseEdit.js";
|
||||
import {parseNumberFromString} from "@/common.js";
|
||||
import Tooltip from "@/components/UI/Tooltip.vue";
|
||||
|
||||
export default {
|
||||
name: "DestinationEditRoutes",
|
||||
components: {Tooltip, DestinationRoute, RadioOption},
|
||||
methods: {
|
||||
selectRoute(id) {
|
||||
// Your route selection logic
|
||||
},
|
||||
validateAnnualAmount(event) {
|
||||
const value = parseNumberFromString(event.target.value, 0);
|
||||
const validatedValue = Math.max(0, value);
|
||||
const stringified = validatedValue.toFixed();
|
||||
|
||||
this.annualAmount = validatedValue;
|
||||
event.target.value = stringified;
|
||||
},
|
||||
validateRateD2d(event) {
|
||||
|
||||
const value = parseNumberFromString(event.target.value, 2);
|
||||
const validatedValue = Math.max(0, value);
|
||||
const stringified = validatedValue.toFixed(2);
|
||||
|
||||
this.rateD2d = validatedValue;
|
||||
event.target.value = stringified;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
console.log("Destination:", this.destination)
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedCalculationModel: 'routing',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
tooltipAnnualAmount() {
|
||||
return `Annual quantity that "${this.destination.destination_node.name}" will source from the supplier`
|
||||
},
|
||||
showRoutes() {
|
||||
return !this.destination.is_d2d && this.destination.routes.length > 0;
|
||||
},
|
||||
showRouteWarning() {
|
||||
return !this.destination.is_d2d && this.destination.routes.length === 0;
|
||||
},
|
||||
calculationModel: {
|
||||
get() {
|
||||
if (this.destination.is_d2d) {
|
||||
return 'd2d';
|
||||
} else {
|
||||
return 'routing';
|
||||
}
|
||||
},
|
||||
set(value) {
|
||||
this.destination.is_d2d = value === 'd2d';
|
||||
}
|
||||
},
|
||||
...mapStores(usePremiseEditStore),
|
||||
destination() {
|
||||
const [dest] = this.premiseEditStore.getSelectedDestinations;
|
||||
return dest;
|
||||
},
|
||||
annualAmount: {
|
||||
get() {
|
||||
return this.destination.annual_amount?.toFixed() ?? '0';
|
||||
},
|
||||
set(value) {
|
||||
this.destination && (this.destination.annual_amount = value);
|
||||
},
|
||||
},
|
||||
rateD2d: {
|
||||
get() {
|
||||
return this.destination.rate_d2d?.toFixed(2) ?? '0.00';
|
||||
},
|
||||
set(value) {
|
||||
this.destination && (this.destination.rate_d2d = value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Smooth fade transition */
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from, .fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.destination-edit-route-warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.4rem;
|
||||
gap: 1.6rem;
|
||||
|
||||
border-radius: 0.8rem;
|
||||
padding: 1.6rem;
|
||||
}
|
||||
|
||||
.destination-edit-cell-routing {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.destination-edit-routes {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: repeat(4, auto);
|
||||
gap: 1.6rem;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.destination-edit-column-data {
|
||||
align-self: center;
|
||||
font-weight: 300;
|
||||
font-size: 1.4rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.destination-edit-column-caption {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
align-self: center;
|
||||
justify-self: end;
|
||||
color: #001D33;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: none;
|
||||
resize: none;
|
||||
font-family: inherit;
|
||||
font-size: 1.4rem;
|
||||
color: #002F54;
|
||||
width: 100%;
|
||||
min-width: 5rem;
|
||||
}
|
||||
|
||||
.input-field-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
border: 0.2rem solid #E3EDFF;
|
||||
transition: all 0.1s ease;
|
||||
flex: 1 1 fit-content(80rem);
|
||||
}
|
||||
|
||||
.input-field-container:hover {
|
||||
background: #EEF4FF;
|
||||
border: 0.2rem solid #8DB3FE;
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
.destination-edit-cell-routes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -41,13 +41,13 @@ export default {
|
|||
return routeElem;
|
||||
},
|
||||
isSea() {
|
||||
return this.route.type === "SEA";
|
||||
return this.route.variant === "SEA";
|
||||
},
|
||||
isRoad() {
|
||||
return this.route.type === "ROAD";
|
||||
return this.route.variant === "ROAD";
|
||||
},
|
||||
isRail() {
|
||||
return this.route.type === "RAIL";
|
||||
return this.route.variant === "RAIL";
|
||||
},
|
||||
containerClass() {
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import router from './router.js';
|
|||
import {createApp} from 'vue'
|
||||
import {createPinia} from 'pinia';
|
||||
import App from './App.vue'
|
||||
|
||||
import { setupErrorBuffer } from './store/error.js'
|
||||
|
||||
import {
|
||||
PhStar,
|
||||
|
|
@ -26,7 +26,7 @@ import {
|
|||
PhArchive,
|
||||
PhFloppyDisk,
|
||||
PhArrowCounterClockwise,
|
||||
PhCheck
|
||||
PhCheck, PhBug
|
||||
} from "@phosphor-icons/vue";
|
||||
|
||||
const app = createApp(App);
|
||||
|
|
@ -56,6 +56,7 @@ app.component('PhCloudArrowUp', PhCloudArrowUp);
|
|||
app.component('PhSealCheck', PhSealCheck);
|
||||
app.component('PhCalculator', PhCalculator);
|
||||
app.component('PhStar', PhStar);
|
||||
app.component('PhBug', PhBug)
|
||||
|
||||
|
||||
app.use(router);
|
||||
|
|
@ -63,6 +64,6 @@ app.use(pinia);
|
|||
|
||||
//app.component('base-button', () => import('./components/UI/BasicButton.vue'));
|
||||
//app.component('base-badge', () => import('./components/UI/BasicBadge.vue'));
|
||||
|
||||
setupErrorBuffer()
|
||||
|
||||
app.mount('#app');
|
||||
|
|
|
|||
|
|
@ -3,18 +3,20 @@
|
|||
<div class="header-container">
|
||||
<h2 class="page-header">Edit Calculation</h2>
|
||||
<div class="header-controls">
|
||||
<basic-button :show-icon="false" :disabled="premiseEditStore.selectedLoading" variant="secondary">Close
|
||||
<basic-button @click="showCustomToast" :show-icon="false" :disabled="premiseEditStore.selectedLoading" variant="secondary">Close
|
||||
</basic-button>
|
||||
<basic-button :show-icon="true" :disabled="premiseEditStore.selectedLoading || premiseEditStore.singleSelectEmpty"
|
||||
icon="Calculator" variant="primary">Calculate & close
|
||||
</basic-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</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 }}
|
||||
{{ premiseEditStore.error.message }} <span class="trace-link" @click="trace">View Trace</span>
|
||||
</notification-bar>
|
||||
|
||||
<div v-if="premiseEditStore.selectedLoading" class="edit-calculation-spinner-container">
|
||||
|
|
@ -44,9 +46,9 @@
|
|||
v-model:weight-unit="premise.handling_unit.weight_unit"
|
||||
v-model:dimension-unit="premise.handling_unit.dimension_unit"
|
||||
v-model:unit-count="premise.handling_unit.content_unit_count"
|
||||
|
||||
v-model:stackable="premise.is_stackable"
|
||||
v-model:mixable="premise.is_mixable"></packaging-edit>
|
||||
v-model:mixable="premise.is_mixable"
|
||||
@save="save"></packaging-edit>
|
||||
</box>
|
||||
<box class="master-data-item">
|
||||
<material-edit :part-number="premise.material.part_number"
|
||||
|
|
@ -83,12 +85,32 @@ import {mapStores} from "pinia";
|
|||
import {usePremiseEditStore} from "@/store/premiseEdit.js";
|
||||
import Spinner from "@/components/UI/Spinner.vue";
|
||||
import NotificationBar from "@/components/UI/NotificationBar.vue";
|
||||
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";
|
||||
|
||||
export default {
|
||||
name: "SingleEdit",
|
||||
components: {
|
||||
Toast,
|
||||
IconButton,
|
||||
TraceView,
|
||||
Modal,
|
||||
NotificationBar,
|
||||
Spinner, DestinationListView, PriceEdit, PackagingEdit, MaterialEdit, Box, SupplierView, BasicButton
|
||||
Spinner,
|
||||
DestinationListView,
|
||||
PriceEdit,
|
||||
PackagingEdit,
|
||||
MaterialEdit,
|
||||
Box,
|
||||
SupplierView,
|
||||
BasicButton
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
traceModal: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(usePremiseEditStore),
|
||||
|
|
@ -97,22 +119,43 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
save(type) {
|
||||
console.log(type);
|
||||
},
|
||||
showCustomToast() {
|
||||
this.$refs.toast.addToast({
|
||||
icon: 'floppy-disk',
|
||||
message: 'Changes saved.',
|
||||
title: 'Success',
|
||||
variant: 'primary',
|
||||
duration: 3000
|
||||
})
|
||||
},
|
||||
updateMaterial(id, action) {
|
||||
console.log(id, action);
|
||||
this.premiseEditStore.setMaterial(id, action === 'updateMasterData');
|
||||
},
|
||||
updateSupplier(data) {
|
||||
this.premiseEditStore.setSupplier(data.nodeId, data.updateMasterData);
|
||||
},
|
||||
trace() {
|
||||
this.traceModal = true;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.premiseEditStore.selectPremise(parseInt(this.$route.params.id));
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.trace-link {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.edit-calculation-spinner-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -40,11 +40,10 @@
|
|||
</transition>
|
||||
</div>
|
||||
|
||||
<transition name="list-edit">
|
||||
<div v-if="showListEdit" class="calculation-list-edit-container">
|
||||
<list-edit></list-edit>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<list-edit :show="showListEdit" @action="handleMultiselectAction"></list-edit>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
|
@ -88,6 +87,9 @@ export default {
|
|||
this.premiseStore.setQuery({});
|
||||
},
|
||||
methods: {
|
||||
handleMultiselectAction(action) {
|
||||
this.premiseStore.selected
|
||||
},
|
||||
updateCheckBoxes(checked) {
|
||||
this.overallCheck = checked;
|
||||
this.premiseStore.premises.forEach(p => {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ const router = createRouter({
|
|||
{
|
||||
path: '/edit/:id',
|
||||
component: CalculationSingleEdit,
|
||||
name: 'edit',
|
||||
},
|
||||
{
|
||||
path: '/bulk',
|
||||
|
|
|
|||
147
src/frontend/src/store/error.js
Normal file
147
src/frontend/src/store/error.js
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import {defineStore, getActivePinia} from 'pinia'
|
||||
import {config} from '@/config'
|
||||
import {toRaw} from "vue";
|
||||
|
||||
export const useErrorStore = defineStore('error', {
|
||||
state() {
|
||||
return {
|
||||
errors: [],
|
||||
sendCache: [],
|
||||
autoSubmitInterval: 30000,
|
||||
autoSubmitTimer: null
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
lastError: (state) => state.errors.length > 0 ? state.errors[state.errors.length - 1].error : null,
|
||||
},
|
||||
actions: {
|
||||
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',
|
||||
message: errorDto.message ?? 'Unknown message',
|
||||
trace: errorDto.trace ?? null,
|
||||
},
|
||||
request: request ? JSON.stringify(request) : null,
|
||||
state: state ? JSON.stringify(state) : null,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
this.errors.push(error);
|
||||
this.sendCache.push(error);
|
||||
await this.transmitErrors();
|
||||
},
|
||||
async transmitErrors() {
|
||||
|
||||
const params = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(toRaw(this.sendCache))
|
||||
};
|
||||
|
||||
const url = `${config.backendUrl}/error/`;
|
||||
|
||||
const response = await fetch(url, params
|
||||
).catch(e => {
|
||||
this.startAutoSubmitTimer();
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.stopAutoSubmitTimer()
|
||||
this.sendCache = [];
|
||||
} else {
|
||||
console.error("Error transmitting errors: " + url, params);
|
||||
console.error(response, await response.text());
|
||||
this.startAutoSubmitTimer();
|
||||
}
|
||||
},
|
||||
startAutoSubmitTimer() {
|
||||
if (this.autoSubmitTimer) return
|
||||
|
||||
this.autoSubmitTimer = setTimeout(() => {
|
||||
this.transmitErrors()
|
||||
}, this.autoSubmitInterval)
|
||||
},
|
||||
stopAutoSubmitTimer() {
|
||||
if (this.autoSubmitTimer) {
|
||||
clearTimeout(this.autoSubmitTimer)
|
||||
this.autoSubmitTimer = null
|
||||
}
|
||||
},
|
||||
captureStoreState(selectedStore = null, global = false) {
|
||||
const storeState = {}
|
||||
|
||||
try {
|
||||
|
||||
if (selectedStore && selectedStore.$id) {
|
||||
storeState[selectedStore.$id] = {
|
||||
...toRaw(selectedStore.$state)
|
||||
}
|
||||
}
|
||||
|
||||
if (global) {
|
||||
const pinia = this.$pinia || getActivePinia()
|
||||
if (pinia && pinia._s) {
|
||||
pinia._s.forEach((store, storeId) => {
|
||||
if (storeId !== 'error' && store.$state) {
|
||||
storeState[storeId] = {
|
||||
...toRaw(store.$state)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to capture store state:', err);
|
||||
return {};
|
||||
}
|
||||
|
||||
return storeState;
|
||||
},
|
||||
async submitOnBeforeUnload() {
|
||||
if (this.errors.length > 0) {
|
||||
navigator.sendBeacon('/api/errors', JSON.stringify(
|
||||
toRaw(this.sendCache)
|
||||
))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
});
|
||||
|
||||
// Global Error Handler Setup
|
||||
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 => {} );
|
||||
})
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
errorStore.submitOnBeforeUnload();
|
||||
})
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import {config} from '@/config'
|
||||
import {useErrorStore} from "@/store/error.js";
|
||||
|
||||
export const useMaterialStore = defineStore('material', {
|
||||
state() {
|
||||
|
|
@ -31,9 +32,18 @@ export const useMaterialStore = defineStore('material', {
|
|||
|
||||
|
||||
const url = `${config.backendUrl}/materials/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||
|
||||
const request = { url: url, params: {method: 'GET'}};
|
||||
|
||||
const response = await fetch(url).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;
|
||||
});
|
||||
|
||||
|
|
@ -44,13 +54,22 @@ export const useMaterialStore = defineStore('material', {
|
|||
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.loading = false;
|
||||
console.log(data);
|
||||
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(this.error, { store: this, request: request});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import {config} from '@/config'
|
||||
import {useErrorStore} from "@/store/error.js";
|
||||
|
||||
|
||||
export const useNodeStore = defineStore('node', {
|
||||
|
|
@ -35,10 +36,18 @@ export const useNodeStore = defineStore('node', {
|
|||
params.append('include_user_node', this.query.includeUserNode);
|
||||
|
||||
const url = `${config.backendUrl}/nodes/search/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||
|
||||
const request = { url: url, params: {method: 'GET'}};
|
||||
|
||||
console.log(url)
|
||||
const response = await fetch(url).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;
|
||||
});
|
||||
|
||||
|
|
@ -49,13 +58,22 @@ export const useNodeStore = defineStore('node', {
|
|||
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.loading = false;
|
||||
console.log(data);
|
||||
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(this.error, { store: this, request: request});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import {config} from '@/config'
|
||||
import {useErrorStore} from "@/store/error.js";
|
||||
|
||||
export const usePremiseStore = defineStore('premise', {
|
||||
state: () => ({
|
||||
|
|
@ -15,6 +16,7 @@ export const usePremiseStore = defineStore('premise', {
|
|||
showData: (state) => !state.loading && state.empty === false,
|
||||
showLoading: (state) => state.loading,
|
||||
showEmpty: (state) => !state.loading && state.empty === true,
|
||||
selectedIds: state => state.premises?.filter(p => p.checked).map(p => p.id) ?? [],
|
||||
},
|
||||
actions: {
|
||||
setQuery(query) {
|
||||
|
|
@ -44,21 +46,39 @@ export const usePremiseStore = defineStore('premise', {
|
|||
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;
|
||||
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(that.error, { store: that, 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;
|
||||
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(that.error, { store: that, request: request});
|
||||
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
that.error = { code: data.error.title, message: data.error.message, trace: data.error.details }
|
||||
that.loading = false;
|
||||
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(that.error, { store: that, request: request});
|
||||
|
||||
console.log(data);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import {config} from '@/config'
|
||||
import {toRaw} from "vue";
|
||||
import {useErrorStore} from "@/store/error.js";
|
||||
|
||||
export const usePremiseEditStore = defineStore('premiseEdit', {
|
||||
state() {
|
||||
|
|
@ -10,13 +12,76 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
selectedLoading: false,
|
||||
empty: false,
|
||||
error: null,
|
||||
selectedDestinations: [],
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
selectedPremise: (state) => state.premisses.find(p => p.id = state.singleSelectId),
|
||||
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),
|
||||
},
|
||||
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.
|
||||
*/
|
||||
selectDestinations(ids) {
|
||||
if (this.premisses === null) return;
|
||||
|
||||
|
||||
const selected = [];
|
||||
ids.forEach(id => {
|
||||
const destination = this.getDestinationById(id);
|
||||
|
||||
if ((destination ?? null) == null) {
|
||||
this.error = {
|
||||
code: 'Frontend error.',
|
||||
message: `Destination not found: ${id}. Please contact support.`,
|
||||
trace: null
|
||||
}
|
||||
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;
|
||||
},
|
||||
deselectDestinations(save = false) {
|
||||
if (this.premisses === null) return;
|
||||
|
||||
if (save) {
|
||||
|
||||
this.selectedDestinations.forEach(destination => {
|
||||
const orig = this.getDestinationById(destination.id);
|
||||
|
||||
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 => {
|
||||
const origRoute = orig.routes.find(r => r.id === route.id);
|
||||
origRoute.is_selected = route.is_selected;
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
this.selectedDestinations = [];
|
||||
},
|
||||
async deleteDestination(id) {
|
||||
if (this.premisses === null) return;
|
||||
|
||||
|
|
@ -45,6 +110,8 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
const body = {destination_node_id: id, premise_id: toBeUpdated};
|
||||
const url = `${config.backendUrl}/calculation/destination/`;
|
||||
|
||||
|
||||
|
||||
this.loading = true;
|
||||
this.selectedLoading = true;
|
||||
|
||||
|
|
@ -61,6 +128,8 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
this.loading = false;
|
||||
this.selectedLoading = false;
|
||||
|
||||
return destinations.map(d => d.id);
|
||||
|
||||
},
|
||||
async setSupplier(id, updateMasterData) {
|
||||
console.log("setSupplier");
|
||||
|
|
@ -104,7 +173,6 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
async selectPremise(id) {
|
||||
|
||||
this.selectedLoading = true;
|
||||
this.singleSelectId = null;
|
||||
|
||||
const toSelect = Array.isArray(id) ? id : [id];
|
||||
const reload = this.premisses ? !toSelect.every((id) => this.premisses.find(d => d.id === id)) : true;
|
||||
|
|
@ -118,6 +186,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
this.premisses.forEach(p => p.selected = toSelect.includes(p.id));
|
||||
this.singleSelectId = Array.isArray(id) ? null : id;
|
||||
this.selectedLoading = false;
|
||||
|
|
@ -125,14 +194,13 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
}
|
||||
,
|
||||
async reloadPremisses(ids) {
|
||||
const reload = this.premisses ? ids.every((id) => this.premisses.find(d => d.id === id)) : true;
|
||||
const reload = this.premisses ? !ids.every((id) => this.premisses.find(d => d.id === id)) : true;
|
||||
|
||||
if (reload) {
|
||||
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()}`;
|
||||
|
|
@ -146,6 +214,8 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
this.empty = data.length === 0;
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
async performRequest(method, url, body, expectResponse = true) {
|
||||
|
||||
|
|
@ -160,7 +230,8 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
params.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
console.log("Request:", url, params);
|
||||
const request = {url: url, params: params};
|
||||
console.log("Request:",request);
|
||||
|
||||
const response = await fetch(url, params
|
||||
).catch(e => {
|
||||
|
|
@ -180,18 +251,31 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
message: "Malformed server response. Please contact support.",
|
||||
trace: null
|
||||
}
|
||||
|
||||
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}
|
||||
console.error(data);
|
||||
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(this.error, {store: this, request: request});
|
||||
throw new Error('Internal backend error');
|
||||
}
|
||||
} else {
|
||||
if (!response.ok) {
|
||||
this.error = {code: "Return code error " + response.status, message: "Server returned wrong response code", trace: null}
|
||||
console.error(data);
|
||||
this.error = {
|
||||
code: "Return code error " + response.status,
|
||||
message: "Server returned wrong response code",
|
||||
trace: null
|
||||
}
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(this.error, { store: this, request: request});
|
||||
throw new Error('Internal backend error');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
src/main/java/de/avatic/lcc/controller/ErrorController.java
Normal file
21
src/main/java/de/avatic/lcc/controller/ErrorController.java
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package de.avatic.lcc.controller;
|
||||
|
||||
import de.avatic.lcc.dto.error.FrontendErrorDTO;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping({"/api/error", "/api/error/"})
|
||||
public class ErrorController {
|
||||
|
||||
@PostMapping
|
||||
public void error(@RequestBody List<FrontendErrorDTO> error) {
|
||||
System.out.println(error.toString());
|
||||
//TODO store in database.
|
||||
}
|
||||
|
||||
}
|
||||
56
src/main/java/de/avatic/lcc/dto/error/FrontendErrorDTO.java
Normal file
56
src/main/java/de/avatic/lcc/dto/error/FrontendErrorDTO.java
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
package de.avatic.lcc.dto.error;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class FrontendErrorDTO {
|
||||
|
||||
private ErrorDTO error;
|
||||
|
||||
private String request;
|
||||
|
||||
private String state;
|
||||
|
||||
private Date timestamp;
|
||||
|
||||
public ErrorDTO getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
public void setError(ErrorDTO error) {
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public String getRequest() {
|
||||
return request;
|
||||
}
|
||||
|
||||
public void setRequest(String request) {
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setState(String state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public Date getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(Date timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "FrontendErrorDto{" +
|
||||
"error=" + error +
|
||||
", request='" + request + '\'' +
|
||||
", state='" + state + '\'' +
|
||||
", timestamp=" + timestamp +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package de.avatic.lcc.dto.error;
|
||||
|
||||
public class FrontendErrorItemDTO {
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ import de.avatic.lcc.dto.calculation.edit.destination.DestinationCreateDTO;
|
|||
import de.avatic.lcc.dto.calculation.edit.destination.DestinationUpdateDTO;
|
||||
import de.avatic.lcc.model.nodes.Node;
|
||||
import de.avatic.lcc.model.premises.route.*;
|
||||
import de.avatic.lcc.model.properties.SystemPropertyMappingId;
|
||||
import de.avatic.lcc.repositories.NodeRepository;
|
||||
import de.avatic.lcc.repositories.premise.*;
|
||||
import de.avatic.lcc.repositories.properties.PropertyRepository;
|
||||
|
|
@ -17,10 +16,7 @@ import org.springframework.stereotype.Service;
|
|||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
|
|
@ -69,6 +65,10 @@ public class DestinationService {
|
|||
}
|
||||
}
|
||||
|
||||
if (premisesIdsToProcess.isEmpty())
|
||||
return new HashMap<>();
|
||||
|
||||
|
||||
var premisesToProcess = premiseRepository.getPremisesById(premisesIdsToProcess);
|
||||
Node destinationNode = nodeRepository.getById(dto.getDestinationNodeId()).orElseThrow();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue