FRONTEND/BACKEND: fixing bugs in schema contstraints. Fixing performance issues in mass calculation (frontend)

This commit is contained in:
Jan 2025-09-05 19:38:07 +02:00
parent 32feeb06a0
commit c47531a335
19 changed files with 758 additions and 574 deletions

File diff suppressed because it is too large Load diff

View file

@ -14,6 +14,7 @@
"dependencies": { "dependencies": {
"@phosphor-icons/vue": "^2.2.1", "@phosphor-icons/vue": "^2.2.1",
"@vueuse/core": "^13.6.0", "@vueuse/core": "^13.6.0",
"loglevel": "^1.9.2",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"vue": "^3.5.18", "vue": "^3.5.18",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"

View file

@ -154,3 +154,5 @@ export class UrlSafeBase64 {
return urlSafeBase64.replace(/-/g, '+').replace(/_/g, '/'); return urlSafeBase64.replace(/-/g, '+').replace(/_/g, '/');
} }
} }

View file

@ -1,7 +1,12 @@
<template> <template>
<div class="checkbox-container"> <div class="checkbox-container">
<label class="checkbox-item" @change="setFilter"> <label class="checkbox-item" :class="{ disabled: disabled }" @change="setFilter">
<input type="checkbox" checked v-model="isChecked" > <input
type="checkbox"
:checked="isChecked"
:disabled="disabled"
v-model="isChecked"
>
<span class="checkmark"></span> <span class="checkmark"></span>
<span class="checkbox-label"><slot></slot></span> <span class="checkbox-label"><slot></slot></span>
</label> </label>
@ -16,6 +21,11 @@ export default{
type: Boolean, type: Boolean,
default: true, default: true,
required: false required: false
},
disabled: {
type: Boolean,
default: false,
required: false
} }
}, },
name: "Checkbox", name: "Checkbox",
@ -30,6 +40,7 @@ export default{
return this.internalChecked; return this.internalChecked;
}, },
set(value) { set(value) {
if (this.disabled) return; // Prevent changes when disabled
this.internalChecked = value; this.internalChecked = value;
this.$emit('checkbox-changed', value); this.$emit('checkbox-changed', value);
} }
@ -42,6 +53,7 @@ export default{
}, },
methods: { methods: {
setFilter(event) { setFilter(event) {
if (this.disabled) return; // Prevent action when disabled
// The computed setter will handle the emit // The computed setter will handle the emit
this.isChecked = event.target.checked; this.isChecked = event.target.checked;
} }
@ -66,6 +78,11 @@ export default{
user-select: none; user-select: none;
} }
.checkbox-item.disabled {
cursor: not-allowed;
opacity: 0.6;
}
.checkbox-item input[type="checkbox"] { .checkbox-item input[type="checkbox"] {
position: absolute; position: absolute;
opacity: 0; opacity: 0;
@ -74,6 +91,10 @@ export default{
width: 0; width: 0;
} }
.checkbox-item.disabled input[type="checkbox"] {
cursor: not-allowed;
}
.checkmark { .checkmark {
position: relative; position: relative;
height: 2rem; height: 2rem;
@ -88,18 +109,28 @@ export default{
justify-content: center; justify-content: center;
} }
.checkbox-item:hover .checkmark { .checkbox-item:not(.disabled):hover .checkmark {
background: #EEF4FF; background: #EEF4FF;
border: 0.2rem solid #8DB3FE; border: 0.2rem solid #8DB3FE;
transform: scale(1.1); transform: scale(1.1);
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.checkbox-item.disabled .checkmark {
background-color: #f5f5f5;
border-color: #d0d0d0;
}
.checkbox-item input:checked ~ .checkmark { .checkbox-item input:checked ~ .checkmark {
background-color: #002F54; background-color: #002F54;
border-color: #002F54; border-color: #002F54;
} }
.checkbox-item.disabled input:checked ~ .checkmark {
background-color: #6b7280;
border-color: #6b7280;
}
.checkmark::after { .checkmark::after {
content: ""; content: "";
position: absolute; position: absolute;
@ -123,4 +154,8 @@ export default{
font-weight: 400; font-weight: 400;
letter-spacing: 0.04em; letter-spacing: 0.04em;
} }
.checkbox-item.disabled .checkbox-label {
color: #8a8a8a;
}
</style> </style>

View file

@ -3,7 +3,7 @@
<div class="edit-calculation-checkbox-cell"> <div class="edit-calculation-checkbox-cell">
<checkbox :checked="isSelected" @checkbox-changed="updateSelected"></checkbox> <checkbox :checked="isSelected" @checkbox-changed="updateSelected"></checkbox>
</div> </div>
<div class="edit-calculation-cell--material" :class="copyModeClass" <div class="edit-calculation-cell--material copyable-cell"
@click="action('material')"> @click="action('material')">
<div class="edit-calculation-cell-line">{{ premise.material.part_number }}</div> <div class="edit-calculation-cell-line">{{ premise.material.part_number }}</div>
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="premise.material.name"> <div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="premise.material.name">
@ -19,7 +19,7 @@
{{ toPercent(premise.tariff_rate) }} % {{ toPercent(premise.tariff_rate) }} %
</div> </div>
</div> </div>
<div class="edit-calculation-cell--price" :class="copyModeClass" v-if="showPrice" <div class="edit-calculation-cell--price copyable-cell" 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: <div class="edit-calculation-cell-line edit-calculation-cell-subline">Oversea share:
@ -28,11 +28,14 @@
<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>
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="showPriceIncomplete">
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
</div>
</div> </div>
<div class="edit-calculation-empty" :class="copyModeClass" v-else @click="action('price')"> <div class="edit-calculation-empty copyable-cell" v-else @click="action('price')">
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge> <basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
</div> </div>
<div v-if="showHu" class="edit-calculation-cell edit-calculation-cell--packaging" :class="copyModeClass" <div v-if="showHu" class="edit-calculation-cell edit-calculation-cell--packaging copyable-cell"
@click="action('packaging')"> @click="action('packaging')">
<div class="edit-calculation-cell-line"> <div class="edit-calculation-cell-line">
<PhVectorThree/> <PhVectorThree/>
@ -53,11 +56,11 @@
<basic-badge v-if="premise.is_mixable" variant="skeleton" icon="shuffle">MIXABLE</basic-badge> <basic-badge v-if="premise.is_mixable" variant="skeleton" icon="shuffle">MIXABLE</basic-badge>
</div> </div>
</div> </div>
<div class="edit-calculation-empty" :class="copyModeClass" v-else <div class="edit-calculation-empty copyable-cell" v-else
@click="action('packaging')"> @click="action('packaging')">
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge> <basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
</div> </div>
<div class="edit-calculation-cell--supplier" :class="copyModeClass" <div class="edit-calculation-cell--supplier copyable-cell"
@click="action('supplier')"> @click="action('supplier')">
<div class="edit-calculation-cell--supplier-container" v-if="premise.supplier"> <div class="edit-calculation-cell--supplier-container" v-if="premise.supplier">
<!-- <div class="edit-calculation-cell&#45;&#45;supplier-flag">--> <!-- <div class="edit-calculation-cell&#45;&#45;supplier-flag">-->
@ -69,7 +72,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="edit-calculation-cell--destination" :class="copyModeClass" v-if="showDestinations" <div class="edit-calculation-cell--destination copyable-cell" v-if="showDestinations"
@click="action('destinations')"> @click="action('destinations')">
<div class="edit-calculation-cell-line"> <div class="edit-calculation-cell-line">
<span class="number-circle"> {{ destinationsCount }} </span> Destinations <span class="number-circle"> {{ destinationsCount }} </span> Destinations
@ -82,7 +85,7 @@
<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 copyable-cell" v-else
@click="action('destinations')"> @click="action('destinations')">
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge> <basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
</div> </div>
@ -109,7 +112,8 @@ import {
PhBarcode, PhBarcode,
PhEmpty, PhEmpty,
PhFactory, PhFactory,
PhHash, PhMapPin, PhHash,
PhMapPin,
PhPercent, PhPercent,
PhVectorThree, PhVectorThree,
PhVectorTwo PhVectorTwo
@ -133,9 +137,9 @@ export default {
type: Number, type: Number,
required: true required: true
}, },
copyMode: { premise: {
type: Boolean, type: Object,
default: false required: true,
} }
}, },
computed: { computed: {
@ -169,6 +173,9 @@ export default {
showPrice() { showPrice() {
return (this.premise.material_cost) return (this.premise.material_cost)
}, },
showPriceIncomplete() {
return !(this.premise.oversea_share)
},
isSelected() { isSelected() {
return this.premise.selected; return this.premise.selected;
}, },
@ -176,16 +183,6 @@ export default {
return this.premise.handling_unit; return this.premise.handling_unit;
}, },
...mapStores(usePremiseEditStore), ...mapStores(usePremiseEditStore),
premise() {
const data = this.premiseEditStore.getById(this.id);
return data;
},
copyModeClass() {
if (this.copyMode) {
return 'edit-calculation-cell--copy-mode';
}
return 'edit-calculation-cell';
}
}, },
methods: { methods: {
toPercent(value) { toPercent(value) {
@ -211,6 +208,19 @@ export default {
<style scoped> <style scoped>
.copyable-cell {
padding: 0.8rem;
border-radius: 0.8rem;
height: 90%;
}
/* Standard hover ohne copy mode */
.copyable-cell:hover {
cursor: pointer;
background-color: rgba(107, 134, 156, 0.05);
border-radius: 0.8rem;
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
}
.bulk-edit-row { .bulk-edit-row {
display: grid; display: grid;

View file

@ -121,6 +121,7 @@ export default {
}, },
methods: { methods: {
async saveProperty(property) { async saveProperty(property) {
this.countryStore.setProperty(property);
}, },
async query(query) { async query(query) {

View file

@ -147,31 +147,12 @@ export default {
font-weight: 500; font-weight: 500;
} }
/* SOLUTION 1: Add relative positioning and min-height to prevent collapse */
.properties-list { .properties-list {
position: relative; position: relative;
min-height: 100px; /* Adjust based on your typical content height */ min-height: 100px;
width: fit-content;
} }
/* SOLUTION 2: Keep elements in normal flow during transition
.properties-fade-enter-active,
.properties-fade-leave-active {
transition: opacity 0.3s ease;
}
.properties-fade-enter-from {
opacity: 0;
}
.properties-fade-leave-to {
opacity: 0;
}*/
/* SOLUTION 3: For transition-group, ensure proper positioning
.property-item-enter-active,
.property-item-leave-active {
transition: all 0.3s ease;
}*/
.property-item-enter-from { .property-item-enter-from {
opacity: 0; opacity: 0;
@ -193,16 +174,4 @@ export default {
.property-item-move { .property-item-move {
transition: transform 0.3s ease; transition: transform 0.3s ease;
} }
/* ALTERNATIVE SOLUTION: If you still have issues, try this instead */
/*
.properties-container {
min-height: 500px;
}
.properties-list {
position: relative;
overflow: hidden;
}
*/
</style> </style>

View file

@ -3,6 +3,7 @@
<div class="caption-column"> <div class="caption-column">
<div class="caption-column-id">{{ property.name }}:</div> <div class="caption-column-id">{{ property.name }}:</div>
<div class="caption-column-name">Ext. Mapping: {{ property.external_mapping_id }}</div>
</div> </div>
<div class="input-column"> <div class="input-column">
@ -197,7 +198,6 @@ export default {
grid-template-columns: 3fr 1fr 0.5fr; grid-template-columns: 3fr 1fr 0.5fr;
align-items: center; align-items: center;
gap: 2.4rem; gap: 2.4rem;
height: 8rem; height: 8rem;
} }

View file

@ -56,13 +56,13 @@
</div> </div>
<div class="input-column-chk"> <div class="input-column-chk">
<tooltip position="left" text="Deselect if the handling unit cannot be stacked">
<checkbox :checked="stackable" @checkbox-changed="updateStackable">stackable</checkbox>
</tooltip>
<tooltip position="left" <tooltip position="left"
text="Deselect if the handling unit cannot be transported together with other handling units"> text="Deselect if the handling unit cannot be transported together with other handling units">
<checkbox :checked="mixable" @checkbox-changed="updateMixable">mixable</checkbox> <checkbox :checked="mixable" @checkbox-changed="updateMixable">mixable</checkbox>
</tooltip> </tooltip>
<tooltip position="left" text="Deselect if the handling unit cannot be stacked">
<checkbox :checked="stackable" @checkbox-changed="updateStackable" :disabled="mixable">stackable</checkbox>
</tooltip>
</div> </div>
</div> </div>
</template> </template>
@ -238,6 +238,10 @@ export default {
this.$emit('update:stackable', value); this.$emit('update:stackable', value);
}, },
updateMixable(value) { updateMixable(value) {
if(value)
this.updateStackable(true);
this.$emit('update:mixable', value); this.$emit('update:mixable', value);
}, },
}, },

View file

@ -0,0 +1,9 @@
import log from 'loglevel'
if (process.env.NODE_ENV === 'production') {
log.setLevel('silent')
} else {
log.setLevel('debug')
}
export default log

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="edit-calculation-container"> <div class="edit-calculation-container" :class="{ 'has-selection': hasSelection }">
<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">
@ -43,8 +43,8 @@
<span class="space-around">No Calculations found.</span> <span class="space-around">No Calculations found.</span>
</div> </div>
<bulk-edit-row v-else class="edit-calculation-list-item" v-for="id in this.premiseEditStore.getPremiseIds" <bulk-edit-row v-else class="edit-calculation-list-item" v-for="premise of this.premiseEditStore.getPremisses"
:key="id" :id="id" @action="onClickAction" :copy-mode="selectCount !== 0"> :key="premise.id" :id="premise.id" :premise="premise" @action="onClickAction">
</bulk-edit-row> </bulk-edit-row>
@ -118,7 +118,6 @@ import Modal from "@/components/UI/Modal.vue";
import PriceEdit from "@/components/layout/edit/PriceEdit.vue"; import PriceEdit from "@/components/layout/edit/PriceEdit.vue";
import MaterialEdit from "@/components/layout/edit/MaterialEdit.vue"; 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 DestinationListView from "@/components/layout/edit/DestinationListView.vue"; import DestinationListView from "@/components/layout/edit/DestinationListView.vue";
import SelectNode from "@/components/layout/node/SelectNode.vue"; import SelectNode from "@/components/layout/node/SelectNode.vue";
@ -136,6 +135,12 @@ export default {
components: {Modal, MassEditDialog, ListEdit, Spinner, CalculationListItem, Checkbox, BulkEditRow, BasicButton}, components: {Modal, MassEditDialog, ListEdit, Spinner, CalculationListItem, Checkbox, BulkEditRow, BasicButton},
computed: { computed: {
...mapStores(usePremiseEditStore), ...mapStores(usePremiseEditStore),
hasSelection() {
if (this.premiseEditStore.isLoading || this.premiseEditStore.selectedLoading) {
return false;
}
return this.premiseEditStore.getSelectedPremissesIds?.length > 0;
},
selectCount() { selectCount() {
return this.selectedPremisses?.length ?? 0; return this.selectedPremisses?.length ?? 0;
}, },
@ -179,7 +184,7 @@ export default {
created() { created() {
this.bulkQuery = this.$route.params.ids; this.bulkQuery = this.$route.params.ids;
this.ids = new UrlSafeBase64().decodeIds(this.$route.params.ids); this.ids = new UrlSafeBase64().decodeIds(this.$route.params.ids);
this.premiseEditStore.loadPremissesForced(this.ids); this.premiseEditStore.loadPremissesIfNeeded(this.ids);
}, },
data() { data() {
return { return {
@ -199,7 +204,7 @@ export default {
dimensionUnit: "MM", dimensionUnit: "MM",
unitCount: 1, unitCount: 1,
mixable: true, mixable: true,
stackable: false stackable: true
} }
}, },
supplier: { supplier: {
@ -223,6 +228,7 @@ export default {
this.$router.push({name: "calculation-list"}); this.$router.push({name: "calculation-list"});
}, },
async updateSupplier(data) { async updateSupplier(data) {
console.log("update supplier", data.nodeId, data.action, data.updateMasterData, this.editIds);
console.log("update supplier", data.nodeId, data.action, data.updateMasterData, this.editIds); console.log("update supplier", data.nodeId, data.action, data.updateMasterData, this.editIds);
this.modalType = null; this.modalType = null;
if (data.action === 'accept') { if (data.action === 'accept') {
@ -240,7 +246,7 @@ export default {
} }
}, },
updateCheckBoxes(value) { updateCheckBoxes(value) {
this.premiseEditStore.setSelectTo(this.ids, value); this.premiseEditStore.setAll(value);
}, },
multiselectAction(action) { multiselectAction(action) {
this.openModal(action, this.selectedPremisses.map(p => p.id)); this.openModal(action, this.selectedPremisses.map(p => p.id));
@ -264,63 +270,30 @@ export default {
console.log("open modal", massEdit, this.modalType, this.editIds, this.dataSourceId) console.log("open modal", massEdit, this.modalType, this.editIds, this.dataSourceId)
}, },
closeEditModalAction(action) { async closeEditModalAction(action) {
if (this.modalType === "destinations") { if (this.modalType === "destinations") {
if (action === "accept") { if (action === "accept") {
this.premiseEditStore.executeDestinationsMassEdit(); await this.premiseEditStore.executeDestinationsMassEdit();
} else { } else {
this.premiseEditStore.cancelMassEdit(); this.premiseEditStore.cancelMassEdit();
} }
} else { } else if (action === "accept") {
if (action === "accept") { const props = this.componentsData[this.modalType].props;
if (this.modalType === "price") { switch(this.modalType) {
this.editIds.forEach(id => { case "price":
const p = this.premiseEditStore.getById(id); await this.premiseEditStore.batchUpdatePrice(this.editIds, props);
p.material_cost = this.componentsData[this.modalType].props.price; break;
p.oversea_share = this.componentsData[this.modalType].props.overSeaShare; case "material":
p.is_fca_enabled = this.componentsData[this.modalType].props.includeFcaFee; await this.premiseEditStore.batchUpdateMaterial(this.editIds, props);
}); break;
case "packaging":
this.premiseEditStore.savePrice(this.editIds); await this.premiseEditStore.batchUpdatePackaging(this.editIds, props);
break;
} else if (this.modalType === "material") {
this.editIds.forEach(id => {
const p = this.premiseEditStore.getById(id);
p.material.part_number = this.componentsData[this.modalType].props.partNumber;
p.material.hs_code = this.componentsData[this.modalType].props.hsCode;
p.tariff_rate = this.componentsData[this.modalType].props.tariffRate;
});
this.premiseEditStore.saveMaterial(this.editIds);
} else if (this.modalType === "packaging") {
this.editIds.forEach(id => {
const p = this.premiseEditStore.getById(id);
p.handling_unit.weight = this.componentsData[this.modalType].props.weight;
p.handling_unit.width = this.componentsData[this.modalType].props.width;
p.handling_unit.length = this.componentsData[this.modalType].props.length;
p.handling_unit.height = this.componentsData[this.modalType].props.height;
p.handling_unit.weight_unit = this.componentsData[this.modalType].props.weightUnit;
p.handling_unit.dimension_unit = this.componentsData[this.modalType].props.dimensionUnit;
p.handling_unit.content_unit_count = this.componentsData[this.modalType].props.unitCount;
p.is_stackable = this.componentsData[this.modalType].props.stackable;
p.is_mixable = this.componentsData[this.modalType].props.mixable;
});
this.premiseEditStore.savePackaging(this.editIds);
}
} }
} }
// clear data. // Clear data
this.fillData(this.modalType); this.fillData(this.modalType);
this.modalType = null; this.modalType = null;
}, },
@ -341,7 +314,7 @@ export default {
dimensionUnit: "MM", dimensionUnit: "MM",
unitCount: 1, unitCount: 1,
mixable: true, mixable: true,
stackable: false stackable: true
} }
}, },
supplier: { supplier: {
@ -394,9 +367,16 @@ export default {
} }
} }
</script> </script>
<style scoped> <style scoped>
/* Global style für copy-mode cursor */
.edit-calculation-container.has-selection :deep(.copyable-cell:hover) {
cursor: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOCIgaGVpZ2h0PSIyOCIgdmlld0JveD0iMCAwIDI1NiAyNTYiPgogIDxyZWN0IHg9Ijg0IiB5PSIzMiIgd2lkdGg9IjEzNiIgaGVpZ2h0PSIxMzYiIGZpbGw9IndoaXRlIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iOCIgcng9IjQiLz4KICA8cmVjdCB4PSIzNiIgeT0iODQiIHdpZHRoPSIxMzYiIGhlaWdodD0iMTM2IiBmaWxsPSJ3aGl0ZSIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjgiIHJ4PSI0Ii8+Cjwvc3ZnPg==") 12 12, pointer;
background-color: rgba(107, 134, 156, 0.05);
border-radius: 0.8rem;
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
}
.space-around { .space-around {
margin: 3rem; margin: 3rem;
} }

View file

@ -134,6 +134,7 @@ export default {
close() { close() {
if(this.bulkEditQuery) { if(this.bulkEditQuery) {
//TODO: deselect and save //TODO: deselect and save
this.premiseEditStore.deselectPremise();
this.$router.push({name: 'bulk', params: {ids: this.bulkEditQuery}}); this.$router.push({name: 'bulk', params: {ids: this.bulkEditQuery}});
} }
else { else {

View file

@ -8,7 +8,6 @@ export const useCountryStore = defineStore('country', {
state() { state() {
return { return {
countries: null, countries: null,
properties: null,
loading: false, loading: false,
query: null, query: null,
selectedCountryId: null, selectedCountryId: null,
@ -27,18 +26,21 @@ export const useCountryStore = defineStore('country', {
}, },
actions: { actions: {
async setProperty(property) { async setProperty(property) {
if(this.properties === null) return; if(this.getSelectedCountry === null) return;
const prop = this.properties.find(p => p.external_mapping_id === property.id); const prop = this.getSelectedCountry.properties.find(p => p.external_mapping_id === property.id);
if((prop ?? null) === null) return; if((prop ?? null) === null) return;
const url = `${config.backendUrl}/properties/country/${property.country.iso_code}/${property.id}`; const url = `${config.backendUrl}/properties/country/${this.getSelectedCountry.iso_code}/${property.id}`;
const body = { value: String(property.value)}; const body = { value: String(property.value)};
await this.performRequest('PUT', url, body, false); await this.performRequest('PUT', url, body, false);
prop.draft_value = property.reset ? null : property.value; prop.draft_value = property.reset ? null : property.value;
const stage = useStageStore();
await stage.checkStagedChanges();
}, },
async selectPeriod(periodId) { async selectPeriod(periodId) {
this.selectedPeriodId = periodId; this.selectedPeriodId = periodId;

View file

@ -2,6 +2,7 @@ import {defineStore} from 'pinia'
import {config} from '@/config' import {config} from '@/config'
import {toRaw} from "vue"; import {toRaw} from "vue";
import {useErrorStore} from "@/store/error.js"; import {useErrorStore} from "@/store/error.js";
import logger from "@/logger.js"
export const usePremiseEditStore = defineStore('premiseEdit', { export const usePremiseEditStore = defineStore('premiseEdit', {
state() { state() {
@ -29,6 +30,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
} }
}, },
getters: { getters: {
/** /**
* Returns the ids of all premises. * Returns the ids of all premises.
* @param state * @param state
@ -247,7 +249,66 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
}, },
actions: { actions: {
async batchUpdatePrice(ids, priceData) {
const updatedPremises = this.premisses.map(p => {
if (ids.includes(p.id)) {
return {
...p,
material_cost: priceData.price,
oversea_share: priceData.overSeaShare,
is_fca_enabled: priceData.includeFcaFee
};
}
return p;
});
this.premisses = updatedPremises;
return await this.savePrice(ids);
},
async batchUpdateMaterial(ids, materialData) {
const updatedPremises = this.premisses.map(p => {
if (ids.includes(p.id)) {
return {
...p,
material: {
...p.material,
part_number: materialData.partNumber,
hs_code: materialData.hsCode
},
tariff_rate: materialData.tariffRate
};
}
return p;
});
this.premisses = updatedPremises;
return await this.saveMaterial(ids);
},
async batchUpdatePackaging(ids, packagingData) {
const updatedPremises = this.premisses.map(p => {
if (ids.includes(p.id)) {
return {
...p,
handling_unit: {
...p.handling_unit,
weight: packagingData.weight,
width: packagingData.width,
length: packagingData.length,
height: packagingData.height,
weight_unit: packagingData.weightUnit,
dimension_unit: packagingData.dimensionUnit,
content_unit_count: packagingData.unitCount
},
is_stackable: packagingData.stackable,
is_mixable: packagingData.mixable
};
}
return p;
});
this.premisses = updatedPremises;
return await this.savePackaging(ids);
},
async startCalculation() { async startCalculation() {
const body = this.premisses.map(p => p.id); const body = this.premisses.map(p => p.id);
@ -406,7 +467,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
selectDestination(id) { selectDestination(id) {
if (this.premisses === null) return; if (this.premisses === null) return;
console.log("selectDestination:", id) logger.info("selectDestination:", id)
const dest = this.destinations.destinations.find(d => d.id === id); const dest = this.destinations.destinations.find(d => d.id === id);
@ -446,7 +507,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
route_selected_id: toDest.routes.find(r => r.is_selected)?.id ?? null, route_selected_id: toDest.routes.find(r => r.is_selected)?.id ?? null,
}; };
console.log(body) logger.info(body)
const url = `${config.backendUrl}/calculation/destination/${toDest.id}`; const url = `${config.backendUrl}/calculation/destination/${toDest.id}`;
await this.performRequest('PUT', url, body, false); await this.performRequest('PUT', url, body, false);
@ -483,15 +544,15 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
const url = `${config.backendUrl}/calculation/destination/${origId}`; const url = `${config.backendUrl}/calculation/destination/${origId}`;
await this.performRequest('DELETE', url, null, false).catch(async e => { await this.performRequest('DELETE', url, null, false).catch(async e => {
console.error("Unable to delete destination: " + origId + ""); logger.error("Unable to delete destination: " + origId + "");
console.error(e); logger.error(e);
await this.loadPremissesIfNeeded(this.premisses.map(p => p.id)); await this.loadPremissesIfNeeded(this.premisses.map(p => p.id));
}); });
for (const p of this.premisses) { for (const p of this.premisses) {
const toBeDeleted = p.destinations.findIndex(d => String(d.id) === String(origId)) const toBeDeleted = p.destinations.findIndex(d => String(d.id) === String(origId))
console.log(toBeDeleted) logger.info(toBeDeleted)
if (toBeDeleted !== -1) { if (toBeDeleted !== -1) {
p.destinations.splice(toBeDeleted, 1) p.destinations.splice(toBeDeleted, 1)
@ -505,7 +566,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
if (this.destinations.massEdit) { if (this.destinations.massEdit) {
const existing = this.destinations.destinations.find(d => d.destination_node.id === node.id); const existing = this.destinations.destinations.find(d => d.destination_node.id === node.id);
console.log(existing) logger.info(existing)
if ((existing ?? null) !== null) { if ((existing ?? null) !== null) {
console.info("Destination already exists", node.id); console.info("Destination already exists", node.id);
@ -568,7 +629,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
*/ */
async setSupplier(id, updateMasterData, ids = null) { async setSupplier(id, updateMasterData, ids = null) {
console.log("setSupplier"); logger.info("setSupplier");
const selectedId = this.singleSelectId; const selectedId = this.singleSelectId;
@ -586,7 +647,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
}, },
async setMaterial(id, updateMasterData, ids = null) { async setMaterial(id, updateMasterData, ids = null) {
console.log("setMaterial"); logger.info("setMaterial");
const body = {material_id: id, update_master_data: updateMasterData}; const body = {material_id: id, update_master_data: updateMasterData};
const url = `${config.backendUrl}/calculation/material/`; const url = `${config.backendUrl}/calculation/material/`;
await this.setData(url, body, ids); await this.setData(url, body, ids);
@ -600,7 +661,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
this.loading = true; this.loading = true;
body.premise_id = toBeUpdated; body.premise_id = toBeUpdated;
console.log(url, body) logger.info(url, body)
const data = await this.performRequest('PUT', url, body).catch(e => { const data = await this.performRequest('PUT', url, body).catch(e => {
this.loading = false; this.loading = false;
@ -626,7 +687,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
replacePremissesById(premisses, loadedData) { replacePremissesById(premisses, loadedData) {
const replacementMap = new Map(loadedData.map(obj => [obj.id, obj])); const replacementMap = new Map(loadedData.map(obj => [obj.id, obj]));
const replaced = premisses.map(obj => replacementMap.get(obj.id) || obj); const replaced = premisses.map(obj => replacementMap.get(obj.id) || obj);
console.log("Replaced", replaced); logger.info("Replaced", replaced);
return replaced; return replaced;
}, },
@ -660,7 +721,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
if (!toBeUpdated?.length) return; if (!toBeUpdated?.length) return;
console.log(toBeUpdated[0]); logger.info(toBeUpdated[0]);
const body = { const body = {
premise_ids: toBeUpdated.map(p => p.id), premise_ids: toBeUpdated.map(p => p.id),
@ -726,10 +787,28 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
this.selectedDestination = null; this.selectedDestination = null;
this.selectedLoading = false; this.selectedLoading = false;
}, },
setAll(value) {
this.selectedLoading = true;
const updatedPremises = this.premisses.map(p => ({
...p,
selected: value
}));
this.premisses = updatedPremises;
this.selectedLoading = false;
},
setSelectTo(ids, value) { setSelectTo(ids, value) {
this.selectedLoading = true; this.selectedLoading = true;
this.premisses.forEach(p => p.selected = ids.includes(p.id) ? value : p.selected); const idsSet = new Set(ids);
const updatedPremises = this.premisses.map(p => ({
...p,
selected: idsSet.has(p.id) ? value : p.selected
}));
this.premisses = updatedPremises;
this.selectedLoading = false; this.selectedLoading = false;
}, },
@ -773,7 +852,6 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
}, },
async loadPremissesForced(ids) { async loadPremissesForced(ids) {
this.loading = true; this.loading = true;
this.premises = []; this.premises = [];
@ -808,7 +886,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
} }
const request = {url: url, params: params}; const request = {url: url, params: params};
console.log("Request:", request); logger.info("Request:", request);
const response = await fetch(url, params const response = await fetch(url, params
).catch(e => { ).catch(e => {
@ -818,7 +896,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
trace: null trace: null
} }
console.error(error); logger.error(error);
const errorStore = useErrorStore(); const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request}); void errorStore.addError(error, {store: this, request: request});
@ -834,7 +912,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
trace: null trace: null
} }
console.error(error); logger.error(error);
const errorStore = useErrorStore(); const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request}); void errorStore.addError(error, {store: this, request: request});
throw e; throw e;
@ -848,7 +926,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
trace: data.error.trace trace: data.error.trace
} }
console.error(error); logger.error(error);
const errorStore = useErrorStore(); const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request}); void errorStore.addError(error, {store: this, request: request});
throw new Error('Internal backend error'); throw new Error('Internal backend error');
@ -862,7 +940,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
message: "Server returned wrong response code", message: "Server returned wrong response code",
trace: null trace: null
} }
console.error(error); logger.error(error);
const errorStore = useErrorStore(); const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request}); void errorStore.addError(error, {store: this, request: request});
throw new Error('Internal backend error'); throw new Error('Internal backend error');
@ -876,7 +954,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
trace: data.error.trace trace: data.error.trace
} }
console.error(error); logger.error(error);
const errorStore = useErrorStore(); const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request}); void errorStore.addError(error, {store: this, request: request});
throw new Error('Internal backend error'); throw new Error('Internal backend error');
@ -885,7 +963,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
} }
} }
console.log("Response:", data); logger.info("Response:", data);
return data; return data;
} }
} }

View file

@ -86,12 +86,12 @@ public class PropertyController {
* *
* @param isoCode The ISO code of the country. * @param isoCode The ISO code of the country.
* @param mappingId The external mapping ID for the property. * @param mappingId 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({"/country/{iso}/{external_mapping_id}", "/country/{iso}/{external_mapping_id}/"}) @PutMapping({"/country/{iso}/{external_mapping_id}", "/country/{iso}/{external_mapping_id}/"})
public ResponseEntity<Void> setCountryProperty(@PathVariable("iso") IsoCode isoCode, @PathVariable(name = "external_mapping_id") String mappingId, @RequestBody String value) { public ResponseEntity<Void> setCountryProperty(@PathVariable("iso") IsoCode isoCode, @PathVariable(name = "external_mapping_id") String mappingId, @RequestBody SetPropertyDTO dto) {
countryService.setProperties(isoCode, mappingId, value); countryService.setProperties(isoCode, mappingId, dto.getValue());
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }

View file

@ -2,6 +2,7 @@ package de.avatic.lcc.repositories.country;
import de.avatic.lcc.dto.generic.PropertyDTO; import de.avatic.lcc.dto.generic.PropertyDTO;
import de.avatic.lcc.model.properties.CountryPropertyMappingId; import de.avatic.lcc.model.properties.CountryPropertyMappingId;
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.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.RowMapper;
@ -28,6 +29,21 @@ public class CountryPropertyRepository {
public void setProperty(Integer setId, Integer countryId, String mappingId, String value) { public void setProperty(Integer setId, Integer countryId, String mappingId, String value) {
Integer typeId = getTypeIdByMappingId(mappingId); Integer typeId = getTypeIdByMappingId(mappingId);
String validValueQuery = """
SELECT cp.property_value
FROM country_property cp
JOIN property_set ps ON ps.id = cp.property_set_id
WHERE ps.state = ? AND cp.country_property_type_id = ? AND cp.country_id = ?""";
String validValue = jdbcTemplate.queryForObject(validValueQuery, String.class,
ValidityPeriodState.VALID.name(), typeId, countryId);
if (value.equals(validValue)) {
String deleteQuery = "DELETE FROM country_property WHERE property_set_id = ? AND country_property_type_id = ? AND country_id = ?";
jdbcTemplate.update(deleteQuery, setId, typeId, countryId);
return;
}
String query = """ String query = """
INSERT INTO country_property (property_value, country_id, country_property_type_id, property_set_id) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE property_value = ? INSERT INTO country_property (property_value, country_id, country_property_type_id, property_set_id) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE property_value = ?
"""; """;

View file

@ -227,7 +227,9 @@ public class CalculationExecutionService {
// Get container calculation // Get container calculation
for (var containerType : ContainerType.values()) { for (var containerType : ContainerType.values()) {
containerCalculation.put(containerType, containerCalculationService.doCalculation(hu, containerType)); containerCalculation.put(containerType, containerCalculationService.doCalculation(hu, containerType));
}
for (var containerType : ContainerType.values()) {
if (!containerType.equals(ContainerType.TRUCK)) { if (!containerType.equals(ContainerType.TRUCK)) {
var sectionInfo = new ArrayList<SectionInfo>(); var sectionInfo = new ArrayList<SectionInfo>();

View file

@ -12,8 +12,6 @@ INSERT INTO system_property_type ( name, external_mapping_id, data_type, validat
INSERT INTO system_property_type ( name, external_mapping_id, data_type, validation_rule) VALUES ( 'Payment terms [days]', 'PAYMENT_TERMS', 'INT', '{}'); INSERT INTO system_property_type ( name, external_mapping_id, data_type, validation_rule) VALUES ( 'Payment terms [days]', 'PAYMENT_TERMS', 'INT', '{}');
INSERT INTO system_property_type ( name, external_mapping_id, data_type, validation_rule) VALUES ( 'Annual working days', 'WORKDAYS', 'INT', '{"GT": 0, "LT": 366}'); INSERT INTO system_property_type ( name, external_mapping_id, data_type, validation_rule) VALUES ( 'Annual working days', 'WORKDAYS', 'INT', '{"GT": 0, "LT": 366}');
INSERT INTO system_property_type ( name, external_mapping_id, data_type, validation_rule) VALUES ( 'Interest rate inventory [%]', 'INTEREST_RATE', 'PERCENTAGE', '{"GTE": 0}'); INSERT INTO system_property_type ( name, external_mapping_id, data_type, validation_rule) VALUES ( 'Interest rate inventory [%]', 'INTEREST_RATE', 'PERCENTAGE', '{"GTE": 0}');
INSERT INTO system_property_type ( name, external_mapping_id, data_type, validation_rule) VALUES ( 'FCA fee [%]', 'FCA_FEE', 'PERCENTAGE', '{"GTE": 0}'); INSERT INTO system_property_type ( name, external_mapping_id, data_type, validation_rule) VALUES ( 'FCA fee [%]', 'FCA_FEE', 'PERCENTAGE', '{"GTE": 0}');
@ -325,6 +323,18 @@ VALUES (
(SELECT spt.id FROM system_property_type spt WHERE spt.external_mapping_id = 'FEU_LOAD'), (SELECT spt.id FROM system_property_type spt WHERE spt.external_mapping_id = 'FEU_LOAD'),
'21000' '21000'
); );
INSERT INTO system_property (property_set_id, system_property_type_id, property_value)
VALUES (
(SELECT ps.id FROM `property_set` ps
WHERE ps.state = 'VALID'
AND ps.start_date <= NOW()
AND (ps.end_date IS NULL OR ps.end_date > NOW())
ORDER BY ps.start_date DESC
LIMIT 1),
(SELECT spt.id FROM system_property_type spt WHERE spt.external_mapping_id = 'TRUCK_LOAD'),
'25000'
);
INSERT INTO system_property (property_set_id, system_property_type_id, property_value) INSERT INTO system_property (property_set_id, system_property_type_id, property_value)
VALUES ( VALUES (

View file

@ -560,8 +560,7 @@ CREATE TABLE IF NOT EXISTS calculation_job_route_section
FOREIGN KEY (calculation_job_destination_id) REFERENCES calculation_job_destination (id), FOREIGN KEY (calculation_job_destination_id) REFERENCES calculation_job_destination (id),
INDEX idx_premise_route_section_id (premise_route_section_id), INDEX idx_premise_route_section_id (premise_route_section_id),
INDEX idx_calculation_job_destination_id (calculation_job_destination_id), INDEX idx_calculation_job_destination_id (calculation_job_destination_id),
CONSTRAINT chk_stacked CHECK (is_unmixed_price IS TRUE OR is_stacked IS TRUE), -- only unmixed transports can be unstacked CONSTRAINT chk_stacked CHECK (is_unmixed_price IS FALSE OR is_stacked IS TRUE) -- only unmixed transports can be unstacked
CONSTRAINT chk_cbm_weight_price CHECK (is_unmixed_price IS FALSE OR
(is_cbm_price IS FALSE AND is_weight_price IS FALSE))
); );