BACKEND:
- Moved calculation to worker threads. - Added pre-calculation checks so that a calculation isn't started with insufficient data. - Missing lead time for D2D added. - Grouping for reporting works now as expected. FRONTEND: - Reporting implemented. Material and Valid period added to report. - Fixed Workflow for the calculation user. Archive/Delete functionality is added. - Missing lead time for D2D added.
This commit is contained in:
parent
849d31bc8e
commit
f885704dc9
52 changed files with 1073 additions and 329 deletions
11
src/frontend/package-lock.json
generated
11
src/frontend/package-lock.json
generated
|
|
@ -14,6 +14,7 @@
|
|||
"loglevel": "^1.9.2",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.18",
|
||||
"vue-chartjs": "^5.3.2",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -2939,6 +2940,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-chartjs": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.2.tgz",
|
||||
"integrity": "sha512-NrkbRRoYshbXbWqJkTN6InoDVwVb90C0R7eAVgMWcB9dPikbruaOoTFjFYHE/+tNPdIe6qdLCDjfjPHQ0fw4jw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"chart.js": "^4.1.1",
|
||||
"vue": "^3.0.0-0 || ^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"loglevel": "^1.9.2",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.18",
|
||||
"vue-chartjs": "^5.3.2",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import logger from "@/logger.js";
|
||||
import {useErrorStore} from "@/store/error.js";
|
||||
|
||||
const performRequest = async (requestingStore, method, url, body, expectResponse = true) => {
|
||||
const performRequest = async (requestingStore, method, url, body, expectResponse = true, expectedException = null) => {
|
||||
|
||||
const params = {
|
||||
method: method,
|
||||
|
|
@ -48,17 +48,23 @@ const performRequest = async (requestingStore, method, url, body, expectResponse
|
|||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = {
|
||||
const errorObj = {
|
||||
code: data.error.code,
|
||||
title: data.error.title,
|
||||
message: data.error.message,
|
||||
trace: data.error.trace
|
||||
}
|
||||
|
||||
logger.error(error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(error, {store: requestingStore, request: request});
|
||||
throw new Error('Internal backend error');
|
||||
const error = new Error('Internal backend error');
|
||||
error.errorObj = errorObj;
|
||||
|
||||
if (expectedException === null || data.error.title !== expectedException) {
|
||||
logger.error(errorObj);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(errorObj, {store: requestingStore, request: request});
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
if (!response.ok) {
|
||||
|
|
@ -76,17 +82,23 @@ const performRequest = async (requestingStore, method, url, body, expectResponse
|
|||
|
||||
});
|
||||
|
||||
const error = {
|
||||
const errorObj = {
|
||||
code: data.error.code,
|
||||
title: data.error.title,
|
||||
message: data.error.message,
|
||||
trace: data.error.trace
|
||||
}
|
||||
|
||||
logger.error(error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(error, {store: requestingStore, request: request});
|
||||
throw new Error('Internal backend error');
|
||||
const error = new Error('Internal backend error');
|
||||
error.errorObj = errorObj;
|
||||
|
||||
if (expectedException === null || data.error.title !== expectedException) {
|
||||
logger.error(errorObj);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(errorObj, {store: requestingStore, request: request});
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,9 +10,11 @@
|
|||
ref="modalOverlay"
|
||||
:style="modalAddStyle"
|
||||
>
|
||||
<box @click.stop>
|
||||
<div class="modal-container">
|
||||
<box @click.stop class="modal-box">
|
||||
<slot></slot>
|
||||
</box>
|
||||
</box>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</teleport>
|
||||
|
|
@ -42,12 +44,17 @@ export default {
|
|||
validators: (value) => value >= 0,
|
||||
default: 5000,
|
||||
},
|
||||
resizeTransitionDuration: {
|
||||
type: String,
|
||||
default: '0.3s'
|
||||
}
|
||||
},
|
||||
emits: ['close'],
|
||||
computed: {
|
||||
modalAddStyle() {
|
||||
return {
|
||||
zIndex: this.zIndex,
|
||||
'--resize-duration': this.resizeTransitionDuration
|
||||
}
|
||||
},
|
||||
isVisible() {
|
||||
|
|
@ -105,7 +112,28 @@ export default {
|
|||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Transition animations */
|
||||
.modal-container {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-box {
|
||||
/* Smooth transitions for size changes */
|
||||
transition:
|
||||
width var(--resize-duration, 0.3s) ease,
|
||||
height var(--resize-duration, 0.3s) ease,
|
||||
max-width var(--resize-duration, 0.3s) ease,
|
||||
max-height var(--resize-duration, 0.3s) ease,
|
||||
transform var(--resize-duration, 0.3s) ease;
|
||||
|
||||
/* Ensure the box can resize smoothly */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Transition animations for modal open/close */
|
||||
.modal-enter-active,
|
||||
.modal-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
|
|
@ -116,13 +144,18 @@ export default {
|
|||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-enter-active .box,
|
||||
.modal-leave-active .box {
|
||||
.modal-enter-active .modal-container,
|
||||
.modal-leave-active .modal-container {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-enter-from .box,
|
||||
.modal-leave-to .box {
|
||||
.modal-enter-from .modal-container,
|
||||
.modal-leave-to .modal-container {
|
||||
transform: scale(0.9) translateY(-10px);
|
||||
}
|
||||
|
||||
/* Optional: Add transition to content inside the modal for smoother changes */
|
||||
.modal-box :deep(*) {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -34,55 +34,81 @@ ChartJS.register(
|
|||
|
||||
export default {
|
||||
name: "ReportChart",
|
||||
computed: {
|
||||
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
mek_a : {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
logistics_costs : {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
risk_cost : {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
chance_cost : {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
scale : {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
data: {
|
||||
labels: ['Sample A'],
|
||||
datasets: [
|
||||
{
|
||||
label: 'MEK A',
|
||||
data: [35],
|
||||
backgroundColor: '#6B869C', // Blue-gray like in your image
|
||||
borderColor: '#6B869C',
|
||||
borderWidth: 1,
|
||||
stack: 'stack1'
|
||||
},
|
||||
{
|
||||
label: 'Logistics costs',
|
||||
data: [15],
|
||||
backgroundColor: '#5AF0B4', // Mint green like in your image
|
||||
borderColor: '#5AF0B4',
|
||||
borderWidth: 1,
|
||||
stack: 'stack1'
|
||||
},
|
||||
{
|
||||
label: 'Error Bars',
|
||||
|
||||
// data: [50, 65, 58, 75], // Total height (used for error bar positioning)
|
||||
type: 'scatter',
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: 'transparent',
|
||||
pointRadius: 0,
|
||||
showLine: false,
|
||||
stack: 'errorBars',
|
||||
errorBars: {
|
||||
plus: [5], // Upper error
|
||||
minus: [4] // Lower error
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
computed: {
|
||||
totalCosts() {
|
||||
return this.mek_a + this.logistics_costs;
|
||||
},
|
||||
upperError() {
|
||||
return (this.risk_cost - this.totalCosts);
|
||||
},
|
||||
lowerError() {
|
||||
return (this.totalCosts - this.chance_cost);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const ctx = this.$refs.stackedBarChart.getContext('2d');
|
||||
|
||||
// Sample data
|
||||
const data = this.data;
|
||||
const data = {
|
||||
labels: [this.title],
|
||||
datasets: [
|
||||
{
|
||||
label: 'MEK A',
|
||||
data: [this.mek_a],
|
||||
backgroundColor: '#6B869C', // Blue-gray like in your image
|
||||
borderColor: '#6B869C',
|
||||
borderWidth: 1,
|
||||
stack: 'stack1'
|
||||
},
|
||||
{
|
||||
label: 'Logistics costs',
|
||||
data: [this.logistics_costs],
|
||||
backgroundColor: '#5AF0B4', // Mint green like in your image
|
||||
borderColor: '#5AF0B4',
|
||||
borderWidth: 1,
|
||||
stack: 'stack1'
|
||||
},
|
||||
{
|
||||
label: 'Error Bars',
|
||||
type: 'scatter',
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: 'transparent',
|
||||
pointRadius: 0,
|
||||
showLine: false,
|
||||
stack: 'errorBars',
|
||||
errorBars: {
|
||||
plus: [this.upperError], // Upper error
|
||||
minus: [this.lowerError] // Lower error
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const config = {
|
||||
type: 'bar',
|
||||
|
|
@ -106,7 +132,7 @@ export default {
|
|||
y: {
|
||||
stacked: true, // Don't stack for error bars
|
||||
beginAtZero: true,
|
||||
max: 90,
|
||||
max: this.scale,
|
||||
grid: {
|
||||
color: '#E3EDFF'
|
||||
},
|
||||
|
|
|
|||
|
|
@ -21,8 +21,9 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="route-section-info-text">
|
||||
<div class="route-section-info-text-header-cell">Calc. model</div><div class="route-section-info-text-data-cell">{{ section.rate_type === 'CONTAINER' ? 'Container rate' : section.rate_type === 'D2D' ? 'Door-2-door' : 'Kilometer rate' }}</div>
|
||||
<div class="route-section-info-text-header-cell">Transit time</div><div class="route-section-info-text-data-cell">{{ section.duration.total }} days</div>
|
||||
<div class="route-section-info-text-header-cell">Distance</div><div class="route-section-info-text-data-cell">{{ section.distance.total.toFixed(2) }} km</div>
|
||||
<div class="route-section-info-text-header-cell" v-if="section.rate_type === 'MATRIX'">Distance</div><div v-if="section.rate_type === 'MATRIX'" class="route-section-info-text-data-cell">{{ section.distance.total.toFixed(2) }} km</div>
|
||||
<div class="route-section-info-text-header-cell">Transport rate</div><div class="route-section-info-text-data-cell">{{ section.cost.total.toFixed(2) }} €</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
help-text-position="left"></icon-button>
|
||||
<icon-button :disabled="!isDraft" icon="trash" @click="deleteClick" help-text="Delete this calculation"
|
||||
help-text-position="left"></icon-button>
|
||||
<icon-button :disabled="isDraft" icon="archive" @click="archiveClick" help-text="Archive/hide this calculation"
|
||||
<icon-button :disabled="isDraft || isArchived" icon="archive" @click="archiveClick" help-text="Archive/hide this calculation"
|
||||
help-text-position="left"></icon-button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -88,6 +88,9 @@ export default {
|
|||
},
|
||||
isDraft() {
|
||||
return this.premise.state === 'DRAFT';
|
||||
},
|
||||
isArchived() {
|
||||
return this.premise.state === 'ARCHIVED';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -95,14 +98,20 @@ export default {
|
|||
this.premise.checked = checked;
|
||||
},
|
||||
editClick() {
|
||||
const urlStr = new UrlSafeBase64().encodeIds([this.id]);
|
||||
this.$router.push({name: 'edit', params: {id: urlStr}});
|
||||
if(this.premise.state === 'DRAFT') {
|
||||
const urlStr = new UrlSafeBase64().encodeIds([this.id]);
|
||||
this.$router.push({name: 'edit', params: {id: urlStr}});
|
||||
}
|
||||
},
|
||||
deleteClick() {
|
||||
|
||||
if(this.premise.state === 'DRAFT') {
|
||||
this.premiseStore.deletePremisses([this.id])
|
||||
}
|
||||
},
|
||||
archiveClick() {
|
||||
|
||||
if(this.premise.state !== 'DRAFT' && this.premise.state !== 'ARCHIVED') {
|
||||
this.premiseStore.archivePremisses([this.id])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,17 +25,19 @@
|
|||
<radio-option name="model" value="d2d" v-model="calculationModel">individual rate</radio-option>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Single grid cell for caption that transitions content -->
|
||||
<div v-if="!showMassEditWarning" class="destination-edit-column-caption destination-edit-caption-top">
|
||||
<transition name="fade" mode="out-in">
|
||||
|
||||
<div v-if="showRoutes || showRouteWarning" key="routes" class="destination-edit-caption-top-elem">Routes</div>
|
||||
<div v-else key="rate" class="destination-edit-caption-top-elem">D2D Rate [EUR]</div>
|
||||
</transition>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Single grid cell for data that transitions content -->
|
||||
<div v-if="!showMassEditWarning" class="destination-edit-column-data destination-edit-routes-cell">
|
||||
<transition name="fade" mode="out-in">
|
||||
|
||||
<div v-if="showRoutes" key="routes" class="destination-edit-cell-routes">
|
||||
<destination-route v-for="route in destination.routes" :key="route.id" :route="route"
|
||||
:selected="route.is_selected" @click="selectRoute(route.id)"></destination-route>
|
||||
|
|
@ -50,8 +52,24 @@
|
|||
<input :value="rateD2d" @blur="validateRateD2d" class="input-field"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="destination-edit-column-caption">
|
||||
|
||||
<div v-if="!showMassEditWarning && !showRoutes && !showRouteWarning"> Lead time [days]</div>
|
||||
|
||||
</div>
|
||||
<div class="destination-edit-column-data">
|
||||
|
||||
<div v-if="!showMassEditWarning && !showRoutes && !showRouteWarning" key="rate" class="text-container">
|
||||
<input :value="leadtimeD2d" @blur="validateLeadTimeD2d" class="input-field"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -87,6 +105,15 @@ export default {
|
|||
|
||||
this.rateD2d = validatedValue;
|
||||
event.target.value = stringified;
|
||||
},
|
||||
validateLeadTimeD2d(event) {
|
||||
|
||||
const value = parseNumberFromString(event.target.value, 0);
|
||||
const validatedValue = Math.max(0, value);
|
||||
const stringified = validatedValue.toFixed();
|
||||
|
||||
this.leadtimeD2d = validatedValue;
|
||||
event.target.value = stringified;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
|
@ -141,6 +168,14 @@ export default {
|
|||
set(value) {
|
||||
this.destination && (this.destination.rate_d2d = value);
|
||||
}
|
||||
},
|
||||
leadtimeD2d: {
|
||||
get() {
|
||||
return this.destination.lead_time_d2d?.toFixed() ?? '0';
|
||||
},
|
||||
set(value) {
|
||||
this.destination && (this.destination.lead_time_d2d = value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -184,7 +219,7 @@ export default {
|
|||
.destination-edit-routes {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: auto auto auto 1fr; /* Last row takes remaining space */
|
||||
grid-template-rows: repeat(5, auto) 1fr; /* Last row takes remaining space */
|
||||
gap: 1.6rem;
|
||||
height: 100%;
|
||||
min-height: 0; /* Important for grid to shrink */
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<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-truck :size="18" v-else-if="isRoad" class="destination-route-icon"></ph-truck>
|
||||
<ph-navigation-arrow :size="18" v-else class="destination-route-icon"></ph-navigation-arrow>
|
||||
<div><span v-for="element in routeElements" class="destination-route-element"> {{ element }} </span></div>
|
||||
<basic-badge v-if="cheapest" variant="secondary">CHEAPEST</basic-badge>
|
||||
|
|
@ -16,12 +16,20 @@
|
|||
<script>
|
||||
|
||||
|
||||
import {PhBoat, PhHandCoins, PhLightning, PhNavigationArrow, PhTrain, PhTruckTrailer} from "@phosphor-icons/vue";
|
||||
import {
|
||||
PhBoat,
|
||||
PhHandCoins,
|
||||
PhLightning,
|
||||
PhNavigationArrow,
|
||||
PhTrain,
|
||||
PhTruck,
|
||||
PhTruckTrailer
|
||||
} from "@phosphor-icons/vue";
|
||||
import BasicBadge from "@/components/UI/BasicBadge.vue";
|
||||
|
||||
export default {
|
||||
name: "DestinationRoute",
|
||||
components: {BasicBadge, PhLightning, PhHandCoins, PhNavigationArrow, PhTrain, PhTruckTrailer, PhBoat},
|
||||
components: {PhTruck, BasicBadge, PhLightning, PhHandCoins, PhNavigationArrow, PhTrain, PhTruckTrailer, PhBoat},
|
||||
props: {
|
||||
route: {
|
||||
type: Object,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,20 @@
|
|||
<template>
|
||||
<div class="report-container">
|
||||
<div class="report-header">{{ report.supplier.name }}</div>
|
||||
<div class="box-gap">
|
||||
<div class="report-header">{{ report.supplier.name }}</div></div>
|
||||
<div class="report-chart">
|
||||
<report-chart></report-chart>
|
||||
<report-chart
|
||||
title=""
|
||||
:mek_a="report.costs.mek_a.total"
|
||||
:logistics_costs="report.risk.mek_b.total-report.costs.mek_a.total"
|
||||
:chance_cost="report.risk.opportunity_scenario.total"
|
||||
:risk_cost="report.risk.risk_scenario.total"
|
||||
:scale="chartScale"
|
||||
></report-chart>
|
||||
</div>
|
||||
|
||||
<collapsible-box :is-collapsable="false" variant="border" title="Overview" size="m" :stretch-content="true">
|
||||
<div class="box-gap">
|
||||
<collapsible-box :is-collapsable="false" variant="border" title="Overview" size="m" :stretch-content="true" >
|
||||
<div class="report-content-container--2-col">
|
||||
|
||||
<div class="report-content-row">
|
||||
|
|
@ -26,7 +35,8 @@
|
|||
|
||||
</div>
|
||||
</collapsible-box>
|
||||
|
||||
</div>
|
||||
<div class="box-gap">
|
||||
<collapsible-box :is-collapsable="false" variant="border" title="Weighted cost breakdown" size="m"
|
||||
:stretch-content="true">
|
||||
<div class="report-content-container--3-col">
|
||||
|
|
@ -61,6 +71,12 @@
|
|||
<div class="report-content-data-cell">{{ (report.costs.main_run.percentage * 100).toFixed(2) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row" v-if="((report.costs.air_freight_cost ?? null) !== null)">
|
||||
<div>Airfreight cost</div>
|
||||
<div class="report-content-data-cell">{{ report.costs.air_freight_cost.total.toFixed(2) }}</div>
|
||||
<div class="report-content-data-cell">{{ (report.costs.air_freight_cost.percentage * 100).toFixed(2) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Post carriage</div>
|
||||
<div class="report-content-data-cell">{{ report.costs.post_run.total.toFixed(2) }}</div>
|
||||
|
|
@ -105,10 +121,13 @@
|
|||
|
||||
</div>
|
||||
</collapsible-box>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="box-gap" :key="premise.id" v-for="premise in report.premises">
|
||||
|
||||
<collapsible-box class="report-content-container" variant="border" :title="premise.destination.name"
|
||||
:key="premise.id" :stretch-content="true" v-for="premise in report.premises">
|
||||
:stretch-content="true" :initially-collapsed="true">
|
||||
<div>
|
||||
<report-route :sections="premise.sections" :destination="premise.destination" ></report-route>
|
||||
|
||||
|
|
@ -136,16 +155,21 @@
|
|||
<div class="report-content-data-cell">{{ (premise.oversea_share * 100).toFixed(2) }}%</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row" v-if="premise.air_freight_share">
|
||||
<div class="report-content-row" v-if="(premise.air_freight_share ?? null) !== null">
|
||||
<div>Airfreight share</div>
|
||||
<div class="report-content-data-cell">{{ (premise.air_freight_share * 100).toFixed(2) }}%</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Transit time</div>
|
||||
<div>Transit time [days]</div>
|
||||
<div class="report-content-data-cell">{{ premise.transport_time }}</div>
|
||||
</div>
|
||||
|
||||
<div class="report-content-row">
|
||||
<div>Safety stock [days]</div>
|
||||
<div class="report-content-data-cell">{{ premise.safety_stock }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="report-sub-header">Packaging</div>
|
||||
|
|
@ -192,14 +216,12 @@
|
|||
<div class="report-content-data-cell">{{ premise.weight_exceeded ? 'weight' : 'volume' }} </div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</collapsible-box>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -218,6 +240,10 @@ export default {
|
|||
report: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
chartScale: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -245,6 +271,11 @@ export default {
|
|||
|
||||
<style scoped>
|
||||
|
||||
.box-gap {
|
||||
padding-left: 0.8rem;
|
||||
padding-right: 0.8rem;
|
||||
}
|
||||
|
||||
.report-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -286,6 +317,8 @@ export default {
|
|||
font-size: 1.6rem;
|
||||
font-weight: 500;
|
||||
color: #001D33;
|
||||
text-wrap: nowrap;
|
||||
|
||||
}
|
||||
|
||||
.report-sub-header {
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@
|
|||
></autosuggest-searchbar>
|
||||
</div>
|
||||
</div>
|
||||
<div class="caption">Select suppliers to compare:</div>
|
||||
<div class="content-container" v-if="suppliers.length !== 0">
|
||||
<div class="caption">Select suppliers to compare:</div>
|
||||
<ul class="item-list">
|
||||
<transition-group name="fade" tag="ul" class="item-list">
|
||||
<li class="item-list-element" v-for="supplier in suppliers" :key="supplier.id"
|
||||
@click="selectSupplier(supplier.id)">
|
||||
<supplier-item :id="String(supplier.id)"
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
:selected="isSelected(supplier.id)"
|
||||
></supplier-item>
|
||||
</li>
|
||||
</ul>
|
||||
</transition-group>
|
||||
</div>
|
||||
<div class="content-container-empty" v-else>no suppliers found.</div>
|
||||
<div class="footer">
|
||||
|
|
@ -56,7 +56,7 @@ export default {
|
|||
},
|
||||
created() {
|
||||
//todo reset the store instead.
|
||||
this.selectedMaterialId = this.reportSearchStore.getMaterial?.id;
|
||||
this.reportSearchStore.reset();
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useMaterialStore, useReportSearchStore),
|
||||
|
|
@ -101,7 +101,7 @@ export default {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
min-width: 90vw;
|
||||
width: 80vw;
|
||||
min-height: min(80vh, 100rem);
|
||||
}
|
||||
|
||||
|
|
@ -122,15 +122,15 @@ export default {
|
|||
|
||||
.content-container {
|
||||
flex: 1;
|
||||
overflow-x: auto; /* Enable horizontal scrolling */
|
||||
overflow-y: hidden; /* Prevent vertical overflow */
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
min-height: 0; /* Allow flex item to shrink below content size */
|
||||
}
|
||||
|
||||
.content-container-empty {
|
||||
flex: 1;
|
||||
overflow-x: auto; /* Enable horizontal scrolling */
|
||||
overflow-y: hidden; /* Prevent vertical overflow */
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
min-height: 0; /* Allow flex item to shrink below content size */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -150,11 +150,11 @@ export default {
|
|||
|
||||
.item-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
list-style: none;
|
||||
gap: 2.4rem 2.4rem;
|
||||
margin: 4.8rem;
|
||||
flex-wrap: nowrap;
|
||||
min-width: max-content;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.item-list-element {
|
||||
|
|
@ -163,5 +163,16 @@ export default {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Fade animation styles */
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from, .fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-enter-to, .fade-leave-from {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -26,7 +26,7 @@ import {
|
|||
PhArchive,
|
||||
PhFloppyDisk,
|
||||
PhArrowCounterClockwise,
|
||||
PhCheck, PhBug, PhShuffle, PhStack, PhFile
|
||||
PhCheck, PhBug, PhShuffle, PhStack, PhFile, PhFilePlus, PhDownloadSimple
|
||||
} from "@phosphor-icons/vue";
|
||||
|
||||
const app = createApp(App);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
</div>
|
||||
</div>
|
||||
<Toast ref="toast" />
|
||||
|
||||
<transition name="list-edit-container" tag="div">
|
||||
<transition-group name="list-edit" mode="out-in" class="edit-calculation-list-container" tag="div">
|
||||
|
|
@ -120,6 +121,7 @@ import MaterialEdit from "@/components/layout/edit/MaterialEdit.vue";
|
|||
import PackagingEdit from "@/components/layout/edit/PackagingEdit.vue";
|
||||
import DestinationListView from "@/components/layout/edit/DestinationListView.vue";
|
||||
import SelectNode from "@/components/layout/node/SelectNode.vue";
|
||||
import Toast from "@/components/UI/Toast.vue";
|
||||
|
||||
|
||||
const COMPONENT_TYPES = {
|
||||
|
|
@ -132,7 +134,7 @@ const COMPONENT_TYPES = {
|
|||
|
||||
export default {
|
||||
name: "MassEdit",
|
||||
components: {Modal, MassEditDialog, ListEdit, Spinner, CalculationListItem, Checkbox, BulkEditRow, BasicButton},
|
||||
components: {Toast, Modal, MassEditDialog, ListEdit, Spinner, CalculationListItem, Checkbox, BulkEditRow, BasicButton},
|
||||
computed: {
|
||||
...mapStores(usePremiseEditStore),
|
||||
hasSelection() {
|
||||
|
|
@ -184,7 +186,7 @@ export default {
|
|||
created() {
|
||||
this.bulkQuery = this.$route.params.ids;
|
||||
this.ids = new UrlSafeBase64().decodeIds(this.$route.params.ids);
|
||||
this.premiseEditStore.loadPremissesIfNeeded(this.ids);
|
||||
this.premiseEditStore.loadPremissesIfNeeded(this.ids, true);
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -221,8 +223,22 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
startCalculation() {
|
||||
this.premiseEditStore.startCalculation();
|
||||
async startCalculation() {
|
||||
const error = await this.premiseEditStore.startCalculation();
|
||||
|
||||
if(error !== null) {
|
||||
|
||||
this.$refs.toast.addToast({
|
||||
icon: 'warning',
|
||||
message: error.message,
|
||||
title: "Cannot start calculation",
|
||||
variant: 'exception',
|
||||
duration: 8000
|
||||
})
|
||||
} else
|
||||
{
|
||||
this.closeMassEdit()
|
||||
}
|
||||
},
|
||||
closeMassEdit() {
|
||||
this.$router.push({name: "calculation-list"});
|
||||
|
|
|
|||
|
|
@ -128,8 +128,24 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
startCalculation() {
|
||||
this.premiseEditStore.startCalculation();
|
||||
async startCalculation() {
|
||||
const error = await this.premiseEditStore.startCalculation();
|
||||
|
||||
|
||||
|
||||
if(error !== null) {
|
||||
|
||||
this.$refs.toast.addToast({
|
||||
icon: 'warning',
|
||||
message: error.message,
|
||||
title: "Cannot start calculation",
|
||||
variant: 'exception',
|
||||
duration: 8000
|
||||
})
|
||||
} else
|
||||
{
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
close() {
|
||||
if(this.bulkEditQuery) {
|
||||
|
|
|
|||
|
|
@ -81,19 +81,34 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
handleMultiselectAction(action) {
|
||||
const selectedPremisses = this.premiseStore.premises.filter(p => p.checked === true);
|
||||
if (action === "delete") {
|
||||
|
||||
const ids = selectedPremisses.filter(p => p.state === "DRAFT").map(p => p.id)
|
||||
|
||||
if (ids.length !== 0) {
|
||||
this.premiseStore.deletePremisses(ids);
|
||||
}
|
||||
|
||||
|
||||
} else if (action === "edit") {
|
||||
const ids = this.premiseStore.premises.filter(p => p.checked === true).map(p => p.id);
|
||||
const ids = selectedPremisses.map(p => p.id);
|
||||
|
||||
if (ids.length === 1) {
|
||||
this.$router.push({name: "edit", params: {id: ids[0]}});
|
||||
this.$router.push({name: "edit", params: {id: new UrlSafeBase64().encodeIds([ids[0]])}});
|
||||
} else {
|
||||
this.$router.push({name: "bulk", params: {ids: new UrlSafeBase64().encodeIds(ids)}});
|
||||
}
|
||||
|
||||
} else if (action === "archive") {
|
||||
|
||||
const ids = selectedPremisses.filter(p => p.state !== "DRAFT" && p.state !== "ARCHIVED").map(p => p.id)
|
||||
|
||||
if (ids.length !== 0) {
|
||||
this.premiseStore.archivePremisses(ids);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
},
|
||||
updateCheckBoxes(checked) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="header-container">
|
||||
<h2 class="page-header">Reporting</h2>
|
||||
<h2 class="page-header page-header-align">Reporting <div class="page-header-badges"><basic-badge variant="primary" v-if="period">{{ period }}</basic-badge><basic-badge variant="secondary" v-if="partNumber">{{ partNumber }}</basic-badge></div></h2>
|
||||
<div class="header-controls">
|
||||
<basic-button @click="showModal = true" icon="file">Create report</basic-button>
|
||||
<basic-button @click="createReport" icon="file">Create report</basic-button>
|
||||
<basic-button :disabled="!hasReport" variant="secondary" @click="downloadReport" icon="Download">Export</basic-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -14,9 +15,7 @@
|
|||
</div>
|
||||
<div v-else-if="hasReport">
|
||||
<box>
|
||||
<report v-for="report in reports" :key="report.id" :report="report"></report>
|
||||
|
||||
|
||||
<report v-for="report in reports" :key="report.id" :report="report" :chart-scale="chartScale"></report>
|
||||
</box>
|
||||
</div>
|
||||
<div v-else class="empty-container">
|
||||
|
|
@ -43,10 +42,11 @@ import Box from "@/components/UI/Box.vue";
|
|||
import Spinner from "@/components/UI/Spinner.vue";
|
||||
import ReportChart from "@/components/UI/ReportChart.vue";
|
||||
import Report from "@/components/layout/report/Report.vue";
|
||||
import BasicBadge from "@/components/UI/BasicBadge.vue";
|
||||
|
||||
export default {
|
||||
name: "Reporting",
|
||||
components: {Report, ReportChart, Spinner, Box, SelectForReport, BasicButton, Modal},
|
||||
components: {BasicBadge, Report, ReportChart, Spinner, Box, SelectForReport, BasicButton, Modal},
|
||||
data() {
|
||||
return {
|
||||
showModal: false,
|
||||
|
|
@ -62,9 +62,33 @@ export default {
|
|||
},
|
||||
loading() {
|
||||
return this.reportsStore.loading;
|
||||
},
|
||||
chartScale() {
|
||||
return this.reportsStore.getChartScale;
|
||||
},
|
||||
partNumber() {
|
||||
if(!this.hasReport) return null;
|
||||
return this.reportsStore.reports[0].material.part_number;
|
||||
},
|
||||
period() {
|
||||
if(!this.hasReport) return null;
|
||||
const start = this.reportsStore.reports[0].start_date;
|
||||
const end = this.reportsStore.reports[0].end_date;
|
||||
|
||||
if(end === null) {
|
||||
return `Validity period: since ${this.buildDate(start)}` ;
|
||||
}
|
||||
|
||||
return `Validity period: ${this.buildDate(start)} – ${this.buildDate(end)}` ;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
createReport() {
|
||||
this.showModal = true;
|
||||
},
|
||||
buildDate(date) {
|
||||
return `${date[0]}-${date[1].toString().padStart(2, '0')}-${date[2].toString().padStart(2, '0')}`
|
||||
},
|
||||
closeModal(data) {
|
||||
console.log("closeModal: ", data.action)
|
||||
if (data.action === 'accept') {
|
||||
|
|
@ -84,6 +108,18 @@ export default {
|
|||
|
||||
<style scoped>
|
||||
|
||||
.page-header-align {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.page-header-badges {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.space-around {
|
||||
margin: 3rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import {config} from '@/config'
|
||||
import {useErrorStore} from "@/store/error.js";
|
||||
import performRequest from "@/backend.js";
|
||||
|
||||
export const usePremiseStore = defineStore('premise', {
|
||||
state: () => ({
|
||||
|
|
@ -12,7 +13,9 @@ export const usePremiseStore = defineStore('premise', {
|
|||
pagination: {}
|
||||
}),
|
||||
getters: {
|
||||
getById: (state) => { return (id) => state.premises.find(p => p.id === id)},
|
||||
getById: (state) => {
|
||||
return (id) => state.premises.find(p => p.id === id)
|
||||
},
|
||||
showData: (state) => !state.loading && state.empty === false,
|
||||
showLoading: (state) => state.loading,
|
||||
showEmpty: (state) => !state.loading && state.empty === true,
|
||||
|
|
@ -23,6 +26,18 @@ export const usePremiseStore = defineStore('premise', {
|
|||
this.query = query;
|
||||
this.updatePremises();
|
||||
},
|
||||
async deletePremisses(ids) {
|
||||
const params = new URLSearchParams();
|
||||
params.append('premissIds', ids.join(', '));
|
||||
await performRequest(this, 'POST', `${config.backendUrl}/calculation/delete/${params.size === 0 ? '' : '?'}${params.toString()}`, null, false);
|
||||
await this.updatePremises();
|
||||
},
|
||||
async archivePremisses(ids) {
|
||||
const params = new URLSearchParams();
|
||||
params.append('premissIds', ids.join(', '));
|
||||
await performRequest(this, 'POST', `${config.backendUrl}/calculation/archive/${params.size === 0 ? '' : '?'}${params.toString()}`, null, false);
|
||||
await this.updatePremises();
|
||||
},
|
||||
async updatePremises() {
|
||||
|
||||
this.premises = [];
|
||||
|
|
@ -45,37 +60,46 @@ export const usePremiseStore = defineStore('premise', {
|
|||
|
||||
const url = `${config.backendUrl}/calculation/view/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||
|
||||
const request = { url: url, params: {method: 'GET'}};
|
||||
const request = {url: url, params: {method: 'GET'}};
|
||||
|
||||
const response = await fetch(url).catch(e => {
|
||||
this.error = { title: 'Network error.', message: "Please check your internet connection.", trace: null }
|
||||
this.error = {title: 'Network error.', message: "Please check your internet connection.", trace: null}
|
||||
this.loading = false;
|
||||
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(this.error, { store: this, request: request});
|
||||
void errorStore.addError(this.error, {store: this, request: request});
|
||||
|
||||
throw e;
|
||||
});
|
||||
|
||||
const data = await response.json().catch(e => {
|
||||
this.error = { title: 'Malformed response', message: "Malformed server response. Please contact support.", trace: null }
|
||||
this.error = {
|
||||
title: 'Malformed response',
|
||||
message: "Malformed server response. Please contact support.",
|
||||
trace: null
|
||||
}
|
||||
this.loading = false;
|
||||
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(this.error, { store: this, request: request});
|
||||
void errorStore.addError(this.error, {store: this, request: request});
|
||||
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
this.error = { code: data.error.code, title: data.error.title, message: data.error.message, trace: data.error.trace }
|
||||
this.error = {
|
||||
code: data.error.code,
|
||||
title: data.error.title,
|
||||
message: data.error.message,
|
||||
trace: data.error.trace
|
||||
}
|
||||
this.loading = false;
|
||||
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(this.error, { store: this, request: request});
|
||||
void errorStore.addError(this.error, {store: this, request: request});
|
||||
|
||||
console.log(data);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {config} from '@/config'
|
|||
import {toRaw} from "vue";
|
||||
import {useErrorStore} from "@/store/error.js";
|
||||
import logger from "@/logger.js"
|
||||
import performRequest from '@/backend.js'
|
||||
|
||||
export const usePremiseEditStore = defineStore('premiseEdit', {
|
||||
state() {
|
||||
|
|
@ -313,17 +314,14 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
|
||||
const body = this.premisses.map(p => p.id);
|
||||
const url = `${config.backendUrl}/calculation/start/`;
|
||||
let error = null;
|
||||
|
||||
const data = await this.performRequest('PUT', url, body).catch(e => {
|
||||
// do something
|
||||
await performRequest(this,'PUT', url, body, false, 'Premiss validation error').catch(e => {
|
||||
console.log("startCalculation exception", e.errorObj);
|
||||
error = e.errorObj;
|
||||
})
|
||||
|
||||
if (data) {
|
||||
// do something
|
||||
}
|
||||
|
||||
|
||||
alert("Finished.");
|
||||
return error;
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -368,11 +366,12 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
disposal_costs: toDest.disposal_costs,
|
||||
is_d2d: toDest.is_d2d,
|
||||
rate_d2d: toDest.rate_d2d,
|
||||
lead_time_d2d: toDest.lead_time_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);
|
||||
performRequest(this,'PUT', url, body, false);
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -397,7 +396,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
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 => {
|
||||
const data = await performRequest(this,'PUT', url, body).catch(e => {
|
||||
this.destinations = null;
|
||||
this.processDestinationMassEdit = false;
|
||||
})
|
||||
|
|
@ -427,6 +426,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
d.annual_amount = from.annual_amount;
|
||||
d.is_d2d = from.is_d2d;
|
||||
d.rate_d2d = from.is_d2d ? from.rate_d2d : null;
|
||||
d.lead_time_d2d = from.is_d2d ? from.lead_time_d2d : null;
|
||||
d.handling_costs = from.handling_costs;
|
||||
d.disposal_costs = from.disposal_costs;
|
||||
d.repackaging_costs = from.repackaging_costs;
|
||||
|
|
@ -441,6 +441,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
d.annual_amount = from.annual_amount;
|
||||
d.is_d2d = from.is_d2d;
|
||||
d.rate_d2d = from.is_d2d ? from.rate_d2d : null;
|
||||
d.lead_time_d2d = from.is_d2d ? from.lead_time_d2d : null;
|
||||
|
||||
if (from.userDefinedHandlingCosts) {
|
||||
d.disposal_costs = from.disposal_costs;
|
||||
|
|
@ -504,13 +505,14 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
disposal_costs: toDest.disposal_costs,
|
||||
is_d2d: toDest.is_d2d,
|
||||
rate_d2d: toDest.rate_d2d,
|
||||
lead_time_d2d: toDest.lead_time_d2d,
|
||||
route_selected_id: toDest.routes.find(r => r.is_selected)?.id ?? null,
|
||||
};
|
||||
|
||||
logger.info(body)
|
||||
|
||||
const url = `${config.backendUrl}/calculation/destination/${toDest.id}`;
|
||||
await this.performRequest('PUT', url, body, false);
|
||||
await performRequest(this,'PUT', url, body, false);
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -543,7 +545,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
const origId = id.substring(1);
|
||||
|
||||
const url = `${config.backendUrl}/calculation/destination/${origId}`;
|
||||
await this.performRequest('DELETE', url, null, false).catch(async e => {
|
||||
await performRequest(this,'DELETE', url, null, false).catch(async e => {
|
||||
logger.error("Unable to delete destination: " + origId + "");
|
||||
logger.error(e);
|
||||
await this.loadPremissesIfNeeded(this.premisses.map(p => p.id));
|
||||
|
|
@ -580,6 +582,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
annual_amount: 0,
|
||||
is_d2d: false,
|
||||
rate_d2d: null,
|
||||
lead_time_d2d: null,
|
||||
disposal_costs: null,
|
||||
repackaging_costs: null,
|
||||
handling_costs: null,
|
||||
|
|
@ -602,7 +605,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
const url = `${config.backendUrl}/calculation/destination/`;
|
||||
|
||||
|
||||
const destinations = await this.performRequest('POST', url, body).catch(e => {
|
||||
const destinations = await performRequest(this,'POST', url, body).catch(e => {
|
||||
this.loading = false;
|
||||
this.selectedLoading = false;
|
||||
throw e;
|
||||
|
|
@ -663,7 +666,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
body.premise_id = toBeUpdated;
|
||||
logger.info(url, body)
|
||||
|
||||
const data = await this.performRequest('PUT', url, body).catch(e => {
|
||||
const data = await performRequest(this,'PUT', url, body).catch(e => {
|
||||
this.loading = false;
|
||||
});
|
||||
|
||||
|
|
@ -708,7 +711,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
is_fca_enabled: toBeUpdated[0].is_fca_enabled
|
||||
};
|
||||
|
||||
await this.performRequest('POST', `${config.backendUrl}/calculation/price/`, body, false).catch(e => {
|
||||
await performRequest(this,'POST', `${config.backendUrl}/calculation/price/`, body, false).catch(e => {
|
||||
success = false;
|
||||
})
|
||||
|
||||
|
|
@ -743,7 +746,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
|
||||
};
|
||||
|
||||
await this.performRequest('POST', `${config.backendUrl}/calculation/packaging/`, body, false).catch(() => {
|
||||
await performRequest(this,'POST', `${config.backendUrl}/calculation/packaging/`, body, false).catch(() => {
|
||||
success = false;
|
||||
})
|
||||
|
||||
|
|
@ -755,7 +758,6 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
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 = {
|
||||
|
|
@ -765,7 +767,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
tariff_rate: toBeUpdated[0].tariff_rate,
|
||||
};
|
||||
|
||||
await this.performRequest('POST', `${config.backendUrl}/calculation/material/`, body, false).catch(() => {
|
||||
await performRequest(this,'POST', `${config.backendUrl}/calculation/material/`, body, false).catch(() => {
|
||||
success = false;
|
||||
})
|
||||
|
||||
|
|
@ -833,7 +835,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
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 => {
|
||||
this.premisses = await performRequest(this,'GET', url, null).catch(e => {
|
||||
this.selectedLoading = false;
|
||||
this.loading = false;
|
||||
});
|
||||
|
|
@ -843,8 +845,8 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
this.selectedLoading = false;
|
||||
this.loading = false;
|
||||
},
|
||||
async loadPremissesIfNeeded(ids) {
|
||||
const reload = this.premisses ? !ids.every((id) => this.premisses.find(d => d.id === id)) : true;
|
||||
async loadPremissesIfNeeded(ids, exact = false) {
|
||||
const reload = this.premisses ? !ids.every((id) => this.premisses.find(d => d.id === id) && (!exact || ids.length === this.premisses.length)) : true;
|
||||
|
||||
if (reload) {
|
||||
await this.loadPremissesForced(ids);
|
||||
|
|
@ -859,7 +861,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
params.append('premissIds', ids.join(', '));
|
||||
const url = `${config.backendUrl}/calculation/edit/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||
|
||||
this.premisses = await this.performRequest('GET', url, null).catch(e => {
|
||||
this.premisses = await performRequest(this,'GET', url, null).catch(e => {
|
||||
this.loading = false;
|
||||
});
|
||||
|
||||
|
|
@ -871,100 +873,6 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
removePremise(id) {
|
||||
const idx = this.premisses.findIndex(p => p.id === id);
|
||||
this.premisses.splice(idx, 1);
|
||||
},
|
||||
async performRequest(method, url, body, expectResponse = true) {
|
||||
|
||||
const params = {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
if (body) {
|
||||
params.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const request = {url: url, params: params};
|
||||
logger.info("Request:", request);
|
||||
|
||||
const response = await fetch(url, params
|
||||
).catch(e => {
|
||||
const error = {
|
||||
code: 'Network error.',
|
||||
message: "Please check your internet connection.",
|
||||
trace: null
|
||||
}
|
||||
|
||||
logger.error(error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(error, {store: this, request: request});
|
||||
|
||||
throw e;
|
||||
});
|
||||
|
||||
let data = null;
|
||||
if (expectResponse) {
|
||||
data = await response.json().catch(e => {
|
||||
const error = {
|
||||
code: 'Malformed response',
|
||||
message: "Malformed server response. Please contact support.",
|
||||
trace: null
|
||||
}
|
||||
|
||||
logger.error(error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(error, {store: this, request: request});
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = {
|
||||
code: data.error.code,
|
||||
title: data.error.title,
|
||||
message: data.error.message,
|
||||
trace: data.error.trace
|
||||
}
|
||||
|
||||
logger.error(error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(error, {store: this, request: request});
|
||||
throw new Error('Internal backend error');
|
||||
}
|
||||
} else {
|
||||
if (!response.ok) {
|
||||
|
||||
const data = await response.json().catch(e => {
|
||||
const error = {
|
||||
code: "Return code error " + response.status,
|
||||
message: "Server returned wrong response code",
|
||||
trace: null
|
||||
}
|
||||
logger.error(error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(error, {store: this, request: request});
|
||||
throw new Error('Internal backend error');
|
||||
|
||||
});
|
||||
|
||||
const error = {
|
||||
code: data.error.code,
|
||||
title: data.error.title,
|
||||
message: data.error.message,
|
||||
trace: data.error.trace
|
||||
}
|
||||
|
||||
logger.error(error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(error, {store: this, request: request});
|
||||
throw new Error('Internal backend error');
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Response:", data);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -35,6 +35,12 @@ export const useReportSearchStore = defineStore('reportSearch', {
|
|||
},
|
||||
},
|
||||
actions: {
|
||||
async reset() {
|
||||
this.suppliers = [];
|
||||
this.material = null;
|
||||
this.selectedIds = [];
|
||||
this.remainingSuppliers = [];
|
||||
},
|
||||
async selectSupplier(id) {
|
||||
|
||||
if (!this.selectedIds.includes(id))
|
||||
|
|
|
|||
|
|
@ -10,7 +10,19 @@ export const useReportsStore = defineStore('reports', {
|
|||
loading: false,
|
||||
}
|
||||
},
|
||||
getters: {},
|
||||
getters: {
|
||||
getChartScale(state) {
|
||||
let max = 0;
|
||||
|
||||
state.reports.forEach(report => {
|
||||
max = Math.max(report.risk.mek_b.total, max);
|
||||
max = Math.max(report.risk.risk_scenario.total, max);
|
||||
})
|
||||
|
||||
const magnitude = Math.pow(10, Math.floor(Math.log10(max)));
|
||||
return Math.ceil(max / magnitude) * magnitude;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async fetchReports(materialId, supplierIds) {
|
||||
if (supplierIds == null || materialId == null) return;
|
||||
|
|
|
|||
24
src/main/java/de/avatic/lcc/config/AsyncConfig.java
Normal file
24
src/main/java/de/avatic/lcc/config/AsyncConfig.java
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package de.avatic.lcc.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
public class AsyncConfig {
|
||||
|
||||
@Bean(name = "taskExecutor")
|
||||
public Executor taskExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(4);
|
||||
executor.setMaxPoolSize(8);
|
||||
executor.setQueueCapacity(100);
|
||||
executor.setThreadNamePrefix("calc-");
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 de.avatic.lcc.util.exception.internalerror.PremiseValidationError;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
|
@ -130,6 +131,18 @@ public class GlobalExceptionHandler {
|
|||
return new ResponseEntity<>(new ErrorResponseDTO(error), HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@ExceptionHandler(PremiseValidationError.class)
|
||||
public ResponseEntity<ErrorResponseDTO> handleGenericException(PremiseValidationError exception) {
|
||||
ErrorDTO error = new ErrorDTO(
|
||||
exception.getClass().getName(),
|
||||
"Premiss validation error",
|
||||
exception.getMessage(),
|
||||
Arrays.asList(exception.getStackTrace())
|
||||
);
|
||||
|
||||
return new ResponseEntity<>(new ErrorResponseDTO(error), HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ErrorResponseDTO> handleGenericException(Exception exception) {
|
||||
ErrorDTO error = new ErrorDTO(
|
||||
|
|
|
|||
|
|
@ -108,14 +108,27 @@ public class PremiseController {
|
|||
}
|
||||
|
||||
|
||||
@PostMapping({"/delete", "/delete/"})
|
||||
public ResponseEntity<Void> deletePremises(@RequestParam List<Integer> premissIds) {
|
||||
premisesServices.delete(premissIds);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping({"/archive", "/archive/"})
|
||||
public ResponseEntity<Void> archivePremises(@RequestParam List<Integer> premissIds) {
|
||||
premisesServices.archive(premissIds);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@GetMapping({"/edit", "/edit/"})
|
||||
public ResponseEntity<List<PremiseDetailDTO>> getPremises(@RequestParam List<Integer> premissIds) {
|
||||
return ResponseEntity.ok(premisesServices.getPremises(premissIds));
|
||||
}
|
||||
|
||||
@PutMapping({"/start", "/start/"})
|
||||
public ResponseEntity<Integer> startCalculation(@RequestBody List<Integer> premiseIds) {
|
||||
return ResponseEntity.ok(premisesServices.startCalculation(premiseIds));
|
||||
public ResponseEntity<Void> startCalculation(@RequestBody List<Integer> premiseIds) {
|
||||
premisesServices.startCalculation(premiseIds);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ public class DestinationDTO {
|
|||
@JsonProperty("rate_d2d")
|
||||
private BigDecimal rateD2d;
|
||||
|
||||
@JsonProperty("lead_time_d2d")
|
||||
private Number leadTimeD2d;
|
||||
|
||||
private List<RouteDTO> routes;
|
||||
|
||||
public Boolean getD2d() {
|
||||
|
|
@ -105,4 +108,12 @@ public class DestinationDTO {
|
|||
public void setRateD2d(BigDecimal rateD2d) {
|
||||
this.rateD2d = rateD2d;
|
||||
}
|
||||
|
||||
public Number getLeadTimeD2d() {
|
||||
return leadTimeD2d;
|
||||
}
|
||||
|
||||
public void setLeadTimeD2d(Number leadTimeD2d) {
|
||||
this.leadTimeD2d = leadTimeD2d;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,18 +13,18 @@ public class DestinationUpdateDTO {
|
|||
private Integer annualAmount;
|
||||
|
||||
@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")
|
||||
@DecimalMin(value = "0.00", message = "Repackaging cost must be greater than or equal 0")
|
||||
@Digits(integer = 13, fraction = 2, message = "Repackaging cost must have at most 2 decimal places")
|
||||
private Number repackingCost;
|
||||
|
||||
@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")
|
||||
@DecimalMin(value = "0.00", message = "Handling cost must be greater than or equal 0")
|
||||
@Digits(integer = 13, fraction = 2, message = "Handling cost must have at most 2 decimal places")
|
||||
private Number handlingCost;
|
||||
|
||||
@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")
|
||||
@DecimalMin(value = "0.00", message = "Disposal cost must be greater than or equal 0")
|
||||
@Digits(integer = 13, fraction = 2, message = "Disposal cost must have at most 2 decimal places")
|
||||
private Number disposalCost;
|
||||
|
||||
@JsonProperty("route_selected_id")
|
||||
|
|
@ -34,11 +34,16 @@ public class DestinationUpdateDTO {
|
|||
@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")
|
||||
@DecimalMin(value = "0.00", message = "Rate must be greater than or equal 0")
|
||||
@Digits(integer = 13, fraction = 2, message = "Rate must have at most 2 decimal places")
|
||||
@JsonProperty("rate_d2d")
|
||||
private Number rateD2d;
|
||||
|
||||
@Min(value = 0, message = "Lead time must be greater than or equal 0")
|
||||
@Digits(integer = 13, fraction = 2, message = "Lead time must have at most 2 decimal places")
|
||||
@JsonProperty("lead_time_d2d")
|
||||
private Integer leadtimeD2d;
|
||||
|
||||
public Number getRepackingCost() {
|
||||
return repackingCost;
|
||||
}
|
||||
|
|
@ -108,4 +113,12 @@ public class DestinationUpdateDTO {
|
|||
", rateD2d=" + rateD2d +
|
||||
'}';
|
||||
}
|
||||
|
||||
public Integer getLeadtimeD2d() {
|
||||
return leadtimeD2d;
|
||||
}
|
||||
|
||||
public void setLeadtimeD2d(Integer leadtimeD2d) {
|
||||
this.leadtimeD2d = leadtimeD2d;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package de.avatic.lcc.dto.generic;
|
|||
public enum ContainerType {
|
||||
FEU(12030, 2350, 2390, 67.7, 24,21),
|
||||
TEU(5890 ,2350,2390, 33.0, 11,10),
|
||||
HQ(12030, 2350, 2690, 76.4, 24,21),
|
||||
HC(12030, 2350, 2690, 76.4, 24,21),
|
||||
TRUCK(13600,2450, 2650, 88.3, 34, 33);
|
||||
|
||||
private final int length;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,23 @@
|
|||
package de.avatic.lcc.dto.report;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import de.avatic.lcc.dto.generic.MaterialDTO;
|
||||
import de.avatic.lcc.dto.generic.NodeDTO;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ReportDTO {
|
||||
|
||||
MaterialDTO material;
|
||||
|
||||
@JsonProperty("start_date")
|
||||
private LocalDateTime startDate;
|
||||
|
||||
@JsonProperty("end_date")
|
||||
private LocalDateTime endDate;
|
||||
|
||||
NodeDTO supplier;
|
||||
|
||||
@JsonProperty("costs")
|
||||
|
|
@ -54,4 +64,28 @@ public class ReportDTO {
|
|||
public void setDestination(List<ReportDestinationDTO> destinations) {
|
||||
this.destinations = destinations;
|
||||
}
|
||||
|
||||
public MaterialDTO getMaterial() {
|
||||
return material;
|
||||
}
|
||||
|
||||
public void setMaterial(MaterialDTO material) {
|
||||
this.material = material;
|
||||
}
|
||||
|
||||
public LocalDateTime getStartDate() {
|
||||
return startDate;
|
||||
}
|
||||
|
||||
public void setStartDate(LocalDateTime startDate) {
|
||||
this.startDate = startDate;
|
||||
}
|
||||
|
||||
public LocalDateTime getEndDate() {
|
||||
return endDate;
|
||||
}
|
||||
|
||||
public void setEndDate(LocalDateTime endDate) {
|
||||
this.endDate = endDate;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ public class ReportDestinationDTO {
|
|||
private Double transportTime;
|
||||
|
||||
@JsonProperty("safety_stock")
|
||||
private Double safetyStock;
|
||||
private Integer safetyStock;
|
||||
|
||||
/* packaging */
|
||||
|
||||
|
|
@ -150,11 +150,11 @@ public class ReportDestinationDTO {
|
|||
this.transportTime = transportTime;
|
||||
}
|
||||
|
||||
public Double getSafetyStock() {
|
||||
public Integer getSafetyStock() {
|
||||
return safetyStock;
|
||||
}
|
||||
|
||||
public void setSafetyStock(Double safetyStock) {
|
||||
public void setSafetyStock(Integer safetyStock) {
|
||||
this.safetyStock = safetyStock;
|
||||
}
|
||||
|
||||
|
|
|
|||
4
src/main/java/de/avatic/lcc/model/ValidityTuple.java
Normal file
4
src/main/java/de/avatic/lcc/model/ValidityTuple.java
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
package de.avatic.lcc.model;
|
||||
|
||||
public record ValidityTuple(Integer periodId, Integer propertySetId) {
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package de.avatic.lcc.model.calculations;
|
||||
|
||||
public class CalculationResult {
|
||||
|
||||
Integer jobId;
|
||||
|
||||
CalculationJobState state;
|
||||
|
||||
Throwable exception;
|
||||
|
||||
|
||||
public CalculationResult(Integer jobId) {
|
||||
this.jobId = jobId;
|
||||
this.exception = null;
|
||||
this.state = CalculationJobState.VALID;
|
||||
}
|
||||
|
||||
public CalculationResult(Integer jobId, Throwable e) {
|
||||
this.jobId = jobId;
|
||||
this.exception = e;
|
||||
this.state = CalculationJobState.EXCEPTION;
|
||||
}
|
||||
|
||||
public CalculationJobState getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setState(CalculationJobState state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public Throwable getException() {
|
||||
return exception;
|
||||
}
|
||||
|
||||
public void setException(Throwable exception) {
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
public Integer getJobId() {
|
||||
return jobId;
|
||||
}
|
||||
|
||||
public void setJobId(Integer jobId) {
|
||||
this.jobId = jobId;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package de.avatic.lcc.repositories;
|
||||
|
||||
import de.avatic.lcc.dto.generic.NodeType;
|
||||
import de.avatic.lcc.model.ValidityTuple;
|
||||
import de.avatic.lcc.model.nodes.Node;
|
||||
import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
|
||||
import de.avatic.lcc.repositories.pagination.SearchQueryResult;
|
||||
|
|
@ -380,7 +381,7 @@ public class NodeRepository {
|
|||
return jdbcTemplate.query(query, new NodeMapper(), countryId, countryId);
|
||||
}
|
||||
|
||||
public List<Node> findNodeListsForReportingByPeriodId(Integer materialId, Integer periodId) {
|
||||
public List<Node> findNodeListsForReportingByPeriodId(Integer materialId, ValidityTuple tuple) {
|
||||
|
||||
|
||||
String suppliersSql = """
|
||||
|
|
@ -390,11 +391,12 @@ public class NodeRepository {
|
|||
INNER JOIN node n ON p.supplier_node_id = n.id
|
||||
WHERE p.material_id = ?
|
||||
AND cj.validity_period_id = ?
|
||||
AND cj.property_set_id = ?
|
||||
AND p.supplier_node_id IS NOT NULL
|
||||
""";
|
||||
|
||||
|
||||
return jdbcTemplate.query(suppliersSql, new NodeMapper(), materialId, periodId);
|
||||
return jdbcTemplate.query(suppliersSql, new NodeMapper(), materialId, tuple.periodId(), tuple.propertySetId());
|
||||
}
|
||||
|
||||
public Optional<Node> getByDestinationId(Integer id) {
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ public class CalculationJobRepository {
|
|||
return Optional.of(job.getFirst());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void setStateTo(Integer id, CalculationJobState calculationJobState) {
|
||||
String sql = "UPDATE calculation_job SET job_state = ? WHERE id = ?";
|
||||
|
||||
|
|
|
|||
|
|
@ -51,9 +51,14 @@ public class CalculationJobRouteSectionRepository {
|
|||
|
||||
jdbcTemplate.update(connection -> {
|
||||
var ps = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS);
|
||||
ps.setInt(1, section.getPremiseRouteSectionId());
|
||||
|
||||
if (section.getPremiseRouteSectionId() == null)
|
||||
ps.setNull(1, java.sql.Types.INTEGER);
|
||||
else
|
||||
ps.setInt(1, section.getPremiseRouteSectionId());
|
||||
|
||||
ps.setInt(2, section.getCalculationJobDestinationId());
|
||||
ps.setString(3, section.getTransportType().name());
|
||||
ps.setString(3, convertRateTypeToString(section.getRateType(), section.getTransportType()));
|
||||
ps.setBoolean(4, section.getUnmixedPrice());
|
||||
ps.setBoolean(5, section.isCbmPrice());
|
||||
ps.setBoolean(6, section.isWeightPrice());
|
||||
|
|
@ -73,6 +78,17 @@ public class CalculationJobRouteSectionRepository {
|
|||
return keyHolder.getKey() != null ? keyHolder.getKey().intValue() : null;
|
||||
}
|
||||
|
||||
private String convertRateTypeToString(RateType rateType, TransportType transportType) {
|
||||
if (rateType == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return switch (rateType) {
|
||||
case MATRIX, D2D -> rateType.name();
|
||||
case CONTAINER -> transportType == null ? null : transportType.name();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@Transactional
|
||||
public List<CalculationJobRouteSection> getRouteSectionsByDestinationId(Integer destinationId) {
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ public class DestinationRepository {
|
|||
}
|
||||
|
||||
@Transactional
|
||||
public void update(Integer id, Integer annualAmount, BigDecimal repackingCost, BigDecimal disposalCost, BigDecimal handlingCost, Boolean isD2d, BigDecimal d2dRate) {
|
||||
public void update(Integer id, Integer annualAmount, BigDecimal repackingCost, BigDecimal disposalCost, BigDecimal handlingCost, Boolean isD2d, BigDecimal d2dRate, BigDecimal d2dLeadTime) {
|
||||
if (id == null) {
|
||||
throw new InvalidArgumentException("ID cannot be null");
|
||||
}
|
||||
|
|
@ -80,6 +80,9 @@ public class DestinationRepository {
|
|||
setClauses.add("rate_d2d = :d2dRate");
|
||||
parameters.put("d2dRate", setD2d ? d2dRate : null);
|
||||
|
||||
setClauses.add("lead_time_d2d = :d2dLeadTime");
|
||||
parameters.put("d2dLeadTime", setD2d ? d2dLeadTime : null);
|
||||
|
||||
|
||||
if (annualAmount != null) {
|
||||
setClauses.add("annual_amount = :annualAmount");
|
||||
|
|
|
|||
|
|
@ -637,6 +637,25 @@ public class PremiseRepository {
|
|||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void setStatus(List<Integer> premisesIds, PremiseState premiseState) {
|
||||
if (premisesIds == null || premisesIds.isEmpty() || premiseState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String placeholders = String.join(",", Collections.nCopies(premisesIds.size(), "?"));
|
||||
String query = "UPDATE premise SET state = ? WHERE id IN (" + placeholders + ")";
|
||||
|
||||
List<Object> params = new ArrayList<>();
|
||||
params.add(premiseState.name());
|
||||
params.addAll(premisesIds);
|
||||
|
||||
var affectedRows = jdbcTemplate.update(query, params.toArray());
|
||||
|
||||
if (affectedRows != premisesIds.size())
|
||||
throw new DatabaseException("Premise update failed for " + premisesIds.size() + " premises. Affected rows: " + affectedRows);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encapsulates SQL query building logic
|
||||
|
|
|
|||
|
|
@ -138,6 +138,17 @@ public class PropertySetRepository {
|
|||
return stateString != null ? ValidityPeriodState.valueOf(stateString) : null;
|
||||
}
|
||||
|
||||
public PropertySet getById(Integer propertySetId) {
|
||||
String query = "SELECT id, start_date, end_date, state FROM property_set WHERE id = ?";
|
||||
|
||||
var set = jdbcTemplate.query(query, new PropertySetMapper(), propertySetId);
|
||||
|
||||
if(set.isEmpty())
|
||||
throw new IllegalArgumentException("Property set with id " + propertySetId + " does not exist");
|
||||
|
||||
return set.getFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapper class for converting SQL query results into {@link PropertySet} objects.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package de.avatic.lcc.repositories.rates;
|
||||
|
||||
import de.avatic.lcc.model.ValidityTuple;
|
||||
import de.avatic.lcc.model.rates.ValidityPeriod;
|
||||
import de.avatic.lcc.model.rates.ValidityPeriodState;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
|
@ -69,8 +70,8 @@ public class ValidityPeriodRepository {
|
|||
*/
|
||||
@Transactional
|
||||
public boolean invalidateById(Integer id) {
|
||||
var affectedRows = jdbcTemplate.update("UPDATE validity_period SET state = ? WHERE id = ? AND state = ? ", ValidityPeriodState.INVALID.name(), id, ValidityPeriodState.EXPIRED.name());
|
||||
return affectedRows > 0;
|
||||
var affectedRows = jdbcTemplate.update("UPDATE validity_period SET state = ? WHERE id = ? AND state = ? ", ValidityPeriodState.INVALID.name(), id, ValidityPeriodState.EXPIRED.name());
|
||||
return affectedRows > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -109,7 +110,7 @@ public class ValidityPeriodRepository {
|
|||
String query = "SELECT * FROM validity_period WHERE state = ?";
|
||||
var period = jdbcTemplate.query(query, new ValidityPeriodMapper(), ValidityPeriodState.VALID.name());
|
||||
|
||||
if(period.isEmpty())
|
||||
if (period.isEmpty())
|
||||
return Optional.empty();
|
||||
|
||||
return Optional.of(period.getFirst());
|
||||
|
|
@ -134,17 +135,17 @@ public class ValidityPeriodRepository {
|
|||
return getDraftPeriod().getId();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Checks if there are any draft matrix rates associated with the current draft validity period.
|
||||
*
|
||||
* @return {@code true} if draft matrix rates exist for the current draft validity period;
|
||||
* {@code false} otherwise.
|
||||
* {@code false} otherwise.
|
||||
*/
|
||||
@Transactional
|
||||
public boolean hasRateDrafts() {
|
||||
Integer id = getDraftPeriodId();
|
||||
if(id == null) return false;
|
||||
if (id == null) return false;
|
||||
|
||||
String query = "SELECT COUNT(*) FROM country_matrix_rate WHERE validity_period_id = ?";
|
||||
var totalCount = jdbcTemplate.queryForObject(query, Integer.class, id);
|
||||
|
|
@ -176,30 +177,30 @@ public class ValidityPeriodRepository {
|
|||
String placeholders = String.join(",", Collections.nCopies(nodeIds.size(), "?"));
|
||||
|
||||
String validityPeriodSql = """
|
||||
SELECT vp.*
|
||||
FROM validity_period vp
|
||||
INNER JOIN (
|
||||
SELECT
|
||||
cj.validity_period_id,
|
||||
COUNT(DISTINCT p.supplier_node_id) as node_count
|
||||
FROM
|
||||
premise p
|
||||
INNER JOIN
|
||||
calculation_job cj ON p.id = cj.premise_id
|
||||
WHERE
|
||||
p.material_id = ?
|
||||
AND p.supplier_node_id IN ("""
|
||||
SELECT vp.*
|
||||
FROM validity_period vp
|
||||
INNER JOIN (
|
||||
SELECT
|
||||
cj.validity_period_id,
|
||||
COUNT(DISTINCT p.supplier_node_id) as node_count
|
||||
FROM
|
||||
premise p
|
||||
INNER JOIN
|
||||
calculation_job cj ON p.id = cj.premise_id
|
||||
WHERE
|
||||
p.material_id = ?
|
||||
AND p.supplier_node_id IN ("""
|
||||
+ placeholders + """
|
||||
)
|
||||
GROUP BY
|
||||
cj.validity_period_id
|
||||
HAVING
|
||||
COUNT(DISTINCT p.supplier_node_id) = ?
|
||||
) matching_periods ON vp.id = matching_periods.validity_period_id
|
||||
ORDER BY
|
||||
vp.start_date DESC
|
||||
LIMIT 1
|
||||
""";
|
||||
)
|
||||
GROUP BY
|
||||
cj.validity_period_id
|
||||
HAVING
|
||||
COUNT(DISTINCT p.supplier_node_id) = ?
|
||||
) matching_periods ON vp.id = matching_periods.validity_period_id
|
||||
ORDER BY
|
||||
vp.start_date DESC
|
||||
LIMIT 1
|
||||
""";
|
||||
|
||||
|
||||
Object[] params = new Object[1 + nodeIds.size() + 1];
|
||||
|
|
@ -218,18 +219,20 @@ public class ValidityPeriodRepository {
|
|||
return Optional.of(periods.getFirst());
|
||||
}
|
||||
|
||||
public List<Integer> findValidityPeriodsWithReportByMaterialId(Integer materialId) {
|
||||
public List<ValidityTuple> findValidityPeriodsWithReportByMaterialId(Integer materialId) {
|
||||
|
||||
String validityPeriodSql = """
|
||||
SELECT DISTINCT cj.validity_period_id
|
||||
SELECT DISTINCT cj.validity_period_id, cj.property_set_id
|
||||
FROM premise p
|
||||
INNER JOIN calculation_job cj ON p.id = cj.premise_id
|
||||
WHERE p.material_id = ?
|
||||
""";
|
||||
|
||||
return jdbcTemplate.queryForList(validityPeriodSql, Integer.class, materialId);
|
||||
return jdbcTemplate.query(validityPeriodSql, (rs, cnt) -> new ValidityTuple(rs.getInt("validity_period_id"), rs.getInt("property_set_id")), materialId);
|
||||
}
|
||||
|
||||
;
|
||||
|
||||
/**
|
||||
* Maps rows of a {@link ResultSet} to {@link ValidityPeriod} objects.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package de.avatic.lcc.repositories.users;
|
||||
|
||||
import de.avatic.lcc.model.ValidityTuple;
|
||||
import de.avatic.lcc.model.nodes.Node;
|
||||
import de.avatic.lcc.repositories.NodeRepository;
|
||||
import de.avatic.lcc.util.exception.base.ForbiddenException;
|
||||
|
|
@ -81,7 +82,7 @@ public class UserNodeRepository {
|
|||
return Optional.of(nodes.getFirst());
|
||||
}
|
||||
|
||||
public List<Node> findNodeListsForReportingByPeriodId(Integer materialId, Integer periodId) {
|
||||
public List<Node> findNodeListsForReportingByPeriodId(Integer materialId, ValidityTuple tuple) {
|
||||
String userSuppliersSql = """
|
||||
SELECT DISTINCT un.*
|
||||
FROM premise p
|
||||
|
|
@ -89,10 +90,11 @@ public class UserNodeRepository {
|
|||
INNER JOIN sys_user_node un ON p.user_supplier_node_id = un.id
|
||||
WHERE p.material_id = ?
|
||||
AND cj.validity_period_id = ?
|
||||
AND cj.property_set_id = ?
|
||||
AND p.user_supplier_node_id IS NOT NULL
|
||||
""";
|
||||
|
||||
return jdbcTemplate.query(userSuppliersSql, new NodeMapper(), materialId, periodId);
|
||||
return jdbcTemplate.query(userSuppliersSql, new NodeMapper(), materialId, tuple.periodId(), tuple.propertySetId());
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,12 @@ import java.math.BigDecimal;
|
|||
@Service
|
||||
public class CustomApiService {
|
||||
public BigDecimal getTariffRate(String hsCode, Integer countryId) {
|
||||
return BigDecimal.valueOf(3);
|
||||
return BigDecimal.valueOf(0.3);
|
||||
|
||||
//TODO implement me
|
||||
}
|
||||
|
||||
public boolean validate(String hsCode) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ public class ContainerRateService {
|
|||
dto.setDestination(nodeTransformer.toNodeDTO(nodeRepository.getById(entity.getToNodeId()).orElseThrow()));
|
||||
|
||||
var rates = new HashMap<String, Number>();
|
||||
rates.put(ContainerType.HQ.name(), entity.getRateHc());
|
||||
rates.put(ContainerType.HC.name(), entity.getRateHc());
|
||||
rates.put(ContainerType.FEU.name(), entity.getRateFeu());
|
||||
rates.put(ContainerType.TEU.name(), entity.getRateTeu());
|
||||
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ public class DestinationService {
|
|||
}
|
||||
|
||||
|
||||
|
||||
private List<Destination> createDestination(List<Integer> premiseIds, Integer destinationNodeId, Integer annualAmount, Number repackingCost, Number disposalCost, Number handlingCost) {
|
||||
|
||||
var premisesToProcess = premiseRepository.getPremisesById(premiseIds);
|
||||
|
|
@ -96,6 +97,8 @@ public class DestinationService {
|
|||
destination.setId(destinationRepository.insert(destination));
|
||||
|
||||
Node source = premise.getSupplierNodeId() == null ? userNodeRepository.getById(premise.getUserSupplierNodeId()).orElseThrow() : nodeRepository.getById(premise.getSupplierNodeId()).orElseThrow();
|
||||
|
||||
//noinspection SpringTransactionalMethodCallsInspection
|
||||
findRouteAndSave(destination.getId(), destinationNode, source, premise.getSupplierNodeId() == null);
|
||||
destinations.add(destination);
|
||||
}
|
||||
|
|
@ -152,7 +155,10 @@ public class DestinationService {
|
|||
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.getD2d(), destinationUpdateDTO.getRateD2d() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getRateD2d().doubleValue()));
|
||||
destinationUpdateDTO.getD2d(), destinationUpdateDTO.getRateD2d() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getRateD2d().doubleValue()),
|
||||
destinationUpdateDTO.getLeadtimeD2d() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getLeadtimeD2d())
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import de.avatic.lcc.dto.calculation.edit.masterData.PackagingUpdateDTO;
|
|||
import de.avatic.lcc.dto.calculation.edit.masterData.PriceUpdateDTO;
|
||||
import de.avatic.lcc.model.calculations.CalculationJob;
|
||||
import de.avatic.lcc.model.calculations.CalculationJobState;
|
||||
import de.avatic.lcc.model.premises.Premise;
|
||||
import de.avatic.lcc.model.premises.PremiseState;
|
||||
import de.avatic.lcc.repositories.calculation.CalculationJobRepository;
|
||||
import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
|
||||
import de.avatic.lcc.repositories.pagination.SearchQueryResult;
|
||||
|
|
@ -17,6 +19,7 @@ import de.avatic.lcc.repositories.properties.PropertySetRepository;
|
|||
import de.avatic.lcc.repositories.rates.ValidityPeriodRepository;
|
||||
import de.avatic.lcc.service.calculation.execution.CalculationExecutionService;
|
||||
import de.avatic.lcc.service.calculation.execution.CalculationStatusService;
|
||||
import de.avatic.lcc.service.precalculation.PreCalculationCheckService;
|
||||
import de.avatic.lcc.service.transformer.generic.DimensionTransformer;
|
||||
import de.avatic.lcc.service.transformer.premise.PremiseTransformer;
|
||||
import de.avatic.lcc.util.exception.base.InternalErrorException;
|
||||
|
|
@ -25,8 +28,10 @@ import org.springframework.transaction.annotation.Transactional;
|
|||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
@Service
|
||||
public class PremisesService {
|
||||
|
|
@ -41,8 +46,9 @@ public class PremisesService {
|
|||
private final ValidityPeriodRepository validityPeriodRepository;
|
||||
private final CalculationStatusService calculationStatusService;
|
||||
private final CalculationExecutionService calculationExecutionService;
|
||||
private final PreCalculationCheckService preCalculationCheckService;
|
||||
|
||||
public PremisesService(PremiseRepository premiseRepository, PremiseTransformer premiseTransformer, DimensionTransformer dimensionTransformer, DestinationService destinationService, CalculationJobRepository calculationJobRepository, PropertyRepository propertyRepository, PropertySetRepository propertySetRepository, ValidityPeriodRepository validityPeriodRepository, CalculationStatusService calculationStatusService, CalculationExecutionService calculationExecutionService) {
|
||||
public PremisesService(PremiseRepository premiseRepository, PremiseTransformer premiseTransformer, DimensionTransformer dimensionTransformer, DestinationService destinationService, CalculationJobRepository calculationJobRepository, PropertyRepository propertyRepository, PropertySetRepository propertySetRepository, ValidityPeriodRepository validityPeriodRepository, CalculationStatusService calculationStatusService, CalculationExecutionService calculationExecutionService, PreCalculationCheckService preCalculationCheckService) {
|
||||
this.premiseRepository = premiseRepository;
|
||||
this.premiseTransformer = premiseTransformer;
|
||||
this.dimensionTransformer = dimensionTransformer;
|
||||
|
|
@ -53,6 +59,7 @@ public class PremisesService {
|
|||
this.validityPeriodRepository = validityPeriodRepository;
|
||||
this.calculationStatusService = calculationStatusService;
|
||||
this.calculationExecutionService = calculationExecutionService;
|
||||
this.preCalculationCheckService = preCalculationCheckService;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
|
|
@ -74,16 +81,18 @@ public class PremisesService {
|
|||
var userId = 1;
|
||||
premiseRepository.checkOwner(premiseIds, userId);
|
||||
|
||||
return premiseRepository.getPremisesById(premiseIds).stream().map(premiseTransformer::toPremiseDetailDTO).toList();
|
||||
return premiseRepository.getPremisesById(premiseIds).stream().filter(p -> p.getState().equals(PremiseState.DRAFT)).map(premiseTransformer::toPremiseDetailDTO).toList();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Transactional
|
||||
public Integer startCalculation(List<Integer> premises) {
|
||||
public void startCalculation(List<Integer> premises) {
|
||||
|
||||
var userId = 1; // TODO get current user id
|
||||
|
||||
// todo check if user is allowed to schedule this
|
||||
|
||||
premises.forEach(preCalculationCheckService::doPrecheck);
|
||||
|
||||
var validSetId = propertySetRepository.getValidSetId();
|
||||
var validPeriodId = validityPeriodRepository.getValidPeriodId().orElseThrow(() -> new InternalErrorException("no valid period found that is VALID"));
|
||||
|
||||
|
|
@ -102,17 +111,28 @@ public class PremisesService {
|
|||
calculationIds.add(calculationJobRepository.insert(job));
|
||||
});
|
||||
|
||||
//TODO set premise to completed.
|
||||
|
||||
premiseRepository.setStatus(premises, PremiseState.COMPLETED);
|
||||
|
||||
calculationIds.forEach(this::scheduleCalculation);
|
||||
return calculationStatusService.schedule(calculationIds);
|
||||
|
||||
try {
|
||||
var futures = calculationIds.stream().map(calculationExecutionService::launchJobCalculation).toList();
|
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
||||
|
||||
for (var future : futures) {
|
||||
var jobResult = future.get();
|
||||
if (jobResult.getState().equals(CalculationJobState.EXCEPTION)) {
|
||||
throw new InternalErrorException("Calculation failed: " + jobResult.getException().getMessage(), new Exception(jobResult.getException()));
|
||||
}
|
||||
}
|
||||
;
|
||||
} catch (CompletionException | InterruptedException | ExecutionException e) {
|
||||
throw new InternalErrorException("Calculation failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: scheduled should be set by worker thread that processes the job.
|
||||
public void scheduleCalculation(Integer id) {
|
||||
calculationJobRepository.setStateTo(id, CalculationJobState.SCHEDULED);
|
||||
calculationExecutionService.calculateJob(id);
|
||||
}
|
||||
|
||||
public CalculationStatus getCalculationStatus(Integer processId) {
|
||||
|
|
@ -150,7 +170,7 @@ public class PremisesService {
|
|||
var price = priceUpdateDTO.getPrice() == null ? null : BigDecimal.valueOf(priceUpdateDTO.getPrice().doubleValue());
|
||||
var overseaShare = priceUpdateDTO.getOverseaShare() == null ? null : BigDecimal.valueOf(priceUpdateDTO.getOverseaShare().doubleValue());
|
||||
|
||||
premiseRepository.updatePrice(priceUpdateDTO.getPremiseIds(), price, priceUpdateDTO.getIncludeFcaFee(),overseaShare);
|
||||
premiseRepository.updatePrice(priceUpdateDTO.getPremiseIds(), price, priceUpdateDTO.getIncludeFcaFee(), overseaShare);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
|
@ -159,8 +179,19 @@ public class PremisesService {
|
|||
var userId = 1;
|
||||
premiseRepository.checkOwner(premiseIds, userId);
|
||||
|
||||
destinationService.deleteAllDestinationsByPremiseId(premiseIds, false);
|
||||
premiseRepository.deletePremisesById(premiseIds);
|
||||
// only delete drafts.
|
||||
var toBeDeleted = premiseRepository.getPremisesById(premiseIds).stream().filter(p -> p.getState().equals(PremiseState.DRAFT)).map(Premise::getId).toList();
|
||||
destinationService.deleteAllDestinationsByPremiseId(toBeDeleted, false);
|
||||
premiseRepository.deletePremisesById(toBeDeleted);
|
||||
}
|
||||
|
||||
public void archive(List<Integer> premiseIds) {
|
||||
//TODO check authorization
|
||||
var userId = 1;
|
||||
premiseRepository.checkOwner(premiseIds, userId);
|
||||
|
||||
// only archive completed.
|
||||
var premisses = premiseRepository.getPremisesById(premiseIds);
|
||||
premiseRepository.setStatus(premisses.stream().filter(p -> p.getState().equals(PremiseState.COMPLETED)).map(Premise::getId).toList(), PremiseState.ARCHIVED);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
package de.avatic.lcc.service.calculation.execution;
|
||||
|
||||
import de.avatic.lcc.calculationmodel.*;
|
||||
import de.avatic.lcc.dto.calculation.CalculationStatus;
|
||||
import de.avatic.lcc.dto.generic.ContainerType;
|
||||
import de.avatic.lcc.dto.generic.RateType;
|
||||
import de.avatic.lcc.model.calculations.CalculationJob;
|
||||
import de.avatic.lcc.model.calculations.CalculationJobDestination;
|
||||
import de.avatic.lcc.model.calculations.CalculationJobRouteSection;
|
||||
import de.avatic.lcc.model.calculations.CalculationJobState;
|
||||
import de.avatic.lcc.model.calculations.*;
|
||||
import de.avatic.lcc.model.packaging.LoadCarrierType;
|
||||
import de.avatic.lcc.model.packaging.PackagingDimension;
|
||||
import de.avatic.lcc.model.premises.Premise;
|
||||
|
|
@ -24,11 +22,14 @@ import de.avatic.lcc.repositories.properties.PropertyRepository;
|
|||
import de.avatic.lcc.service.calculation.execution.steps.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Service
|
||||
public class CalculationExecutionService {
|
||||
|
|
@ -83,6 +84,19 @@ public class CalculationExecutionService {
|
|||
.orElse(null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Async("taskExecutor")
|
||||
public CompletableFuture<CalculationResult> launchJobCalculation(Integer calculationId) {
|
||||
try {
|
||||
calculateJob(calculationId);
|
||||
} catch (Throwable e) {
|
||||
//TODO put error in database.
|
||||
calculationJobRepository.setStateTo(calculationId, CalculationJobState.EXCEPTION);
|
||||
return CompletableFuture.completedFuture(new CalculationResult(calculationId, e));
|
||||
}
|
||||
return CompletableFuture.completedFuture(new CalculationResult(calculationId));
|
||||
}
|
||||
|
||||
public void calculateJob(Integer calculationId) {
|
||||
|
||||
CalculationJob calculation = calculationJobRepository.getCalculationJob(calculationId).orElseThrow();
|
||||
|
|
@ -116,7 +130,7 @@ public class CalculationExecutionService {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
calculationJobRepository.setStateTo(calculationId, CalculationJobState.VALID);
|
||||
log.info("Calculation job {} finished", calculationId);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import de.avatic.lcc.repositories.properties.PropertyRepository;
|
|||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
@Service
|
||||
public class AirfreightCalculationService {
|
||||
|
|
@ -46,7 +47,7 @@ public class AirfreightCalculationService {
|
|||
var result = new AirfreightResult();
|
||||
|
||||
var huCost = BigDecimal.valueOf(1.08).multiply(((getShippingWeight(hu).multiply(preCarriage.add(mainCarriage).add(postCarriage).add(terminalFee))).add(preCarriageFee).add(customsClearanceFee).add(handOverFee)));
|
||||
var totalCost = huCost.multiply(BigDecimal.valueOf(destination.getAnnualAmount()).divide(BigDecimal.valueOf(premise.getHuUnitCount())));
|
||||
var totalCost = huCost.multiply(BigDecimal.valueOf(destination.getAnnualAmount()).divide(BigDecimal.valueOf(premise.getHuUnitCount()), 2, RoundingMode.HALF_UP));
|
||||
var airfreightShareCost = totalCost.multiply(BigDecimal.valueOf(airfreightShare));
|
||||
|
||||
result.setVolumetricWeight(BigDecimal.valueOf(getVolumeWeight(hu)));
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ public class ContainerCalculationService {
|
|||
ContainerType.FEU, SystemPropertyMappingId.FEU_LOAD,
|
||||
ContainerType.TRUCK, SystemPropertyMappingId.TRUCK_LOAD,
|
||||
ContainerType.TEU, SystemPropertyMappingId.TEU_LOAD,
|
||||
ContainerType.HQ, SystemPropertyMappingId.FEU_LOAD
|
||||
ContainerType.HC, SystemPropertyMappingId.FEU_LOAD
|
||||
);
|
||||
|
||||
SystemPropertyMappingId mappingId = mappings.get(containerType);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package de.avatic.lcc.service.calculation.execution.steps;
|
|||
import de.avatic.lcc.calculationmodel.ContainerCalculationResult;
|
||||
import de.avatic.lcc.calculationmodel.CustomResult;
|
||||
import de.avatic.lcc.calculationmodel.SectionInfo;
|
||||
import de.avatic.lcc.dto.generic.RateType;
|
||||
import de.avatic.lcc.model.premises.Premise;
|
||||
import de.avatic.lcc.model.premises.route.Destination;
|
||||
import de.avatic.lcc.model.properties.CountryPropertyMappingId;
|
||||
|
|
@ -117,6 +118,11 @@ public class CustomCostCalculationService {
|
|||
|
||||
List<SectionInfo> customSections = new ArrayList<>();
|
||||
|
||||
if (sections.size() == 1 && sections.getFirst().result().getRateType().equals(RateType.D2D)) {
|
||||
customSections.add(sections.getFirst());
|
||||
return customSections;
|
||||
}
|
||||
|
||||
for (SectionInfo section : sections) {
|
||||
if (!(CustomUnionType.EU == getCustomUnionByRouteNodeId(section.section().getFromRouteNodeId()) &&
|
||||
CustomUnionType.EU == getCustomUnionByRouteNodeId(section.section().getToRouteNodeId()))) {
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ public class RouteSectionCostCalculationService {
|
|||
private BigDecimal getContainerRate(ContainerRate rate, ContainerType containerType) {
|
||||
|
||||
switch (containerType) {
|
||||
case HQ -> {
|
||||
case HC -> {
|
||||
return rate.getRateHc();
|
||||
}
|
||||
case FEU -> {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
package de.avatic.lcc.service.precalculation;
|
||||
|
||||
import de.avatic.lcc.model.nodes.Node;
|
||||
import de.avatic.lcc.model.premises.Premise;
|
||||
import de.avatic.lcc.model.premises.route.Destination;
|
||||
import de.avatic.lcc.model.premises.route.Route;
|
||||
import de.avatic.lcc.repositories.NodeRepository;
|
||||
import de.avatic.lcc.repositories.premise.DestinationRepository;
|
||||
import de.avatic.lcc.repositories.premise.PremiseRepository;
|
||||
import de.avatic.lcc.repositories.premise.RouteRepository;
|
||||
import de.avatic.lcc.service.CustomApiService;
|
||||
import de.avatic.lcc.util.exception.internalerror.PremiseValidationError;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Service
|
||||
public class PreCalculationCheckService {
|
||||
|
||||
private final PremiseRepository premiseRepository;
|
||||
private final CustomApiService customApiService;
|
||||
private final DestinationRepository destinationRepository;
|
||||
private final RouteRepository routeRepository;
|
||||
private final NodeRepository nodeRepository;
|
||||
|
||||
public PreCalculationCheckService(PremiseRepository premiseRepository, CustomApiService customApiService, DestinationRepository destinationRepository, RouteRepository routeRepository, NodeRepository nodeRepository) {
|
||||
this.premiseRepository = premiseRepository;
|
||||
this.customApiService = customApiService;
|
||||
this.destinationRepository = destinationRepository;
|
||||
this.routeRepository = routeRepository;
|
||||
this.nodeRepository = nodeRepository;
|
||||
}
|
||||
|
||||
public void doPrecheck(Integer premiseId) {
|
||||
var premise = premiseRepository.getPremiseById(premiseId).orElseThrow();
|
||||
|
||||
supplierCheck(premise);
|
||||
|
||||
materialCheck(premise);
|
||||
|
||||
packagingCheck(premise);
|
||||
|
||||
priceCheck(premise);
|
||||
|
||||
|
||||
for (Destination destination : destinationRepository.getByPremiseId(premiseId)) {
|
||||
|
||||
var node = nodeRepository.getByDestinationId(destination.getId()).orElseThrow();
|
||||
|
||||
destinationCheck(destination, node);
|
||||
|
||||
var routes = routeRepository.getByDestinationId(destination.getId());
|
||||
|
||||
|
||||
|
||||
if(routes.isEmpty())
|
||||
throw new PremiseValidationError("No route found for destination " + node.getName() + ". Cannot use standard routing.");
|
||||
|
||||
if(routes.stream().noneMatch(Route::getSelected))
|
||||
throw new PremiseValidationError("No route selected for destination " + node.getName());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void destinationCheck(Destination destination, Node node) {
|
||||
|
||||
if (destination.getAnnualAmount() == null || destination.getAnnualAmount() == 0)
|
||||
throw new PremiseValidationError("In destination " + node.getName() + ": annual quantity must be greater than zero.");
|
||||
|
||||
if (destination.getD2d() == null)
|
||||
throw new PremiseValidationError("In destination " + node.getName() +": D2D not set.");
|
||||
|
||||
if (destination.getD2d() == true) {
|
||||
if (destination.getRateD2d() == null || destination.getRateD2d().equals(BigDecimal.ZERO)) {
|
||||
throw new PremiseValidationError("In destination " + node.getName() + ": D2D Rate not set or set to zero.");
|
||||
}
|
||||
|
||||
if (destination.getLeadTimeD2d() == null || destination.getLeadTimeD2d() == 0) {
|
||||
throw new PremiseValidationError("In destination " + node.getName() + ": D2D lead time not set or set to zero.");
|
||||
}
|
||||
}
|
||||
|
||||
if (destination.getCountryId() == null || destination.getCountryId() == 0) {
|
||||
throw new PremiseValidationError("In destination " + node.getName() +": destination country ID not set. Please contact administrator");
|
||||
}
|
||||
|
||||
if (destination.getGeoLat() == null || destination.getGeoLng() == null) {
|
||||
throw new PremiseValidationError("In destination " + node.getName() + ": destination geo location not set. Please contact administrator");
|
||||
}
|
||||
|
||||
if (destination.getDisposalCost() != null && destination.getDisposalCost().equals(BigDecimal.ZERO)) {
|
||||
throw new PremiseValidationError("In destination " + node.getName() + ": disposal cost set to zero.");
|
||||
}
|
||||
|
||||
if (destination.getHandlingCost() != null && destination.getHandlingCost().equals(BigDecimal.ZERO)) {
|
||||
throw new PremiseValidationError("In destination " + node.getName() + ": handling cost set to zero.");
|
||||
}
|
||||
|
||||
if (destination.getRepackingCost() != null && destination.getRepackingCost().equals(BigDecimal.ZERO)) {
|
||||
throw new PremiseValidationError("In destination " + node.getName() + ": repackaging cost set to zero.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void supplierCheck(Premise premise) {
|
||||
|
||||
if (premise.getSupplierNodeId() == null && premise.getUserSupplierNodeId() == null)
|
||||
throw new PremiseValidationError("Supplier node is not set");
|
||||
|
||||
}
|
||||
|
||||
private void priceCheck(Premise premise) {
|
||||
|
||||
if (premise.getMaterialCost() == null || premise.getMaterialCost().equals(BigDecimal.ZERO)) {
|
||||
throw new PremiseValidationError("MEK_A is not set or set to zero");
|
||||
}
|
||||
|
||||
if (premise.getOverseaShare() == null || premise.getOverseaShare().equals(BigDecimal.ZERO)) {
|
||||
throw new PremiseValidationError("Oversea share is not set or set to zero");
|
||||
}
|
||||
|
||||
if (premise.getFcaEnabled() == null) {
|
||||
throw new PremiseValidationError("FCA fee is not set");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void packagingCheck(Premise premise) {
|
||||
|
||||
if (premise.getHuMixable() == null) {
|
||||
throw new PremiseValidationError("Mixable not set.");
|
||||
}
|
||||
|
||||
if (premise.getHuStackable() == null) {
|
||||
throw new PremiseValidationError("Stackable not set.");
|
||||
}
|
||||
|
||||
if (premise.getHuStackable() == false && premise.getHuMixable() == true) {
|
||||
throw new PremiseValidationError("Stackable cannot be false, when mixable is true.");
|
||||
}
|
||||
|
||||
if (premise.getIndividualHuLength() == null || premise.getIndividualHuLength() == 0) {
|
||||
throw new PremiseValidationError("Packaging length is not set or set to zero");
|
||||
}
|
||||
|
||||
if (premise.getIndividualHuWidth() == null || premise.getIndividualHuWidth() == 0) {
|
||||
throw new PremiseValidationError("Packaging width is not set or set to zero");
|
||||
}
|
||||
|
||||
if (premise.getIndividualHuHeight() == null || premise.getIndividualHuHeight() == 0) {
|
||||
throw new PremiseValidationError("Packaging height is not set or set to zero");
|
||||
}
|
||||
|
||||
if (premise.getIndividualHuWeight() == null || premise.getIndividualHuWeight() == 0) {
|
||||
throw new PremiseValidationError("Packaging weight is not set or set to zero");
|
||||
}
|
||||
|
||||
if (premise.getHuDisplayedWeightUnit() == null) {
|
||||
throw new PremiseValidationError("Packaging weight unit is not set");
|
||||
}
|
||||
|
||||
if (premise.getHuDisplayedDimensionUnit() == null) {
|
||||
throw new PremiseValidationError("Packaging dimension unit is not set");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void materialCheck(Premise premise) {
|
||||
if (!customApiService.validate(premise.getHsCode()))
|
||||
throw new PremiseValidationError("Invalid HS code.");
|
||||
|
||||
if (premise.getTariffRate() == null) {
|
||||
throw new PremiseValidationError("Tariff Rate not set.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -35,6 +35,7 @@ public class DestinationTransformer {
|
|||
|
||||
dto.setD2d(destination.getD2d());
|
||||
dto.setRateD2d(destination.getRateD2d());
|
||||
dto.setLeadTimeD2d(destination.getLeadTimeD2d());
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package de.avatic.lcc.service.transformer.report;
|
||||
|
||||
import de.avatic.lcc.dto.generic.NodeType;
|
||||
import de.avatic.lcc.dto.generic.RateType;
|
||||
import de.avatic.lcc.dto.report.ReportDTO;
|
||||
import de.avatic.lcc.dto.report.ReportDestinationDTO;
|
||||
import de.avatic.lcc.dto.report.ReportEntryDTO;
|
||||
|
|
@ -8,19 +9,26 @@ import de.avatic.lcc.dto.report.ReportSectionDTO;
|
|||
import de.avatic.lcc.model.calculations.CalculationJob;
|
||||
import de.avatic.lcc.model.calculations.CalculationJobDestination;
|
||||
import de.avatic.lcc.model.calculations.CalculationJobRouteSection;
|
||||
import de.avatic.lcc.model.nodes.Node;
|
||||
import de.avatic.lcc.model.premises.Premise;
|
||||
import de.avatic.lcc.model.properties.SystemPropertyMappingId;
|
||||
import de.avatic.lcc.repositories.MaterialRepository;
|
||||
import de.avatic.lcc.repositories.NodeRepository;
|
||||
import de.avatic.lcc.repositories.calculation.CalculationJobDestinationRepository;
|
||||
import de.avatic.lcc.repositories.calculation.CalculationJobRouteSectionRepository;
|
||||
import de.avatic.lcc.repositories.premise.PremiseRepository;
|
||||
import de.avatic.lcc.repositories.premise.RouteNodeRepository;
|
||||
import de.avatic.lcc.repositories.properties.PropertyRepository;
|
||||
import de.avatic.lcc.repositories.properties.PropertySetRepository;
|
||||
import de.avatic.lcc.repositories.rates.ValidityPeriodRepository;
|
||||
import de.avatic.lcc.repositories.users.UserNodeRepository;
|
||||
import de.avatic.lcc.service.transformer.generic.MaterialTransformer;
|
||||
import de.avatic.lcc.service.transformer.generic.NodeTransformer;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
|
@ -35,8 +43,13 @@ public class ReportTransformer {
|
|||
private final NodeTransformer nodeTransformer;
|
||||
private final RouteNodeRepository routeNodeRepository;
|
||||
private final PropertyRepository propertyRepository;
|
||||
private final UserNodeRepository userNodeRepository;
|
||||
private final MaterialRepository materialRepository;
|
||||
private final MaterialTransformer materialTransformer;
|
||||
private final PropertySetRepository propertySetRepository;
|
||||
private final ValidityPeriodRepository validityPeriodRepository;
|
||||
|
||||
public ReportTransformer(CalculationJobDestinationRepository calculationJobDestinationRepository, CalculationJobRouteSectionRepository calculationJobRouteSectionRepository, PremiseRepository premiseRepository, NodeRepository nodeRepository, NodeTransformer nodeTransformer, RouteNodeRepository routeNodeRepository, PropertyRepository propertyRepository) {
|
||||
public ReportTransformer(CalculationJobDestinationRepository calculationJobDestinationRepository, CalculationJobRouteSectionRepository calculationJobRouteSectionRepository, PremiseRepository premiseRepository, NodeRepository nodeRepository, NodeTransformer nodeTransformer, RouteNodeRepository routeNodeRepository, PropertyRepository propertyRepository, UserNodeRepository userNodeRepository, MaterialRepository materialRepository, MaterialTransformer materialTransformer, PropertySetRepository propertySetRepository, ValidityPeriodRepository validityPeriodRepository) {
|
||||
this.calculationJobDestinationRepository = calculationJobDestinationRepository;
|
||||
this.calculationJobRouteSectionRepository = calculationJobRouteSectionRepository;
|
||||
this.premiseRepository = premiseRepository;
|
||||
|
|
@ -44,6 +57,11 @@ public class ReportTransformer {
|
|||
this.nodeTransformer = nodeTransformer;
|
||||
this.routeNodeRepository = routeNodeRepository;
|
||||
this.propertyRepository = propertyRepository;
|
||||
this.userNodeRepository = userNodeRepository;
|
||||
this.materialRepository = materialRepository;
|
||||
this.materialTransformer = materialTransformer;
|
||||
this.propertySetRepository = propertySetRepository;
|
||||
this.validityPeriodRepository = validityPeriodRepository;
|
||||
}
|
||||
|
||||
public ReportDTO toReportDTO(CalculationJob job) {
|
||||
|
|
@ -59,10 +77,25 @@ public class ReportTransformer {
|
|||
var weightedTotalCost = getWeightedTotalCosts(sections);
|
||||
|
||||
Premise premise = premiseRepository.getPremiseById(job.getPremiseId()).orElseThrow();
|
||||
reportDTO.setMaterial(materialTransformer.toMaterialDTO(materialRepository.getById(premise.getMaterialId()).orElseThrow()));
|
||||
|
||||
var period = getPeriod(job);
|
||||
|
||||
reportDTO.setStartDate(period.startDate);
|
||||
reportDTO.setEndDate(period.endDate);
|
||||
|
||||
Node sourceNode = null;
|
||||
|
||||
if (premise.getSupplierNodeId() != null)
|
||||
sourceNode = nodeRepository.getById(premise.getSupplierNodeId()).orElseThrow();
|
||||
|
||||
if (premise.getUserSupplierNodeId() != null)
|
||||
sourceNode = userNodeRepository.getById(premise.getUserSupplierNodeId()).orElseThrow();
|
||||
|
||||
reportDTO.setCost(getCostMap(job, destinations, weightedTotalCost, includeAirfreight));
|
||||
reportDTO.setRisk(getRisk(job, destinations, weightedTotalCost, includeAirfreight));
|
||||
reportDTO.setDestination(destinations.stream().map(d -> getDestinationDTO(d, sections.get(d.getId()), premise)).toList());
|
||||
Node finalSourceNode = sourceNode;
|
||||
reportDTO.setDestination(destinations.stream().map(d -> getDestinationDTO(d, sections.get(d.getId()), premise, finalSourceNode, includeAirfreight)).toList());
|
||||
|
||||
if (!reportDTO.getDestinations().isEmpty()) {
|
||||
var source = reportDTO.getDestinations().getFirst().getSections().stream().map(ReportSectionDTO::getFromNode).filter(n -> n.getTypes().contains(NodeType.SOURCE)).findFirst().orElseThrow();
|
||||
|
|
@ -73,6 +106,25 @@ public class ReportTransformer {
|
|||
|
||||
}
|
||||
|
||||
private TimePeriod getPeriod(CalculationJob job) {
|
||||
var propertySet = propertySetRepository.getById(job.getPropertySetId());
|
||||
var validityPeriod = validityPeriodRepository.getById(job.getValidityPeriodId());
|
||||
|
||||
var startDate = propertySet.getStartDate().isBefore(validityPeriod.getStartDate()) ? validityPeriod.getStartDate() : propertySet.getStartDate();
|
||||
|
||||
if(propertySet.getEndDate() == null)
|
||||
return new TimePeriod(startDate, validityPeriod.getEndDate());
|
||||
|
||||
if(validityPeriod.getEndDate() == null)
|
||||
return new TimePeriod(startDate, propertySet.getEndDate());
|
||||
|
||||
var endDate = propertySet.getEndDate().isAfter(validityPeriod.getEndDate()) ? validityPeriod.getEndDate() : propertySet.getEndDate();
|
||||
|
||||
return new TimePeriod(startDate, endDate);
|
||||
}
|
||||
|
||||
;
|
||||
|
||||
private WeightedTotalCosts getWeightedTotalCosts(Map<Integer, List<CalculationJobRouteSection>> sectionsMap) {
|
||||
|
||||
BigDecimal totalPreRunCost = BigDecimal.ZERO;
|
||||
|
|
@ -98,15 +150,14 @@ public class ReportTransformer {
|
|||
return new WeightedTotalCosts(totalPreRunCost, totalMainRunCost, totalPostRunCost, totalCost);
|
||||
}
|
||||
|
||||
private ReportDestinationDTO getDestinationDTO(CalculationJobDestination destination, List<CalculationJobRouteSection> sections, Premise premise) {
|
||||
private ReportDestinationDTO getDestinationDTO(CalculationJobDestination destination, List<CalculationJobRouteSection> sections, Premise premise, Node sourceNode, boolean includeAirfreight) {
|
||||
var destinationNode = nodeRepository.getByDestinationId(destination.getPremiseDestinationId()).orElseThrow();
|
||||
|
||||
|
||||
var dimensionUnit = premise.getHuDisplayedDimensionUnit();
|
||||
var weightUnit = premise.getHuDisplayedWeightUnit();
|
||||
|
||||
ReportDestinationDTO destinationDTO = new ReportDestinationDTO();
|
||||
destinationDTO.setSections(sections.stream().map(this::getSection).toList());
|
||||
destinationDTO.setSections(sections.stream().map(s -> getSection(s, sourceNode, destinationNode, premise)).toList());
|
||||
|
||||
var totalAnnualCost = sections.stream().map(CalculationJobRouteSection::getAnnualCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
destinationDTO.getSections().forEach(s -> {
|
||||
|
|
@ -132,9 +183,11 @@ public class ReportTransformer {
|
|||
destinationDTO.setLayer(destination.getLayerCount());
|
||||
|
||||
destinationDTO.setOverseaShare(premise.getOverseaShare().doubleValue());
|
||||
destinationDTO.setSafetyStock(destination.getSafetyStock().doubleValue());
|
||||
destinationDTO.setSafetyStock(destination.getSafetyStockInDays().intValue());
|
||||
destinationDTO.setTransportTime(destination.getTotalTransitTime().doubleValue());
|
||||
destinationDTO.setAirFreightShare(destination.getAirFreightShare().doubleValue());
|
||||
|
||||
if (includeAirfreight)
|
||||
destinationDTO.setAirFreightShare(destination.getAirFreightShare().doubleValue());
|
||||
|
||||
CalculationJobRouteSection mainRun = sections.stream().filter(CalculationJobRouteSection::getMainRun).findFirst().orElse(null);
|
||||
|
||||
|
|
@ -151,13 +204,20 @@ public class ReportTransformer {
|
|||
return destinationDTO;
|
||||
}
|
||||
|
||||
private ReportSectionDTO getSection(CalculationJobRouteSection section) {
|
||||
private ReportSectionDTO getSection(CalculationJobRouteSection section, Node sourceNode, Node destinationNode, Premise premise) {
|
||||
ReportSectionDTO sectionDTO = new ReportSectionDTO();
|
||||
|
||||
sectionDTO.setId(section.getId());
|
||||
sectionDTO.setTransportType(section.getTransportType());
|
||||
sectionDTO.setFromNode(nodeTransformer.toNodeDTO(routeNodeRepository.getFromNodeBySectionId(section.getPremiseRouteSectionId()).orElseThrow()));
|
||||
sectionDTO.setToNode(nodeTransformer.toNodeDTO(routeNodeRepository.getToNodeBySectionId(section.getPremiseRouteSectionId()).orElseThrow()));
|
||||
|
||||
if (!section.getRateType().equals(RateType.D2D)) {
|
||||
sectionDTO.setFromNode(nodeTransformer.toNodeDTO(routeNodeRepository.getFromNodeBySectionId(section.getPremiseRouteSectionId()).orElseThrow()));
|
||||
sectionDTO.setToNode(nodeTransformer.toNodeDTO(routeNodeRepository.getToNodeBySectionId(section.getPremiseRouteSectionId()).orElseThrow()));
|
||||
} else {
|
||||
sectionDTO.setFromNode(nodeTransformer.toNodeDTO(sourceNode));
|
||||
sectionDTO.setToNode(nodeTransformer.toNodeDTO(destinationNode));
|
||||
}
|
||||
|
||||
sectionDTO.setRateType(section.getRateType());
|
||||
|
||||
|
||||
|
|
@ -170,13 +230,12 @@ public class ReportTransformer {
|
|||
sectionDTO.setDuration(duration);
|
||||
|
||||
var cost = new ReportEntryDTO();
|
||||
cost.setTotal(section.getAnnualCost());
|
||||
cost.setTotal(section.getRate());
|
||||
sectionDTO.setCost(cost);
|
||||
|
||||
return sectionDTO;
|
||||
}
|
||||
|
||||
|
||||
private Map<String, ReportEntryDTO> getRisk(CalculationJob job, List<CalculationJobDestination> destination, WeightedTotalCosts weightedTotalCost, boolean includeAirfreight) {
|
||||
Map<String, ReportEntryDTO> risk = new HashMap<>();
|
||||
|
||||
|
|
@ -232,8 +291,8 @@ public class ReportTransformer {
|
|||
|
||||
var totalValue = annualAmount.equals(BigDecimal.ZERO) ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getTotalCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
|
||||
|
||||
if(includeAirfreight) {
|
||||
totalValue =totalValue.add(airfreightValue);
|
||||
if (includeAirfreight) {
|
||||
totalValue = totalValue.add(airfreightValue);
|
||||
}
|
||||
|
||||
ReportEntryDTO total = new ReportEntryDTO();
|
||||
|
|
@ -306,6 +365,8 @@ public class ReportTransformer {
|
|||
return cost;
|
||||
}
|
||||
|
||||
private record TimePeriod(LocalDateTime startDate, LocalDateTime endDate) {
|
||||
}
|
||||
|
||||
private record WeightedTotalCosts(BigDecimal totalPreRunCost, BigDecimal totalMainRunCost,
|
||||
BigDecimal totalPostRunCost, BigDecimal totalCost) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
package de.avatic.lcc.util.exception.internalerror;
|
||||
|
||||
import de.avatic.lcc.util.exception.base.InternalErrorException;
|
||||
|
||||
public class PremiseValidationError extends InternalErrorException {
|
||||
|
||||
public PremiseValidationError(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -466,6 +466,7 @@ CREATE TABLE IF NOT EXISTS calculation_job
|
|||
property_set_id INT NOT NULL,
|
||||
job_state CHAR(10) NOT NULL CHECK (job_state IN
|
||||
('CREATED', 'SCHEDULED', 'VALID', 'INVALID', 'EXCEPTION')),
|
||||
error_id INT DEFAULT NULL,
|
||||
user_id INT NOT NULL,
|
||||
FOREIGN KEY (premise_id) REFERENCES premise (id),
|
||||
FOREIGN KEY (validity_period_id) REFERENCES validity_period (id),
|
||||
|
|
@ -539,7 +540,7 @@ CREATE TABLE IF NOT EXISTS calculation_job_destination
|
|||
CREATE TABLE IF NOT EXISTS calculation_job_route_section
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
premise_route_section_id INT NOT NULL,
|
||||
premise_route_section_id INT,
|
||||
calculation_job_destination_id INT NOT NULL,
|
||||
transport_type CHAR(16) CHECK (transport_type IN
|
||||
('RAIL', 'SEA', 'ROAD', 'POST_RUN', 'MATRIX', 'D2D')),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue