Working on MassEdit and SingleEdit

This commit is contained in:
Jan 2025-08-28 21:55:02 +02:00
parent 0061d01b75
commit f5c4e1159f
28 changed files with 604 additions and 289 deletions

View file

@ -17,7 +17,6 @@
weight="regular"
size="24"
class="icon-btn"
:class="toast__icon_sizing"
/>
</div>
<div class="toast__content">

View file

@ -10,23 +10,24 @@
{{ premise.material.name }}
</div>
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="premise.hs_code">
<PhBarcode/>
HS Code:
{{ premise.material.hs_code }}
</div>
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="premise.tariff_rate">
<PhPercent/>
{{ premise.tariff_rate }}
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="premise.tariff_rate && premise.tariff_rate > 0">
Tariff rate:
{{ toPercent(premise.tariff_rate) }} %
</div>
</div>
<div class="edit-calculation-cell--price" :class="copyModeClass" v-if="showPrice"
@click="action('price')">
<div class="edit-calculation-cell-line">{{ premise.material_cost }} EUR</div>
<div class="edit-calculation-cell-line edit-calculation-cell-subline" >
<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">
<basic-badge icon="plus" variant="primary">FCA FEE</basic-badge>
</div>
</div>
<div class="edit-calculation-empty" :class="copyModeClass" v-else @click="action('price')">
<basic-badge variant="exception" icon="warning">MISSING</basic-badge>
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
</div>
<div v-if="showHu" class="edit-calculation-cell edit-calculation-cell--packaging" :class="copyModeClass"
@click="action('packaging')">
@ -36,11 +37,11 @@
{{ premise.handling_unit.width }} x
{{ premise.handling_unit.height }} {{ premise.handling_unit.dimension_unit }}
</div>
<div class="edit-calculation-cell-line">
<div class="edit-calculation-cell-line edit-calculation-cell-subline">
<PhBarbell/>
<span>{{ premise.handling_unit.weight }} {{ premise.handling_unit.weight_unit }}</span>
</div>
<div class="edit-calculation-cell-line">
<div class="edit-calculation-cell-line edit-calculation-cell-subline">
<PhHash/>
{{ premise.handling_unit.content_unit_count }} pcs.
</div>
@ -51,7 +52,7 @@
</div>
<div class="edit-calculation-empty" :class="copyModeClass" v-else
@click="action('packaging')">
<basic-badge variant="exception" icon="warning">MISSING</basic-badge>
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
</div>
<div class="edit-calculation-cell--supplier" :class="copyModeClass"
@click="action('supplier')">
@ -77,7 +78,7 @@
</div>
<div class="edit-calculation-empty" :class="copyModeClass" v-else
@click="action('destinations')">
<basic-badge variant="exception" icon="warning">MISSING</basic-badge>
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
</div>
<div class="edit-calculation-actions-cell">
@ -178,7 +179,9 @@ export default {
}
},
methods: {
toPercent(value) {
return value !== null ? (value * 100).toFixed(2) : '0.00';
},
updateSelected(value) {
this.premiseEditStore.setSelectTo([this.id], value);
},
@ -214,10 +217,6 @@ export default {
overflow: hidden;
}
/*.bulk-edit-row:hover {
// background-color: rgba(107, 134, 156, 0.05);
//} */
.bulk-edit-row:last-child {
border-bottom: none;
}
@ -249,11 +248,6 @@ export default {
.edit-calculation-cell--copy-mode:hover {
cursor: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOCIgaGVpZ2h0PSIyOCIgdmlld0JveD0iMCAwIDI1NiAyNTYiPgogIDxyZWN0IHg9Ijg0IiB5PSIzMiIgd2lkdGg9IjEzNiIgaGVpZ2h0PSIxMzYiIGZpbGw9IndoaXRlIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iOCIgcng9IjQiLz4KICA8cmVjdCB4PSIzNiIgeT0iODQiIHdpZHRoPSIxMzYiIGhlaWdodD0iMTM2IiBmaWxsPSJ3aGl0ZSIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjgiIHJ4PSI0Ii8+Cjwvc3ZnPg==") 12 12, pointer;
/*cursor: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0iI2ZmZmZmZiIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjQiIHZpZXdCb3g9IjAgMCAyNTYgMjU2Ij4KICA8cGF0aCBkPSJNMjAwLDMySDE2My43NGE0Ny45Miw0Ny45MiwwLDAsMC03MS40OCwwSDU2QTE2LDE2LDAsMCwwLDQwLDQ4VjIxNmExNiwxNiwwLDAsMCwxNiwxNkgyMDBhMTYsMTYsMCwwLDAsMTYtMTZWNDhBMTYsMTYsMCwwLDAsMjAwLDMyWm0tNzIsMGEzMiwzMiwwLDAsMSwzMiwzMkg5NkEzMiwzMiwwLDAsMSwxMjgsMzJaIj48L3BhdGg+Cjwvc3ZnPg==") 12 12, pointer;
*/
background-color: rgba(107, 134, 156, 0.05);
border-radius: 0.8rem;
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);

View file

@ -93,12 +93,10 @@ export default {
},
async addDestination(node) {
console.log(node)
// todo add to massEdit copy only
const [id] = await this.premiseEditStore.addDestination(node);
this.editDestination(id);
},
deleteDestination(id) {
// todo delete from to massEdit copy only
this.premiseEditStore.deleteDestination(id);
},
editDestination(id) {

View file

@ -28,14 +28,14 @@
<div class="input-column">
<div class="hs-code-container">
<autosuggest-searchbar :fetch-suggestions="fetchHsCode" :initial-value="hsCode"
placeholder="Find hs code"></autosuggest-searchbar>
placeholder="Find hs code" no-results-text="Not found."></autosuggest-searchbar>
<icon-button icon="ArrowCounterClockwise"></icon-button>
</div>
<div class="caption-column">Tariff rate [%]</div>
<div class="input-field-container input-field-tariffrate">
<input ref="tariffRateInput" :value="tariffRatePercent" @blur="validateInput('tariffRate',$event)"
<input ref="tariffRateInput" :value="tariffRatePercent" @blur="validateTariffRate"
class="input-field"
autocomplete="off"/>
</div>
@ -59,6 +59,7 @@ import {mapStores} from "pinia";
import {parseNumberFromString} from "@/common.js";
import Modal from "@/components/UI/Modal.vue";
import SelectMaterial from "@/components/layout/material/SelectMaterial.vue";
import {useCustomsStore} from "@/store/customs.js";
export default {
name: "MaterialEdit",
@ -66,7 +67,7 @@ export default {
SelectMaterial,
Modal, PhArrowCounterClockwise, ModalDialog, AutosuggestSearchbar, InputField, Flag, IconButton
},
emits: ["update:tariffRate", "updateMaterial", "update:partNumber", "update:hsCode"],
emits: ["update:tariffRate", "updateMaterial", "update:partNumber", "update:hsCode", "save"],
props: {
description: {
type: String,
@ -86,7 +87,7 @@ export default {
},
},
computed: {
...mapStores(useMaterialStore),
...mapStores(useMaterialStore, useCustomsStore),
tariffRatePercent() {
return this.tariffRate ? (this.tariffRate * 100).toFixed(2) : '';
}
@ -119,25 +120,25 @@ export default {
}
});
},
validateInput(type, event) {
const decimals = 2
const parsed = parseNumberFromString(event.target.value, decimals);
console.log('validateInput', type, event.target.value, parsed);
validateTariffRate(event) {
const percentValue = parseNumberFromString(event.target.value, 4);
this.$emit(`update:${type}`, parsed);
const validatedPercent = Math.max(0, Math.min(999.99, percentValue));
const validatedDecimal = validatedPercent / 100;
// Force update the input field with the correctly formatted value
const formattedValue = parsed.toFixed(decimals);
const inputRef = `${type}Input`;
this.updateInputValue(inputRef, formattedValue);
if (validatedDecimal !== this.tariffRate) {
this.$emit('update:tariffRate', validatedDecimal);
}
event.target.value = validatedPercent.toFixed(2);
},
activateEditMode() {
this.modalSelectMaterial = true;
},
async fetchHsCode(query) {
const hsCodeQuery = {searchTerm: query};
await this.customs.setQuery(hsCodeQuery);
return this.customs.hsCodes;
await this.customsStore.setQuery(hsCodeQuery);
return this.customsStore.hsCodes;
}
}
}

View file

@ -34,7 +34,7 @@ import {parseNumberFromString} from "@/common.js";
export default {
name: "PriceEdit",
components: {Tooltip, Checkbox},
emits: ['update:price', 'update:overSeaShare', 'update:includeFcaFee'],
emits: ['update:price', 'update:overSeaShare', 'update:includeFcaFee', 'save'],
props: {
price: {
required: true,

View file

@ -65,7 +65,7 @@ export default {
flex-direction: column;
gap: 1.6rem;
flex: 1 0 min(60vw, 120rem);
height: min(70vh, 50rem);
height: min(60vh, 120rem);
min-height: 0; /* Critical: allows flex child to shrink below content size */
}

View file

@ -102,13 +102,13 @@ export default {
return `Annual quantity that "${this.destination.destination_node.name}" will source from the supplier`
},
showMassEditWarning() {
return (this.destination.massEdit ?? false);
return (this.destination.routes ?? null) === null;
},
showRoutes() {
return !this.destination.is_d2d && this.destination.routes?.length > 0 | false;
return !this.destination.is_d2d && (this.destination.routes?.length > 0);
},
showRouteWarning() {
return !this.destination.is_d2d && this.destination.routes.length === 0;
return !this.destination.is_d2d && this.destination.routes?.length === 0;
},
calculationModel: {
get() {

View file

@ -22,7 +22,7 @@
<div class="select-material-input-column" v-if="materialSelected">{{ hsCode }}</div>
<div class="select-material-checkbox" v-if="materialSelected">
<tooltip
text="Your current master data (like packaging dimensions) might be outdated after material change. Tick to reload master data">
text="Tick to reload master data from database (if present) and overwrite current values.">
<checkbox :checked="updateMasterData" @checkbox-changed="checkboxChanged">update master data</checkbox>
</tooltip>
</div>

View file

@ -1,3 +1,5 @@
<!--TODO: isMassEdit-->
<template>
<div class="edit-calculation-container">
<div class="header-container">
@ -47,7 +49,7 @@
</transition>
<modal :z-index="3000" :state="showProcessingModal" @close="closeEditModal">
<modal :z-index="3000" :state="showProcessingModal">
<div class="edit-calculation-spinner-container space-around">
<spinner></spinner>
<span>{{ processingMessage }}</span>
@ -58,11 +60,32 @@
:select-count="selectCount"></mass-edit-dialog>
<modal :z-index="2000" :state="showEditModal" @close="closeEditModal">
<modal :z-index="2000" :state="showEditModal">
<div class="modal-content-container">
<component
:is="componentType"
v-bind="componentProps"
v-model:partNumber="componentProps.partNumber"
v-model:hsCode="componentProps.hsCode"
v-model:tariffRate="componentProps.tariffRate"
v-model:description="componentProps.description"
v-model:price="componentProps.price"
v-model:overSeaShare="componentProps.overSeaShare"
v-model:includeFcaFee="componentProps.includeFcaFee"
v-model:length="componentProps.length"
v-model:width="componentProps.width"
v-model:height="componentProps.height"
v-model:weight="componentProps.weight"
v-model:weightUnit="componentProps.weightUnit"
v-model:dimensionUnit="componentProps.dimensionUnit"
v-model:unitCount="componentProps.unitCount"
v-model:mixable="componentProps.mixable"
v-model:stackable="componentProps.stackable"
v-model:supplierName="componentProps.supplierName"
v-model:supplierAddress="componentProps.supplierAddress"
v-model:supplierCoordinates="componentProps.supplierCoordinates"
v-model:isoCode="componentProps.isoCode"
@update-material="updateMaterial"
>
</component>
<div class="modal-content-footer">
@ -128,7 +151,7 @@ export default {
return this.premiseEditStore.isLoading ? false : this.premiseEditStore.getPremisses?.every(p => p.selected === true) ?? false;
},
showMultiselectAction() {
return this.premiseEditStore.getPremisses?.some(p => p.selected === true) ?? false;
return this.selectCount > 0;
},
showEditModal() {
return ((this.modalType ?? null) !== null);
@ -183,10 +206,20 @@ export default {
destinations: {props: {}},
},
editIds: null,
dataSourceId: null,
processingMessage: "Please wait. Calculating routes ...",
}
},
methods: {
async updateMaterial(id, action) {
console.log(id, action);
await this.premiseEditStore.setMaterial(id, action === 'updateMasterData', this.editIds);
if (this.dataSourceId !== null) {
this.fillData("material", this.dataSourceId);
}
},
updateCheckBoxes(value) {
this.premiseEditStore.setSelectTo(this.ids, value);
},
@ -194,94 +227,91 @@ export default {
this.openModal(action, this.selectedPremisses.map(p => p.id));
},
onClickAction(data) {
if (0 !== this.selectCount) {
this.openModal(data.action, this.premiseEditStore.getSelectedPremissesIds, data.id);
} else {
this.openModal(data.action, [data.id], data.id, true);
}
const massEdit = 0 !== this.selectCount
this.openModal(data.action, massEdit ? this.premiseEditStore.getSelectedPremissesIds : [data.id], data.id, massEdit);
},
openModal(type, ids, dataSource = -1, noMassEdit = false) {
openModal(type, ids, dataSource = -1, massEdit = true) {
if (type !== 'destinations')
this.fillData(type, dataSource)
else {
if (noMassEdit)
this.premiseEditStore.selectPremise(dataSource);
else
this.premiseEditStore.prepareMassEdit(dataSource, ids);
console.log(ids, dataSource, massEdit)
this.premiseEditStore.prepareDestinations(dataSource, ids, massEdit, true);
}
this.dataSourceId = dataSource !== -1 ? dataSource : null;
this.editIds = ids;
this.modalType = type;
},
closeEditModalAction(action) {
if (this.modalType === "destinations") {
if(this.premiseEditStore.isMassEdit) {
if (action === "accept") {
this.premiseEditStore.finishMassEdit();
} else {
console.log("cancel mass edit")
this.premiseEditStore.cancelMassEdit();
}
if (action === "accept") {
this.premiseEditStore.executeDestinationsMassEdit();
} else {
//TODO save
this.premiseEditStore.deselectPremise();
console.log("cancel mass edit")
this.premiseEditStore.cancelMassEdit();
}
} else {
if (action === "accept") {
if (this.modalType === "price") {
this.selectedPremisses.forEach(p => {
p.material_cost = componentsData[this.modalType].props.price;
p.oversea_share = componentsData[this.modalType].props.overSeaShare;
p.is_fca_enabled = componentsData[this.modalType].props.includeFcaFee;
})
this.editIds.forEach(id => {
const p = this.premiseEditStore.getById(id);
p.material_cost = this.componentsData[this.modalType].props.price;
p.oversea_share = this.componentsData[this.modalType].props.overSeaShare;
p.is_fca_enabled = this.componentsData[this.modalType].props.includeFcaFee;
});
this.premiseEditStore.savePrice(this.editIds);
} else if (this.modalType === "material") {
this.selectedPremisses.forEach(p => {
p.material.part_number = componentsData[this.modalType].props.partNumber;
p.material.hs_code = componentsData[this.modalType].props.hsCode;
p.tariff_rate = componentsData[this.modalType].props.tariffRate;
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.selectedPremisses.forEach(p => {
p.handling_unit.weight = componentsData[this.modalType].props.weight;
p.handling_unit.height = componentsData[this.modalType].props.height;
p.handling_unit.length = componentsData[this.modalType].props.length;
p.handling_unit.height = componentsData[this.modalType].props.height;
p.handling_unit.weight_unit = componentsData[this.modalType].props.weightUnit;
p.handling_unit.dimension_unit = componentsData[this.modalType].props.dimensionUnit;
p.handling_unit.content_unit_count = componentsData[this.modalType].props.unitCount;
this.editIds.forEach(id => {
const p = this.premiseEditStore.getById(id);
p.handling_unit.weight = this.componentsData[this.modalType].props.weight;
p.handling_unit.height = this.componentsData[this.modalType].props.height;
p.handling_unit.length = this.componentsData[this.modalType].props.length;
p.handling_unit.height = this.componentsData[this.modalType].props.height;
p.is_stackable = componentsData[this.modalType].props.is_stackable;
p.is_mixable = componentsData[this.modalType].props.is_mixable;
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);
} else if (this.modalType === "supplier") {
//set supplier.
}
}
}
// clear data.
this.fillData(this.modalType);
this.closeEditModal();
},
closeEditModal() {
if (this.modalType === "destinations") {
if(!this.premiseEditStore.isMassEdit) {
this.premiseEditStore.deselectPremise();
}
this.premiseEditStore.cancelMassEdit();
}
this.modalType = null;
},
fillData(type, id = -1) {
console.log("fillData", type, id);
if (id === -1) {
// clear
this.componentsData = {
@ -332,8 +362,8 @@ export default {
width: premise.handling_unit.width ?? 0,
height: premise.handling_unit.height ?? 0,
weight: premise.handling_unit.weight ?? 0,
weightUnit: premise.handling_unit.weightUnit ?? "KG",
dimensionUnit: premise.handling_unit.dimensionUnit ?? "MM",
weightUnit: premise.handling_unit.weight_unit ?? "KG",
dimensionUnit: premise.handling_unit.dimension_unit ?? "MM",
unitCount: premise.handling_unit.content_unit_count ?? 1,
mixable: premise.is_mixable ?? true,
stackable: premise.is_stackable ?? true

View file

@ -30,7 +30,8 @@
:supplier-name="premise.supplier.name"
:supplier-coordinates="premise.supplier.location"
:iso-code="premise.supplier.country.iso_code"
@update-supplier="updateSupplier"></supplier-view>
@update-supplier="updateSupplier"
></supplier-view>
</box>
<box class="master-data-item master-data-stretched-item master-data-packaging">
<packaging-edit v-model:length="premise.handling_unit.length"
@ -50,11 +51,14 @@
:id="premise.material.id"
v-model:hs-code="premise.hs_code"
v-model:tariff-rate="premise.tariff_rate"
@update-material="updateMaterial"></material-edit>
@update-material="updateMaterial"
@save="save"></material-edit>
</box>
<box class="master-data-item">
<price-edit v-model:include-fca-fee="premise.is_fca_enabled" v-model:over-sea-share="premise.oversea_share"
v-model:price="premise.material_cost"></price-edit>
<price-edit v-model:include-fca-fee="premise.is_fca_enabled"
v-model:over-sea-share="premise.oversea_share"
v-model:price="premise.material_cost"
@save="save"></price-edit>
</box>
</div>
@ -129,17 +133,29 @@ export default {
this.$router.push({name: 'home'});
}
},
save(type) {
console.log(type);
},
showCustomToast() {
this.$refs.toast.addToast({
icon: 'floppy-disk',
message: 'Changes saved.',
title: 'Success',
variant: 'primary',
duration: 3000
})
async save(type) {
let success = false;
if(type === 'price') {
success = await this.premiseEditStore.savePrice();
} else if(type === 'material') {
success = await this.premiseEditStore.saveMaterial();
} else if(type === 'packaging') {
success = await this.premiseEditStore.savePackaging();
}
console.log("save success: ", success);
if(success) {
this.$refs.toast.addToast({
icon: 'floppy-disk',
message: `Changes on ${type} saved.`,
variant: 'primary',
duration: 3000
})
}
},
updateMaterial(id, action) {
console.log(id, action);
@ -153,11 +169,11 @@ export default {
}
},
created() {
this.id = new UrlSafeBase64().decodeIds(this.$route.params.id);
[this.id] = new UrlSafeBase64().decodeIds(this.$route.params.id);
if(this.$route.params.ids) {
this.bulkEditQuery = this.$route.params.ids;
this.premiseEditStore.selectPremises(this.id, new UrlSafeBase64().decodeIds(this.$route.params.ids));
this.premiseEditStore.selectSinglePremise(this.id, new UrlSafeBase64().decodeIds(this.$route.params.ids));
} else {
this.premiseEditStore.loadAndSelectSinglePremise(this.id)
}

View file

@ -19,8 +19,8 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
selectedLoading: false,
massEditDestinations: null,
processMassEdit: false,
destinations: null,
processDestinationMassEdit: false,
selectedDestination: null,
@ -149,7 +149,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
},
showProcessingModal(state) {
return state.processMassEdit;
return state.processDestinationMassEdit;
},
/**
@ -223,11 +223,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
*/
getDestinationsView(state) {
if (state.massEditDestinations !== null) {
return state.massEditDestinations.destinations;
}
return state.singleSelectedPremise?.destinations ?? [];
return state.destinations?.destinations ?? [];
},
getDestinationById(state) {
@ -248,14 +244,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
getSelectedDestinationsData: (state) => state.selectedDestination,
/**
* Mass editing stuff
* ===============================
*/
isMassEdit(state) {
return state.massEditDestinations !== null;
}
},
actions: {
@ -264,73 +253,118 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
* =================
*/
prepareMassEdit(dataSourcePremiseId, editedPremiseIds) {
prepareDestinations(dataSourcePremiseId, editedPremiseIds, massEdit = false, fromMassEditView = false) {
if (this.premisses === null) return;
if (!editedPremiseIds || !dataSourcePremiseId || editedPremiseIds.length === 0) return;
this.massEditDestinations = {
this.destinations = {
premise_ids: editedPremiseIds,
destinations: this.getById(dataSourcePremiseId)?.destinations.map(d => this.copyAllButRouting(d)) ?? [],
toBeAdded: null,
toBeRemoved: null,
massEdit: massEdit,
fromMassEditView: fromMassEditView,
destinations: this.premisses.find(p => String(p.id) === String(dataSourcePremiseId))?.destinations.map(d => this.copyAllFromPremises(d, !massEdit)) ?? [],
};
this.selectedDestination = null;
},
async finishMassEdit() {
this.processMassEdit = true;
async executeDestinationsMassEdit() {
const destinations = [];
if (!this.destinations.massEdit) {
this.massEditDestinations.destinations.forEach(d => {
const dest = {
destination_node_id: d.destination_node.id,
annual_amount: d.annual_amount,
disposal_costs: d.userDefinedHandlingCosts ? d.disposal_costs : null,
repackaging_costs: d.userDefinedHandlingCosts ? d.repackaging_costs : null,
handling_costs: d.userDefinedHandlingCosts ? d.handling_costs : null,
this.destinations.premise_ids.forEach(premiseId => {
const toPremise = this.getById(premiseId);
this.destinations.destinations.forEach(fromDest => {
const toDest = toPremise.destinations.find(to => fromDest.id.substring(1) === String(to.id));
if ((toDest ?? null) === null) {
throw new Error("Destination not found in premise: " + premiseId + " -> " + d.id);
}
this.copyAllToPremise(fromDest, toDest);
const body = {
annual_amount: toDest.annual_amount,
repackaging_costs: toDest.repackaging_costs,
handling_costs: toDest.handling_costs,
disposal_costs: toDest.disposal_costs,
is_d2d: toDest.is_d2d,
rate_d2d: toDest.rate_d2d,
route_selected_id: toDest.routes.find(r => r.is_selected)?.id ?? null,
};
const url = `${config.backendUrl}/calculation/destination/${toDest.id}`;
this.performRequest('PUT', url, body, false);
});
});
} else {
this.processDestinationMassEdit = true;
const destinations = [];
this.destinations.destinations.forEach(d => {
const dest = {
destination_node_id: d.destination_node.id,
annual_amount: d.annual_amount,
disposal_costs: d.userDefinedHandlingCosts ? d.disposal_costs : null,
repackaging_costs: d.userDefinedHandlingCosts ? d.repackaging_costs : null,
handling_costs: d.userDefinedHandlingCosts ? d.handling_costs : null,
}
destinations.push(dest);
})
const body = {destinations: destinations, premise_id: this.destinations.premise_ids};
const url = `${config.backendUrl}/calculation/destination/`;
const data = await this.performRequest('PUT', url, body).catch(e => {
this.destinations = null;
this.processDestinationMassEdit = false;
})
if (data) {
for (const id of Object.keys(data)) {
this.premisses.find(p => String(p.id) === id).destinations = data[id];
}
}
destinations.push(dest);
})
const body = {destinations: destinations, premise_id: this.massEditDestinations.premise_ids};
const url = `${config.backendUrl}/calculation/destination/`;
const data = await this.performRequest('PUT', url, body).catch(e => {
this.massEditDestinations = null;
this.processMassEdit = false;
})
if (data) {
for (const id of Object.keys(data)) {
this.premisses.find(p => String(p.id) === id).destinations = data[id];
}
this.destinations = null;
this.processDestinationMassEdit = false;
}
this.massEditDestinations = null;
this.processMassEdit = false;
},
cancelMassEdit() {
this.massEditDestinations = null;
this.destinations = null;
},
copyAllButRouting(from, to = null) {
const fromIsOrig = (to === null);
copyAllFromPremises(from, fullCopy = true) {
const d = {};
d.id = `e${from.id}`;
d.destination_node = structuredClone(toRaw(from.destination_node));
d.routes = fullCopy ? structuredClone(toRaw(from.routes)) : null;
d.annual_amount = from.annual_amount;
d.is_d2d = from.is_d2d;
d.rate_d2d = from.is_d2d ? from.rate_d2d : null;
d.handling_costs = from.handling_costs;
d.disposal_costs = from.disposal_costs;
d.repackaging_costs = from.repackaging_costs;
d.userDefinedHandlingCosts = from.handling_costs !== null || from.disposal_costs !== null || from.repackaging_costs !== null;
return d;
},
copyAllToPremise(from, to, fullCopy = true) {
const d = to ?? {};
if (fromIsOrig) {
d.id = `e${from.id}`;
d.destination_node = structuredClone(toRaw(from.destination_node));
d.massEdit = true;
}
d.annual_amount = from.annual_amount;
d.is_d2d = from.is_d2d;
d.rate_d2d = from.is_d2d ? from.rate_d2d : null;
if (fromIsOrig || from.userDefinedHandlingCosts) {
if (from.userDefinedHandlingCosts) {
d.disposal_costs = from.disposal_costs;
d.repackaging_costs = from.repackaging_costs;
d.handling_costs = from.handling_costs;
@ -340,6 +374,10 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
d.handling_costs = null;
}
if (fullCopy && (from.routes ?? null) !== null) {
to.routes.forEach(route => route.is_selected = from.routes.find(r => r.id === route.id)?.is_selected ?? false);
}
return d;
},
@ -351,10 +389,12 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
selectDestination(id) {
if (this.premisses === null) return;
const destination = !this.isMassEdit ? this.getDestinationById(id) : this.massEditDestinations.destinations.find(d => d.id === id);
console.log("selectDestination:", id)
const dest = this.destinations.destinations.find(d => d.id === id);
if ((destination ?? null) == null) {
if ((dest ?? null) == null) {
const error = {
code: 'Frontend error.',
message: `Destination not found: ${id}. Please contact support.`,
@ -363,54 +403,79 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
throw new Error("Internal frontend error: Destination not found: " + id);
}
const data = structuredClone(toRaw(destination));
data.userDefinedHandlingCosts = data.handling_costs !== null || data.disposal_costs !== null || data.repackaging_costs !== null;
this.selectedDestination = data;
this.selectedDestination = structuredClone(toRaw(dest));
},
deselectDestinations(save = false) {
async deselectDestinations(save = false) {
if (this.premisses === null) return;
if (save) {
const orig = this.isMassEdit ?
this.massEditDestinations.destinations.find(d => d.id === this.selectedDestination.id) :
this.getDestinationById(this.selectedDestination.id)
const idx = this.destinations.destinations.findIndex(d => d.id === this.selectedDestination.id);
this.destinations.destinations.splice(idx, 1, this.selectedDestination);
this.copyAllButRouting(this.selectedDestination, orig);
if (!this.destinations.fromMassEditView) {
//TODO write trough backend if no massEdit
const toDest = this.singleSelectedPremise.destinations.find(to => this.selectedDestination.id.substring(1) === String(to.id));
this.copyAllToPremise(this.selectedDestination, toDest);
const body = {
annual_amount: toDest.annual_amount,
repackaging_costs: toDest.repackaging_costs,
handling_costs: toDest.handling_costs,
disposal_costs: toDest.disposal_costs,
is_d2d: toDest.is_d2d,
rate_d2d: toDest.rate_d2d,
route_selected_id: toDest.routes.find(r => r.is_selected)?.id ?? null,
};
console.log(body)
const url = `${config.backendUrl}/calculation/destination/${toDest.id}`;
await this.performRequest('PUT', url, body, false);
if (!this.isMassEdit) {
this.selectedDestination.routes.forEach(route => {
const origRoute = orig.routes.find(r => r.id === route.id);
origRoute.is_selected = route.is_selected;
});
}
}
this.selectedDestination = null;
},
async deleteDestination(id) {
if (this.isMassEdit) {
const idx = this.massEditDestinations.destinations.findIndex(d => d.id === id);
/*
* 1. delete from destinations copy
*/
const idx = this.destinations.destinations.findIndex(d => d.id === id);
if (idx === -1) {
console.info("Destination not found in mass edit: , id)");
return;
}
if (idx === -1) {
console.info("Destination not found in mass edit: , id)");
return;
}
this.massEditDestinations.destinations.splice(idx, 1);
} else {
this.destinations.destinations.splice(idx, 1);
/*
* 2. delete from backend if not mass edit
*/
if (!this.destinations.massEdit && id.startsWith('e')) { /* 'v'-ids cannot be deleted because they only exist in the frontend */
if (this.premisses === null) return;
const url = `${config.backendUrl}/calculation/destination/${id}`;
const origId = id.substring(1);
const url = `${config.backendUrl}/calculation/destination/${origId}`;
await this.performRequest('DELETE', url, null, false).catch(async e => {
console.error("Unable to delete destination: " + id + "");
console.error("Unable to delete destination: " + origId + "");
console.error(e);
await this.loadPremissesIfNeeded(this.premisses.map(p => p.id));
});
for (const p of this.premisses) {
const toBeDeleted = p.destinations.findIndex(d => d.id === id)
const toBeDeleted = p.destinations.findIndex(d => String(d.id) === String(origId))
console.log(toBeDeleted)
if (toBeDeleted !== -1) {
p.destinations.splice(toBeDeleted, 1)
break;
@ -420,9 +485,9 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
},
async addDestination(node) {
if (this.isMassEdit) {
if (this.destinations.massEdit) {
const existing = this.massEditDestinations.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)
if ((existing ?? null) !== null) {
@ -443,16 +508,17 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
userDefinedHandlingCosts: false,
};
this.massEditDestinations.destinations.push(destination);
this.destinations.destinations.push(destination);
return [destination.id];
} else {
const id = node.id;
const toBeUpdated = this.premisses?.filter(p => p.selected).map(p => p.id);
if (toBeUpdated === null) return;
const toBeUpdated = this.destinations.fromMassEditView ? this.destinations.premise_ids : this.premisses?.filter(p => p.selected).map(p => p.id);
if (toBeUpdated === null || toBeUpdated.length === 0) return;
const body = {destination_node_id: id, premise_id: toBeUpdated};
const url = `${config.backendUrl}/calculation/destination/`;
@ -464,11 +530,17 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
throw e;
});
const mappedIds = []
for (const id of Object.keys(destinations)) {
this.premisses.find(p => String(p.id) === id).destinations.push(destinations[id]);
const premise = this.premisses.find(p => String(p.id) === id)
premise.destinations.push(destinations[id]);
const mappedDestination = this.copyAllFromPremises(destinations[id], true);
mappedIds.push(mappedDestination.id);
this.destinations.destinations.push(mappedDestination);
}
return Object.values(destinations).map(d => d.id);
return mappedIds;
}
},
@ -478,21 +550,29 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
* (these are more extensive changes. The edited premises are replaced by the one returned by the backend)
*/
async setSupplier(id, updateMasterData) {
async setSupplier(id, updateMasterData, ids = null) {
console.log("setSupplier");
const selectedId = this.singleSelectId;
const body = {supplier_node_id: id, update_master_data: updateMasterData};
const url = `${config.backendUrl}/calculation/supplier/`;
await this.setData(url, body);
if (selectedId != null && this.destinations && !this.destinations.fromMassEditView) {
this.prepareDestinations(selectedId, [selectedId]);
}
},
async setMaterial(id, updateMasterData) {
async setMaterial(id, updateMasterData, ids = null) {
console.log("setMaterial");
const body = {material_id: id, update_master_data: updateMasterData};
const url = `${config.backendUrl}/calculation/material/`;
await this.setData(url, body);
await this.setData(url, body, ids);
},
async setData(url, body) {
async setData(url, body, ids = null) {
const toBeUpdated = this.premisses ? this.premisses.filter(p => p.selected).map(p => p.id) : null;
const toBeUpdated = this.premisses ? (ids ? (ids) : (this.premisses.filter(p => p.selected).map(p => p.id))) : null;
if (null !== toBeUpdated) {
this.selectedLoading = true;
@ -529,6 +609,88 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
return replaced;
},
/**
* Save
*/
async savePrice(ids = null) {
let success = true;
const toBeUpdated = this.premisses ? (ids ? (ids.map(id => this.premisses.find(p => String(p.id) === String(id)))) : (this.premisses.filter(p => p.selected))) : null;
if (!toBeUpdated?.length) return;
const body = {
premise_ids: toBeUpdated.map(p => p.id),
material_cost: toBeUpdated[0].material_cost,
oversea_share: toBeUpdated[0].oversea_share,
is_fca_enabled: toBeUpdated[0].is_fca_enabled
};
await this.performRequest('POST', `${config.backendUrl}/calculation/price/`, body, false).catch(e => {
success = false;
})
return success;
},
async savePackaging(ids = null) {
let success = true;
const toBeUpdated = this.premisses ? (ids ? (ids.map(id => this.premisses.find(p => String(p.id) === String(id)))) : (this.premisses.filter(p => p.selected))) : null;
if (!toBeUpdated?.length) return;
console.log(toBeUpdated[0]);
const body = {
premise_ids: toBeUpdated.map(p => p.id),
handling_unit: {
weight: toBeUpdated[0].handling_unit.weight,
weight_unit: toBeUpdated[0].handling_unit.weight_unit,
length: toBeUpdated[0].handling_unit.length,
width: toBeUpdated[0].handling_unit.width,
height: toBeUpdated[0].handling_unit.height,
dimension_unit: toBeUpdated[0].handling_unit.dimension_unit,
content_unit_count: toBeUpdated[0].handling_unit.content_unit_count,
},
is_mixable: toBeUpdated[0].is_mixable,
is_stackable: toBeUpdated[0].is_stackable,
};
await this.performRequest('POST', `${config.backendUrl}/calculation/packaging/`, body, false).catch(() => {
success = false;
})
return success;
},
async saveMaterial(ids = null) {
let success = true;
const toBeUpdated = this.premisses ? (ids ? (ids.map(id => this.premisses.find(p => String(p.id) === String(id)))) : (this.premisses.filter(p => p.selected))) : null;
if (!toBeUpdated?.length) return;
const body = {
premise_ids: toBeUpdated.map(p => p.id),
hs_code: toBeUpdated[0].hs_code,
tariff_rate: toBeUpdated[0].tariff_rate,
};
await this.performRequest('POST', `${config.backendUrl}/calculation/material/`, body, false).catch(() => {
success = false;
})
return success;
},
/**
* PREMISE stuff
@ -536,24 +698,29 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
*/
deselectPremise() {
this.selectedLoading = true;
this.premisses.forEach(p => p.selected = false);
this.selectedLoading = false;
},
selectPremise(id) {
this.selectedLoading = true;
this.premisses.forEach(p => p.selected = p.id === id);
this.destinations = null;
this.selectedDestination = null;
this.selectedLoading = false;
},
setSelectTo(ids, value) {
this.selectedLoading = true;
this.premisses.forEach(p => p.selected = ids.includes(p.id) ? value : p.selected);
this.selectedLoading = false;
},
async selectPremises(id, ids) {
async selectSinglePremise(id, ids) {
this.selectedLoading = true;
await this.loadPremissesIfNeeded(ids);
const toSelect = String(id);
this.premisses.forEach(p => p.selected = toSelect.includes(String(p.id)));
this.premisses.forEach(p => p.selected = String(id) === String(p.id));
this.prepareDestinations(id, [id]);
this.selectedLoading = false;
},
async loadAndSelectSinglePremise(id) {
@ -563,7 +730,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
this.premises = [];
const params = new URLSearchParams();
params.append('premissIds', `${id}`);
params.append('premissIds', `${[id]}`);
const url = `${config.backendUrl}/calculation/edit/${params.size === 0 ? '' : '?'}${params.toString()}`;
this.premisses = await this.performRequest('GET', url, null).catch(e => {
@ -572,6 +739,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
});
this.premisses.forEach(p => p.selected = true);
this.prepareDestinations(id, [id]);
this.selectedLoading = false;
this.loading = false;
},
@ -605,7 +773,6 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
const idx = this.premisses.findIndex(p => p.id === id);
this.premisses.splice(idx, 1);
},
async performRequest(method, url, body, expectResponse = true) {
const params = {
@ -667,15 +834,33 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
}
} 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: "Return code error " + response.status,
message: "Server returned wrong response code",
trace: null
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');
}
}

View file

@ -4,6 +4,7 @@ import de.avatic.lcc.dto.error.ErrorDTO;
import de.avatic.lcc.dto.error.ErrorResponseDTO;
import de.avatic.lcc.util.exception.base.BadRequestException;
import de.avatic.lcc.util.exception.base.ForbiddenException;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@ -18,6 +19,7 @@ import org.springframework.web.method.annotation.MethodArgumentTypeMismatchExcep
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
@ControllerAdvice
public class GlobalExceptionHandler {
@ -42,33 +44,28 @@ public class GlobalExceptionHandler {
public ResponseEntity<ErrorResponseDTO> handleValidationExceptions(
MethodArgumentNotValidException exception) {
// Extract field-specific validation errors
Map<String, String> fieldErrors = new HashMap<>();
exception.getBindingResult().getFieldErrors().forEach(error -> {
fieldErrors.put(error.getField(), error.getDefaultMessage());
});
// Create a readable error message
String errorMessage = fieldErrors.isEmpty() ?
"Validation failed" :
"Validation failed for fields: " + String.join(", ", fieldErrors.keySet().stream().map(key -> key + " - " + fieldErrors.get(key)).toList());
// You might want to create a custom ErrorDTO that can handle field errors
ErrorDTO error = new ErrorDTO(
exception.getClass().getName(),
"Constraint Violation",
exception.getMessage(),
"Validation Failed",
errorMessage,
Arrays.asList(exception.getStackTrace())
);
return new ResponseEntity<>(new ErrorResponseDTO(error), HttpStatus.BAD_REQUEST);
}
// @ResponseStatus(HttpStatus.BAD_REQUEST)
// @ExceptionHandler(MethodArgumentNotValidException.class)
// public ResponseEntity<ErrorResponseDTO> handleMethodArgumentNotValid(MethodArgumentNotValidException exception) {
//
// ErrorDTO error = new ErrorDTO(
// exception.getClass().getSimpleName(),
// exception.getTitle(),
// exception.getMessage(),
// "Invalid Arguments",
// new HashMap<>() {{
// put("errorMessage", exception.getMessage());
// }}
// );
//
// return new ResponseEntity<>(new ErrorResponseDTO(error), HttpStatus.BAD_REQUEST);
// }
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(BadRequestException.class)
public ResponseEntity<ErrorResponseDTO> handleMethodArgumentNotValid(BadRequestException exception) {
@ -115,12 +112,18 @@ public class GlobalExceptionHandler {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ErrorResponseDTO> handleConstraintViolation(ConstraintViolationException exception) { //
public ResponseEntity<ErrorResponseDTO> handleConstraintViolation(ConstraintViolationException exception) {
// Extract constraint violation details
String errorMessage = exception.getConstraintViolations()
.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(", "));
ErrorDTO error = new ErrorDTO(
exception.getClass().getName(),
"Constraint Violation",
exception.getMessage(),
errorMessage.isEmpty() ? "Validation constraints violated" : errorMessage,
Arrays.asList(exception.getStackTrace())
);

View file

@ -24,6 +24,8 @@ import de.avatic.lcc.util.exception.badrequest.InvalidArgumentException;
import de.avatic.lcc.util.exception.base.BadRequestException;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@ -40,6 +42,7 @@ import java.util.Map;
@Validated
public class PremiseController {
private static final Logger log = LoggerFactory.getLogger(PremiseController.class);
private final PremiseSearchStringAnalyzerService premiseSearchStringAnalyzerService;
private final PremisesService premisesServices;
private final PremiseCreationService premiseCreationService;
@ -146,7 +149,7 @@ public class PremiseController {
}
@PostMapping({"/destination", "/destination/"})
public ResponseEntity<Map<Integer, DestinationDTO>> createDestination(@RequestBody DestinationCreateDTO destinationCreateDTO) {
public ResponseEntity<Map<Integer, DestinationDTO>> createDestination(@RequestBody @Valid DestinationCreateDTO destinationCreateDTO) {
return ResponseEntity.ok(destinationService.createDestination(destinationCreateDTO));
}
@ -162,6 +165,7 @@ public class PremiseController {
@PutMapping({"/destination/{id}", "/destination/{id}/"})
public ResponseEntity<Void> updateDestination(@PathVariable @Min(1) Integer id, @RequestBody @Valid DestinationUpdateDTO destinationUpdateDTO) {
log.info("Updating destination {}", destinationUpdateDTO);
destinationService.updateDestination(id, destinationUpdateDTO);
return ResponseEntity.ok().build();
}

View file

@ -1,14 +1,21 @@
package de.avatic.lcc.dto.calculation.edit.destination;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.util.List;
public class DestinationCreateDTO {
@NotEmpty (message = "At least one premise must be selected")
@NotNull (message = "At least one premise must be selected")
@JsonProperty("premise_id")
List<Integer> premiseId;
List<@Min(value = 1, message = "Invalid premise id") Integer> premiseId;
@Min(value = 1, message = "Invalid destination node id")
@NotNull (message = "Destination node id must be provided")
@JsonProperty("destination_node_id")
Integer destinationNodeId;

View file

@ -16,17 +16,17 @@ public class DestinationSetListItemDTO {
@Min(1)
private Integer annualAmount;
@JsonProperty("repacking_cost")
@JsonProperty("repackaging_costs")
@DecimalMin(value = "0.00", message = "Amount must be greater than or equal 0")
@Digits(integer = 13, fraction = 2, message = "Amount must have at most 2 decimal places")
private Number repackingCost;
@JsonProperty("handling_cost")
@JsonProperty("handling_costs")
@DecimalMin(value = "0.00", message = "Amount must be greater than or equal 0")
@Digits(integer = 13, fraction = 2, message = "Amount must have at most 2 decimal places")
private Number handlingCost;
@JsonProperty("disposal_cost")
@JsonProperty("disposal_costs")
@DecimalMin(value = "0.00", message = "Amount must be greater than or equal 0")
@Digits(integer = 13, fraction = 2, message = "Amount must have at most 2 decimal places")
private Number disposalCost;

View file

@ -1,5 +1,6 @@
package de.avatic.lcc.dto.calculation.edit.destination;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
@ -11,17 +12,17 @@ public class DestinationUpdateDTO {
@Min(1)
private Integer annualAmount;
@JsonProperty("repacking_cost")
@JsonProperty("repackaging_costs")
@DecimalMin(value = "0.00", message = "Amount must be greater than or equal 0")
@Digits(integer = 13, fraction = 2, message = "Amount must have at most 2 decimal places")
private Number repackingCost;
@JsonProperty("handling_cost")
@JsonProperty("handling_costs")
@DecimalMin(value = "0.00", message = "Amount must be greater than or equal 0")
@Digits(integer = 13, fraction = 2, message = "Amount must have at most 2 decimal places")
private Number handlingCost;
@JsonProperty("disposal_cost")
@JsonProperty("disposal_costs")
@DecimalMin(value = "0.00", message = "Amount must be greater than or equal 0")
@Digits(integer = 13, fraction = 2, message = "Amount must have at most 2 decimal places")
private Number disposalCost;
@ -30,6 +31,14 @@ public class DestinationUpdateDTO {
@Min(1)
private Integer routeSelectedId;
@JsonProperty("is_d2d")
private Boolean d2d;
@DecimalMin(value = "0.00", message = "Amount must be greater than or equal 0")
@Digits(integer = 13, fraction = 2, message = "Amount must have at most 2 decimal places")
@JsonProperty("rate_d2d")
private Number rateD2d;
public Number getRepackingCost() {
return repackingCost;
}
@ -69,4 +78,34 @@ public class DestinationUpdateDTO {
public void setAnnualAmount(Integer annualAmount) {
this.annualAmount = annualAmount;
}
@JsonIgnore
public Boolean getD2d() {
return d2d;
}
public void setD2d(Boolean d2d) {
this.d2d = d2d;
}
public Number getRateD2d() {
return rateD2d;
}
public void setRateD2d(Number rateD2d) {
this.rateD2d = rateD2d;
}
@Override
public String toString() {
return "DestinationUpdateDTO{" +
"annualAmount=" + annualAmount +
", repackingCost=" + repackingCost +
", handlingCost=" + handlingCost +
", disposalCost=" + disposalCost +
", routeSelectedId=" + routeSelectedId +
", d2d=" + d2d +
", rateD2d=" + rateD2d +
'}';
}
}

View file

@ -8,7 +8,7 @@ import java.util.List;
public class MaterialUpdateDTO {
@JsonProperty("premise_id")
@JsonProperty("premise_ids")
private List<Integer> premiseIds;
@JsonProperty("hs_code")

View file

@ -17,8 +17,8 @@ public class PackagingUpdateDTO {
@JsonProperty("is_stackable")
private Boolean isStackable;
private List<@Min(1) Integer> premiseIds;
@JsonProperty("premise_ids")
private List<@Min(value = 1, message = "Invalid value for premise id. Must be equal to 1 or greater") Integer> premiseIds;
public DimensionDTO getDimensions() {
return dimensions;

View file

@ -14,6 +14,7 @@ public class PriceUpdateDTO {
@JsonProperty("premise_ids")
private List<@Min(1) Integer> premiseIds;
@JsonProperty("material_cost")
@DecimalMin(value = "0.01", message = "Amount must be greater than 0")
@Digits(integer = 13, fraction = 2, message = "Amount must have at most 2 decimal places")
private Number price;
@ -23,7 +24,7 @@ public class PriceUpdateDTO {
@Digits(integer = 4, fraction = 4, message = "Amount must have at most 4 decimal places")
private Number overseaShare;
@JsonProperty("fca_fee_included")
@JsonProperty("is_fca_enabled")
private Boolean includeFcaFee;

View file

@ -179,4 +179,8 @@ public class Node {
public String getDebugText() {
return externalMappingId == null ? "\uD83D\uDC64" + name : externalMappingId;
}
public String toString() {
return getDebugText();
}
}

View file

@ -92,7 +92,7 @@ public class MaterialRepository {
var materials = filter.isPresent() ?
jdbcTemplate.query(query, new MaterialMapper(),
"%" + filter.get() + "%", "%" + filter.get() + "%", pagination.getLimit(), pagination.getOffset()) :
filter.get() + "%", filter.get() + "%", pagination.getLimit(), pagination.getOffset()) :
jdbcTemplate.query(query, new MaterialMapper(),
pagination.getLimit(), pagination.getOffset());
@ -100,7 +100,7 @@ public class MaterialRepository {
Integer totalCount = filter.isPresent() ?
jdbcTemplate.queryForObject(countQuery, Integer.class,
"%" + filter.get() + "%", "%" + filter.get() + "%") :
filter.get() + "%", filter.get() + "%") :
jdbcTemplate.queryForObject(countQuery, Integer.class);
return new SearchQueryResult<>(materials, pagination.getPage(), totalCount, pagination.getLimit());

View file

@ -51,7 +51,7 @@ public class DestinationRepository {
}
@Transactional
public void update(Integer id, Integer annualAmount, BigDecimal repackingCost, BigDecimal disposalCost, BigDecimal handlingCost) {
public void update(Integer id, Integer annualAmount, BigDecimal repackingCost, BigDecimal disposalCost, BigDecimal handlingCost, Boolean isD2d, BigDecimal d2dRate) {
if (id == null) {
throw new InvalidArgumentException("ID cannot be null");
}
@ -61,20 +61,25 @@ public class DestinationRepository {
List<String> setClauses = new ArrayList<>();
// Build dynamic SET clauses based on non-null parameters
if (repackingCost != null) {
setClauses.add("repacking_cost = :repackingCost");
parameters.put("repackingCost", repackingCost);
}
if (disposalCost != null) {
setClauses.add("disposal_cost = :disposalCost");
parameters.put("disposalCost", disposalCost);
}
setClauses.add("repacking_cost = :repackingCost");
parameters.put("repackingCost", repackingCost);
setClauses.add("disposal_cost = :disposalCost");
parameters.put("disposalCost", disposalCost);
setClauses.add("handling_cost = :handlingCost");
parameters.put("handlingCost", handlingCost);
var setD2d = isD2d != null ? isD2d : false;
setClauses.add("is_d2d = :isD2d");
parameters.put("isD2d", setD2d);
setClauses.add("rate_d2d = :d2dRate");
parameters.put("d2dRate", setD2d ? d2dRate : null);
if (handlingCost != null) {
setClauses.add("handling_cost = :handlingCost");
parameters.put("handlingCost", handlingCost);
}
if (annualAmount != null) {
setClauses.add("annual_amount = :annualAmount");
@ -147,7 +152,7 @@ public class DestinationRepository {
public Map<Integer, List<Destination>> getByPremiseIdsAndNodeIds(List<Integer> premiseIds, List<Integer> nodeIds, Integer userId) {
Map<Integer, List<Destination>> destinations = new HashMap<>();
for (Integer premiseId : premiseIds) {
String placeholder = String.join(",", Collections.nCopies(nodeIds.size(), "?"));
String query = "SELECT * FROM premise_destination JOIN premise ON premise_destination.premise_id = premise.id WHERE premise_destination.premise_id = ? AND premise_destination.destination_node_id IN (" + placeholder + ") AND premise.user_id = ?";
@ -159,9 +164,9 @@ public class DestinationRepository {
}
params[nodeIds.size()] = premiseId;
params[nodeIds.size() + 1] = userId;
destinations.put(premiseId,jdbcTemplate.query(query, new DestinationMapper(), params));
destinations.put(premiseId, jdbcTemplate.query(query, new DestinationMapper(), params));
}
return destinations;
}
@ -214,7 +219,7 @@ public class DestinationRepository {
var ownerId = getOwnerIdById(id);
if (ownerId.isEmpty() || !ownerId.get().equals(userId)) {
throw new ForbiddenException("Access violation. Accessing destination with id " + id + " (owner: "+ownerId.orElse(null)+" user: " + userId + ")");
throw new ForbiddenException("Access violation. Accessing destination with id " + id + " (owner: " + ownerId.orElse(null) + " user: " + userId + ")");
}
}

View file

@ -83,7 +83,7 @@ public class DestinationService {
var destination = new Destination();
destination.setDestinationNodeId(destinationNodeId);
destination.setPremiseId(premise.getId());
destination.setAnnualAmount(annualAmount);
destination.setAnnualAmount(annualAmount == null ? 0 : annualAmount);
destination.setD2d(false);
destination.setLeadTimeD2d(null);
destination.setRateD2d(null);
@ -151,7 +151,8 @@ public class DestinationService {
destinationUpdateDTO.getAnnualAmount(),
destinationUpdateDTO.getRepackingCost() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getRepackingCost().doubleValue()),
destinationUpdateDTO.getDisposalCost() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getDisposalCost().doubleValue()),
destinationUpdateDTO.getHandlingCost() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getHandlingCost().doubleValue()));
destinationUpdateDTO.getHandlingCost() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getHandlingCost().doubleValue()),
destinationUpdateDTO.getD2d(), destinationUpdateDTO.getRateD2d() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getRateD2d().doubleValue()));
}

View file

@ -81,6 +81,7 @@ public class ChainResolver {
}
log.info("Found {} chains for node {}", foundChains.size(), nodeId);
log.info("Found chains: {}", foundChains);
return foundChains;
}

View file

@ -63,13 +63,13 @@ public class ChangeMaterialService {
Map<Integer, Premise> uniqueMap = new HashMap<>();
List<Premise> premisesToBeDeleted = new ArrayList<>();
allPremises.forEach(p -> {
if (null != uniqueMap.putIfAbsent(p.getMaterialId(), p)) premisesToBeDeleted.add(p);
if (null != uniqueMap.putIfAbsent(p.getSupplierNodeId(), p)) premisesToBeDeleted.add(p);
});
Collection<Premise> premisesToProcess = uniqueMap.values();
// check if user owns all premises:
if (allPremises.stream().anyMatch(p -> !p.getUserId().equals(userId)))
throw new IllegalArgumentException("Not authorized to change suppliers of premises owned by other users");
throw new IllegalArgumentException("Not authorized to change material of premises owned by other users");
// check for any other collisions, and mark as "to be deleted":
premisesToBeDeleted.addAll(premisesToProcess.stream().map(p -> premiseRepository.getCollidingPremisesOnChange(userId, p.getId(), materialId, p.getSupplierNodeId())).flatMap(List::stream).toList());

View file

@ -4,6 +4,8 @@ import de.avatic.lcc.dto.generic.DimensionDTO;
import de.avatic.lcc.model.packaging.PackagingDimension;
import de.avatic.lcc.model.packaging.PackagingType;
import de.avatic.lcc.model.premises.Premise;
import de.avatic.lcc.model.utils.DimensionUnit;
import de.avatic.lcc.model.utils.WeightUnit;
import de.avatic.lcc.util.exception.badrequest.InvalidArgumentException;
import org.springframework.stereotype.Service;
@ -41,11 +43,11 @@ public class DimensionTransformer {
entity.setId(dto.getId());
entity.setType(dto.getType());
entity.setLength(dto.getDimensionUnit().convertToMM(dto.getLength()));
entity.setWidth(dto.getDimensionUnit().convertToMM(dto.getWidth()));
entity.setHeight( dto.getDimensionUnit().convertToMM(dto.getHeight()));
entity.setLength(doDimensionConversion(dto.getLength(), dto.getDimensionUnit()));
entity.setWidth(doDimensionConversion(dto.getWidth(), dto.getDimensionUnit()));
entity.setHeight( doDimensionConversion(dto.getHeight(), dto.getDimensionUnit()));
entity.setDimensionUnit(dto.getDimensionUnit());
entity.setWeight(dto.getWeightUnit().convertToG(dto.getWeight()));
entity.setWeight(doWeightConversion(dto.getWeight(), dto.getWeightUnit()));
entity.setWeightUnit(dto.getWeightUnit());
entity.setContentUnitCount(dto.getContentUnitCount());
entity.setDeprecated(dto.getDeprecated());
@ -53,6 +55,26 @@ public class DimensionTransformer {
return entity;
}
private Integer doWeightConversion(Double value, WeightUnit unit) {
if(value == null) return null;
if(value <= 0) {
return null;
}
return unit.convertToG(value);
}
private Integer doDimensionConversion(Double value, DimensionUnit unit) {
if(value == null) return null;
if(value <= 0) {
return null;
}
return unit.convertToMM(value);
}
public PackagingDimension toDimensionEntity(Premise entity) {
var packaging = new PackagingDimension();

View file

@ -112,10 +112,13 @@ public class PremiseTransformer {
dto.setDestinations(destinationRepository.getByPremiseId(entity.getId()).stream().map(destinationTransformer::toDestinationDTO).toList());
dto.setHsCode(entity.getHsCode());
dto.setTariffRate(entity.getTariffRate() == null ? null : entity.getTariffRate().doubleValue());
dto.setFcaEnabled(entity.getFcaEnabled());
dto.setOverseaShare(entity.getOverseaShare() == null ? null : entity.getOverseaShare().doubleValue());
dto.setMaterialCost(entity.getMaterialCost() == null ? null : entity.getMaterialCost().doubleValue());
return dto;
}

View file

@ -38,7 +38,9 @@ public class RouteTransformer {
List<RouteSection> sections = routeSectionRepository.getByRouteId(entity.getId());
dto.setTransitNodes(getRouteNodes(sections).stream().map(nodeTransformer::toNodeDTO).toList());
dto.setType(TransportType.valueOf(sections.stream().filter(RouteSection::getMainRun).findFirst().orElseThrow().getTransportType().name()));
var mainRun = sections.stream().filter(RouteSection::getMainRun).findFirst().orElse(null);
dto.setType(mainRun == null ? TransportType.ROAD : mainRun.getTransportType());
return dto;
}