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:
Jan 2025-09-03 15:59:02 +02:00
parent f5c4e1159f
commit 45742d731d
41 changed files with 1775 additions and 177 deletions

View file

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

View file

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

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

View file

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

View file

@ -0,0 +1,15 @@
<template>
<h3>Bulk Operations</h3>
</template>
<script>
export default {
name: "BulkUpload"
}
</script>
<style scoped>
</style>

View file

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

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,6 +12,7 @@ const router = createRouter({
{ {
path: '/', path: '/',
redirect: '/calculations', redirect: '/calculations',
name: 'calculation-list',
}, },
{ {

View 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;
}
},
});

View file

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

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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