From 155ad018c9c8c1574d5dea41f52f5c761cfa1be9 Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 17 Aug 2025 12:41:18 +0200 Subject: [PATCH] FRONTEND/BACKEND: Add support for setting material details (`hsCode`, `tariffRate`, `materialCost`, etc.) in `PremiseTransformer` and `PremiseDetailDTO`; implement `useMaterialStore` and `useCustomsStore` for querying and managing state; enhance supplier and material editing views with new UI components (`SelectNode`), a map placeholder, and modal dialogs; update schema for `premise_route_node` and enhance repository queries; upgrade dependencies (`vite`, `rollup`) and introduce initial Google Maps integration. --- src/frontend/package-lock.json | 8 +- .../components/UI/AutoSuggestSearchBar.vue | 33 ++- .../layout/edit/DestinationListView.vue | 59 +++-- .../components/layout/edit/MaterialEdit.vue | 90 ++++++- .../components/layout/edit/PackagingEdit.vue | 174 +++++++++++-- .../src/components/layout/edit/PriceEdit.vue | 71 +++++- .../components/layout/edit/SupplierView.vue | 48 +++- .../edit/destination/DestinationItem.vue | 74 +++++- .../edit/destination/DestinationRoute.vue | 118 +++++++++ .../components/layout/node/CreateNewNode.vue | 8 + .../src/components/layout/node/SelectNode.vue | 196 ++++++++++++++ .../src/pages/CalculationSingleEdit.vue | 118 +++++++-- src/frontend/src/store/customs.js | 20 ++ src/frontend/src/store/material.js | 63 +++++ src/frontend/src/store/premiseEdit.js | 241 ++++++++++++------ .../calculation/PremiseController.java | 2 +- .../lcc/dto/calculation/DestinationDTO.java | 34 +++ .../calculation/edit/PremiseDetailDTO.java | 62 ++++- .../lcc/dto/calculation/edit/SetDataDTO.java | 2 +- .../lcc/model/premises/route/RouteNode.java | 9 + .../premise/RouteNodeRepository.java | 3 +- .../service/access/DestinationService.java | 2 +- .../calculation/ChangeMaterialService.java | 13 +- .../service/calculation/RoutingService.java | 9 + .../premise/DestinationTransformer.java | 4 + .../premise/PremiseTransformer.java | 8 +- src/main/resources/schema.sql | 28 +- 27 files changed, 1262 insertions(+), 235 deletions(-) create mode 100644 src/frontend/src/components/layout/edit/destination/DestinationRoute.vue create mode 100644 src/frontend/src/components/layout/node/SelectNode.vue create mode 100644 src/frontend/src/store/customs.js create mode 100644 src/frontend/src/store/material.js diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index cd8cd57..171e236 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -2661,9 +2661,9 @@ } }, "node_modules/vite": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz", - "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.2.tgz", + "integrity": "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2671,7 +2671,7 @@ "fdir": "^6.4.6", "picomatch": "^4.0.3", "postcss": "^8.5.6", - "rollup": "^4.40.0", + "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "bin": { diff --git a/src/frontend/src/components/UI/AutoSuggestSearchBar.vue b/src/frontend/src/components/UI/AutoSuggestSearchBar.vue index 0f88556..c3e4496 100644 --- a/src/frontend/src/components/UI/AutoSuggestSearchBar.vue +++ b/src/frontend/src/components/UI/AutoSuggestSearchBar.vue @@ -48,7 +48,7 @@
- {{ getTitleFor(suggestion) }} +
{{ getTitleFor(suggestion) }}
{{ getSubtitleFor(suggestion) }} @@ -60,6 +60,7 @@
+
- - - Add new Destination +
@@ -16,18 +21,15 @@
Action
-
- - - - +
+
-
+
No Destinations found.
-
@@ -43,12 +45,32 @@ import CalculationListItem from "@/components/layout/calculation/CalculationList import DestinationItem from "@/components/layout/edit/destination/DestinationItem.vue"; import {mapStores} from "pinia"; import {usePremiseEditStore} from "@/store/premiseEdit.js"; +import {useNodeStore} from "@/store/node.js"; export default { name: "DestinationListView", components: {DestinationItem, CalculationListItem, Checkbox, Spinner, BasicButton, AutosuggestSearchbar, IconButton}, + props: { + destinations: { + type: Object, + required: true + } + }, computed: { - ...mapStores(usePremiseEditStore) + ...mapStores(usePremiseEditStore, useNodeStore) + }, + methods: { + async fetchDestinations(query) { + await this.nodeStore.setQuery({searchTerm: query, nodeType: "DESTINATION", includeUserNode: false}); + return this.nodeStore.nodes; + }, + resolveFlag(node) { + return node.country.iso_code; + }, + addDestination(node) { + console.log(node) + // this.premiseEditStore.addDestination(node.id); + } } } @@ -66,12 +88,13 @@ export default { background: white; border-radius: 1.2rem; box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1); - overflow: hidden; margin-top: 3rem; margin-bottom: 3rem; + } .search-bar-container { + margin: 3rem 3rem 0 3rem; display: flex; justify-content: flex-end; @@ -79,9 +102,13 @@ export default { gap: 1.6rem; } -.list-container { - gap: 1.6rem; +.search-bar { + flex: 0 1 max(20rem, 40vw); +} +.list-container { + overflow: hidden; + gap: 1.6rem; } .empty-container { @@ -93,7 +120,7 @@ export default { .destination-list-header { display: grid; - grid-template-columns: 1fr 1fr 2fr 14rem 10rem; + grid-template-columns: 2fr 2fr 5fr auto; gap: 1.6rem; padding: 2.4rem; background-color: #ffffff; diff --git a/src/frontend/src/components/layout/edit/MaterialEdit.vue b/src/frontend/src/components/layout/edit/MaterialEdit.vue index 6e29a7f..0e5790a 100644 --- a/src/frontend/src/components/layout/edit/MaterialEdit.vue +++ b/src/frontend/src/components/layout/edit/MaterialEdit.vue @@ -4,8 +4,13 @@
Part number
- + {{ partNumber }}
-
@@ -39,7 +44,8 @@
Tariff rate [%]
-
{{ tariffRatePercent }} @@ -58,21 +64,25 @@ import InputField from "@/components/UI/InputField.vue"; import AutosuggestSearchbar from "@/components/UI/AutoSuggestSearchBar.vue"; import ModalDialog from "@/components/UI/ModalDialog.vue"; import {PhArrowCounterClockwise} from "@phosphor-icons/vue"; +import {useMaterialStore} from "@/store/material.js"; +import {mapStores} from "pinia"; export default { name: "MaterialEdit", components: {PhArrowCounterClockwise, ModalDialog, AutosuggestSearchbar, InputField, Flag, IconButton}, + emits: ["update:tariffRate", "updateMaterial", "update:partNumber", "update:hsCode"], props: { description: { type: String, required: true, }, hsCode: { - type: String, + required: true, + validator: (value) => value === null || typeof value === 'string' }, tariffRate: { - type: Number, required: true, + validator: (value) => value === null || typeof value === 'number' }, partNumber: { type: String, @@ -82,37 +92,89 @@ export default { type: Number, required: true, } - }, computed: { + ...mapStores(useMaterialStore), editIconPartNumber() { return this.editMode ? "check" : "pencil-simple"; }, tariffRatePercent() { - return this.tariffRate * 100; + return this.tariffRate ? (this.tariffRate * 100).toFixed(2) : ''; } }, data() { return { editMode: false, modalDialogPartNumberState: false, + selectedMaterial: null, } }, methods: { + partNumberSelected(material) { + this.selectedMaterial = material; + }, modalDialogClick(action) { - console.log(action); this.modalDialogPartNumberState = false; + if (action === 'accept') { + this.$emit('updateMaterial', this.selectedMaterial.id, 'updateMasterData'); + this.editMode = false; + this.selectedMaterial = null; + } else if(action === 'deny') { + this.$emit('updateMaterial', this.selectedMaterial.id, 'keepMasterData'); + this.editMode = false; + this.selectedMaterial = null; + } + }, + parseNumberFromString(value, decimals = 2) { + if (typeof value === 'number') return value; + if (!value || typeof value !== 'string') return 0; + const normalizedValue = value.replace(',', '.').replace(/[^0-9.]/g, ''); + + const parsed = parseFloat(normalizedValue); + + if (isNaN(parsed)) return 0; + return Math.round(parsed * Math.pow(10, decimals)) / Math.pow(10, decimals); + }, + updateInputValue(inputRef, formattedValue) { + this.$nextTick(() => { + if (this.$refs[inputRef] && this.$refs[inputRef].value !== formattedValue) { + this.$refs[inputRef].value = formattedValue; + } + }); + }, + validateInput(type, event) { + const decimals = 2 + const parsed = this.parseNumberFromString(event.target.value, decimals); + console.log('validateInput', type, event.target.value, parsed); + + this.$emit(`update:${type}`, parsed); + + // Force update the input field with the correctly formatted value + const formattedValue = parsed.toFixed(decimals); + const inputRef = `${type}Input`; + this.updateInputValue(inputRef, formattedValue); }, toggleEditMode() { - if(this.editMode) { - this.modalDialogPartNumberState = true; + if (this.editMode) { + if (this.selectedMaterial != null && this.selectedMaterial !== this.partNumber) { + this.modalDialogPartNumberState = true; + } else { + this.editMode = false; + } } else { this.editMode = true; } }, - fetchPartNumbers(query) { - return [1, 2, 3]; + async fetchPartNumbers(query) { + const materialQuery = {searchTerm: query}; + await this.materialStore.setQuery(materialQuery); + return this.materialStore.materials; + }, + async fetchHsCode(query) { + const hsCodeQuery = {searchTerm: query}; + await this.customs.setQuery(hsCodeQuery); + return this.customs.hsCodes; } } } @@ -185,7 +247,7 @@ export default { } .input-field-tariffrate { - min-width: 7rem; + min-width: 10rem; flex: 0 1 10rem; } diff --git a/src/frontend/src/components/layout/edit/PackagingEdit.vue b/src/frontend/src/components/layout/edit/PackagingEdit.vue index 5def1a0..aa37c42 100644 --- a/src/frontend/src/components/layout/edit/PackagingEdit.vue +++ b/src/frontend/src/components/layout/edit/PackagingEdit.vue @@ -3,7 +3,7 @@
Length
-
@@ -16,7 +16,7 @@
Width
-
@@ -27,7 +27,7 @@
Height
-
@@ -38,7 +38,7 @@
Weight
-
@@ -49,12 +49,10 @@
Pieces per HU
- -
- -
-
+
+ +
@@ -76,16 +74,39 @@ import Checkbox from "@/components/UI/Checkbox.vue"; import Dropdown from "@/components/UI/Dropdown.vue"; import Tooltip from "@/components/UI/Tooltip.vue"; -import {set} from "@vueuse/core"; export default { name: "PackagingEdit", components: {Tooltip, Dropdown, Checkbox}, - emits: ['update:stackable', 'update:mixable' ], + emits: ['update:stackable', 'update:mixable', 'update:length', 'update:width', 'update:height', 'update:weight', 'update:unitCount', 'update:weightUnit', 'update:dimensionUnit'], props: { - handlingUnit: { - type: Object, + length: { required: true, + validator: (value) => value === null || typeof value === 'number' + }, + width: { + required: true, + validator: (value) => value === null || typeof value === 'number' + }, + height: { + required: true, + validator: (value) => value === null || typeof value === 'number' + }, + weight: { + required: true, + validator: (value) => value === null || typeof value === 'number' + }, + dimensionUnit: { + required: true, + validator: (value) => value === null || typeof value === 'string' + }, + weightUnit: { + required: true, + validator: (value) => value === null || typeof value === 'string' + }, + unitCount: { + required: true, + validator: (value) => value === null || typeof value === 'number' }, mixable: { type: Boolean, @@ -97,26 +118,131 @@ export default { } }, computed: { + huWeight() { + return this.weight?.toFixed(this.weightDecimals) ?? ''; + }, + huLength() { + return this.length?.toFixed(this.dimensionDecimals) ?? ''; + }, + huWidth() { + console.log("huWidth",this.width, this.width?.toFixed(this.dimensionDecimals)); + return this.width?.toFixed(this.dimensionDecimals) ?? ''; + }, + huHeight() { + return this.height?.toFixed(this.dimensionDecimals) ?? ''; + }, + huUnitCount() { + return this.unitCount ?? ''; + }, + dimensionDecimals() { + const unitType = this.huDimensionUnits.find(unit => unit.id === this.huDimensionUnitSelected)?.value; + return (unitType === 'cm') ? 2 : ((unitType === 'm') ? 4 : 0); + }, + weightDecimals() { + const unitType = this.huWeightUnits.find(unit => unit.id === this.huWeightUnitSelected)?.value; + return (unitType === 'kg') ? 4 : ((unitType === 't') ? 8 : 0); + }, huDimensionUnitSelected: { get() { - return this.huDimensionUnits.find(unit => unit.value.toLowerCase() === this.handlingUnit.dimension_unit.toLowerCase())?.id; + if(!this.dimensionUnit) + this.$emit('update:dimensionUnit', 'mm'); + + return this.huDimensionUnits.find(unit => unit.value.toLowerCase() === (this.dimensionUnit?.toLowerCase() ?? 'mm'))?.id; }, set(value) { - this.handlingUnit.dimension_unit = this.huDimensionUnits.find(unit => unit.id === value)?.value; + const unitType = this.huDimensionUnits.find(unit => unit.id === value)?.value; + const decimals = (unitType === 'cm') ? 2 : ((unitType === 'm') ? 4 : 0); + + if (this.length) { + const parsedLength = parseFloat(this.length.toFixed(decimals)); + this.$emit('update:length', parsedLength); + } + + if (this.height) { + const parsedHeight = parseFloat(this.height.toFixed(decimals)); + this.$emit('update:height', parsedHeight); + } + + if (this.weight) { + const parsedWidth = parseFloat(this.width.toFixed(decimals)); + this.$emit('update:width', parsedWidth); + } + + this.$emit('update:dimensionUnit', unitType); } }, huWeightUnitSelected: { - get() - { - return this.huWeightUnits.find(unit => unit.value.toLowerCase() === this.handlingUnit.weight_unit.toLowerCase())?.id; + get() { + if(!this.weightUnit) + this.$emit('update:weightUnit', 'kg'); + + return this.huWeightUnits.find(unit => unit.value.toLowerCase() === (this.weightUnit?.toLowerCase() ?? 'kg'))?.id; }, set(value) { - this.handlingUnit.weight_unit = this.huWeightUnits.find(unit => unit.id === value)?.value; + const unitType = this.huWeightUnits.find(unit => unit.id === value)?.value; + const decimals = (unitType === 'kg') ? 4 : ((unitType === 't') ? 8 : 0); + + if (this.weight) { + const parsedWeight = parseFloat(this.weight.toFixed(decimals)); + this.$emit('update:weight', parsedWeight); + } + + this.$emit('update:weightUnit', unitType); } } }, methods: { + parseNumberFromString(value, decimals = 2) { + if (typeof value === 'number') return value; + if (!value || typeof value !== 'string') return 0; + + const normalizedValue = value.replace(',', '.').replace(/[^0-9.]/g, ''); + + const parsed = parseFloat(normalizedValue); + + if (isNaN(parsed)) return 0; + return Math.round(parsed * Math.pow(10, decimals)) / Math.pow(10, decimals); + }, + updateInputValue(inputRef, formattedValue) { + this.$nextTick(() => { + if (this.$refs[inputRef] && this.$refs[inputRef].value !== formattedValue) { + this.$refs[inputRef].value = formattedValue; + } + }); + }, + validateDimension(type, event) { + const decimals = (this.huDimensionUnitSelected === 2) ? 2 : ((this.huDimensionUnitSelected === 3) ? 4 : 0); + const parsed = this.parseNumberFromString(event.target.value, decimals); + console.log('validateDimension', type, event.target.value, parsed); + + this.$emit(`update:${type}`, parsed); + + // Force update the input field with the correctly formatted value + const formattedValue = parsed.toFixed(decimals); + const inputRef = `${type}Input`; + this.updateInputValue(inputRef, formattedValue); + }, + validateWeight(type, event) { + const decimals = (this.huWeightUnitSelected === 2) ? 4 : ((this.huWeightUnitSelected === 3) ? 8 : 0); + const parsed = this.parseNumberFromString(event.target.value, decimals); + console.log('validateDimension', type, event.target.value, parsed); + + this.$emit('update:weight', parsed); + + // Force update the input field with the correctly formatted value + const formattedValue = parsed.toFixed(decimals); + this.updateInputValue('weightInput', formattedValue); + }, + validateCount(event) { + const parsed = this.parseNumberFromString(event.target.value, 0); + console.log(parsed); + this.$emit('update:unitCount', parsed); + + // Force update the input field with the correctly formatted value + const formattedValue = parsed.toString(); + this.updateInputValue('unitCountInput', formattedValue); + }, updateStackable(value) { this.$emit('update:stackable', value); }, @@ -126,8 +252,8 @@ export default { }, data() { return { - huDimensionUnits: [{id: 1, value: "cm"}, {id: 2, value: "mm"}, {id: 3, value: "m"}], - huWeightUnits: [{id: 1, value: "kg"}, {id: 2, value: "g"}, {id: 3, value: "t"}], + huDimensionUnits: [{id: 1, value: "mm"}, {id: 2, value: "cm"}, {id: 3, value: "m"}], + huWeightUnits: [{id: 1, value: "g"}, {id: 2, value: "kg"}, {id: 3, value: "t"}], } } } @@ -137,7 +263,7 @@ export default { \ No newline at end of file diff --git a/src/frontend/src/components/layout/edit/destination/DestinationItem.vue b/src/frontend/src/components/layout/edit/destination/DestinationItem.vue index 291b4f3..19043bb 100644 --- a/src/frontend/src/components/layout/edit/destination/DestinationItem.vue +++ b/src/frontend/src/components/layout/edit/destination/DestinationItem.vue @@ -1,34 +1,94 @@ \ No newline at end of file diff --git a/src/frontend/src/components/layout/edit/destination/DestinationRoute.vue b/src/frontend/src/components/layout/edit/destination/DestinationRoute.vue new file mode 100644 index 0000000..35f7b06 --- /dev/null +++ b/src/frontend/src/components/layout/edit/destination/DestinationRoute.vue @@ -0,0 +1,118 @@ + + + + + \ No newline at end of file diff --git a/src/frontend/src/components/layout/node/CreateNewNode.vue b/src/frontend/src/components/layout/node/CreateNewNode.vue index 0357e90..41c2802 100644 --- a/src/frontend/src/components/layout/node/CreateNewNode.vue +++ b/src/frontend/src/components/layout/node/CreateNewNode.vue @@ -14,6 +14,9 @@
Coordinates:
{{ nodeCoordinates }}
+
+ map +