FRONTEND/BACKEND: Refactor destination editing and input components; add startCalculation method to PremiseEditStore, implement Properties and BulkUpload components, introduce case-insensitive deserialization for DimensionUnit and WeightUnit, improve UI handling and modularity across input fields, and revise DTO and repository logic for improved state handling and validation.
This commit is contained in:
parent
f5c4e1159f
commit
45742d731d
41 changed files with 1775 additions and 177 deletions
|
|
@ -3,9 +3,10 @@
|
||||||
<button
|
<button
|
||||||
ref="trigger"
|
ref="trigger"
|
||||||
class="dropdown-trigger"
|
class="dropdown-trigger"
|
||||||
:class="{ 'dropdown-trigger--open': isOpen }"
|
:class="{ 'dropdown-trigger--open': isOpen}"
|
||||||
@click="toggleDropdown"
|
@click="toggleDropdown"
|
||||||
@keydown="handleTriggerKeydown"
|
@keydown="handleTriggerKeydown"
|
||||||
|
:disabled="disabled"
|
||||||
>
|
>
|
||||||
<span class="dropdown-trigger-text">
|
<span class="dropdown-trigger-text">
|
||||||
{{ selectedOption ? selectedOption[displayKey] : placeholder }}
|
{{ selectedOption ? selectedOption[displayKey] : placeholder }}
|
||||||
|
|
@ -94,7 +95,7 @@ export default {
|
||||||
return {
|
return {
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
focusedIndex: -1,
|
focusedIndex: -1,
|
||||||
labelId: `dropdown-${Math.random().toString(36).substr(2, 9)}`
|
labelId: `dropdown-${Math.random().toString(36).substring(2, 9)}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -327,14 +328,26 @@ export default {
|
||||||
|
|
||||||
/* Disabled state */
|
/* Disabled state */
|
||||||
.dropdown-trigger:disabled {
|
.dropdown-trigger:disabled {
|
||||||
background-color: #f7fafc;
|
background: white;
|
||||||
color: #a0aec0;
|
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
border-color: #e2e8f0;
|
border: 0.2rem solid rgba(227, 237, 255, 0.5);
|
||||||
|
color: rgba(0, 47, 84, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-trigger:disabled:hover {
|
.dropdown-trigger:disabled:hover {
|
||||||
border-color: #e2e8f0;
|
border: 0.2rem solid rgba(227, 237, 255, 0.5);
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Add this CSS rule to your existing styles */
|
||||||
|
.dropdown-trigger:disabled .dropdown-trigger-text {
|
||||||
|
color: rgba(0, 47, 84, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* You might also want to style the icon when disabled */
|
||||||
|
.dropdown-trigger:disabled .dropdown-trigger-icon {
|
||||||
|
color: rgba(113, 128, 150, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive adjustments */
|
/* Responsive adjustments */
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="input-field-container">
|
<div class="text-container">
|
||||||
<input class="input-field"
|
<input class="input-field"
|
||||||
v-model="modelValue"
|
v-model="modelValue"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
|
|
@ -41,7 +41,7 @@ export default {
|
||||||
color: #002F54;
|
color: #002F54;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field-container {
|
.text-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: white;
|
background: white;
|
||||||
|
|
@ -53,7 +53,7 @@ export default {
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field-container:hover {
|
.text-container:hover {
|
||||||
background: #EEF4FF;
|
background: #EEF4FF;
|
||||||
border: 0.2rem solid #8DB3FE;
|
border: 0.2rem solid #8DB3FE;
|
||||||
/*transform: translateY(2px);*/
|
/*transform: translateY(2px);*/
|
||||||
|
|
|
||||||
125
src/frontend/src/components/UI/ToogleSwitch.vue
Normal file
125
src/frontend/src/components/UI/ToogleSwitch.vue
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
<template>
|
||||||
|
<div class="toggle-switch-wrapper">
|
||||||
|
<label class="toggle-switch" :class="{ 'disabled': disabled }">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="modelValue"
|
||||||
|
:disabled="disabled"
|
||||||
|
@change="handleToggle"
|
||||||
|
class="toggle-input"
|
||||||
|
/>
|
||||||
|
<span class="toggle-slider" :class="{ 'active': modelValue }">
|
||||||
|
<span class="toggle-knob" :class="{ 'active': modelValue }"></span>
|
||||||
|
</span>
|
||||||
|
<span v-if="label" class="toggle-label">{{ label }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ToggleSwitch',
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue', 'change'],
|
||||||
|
methods: {
|
||||||
|
handleToggle(event) {
|
||||||
|
if (this.disabled) return;
|
||||||
|
|
||||||
|
const newValue = event.target.checked;
|
||||||
|
this.$emit('update:modelValue', newValue);
|
||||||
|
this.$emit('change', newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.toggle-switch-wrapper {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
gap: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch.disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider {
|
||||||
|
position: relative;
|
||||||
|
width: 4.8rem;
|
||||||
|
height: 2.4rem;
|
||||||
|
background-color: rgba(107, 134, 156, 0.1);
|
||||||
|
border-radius: 1.4rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: inset 0 0.2rem 0.4rem rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider.active {
|
||||||
|
background-color: #5AF0B4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-knob {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.4rem;
|
||||||
|
left: 0.4rem;
|
||||||
|
width: 1.6rem;
|
||||||
|
height: 1.6rem;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 0.2rem 0.4rem rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-knob.active {
|
||||||
|
transform: translateX(2.4rem);
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover-Effekte */
|
||||||
|
.toggle-switch:not(.disabled):hover .toggle-slider {
|
||||||
|
box-shadow: inset 0 0.2rem 0.4rem rgba(0, 0, 0, 0.15), 0 0 0 0.2rem rgba(0, 47, 84, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch:not(.disabled):hover .toggle-slider.active {
|
||||||
|
box-shadow: inset 0 0.2rem 0.4rem rgba(0, 0, 0, 0.15), 0 0 0 0.2rem rgba(0, 47, 84, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation beim Laden */
|
||||||
|
.toggle-slider,
|
||||||
|
.toggle-knob {
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -13,7 +13,8 @@
|
||||||
HS Code:
|
HS Code:
|
||||||
{{ premise.material.hs_code }}
|
{{ premise.material.hs_code }}
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="premise.tariff_rate && premise.tariff_rate > 0">
|
<div class="edit-calculation-cell-line edit-calculation-cell-subline"
|
||||||
|
v-if="premise.tariff_rate && premise.tariff_rate > 0">
|
||||||
Tariff rate:
|
Tariff rate:
|
||||||
{{ toPercent(premise.tariff_rate) }} %
|
{{ toPercent(premise.tariff_rate) }} %
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -21,7 +22,9 @@
|
||||||
<div class="edit-calculation-cell--price" :class="copyModeClass" v-if="showPrice"
|
<div class="edit-calculation-cell--price" :class="copyModeClass" v-if="showPrice"
|
||||||
@click="action('price')">
|
@click="action('price')">
|
||||||
<div class="edit-calculation-cell-line">{{ premise.material_cost }} EUR</div>
|
<div class="edit-calculation-cell-line">{{ premise.material_cost }} EUR</div>
|
||||||
<div class="edit-calculation-cell-line edit-calculation-cell-subline">Oversea share: {{ toPercent(premise.oversea_share) }} %</div>
|
<div class="edit-calculation-cell-line edit-calculation-cell-subline">Oversea share:
|
||||||
|
{{ toPercent(premise.oversea_share) }} %
|
||||||
|
</div>
|
||||||
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="premise.is_fca_enabled">
|
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="premise.is_fca_enabled">
|
||||||
<basic-badge icon="plus" variant="primary">FCA FEE</basic-badge>
|
<basic-badge icon="plus" variant="primary">FCA FEE</basic-badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -62,7 +65,7 @@
|
||||||
<!-- </div>-->
|
<!-- </div>-->
|
||||||
<div class="calculation-list-supplier-data">
|
<div class="calculation-list-supplier-data">
|
||||||
<div class="edit-calculation-cell-line">{{ premise.supplier.name }}</div>
|
<div class="edit-calculation-cell-line">{{ premise.supplier.name }}</div>
|
||||||
<!-- <div class="edit-calculation-cell-subline"> {{ premise.supplier.address }}</div>-->
|
<div class="edit-calculation-cell-subline"> {{ premise.supplier.address }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -72,9 +75,12 @@
|
||||||
<span class="number-circle"> {{ destinationsCount }} </span> Destinations
|
<span class="number-circle"> {{ destinationsCount }} </span> Destinations
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-calculation-cell-subline" v-for="name in destinationNames"> {{ name }}</div>
|
<div class="edit-calculation-cell-subline" v-for="name in destinationNames"> {{ name }}</div>
|
||||||
|
<div class="edit-calculation-cell-subline" v-if="showDestinationIncomplete">
|
||||||
|
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-calculation-empty" v-else-if="showMassEdit">
|
<div class="edit-calculation-empty" v-else-if="showMassEdit">
|
||||||
<spinner> </spinner>
|
<spinner></spinner>
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-calculation-empty" :class="copyModeClass" v-else
|
<div class="edit-calculation-empty" :class="copyModeClass" v-else
|
||||||
@click="action('destinations')">
|
@click="action('destinations')">
|
||||||
|
|
@ -140,7 +146,7 @@ export default {
|
||||||
return this.premise.destinations.map(d => d.destination_node.name).join(', ');
|
return this.premise.destinations.map(d => d.destination_node.name).join(', ');
|
||||||
},
|
},
|
||||||
destinationNames() {
|
destinationNames() {
|
||||||
const spliceCnt = ((this.premise.destinations.length === 4) ? 4 : 3);
|
const spliceCnt = ((this.premise.destinations.length === 4) ? 4 : 3) - this.showDestinationIncomplete;
|
||||||
const names = this.premise.destinations.map(d => d.destination_node.name).slice(0, spliceCnt);
|
const names = this.premise.destinations.map(d => d.destination_node.name).slice(0, spliceCnt);
|
||||||
if (this.premise.destinations.length > 4) {
|
if (this.premise.destinations.length > 4) {
|
||||||
names.push('and more ...');
|
names.push('and more ...');
|
||||||
|
|
@ -148,6 +154,9 @@ export default {
|
||||||
|
|
||||||
return names;
|
return names;
|
||||||
},
|
},
|
||||||
|
showDestinationIncomplete() {
|
||||||
|
return this.premise.destinations.some(p => ((p.annual_amount ?? null) === null) || p.annual_amount === 0 || p.routes?.every(r => !r.is_selected))
|
||||||
|
},
|
||||||
showDestinations() {
|
showDestinations() {
|
||||||
return (this.destinationsCount > 0);
|
return (this.destinationsCount > 0);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
15
src/frontend/src/components/layout/config/BulkUpload.vue
Normal file
15
src/frontend/src/components/layout/config/BulkUpload.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<template>
|
||||||
|
<h3>Bulk Operations</h3>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "BulkUpload"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
<h3>Country properties</h3>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import {mapStores} from "pinia";
|
||||||
|
import {usePropertiesStore} from "@/store/properties.js";
|
||||||
|
import {usePremiseEditStore} from "@/store/premiseEdit.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "CountryProperties",
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
151
src/frontend/src/components/layout/config/Properties.vue
Normal file
151
src/frontend/src/components/layout/config/Properties.vue
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
<template>
|
||||||
|
<div class="properties-container">
|
||||||
|
|
||||||
|
<div v-if="!loading" class="period-select-container"><span class="period-select-caption">Property set:</span>
|
||||||
|
<dropdown :options="periods"
|
||||||
|
emptyText="No property set available"
|
||||||
|
class="period-select"
|
||||||
|
placeholder="Select a property set"
|
||||||
|
v-model="selectedPeriod"
|
||||||
|
|
||||||
|
></dropdown>
|
||||||
|
<tooltip position="left" text="Invalidate the selected property set">
|
||||||
|
<icon-button icon="trash" @click="deletePeriod" :disabled="disableDeleteButton"></icon-button>
|
||||||
|
</tooltip>
|
||||||
|
<modal-dialog title="Do you really want to invalidate this property set?" dismiss-text="No" accept-text="Yes"
|
||||||
|
:state="modalDialogDeleteState"
|
||||||
|
message="If you invalidate this property set, this will also invalidate all calculations done with this property set. This cannot be undone!"
|
||||||
|
@click="deleteModalClick"
|
||||||
|
>
|
||||||
|
</modal-dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!loading" class="properties-list">
|
||||||
|
<property v-for="property in properties" :key="property.external_mapping_id" :property="property"
|
||||||
|
:disabled="!isValidPeriodActive" @save="saveProperty"></property>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import {mapStores} from "pinia";
|
||||||
|
import {usePropertiesStore} from "@/store/properties.js";
|
||||||
|
import Property from "@/components/layout/config/Property.vue";
|
||||||
|
import Dropdown from "@/components/UI/Dropdown.vue";
|
||||||
|
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||||
|
import IconButton from "@/components/UI/IconButton.vue";
|
||||||
|
import Tooltip from "@/components/UI/Tooltip.vue";
|
||||||
|
import modalDialog from "@/components/UI/ModalDialog.vue";
|
||||||
|
import ModalDialog from "@/components/UI/ModalDialog.vue";
|
||||||
|
import NotificationBar from "@/components/UI/NotificationBar.vue";
|
||||||
|
import {usePropertySetsStore} from "@/store/propertySets.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Properties",
|
||||||
|
components: {NotificationBar, ModalDialog, Tooltip, IconButton, BasicButton, Dropdown, Property},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
modalDialogDeleteState: false,
|
||||||
|
selectedPeriodId: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapStores(usePropertiesStore, usePropertySetsStore),
|
||||||
|
loading() {
|
||||||
|
return this.propertiesStore.isLoading;
|
||||||
|
},
|
||||||
|
isValidPeriodActive() {
|
||||||
|
const state = this.propertySetsStore.getPeriodState(this.selectedPeriod);
|
||||||
|
return state === "VALID" || state === "DRAFT";
|
||||||
|
},
|
||||||
|
disableDeleteButton() {
|
||||||
|
const state = this.propertySetsStore.getPeriodState(this.selectedPeriod);
|
||||||
|
return state === "VALID" || state === "INVALID" || state === "DRAFT";
|
||||||
|
},
|
||||||
|
selectedPeriod: {
|
||||||
|
get() {
|
||||||
|
return this.selectedPeriodId === null ? this.propertySetsStore.getCurrentPeriodId : this.selectedPeriodId;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
console.log(value)
|
||||||
|
this.selectedPeriodId = value;
|
||||||
|
this.propertiesStore.loadProperties(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
properties() {
|
||||||
|
return this.propertiesStore.getProperties;
|
||||||
|
},
|
||||||
|
periods() {
|
||||||
|
const periods = [];
|
||||||
|
|
||||||
|
const ps = this.propertySetsStore.getPeriods;
|
||||||
|
const current = this.propertySetsStore.getCurrentPeriodId;
|
||||||
|
|
||||||
|
if ((ps ?? null) === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const p of ps) {
|
||||||
|
const value = (p.state === "DRAFT" || p.state === "VALID") ? "CURRENT" : `${this.buildDate(p.start_date)} - ${this.buildDate(p.end_date)} ${p.state === "INVALID" ? "(INVALID)" : ""}`;
|
||||||
|
const period = {id: p.id, value: value};
|
||||||
|
|
||||||
|
console.log(p, p.state !== "VALID" , p.id === current, period)
|
||||||
|
|
||||||
|
if (p.state !== "VALID" || p.id === current)
|
||||||
|
periods.push(period);
|
||||||
|
}
|
||||||
|
|
||||||
|
return periods;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.propertiesStore.reload();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
saveProperty(property) {
|
||||||
|
this.propertiesStore.setProperty(property);
|
||||||
|
},
|
||||||
|
buildDate(date) {
|
||||||
|
return `${date[0]}-${date[1].toString().padStart(2, '0')}-${date[2].toString().padStart(2, '0')} ${date[3].toString().padStart(2, '0')}:${date[4].toString().padStart(2, '0')}:${date[5].toString().padStart(2, '0')}`
|
||||||
|
},
|
||||||
|
deletePeriod() {
|
||||||
|
if (!this.disableDeleteButton) {
|
||||||
|
this.modalDialogDeleteState = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteModalClick(action) {
|
||||||
|
this.modalDialogDeleteState = false;
|
||||||
|
|
||||||
|
if (action === 'accept')
|
||||||
|
this.propertySetsStore.invalidate(this.selectedPeriodId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
|
||||||
|
.period-select-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
gap: 1.6rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.period-select {
|
||||||
|
flex: 0 1 40rem
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.period-select-caption {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
317
src/frontend/src/components/layout/config/Property.vue
Normal file
317
src/frontend/src/components/layout/config/Property.vue
Normal file
|
|
@ -0,0 +1,317 @@
|
||||||
|
<template>
|
||||||
|
<div class="property-container">
|
||||||
|
|
||||||
|
<div class="caption-column">
|
||||||
|
<div class="caption-column-id">{{ property.name }}:</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-column">
|
||||||
|
|
||||||
|
<div v-if="showTextField" :class="textContainerClasses">
|
||||||
|
<input :disabled="disabled" @blur="validate('text', $event)" :class="inputFieldClasses" :value="currentValue"
|
||||||
|
ref="textInputField"
|
||||||
|
autocomplete="off"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showToggleSwitch" class="boolean-container">
|
||||||
|
<toggle-switch v-model="currentValue" @change="validate('toggle', $event)" :disabled="disabled"></toggle-switch>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showDropDown" class="dropdown-container">
|
||||||
|
<dropdown :disabled="disabled" :options="options" v-model:model-value="currentValue"
|
||||||
|
@change="validate('dropdown', $event)"
|
||||||
|
></dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="reset-button-container">
|
||||||
|
<tooltip text="reset changes" :position="'left'" v-if="showResetButton">
|
||||||
|
<icon-button icon="ArrowCounterClockwise" @click="resetProperty"></icon-button>
|
||||||
|
</tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import InputField from "@/components/UI/InputField.vue";
|
||||||
|
import {PhGear} from "@phosphor-icons/vue";
|
||||||
|
import ToggleSwitch from "@/components/UI/ToogleSwitch.vue";
|
||||||
|
import Dropdown from "@/components/UI/Dropdown.vue";
|
||||||
|
import IconButton from "@/components/UI/IconButton.vue";
|
||||||
|
import Tooltip from "@/components/UI/Tooltip.vue";
|
||||||
|
import {parseNumberFromString} from "@/common.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Property",
|
||||||
|
components: {Tooltip, IconButton, Dropdown, ToggleSwitch, PhGear, InputField},
|
||||||
|
props: {
|
||||||
|
property: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
showTextField() {
|
||||||
|
return !(this.showDropDown || this.showToggleSwitch);
|
||||||
|
},
|
||||||
|
showDropDown() {
|
||||||
|
return this.property.data_type === 'ENUMERATION';
|
||||||
|
},
|
||||||
|
showToggleSwitch() {
|
||||||
|
return this.property.data_type === 'BOOLEAN';
|
||||||
|
},
|
||||||
|
showResetButton() {
|
||||||
|
return this.property.current_value !== this.property.draft_value && this.property.draft_value !== null;
|
||||||
|
},
|
||||||
|
currentValue: {
|
||||||
|
get() {
|
||||||
|
if (this.property.data_type === 'INT')
|
||||||
|
return this.value.toFixed();
|
||||||
|
if (this.property.data_type === 'PERCENTAGE')
|
||||||
|
return `${(this.value * 100).toFixed(2)}%`;
|
||||||
|
if (this.property.data_type === 'CURRENCY')
|
||||||
|
return `${(this.value).toFixed(2)}€`;
|
||||||
|
|
||||||
|
return this.value;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
textContainerClasses() {
|
||||||
|
return this.disabled ? 'text-container--disabled' : 'text-container';
|
||||||
|
},
|
||||||
|
inputFieldClasses() {
|
||||||
|
return this.disabled ? 'input-field--disabled' : 'input-field';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
options: {},
|
||||||
|
value: null,
|
||||||
|
parsedText: null,
|
||||||
|
validationRule: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (this.property === null) return;
|
||||||
|
this.initialProcessing();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
validate(type, event) {
|
||||||
|
let emitEvent = false;
|
||||||
|
|
||||||
|
if (type === 'text') {
|
||||||
|
|
||||||
|
if (this.property.data_type === 'CURRENCY' || this.property.data_type === 'PERCENTAGE' || this.property.data_type === 'INT') {
|
||||||
|
const parsed = parseNumberFromString(event.target.value, (this.property.data_type === 'INT') ? 0 : 2) * ((this.property.data_type === 'PERCENTAGE') ? 0.01 : 1);
|
||||||
|
emitEvent = (parsed !== this.value);
|
||||||
|
this.value = parsed;
|
||||||
|
this.updateInputValue();
|
||||||
|
} else if (this.property.data_type === 'TEXT') {
|
||||||
|
emitEvent = (event.target.value !== this.value);
|
||||||
|
this.value = event.target.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (type === 'toggle') {
|
||||||
|
emitEvent = (event !== this.value);
|
||||||
|
this.value = event;
|
||||||
|
} else if (type === 'dropdown') {
|
||||||
|
emitEvent = (event.value !== this.value);
|
||||||
|
this.value = event.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emitEvent)
|
||||||
|
this.$emit('save', {id: this.property.external_mapping_id, value: this.value, reset: false});
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
updateInputValue() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs["textInputField"] && this.$refs["textInputField"].value !== this.currentValue) {
|
||||||
|
this.$refs["textInputField"].value = this.currentValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getEnum(rule) {
|
||||||
|
const options = [];
|
||||||
|
const enumValues = rule['ENUM'];
|
||||||
|
|
||||||
|
for (const value of enumValues) {
|
||||||
|
const option = {
|
||||||
|
id: value,
|
||||||
|
value: value,
|
||||||
|
}
|
||||||
|
|
||||||
|
options.push(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
},
|
||||||
|
resetProperty() {
|
||||||
|
this.property.draft_value = null;
|
||||||
|
this.initialProcessing();
|
||||||
|
|
||||||
|
this.$emit('save', {id: this.property.external_mapping_id, value: this.value, reset: true});
|
||||||
|
|
||||||
|
},
|
||||||
|
initialProcessing() {
|
||||||
|
this.validationRule = JSON.parse(this.property.validation_rule);
|
||||||
|
this.value = this.property.draft_value === null ? this.property.current_value : this.property.draft_value;
|
||||||
|
|
||||||
|
if (this.property.data_type === 'ENUMERATION') {
|
||||||
|
this.options = this.getEnum(this.validationRule);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.property.data_type === 'BOOLEAN') {
|
||||||
|
this.value = this.value === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.property.data_type === 'INT') {
|
||||||
|
this.value = parseNumberFromString(this.value, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.property.data_type === 'PERCENTAGE') {
|
||||||
|
this.value = parseNumberFromString(this.value, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.property.data_type === 'CURRENCY') {
|
||||||
|
this.value = parseNumberFromString(this.value, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.property-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 3fr 1fr 0.5fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2.4rem;
|
||||||
|
|
||||||
|
height: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption-column-id {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption-column-name {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-column {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 300;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-container--disabled {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: white;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
border: 0.2rem solid rgba(227, 237, 255, 0.5);
|
||||||
|
color: rgba(0, 47, 84, 0.3);
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 30rem;
|
||||||
|
min-width: 20rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: white;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
border: 0.2rem solid #E3EDFF;
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 30rem;
|
||||||
|
min-width: 20rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-container:hover {
|
||||||
|
background: #EEF4FF;
|
||||||
|
border: 0.2rem solid #8DB3FE;
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
.boolean-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 30rem;
|
||||||
|
min-width: 20rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 30rem;
|
||||||
|
min-width: 20rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: none;
|
||||||
|
resize: none;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #002F54;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 5rem;
|
||||||
|
max-width: 100rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.input-field--disabled {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: none;
|
||||||
|
resize: none;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: rgba(0, 47, 84, 0.5);
|
||||||
|
width: 100%;
|
||||||
|
min-width: 5rem;
|
||||||
|
max-width: 100rem;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.reset-button-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
110
src/frontend/src/components/layout/config/StagedChanges.vue
Normal file
110
src/frontend/src/components/layout/config/StagedChanges.vue
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<transition name="fade">
|
||||||
|
<div v-if="stagedChanges" class="staged-changes-container">
|
||||||
|
<div class="staged-changes-info">
|
||||||
|
<ph-warning size="18px"></ph-warning>
|
||||||
|
There are changes to system properties or country properties. Press save icon to apply them.
|
||||||
|
</div>
|
||||||
|
<div class="staged-changes-save">
|
||||||
|
<icon-button icon="floppy-disk" @click="applyChanges" variant="blue"></icon-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
<modal-dialog title="Do you really want to apply the current changes?" dismiss-text="No" accept-text="Yes"
|
||||||
|
:state="modalDialogStagedChangesState"
|
||||||
|
message='As soon as you change the system properties and country properties, new calculations are no longer comparable with previous calculations.
|
||||||
|
Therefore, a new "Property set" is created. Only calculations that were created with the same "Property set" can be compared within reporting. Would you like to continue?'
|
||||||
|
@click="applyChangesModalClick"
|
||||||
|
>
|
||||||
|
</modal-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import IconButton from "@/components/UI/IconButton.vue";
|
||||||
|
import ModalDialog from "@/components/UI/ModalDialog.vue";
|
||||||
|
import {mapStores} from "pinia";
|
||||||
|
import {useStageStore} from "@/store/stage.js";
|
||||||
|
import {usePropertiesStore} from "@/store/properties.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "StagedChanges",
|
||||||
|
components: {ModalDialog, IconButton},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
modalDialogStagedChangesState: false,
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
... mapStores(useStageStore, usePropertiesStore),
|
||||||
|
stagedChanges() {
|
||||||
|
return this.stageStore.hasStagedChanges;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
applyChanges() {
|
||||||
|
this.modalDialogStagedChangesState = true;
|
||||||
|
|
||||||
|
},
|
||||||
|
async applyChangesModalClick(action) {
|
||||||
|
this.modalDialogStagedChangesState = false;
|
||||||
|
if (action === 'accept') {
|
||||||
|
await this.stageStore.applyChanges();
|
||||||
|
await this.propertiesStore.reload();
|
||||||
|
await this.countryPropertiesStore.reload();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.stageStore.checkStagedChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.staged-changes-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
gap: 1.6rem;
|
||||||
|
background-color: #5AF0B4;
|
||||||
|
color: #002F54;
|
||||||
|
border-radius: 0.8rem;
|
||||||
|
padding: 1.6rem;
|
||||||
|
margin-bottom: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staged-changes-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staged-changes-save {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fade transition styles */
|
||||||
|
.fade-enter-active {
|
||||||
|
transition: opacity 0.3s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -27,14 +27,14 @@
|
||||||
|
|
||||||
<div class="input-column">
|
<div class="input-column">
|
||||||
<div class="hs-code-container">
|
<div class="hs-code-container">
|
||||||
<autosuggest-searchbar :fetch-suggestions="fetchHsCode" :initial-value="hsCode"
|
<autosuggest-searchbar :activate-watcher="true" :fetch-suggestions="fetchHsCode" :initial-value="hsCode"
|
||||||
placeholder="Find hs code" no-results-text="Not found."></autosuggest-searchbar>
|
placeholder="Find hs code" no-results-text="Not found."></autosuggest-searchbar>
|
||||||
<icon-button icon="ArrowCounterClockwise"></icon-button>
|
<icon-button icon="ArrowCounterClockwise"></icon-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="caption-column">Tariff rate [%]</div>
|
<div class="caption-column">Tariff rate [%]</div>
|
||||||
<div class="input-field-container input-field-tariffrate">
|
<div class="text-container input-field-tariffrate">
|
||||||
<input ref="tariffRateInput" :value="tariffRatePercent" @blur="validateTariffRate"
|
<input ref="tariffRateInput" :value="tariffRatePercent" @blur="validateTariffRate"
|
||||||
class="input-field"
|
class="input-field"
|
||||||
autocomplete="off"/>
|
autocomplete="off"/>
|
||||||
|
|
@ -67,7 +67,7 @@ export default {
|
||||||
SelectMaterial,
|
SelectMaterial,
|
||||||
Modal, PhArrowCounterClockwise, ModalDialog, AutosuggestSearchbar, InputField, Flag, IconButton
|
Modal, PhArrowCounterClockwise, ModalDialog, AutosuggestSearchbar, InputField, Flag, IconButton
|
||||||
},
|
},
|
||||||
emits: ["update:tariffRate", "updateMaterial", "update:partNumber", "update:hsCode", "save"],
|
emits: ["update:tariffRate", "updateMaterial", "update:partNumber", "update:hsCode", "save", "close"],
|
||||||
props: {
|
props: {
|
||||||
description: {
|
description: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
@ -85,6 +85,10 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
openSelectDirect: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(useMaterialStore, useCustomsStore),
|
...mapStores(useMaterialStore, useCustomsStore),
|
||||||
|
|
@ -97,6 +101,9 @@ export default {
|
||||||
modalSelectMaterial: false,
|
modalSelectMaterial: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
this.modalSelectMaterial = this.openSelectDirect;
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
focusLost(event) {
|
focusLost(event) {
|
||||||
if (!this.$el.contains(event.relatedTarget)) {
|
if (!this.$el.contains(event.relatedTarget)) {
|
||||||
|
|
@ -105,12 +112,18 @@ export default {
|
||||||
},
|
},
|
||||||
closeEditModal() {
|
closeEditModal() {
|
||||||
this.modalSelectMaterial = false;
|
this.modalSelectMaterial = false;
|
||||||
|
if (this.openSelectDirect) {
|
||||||
|
this.$emit('close');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
modalEditClick(data) {
|
modalEditClick(data) {
|
||||||
this.closeEditModal();
|
this.modalSelectMaterial = false;
|
||||||
|
|
||||||
if (data.action === 'accept') {
|
if (data.action === 'accept') {
|
||||||
this.selectedMaterial = data.material;
|
this.selectedMaterial = data.material;
|
||||||
this.$emit('updateMaterial', data.material.id, data.updateMasterData ? 'updateMasterData' : 'keepMasterData');
|
this.$emit('updateMaterial', data.material.id, data.updateMasterData ? 'updateMasterData' : 'keepMasterData');
|
||||||
|
} else if (this.openSelectDirect) {
|
||||||
|
this.$emit('close');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateInputValue(inputRef, formattedValue) {
|
updateInputValue(inputRef, formattedValue) {
|
||||||
|
|
@ -182,7 +195,7 @@ export default {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field-container {
|
.text-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: white;
|
background: white;
|
||||||
|
|
@ -194,7 +207,7 @@ export default {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field-container:hover {
|
.text-container:hover {
|
||||||
background: #EEF4FF;
|
background: #EEF4FF;
|
||||||
border: 0.2rem solid #8DB3FE;
|
border: 0.2rem solid #8DB3FE;
|
||||||
/*transform: translateY(2px);*/
|
/*transform: translateY(2px);*/
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="container" @focusout="focusLost">
|
<div class="container" @focusout="focusLost">
|
||||||
<div class="caption-column">Length</div>
|
<div class="caption-column">Length</div>
|
||||||
<div class="input-column">
|
<div class="input-column">
|
||||||
<div class="input-field-container">
|
<div class="text-container">
|
||||||
<input ref="lengthInput" :value="huLength" @blur="validateDimension('length', $event)" class="input-field"
|
<input ref="lengthInput" :value="huLength" @blur="validateDimension('length', $event)" class="input-field"
|
||||||
autocomplete="off"/>
|
autocomplete="off"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
<div class="caption-column">Width</div>
|
<div class="caption-column">Width</div>
|
||||||
<div class="input-column">
|
<div class="input-column">
|
||||||
<div class="input-field-container">
|
<div class="text-container">
|
||||||
<input ref="widthInput" :value="huWidth" @blur="validateDimension('width', $event)" class="input-field"
|
<input ref="widthInput" :value="huWidth" @blur="validateDimension('width', $event)" class="input-field"
|
||||||
autocomplete="off"/>
|
autocomplete="off"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
<div class="caption-column">Height</div>
|
<div class="caption-column">Height</div>
|
||||||
<div class="input-column">
|
<div class="input-column">
|
||||||
<div class="input-field-container">
|
<div class="text-container">
|
||||||
<input ref="heightInput" :value="huHeight" @blur="validateDimension('height', $event)" class="input-field"
|
<input ref="heightInput" :value="huHeight" @blur="validateDimension('height', $event)" class="input-field"
|
||||||
autocomplete="off"/>
|
autocomplete="off"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
|
|
||||||
<div class="caption-column">Weight</div>
|
<div class="caption-column">Weight</div>
|
||||||
<div class="input-column">
|
<div class="input-column">
|
||||||
<div class="input-field-container">
|
<div class="text-container">
|
||||||
<input ref="weightInput" :value="huWeight" @blur="validateWeight('weight', $event)" class="input-field"
|
<input ref="weightInput" :value="huWeight" @blur="validateWeight('weight', $event)" class="input-field"
|
||||||
autocomplete="off"/>
|
autocomplete="off"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
|
|
||||||
<div class="caption-column">Pieces per HU</div>
|
<div class="caption-column">Pieces per HU</div>
|
||||||
<div class="input-column">
|
<div class="input-column">
|
||||||
<div class="input-field-container">
|
<div class="text-container">
|
||||||
<input ref="unitCountInput" :value="huUnitCount" @blur="validateCount" class="input-field"
|
<input ref="unitCountInput" :value="huUnitCount" @blur="validateCount" class="input-field"
|
||||||
autocomplete="off"/>
|
autocomplete="off"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -292,7 +292,7 @@ export default {
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field-container {
|
.text-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: white;
|
background: white;
|
||||||
|
|
@ -305,7 +305,7 @@ export default {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field-container:hover {
|
.text-container:hover {
|
||||||
background: #EEF4FF;
|
background: #EEF4FF;
|
||||||
border: 0.2rem solid #8DB3FE;
|
border: 0.2rem solid #8DB3FE;
|
||||||
/*transform: translateY(2px);*/
|
/*transform: translateY(2px);*/
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,14 @@
|
||||||
<div class="container" @focusout="focusLost">
|
<div class="container" @focusout="focusLost">
|
||||||
<div class="caption-column">MEK_A [EUR]</div>
|
<div class="caption-column">MEK_A [EUR]</div>
|
||||||
<div class="input-column">
|
<div class="input-column">
|
||||||
<div class="input-field-container">
|
<div class="text-container">
|
||||||
<input :value="priceFormatted" @blur="validatePrice" class="input-field"
|
<input :value="priceFormatted" @blur="validatePrice" class="input-field"
|
||||||
autocomplete="off"/>
|
autocomplete="off"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="caption-column">Oversea share [%]</div>
|
<div class="caption-column">Oversea share [%]</div>
|
||||||
<div class="input-column">
|
<div class="input-column">
|
||||||
<div class="input-field-container">
|
<div class="text-container">
|
||||||
<input :value="overSeaSharePercent" @blur="validateOverSeaShare" class="input-field"
|
<input :value="overSeaSharePercent" @blur="validateOverSeaShare" class="input-field"
|
||||||
autocomplete="off"/>
|
autocomplete="off"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -127,7 +127,7 @@ export default {
|
||||||
min-width: 5rem;
|
min-width: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field-container {
|
.text-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: white;
|
background: white;
|
||||||
|
|
@ -139,7 +139,7 @@ export default {
|
||||||
flex: 1 1 fit-content(80rem);
|
flex: 1 1 fit-content(80rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field-container:hover {
|
.text-container:hover {
|
||||||
background: #EEF4FF;
|
background: #EEF4FF;
|
||||||
border: 0.2rem solid #8DB3FE;
|
border: 0.2rem solid #8DB3FE;
|
||||||
/*transform: translateY(2px);*/
|
/*transform: translateY(2px);*/
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<modal :state="selectSupplierModalState" @close="closeEditModal">
|
<modal :state="selectSupplierModalState" @close="closeEditModal">
|
||||||
<select-node @close="modalDialogClose"></select-node>
|
<select-node @update-supplier="modalDialogClose"></select-node>
|
||||||
</modal>
|
</modal>
|
||||||
<icon-button icon="plus" @click="openModal"></icon-button>
|
<icon-button icon="plus" @click="openModal"></icon-button>
|
||||||
<icon-button icon="pencil-simple" @click="openModal"></icon-button>
|
<icon-button icon="pencil-simple" @click="openModal"></icon-button>
|
||||||
|
|
@ -65,6 +65,10 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
|
},
|
||||||
|
openSelectDirect: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -79,6 +83,9 @@ export default {
|
||||||
selectSupplierModalState: false
|
selectSupplierModalState: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
this.selectSupplierModalState = this.openSelectDirect;
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
closeEditModal() {
|
closeEditModal() {
|
||||||
this.selectSupplierModalState = false;
|
this.selectSupplierModalState = false;
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,8 @@ export default {
|
||||||
min-height: 0; /* Critical: allows flex child to shrink below content size */
|
min-height: 0; /* Critical: allows flex child to shrink below content size */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.destination-edit-modal-container {
|
.destination-edit-modal-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -13,21 +13,21 @@
|
||||||
<div class="destination-edit-handling-cost-container" v-show="inputFieldsActive">
|
<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-caption">Repackaging cost [EUR]</div>
|
||||||
<div class="destination-edit-column-data">
|
<div class="destination-edit-column-data">
|
||||||
<div class="input-field-container">
|
<div class="text-container">
|
||||||
<input :value="repackaging" @blur="validate('repackaging', $event)" class="input-field"
|
<input :value="repackaging" @blur="validate('repackaging', $event)" class="input-field"
|
||||||
autocomplete="off"/>
|
autocomplete="off"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="destination-edit-column-caption">Handling cost [EUR]</div>
|
<div class="destination-edit-column-caption">Handling cost [EUR]</div>
|
||||||
<div class="destination-edit-column-data">
|
<div class="destination-edit-column-data">
|
||||||
<div class="input-field-container">
|
<div class="text-container">
|
||||||
<input :value="handling" @blur="validate('handling', $event)" class="input-field"
|
<input :value="handling" @blur="validate('handling', $event)" class="input-field"
|
||||||
autocomplete="off"/>
|
autocomplete="off"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="destination-edit-column-caption">Disposal cost [EUR]</div>
|
<div class="destination-edit-column-caption">Disposal cost [EUR]</div>
|
||||||
<div class="destination-edit-column-data">
|
<div class="destination-edit-column-data">
|
||||||
<div class="input-field-container">
|
<div class="text-container">
|
||||||
<input :value="disposal" @blur="validate('disposal', $event)" class="input-field"
|
<input :value="disposal" @blur="validate('disposal', $event)" class="input-field"
|
||||||
autocomplete="off"/>
|
autocomplete="off"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -161,7 +161,7 @@ export default {
|
||||||
min-width: 5rem;
|
min-width: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field-container {
|
.text-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: white;
|
background: white;
|
||||||
|
|
@ -172,7 +172,7 @@ export default {
|
||||||
flex: 1 1 fit-content(80rem);
|
flex: 1 1 fit-content(80rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field-container:hover {
|
.text-container:hover {
|
||||||
background: #EEF4FF;
|
background: #EEF4FF;
|
||||||
border: 0.2rem solid #8DB3FE;
|
border: 0.2rem solid #8DB3FE;
|
||||||
transform: scale(1.01);
|
transform: scale(1.01);
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
<tooltip :text="tooltipAnnualAmount" position="right">Annual quantity</tooltip>
|
<tooltip :text="tooltipAnnualAmount" position="right">Annual quantity</tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="destination-edit-column-data">
|
<div class="destination-edit-column-data">
|
||||||
<div class="input-field-container">
|
<div class="text-container">
|
||||||
<input :value="annualAmount" @blur="validateAnnualAmount" class="input-field"
|
<input :value="annualAmount" @blur="validateAnnualAmount" class="input-field"
|
||||||
autocomplete="off"/>
|
autocomplete="off"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
Unable to route from supplier to {{ this.destination.destination_node.name }}
|
Unable to route from supplier to {{ this.destination.destination_node.name }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else key="rate" class="input-field-container">
|
<div v-else key="rate" class="text-container">
|
||||||
<input :value="rateD2d" @blur="validateRateD2d" class="input-field"
|
<input :value="rateD2d" @blur="validateRateD2d" class="input-field"
|
||||||
autocomplete="off"/>
|
autocomplete="off"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -173,7 +173,6 @@ export default {
|
||||||
border-radius: 0.8rem;
|
border-radius: 0.8rem;
|
||||||
padding: 1.6rem;
|
padding: 1.6rem;
|
||||||
margin-bottom: 1.6rem;
|
margin-bottom: 1.6rem;
|
||||||
margin-bottom: 1.6rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.destination-edit-cell-routing {
|
.destination-edit-cell-routing {
|
||||||
|
|
@ -235,7 +234,7 @@ export default {
|
||||||
min-width: 5rem;
|
min-width: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field-container {
|
.text-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: white;
|
background: white;
|
||||||
|
|
@ -246,7 +245,7 @@ export default {
|
||||||
flex: 1 1 fit-content(80rem);
|
flex: 1 1 fit-content(80rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field-container:hover {
|
.text-container:hover {
|
||||||
background: #EEF4FF;
|
background: #EEF4FF;
|
||||||
border: 0.2rem solid #8DB3FE;
|
border: 0.2rem solid #8DB3FE;
|
||||||
transform: scale(1.01);
|
transform: scale(1.01);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="select-material-modal-container">
|
<div class="select-material-modal-container">
|
||||||
<h3 class="sub-header">Select material</h3>
|
<h3 class="sub-header">Select part number</h3>
|
||||||
<div class="select-material-container">
|
<div class="select-material-container">
|
||||||
<div class="select-material-caption-column">Part number</div>
|
<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-column select-material-input-field-suppliername">
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@
|
||||||
<form @submit.prevent="send">
|
<form @submit.prevent="send">
|
||||||
<div class="create-new-node-form-container">
|
<div class="create-new-node-form-container">
|
||||||
<div class="input-field-caption">Name:</div>
|
<div class="input-field-caption">Name:</div>
|
||||||
<div><div class="input-field-container"><input class="input-field" v-model="nodeName"/></div></div>
|
<div><div class="text-container"><input class="input-field" v-model="nodeName"/></div></div>
|
||||||
<div></div>
|
<div></div>
|
||||||
<div class="input-field-caption">Address:</div>
|
<div class="input-field-caption">Address:</div>
|
||||||
<div><div class="input-field-container"><input class="input-field" v-model="nodeAddress"/></div></div>
|
<div><div class="text-container"><input class="input-field" v-model="nodeAddress"/></div></div>
|
||||||
<div>
|
<div>
|
||||||
<basic-button icon="SealCheck">Verify address</basic-button>
|
<basic-button icon="SealCheck">Verify address</basic-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -89,7 +89,7 @@ export default {
|
||||||
color: #002F54;
|
color: #002F54;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field-container {
|
.text-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: white;
|
background: white;
|
||||||
|
|
@ -101,7 +101,7 @@ export default {
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field-container:hover {
|
.text-container:hover {
|
||||||
background: #EEF4FF;
|
background: #EEF4FF;
|
||||||
border: 0.2rem solid #8DB3FE;
|
border: 0.2rem solid #8DB3FE;
|
||||||
/*transform: translateY(2px);*/
|
/*transform: translateY(2px);*/
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@
|
||||||
variant="flags"
|
variant="flags"
|
||||||
@selected="selected"
|
@selected="selected"
|
||||||
ref="searchbar"
|
ref="searchbar"
|
||||||
|
:initial-value="initialValue"
|
||||||
|
:activate-watcher="true"
|
||||||
>
|
>
|
||||||
</autosuggest-searchbar>
|
</autosuggest-searchbar>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -51,10 +53,27 @@ import Checkbox from "@/components/UI/Checkbox.vue";
|
||||||
export default {
|
export default {
|
||||||
name: "SelectNode",
|
name: "SelectNode",
|
||||||
components: {Checkbox, AutosuggestSearchbar, Flag, InputField, BasicButton},
|
components: {Checkbox, AutosuggestSearchbar, Flag, InputField, BasicButton},
|
||||||
emits: ['close'],
|
emits: ['updateSupplier'],
|
||||||
|
props: {
|
||||||
|
openSelectDirect: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
preSelectedNode: {
|
||||||
|
type: Object,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
console.log("SelectNode created with openSelectDirect: " + this.openSelectDirect, this.preSelectedNode);
|
||||||
|
if(this.openSelectDirect) {
|
||||||
|
this.node = this.preSelectedNode;
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
action(action) {
|
action(action) {
|
||||||
this.$emit('close', {action: action, nodeId: this.node?.id, updateMasterData: this.updateMasterData});
|
this.$emit('updateSupplier', {action: action, nodeId: this.node?.id, updateMasterData: this.updateMasterData});
|
||||||
},
|
},
|
||||||
checkboxChanged(value) {
|
checkboxChanged(value) {
|
||||||
this.updateMasterData = value;
|
this.updateMasterData = value;
|
||||||
|
|
@ -106,6 +125,13 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(useNodeStore),
|
...mapStores(useNodeStore),
|
||||||
|
initialValue() {
|
||||||
|
if(this.node) {
|
||||||
|
return this.node.name;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
supplierAddress() {
|
supplierAddress() {
|
||||||
return this.node?.address ?? '';
|
return this.node?.address ?? '';
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
<!--TODO: isMassEdit-->
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="edit-calculation-container">
|
<div class="edit-calculation-container">
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<h2 class="page-header">Mass edit calculation</h2>
|
<h2 class="page-header">Mass edit calculation</h2>
|
||||||
<div class="header-controls">
|
<div class="header-controls">
|
||||||
<basic-button :show-icon="false" :disabled="premiseEditStore.selectedLoading"
|
<basic-button :show-icon="false"
|
||||||
variant="secondary">Close
|
:disabled="premiseEditStore.selectedLoading"
|
||||||
|
variant="secondary"
|
||||||
|
@click="closeMassEdit"
|
||||||
|
>Close
|
||||||
</basic-button>
|
</basic-button>
|
||||||
<basic-button :show-icon="true"
|
<basic-button :show-icon="true"
|
||||||
:disabled="premiseEditStore.selectedLoading"
|
:disabled="premiseEditStore.selectedLoading"
|
||||||
icon="Calculator" variant="primary">Calculate & close
|
icon="Calculator" variant="primary"
|
||||||
|
@click="startCalculation"
|
||||||
|
>Calculate & close
|
||||||
</basic-button>
|
</basic-button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -80,15 +83,15 @@
|
||||||
v-model:unitCount="componentProps.unitCount"
|
v-model:unitCount="componentProps.unitCount"
|
||||||
v-model:mixable="componentProps.mixable"
|
v-model:mixable="componentProps.mixable"
|
||||||
v-model:stackable="componentProps.stackable"
|
v-model:stackable="componentProps.stackable"
|
||||||
v-model:supplierName="componentProps.supplierName"
|
v-model:preSelectedNode="componentProps.preSelectedNode"
|
||||||
v-model:supplierAddress="componentProps.supplierAddress"
|
v-model:openSelectDirect="componentProps.openSelectDirect"
|
||||||
v-model:supplierCoordinates="componentProps.supplierCoordinates"
|
|
||||||
v-model:isoCode="componentProps.isoCode"
|
|
||||||
@update-material="updateMaterial"
|
@update-material="updateMaterial"
|
||||||
|
@update-supplier="updateSupplier"
|
||||||
|
@close="closeEditModalAction('cancel')"
|
||||||
|
|
||||||
>
|
>
|
||||||
</component>
|
</component>
|
||||||
<div class="modal-content-footer">
|
<div class="modal-content-footer" v-if="showModalFooter">
|
||||||
<basic-button :show-icon="false" @click="closeEditModalAction('accept')">OK</basic-button>
|
<basic-button :show-icon="false" @click="closeEditModalAction('accept')">OK</basic-button>
|
||||||
<basic-button variant="secondary" :show-icon="false" @click="closeEditModalAction('cancel')">Cancel
|
<basic-button variant="secondary" :show-icon="false" @click="closeEditModalAction('cancel')">Cancel
|
||||||
</basic-button>
|
</basic-button>
|
||||||
|
|
@ -117,13 +120,14 @@ import MaterialEdit from "@/components/layout/edit/MaterialEdit.vue";
|
||||||
import PackagingEdit from "@/components/layout/edit/PackagingEdit.vue";
|
import PackagingEdit from "@/components/layout/edit/PackagingEdit.vue";
|
||||||
import SupplierView from "@/components/layout/edit/SupplierView.vue";
|
import SupplierView from "@/components/layout/edit/SupplierView.vue";
|
||||||
import DestinationListView from "@/components/layout/edit/DestinationListView.vue";
|
import DestinationListView from "@/components/layout/edit/DestinationListView.vue";
|
||||||
|
import SelectNode from "@/components/layout/node/SelectNode.vue";
|
||||||
|
|
||||||
|
|
||||||
const COMPONENT_TYPES = {
|
const COMPONENT_TYPES = {
|
||||||
price: PriceEdit,
|
price: PriceEdit,
|
||||||
material: MaterialEdit,
|
material: MaterialEdit,
|
||||||
packaging: PackagingEdit,
|
packaging: PackagingEdit,
|
||||||
supplier: SupplierView,
|
supplier: SelectNode,
|
||||||
destinations: DestinationListView,
|
destinations: DestinationListView,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,6 +157,9 @@ export default {
|
||||||
showMultiselectAction() {
|
showMultiselectAction() {
|
||||||
return this.selectCount > 0;
|
return this.selectCount > 0;
|
||||||
},
|
},
|
||||||
|
showModalFooter() {
|
||||||
|
return this.modalType !== 'supplier';
|
||||||
|
},
|
||||||
showEditModal() {
|
showEditModal() {
|
||||||
return ((this.modalType ?? null) !== null);
|
return ((this.modalType ?? null) !== null);
|
||||||
},
|
},
|
||||||
|
|
@ -181,7 +188,7 @@ export default {
|
||||||
modalType: null,
|
modalType: null,
|
||||||
componentsData: {
|
componentsData: {
|
||||||
price: {props: {price: 0, overSeaShare: 0, includeFcaFee: true}},
|
price: {props: {price: 0, overSeaShare: 0, includeFcaFee: true}},
|
||||||
material: {props: {partNumber: "", hsCode: "", tariffRate: 0.00, description: ""}},
|
material: {props: {partNumber: "", hsCode: "", tariffRate: 0.00, description: "", openSelectDirect: true}},
|
||||||
packaging: {
|
packaging: {
|
||||||
props: {
|
props: {
|
||||||
length: 0,
|
length: 0,
|
||||||
|
|
@ -197,10 +204,8 @@ export default {
|
||||||
},
|
},
|
||||||
supplier: {
|
supplier: {
|
||||||
props: {
|
props: {
|
||||||
supplierName: "",
|
preSelectedNode: null,
|
||||||
supplierAddress: "",
|
openSelectDirect: false
|
||||||
supplierCoordinates: {latitude: 1, longitude: 2},
|
|
||||||
isoCode: "DE"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
destinations: {props: {}},
|
destinations: {props: {}},
|
||||||
|
|
@ -211,15 +216,29 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async updateMaterial(id, action) {
|
startCalculation() {
|
||||||
console.log(id, action);
|
this.premiseEditStore.startCalculation();
|
||||||
await this.premiseEditStore.setMaterial(id, action === 'updateMasterData', this.editIds);
|
},
|
||||||
|
closeMassEdit() {
|
||||||
|
this.$router.push({name: "calculation-list"});
|
||||||
|
},
|
||||||
|
async updateSupplier(data) {
|
||||||
|
console.log("update supplier", data.nodeId, data.action, data.updateMasterData, this.editIds);
|
||||||
|
this.modalType = null;
|
||||||
|
if (data.action === 'accept') {
|
||||||
|
await this.premiseEditStore.setSupplier(data.nodeId, data.updateMasterData, this.editIds);
|
||||||
|
|
||||||
if (this.dataSourceId !== null) {
|
|
||||||
this.fillData("material", this.dataSourceId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
async updateMaterial(id, action) {
|
||||||
|
console.log(id, action, this.editIds);
|
||||||
|
await this.premiseEditStore.setMaterial(id, action === 'updateMasterData', this.editIds);
|
||||||
|
|
||||||
|
if (this.editIds[0] !== null) {
|
||||||
|
this.fillData("material", this.editIds[0]);
|
||||||
|
}
|
||||||
|
},
|
||||||
updateCheckBoxes(value) {
|
updateCheckBoxes(value) {
|
||||||
this.premiseEditStore.setSelectTo(this.ids, value);
|
this.premiseEditStore.setSelectTo(this.ids, value);
|
||||||
},
|
},
|
||||||
|
|
@ -235,13 +254,15 @@ export default {
|
||||||
if (type !== 'destinations')
|
if (type !== 'destinations')
|
||||||
this.fillData(type, dataSource)
|
this.fillData(type, dataSource)
|
||||||
else {
|
else {
|
||||||
console.log(ids, dataSource, massEdit)
|
|
||||||
this.premiseEditStore.prepareDestinations(dataSource, ids, massEdit, true);
|
this.premiseEditStore.prepareDestinations(dataSource, ids, massEdit, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dataSourceId = dataSource !== -1 ? dataSource : null;
|
this.dataSourceId = dataSource !== -1 ? dataSource : null;
|
||||||
this.editIds = ids;
|
this.editIds = ids;
|
||||||
this.modalType = type;
|
this.modalType = type;
|
||||||
|
|
||||||
|
console.log("open modal", massEdit, this.modalType, this.editIds, this.dataSourceId)
|
||||||
|
|
||||||
},
|
},
|
||||||
closeEditModalAction(action) {
|
closeEditModalAction(action) {
|
||||||
|
|
||||||
|
|
@ -249,7 +270,6 @@ export default {
|
||||||
if (action === "accept") {
|
if (action === "accept") {
|
||||||
this.premiseEditStore.executeDestinationsMassEdit();
|
this.premiseEditStore.executeDestinationsMassEdit();
|
||||||
} else {
|
} else {
|
||||||
console.log("cancel mass edit")
|
|
||||||
this.premiseEditStore.cancelMassEdit();
|
this.premiseEditStore.cancelMassEdit();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -282,7 +302,7 @@ export default {
|
||||||
this.editIds.forEach(id => {
|
this.editIds.forEach(id => {
|
||||||
const p = this.premiseEditStore.getById(id);
|
const p = this.premiseEditStore.getById(id);
|
||||||
p.handling_unit.weight = this.componentsData[this.modalType].props.weight;
|
p.handling_unit.weight = this.componentsData[this.modalType].props.weight;
|
||||||
p.handling_unit.height = this.componentsData[this.modalType].props.height;
|
p.handling_unit.width = this.componentsData[this.modalType].props.width;
|
||||||
p.handling_unit.length = this.componentsData[this.modalType].props.length;
|
p.handling_unit.length = this.componentsData[this.modalType].props.length;
|
||||||
p.handling_unit.height = this.componentsData[this.modalType].props.height;
|
p.handling_unit.height = this.componentsData[this.modalType].props.height;
|
||||||
|
|
||||||
|
|
@ -296,11 +316,7 @@ export default {
|
||||||
|
|
||||||
this.premiseEditStore.savePackaging(this.editIds);
|
this.premiseEditStore.savePackaging(this.editIds);
|
||||||
|
|
||||||
} else if (this.modalType === "supplier") {
|
|
||||||
//set supplier.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -310,13 +326,11 @@ export default {
|
||||||
},
|
},
|
||||||
fillData(type, id = -1) {
|
fillData(type, id = -1) {
|
||||||
|
|
||||||
console.log("fillData", type, id);
|
|
||||||
|
|
||||||
if (id === -1) {
|
if (id === -1) {
|
||||||
// clear
|
// clear
|
||||||
this.componentsData = {
|
this.componentsData = {
|
||||||
price: {props: {price: 0, overSeaShare: 0.0, includeFcaFee: true}},
|
price: {props: {price: 0, overSeaShare: 0.0, includeFcaFee: true}},
|
||||||
material: {props: {partNumber: "", hsCode: "", tariffRate: 0.00, description: ""}},
|
material: {props: {partNumber: "", hsCode: "", tariffRate: 0.00, description: "", openSelectDirect: true}},
|
||||||
packaging: {
|
packaging: {
|
||||||
props: {
|
props: {
|
||||||
length: 0,
|
length: 0,
|
||||||
|
|
@ -332,10 +346,8 @@ export default {
|
||||||
},
|
},
|
||||||
supplier: {
|
supplier: {
|
||||||
props: {
|
props: {
|
||||||
supplierName: "",
|
preSelectedNode: null,
|
||||||
supplierAddress: "",
|
openSelectDirect: false
|
||||||
supplierCoordinates: {latitude: 1, longitude: 2},
|
|
||||||
isoCode: "DE"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
destinations: {props: {}},
|
destinations: {props: {}},
|
||||||
|
|
@ -350,12 +362,15 @@ export default {
|
||||||
includeFcaFee: premise.is_fca_enabled
|
includeFcaFee: premise.is_fca_enabled
|
||||||
}
|
}
|
||||||
} else if (type === "material") {
|
} else if (type === "material") {
|
||||||
|
|
||||||
this.componentsData.material.props = {
|
this.componentsData.material.props = {
|
||||||
partNumber: premise.material.part_number,
|
partNumber: premise.material.part_number,
|
||||||
hsCode: premise.material.hs_code ?? "",
|
hsCode: premise.material.hs_code ?? "",
|
||||||
tariffRate: premise.tariff_rate ?? 0.00,
|
tariffRate: premise.tariff_rate ?? 0.00,
|
||||||
description: premise.material.name ?? ""
|
description: premise.material.name ?? "",
|
||||||
|
openSelectDirect: false
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (type === "packaging") {
|
} else if (type === "packaging") {
|
||||||
this.componentsData.packaging.props = {
|
this.componentsData.packaging.props = {
|
||||||
length: premise.handling_unit.length ?? 0,
|
length: premise.handling_unit.length ?? 0,
|
||||||
|
|
@ -370,13 +385,8 @@ export default {
|
||||||
}
|
}
|
||||||
} else if (type === "supplier") {
|
} else if (type === "supplier") {
|
||||||
this.componentsData.supplier.props = {
|
this.componentsData.supplier.props = {
|
||||||
supplierName: premise.supplier.name ?? "",
|
preSelectedNode: premise.supplier,
|
||||||
supplierAddress: premise.supplier.address ?? "",
|
openSelectDirect: true
|
||||||
supplierCoordinates: {
|
|
||||||
latitude: premise.supplier.location.latitude,
|
|
||||||
longitude: premise.supplier.location.longitude
|
|
||||||
},
|
|
||||||
isoCode: premise.supplier.country.iso_code
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,13 @@
|
||||||
<div class="header-controls">
|
<div class="header-controls">
|
||||||
<basic-button @click="close" :show-icon="false" :disabled="premiseEditStore.selectedLoading" variant="secondary"> {{ fromMassEdit ? 'Back' : 'Close' }}
|
<basic-button @click="close" :show-icon="false" :disabled="premiseEditStore.selectedLoading" variant="secondary"> {{ fromMassEdit ? 'Back' : 'Close' }}
|
||||||
</basic-button>
|
</basic-button>
|
||||||
<basic-button v-if="!fromMassEdit" :show-icon="true" :disabled="premiseEditStore.selectedLoading || !premiseEditStore.isSingleSelect"
|
<basic-button v-if="!fromMassEdit"
|
||||||
icon="Calculator" variant="primary">Calculate & close
|
:show-icon="true"
|
||||||
|
:disabled="premiseEditStore.selectedLoading || !premiseEditStore.isSingleSelect"
|
||||||
|
icon="Calculator"
|
||||||
|
variant="primary"
|
||||||
|
@click="startCalculation"
|
||||||
|
>Calculate & close
|
||||||
</basic-button>
|
</basic-button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -123,6 +128,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
startCalculation() {
|
||||||
|
this.premiseEditStore.startCalculation();
|
||||||
|
},
|
||||||
close() {
|
close() {
|
||||||
if(this.bulkEditQuery) {
|
if(this.bulkEditQuery) {
|
||||||
//TODO: deselect and save
|
//TODO: deselect and save
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,77 @@
|
||||||
|
<template>
|
||||||
|
<div class="edit-calculation-container">
|
||||||
|
<div class="header-container">
|
||||||
|
<h2 class="page-header">Configuration</h2>
|
||||||
|
<div>
|
||||||
|
<staged-changes></staged-changes>
|
||||||
|
<box class="box-container">
|
||||||
|
<tab-container :tabs="tabsConfig" class="tab-container">
|
||||||
|
</tab-container>
|
||||||
|
</box>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||||
|
import TabContainer from "@/components/UI/TabContainer.vue";
|
||||||
|
import {markRaw} from "vue";
|
||||||
|
import Properties from "@/components/layout/config/Properties.vue";
|
||||||
|
import Box from "@/components/UI/Box.vue";
|
||||||
|
import CountryProperties from "@/components/layout/config/CountryProperties.vue";
|
||||||
|
import StagedChanges from "@/components/layout/config/StagedChanges.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Config"
|
name: "Config",
|
||||||
|
components: {StagedChanges, Box, TabContainer, BasicButton},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentTab: null,
|
||||||
|
tabsConfig: [
|
||||||
|
{
|
||||||
|
title: 'System properties',
|
||||||
|
component: markRaw(Properties),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Countries',
|
||||||
|
component: markRaw(CountryProperties),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Nodes',
|
||||||
|
component: (null),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Kilometer rates',
|
||||||
|
component: (null),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Container rates',
|
||||||
|
component: (null),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Materials & packaging',
|
||||||
|
component: (null),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Bulk operations',
|
||||||
|
component: (null),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
<h2 class="page-header">Configuration</h2>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
|
.box-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.6rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -12,6 +12,7 @@ const router = createRouter({
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
redirect: '/calculations',
|
redirect: '/calculations',
|
||||||
|
name: 'calculation-list',
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
157
src/frontend/src/store/country.js
Normal file
157
src/frontend/src/store/country.js
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
import {defineStore} from 'pinia'
|
||||||
|
import {config} from '@/config'
|
||||||
|
import {useErrorStore} from "@/store/error.js";
|
||||||
|
import {useStageStore} from "@/store/stage.js";
|
||||||
|
|
||||||
|
export const useCountryStore = defineStore('countryStore', {
|
||||||
|
state() {
|
||||||
|
return {
|
||||||
|
properties: null,
|
||||||
|
periods: null,
|
||||||
|
loadedPeriod: null,
|
||||||
|
loading: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async setProperty(property) {
|
||||||
|
if(this.properties === null) return;
|
||||||
|
|
||||||
|
console.log(property)
|
||||||
|
|
||||||
|
const prop = this.properties.find(p => p.external_mapping_id === property.id);
|
||||||
|
|
||||||
|
if((prop ?? null) === null) return;
|
||||||
|
|
||||||
|
const url = `${config.backendUrl}/properties/country/${property.country.iso_code}/${property.id}`;
|
||||||
|
const body = { value: String(property.value)};
|
||||||
|
|
||||||
|
await this.performRequest('PUT', url, body, false);
|
||||||
|
|
||||||
|
prop.draft_value = property.reset ? null : property.value;
|
||||||
|
|
||||||
|
},
|
||||||
|
async reload() {
|
||||||
|
await this.loadPeriods();
|
||||||
|
await this.loadCountries();
|
||||||
|
|
||||||
|
const stage = useStageStore();
|
||||||
|
await stage.checkStagedChanges();
|
||||||
|
},
|
||||||
|
async loadPeriods() {
|
||||||
|
this.loading = true;
|
||||||
|
const url = `${config.backendUrl}/properties/periods`;
|
||||||
|
this.periods = await this.performRequest('GET', url, null);
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
async loadCountries(period = null) {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (period !== null)
|
||||||
|
params.append('property_set', period);
|
||||||
|
const url = `${config.backendUrl}/countries/all/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||||
|
this.properties = await this.performRequest('GET', url, null);
|
||||||
|
this.loadedPeriod = period;
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
async performRequest(method, url, body, expectResponse = true) {
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if ((body ?? null) !== null) {
|
||||||
|
params.body = JSON.stringify(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = {url: url, params: params};
|
||||||
|
console.log("Request:", request);
|
||||||
|
|
||||||
|
const response = await fetch(url, params
|
||||||
|
).catch(e => {
|
||||||
|
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 => {
|
||||||
|
const error = {
|
||||||
|
code: 'Malformed response',
|
||||||
|
message: "Malformed server response. Please contact support.",
|
||||||
|
trace: null
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = {
|
||||||
|
code: data.error.code,
|
||||||
|
title: data.error.title,
|
||||||
|
message: data.error.message,
|
||||||
|
trace: data.error.trace
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
throw new Error('Internal backend error');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!response.ok) {
|
||||||
|
|
||||||
|
const data = await response.json().catch(e => {
|
||||||
|
const error = {
|
||||||
|
code: "Return code error " + response.status,
|
||||||
|
message: "Server returned wrong response code",
|
||||||
|
trace: null
|
||||||
|
}
|
||||||
|
console.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
throw new Error('Internal backend error');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = {
|
||||||
|
code: data.error.code,
|
||||||
|
title: data.error.title,
|
||||||
|
message: data.error.message,
|
||||||
|
trace: data.error.trace
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
throw new Error('Internal backend error');
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Response:", data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
@ -248,6 +248,23 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
|
||||||
|
async startCalculation() {
|
||||||
|
|
||||||
|
const body = this.premisses.map(p => p.id);
|
||||||
|
const url = `${config.backendUrl}/calculation/start/`;
|
||||||
|
|
||||||
|
const data = await this.performRequest('PUT', url, body).catch(e => {
|
||||||
|
// do something
|
||||||
|
})
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
// do something
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
alert("Finished.");
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DESTINATION stuff
|
* DESTINATION stuff
|
||||||
* =================
|
* =================
|
||||||
|
|
@ -555,14 +572,18 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
||||||
|
|
||||||
const selectedId = this.singleSelectId;
|
const selectedId = this.singleSelectId;
|
||||||
|
|
||||||
|
this.processDestinationMassEdit = true;
|
||||||
|
|
||||||
const body = {supplier_node_id: id, update_master_data: updateMasterData};
|
const body = {supplier_node_id: id, update_master_data: updateMasterData};
|
||||||
const url = `${config.backendUrl}/calculation/supplier/`;
|
const url = `${config.backendUrl}/calculation/supplier/`;
|
||||||
await this.setData(url, body);
|
await this.setData(url, body, ids);
|
||||||
|
|
||||||
if (selectedId != null && this.destinations && !this.destinations.fromMassEditView) {
|
if (selectedId != null && this.destinations && !this.destinations.fromMassEditView) {
|
||||||
this.prepareDestinations(selectedId, [selectedId]);
|
this.prepareDestinations(selectedId, [selectedId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.processDestinationMassEdit = false;
|
||||||
|
|
||||||
},
|
},
|
||||||
async setMaterial(id, updateMasterData, ids = null) {
|
async setMaterial(id, updateMasterData, ids = null) {
|
||||||
console.log("setMaterial");
|
console.log("setMaterial");
|
||||||
|
|
|
||||||
162
src/frontend/src/store/properties.js
Normal file
162
src/frontend/src/store/properties.js
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
import {defineStore} from 'pinia'
|
||||||
|
import {config} from '@/config'
|
||||||
|
import {useErrorStore} from "@/store/error.js";
|
||||||
|
import { useStageStore } from './stage.js'
|
||||||
|
import {usePropertySetsStore} from "@/store/propertySets.js";
|
||||||
|
|
||||||
|
export const usePropertiesStore = defineStore('properties', {
|
||||||
|
state() {
|
||||||
|
return {
|
||||||
|
periods: null,
|
||||||
|
properties: null,
|
||||||
|
loadedPeriod: null,
|
||||||
|
loading: false,
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
isLoading(state) {
|
||||||
|
return state.loading;
|
||||||
|
},
|
||||||
|
getProperties(state) {
|
||||||
|
return state.properties;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async reload() {
|
||||||
|
await this.loadProperties();
|
||||||
|
|
||||||
|
const periods = usePropertySetsStore();
|
||||||
|
await periods.loadPeriods();
|
||||||
|
|
||||||
|
const stage = useStageStore();
|
||||||
|
await stage.checkStagedChanges();
|
||||||
|
},
|
||||||
|
async setProperty(property) {
|
||||||
|
if(this.properties === null) return;
|
||||||
|
|
||||||
|
console.log(property)
|
||||||
|
|
||||||
|
const prop = this.properties.find(p => p.external_mapping_id === property.id);
|
||||||
|
|
||||||
|
if((prop ?? null) === null) return;
|
||||||
|
|
||||||
|
const url = `${config.backendUrl}/properties/system/${property.id}`;
|
||||||
|
const body = { value: String(property.value)};
|
||||||
|
|
||||||
|
await this.performRequest('PUT', url, body, false);
|
||||||
|
|
||||||
|
prop.draft_value = property.reset ? null : property.value;
|
||||||
|
|
||||||
|
const stage = useStageStore();
|
||||||
|
await stage.checkStagedChanges();
|
||||||
|
|
||||||
|
},
|
||||||
|
async loadProperties(period = null) {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (period !== null)
|
||||||
|
params.append('property_set', period);
|
||||||
|
const url = `${config.backendUrl}/properties/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||||
|
this.properties = await this.performRequest('GET', url, null);
|
||||||
|
this.loadedPeriod = period;
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
async performRequest(method, url, body, expectResponse = true) {
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if ((body ?? null) !== null) {
|
||||||
|
params.body = JSON.stringify(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = {url: url, params: params};
|
||||||
|
console.log("Request:", request);
|
||||||
|
|
||||||
|
const response = await fetch(url, params
|
||||||
|
).catch(e => {
|
||||||
|
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 => {
|
||||||
|
const error = {
|
||||||
|
code: 'Malformed response',
|
||||||
|
message: "Malformed server response. Please contact support.",
|
||||||
|
trace: null
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = {
|
||||||
|
code: data.error.code,
|
||||||
|
title: data.error.title,
|
||||||
|
message: data.error.message,
|
||||||
|
trace: data.error.trace
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
throw new Error('Internal backend error');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!response.ok) {
|
||||||
|
|
||||||
|
const data = await response.json().catch(e => {
|
||||||
|
const error = {
|
||||||
|
code: "Return code error " + response.status,
|
||||||
|
message: "Server returned wrong response code",
|
||||||
|
trace: null
|
||||||
|
}
|
||||||
|
console.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
throw new Error('Internal backend error');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = {
|
||||||
|
code: data.error.code,
|
||||||
|
title: data.error.title,
|
||||||
|
message: data.error.message,
|
||||||
|
trace: data.error.trace
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
throw new Error('Internal backend error');
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Response:", data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
150
src/frontend/src/store/propertySets.js
Normal file
150
src/frontend/src/store/propertySets.js
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
import {defineStore} from 'pinia'
|
||||||
|
import {config} from '@/config'
|
||||||
|
import {useErrorStore} from "@/store/error.js";
|
||||||
|
import { useStageStore } from './stage.js'
|
||||||
|
|
||||||
|
export const usePropertySetsStore = defineStore('propertySets', {
|
||||||
|
state() {
|
||||||
|
return {
|
||||||
|
periods: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
getPeriods(state) {
|
||||||
|
return state.periods;
|
||||||
|
},
|
||||||
|
getCurrentPeriodId(state) {
|
||||||
|
if (state.periods === null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
for (const period of state.periods) {
|
||||||
|
if (period.state === "DRAFT")
|
||||||
|
return period.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const period of state.periods) {
|
||||||
|
if (period.state === "VALID")
|
||||||
|
return period.id;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getPeriodState(state) {
|
||||||
|
return function(periodId) {
|
||||||
|
if (state.periods === null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return state.periods.find(p => p.id === periodId)?.state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async invalidate(periodId) {
|
||||||
|
|
||||||
|
|
||||||
|
const url = `${config.backendUrl}/properties/periods/${periodId}/`;
|
||||||
|
this.periods = await this.performRequest('DELETE', url, null, false);
|
||||||
|
|
||||||
|
await this.reload();
|
||||||
|
|
||||||
|
},
|
||||||
|
async loadPeriods() {
|
||||||
|
this.loading = true;
|
||||||
|
const url = `${config.backendUrl}/properties/periods`;
|
||||||
|
this.periods = await this.performRequest('GET', url, null, );
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
async performRequest(method, url, body, expectResponse = true) {
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if ((body ?? null) !== null) {
|
||||||
|
params.body = JSON.stringify(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = {url: url, params: params};
|
||||||
|
console.log("Request:", request);
|
||||||
|
|
||||||
|
const response = await fetch(url, params
|
||||||
|
).catch(e => {
|
||||||
|
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 => {
|
||||||
|
const error = {
|
||||||
|
code: 'Malformed response',
|
||||||
|
message: "Malformed server response. Please contact support.",
|
||||||
|
trace: null
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = {
|
||||||
|
code: data.error.code,
|
||||||
|
title: data.error.title,
|
||||||
|
message: data.error.message,
|
||||||
|
trace: data.error.trace
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
throw new Error('Internal backend error');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!response.ok) {
|
||||||
|
|
||||||
|
const data = await response.json().catch(e => {
|
||||||
|
const error = {
|
||||||
|
code: "Return code error " + response.status,
|
||||||
|
message: "Server returned wrong response code",
|
||||||
|
trace: null
|
||||||
|
}
|
||||||
|
console.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
throw new Error('Internal backend error');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = {
|
||||||
|
code: data.error.code,
|
||||||
|
title: data.error.title,
|
||||||
|
message: data.error.message,
|
||||||
|
trace: data.error.trace
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
throw new Error('Internal backend error');
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Response:", data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
122
src/frontend/src/store/stage.js
Normal file
122
src/frontend/src/store/stage.js
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
import {defineStore} from 'pinia'
|
||||||
|
import {config} from '@/config'
|
||||||
|
import {useErrorStore} from "@/store/error.js";
|
||||||
|
|
||||||
|
export const useStageStore = defineStore('stage', {
|
||||||
|
state() {
|
||||||
|
return {
|
||||||
|
stagedChanges: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
hasStagedChanges(state) {
|
||||||
|
return state.stagedChanges;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async checkStagedChanges() {
|
||||||
|
const url = `${config.backendUrl}/properties/staged_changes`;
|
||||||
|
this.stagedChanges = await this.performRequest('GET', url, null);
|
||||||
|
},
|
||||||
|
async applyChanges() {
|
||||||
|
const url = `${config.backendUrl}/properties/staged_changes`;
|
||||||
|
await this.performRequest('PUT', url, null, false);
|
||||||
|
this.stagedChanges = false;
|
||||||
|
},
|
||||||
|
async performRequest(method, url, body, expectResponse = true) {
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if ((body ?? null) !== null) {
|
||||||
|
params.body = JSON.stringify(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = {url: url, params: params};
|
||||||
|
console.log("Request:", request);
|
||||||
|
|
||||||
|
const response = await fetch(url, params
|
||||||
|
).catch(e => {
|
||||||
|
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 => {
|
||||||
|
const error = {
|
||||||
|
code: 'Malformed response',
|
||||||
|
message: "Malformed server response. Please contact support.",
|
||||||
|
trace: null
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = {
|
||||||
|
code: data.error.code,
|
||||||
|
title: data.error.title,
|
||||||
|
message: data.error.message,
|
||||||
|
trace: data.error.trace
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
throw new Error('Internal backend error');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!response.ok) {
|
||||||
|
|
||||||
|
const data = await response.json().catch(e => {
|
||||||
|
const error = {
|
||||||
|
code: "Return code error " + response.status,
|
||||||
|
message: "Server returned wrong response code",
|
||||||
|
trace: null
|
||||||
|
}
|
||||||
|
console.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
throw new Error('Internal backend error');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = {
|
||||||
|
code: data.error.code,
|
||||||
|
title: data.error.title,
|
||||||
|
message: data.error.message,
|
||||||
|
trace: data.error.trace
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
throw new Error('Internal backend error');
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Response:", data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
@ -2,13 +2,13 @@ package de.avatic.lcc.controller.configuration;
|
||||||
|
|
||||||
import de.avatic.lcc.dto.generic.PropertyDTO;
|
import de.avatic.lcc.dto.generic.PropertyDTO;
|
||||||
import de.avatic.lcc.dto.generic.ValidityPeriodDTO;
|
import de.avatic.lcc.dto.generic.ValidityPeriodDTO;
|
||||||
|
import de.avatic.lcc.dto.configuration.properties.SetPropertyDTO;
|
||||||
import de.avatic.lcc.model.country.IsoCode;
|
import de.avatic.lcc.model.country.IsoCode;
|
||||||
import de.avatic.lcc.service.access.CountryService;
|
import de.avatic.lcc.service.access.CountryService;
|
||||||
import de.avatic.lcc.service.access.PropertyService;
|
import de.avatic.lcc.service.access.PropertyService;
|
||||||
import de.avatic.lcc.service.configuration.PropertyApprovalService;
|
import de.avatic.lcc.service.configuration.PropertyApprovalService;
|
||||||
import de.avatic.lcc.util.exception.badrequest.NotFoundException;
|
import de.avatic.lcc.util.exception.badrequest.NotFoundException;
|
||||||
import jakarta.validation.constraints.Min;
|
import jakarta.validation.constraints.Min;
|
||||||
import jdk.jfr.Unsigned;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
@ -99,12 +99,12 @@ public class PropertyController {
|
||||||
* Sets a system-wide property by external mapping ID.
|
* Sets a system-wide property by external mapping ID.
|
||||||
*
|
*
|
||||||
* @param externalMappingId The external mapping ID for the property.
|
* @param externalMappingId The external mapping ID for the property.
|
||||||
* @param value The value to set for the property.
|
* @param dto The value to set for the property.
|
||||||
* @return ResponseEntity indicating the operation status.
|
* @return ResponseEntity indicating the operation status.
|
||||||
*/
|
*/
|
||||||
@PutMapping({"/system/{external_mapping_id}", "/system/{external_mapping_id}/"})
|
@PutMapping({"/system/{external_mapping_id}", "/system/{external_mapping_id}/"})
|
||||||
public ResponseEntity<Void> setProperties(@PathVariable(name = "external_mapping_id") String externalMappingId, @RequestBody String value) {
|
public ResponseEntity<Void> setProperties(@PathVariable(name = "external_mapping_id") String externalMappingId, @RequestBody SetPropertyDTO dto) {
|
||||||
propertyService.setProperties(externalMappingId, value);
|
propertyService.setProperties(externalMappingId, dto.getValue());
|
||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import jakarta.validation.constraints.Min;
|
||||||
public class DestinationUpdateDTO {
|
public class DestinationUpdateDTO {
|
||||||
|
|
||||||
@JsonProperty("annual_amount")
|
@JsonProperty("annual_amount")
|
||||||
@Min(1)
|
@Min(value= 1, message = "Amount must be greater than or equal 1")
|
||||||
private Integer annualAmount;
|
private Integer annualAmount;
|
||||||
|
|
||||||
@JsonProperty("repackaging_costs")
|
@JsonProperty("repackaging_costs")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package de.avatic.lcc.dto.configuration.properties;
|
||||||
|
|
||||||
|
public class SetPropertyDTO {
|
||||||
|
|
||||||
|
String value;
|
||||||
|
|
||||||
|
public String getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValue(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package de.avatic.lcc.model.utils;
|
package de.avatic.lcc.model.utils;
|
||||||
|
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
import com.fasterxml.jackson.annotation.JsonValue;
|
import com.fasterxml.jackson.annotation.JsonValue;
|
||||||
import de.avatic.lcc.util.exception.badrequest.InvalidArgumentException;
|
import de.avatic.lcc.util.exception.badrequest.InvalidArgumentException;
|
||||||
|
|
||||||
|
|
@ -28,6 +29,26 @@ public enum DimensionUnit {
|
||||||
this.baseFactor = baseFactor;
|
this.baseFactor = baseFactor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom deserializer that handles case-insensitive matching
|
||||||
|
*/
|
||||||
|
@JsonCreator
|
||||||
|
public static DimensionUnit fromString(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (DimensionUnit unit : DimensionUnit.values()) {
|
||||||
|
if (unit.displayedName.equalsIgnoreCase(value)) {
|
||||||
|
return unit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("Unknown DimensionUnit: " + value +
|
||||||
|
". Valid values are: t, kg, g (case insensitive)");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@JsonValue
|
@JsonValue
|
||||||
public String getDisplayedName() {
|
public String getDisplayedName() {
|
||||||
return displayedName;
|
return displayedName;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package de.avatic.lcc.model.utils;
|
package de.avatic.lcc.model.utils;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
import com.fasterxml.jackson.annotation.JsonValue;
|
import com.fasterxml.jackson.annotation.JsonValue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -25,6 +26,25 @@ public enum WeightUnit {
|
||||||
this.baseFactor = baseFactor;
|
this.baseFactor = baseFactor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom deserializer that handles case-insensitive matching
|
||||||
|
*/
|
||||||
|
@JsonCreator
|
||||||
|
public static WeightUnit fromString(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (WeightUnit unit : WeightUnit.values()) {
|
||||||
|
if (unit.displayedName.equalsIgnoreCase(value)) {
|
||||||
|
return unit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("Unknown WeightUnit: " + value +
|
||||||
|
". Valid values are: t, kg, g (case insensitive)");
|
||||||
|
}
|
||||||
|
|
||||||
@JsonValue
|
@JsonValue
|
||||||
public String getDisplayedName() {
|
public String getDisplayedName() {
|
||||||
return displayedName;
|
return displayedName;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import de.avatic.lcc.dto.generic.PropertyDTO;
|
||||||
import de.avatic.lcc.model.properties.SystemPropertyMappingId;
|
import de.avatic.lcc.model.properties.SystemPropertyMappingId;
|
||||||
import de.avatic.lcc.model.rates.ValidityPeriodState;
|
import de.avatic.lcc.model.rates.ValidityPeriodState;
|
||||||
import de.avatic.lcc.util.exception.internalerror.DatabaseException;
|
import de.avatic.lcc.util.exception.internalerror.DatabaseException;
|
||||||
|
import org.springframework.dao.DataAccessException;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.jdbc.core.RowMapper;
|
import org.springframework.jdbc.core.RowMapper;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
@ -12,7 +13,9 @@ import org.springframework.transaction.annotation.Transactional;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -40,15 +43,31 @@ public class PropertyRepository {
|
||||||
public void setProperty(Integer setId, String mappingId, String value) {
|
public void setProperty(Integer setId, String mappingId, String value) {
|
||||||
var typeId = getTypeIdByMappingId(mappingId);
|
var typeId = getTypeIdByMappingId(mappingId);
|
||||||
|
|
||||||
|
String validValueQuery = """
|
||||||
|
SELECT sp.property_value
|
||||||
|
FROM system_property sp
|
||||||
|
JOIN property_set ps ON ps.id = sp.property_set_id
|
||||||
|
WHERE ps.state = ? AND sp.system_property_type_id = ?""";
|
||||||
|
|
||||||
|
String validValue = jdbcTemplate.queryForObject(validValueQuery, String.class,
|
||||||
|
ValidityPeriodState.VALID.name(), typeId);
|
||||||
|
|
||||||
|
if (value.equals(validValue)) {
|
||||||
|
String deleteQuery = "DELETE FROM system_property WHERE property_set_id = ? AND system_property_type_id = ?";
|
||||||
|
jdbcTemplate.update(deleteQuery, setId, typeId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
String query = """
|
String query = """
|
||||||
INSERT INTO system_property (property_set_id, system_property_type_id, property_value) VALUES (?, ?, ?)
|
INSERT INTO system_property (property_set_id, system_property_type_id, property_value) VALUES (?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE property_value = ?""";
|
ON DUPLICATE KEY UPDATE property_value = ?""";
|
||||||
|
|
||||||
var affectedRows = jdbcTemplate.update(query, setId, typeId, value, value);
|
var affectedRows = jdbcTemplate.update(query, setId, typeId, value, value);
|
||||||
|
|
||||||
if(!(affectedRows > 0)) {
|
if (!(affectedRows > 0)) {
|
||||||
throw new DatabaseException("Could not update property value for property set " + setId + " and property type " + mappingId);
|
throw new DatabaseException("Could not update property value for property set " + setId + " and property type " + mappingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -79,7 +98,7 @@ public class PropertyRepository {
|
||||||
LEFT JOIN system_property AS sp ON sp.system_property_type_id = type.id
|
LEFT JOIN system_property AS sp ON sp.system_property_type_id = type.id
|
||||||
LEFT JOIN property_set AS ps ON ps.id = sp.property_set_id AND ps.state IN (?, ?)
|
LEFT JOIN property_set AS ps ON ps.id = sp.property_set_id AND ps.state IN (?, ?)
|
||||||
GROUP BY type.id, type.name, type.data_type, type.external_mapping_id, type.validation_rule
|
GROUP BY type.id, type.name, type.data_type, type.external_mapping_id, type.validation_rule
|
||||||
HAVING draftValue IS NOT NULL OR validValue IS NOT NULL;
|
HAVING draftValue IS NOT NULL OR validValue IS NOT NULL ORDER BY type.name;
|
||||||
""";
|
""";
|
||||||
|
|
||||||
return jdbcTemplate.query(query, new PropertyMapper(), ValidityPeriodState.DRAFT.name(), ValidityPeriodState.VALID.name(), ValidityPeriodState.DRAFT.name(), ValidityPeriodState.VALID.name());
|
return jdbcTemplate.query(query, new PropertyMapper(), ValidityPeriodState.DRAFT.name(), ValidityPeriodState.VALID.name(), ValidityPeriodState.DRAFT.name(), ValidityPeriodState.VALID.name());
|
||||||
|
|
@ -100,10 +119,10 @@ public class PropertyRepository {
|
||||||
FROM system_property_type AS type
|
FROM system_property_type AS type
|
||||||
LEFT JOIN system_property AS property ON property.system_property_type_id = type.id
|
LEFT JOIN system_property AS property ON property.system_property_type_id = type.id
|
||||||
LEFT JOIN property_set AS propertySet ON propertySet.id = property.property_set_id
|
LEFT JOIN property_set AS propertySet ON propertySet.id = property.property_set_id
|
||||||
WHERE propertySet.id = ?
|
WHERE propertySet.id = ? AND (propertySet.state = ? OR propertySet.state = ?) ORDER BY type.name;
|
||||||
""";
|
""";
|
||||||
|
|
||||||
return jdbcTemplate.query(query, new PropertyMapper(), propertySetId);
|
return jdbcTemplate.query(query, new PropertyMapper(), propertySetId, ValidityPeriodState.EXPIRED.name(), ValidityPeriodState.INVALID.name());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
|
|
@ -130,19 +149,32 @@ public class PropertyRepository {
|
||||||
*
|
*
|
||||||
* @param setId the ID of the draft property set to fill
|
* @param setId the ID of the draft property set to fill
|
||||||
*/
|
*/
|
||||||
|
@Transactional
|
||||||
public void fillDraft(Integer setId) {
|
public void fillDraft(Integer setId) {
|
||||||
String query = """
|
String query = """
|
||||||
SELECT type.id AS typeId, property.property_value as value FROM system_property_type AS type
|
SELECT type.id AS typeId, property.property_value as value FROM system_property_type AS type
|
||||||
LEFT JOIN system_property AS property ON property.system_property_type_id = type.id
|
LEFT JOIN system_property AS property ON property.system_property_type_id = type.id
|
||||||
LEFT JOIN property_set AS propertySet ON propertySet.id = property.property_set_id WHERE propertySet.state = ?""";
|
LEFT JOIN property_set AS propertySet ON propertySet.id = property.property_set_id
|
||||||
|
WHERE propertySet.state = ?""";
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<Map<String, Object>> results = jdbcTemplate.queryForList(query, ValidityPeriodState.VALID.name());
|
||||||
|
|
||||||
jdbcTemplate.query(query, (rs, rowNum) -> {
|
String insertQuery = """
|
||||||
String insertQuery = "INSERT IGNORE INTO system_property (property_value, system_property_type_id, property_set_id) VALUES (?, ?, ?)";
|
INSERT IGNORE INTO system_property (property_value, system_property_type_id, property_set_id)
|
||||||
jdbcTemplate.update(insertQuery, rs.getString("value"), rs.getInt("typeId"), setId, ValidityPeriodState.VALID.name());
|
VALUES (?, ?, ?)""";
|
||||||
return null;
|
|
||||||
|
|
||||||
}, setId);
|
List<Object[]> batchArgs = results.stream()
|
||||||
|
.map(row -> new Object[]{row.get("value"), row.get("typeId"), setId})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (!batchArgs.isEmpty()) {
|
||||||
|
jdbcTemplate.batchUpdate(insertQuery, batchArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (DataAccessException e) {
|
||||||
|
throw new DatabaseException("Failed to fill draft property set with ID: " + setId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class PropertyMapper implements RowMapper<PropertyDTO> {
|
private static class PropertyMapper implements RowMapper<PropertyDTO> {
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,12 @@ public class PropertySetRepository {
|
||||||
return totalCount != null && totalCount > 0;
|
return totalCount != null && totalCount > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ValidityPeriodState getState(Integer propertySetId) {
|
||||||
|
String query = "SELECT state FROM property_set WHERE id = ?";
|
||||||
|
String stateString = jdbcTemplate.queryForObject(query, String.class, propertySetId);
|
||||||
|
return stateString != null ? ValidityPeriodState.valueOf(stateString) : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapper class for converting SQL query results into {@link PropertySet} objects.
|
* Mapper class for converting SQL query results into {@link PropertySet} objects.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package de.avatic.lcc.service.access;
|
||||||
import de.avatic.lcc.dto.generic.PropertyDTO;
|
import de.avatic.lcc.dto.generic.PropertyDTO;
|
||||||
import de.avatic.lcc.dto.generic.ValidityPeriodDTO;
|
import de.avatic.lcc.dto.generic.ValidityPeriodDTO;
|
||||||
import de.avatic.lcc.model.properties.SystemPropertyMappingId;
|
import de.avatic.lcc.model.properties.SystemPropertyMappingId;
|
||||||
|
import de.avatic.lcc.model.rates.ValidityPeriodState;
|
||||||
import de.avatic.lcc.repositories.properties.PropertyRepository;
|
import de.avatic.lcc.repositories.properties.PropertyRepository;
|
||||||
import de.avatic.lcc.repositories.properties.PropertySetRepository;
|
import de.avatic.lcc.repositories.properties.PropertySetRepository;
|
||||||
import de.avatic.lcc.service.transformer.rates.ValidityPeriodTransformer;
|
import de.avatic.lcc.service.transformer.rates.ValidityPeriodTransformer;
|
||||||
|
|
@ -82,7 +83,11 @@ public class PropertyService {
|
||||||
* @return a list of properties as {@link PropertyDTO} objects
|
* @return a list of properties as {@link PropertyDTO} objects
|
||||||
*/
|
*/
|
||||||
public List<PropertyDTO> listProperties(Integer propertySetId) {
|
public List<PropertyDTO> listProperties(Integer propertySetId) {
|
||||||
if (propertySetId == 0) {
|
|
||||||
|
|
||||||
|
var state = propertySetId != 0 ? propertySetRepository.getState(propertySetId) : null;
|
||||||
|
|
||||||
|
if (propertySetId == 0 || (state != ValidityPeriodState.EXPIRED && state != ValidityPeriodState.INVALID)) {
|
||||||
return propertyRepository.listProperties();
|
return propertyRepository.listProperties();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -163,17 +163,19 @@ public class PropertyValidationService {
|
||||||
if (!((List<String>) this.value).contains(value)) {
|
if (!((List<String>) this.value).contains(value)) {
|
||||||
throw new PropertyValidationException(propertyId, operator.getIdentifier(), ((List<String>) this.value).toString(), value);
|
throw new PropertyValidationException(propertyId, operator.getIdentifier(), ((List<String>) this.value).toString(), value);
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
try {
|
||||||
|
if (!operator.evaluate(Double.parseDouble(value), (Double) this.value)) {
|
||||||
|
throw new PropertyValidationException(propertyId, operator.getIdentifier(), ((Double) this.value).toString(), value);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
} catch (NumberFormatException e) {
|
||||||
if (!operator.evaluate(Double.parseDouble(value), (Double) this.value)) {
|
throw new PropertyValidationException(propertyId, value, e);
|
||||||
throw new PropertyValidationException(propertyId, operator.getIdentifier(), ((Double) this.value).toString(), value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
throw new PropertyValidationException(propertyId, value, e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ public class CalculationExecutionService {
|
||||||
CalculationJob calculation = calculationJobRepository.getCalculationJob(calculationId).orElseThrow();
|
CalculationJob calculation = calculationJobRepository.getCalculationJob(calculationId).orElseThrow();
|
||||||
|
|
||||||
if (CalculationJobState.SCHEDULED.equals(calculation.getJobState())) {
|
if (CalculationJobState.SCHEDULED.equals(calculation.getJobState())) {
|
||||||
Premise premise = premiseRepository.getPremiseById(calculation.getId()).orElseThrow();
|
Premise premise = premiseRepository.getPremiseById(calculation.getPremiseId()).orElseThrow();
|
||||||
|
|
||||||
// material cost + fca cost
|
// material cost + fca cost
|
||||||
var materialCost = premise.getMaterialCost();
|
var materialCost = premise.getMaterialCost();
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,6 @@ public class CalculationStatusService {
|
||||||
|
|
||||||
public Integer schedule(ArrayList<Integer> calculationIds) {
|
public Integer schedule(ArrayList<Integer> calculationIds) {
|
||||||
//TODO
|
//TODO
|
||||||
return null;
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,68 +12,68 @@ import org.springframework.stereotype.Service;
|
||||||
@Service
|
@Service
|
||||||
public class DimensionTransformer {
|
public class DimensionTransformer {
|
||||||
|
|
||||||
public DimensionDTO toDimensionDTO(PackagingDimension entity) {
|
public DimensionDTO toDimensionDTO(PackagingDimension entity) {
|
||||||
DimensionDTO dto = new DimensionDTO();
|
DimensionDTO dto = new DimensionDTO();
|
||||||
|
|
||||||
dto.setId(entity.getId());
|
dto.setId(entity.getId());
|
||||||
dto.setType(entity.getType());
|
dto.setType(entity.getType());
|
||||||
dto.setLength(entity.getDimensionUnit().convertFromMM(entity.getLength()).doubleValue());
|
dto.setLength(entity.getDimensionUnit().convertFromMM(entity.getLength()).doubleValue());
|
||||||
dto.setWidth(entity.getDimensionUnit().convertFromMM(entity.getWidth()).doubleValue());
|
dto.setWidth(entity.getDimensionUnit().convertFromMM(entity.getWidth()).doubleValue());
|
||||||
dto.setHeight(entity.getDimensionUnit().convertFromMM(entity.getHeight()).doubleValue());
|
dto.setHeight(entity.getDimensionUnit().convertFromMM(entity.getHeight()).doubleValue());
|
||||||
dto.setDimensionUnit(entity.getDimensionUnit());
|
dto.setDimensionUnit(entity.getDimensionUnit());
|
||||||
dto.setWeight(entity.getWeightUnit().convertFromG(entity.getWeight()).doubleValue());
|
dto.setWeight(entity.getWeightUnit().convertFromG(entity.getWeight()).doubleValue());
|
||||||
dto.setWeightUnit(entity.getWeightUnit());
|
dto.setWeightUnit(entity.getWeightUnit());
|
||||||
dto.setContentUnitCount(entity.getContentUnitCount());
|
dto.setContentUnitCount(entity.getContentUnitCount());
|
||||||
dto.setDeprecated(entity.getDeprecated());
|
dto.setDeprecated(entity.getDeprecated());
|
||||||
|
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PackagingDimension toDimensionEntity(DimensionDTO dto) {
|
public PackagingDimension toDimensionEntity(DimensionDTO dto) {
|
||||||
|
|
||||||
if(dto.getDimensionUnit() == null) {
|
if (dto.getDimensionUnit() == null) {
|
||||||
throw new InvalidArgumentException("dimension_unit", "null");
|
throw new InvalidArgumentException("dimension_unit", "null");
|
||||||
}
|
}
|
||||||
|
|
||||||
if(dto.getWeightUnit() == null) {
|
if (dto.getWeightUnit() == null) {
|
||||||
throw new InvalidArgumentException("weight_unit", "null");
|
throw new InvalidArgumentException("weight_unit", "null");
|
||||||
}
|
}
|
||||||
|
|
||||||
var entity = new PackagingDimension();
|
var entity = new PackagingDimension();
|
||||||
entity.setId(dto.getId());
|
entity.setId(dto.getId());
|
||||||
entity.setType(dto.getType());
|
entity.setType(dto.getType());
|
||||||
|
|
||||||
entity.setLength(doDimensionConversion(dto.getLength(), dto.getDimensionUnit()));
|
entity.setLength(doDimensionConversion(dto.getLength(), dto.getDimensionUnit()));
|
||||||
entity.setWidth(doDimensionConversion(dto.getWidth(), dto.getDimensionUnit()));
|
entity.setWidth(doDimensionConversion(dto.getWidth(), dto.getDimensionUnit()));
|
||||||
entity.setHeight( doDimensionConversion(dto.getHeight(), dto.getDimensionUnit()));
|
entity.setHeight(doDimensionConversion(dto.getHeight(), dto.getDimensionUnit()));
|
||||||
entity.setDimensionUnit(dto.getDimensionUnit());
|
entity.setDimensionUnit(dto.getDimensionUnit());
|
||||||
entity.setWeight(doWeightConversion(dto.getWeight(), dto.getWeightUnit()));
|
entity.setWeight(doWeightConversion(dto.getWeight(), dto.getWeightUnit()));
|
||||||
entity.setWeightUnit(dto.getWeightUnit());
|
entity.setWeightUnit(dto.getWeightUnit());
|
||||||
entity.setContentUnitCount(dto.getContentUnitCount());
|
entity.setContentUnitCount(dto.getContentUnitCount());
|
||||||
entity.setDeprecated(dto.getDeprecated());
|
entity.setDeprecated(dto.getDeprecated());
|
||||||
|
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Integer doWeightConversion(Double value, WeightUnit unit) {
|
private Integer doWeightConversion(Double value, WeightUnit unit) {
|
||||||
if(value == null) return null;
|
if (value == null) return null;
|
||||||
|
|
||||||
if(value <= 0) {
|
if (value <= 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return unit.convertToG(value);
|
return unit.convertToG(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Integer doDimensionConversion(Double value, DimensionUnit unit) {
|
private Integer doDimensionConversion(Double value, DimensionUnit unit) {
|
||||||
if(value == null) return null;
|
if (value == null) return null;
|
||||||
|
|
||||||
if(value <= 0) {
|
if (value <= 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return unit.convertToMM(value);
|
return unit.convertToMM(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PackagingDimension toDimensionEntity(Premise entity) {
|
public PackagingDimension toDimensionEntity(Premise entity) {
|
||||||
var packaging = new PackagingDimension();
|
var packaging = new PackagingDimension();
|
||||||
|
|
@ -97,11 +97,11 @@ public class DimensionTransformer {
|
||||||
|
|
||||||
dto.setId(null);
|
dto.setId(null);
|
||||||
dto.setType(PackagingType.HU);
|
dto.setType(PackagingType.HU);
|
||||||
dto.setLength(entity.getHuDisplayedDimensionUnit().convertFromMM(entity.getIndividualHuLength()).doubleValue());
|
dto.setLength(entity.getIndividualHuLength() == null ? null : entity.getHuDisplayedDimensionUnit().convertFromMM(entity.getIndividualHuLength()));
|
||||||
dto.setWidth(entity.getHuDisplayedDimensionUnit().convertFromMM(entity.getIndividualHuWidth()).doubleValue());
|
dto.setWidth(entity.getIndividualHuWidth() == null ? null : entity.getHuDisplayedDimensionUnit().convertFromMM(entity.getIndividualHuWidth()));
|
||||||
dto.setHeight(entity.getHuDisplayedDimensionUnit().convertFromMM(entity.getIndividualHuHeight()).doubleValue());
|
dto.setHeight(entity.getIndividualHuHeight() == null ? null : entity.getHuDisplayedDimensionUnit().convertFromMM(entity.getIndividualHuHeight()));
|
||||||
dto.setDimensionUnit(entity.getHuDisplayedDimensionUnit());
|
dto.setDimensionUnit(entity.getHuDisplayedDimensionUnit());
|
||||||
dto.setWeight(entity.getHuDisplayedWeightUnit().convertFromG(entity.getIndividualHuWeight()).doubleValue());
|
dto.setWeight(entity.getIndividualHuWeight() == null ? null : entity.getHuDisplayedWeightUnit().convertFromG(entity.getIndividualHuWeight()));
|
||||||
dto.setWeightUnit(entity.getHuDisplayedWeightUnit());
|
dto.setWeightUnit(entity.getHuDisplayedWeightUnit());
|
||||||
dto.setContentUnitCount(entity.getHuUnitCount());
|
dto.setContentUnitCount(entity.getHuUnitCount());
|
||||||
dto.setDeprecated(null);
|
dto.setDeprecated(null);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package de.avatic.lcc.service.transformer.premise;
|
||||||
|
|
||||||
import de.avatic.lcc.dto.calculation.PremiseDTO;
|
import de.avatic.lcc.dto.calculation.PremiseDTO;
|
||||||
import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO;
|
import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO;
|
||||||
import de.avatic.lcc.dto.generic.DimensionDTO;
|
|
||||||
import de.avatic.lcc.dto.generic.LocationDTO;
|
import de.avatic.lcc.dto.generic.LocationDTO;
|
||||||
import de.avatic.lcc.dto.generic.NodeDTO;
|
import de.avatic.lcc.dto.generic.NodeDTO;
|
||||||
import de.avatic.lcc.dto.generic.NodeType;
|
import de.avatic.lcc.dto.generic.NodeType;
|
||||||
|
|
@ -96,10 +95,8 @@ public class PremiseTransformer {
|
||||||
dto.setMixable(entity.getHuMixable());
|
dto.setMixable(entity.getHuMixable());
|
||||||
dto.setStackable(entity.getHuStackable());
|
dto.setStackable(entity.getHuStackable());
|
||||||
|
|
||||||
if (entity.getIndividualHuHeight() == null || entity.getIndividualHuWidth() == null || entity.getIndividualHuLength() == null || entity.getIndividualHuWeight() == null)
|
|
||||||
dto.setDimension(new DimensionDTO());
|
dto.setDimension(dimensionTransformer.toDimensionDTO(entity));
|
||||||
else
|
|
||||||
dto.setDimension(dimensionTransformer.toDimensionDTO(entity));
|
|
||||||
|
|
||||||
if (entity.getSupplierNodeId() != null)
|
if (entity.getSupplierNodeId() != null)
|
||||||
dto.setSupplier(nodeRepository.getById(entity.getSupplierNodeId()).map(nodeTransformer::toNodeDTO).orElseThrow());
|
dto.setSupplier(nodeRepository.getById(entity.getSupplierNodeId()).map(nodeTransformer::toNodeDTO).orElseThrow());
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue