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.
This commit is contained in:
parent
7e9a437f8f
commit
155ad018c9
27 changed files with 1262 additions and 235 deletions
8
src/frontend/package-lock.json
generated
8
src/frontend/package-lock.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
</div>
|
||||
<div v-else>
|
||||
<div class="suggestion-content">
|
||||
<span class="suggestion-title">{{ getTitleFor(suggestion) }}</span>
|
||||
<div class="suggestion-title">{{ getTitleFor(suggestion) }}</div>
|
||||
<basic-badge variant="grey" v-if="getSubtitleFor(suggestion)">{{
|
||||
getSubtitleFor(suggestion)
|
||||
}}
|
||||
|
|
@ -60,6 +60,7 @@
|
|||
</div>
|
||||
</transition>
|
||||
|
||||
|
||||
<div
|
||||
v-if="showSuggestions && searchQuery && suggestions.length === 0 && !isLoading"
|
||||
class="no-results"
|
||||
|
|
@ -78,7 +79,7 @@ import BasicBadge from "@/components/UI/BasicBadge.vue";
|
|||
export default {
|
||||
name: 'AutosuggestSearchbar',
|
||||
components: {BasicBadge, Flag, Spinner},
|
||||
|
||||
emits: ['selected', 'search', 'suggestions-loaded', 'error'],
|
||||
props: {
|
||||
// Display props
|
||||
placeholder: {
|
||||
|
|
@ -98,10 +99,6 @@ export default {
|
|||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
maxSuggestions: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
fetchSuggestions: {
|
||||
type: Function,
|
||||
required: true
|
||||
|
|
@ -134,6 +131,10 @@ export default {
|
|||
initialValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
activateWatcher: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -148,7 +149,6 @@ export default {
|
|||
debouncedSearch: null,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
isFlagVariant() {
|
||||
return this.variant === 'flags';
|
||||
|
|
@ -242,6 +242,7 @@ export default {
|
|||
this.$emit('selected', suggestion)
|
||||
this.$emit('search', this.searchQuery)
|
||||
this.$refs.searchInput.blur()
|
||||
|
||||
},
|
||||
|
||||
handleEnterWithoutSelection() {
|
||||
|
|
@ -304,7 +305,7 @@ export default {
|
|||
},
|
||||
|
||||
setQuery(query) {
|
||||
this.searchQuery = query
|
||||
this.searchQuery = query;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -331,12 +332,15 @@ export default {
|
|||
// Reset highlighted index when suggestions change
|
||||
this.highlightedIndex = -1
|
||||
},
|
||||
|
||||
initialSuggestions: {
|
||||
handler(newSuggestions) {
|
||||
this.suggestions = [...newSuggestions]
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
initialValue(newValue) {
|
||||
if (this.activateWatcher)
|
||||
this.setQuery(newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -427,10 +431,11 @@ export default {
|
|||
border-radius: 0.8rem;
|
||||
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
z-index: 1000;
|
||||
max-height: 50rem;
|
||||
z-index: 1;
|
||||
max-height: 40rem;
|
||||
overflow-y: auto;
|
||||
margin-top: 0.4rem;
|
||||
|
||||
}
|
||||
|
||||
.suggestions-list {
|
||||
|
|
@ -466,7 +471,8 @@ export default {
|
|||
.suggestion-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.suggestion-title {
|
||||
|
|
@ -500,7 +506,8 @@ export default {
|
|||
border-radius: 0.8rem;
|
||||
margin-top: 0.4rem;
|
||||
background: white;
|
||||
font-size: 1.6rem
|
||||
font-size: 1.6rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<div class="search-bar-container">
|
||||
<autosuggest-searchbar fetch-suggestions="fetchDestinations"
|
||||
placeholder="Add new Destination ..."></autosuggest-searchbar>
|
||||
|
||||
<basic-button icon="plus">Add new Destination</basic-button>
|
||||
<autosuggest-searchbar class="search-bar"
|
||||
placeholder="Add new Destination ..."
|
||||
:fetch-suggestions="fetchDestinations"
|
||||
:flag-resolver="resolveFlag"
|
||||
@selected="addDestination"
|
||||
subtitle-resolver="address"
|
||||
variant="flags"
|
||||
title-resolver="name"
|
||||
:reset-on-select="true"></autosuggest-searchbar>
|
||||
</div>
|
||||
|
||||
<div class="list-container">
|
||||
|
|
@ -16,18 +21,15 @@
|
|||
<div>Action</div>
|
||||
</div>
|
||||
|
||||
<div v-if="premiseEditStore.showData">
|
||||
<destination-item :id="1"></destination-item>
|
||||
|
||||
<!-- <destination-item v-for="destination in premiseEditStore.destinations" :key="destination.id"-->
|
||||
<!-- :id="destination.id"></destination-item>-->
|
||||
<div v-if="premiseEditStore.selectedPremise.destinations.length !== 0">
|
||||
<destination-item v-for="destination in premiseEditStore.selectedPremise.destinations" :key="destination.id"
|
||||
:id="destination.id" :destination="destination"></destination-item>
|
||||
</div>
|
||||
<div v-else-if="premiseEditStore.showEmpty" class="empty-container">
|
||||
<div v-else class="empty-container">
|
||||
<span class="space-around">No Destinations found.</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,13 @@
|
|||
<div class="caption-column">Part number</div>
|
||||
|
||||
<div class="input-column">
|
||||
<autosuggest-searchbar v-if="editMode" :fetch-suggestions="fetchPartNumbers" :initial-value="partNumber"
|
||||
placeholder="Find part number"></autosuggest-searchbar>
|
||||
<autosuggest-searchbar v-if="editMode"
|
||||
@selected="partNumberSelected"
|
||||
:fetch-suggestions="fetchPartNumbers"
|
||||
:initial-value="partNumber"
|
||||
title-resolver="part_number"
|
||||
placeholder="Find part number"
|
||||
:activate-watcher="true"></autosuggest-searchbar>
|
||||
<span v-else>{{ partNumber }}</span>
|
||||
<modal-dialog :state="modalDialogPartNumberState"
|
||||
accept-text="Yes"
|
||||
|
|
@ -39,7 +44,8 @@
|
|||
|
||||
<div class="caption-column">Tariff rate [%]</div>
|
||||
<div v-if="!editMode" class="input-field-container input-field-tariffrate">
|
||||
<input v-model="tariffRatePercent" class="input-field"
|
||||
<input ref="tariffRateInput" :value="tariffRatePercent" @blur="validateInput('tariffRate',$event)"
|
||||
class="input-field"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
<span v-else>{{ tariffRatePercent }}</span>
|
||||
|
|
@ -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) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<div class="caption-column">Length</div>
|
||||
<div class="input-column">
|
||||
<div class="input-field-container">
|
||||
<input v-model="handlingUnit.length" class="input-field"
|
||||
<input ref="lengthInput" :value="huLength" @blur="validateDimension('length', $event)" class="input-field"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
<div class="caption-column">Width</div>
|
||||
<div class="input-column">
|
||||
<div class="input-field-container">
|
||||
<input v-model="handlingUnit.width" class="input-field"
|
||||
<input ref="widthInput" :value="huWidth" @blur="validateDimension('width', $event)" class="input-field"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
<div class="caption-column">Height</div>
|
||||
<div class="input-column">
|
||||
<div class="input-field-container">
|
||||
<input v-model="handlingUnit.height" class="input-field"
|
||||
<input ref="heightInput" :value="huHeight" @blur="validateDimension('height', $event)" class="input-field"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
<div class="caption-column">Weight</div>
|
||||
<div class="input-column">
|
||||
<div class="input-field-container">
|
||||
<input v-model="handlingUnit.weight" class="input-field"
|
||||
<input ref="weightInput" :value="huWeight" @blur="validateWeight('weight', $event)" class="input-field"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -49,12 +49,10 @@
|
|||
|
||||
<div class="caption-column">Pieces per HU</div>
|
||||
<div class="input-column">
|
||||
<tooltip text="Single pieces per HU">
|
||||
<div class="input-field-container">
|
||||
<input v-model="handlingUnit.content_unit_count" class="input-field"
|
||||
<input ref="unitCountInput" :value="huUnitCount" @blur="validateCount" class="input-field"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</tooltip>
|
||||
</div>
|
||||
|
||||
<div class="input-column-chk">
|
||||
|
|
@ -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 {
|
|||
<style scoped>
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, auto);
|
||||
grid-template-columns: auto 1fr auto auto;
|
||||
grid-template-rows: repeat(5, fit-content(0));
|
||||
gap: 1.2rem 1.6rem;
|
||||
flex: 1 1 auto;
|
||||
|
|
@ -184,7 +310,7 @@ export default {
|
|||
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);*/
|
||||
border: 0.2rem solid #E3EDFF;
|
||||
transition: all 0.1s ease;
|
||||
flex: 1 1 min-content;
|
||||
flex: 0 1 min(30rem, 100%);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,20 +3,22 @@
|
|||
<div class="caption-column">MEK_A [EUR]</div>
|
||||
<div class="input-column">
|
||||
<div class="input-field-container">
|
||||
<input v-model="price" class="input-field"
|
||||
<input :value="priceFormatted" @blur="validatePrice" class="input-field"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="caption-column">Oversea share [%]</div>
|
||||
<div class="input-column">
|
||||
<div class="input-field-container">
|
||||
<input v-model="overSeashare" class="input-field"
|
||||
<input :value="overSeaSharePercent" @blur="validateOverSeaShare" class="input-field"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="caption-column">Include FCA Fee</div>
|
||||
<div class="input-column">
|
||||
<tooltip text="Select if a additional FCA has to be added during calculation"><checkbox></checkbox></tooltip>
|
||||
<tooltip text="Select if a additional FCA has to be added during calculation">
|
||||
<checkbox :checked="includeFcaFee" @checkbox-changed="updateIncludeFcaFee"></checkbox>
|
||||
</tooltip>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -31,16 +33,67 @@ import Tooltip from "@/components/UI/Tooltip.vue";
|
|||
export default {
|
||||
name: "PriceEdit",
|
||||
components: {Tooltip, Checkbox},
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
price: 0.0,
|
||||
overSeashare: 0.0,
|
||||
addFcaFee: false
|
||||
emits: ['update:price', 'update:overSeaShare', 'update:includeFcaFee'],
|
||||
props: {
|
||||
price: {
|
||||
required: true,
|
||||
validator: (value) => value === null || typeof value === 'number'
|
||||
},
|
||||
overSeaShare: {
|
||||
required: true,
|
||||
validator: (value) => value === null || typeof value === 'number',
|
||||
},
|
||||
includeFcaFee: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
priceFormatted() {
|
||||
console.log('priceFormatted', this.price, this.price?.toFixed(2));
|
||||
return this.price?.toFixed(2) ?? '';
|
||||
},
|
||||
overSeaSharePercent() {
|
||||
return this.overSeaShare ? (this.overSeaShare * 100).toFixed(2) : '';
|
||||
}
|
||||
},
|
||||
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);
|
||||
},
|
||||
validatePrice(event) {
|
||||
const value = this.parseNumberFromString(event.target.value, 2);
|
||||
const validatedValue = Math.max(0, value);
|
||||
console.log('validate', event.target.value, validatedValue);
|
||||
|
||||
if (validatedValue !== this.price) {
|
||||
this.$emit('update:price', validatedValue);
|
||||
}
|
||||
|
||||
event.target.value = validatedValue.toFixed(2);
|
||||
},
|
||||
updateIncludeFcaFee(value) {
|
||||
this.$emit('update:includeFcaFee', value);
|
||||
},
|
||||
validateOverSeaShare(event) {
|
||||
const percentValue = this.parseNumberFromString(event.target.value, 4);
|
||||
|
||||
const validatedPercent = Math.max(0, Math.min(100, percentValue));
|
||||
const validatedDecimal = validatedPercent / 100;
|
||||
|
||||
if (validatedDecimal !== this.overSeaShare) {
|
||||
this.$emit('update:overSeaShare', validatedDecimal);
|
||||
}
|
||||
|
||||
event.target.value = validatedPercent.toFixed(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@
|
|||
weight="fill"></ph-user></span> {{ supplierName }}
|
||||
</div>
|
||||
<div class="caption-column">Address</div>
|
||||
<div class="input-field input-field-address">
|
||||
<div class="input-field select-node-input-field-address">
|
||||
<div class="supplier-flag">
|
||||
<flag iso="CN"/>
|
||||
<flag :iso="isoCode"/>
|
||||
</div>
|
||||
<div class="supplier-address">{{ supplierAddress }}</div>
|
||||
</div>
|
||||
|
|
@ -16,10 +16,15 @@
|
|||
<div class="input-field">{{ coordinatesDMS }}</div>
|
||||
</div>
|
||||
<div class="supplier-map">
|
||||
<img width="300px" src="https://www.galerie-braunbehrens.de/wp-content/uploads/2020/06/placeholder-google-maps.jpg" alt="map">
|
||||
<img width="300px"
|
||||
src="https://www.galerie-braunbehrens.de/wp-content/uploads/2020/06/placeholder-google-maps.jpg" alt="map">
|
||||
</div>
|
||||
<div class="footer">
|
||||
<icon-button icon="pencil-simple"></icon-button>
|
||||
<modal :state="selectSupplierModalState">
|
||||
<select-node @close="modalDialogClose"></select-node>
|
||||
</modal>
|
||||
<icon-button icon="plus" @click="openModal"></icon-button>
|
||||
<icon-button icon="pencil-simple" @click="openModal"></icon-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -30,10 +35,14 @@ import IconButton from "@/components/UI/IconButton.vue";
|
|||
import InputField from "@/components/UI/InputField.vue";
|
||||
import Flag from "@/components/UI/Flag.vue";
|
||||
import {PhUser} from "@phosphor-icons/vue";
|
||||
import ModalDialog from "@/components/UI/ModalDialog.vue";
|
||||
import SelectNode from "@/components/layout/node/SelectNode.vue";
|
||||
import Modal from "@/components/UI/Modal.vue";
|
||||
|
||||
export default {
|
||||
name: "SupplierView",
|
||||
components: {PhUser, Flag, InputField, IconButton},
|
||||
components: {Modal, SelectNode, ModalDialog, PhUser, Flag, InputField, IconButton},
|
||||
emits: ['updateSupplier'],
|
||||
props: {
|
||||
supplierName: {
|
||||
type: String,
|
||||
|
|
@ -43,11 +52,14 @@ export default {
|
|||
type: String,
|
||||
required: true
|
||||
},
|
||||
isoCode: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
supplierCoordinates: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
|
||||
},
|
||||
isUserSupplier: {
|
||||
type: Boolean,
|
||||
|
|
@ -62,7 +74,21 @@ export default {
|
|||
return `${this.convertToDMS(this.supplierCoordinates?.latitude, 'lat')}, ${this.convertToDMS(this.supplierCoordinates?.longitude, 'lng')}`;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectSupplierModalState: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
modalDialogClose(data) {
|
||||
this.selectSupplierModalState = false;
|
||||
if (data.action === 'accept') {
|
||||
this.$emit('updateSupplier', data);
|
||||
}
|
||||
},
|
||||
openModal() {
|
||||
this.selectSupplierModalState = true;
|
||||
},
|
||||
convertToDMS(coordinate, type) {
|
||||
|
||||
if (!coordinate)
|
||||
|
|
@ -118,7 +144,7 @@ export default {
|
|||
flex: 0 1 10rem
|
||||
}
|
||||
|
||||
.input-field-address {
|
||||
.select-node-input-field-address {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
align-items: center;
|
||||
|
|
@ -142,6 +168,7 @@ export default {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.supplier-container {
|
||||
|
|
@ -154,9 +181,10 @@ export default {
|
|||
}
|
||||
|
||||
.footer {
|
||||
grid-column: 1/-1;
|
||||
justify-self: end;
|
||||
align-self: end;
|
||||
display: flex;
|
||||
gap: 1.6rem;
|
||||
justify-self: flex-end;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -1,29 +1,49 @@
|
|||
<template>
|
||||
<div class="destination-item-container">
|
||||
<div class="destination-item-name">{{ destination.name }}</div>
|
||||
<div class="destination-item-row">
|
||||
<div class="destination-item-name"><flag :iso="destinationIsoCode" />{{ destination.destination_node.name }}</div>
|
||||
<div class="destination-item-annual">{{ destination.annual_amount }}</div>
|
||||
<div class="destination-item-route" v-if="hasRoute"><destination-route :showBorder="false" :route="selectedRoute"></destination-route></div>
|
||||
<div class="destination-item-route" v-else-if="isD2d"><div class="d2d-routing-container"><PhShippingContainer /> D2D routing</div></div>
|
||||
<div class="destination-item-route" v-else><div class="d2d-routing-container"><PhEmpty /> No route selected</div></div>
|
||||
<div class="destination-item-action"><icon-button icon="pencil-simple"></icon-button><icon-button icon="trash"></icon-button></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
import {usePremiseEditStore} from "@/store/premiseEdit.js";
|
||||
import {mapStores} from "pinia";
|
||||
import DestinationRoute from "@/components/layout/edit/destination/DestinationRoute.vue";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import Flag from "@/components/UI/Flag.vue";
|
||||
import {PhEmpty, PhShippingContainer} from "@phosphor-icons/vue";
|
||||
|
||||
export default {
|
||||
name: "DestinationItem",
|
||||
components: {PhEmpty, PhShippingContainer, Flag, IconButton, DestinationRoute},
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
destination: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(usePremiseEditStore),
|
||||
destination() {
|
||||
return { destination_node: 'Aschaffenburg'};
|
||||
}
|
||||
destinationIsoCode() {
|
||||
return this.destination.destination_node.country.iso_code;
|
||||
},
|
||||
hasRoute() {
|
||||
return this.destination.routes.some(route => route.is_selected);
|
||||
},
|
||||
isD2d() {
|
||||
return this.destination.is_d2d;
|
||||
},
|
||||
selectedRoute() {
|
||||
return this.destination.routes.find(route => route.is_selected);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
|
|
@ -31,4 +51,44 @@ export default {
|
|||
|
||||
<style scoped>
|
||||
|
||||
.d2d-routing-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.6rem 1.2rem;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.destination-item-name {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
color: #001D33;
|
||||
|
||||
}
|
||||
|
||||
.destination-item-row {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 2fr 5fr auto;
|
||||
gap: 1.6rem;
|
||||
padding: 2.4rem;
|
||||
border-bottom: 0.16rem solid #f3f4f6;
|
||||
align-items: center;
|
||||
transition: background-color 0.2s ease;
|
||||
font-size: 1.4rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.destination-item-row:hover {
|
||||
background-color: rgba(107, 134, 156, 0.05);
|
||||
}
|
||||
|
||||
|
||||
.destination-item-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
<template>
|
||||
<div class="destination-route-container">
|
||||
<div :class="containerClass">
|
||||
<ph-boat :size="18" v-if="isSea" class="destination-route-icon"></ph-boat>
|
||||
<ph-train :size="18" v-else-if="isRail" class="destination-route-icon"></ph-train>
|
||||
<ph-truck-trailer :size="18" v-else-if="isRoad" class="destination-route-icon"></ph-truck-trailer>
|
||||
<ph-navigation-arrow :size="18" v-else class="destination-route-icon"></ph-navigation-arrow>
|
||||
<span v-for="element in routeElements" class="destination-route-element"> {{ element }} </span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
import {PhBoat, PhNavigationArrow, PhTrain, PhTruckTrailer} from "@phosphor-icons/vue";
|
||||
|
||||
export default {
|
||||
name: "DestinationRoute",
|
||||
components: {PhNavigationArrow, PhTrain, PhTruckTrailer, PhBoat},
|
||||
props: {
|
||||
route: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
showBorder: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
routeElements() {
|
||||
const routeElem = this.route.transit_nodes.map(n => n.external_mapping_id);
|
||||
return routeElem;
|
||||
},
|
||||
isSea() {
|
||||
return this.route.type === "SEA";
|
||||
},
|
||||
isRoad() {
|
||||
return this.route.type === "ROAD";
|
||||
},
|
||||
isRail() {
|
||||
return this.route.type === "RAIL";
|
||||
},
|
||||
containerClass() {
|
||||
|
||||
let classes = ['destination-route-inner-container'];
|
||||
|
||||
if(this.showBorder) {
|
||||
classes.push('destination-route-inner-container--bordered')
|
||||
}
|
||||
|
||||
if(this.selected && this.showBorder) {
|
||||
classes.push('destination-route-inner-container--selected')
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.destination-route-inner-container {
|
||||
display: flex;
|
||||
color: #6B869C;
|
||||
background: transparent;
|
||||
border-radius: 0.8rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
transition: all 0.1s ease;
|
||||
flex: 0 1 fit-content;
|
||||
}
|
||||
|
||||
.destination-route-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
|
||||
.destination-route-inner-container--bordered {
|
||||
border: 0.2rem solid #E3EDFF;
|
||||
}
|
||||
|
||||
.destination-route-inner-container--selected {
|
||||
background: #EEF4FF;
|
||||
border: 0.2rem solid #8DB3FE;
|
||||
}
|
||||
|
||||
.destination-route-inner-container--bordered:hover {
|
||||
background: #EEF4FF;
|
||||
border: 0.2rem solid #8DB3FE;
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
|
||||
.destination-route-icon {
|
||||
margin-right: 0.8rem;
|
||||
}
|
||||
|
||||
.destination-route-element {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.destination-route-element:not(:last-child)::after {
|
||||
content: ">";
|
||||
font-size: 1.4rem;
|
||||
margin-left: 0.4rem;
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -14,6 +14,9 @@
|
|||
<div class="input-field-caption">Coordinates:</div>
|
||||
<div>{{ nodeCoordinates }}</div>
|
||||
</div>
|
||||
<div class="create-new-node-map">
|
||||
<img width="300px" src="https://www.galerie-braunbehrens.de/wp-content/uploads/2020/06/placeholder-google-maps.jpg" alt="map">
|
||||
</div>
|
||||
<div class="create-new-node-footer">
|
||||
<basic-button variant="primary" :show-icon="false" :disabled="unverified">OK</basic-button>
|
||||
<basic-button @click.prevent="cancel" variant="secondary" :show-icon="false">Cancel</basic-button>
|
||||
|
|
@ -56,6 +59,11 @@ export default {
|
|||
|
||||
<style scoped>
|
||||
|
||||
.create-new-node-map {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sub-header {
|
||||
font-weight: normal;
|
||||
font-size: 1.4rem;
|
||||
|
|
|
|||
196
src/frontend/src/components/layout/node/SelectNode.vue
Normal file
196
src/frontend/src/components/layout/node/SelectNode.vue
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
<template>
|
||||
<div class="select-node-modal-container">
|
||||
<h3 class="sub-header">Select supplier</h3>
|
||||
<div class="select-node-container">
|
||||
<div class="select-node-caption-column">Supplier</div>
|
||||
<div class="select-node-input-column select-node-input-field-suppliername">
|
||||
<div class="select-node-input-field-suppliername-searchbar">
|
||||
<autosuggest-searchbar :fetch-suggestions="fetchSupplier"
|
||||
placeholder="Find supplier..."
|
||||
title-resolver="name"
|
||||
subtitle-resolver="address"
|
||||
:flag-resolver="resolveFlag"
|
||||
variant="flags"
|
||||
@selected="selected"
|
||||
ref="searchbar"
|
||||
>
|
||||
</autosuggest-searchbar>
|
||||
</div>
|
||||
</div>
|
||||
<div class="select-node-caption-column" v-if="nodeSelected">Address</div>
|
||||
<div class="select-node-input-column select-node-input-field-address" v-if="nodeSelected">
|
||||
<div class="supplier-flag">
|
||||
<flag :iso="isoCode"/>
|
||||
</div>
|
||||
<div class="supplier-address">{{ supplierAddress }}</div>
|
||||
</div>
|
||||
<div class="select-node-caption-column" v-if="nodeSelected">Coordinates</div>
|
||||
<div class="select-node-input-column" v-if="nodeSelected">{{ coordinatesDMS }}</div>
|
||||
<div class="select-node-checkbox" v-if="nodeSelected">
|
||||
<checkbox :checked="updateMasterData" @checkbox-changed="checkboxChanged">update master data</checkbox>
|
||||
</div>
|
||||
<div class="select-node-footer">
|
||||
<basic-button :show-icon="false" :disabled="!nodeSelected" @click="action('accept')">OK</basic-button>
|
||||
<basic-button variant="secondary" :show-icon="false" @click="action('cancel')">Cancel</basic-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||
import InputField from "@/components/UI/InputField.vue";
|
||||
import Flag from "@/components/UI/Flag.vue";
|
||||
import AutosuggestSearchbar from "@/components/UI/AutoSuggestSearchBar.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {useNodeStore} from "@/store/node.js";
|
||||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
|
||||
export default {
|
||||
name: "SelectNode",
|
||||
components: {Checkbox, AutosuggestSearchbar, Flag, InputField, BasicButton},
|
||||
emits: ['close'],
|
||||
methods: {
|
||||
action(action) {
|
||||
this.$emit('close', {action: action, nodeId: this.node?.id, updateMasterData: this.updateMasterData});
|
||||
},
|
||||
checkboxChanged(value) {
|
||||
this.updateMasterData = value;
|
||||
},
|
||||
async fetchSupplier(query) {
|
||||
console.log("Fetching supplier for query: " + query);
|
||||
await this.nodeStore.setQuery({searchTerm: query, nodeType: 'SOURCE', includeUserNode: true});
|
||||
return this.nodeStore.nodes;
|
||||
},
|
||||
resolveFlag(node) {
|
||||
return node.country.iso_code;
|
||||
},
|
||||
selected(node) {
|
||||
console.log("Selected node: ", node);
|
||||
this.$refs.searchbar.clearSuggestions();
|
||||
this.node = node;
|
||||
},
|
||||
convertToDMS(coordinate, type) {
|
||||
|
||||
if (!coordinate)
|
||||
return '';
|
||||
|
||||
let direction;
|
||||
if (type === 'lat') {
|
||||
direction = coordinate >= 0 ? 'N' : 'S';
|
||||
} else {
|
||||
direction = coordinate >= 0 ? 'E' : 'W';
|
||||
}
|
||||
|
||||
// Arbeite mit Absolutwert
|
||||
const abs = Math.abs(coordinate);
|
||||
|
||||
// Grad (ganzzahliger Teil)
|
||||
const degrees = Math.floor(abs);
|
||||
|
||||
// Minuten
|
||||
const minutesFloat = (abs - degrees) * 60;
|
||||
const minutes = minutesFloat.toFixed(4);
|
||||
|
||||
|
||||
return `${degrees}° ${minutes}' ${direction}`;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
node: null,
|
||||
updateMasterData: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useNodeStore),
|
||||
supplierAddress() {
|
||||
return this.node?.address ?? '';
|
||||
},
|
||||
nodeSelected() {
|
||||
return this.node != null;
|
||||
},
|
||||
isoCode() {
|
||||
return this.node?.country.iso_code ?? 'DE';
|
||||
},
|
||||
coordinatesDMS() {
|
||||
return `${this.convertToDMS(this.node?.location?.latitude, 'lat')}, ${this.convertToDMS(this.node?.location?.longitude, 'lng')}`;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.select-node-input-column {
|
||||
font-size: 1.4rem;
|
||||
color: #6b7280;
|
||||
max-width: 30rem;
|
||||
}
|
||||
|
||||
.select-node-input-field-suppliername {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.select-node-input-field-suppliername-searchbar {
|
||||
flex: 1 1 auto;
|
||||
min-width: 50rem;
|
||||
}
|
||||
|
||||
.select-node-modal-container {
|
||||
width: 60rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.select-node-container {
|
||||
flex: 1 1 auto;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: repeat(3, fit-content(0));
|
||||
align-content: center;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.select-node-caption-column {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
align-self: center;
|
||||
justify-self: end;
|
||||
color: #001D33
|
||||
}
|
||||
|
||||
.select-node-footer {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.select-node-checkbox {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.select-node-input-field-address {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.supplier-address {
|
||||
font-size: 1.4rem;
|
||||
color: #6b7280;
|
||||
max-width: 30rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -3,38 +3,71 @@
|
|||
<div class="header-container">
|
||||
<h2 class="page-header">Edit Calculation</h2>
|
||||
<div class="header-controls">
|
||||
<basic-button :show-icon="false" variant="secondary">Close</basic-button>
|
||||
<basic-button :show-icon="true" icon="Calculator" variant="primary">Calculate & close</basic-button>
|
||||
<basic-button :show-icon="false" :disabled="premiseEditStore.selectedLoading" variant="secondary">Close
|
||||
</basic-button>
|
||||
<basic-button :show-icon="true" :disabled="premiseEditStore.selectedLoading || premiseEditStore.selectedEmpty"
|
||||
icon="Calculator" variant="primary">Calculate & close
|
||||
</basic-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<notification-bar v-if="premiseEditStore.error != null" variant="exception" icon="x"
|
||||
@icon-clicked="premiseEditStore.error = null">
|
||||
<div class="errorCode">{{ premiseEditStore.error.code }}</div>
|
||||
{{ premiseEditStore.error.message }}
|
||||
</notification-bar>
|
||||
|
||||
<div v-if="premiseEditStore.selectedLoading" class="edit-calculation-spinner-container">
|
||||
<box class="edit-calculation-spinner">
|
||||
<spinner></spinner>
|
||||
</box>
|
||||
</div>
|
||||
<div v-else-if="premiseEditStore.selectedEmpty" class="edit-calculation-spinner-container">
|
||||
<box class="edit-calculation-spinner">No calculation found.</box>
|
||||
</div>
|
||||
<div v-else>
|
||||
|
||||
<h3 class="sub-header">Master data</h3>
|
||||
<div class="master-data-container">
|
||||
<box class="master-data-item master-data-stretched-item">
|
||||
<supplier-view :supplier-address="premise.supplier.address"
|
||||
:supplier-name="premise.supplier.name"
|
||||
:supplier-coordinates="premise.supplier.location"></supplier-view>
|
||||
:supplier-coordinates="premise.supplier.location"
|
||||
:iso-code="premise.supplier.country.iso_code"
|
||||
@update-supplier="updateSupplier"></supplier-view>
|
||||
</box>
|
||||
<box class="master-data-item master-data-stretched-item master-data-packaging">
|
||||
<packaging-edit :handling-unit="premise.handling_unit" v-model:stackable="premise.is_stackable" v-model:mixable="premise.is_mixable"></packaging-edit>
|
||||
<packaging-edit v-model:length="premise.handling_unit.length"
|
||||
v-model:width="premise.handling_unit.width"
|
||||
v-model:height="premise.handling_unit.height"
|
||||
v-model:weight="premise.handling_unit.weight"
|
||||
v-model:weight-unit="premise.handling_unit.weight_unit"
|
||||
v-model:dimension-unit="premise.handling_unit.dimension_unit"
|
||||
v-model:unit-count="premise.handling_unit.content_unit_count"
|
||||
|
||||
v-model:stackable="premise.is_stackable"
|
||||
v-model:mixable="premise.is_mixable"></packaging-edit>
|
||||
</box>
|
||||
<box class="master-data-item">
|
||||
<material-edit :part-number="premise.material.part_number"
|
||||
:description="premise.material.name"
|
||||
:id="premise.material.id"
|
||||
:hs-code="premise.material.hs_code"
|
||||
:tariff-rate="premise.tariff_rate"></material-edit>
|
||||
v-model:hs-code="premise.hs_code"
|
||||
v-model:tariff-rate="premise.tariff_rate"
|
||||
@update-material="updateMaterial"></material-edit>
|
||||
</box>
|
||||
<box class="master-data-item">
|
||||
<price-edit></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"></price-edit>
|
||||
</box>
|
||||
|
||||
</div>
|
||||
|
||||
<h3 class="sub-header">Destinations & routes</h3>
|
||||
<destination-list-view></destination-list-view>
|
||||
<destination-list-view :destinations="premise.destinations"></destination-list-view>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
@ -48,21 +81,54 @@ import PriceEdit from "@/components/layout/edit/PriceEdit.vue";
|
|||
import DestinationListView from "@/components/layout/edit/DestinationListView.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {usePremiseEditStore} from "@/store/premiseEdit.js";
|
||||
import Spinner from "@/components/UI/Spinner.vue";
|
||||
import NotificationBar from "@/components/UI/NotificationBar.vue";
|
||||
|
||||
export default {
|
||||
name: "SingleEdit",
|
||||
components: {DestinationListView, PriceEdit, PackagingEdit, MaterialEdit, Box, SupplierView, BasicButton},
|
||||
components: {
|
||||
NotificationBar,
|
||||
Spinner, DestinationListView, PriceEdit, PackagingEdit, MaterialEdit, Box, SupplierView, BasicButton
|
||||
},
|
||||
computed: {
|
||||
...mapStores(usePremiseEditStore),
|
||||
premise() {
|
||||
return this.premiseEditStore.selectedPremise;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateMaterial(id, action) {
|
||||
console.log(id, action);
|
||||
this.premiseEditStore.setMaterial(id, action === 'updateMasterData');
|
||||
},
|
||||
updateSupplier(data) {
|
||||
this.premiseEditStore.setSupplier(data.nodeId, data.updateMasterData);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.premiseEditStore.selectPremise(parseInt(this.$route.params.id));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.edit-calculation-spinner-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1 1 30rem
|
||||
}
|
||||
|
||||
.edit-calculation-spinner {
|
||||
font-size: 1.6rem;
|
||||
width: 24rem;
|
||||
height: 12rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.edit-calculation-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
20
src/frontend/src/store/customs.js
Normal file
20
src/frontend/src/store/customs.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import {config} from '@/config'
|
||||
|
||||
export const useCustomsStore = defineStore('customs', {
|
||||
state() {
|
||||
return {
|
||||
hsCodes: [],
|
||||
query: null
|
||||
}
|
||||
},
|
||||
getters: {},
|
||||
actions: {
|
||||
async setQuery(query) {
|
||||
this.query = query;
|
||||
},
|
||||
async queryCustomApi(query) {
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
63
src/frontend/src/store/material.js
Normal file
63
src/frontend/src/store/material.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import {config} from '@/config'
|
||||
|
||||
export const useMaterialStore = defineStore('material', {
|
||||
state() {
|
||||
return {
|
||||
materials: [],
|
||||
loading: false,
|
||||
empty: true,
|
||||
error: null,
|
||||
query: {},
|
||||
pagination: {}
|
||||
}
|
||||
},
|
||||
getters: {},
|
||||
actions: {
|
||||
async setQuery(query) {
|
||||
this.query = query;
|
||||
await this.updateMaterials();
|
||||
},
|
||||
async updateMaterials() {
|
||||
|
||||
this.premises = [];
|
||||
this.loading = true;
|
||||
this.empty = true;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (this.query.searchTerm)
|
||||
params.append('filter', this.query.searchTerm);
|
||||
|
||||
|
||||
const url = `${config.backendUrl}/materials/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||
const response = await fetch(url).catch(e => {
|
||||
this.error = {code: 'Network error.', message: "Please check your internet connection.", trace: null}
|
||||
this.loading = false;
|
||||
throw e;
|
||||
});
|
||||
|
||||
const data = await response.json().catch(e => {
|
||||
this.error = {
|
||||
code: 'Malformed response',
|
||||
message: "Malformed server response. Please contact support.",
|
||||
trace: null
|
||||
}
|
||||
this.loading = false;
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
this.error = {code: data.error.title, message: data.error.message, trace: data.error.details}
|
||||
this.loading = false;
|
||||
console.log(data);
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
this.empty = data.length === 0;
|
||||
this.materials = data;
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -1,101 +1,174 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import {config} from '@/config'
|
||||
import logger from "@vitejs/plugin-vue";
|
||||
|
||||
export const usePremiseEditStore = defineStore('premiseEdit', {
|
||||
state() {
|
||||
return {
|
||||
premisses: null,
|
||||
currentId: 7,
|
||||
selectedPremise:
|
||||
{
|
||||
"id": 7,
|
||||
"material": {
|
||||
"id": 2,
|
||||
"name": "planet gear carrier blank 'stage 1",
|
||||
"part_number": "8222640822",
|
||||
"hs_code": "84839089"
|
||||
},
|
||||
"supplier": {
|
||||
"id": 16,
|
||||
"name": "Linde (China) Forklift Truck (Supplier)",
|
||||
"country": {
|
||||
"name": "China",
|
||||
"id": 48,
|
||||
"iso_code": "CN",
|
||||
"region_code": "APAC"
|
||||
},
|
||||
"address": "Linde (China) Forklift Truck Corp. Ltd.\n 1258 Gonghexin Road\n 闸北区, 上海市 200070\n People's Republic of China",
|
||||
"types": [
|
||||
"source"
|
||||
],
|
||||
"location": {
|
||||
"latitude": 31.2872,
|
||||
"longitude": 121.4581
|
||||
},
|
||||
"external_mapping_id": "LX",
|
||||
"is_user_node": false,
|
||||
"is_deprecated": false
|
||||
},
|
||||
"destinations": [
|
||||
{
|
||||
"id": 1,
|
||||
"routes": [],
|
||||
"repackaging_costs": null,
|
||||
"handling_costs": null,
|
||||
"disposal_costs": null,
|
||||
"destination_node": {
|
||||
"id": 19,
|
||||
"name": "Aschaffenburg (KION plant)",
|
||||
"country": {
|
||||
"name": "Germany",
|
||||
"id": 57,
|
||||
"iso_code": "DE",
|
||||
"region_code": "EMEA"
|
||||
},
|
||||
"address": "Thüngenstraße 1, 63743 Aschaffenburg, Germany",
|
||||
"types": [
|
||||
"destination",
|
||||
"source"
|
||||
],
|
||||
"location": {
|
||||
"latitude": 49.9763,
|
||||
"longitude": 9.1432
|
||||
},
|
||||
"external_mapping_id": "AB",
|
||||
"is_user_node": false,
|
||||
"is_deprecated": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"handling_unit": {
|
||||
"id": null,
|
||||
"type": "HU",
|
||||
"length": 1200.0,
|
||||
"width": 700.0,
|
||||
"height": 790.0,
|
||||
"weight": 677000.0,
|
||||
"dimension_unit": "mm",
|
||||
"weight_unit": "g",
|
||||
"content_unit_count": 3,
|
||||
"is_deprecated": null
|
||||
},
|
||||
"is_mixable": true,
|
||||
"is_stackable": true
|
||||
}
|
||||
loading: false,
|
||||
empty: false,
|
||||
error: null,
|
||||
currentId: null,
|
||||
selectedLoading: false,
|
||||
selectedEmpty: false,
|
||||
selectedPremise: null,
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
showData: (state) => true,
|
||||
showEmpty: (state) => false,
|
||||
getDestinationsById: (state) => {
|
||||
return (id) => selectedPremises.find(d => d.id === id);
|
||||
},
|
||||
|
||||
},
|
||||
actions: {
|
||||
setCurrentId(id) {
|
||||
//this.currentId = id;
|
||||
//this.selectedPremises = this.premisses.find(p => p.id === id);
|
||||
async setSupplier(id, updateMasterData){
|
||||
console.log("setSupplier");
|
||||
const body = { supplier_node_id: id, update_master_data: updateMasterData };
|
||||
const url = `${config.backendUrl}/calculation/supplier/`;
|
||||
await this.setData(url, body);
|
||||
},
|
||||
async setMaterial(id, updateMasterData){
|
||||
console.log("setMaterial");
|
||||
const body = { material_id: id, update_master_data: updateMasterData };
|
||||
const url = `${config.backendUrl}/calculation/material/`;
|
||||
await this.setData(url, body);
|
||||
},
|
||||
async setData(url, body) {
|
||||
|
||||
let premiseIds = null;
|
||||
|
||||
/* todo multiple premisses should be selectable */
|
||||
if(this.currentId) {
|
||||
premiseIds = [ this.currentId ];
|
||||
this.selectedLoading = true;
|
||||
this.selectedEmpty = true;
|
||||
this.selectedPremise = null;
|
||||
} else if(this.premisses) {
|
||||
premiseIds = this.premisses.map(p => p.id);
|
||||
this.loading = true;
|
||||
this.empty = true;
|
||||
this.premisses = [];
|
||||
}
|
||||
|
||||
if(null !== premiseIds) {
|
||||
body.premise_id = premiseIds;
|
||||
console.log(url, body)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
}).catch(e => {
|
||||
this.error = {
|
||||
code: 'Network error.',
|
||||
message: "Please check your internet connection.",
|
||||
trace: null
|
||||
}
|
||||
this.loading = false;
|
||||
throw e;
|
||||
});
|
||||
|
||||
|
||||
const data = await response.json().catch(e => {
|
||||
this.error = {
|
||||
code: 'Malformed response',
|
||||
message: "Malformed server response. Please contact support.",
|
||||
trace: null
|
||||
}
|
||||
this.loading = false;
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
this.error = {code: data.error.title, message: data.error.message, trace: data.error.details}
|
||||
this.loading = false;
|
||||
console.log(data);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(data, this.currentId);
|
||||
|
||||
this.selectedPremise = data.find(p => p.id === this.currentId);
|
||||
this.selectedLoading = false;
|
||||
this.selectedEmpty = ((this.selectedPremise ?? null) === null);
|
||||
|
||||
this.loading = false;
|
||||
this.empty = data.length === 0;
|
||||
this.premisses = data;
|
||||
|
||||
}
|
||||
},
|
||||
async selectPremise(id) {
|
||||
|
||||
this.currentId = null;
|
||||
this.selectedLoading = true;
|
||||
this.selectedEmpty = true;
|
||||
this.selectedPremise = this.premisses?.find(p => p.id === id);
|
||||
|
||||
if ((this.selectedPremise ?? null) === null) {
|
||||
try {
|
||||
await this.reloadPremisses([id]);
|
||||
} catch (e) {
|
||||
this.selectedLoading = false;
|
||||
throw e;
|
||||
}
|
||||
this.selectedPremise = this.premisses?.find(p => p.id === id);
|
||||
}
|
||||
this.currentId = id;
|
||||
this.selectedEmpty = ((this.selectedPremise ?? null) === null);
|
||||
this.selectedLoading = false;
|
||||
},
|
||||
async reloadPremisses(ids) {
|
||||
|
||||
const reload = this.premisses ? ids.every((id) => this.premisses.find(d => d.id === id)) : true;
|
||||
|
||||
if (reload) {
|
||||
this.premises = [];
|
||||
this.loading = true;
|
||||
this.empty = true;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
||||
params.append('premissIds', ids.join(', '));
|
||||
|
||||
|
||||
const url = `${config.backendUrl}/calculation/edit/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||
const response = await fetch(url).catch(e => {
|
||||
this.error = {
|
||||
code: 'Network error.',
|
||||
message: "Please check your internet connection.",
|
||||
trace: null
|
||||
}
|
||||
this.loading = false;
|
||||
throw e;
|
||||
});
|
||||
|
||||
const data = await response.json().catch(e => {
|
||||
this.error = {
|
||||
code: 'Malformed response',
|
||||
message: "Malformed server response. Please contact support.",
|
||||
trace: null
|
||||
}
|
||||
this.loading = false;
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
this.error = {code: data.error.title, message: data.error.message, trace: data.error.details}
|
||||
this.loading = false;
|
||||
console.log(data);
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
this.empty = data.length === 0;
|
||||
|
||||
console.log(data);
|
||||
|
||||
this.premisses = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -170,7 +170,7 @@ public class PremiseController {
|
|||
return ResponseEntity.ok(changeSupplierService.setSupplier(setSupplierDTO));
|
||||
}
|
||||
|
||||
@PutMapping("/material")
|
||||
@PutMapping({"/material", "/material/"})
|
||||
public ResponseEntity<List<PremiseDetailDTO>> setMaterial(@RequestBody SetDataDTO setMaterialDTO) {
|
||||
return ResponseEntity.ok(changeMaterialService.setMaterial(setMaterialDTO));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,16 @@ package de.avatic.lcc.dto.calculation;
|
|||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import de.avatic.lcc.dto.generic.NodeDTO;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
public class DestinationDTO {
|
||||
|
||||
private Integer id;
|
||||
|
||||
@JsonProperty("annual_amount")
|
||||
private Number annualAmount;
|
||||
|
||||
@JsonProperty("repackaging_costs")
|
||||
private Number repackingCosts;
|
||||
|
||||
|
|
@ -21,6 +25,28 @@ public class DestinationDTO {
|
|||
@JsonProperty("destination_node")
|
||||
private NodeDTO destinationNode;
|
||||
|
||||
@JsonProperty("is_d2d")
|
||||
private Boolean d2d;
|
||||
|
||||
@JsonProperty("rate_d2d")
|
||||
private BigDecimal rateD2d;
|
||||
|
||||
public Boolean getD2d() {
|
||||
return d2d;
|
||||
}
|
||||
|
||||
public BigDecimal getRateD2d() {
|
||||
return rateD2d;
|
||||
}
|
||||
|
||||
public Number getAnnualAmount() {
|
||||
return annualAmount;
|
||||
}
|
||||
|
||||
public void setAnnualAmount(Number annualAmount) {
|
||||
this.annualAmount = annualAmount;
|
||||
}
|
||||
|
||||
private List<RouteDTO> routes;
|
||||
|
||||
public Integer getId() {
|
||||
|
|
@ -70,4 +96,12 @@ public class DestinationDTO {
|
|||
public void setRoutes(List<RouteDTO> routes) {
|
||||
this.routes = routes;
|
||||
}
|
||||
|
||||
public void setD2d(Boolean d2d) {
|
||||
this.d2d = d2d;
|
||||
}
|
||||
|
||||
public void setRateD2d(BigDecimal rateD2d) {
|
||||
this.rateD2d = rateD2d;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,12 +28,62 @@ public class PremiseDetailDTO {
|
|||
|
||||
private List<DestinationDTO> destinations;
|
||||
|
||||
// TODO: missing values:
|
||||
// "material_cost": "number",
|
||||
// "is_fca_enabled": "boolean",
|
||||
// "oversea_share": "number",
|
||||
// "hs_code": "string",
|
||||
// "tariff_rate": "number",
|
||||
@JsonProperty("material_cost")
|
||||
private Double materialCost;
|
||||
|
||||
@JsonProperty("oversea_share")
|
||||
private Double overseaShare;
|
||||
|
||||
@JsonProperty("hs_code")
|
||||
private String hsCode;
|
||||
|
||||
@JsonProperty("tariff_rate")
|
||||
private Double tariffRate;
|
||||
|
||||
@JsonProperty("is_fca_enabled")
|
||||
private Boolean isFcaEnabled;
|
||||
|
||||
|
||||
public Double getMaterialCost() {
|
||||
return materialCost;
|
||||
}
|
||||
|
||||
public void setMaterialCost(Double materialCost) {
|
||||
this.materialCost = materialCost;
|
||||
}
|
||||
|
||||
public Double getOverseaShare() {
|
||||
return overseaShare;
|
||||
}
|
||||
|
||||
public void setOverseaShare(Double overseaShare) {
|
||||
this.overseaShare = overseaShare;
|
||||
}
|
||||
|
||||
public String getHsCode() {
|
||||
return hsCode;
|
||||
}
|
||||
|
||||
public void setHsCode(String hsCode) {
|
||||
this.hsCode = hsCode;
|
||||
}
|
||||
|
||||
public Double getTariffRate() {
|
||||
return tariffRate;
|
||||
}
|
||||
|
||||
public void setTariffRate(Double tariffRate) {
|
||||
this.tariffRate = tariffRate;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public Boolean getFcaEnabled() {
|
||||
return isFcaEnabled;
|
||||
}
|
||||
|
||||
public void setFcaEnabled(Boolean fcaEnabled) {
|
||||
isFcaEnabled = fcaEnabled;
|
||||
}
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ public class SetDataDTO {
|
|||
@JsonProperty("update_master_data")
|
||||
boolean updateMasterData;
|
||||
|
||||
@JsonProperty(defaultValue = "supplier_node_id", required = false)
|
||||
@JsonProperty(value = "supplier_node_id", required = false)
|
||||
Integer supplierNodeId;
|
||||
|
||||
@JsonProperty("is_user_supplier_node")
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ public class RouteNode {
|
|||
|
||||
private String address;
|
||||
|
||||
private String externalMappingId;
|
||||
|
||||
private Boolean isDestination;
|
||||
|
||||
private Boolean isIntermediate;
|
||||
|
|
@ -29,6 +31,13 @@ public class RouteNode {
|
|||
|
||||
private Integer countryId;
|
||||
|
||||
public String getExternalMappingId() {
|
||||
return externalMappingId;
|
||||
}
|
||||
|
||||
public void setExternalMappingId(String externalMappingId) {
|
||||
this.externalMappingId = externalMappingId;
|
||||
}
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ public class RouteNodeRepository {
|
|||
public Integer insert(RouteNode node) {
|
||||
String sql = """
|
||||
INSERT INTO premise_route_node (name, address, geo_lat, geo_lng, is_destination, is_intermediate,
|
||||
is_source, node_id, user_node_id, is_outdated, country_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
is_source, node_id, user_node_id, is_outdated, country_id, external_mapping_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""";
|
||||
KeyHolder keyHolder = new GeneratedKeyHolder();
|
||||
|
||||
|
|
@ -67,6 +67,7 @@ public class RouteNodeRepository {
|
|||
ps.setObject(9, node.getUserNodeId(), java.sql.Types.INTEGER);
|
||||
ps.setBoolean(10, Boolean.TRUE.equals(node.getOutdated()));
|
||||
ps.setObject(11, node.getCountryId(), java.sql.Types.INTEGER);
|
||||
ps.setString(12, node.getExternalMappingId());
|
||||
return ps;
|
||||
}, keyHolder);
|
||||
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ public class DestinationService {
|
|||
Integer userId = 1;
|
||||
Optional<Integer> ownerId = destinationRepository.getOwnerIdById(id);
|
||||
|
||||
if (ownerId.isPresent() && ownerId.get().equals(userId)) {
|
||||
if (userId == 1 /* todo remove */ || ownerId.isPresent() && ownerId.get().equals(userId)) {
|
||||
List<Route> routes = routeRepository.getByDestinationId(id);
|
||||
|
||||
for (var route : routes) {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import de.avatic.lcc.model.packaging.PackagingDimension;
|
|||
import de.avatic.lcc.model.premises.Premise;
|
||||
import de.avatic.lcc.model.properties.PackagingProperty;
|
||||
import de.avatic.lcc.model.properties.PackagingPropertyMappingId;
|
||||
import de.avatic.lcc.repositories.MaterialRepository;
|
||||
import de.avatic.lcc.repositories.NodeRepository;
|
||||
import de.avatic.lcc.repositories.packaging.PackagingDimensionRepository;
|
||||
import de.avatic.lcc.repositories.packaging.PackagingPropertiesRepository;
|
||||
|
|
@ -30,8 +31,9 @@ public class ChangeMaterialService {
|
|||
private final PackagingRepository packagingRepository;
|
||||
private final PackagingDimensionRepository packagingDimensionRepository;
|
||||
private final PackagingPropertiesRepository packagingPropertiesRepository;
|
||||
private final MaterialRepository materialRepository;
|
||||
|
||||
public ChangeMaterialService(PremiseRepository premiseRepository, PremisesService premisesService, CustomApiService customApiService, NodeRepository nodeRepository, UserNodeRepository userNodeRepository, PackagingRepository packagingRepository, PackagingDimensionRepository packagingDimensionRepository, PackagingPropertiesRepository packagingPropertiesRepository) {
|
||||
public ChangeMaterialService(PremiseRepository premiseRepository, PremisesService premisesService, CustomApiService customApiService, NodeRepository nodeRepository, UserNodeRepository userNodeRepository, PackagingRepository packagingRepository, PackagingDimensionRepository packagingDimensionRepository, PackagingPropertiesRepository packagingPropertiesRepository, MaterialRepository materialRepository) {
|
||||
this.premiseRepository = premiseRepository;
|
||||
this.premisesService = premisesService;
|
||||
this.customApiService = customApiService;
|
||||
|
|
@ -40,6 +42,7 @@ public class ChangeMaterialService {
|
|||
this.packagingRepository = packagingRepository;
|
||||
this.packagingDimensionRepository = packagingDimensionRepository;
|
||||
this.packagingPropertiesRepository = packagingPropertiesRepository;
|
||||
this.materialRepository = materialRepository;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
|
@ -52,6 +55,7 @@ public class ChangeMaterialService {
|
|||
if (materialId == null || premiseIds == null || premiseIds.isEmpty())
|
||||
throw new IllegalArgumentException("No supplier supplierNodeId or premises given");
|
||||
|
||||
|
||||
// get all premises first.
|
||||
List<Premise> allPremises = premiseRepository.getPremisesById(premiseIds);
|
||||
|
||||
|
|
@ -71,10 +75,13 @@ public class ChangeMaterialService {
|
|||
premisesToBeDeleted.addAll(premisesToProcess.stream().map(p -> premiseRepository.getCollidingPremisesOnChange(userId, p.getId(), materialId, p.getSupplierNodeId())).flatMap(List::stream).toList());
|
||||
|
||||
if(dto.isUpdateMasterData()) {
|
||||
|
||||
var material = materialRepository.getById(materialId).orElseThrow(() -> new IllegalArgumentException("No material with id " + materialId));
|
||||
|
||||
for (var premise : premisesToProcess) {
|
||||
var countryId = dto.isUserSupplierNode() ? userNodeRepository.getById(premise.getUserSupplierNodeId()).orElseThrow().getCountryId() : nodeRepository.getById(premise.getSupplierNodeId()).orElseThrow().getCountryId();
|
||||
var tariffRate = customApiService.getTariffRate(premise.getHsCode(), countryId);
|
||||
premiseRepository.updateTariffRate(premise.getId(), tariffRate);
|
||||
var tariffRate = customApiService.getTariffRate(material.getHsCode(), countryId);
|
||||
premiseRepository.updateMaterial(Collections.singletonList(premise.getId()), material.getHsCode(), tariffRate);
|
||||
|
||||
if (!dto.isUserSupplierNode()) {
|
||||
var packaging = packagingRepository.getByMaterialIdAndSupplierId(dto.getMaterialId(), premise.getSupplierNodeId());
|
||||
|
|
|
|||
|
|
@ -137,10 +137,19 @@ public class RoutingService {
|
|||
routeNode.setDestination(node.getDestination() != null ? node.getDestination() : false);
|
||||
routeNode.setSource(node.getSource() != null ? node.getSource() : false);
|
||||
routeNode.setOutdated(node.getDeprecated());
|
||||
routeNode.setExternalMappingId(isUserNode ? generateShortName(node.getName()) : node.getExternalMappingId());
|
||||
|
||||
return routeNode;
|
||||
}
|
||||
|
||||
private String generateShortName(String name) {
|
||||
if(name.length() < 13){
|
||||
return name;
|
||||
}
|
||||
|
||||
return name.substring(0, 10) + " ...";
|
||||
}
|
||||
|
||||
private Route mapRoute(TemporaryRouteObject route) {
|
||||
Route routeObj = new Route();
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ public class DestinationTransformer {
|
|||
dto.setHandlingCosts(destination.getHandlingCost());
|
||||
dto.setDestinationNode(nodeTransformer.toNodeDTO(nodeRepository.getById(destination.getDestinationNodeId()).orElseThrow()));
|
||||
dto.setRoutes(routeRepository.getByDestinationId(destination.getId()).stream().map(routeTransformer::toRouteDTO).toList());
|
||||
dto.setAnnualAmount(destination.getAnnualAmount());
|
||||
|
||||
dto.setD2d(destination.getD2d());
|
||||
dto.setRateD2d(destination.getRateD2d());
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package de.avatic.lcc.service.transformer.premise;
|
|||
|
||||
import de.avatic.lcc.dto.calculation.PremiseDTO;
|
||||
import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO;
|
||||
import de.avatic.lcc.dto.generic.DimensionDTO;
|
||||
import de.avatic.lcc.dto.generic.LocationDTO;
|
||||
import de.avatic.lcc.dto.generic.NodeDTO;
|
||||
import de.avatic.lcc.dto.generic.NodeType;
|
||||
|
|
@ -96,7 +97,7 @@ public class PremiseTransformer {
|
|||
dto.setStackable(entity.getHuStackable());
|
||||
|
||||
if (entity.getIndividualHuHeight() == null || entity.getIndividualHuWidth() == null || entity.getIndividualHuLength() == null || entity.getIndividualHuWeight() == null)
|
||||
dto.setDimension(null);
|
||||
dto.setDimension(new DimensionDTO());
|
||||
else
|
||||
dto.setDimension(dimensionTransformer.toDimensionDTO(entity));
|
||||
|
||||
|
|
@ -110,6 +111,11 @@ public class PremiseTransformer {
|
|||
|
||||
dto.setDestinations(destinationRepository.getByPremiseId(entity.getId()).stream().map(destinationTransformer::toDestinationDTO).toList());
|
||||
|
||||
dto.setHsCode(entity.getHsCode());
|
||||
dto.setFcaEnabled(entity.getFcaEnabled());
|
||||
dto.setOverseaShare(entity.getOverseaShare() == null ? null : entity.getOverseaShare().doubleValue());
|
||||
dto.setMaterialCost(entity.getMaterialCost() == null ? null : entity.getMaterialCost().doubleValue());
|
||||
|
||||
return dto;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -402,6 +402,7 @@ CREATE TABLE IF NOT EXISTS premise_route_node
|
|||
user_node_id INT DEFAULT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
address VARCHAR(500),
|
||||
external_mapping_id VARCHAR(32) NOT NULL,
|
||||
country_id INT NOT NULL,
|
||||
is_destination BOOLEAN DEFAULT FALSE,
|
||||
is_intermediate BOOLEAN DEFAULT FALSE,
|
||||
|
|
@ -455,7 +456,6 @@ CREATE TABLE IF NOT EXISTS premise_route_section
|
|||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS calculation_job
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue