FRONTEND/BACKEND: Enriching informations in report. Added route widget to destinations

This commit is contained in:
Jan 2025-09-09 19:10:05 +02:00
parent ca3c15ecd2
commit abed6b82e5
11 changed files with 888 additions and 169 deletions

View file

@ -0,0 +1,97 @@
import logger from "@/logger.js";
import {useErrorStore} from "@/store/error.js";
const performRequest = async (requestingStore, 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: requestingStore, 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: requestingStore, 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: requestingStore, 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: requestingStore, 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: requestingStore, request: request});
throw new Error('Internal backend error');
}
}
logger.info("Response:", data);
return data;
}
export default performRequest;

View file

@ -1,5 +1,5 @@
<template>
<div class="box">
<div class="box" :class="{'box-shadowed': variant === 'shadow', 'box-bordered': variant === 'border', 'stretch-content': stretchContent}">
<slot></slot>
</div>
</template>
@ -7,20 +7,50 @@
<script>
export default {
name: "Box"
name: "Box",
props: {
variant: {
type: String,
default: 'shadow',
validator: (value) => ['shadow', 'border'].includes(value)
},
stretchContent: {
type: Boolean,
default: false
}
}
}
</script>
<style scoped>
.box-shadowed {
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
}
.box-bordered:hover {
background-color: rgba(107, 134, 156, 0.02);
}
.box-bordered {
border: 0.1rem solid #E3EDFF;
}
.box {
display: flex;
background: white;
border-radius: 0.8rem;
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
position: relative;
padding: 1.5rem;
}
.stretch-content {
flex-direction: column;
width: 100%;
}
/* Fade transition for Vue.js */
.fade-enter-active,
.fade-leave-active {

View file

@ -0,0 +1,141 @@
<template>
<box :variant="variant" @click="(isCollapsable && isCollapsed) ? toggleCollapse() : null"
:class="{ 'collapsible': isCollapsable && isCollapsed }" :stretch-content="stretchContent">
<div>
<div
class="box-header"
:class="{ 'collapsible': isCollapsable, 'box-header--size-l': size === 'l', 'box-header--size-m': size === 'm' }"
>
<button
v-if="isCollapsable"
class="collapse-button"
@click.stop="isCollapsable ? toggleCollapse() : null"
>
<PhCaretDown :size="12" :class="{ 'rotated': isCollapsed }"/>
</button>
<span @click.stop="isCollapsable ? toggleCollapse() : null">{{ title }}</span>
</div>
<div class="box-content" :class="{ 'collapsed': isCollapsed }">
<slot></slot>
</div>
</div>
</box>
</template>
<script>
import Box from "@/components/UI/Box.vue";
import {PhCaretDown} from "@phosphor-icons/vue";
export default {
name: "CollapsibleBox",
components: {PhCaretDown, Box},
emits: ['collapsed'],
props: {
title: {
type: String,
required: true
},
size: {
type: String,
required: false,
default: 'l',
validator: (value) => ['l', 'm'].includes(value)
},
variant: {
type: String,
required: false,
default: 'shadow',
validator: (value) => ['shadow', 'border'].includes(value)
},
isCollapsable: {
type: Boolean,
required: false,
default: true
},
initiallyCollapsed: {
type: Boolean,
required: false,
default: false
},
stretchContent: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
isCollapsed: this.initiallyCollapsed
};
},
methods: {
toggleCollapse() {
this.isCollapsed = !this.isCollapsed;
this.$emit('collapsed', this.isCollapsed);
}
}
};
</script>
<style scoped>
.box-header {
font-weight: 500;
display: flex;
justify-content: flex-start;
gap: 0.8rem;
align-items: center;
color: #001D33;
}
.box-header--size-l {
font-size: 1.6rem;
}
.box-header--size-m {
font-size: 1.4rem;
}
.collapsible {
cursor: pointer;
user-select: none;
}
.collapse-button {
background: none;
border: none;
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s ease;
}
.collapse-button svg {
transition: transform 0.2s ease;
}
.collapse-button svg.rotated {
transform: rotate(-90deg);
}
.box-content {
transition: all 0.3s ease;
max-height: 1000px; /* Adjust based on your content */
opacity: 1;
width: 100%; /* Füge dies hinzu */
flex: 1; /* Füge dies hinzu */
}
.box-content.collapsed {
max-height: 0;
opacity: 0;
margin-top: 0;
margin-bottom: 0;
padding-top: 0;
padding-bottom: 0;
}
</style>

View file

@ -1,15 +1,187 @@
<script lang="ts">
import {defineComponent} from 'vue'
export default defineComponent({
name: "ReportRoute"
})
</script>
<template>
<div class="route-container">
<div class="route">
<div class="route-section" v-for="section in sections" :key="section.id">
<div class="route-section-line"></div>
<div class="route-node">
<div class="route-node-dot"></div>
<div class="route-node-name">{{ section.from_node.external_mapping_id ?? section.from_node.name }}</div>
</div>
<div class="route-section-info">
<div class="route-section-info-header">
<ph-boat size="24" weight="fill" v-if="section.transport_type === 'SEA'"></ph-boat>
<ph-train size="24" weight="fill" v-if="section.transport_type === 'RAIL'"></ph-train>
<ph-truck size="24" weight="fill"
v-if="section.transport_type === 'ROAD' || section.transport_type === 'POST_RUN'"></ph-truck>
<div>
{{ section.from_node.external_mapping_id ?? section.from_node.name }} >
{{ section.to_node.external_mapping_id ?? section.to_node.name }}
</div>
</div>
<div class="route-section-info-text">
<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">Transport rate</div><div class="route-section-info-text-data-cell">{{ section.cost.total.toFixed(2) }} &euro;</div>
</div>
</div>
</div>
<div class="route-section">
<div class="route-node">
<div class="route-node-dot"></div>
<div class="route-node-name">{{ destination.external_mapping_id ?? destination.name }}</div>
</div>
</div>
</div>
</div>
</template>
<script>
import {PhBoat, PhTrain, PhTruck, PhTruckTrailer} from "@phosphor-icons/vue";
export default {
name: 'ReportRoute',
components: {PhTruck, PhTruckTrailer, PhTrain, PhBoat},
props: {
sections: {
type: Array,
required: true,
},
destination: {
type: Object,
required: true,
}
},
data() {
return {}
}
}
</script>
<style scoped>
.route-container {
padding: 20px;
}
.route {
position: relative;
max-width: 200px;
}
.route-section {
position: relative;
display: flex;
height: 3.8rem;
}
.route-section:last-child {
position: relative;
display: flex;
height: 0;
}
.route-section-line {
position: absolute;
top: 1rem;
left: 0;
width: 0.4rem;
height: max(100%, 4rem);
background-color: #6B869C;
}
.route-section-info {
position: absolute;
left: 8rem;
display: flex;
flex-direction: column;
justify-content: center;
gap: 1.2rem;
z-index: 5000;
background-color: #fff;
border-radius: 0.8rem;
border: 0.1rem solid #E3EDFF;
visibility: hidden;
opacity: 0;
transform: translateY(-20px);
padding: 1.6rem 2.4rem;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
min-width: 25rem;
}
.route-section-info-text {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.6rem 0.8rem;
font-size: 1.2rem;
width: 100%;
color: #6B869C;
padding-left: 0.4rem;
}
.route-section-info-text-header-cell {
text-wrap: nowrap;
}
.route-section-info-text-data-cell {
text-align: right;
text-wrap: nowrap;
}
.route-section-info-header {
display: flex;
align-items: center;
gap: 0.8rem;
font-size: 1.6rem;
font-weight: 500;
text-transform: uppercase;
text-wrap: nowrap;
color: #6B869C;
}
.route-section:hover .route-section-info {
visibility: visible;
opacity: 1;
transform: translateY(0);
transition: opacity 0.5s ease, transform 0.5s ease;
}
.route-node {
display: flex;
gap: 0.8rem;
top: 0;
left: -0.4rem;
align-items: center;
position: absolute;
font-size: 1.2rem;
font-weight: 300;
color: #6B869C;
text-transform: uppercase;
}
.route-section:not(:last-child):hover .route-node-dot {
background-color: #5AF0B4;
transition: all 0.4s;
}
.route-section:not(:last-child):hover .route-section-line {
background-color: #5AF0B4;
transition: all 0.4s;
}
.route-section:not(:last-child):hover + .route-section .route-node-dot {
background-color: #5AF0B4;
transition: all 0.4s;
}
.route-node-dot {
width: 1.2rem;
height: 1.2rem;
border-radius: 50%;
background-color: #6B869C;
}
</style>

View file

@ -1,14 +1,298 @@
<template>
<div class="report-container">
<div class="report-header">{{ report.supplier.name }}</div>
<div class="report-chart">
<report-chart></report-chart>
</div>
<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">
<div>MEK B</div>
<div class="report-content-data-cell">{{ report.risk.mek_b.total.toFixed(2) }}</div>
</div>
<div class="report-content-row">
<div>Opportunity scenario</div>
<div class="report-content-data-cell">{{ report.risk.opportunity_scenario.total.toFixed(2) }}</div>
</div>
<div class="report-content-row">
<div>Risk scenario</div>
<div class="report-content-data-cell">{{ report.risk.risk_scenario.total.toFixed(2) }}</div>
</div>
</div>
</collapsible-box>
<collapsible-box :is-collapsable="false" variant="border" title="Weighted cost breakdown" size="m"
:stretch-content="true">
<div class="report-content-container--3-col">
<div class="report-content-row">
<div></div>
<div class="report-content-data-header-cell">total [&euro;]</div>
<div class="report-content-data-header-cell">share [%]</div>
</div>
<div class="report-content-row">
<div>MEK A</div>
<div class="report-content-data-cell">{{ report.costs.mek_a.total.toFixed(2) }}</div>
<div class="report-content-data-cell">{{ (report.costs.mek_a.percentage * 100).toFixed(2) }}</div>
</div>
<div class="report-content-row">
<div>FCA fee</div>
<div class="report-content-data-cell">{{ report.costs.fca_fees.total.toFixed(2) }}</div>
<div class="report-content-data-cell">{{ (report.costs.fca_fees.percentage * 100).toFixed(2) }}</div>
</div>
<div class="report-content-row">
<div>Pre carriage</div>
<div class="report-content-data-cell">{{ report.costs.pre_run.total.toFixed(2) }}</div>
<div class="report-content-data-cell">{{ (report.costs.pre_run.percentage * 100).toFixed(2) }}</div>
</div>
<div class="report-content-row">
<div>Main run</div>
<div class="report-content-data-cell">{{ report.costs.main_run.total.toFixed(2) }}</div>
<div class="report-content-data-cell">{{ (report.costs.main_run.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>
<div class="report-content-data-cell">{{ (report.costs.post_run.percentage * 100).toFixed(2) }}</div>
</div>
<div class="report-content-row">
<div>Custom duty</div>
<div class="report-content-data-cell">{{ report.costs.custom.total.toFixed(2) }}</div>
<div class="report-content-data-cell">{{ (report.costs.custom.percentage * 100).toFixed(2) }}</div>
</div>
<div class="report-content-row">
<div>Repackaging</div>
<div class="report-content-data-cell">{{ report.costs.repacking.total.toFixed(2) }}</div>
<div class="report-content-data-cell">{{ (report.costs.repacking.percentage * 100).toFixed(2) }}</div>
</div>
<div class="report-content-row">
<div>Handling</div>
<div class="report-content-data-cell">{{ report.costs.handling.total.toFixed(2) }}</div>
<div class="report-content-data-cell">{{ (report.costs.handling.percentage * 100).toFixed(2) }}</div>
</div>
<div class="report-content-row">
<div>Space cost</div>
<div class="report-content-data-cell">{{ report.costs.storage.total.toFixed(2) }}</div>
<div class="report-content-data-cell">{{ (report.costs.storage.percentage * 100).toFixed(2) }}</div>
</div>
<div class="report-content-row">
<div>Capital cost</div>
<div class="report-content-data-cell">{{ report.costs.capital.total.toFixed(2) }}</div>
<div class="report-content-data-cell">{{ (report.costs.capital.percentage * 100).toFixed(2) }}</div>
</div>
<div class="report-content-row">
<div>Disposal cost</div>
<div class="report-content-data-cell">{{ report.costs.disposal.total.toFixed(2) }}</div>
<div class="report-content-data-cell">{{ (report.costs.disposal.percentage * 100).toFixed(2) }}</div>
</div>
</div>
</collapsible-box>
<collapsible-box class="report-content-container" variant="border" :title="premise.destination.name"
:key="premise.id" :stretch-content="true" v-for="premise in report.premises">
<div>
<report-route :sections="premise.sections" :destination="premise.destination" ></report-route>
<div class="report-sub-header">Premisses</div>
<div class="report-content-container--2-col">
<div class="report-content-row">
<div>Annual Quantity</div>
<div class="report-content-data-cell">{{ premise.annual_quantity }}</div>
</div>
<div class="report-content-row">
<div>HS code</div>
<div class="report-content-data-cell">{{ premise.hs_code }}</div>
</div>
<div class="report-content-row">
<div>Tariff rate</div>
<div class="report-content-data-cell">{{ (premise.tariff_rate * 100).toFixed(2) }}%</div>
</div>
<div class="report-content-row">
<div>Oversea share</div>
<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>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 class="report-content-data-cell">{{ premise.transport_time }}</div>
</div>
</div>
<div class="report-sub-header">Packaging</div>
<div class="report-content-container--2-col">
<div class="report-content-row">
<div>HU dimensions [{{ premise.dimension_unit }}]</div>
<div class="report-content-data-cell">{{ toFixedDimension(premise.length, premise.dimension_unit) }} x {{ toFixedDimension(premise.width, premise.dimension_unit) }} x {{ toFixedDimension(premise.height, premise.dimension_unit) }} </div>
</div>
<div class="report-content-row">
<div>HU weight [{{ premise.weight_unit }}]</div>
<div class="report-content-data-cell">{{ toFixedWeight(premise.weight, premise.weight_unit) }} </div>
</div>
<div class="report-content-row">
<div>HU unit count</div>
<div class="report-content-data-cell">{{ premise.hu_unit_count }} </div>
</div>
<div class="report-content-row">
<div>Mixable HU</div>
<div class="report-content-data-cell">{{ premise.mixed ? 'yes' : 'no' }} </div>
</div>
<div class="report-content-row">
<div>Stacking factor</div>
<div class="report-content-data-cell">{{ premise.layer }} </div>
</div>
<div class="report-content-row">
<div>Container unit count</div>
<div class="report-content-data-cell">{{ premise.unit_count * premise.hu_unit_count }} </div>
</div>
<div class="report-content-row">
<div>Container type</div>
<div class="report-content-data-cell">{{ premise.container_type }} </div>
</div>
<div class="report-content-row">
<div>Limiting factor</div>
<div class="report-content-data-cell">{{ premise.weight_exceeded ? 'weight' : 'volume' }} </div>
</div>
</div>
</div>
</collapsible-box>
</div>
</template>
<script>
import ReportChart from "@/components/UI/ReportChart.vue";
import Box from "@/components/UI/Box.vue";
import CollapsibleBox from "@/components/UI/CollapsibleBox.vue";
import ReportRoute from "@/components/UI/ReportRoute.vue";
export default {
name: "SingleReport"
name: "Report",
components: {ReportRoute, CollapsibleBox, Box, ReportChart},
computed: {},
props: {
report: {
type: Object,
required: true
}
},
methods: {
toFixedDimension(value, unit) {
if(unit === 'm') {
return value.toFixed(4);
} else if(unit === 'cm') {
return value.toFixed(2);
} else if(unit === 'mm') {
return value.toFixed();
}
},
toFixedWeight(value, unit) {
if (unit === 'kg') {
return value.toFixed(2);
} else if (unit === 'g') {
return value.toFixed(4);
} else if (unit === 't') {
return value.toFixed();
}
}
}
}
</script>
<template>
</template>
<style scoped>
.report-container {
display: flex;
flex-direction: column;
gap: 1rem;
}
.report-content-data-cell {
text-align: right;
}
.report-content-data-header-cell {
text-align: right;
color: #001D33;
}
.report-content-container--2-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin: 1.6rem 0;
font-size: 1.2rem;
width: 100%;
}
.report-content-container--3-col {
display: grid;
grid-template-columns: 2fr 1fr 1fr;
gap: 1rem;
margin-top: 1.6rem;
font-size: 1.2rem;
}
.report-content-row {
display: contents;
color: #6B869C;
}
.report-header {
font-size: 1.6rem;
font-weight: 500;
color: #001D33;
}
.report-sub-header {
font-size: 1.4rem;
font-weight: 500;
color: #001D33;
margin-top: 1.6rem;
}
</style>

View file

@ -55,6 +55,7 @@ export default {
}
},
created() {
//todo reset the store instead.
this.selectedMaterialId = this.reportSearchStore.getMaterial?.id;
},
computed: {

View file

@ -7,9 +7,16 @@
</div>
</div>
<div v-if="hasReport">
<div v-if="loading" class="report-spinner-container">
<div class="report-spinner">
<spinner></spinner>
</div>
</div>
<div v-else-if="hasReport">
<box>
<report v-for="report in reports" :key="report.id" :report="report"></report>
</box>
</div>
<div v-else class="empty-container">
@ -33,10 +40,13 @@ import SelectForReport from "@/components/layout/report/SelectForReport.vue";
import {mapStores} from "pinia";
import {useReportsStore} from "@/store/reports.js";
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";
export default {
name: "Reporting",
components: {Box, SelectForReport, BasicButton, Modal},
components: {Report, ReportChart, Spinner, Box, SelectForReport, BasicButton, Modal},
data() {
return {
showModal: false,
@ -45,7 +55,13 @@ export default {
computed: {
...mapStores(useReportsStore),
hasReport() {
return false;
return this.reportsStore.reports?.length > 0;
},
reports() {
return this.reportsStore.reports
},
loading() {
return this.reportsStore.loading;
}
},
methods: {
@ -84,6 +100,7 @@ export default {
gap: 1.6rem;
}
.empty-container {
display: flex;
justify-content: center;
@ -92,4 +109,20 @@ export default {
font-weight: 500;
}
.report-spinner-container {
display: flex;
align-items: center;
justify-content: center;
flex: 1 1 30rem
}
.report-spinner {
font-size: 1.6rem;
width: 24rem;
height: 12rem;
display: flex;
justify-content: center;
align-items: center;
}
</style>

View file

@ -1,9 +1,7 @@
import {defineStore} from 'pinia'
import {config} from '@/config'
import {useErrorStore} from "@/store/error.js";
import {useStageStore} from "@/store/stage.js";
import {usePropertySetsStore} from "@/store/propertySets.js";
import logger from "@/logger.js";
import performRequest from '@/backend.js'
export const useReportsStore = defineStore('reports', {
state() {
@ -12,9 +10,7 @@ export const useReportsStore = defineStore('reports', {
loading: false,
}
},
getters: {
},
getters: {},
actions: {
async fetchReports(materialId, supplierIds) {
if (supplierIds == null || materialId == null) return;
@ -22,114 +18,18 @@ export const useReportsStore = defineStore('reports', {
this.loading = true;
this.reports = [];
console.log("fetchreports")
const params = new URLSearchParams();
params.append('material', materialId);
params.append('sources', supplierIds);
const url = `${config.backendUrl}/reports/view/${params.size === 0 ? '' : '?'}${params.toString()}`;
this.reports = await this.performRequest('GET', url, null).catch(e => {
this.reports = await performRequest(this,'GET', url, null).catch(e => {
this.loading = false;
});
this.loading = false;
},
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;
}
},
});
);

View file

@ -9,7 +9,7 @@ public class ReportSectionDTO {
private Integer id;
@JsonProperty("route_type")
@JsonProperty("transport_type")
private TransportType transportType;
@JsonProperty("rate_type")
@ -18,12 +18,18 @@ public class ReportSectionDTO {
@JsonProperty("from_node")
private NodeDTO fromNode;
@JsonProperty("to_node")
private NodeDTO toNode;
@JsonProperty("cost")
private ReportEntryDTO cost;
@JsonProperty("duration")
private ReportEntryDTO duration;
@JsonProperty("distance")
private ReportEntryDTO distance;
public Integer getId() {
return id;
}
@ -71,4 +77,20 @@ public class ReportSectionDTO {
public void setDuration(ReportEntryDTO duration) {
this.duration = duration;
}
public ReportEntryDTO getDistance() {
return distance;
}
public void setDistance(ReportEntryDTO distance) {
this.distance = distance;
}
public NodeDTO getToNode() {
return toNode;
}
public void setToNode(NodeDTO toNode) {
this.toNode = toNode;
}
}

View file

@ -1,8 +1,8 @@
package de.avatic.lcc.repositories.calculation;
import de.avatic.lcc.dto.generic.RateType;
import de.avatic.lcc.dto.generic.TransportType;
import de.avatic.lcc.model.calculations.CalculationJobRouteSection;
import de.avatic.lcc.model.premises.route.Destination;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
@ -117,7 +117,20 @@ public class CalculationJobRouteSectionRepository {
entity.setCalculationJobDestinationId(rs.getInt("calculation_job_destination_id"));
// Rule and price type flags
String transportType = rs.getString("transport_type");
if ("MATRIX".equals(transportType)) {
entity.setTransportType(TransportType.ROAD);
entity.setRateType(RateType.MATRIX);
} else if ("D2D".equals(transportType)) {
entity.setTransportType(TransportType.ROAD);
entity.setRateType(RateType.D2D);
} else {
entity.setRateType(RateType.CONTAINER);
entity.setTransportType(TransportType.valueOf(rs.getString("transport_type")));
}
entity.setUnmixedPrice(rs.getBoolean("is_unmixed_price"));
entity.setCbmPrice(rs.getBoolean("is_cbm_price"));
entity.setWeightPrice(rs.getBoolean("is_weight_price"));

View file

@ -9,11 +9,13 @@ 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.premises.Premise;
import de.avatic.lcc.model.properties.SystemPropertyMappingId;
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.service.transformer.generic.NodeTransformer;
import org.springframework.stereotype.Service;
@ -32,20 +34,25 @@ public class ReportTransformer {
private final NodeRepository nodeRepository;
private final NodeTransformer nodeTransformer;
private final RouteNodeRepository routeNodeRepository;
private final PropertyRepository propertyRepository;
public ReportTransformer(CalculationJobDestinationRepository calculationJobDestinationRepository, CalculationJobRouteSectionRepository calculationJobRouteSectionRepository, PremiseRepository premiseRepository, NodeRepository nodeRepository, NodeTransformer nodeTransformer, RouteNodeRepository routeNodeRepository) {
public ReportTransformer(CalculationJobDestinationRepository calculationJobDestinationRepository, CalculationJobRouteSectionRepository calculationJobRouteSectionRepository, PremiseRepository premiseRepository, NodeRepository nodeRepository, NodeTransformer nodeTransformer, RouteNodeRepository routeNodeRepository, PropertyRepository propertyRepository) {
this.calculationJobDestinationRepository = calculationJobDestinationRepository;
this.calculationJobRouteSectionRepository = calculationJobRouteSectionRepository;
this.premiseRepository = premiseRepository;
this.nodeRepository = nodeRepository;
this.nodeTransformer = nodeTransformer;
this.routeNodeRepository = routeNodeRepository;
this.propertyRepository = propertyRepository;
}
public ReportDTO toReportDTO(CalculationJob job) {
ReportDTO reportDTO = new ReportDTO();
var reportingProperty = propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.REPORTING).orElseThrow();
boolean includeAirfreight = reportingProperty.getCurrentValue().equals("MEK_C");
List<CalculationJobDestination> destinations = calculationJobDestinationRepository.getDestinationsByJobId(job.getId());
Map<Integer, List<CalculationJobRouteSection>> sections = calculationJobRouteSectionRepository.getRouteSectionsByDestinationIds(destinations.stream().map(CalculationJobDestination::getId).toList());
@ -53,8 +60,8 @@ public class ReportTransformer {
Premise premise = premiseRepository.getPremiseById(job.getPremiseId()).orElseThrow();
reportDTO.setCost(getCostMap(job, destinations, weightedTotalCost));
reportDTO.setRisk(getRisk(job, destinations, weightedTotalCost));
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());
if (!reportDTO.getDestinations().isEmpty()) {
@ -150,8 +157,14 @@ public class ReportTransformer {
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()));
sectionDTO.setRateType(section.getRateType());
var distance = new ReportEntryDTO();
distance.setTotal(section.getDistance());
sectionDTO.setDistance(distance);
var duration = new ReportEntryDTO();
duration.setTotal(section.getTransitTime());
sectionDTO.setDuration(duration);
@ -164,43 +177,45 @@ public class ReportTransformer {
}
private Map<String, ReportEntryDTO> getRisk(CalculationJob job, List<CalculationJobDestination> destination, WeightedTotalCosts weightedTotalCost) {
private Map<String, ReportEntryDTO> getRisk(CalculationJob job, List<CalculationJobDestination> destination, WeightedTotalCosts weightedTotalCost, boolean includeAirfreight) {
Map<String, ReportEntryDTO> risk = new HashMap<>();
var annualAmount = destination.stream().map(CalculationJobDestination::getAnnualAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
var airfreightValue = destination.stream().map(CalculationJobDestination::getAnnualAirFreightCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
var worstValue = destination.stream().map(CalculationJobDestination::getTotalRiskCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
var bestValue = destination.stream().map(CalculationJobDestination::getTotalChanceCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
var totalValue = annualAmount.equals(BigDecimal.ZERO) ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getTotalCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
//var totalValue = weightedTotalCost.totalCost.divide(annualAmount, 2, RoundingMode.HALF_UP);
ReportEntryDTO airfreight = new ReportEntryDTO();
airfreight.setTotal(airfreightValue);
airfreight.setPercentage(airfreightValue.divide(totalValue, 4, RoundingMode.HALF_UP));
risk.put("air_freight_cost", airfreight);
totalValue = totalValue.add(airfreightValue.multiply(BigDecimal.valueOf(includeAirfreight ? 1 : 0)));
ReportEntryDTO total = new ReportEntryDTO();
total.setTotal(totalValue);
total.setPercentage(totalValue.divide(totalValue, 4, RoundingMode.HALF_UP));
risk.put("mek_b", total);
ReportEntryDTO worst = new ReportEntryDTO();
worst.setTotal(worstValue);
worst.setPercentage(worstValue.divide(totalValue, 4, RoundingMode.HALF_UP));
risk.put("worst_case_cost", worst);
risk.put("risk_scenario", worst);
ReportEntryDTO best = new ReportEntryDTO();
best.setTotal(bestValue);
best.setPercentage(bestValue.divide(totalValue, 4, RoundingMode.HALF_UP));
risk.put("best_case_cost", best);
risk.put("opportunity_scenario", best);
return risk;
}
private Map<String, ReportEntryDTO> getCostMap(CalculationJob job, List<CalculationJobDestination> destination, WeightedTotalCosts weightedTotalCost) {
private Map<String, ReportEntryDTO> getCostMap(CalculationJob job, List<CalculationJobDestination> destination, WeightedTotalCosts weightedTotalCost, boolean includeAirfreight) {
Map<String, ReportEntryDTO> cost = new HashMap<>();
var annualAmount = destination.stream().map(CalculationJobDestination::getAnnualAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
var airfreightValue = destination.stream().map(CalculationJobDestination::getAnnualAirFreightCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
var materialValue = destination.stream().map(CalculationJobDestination::getMaterialCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(BigDecimal.valueOf(destination.size()), 4, RoundingMode.HALF_UP);
var fcaFeesValues = destination.stream().map(CalculationJobDestination::getFcaCost).reduce(BigDecimal.ZERO, BigDecimal::add);
var repackingValues = annualAmount.equals(BigDecimal.ZERO) ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getAnnualRepackingCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
@ -217,30 +232,41 @@ 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);
}
ReportEntryDTO total = new ReportEntryDTO();
total.setTotal(totalValue);
total.setPercentage(BigDecimal.valueOf(1));
cost.put("total", total);
if (includeAirfreight) {
ReportEntryDTO airfreight = new ReportEntryDTO();
airfreight.setTotal(airfreightValue);
airfreight.setPercentage(airfreightValue.divide(totalValue, 4, RoundingMode.HALF_UP));
cost.put("air_freight_cost", airfreight);
}
ReportEntryDTO preRun = new ReportEntryDTO();
preRun.setTotal(preRunValues);
preRun.setPercentage(preRunValues.divide(totalValue, 4, RoundingMode.HALF_UP));
cost.put("preRun", preRun);
cost.put("pre_run", preRun);
ReportEntryDTO mainRun = new ReportEntryDTO();
mainRun.setTotal(mainRunValues);
mainRun.setPercentage(mainRunValues.divide(totalValue, 4, RoundingMode.HALF_UP));
cost.put("mainRun", mainRun);
cost.put("main_run", mainRun);
ReportEntryDTO postRun = new ReportEntryDTO();
postRun.setTotal(postRunValues);
postRun.setPercentage(postRunValues.divide(totalValue, 4, RoundingMode.HALF_UP));
cost.put("postRun", postRun);
cost.put("post_run", postRun);
ReportEntryDTO material = new ReportEntryDTO();
material.setTotal(materialValue);
material.setPercentage(materialValue.divide(totalValue, 4, RoundingMode.HALF_UP));
cost.put("material", material);
cost.put("mek_a", material);
ReportEntryDTO custom = new ReportEntryDTO();
custom.setTotal(customValues);
@ -250,7 +276,7 @@ public class ReportTransformer {
ReportEntryDTO fcaFees = new ReportEntryDTO();
fcaFees.setTotal(fcaFeesValues);
fcaFees.setPercentage(fcaFeesValues.divide(totalValue, 4, RoundingMode.HALF_UP));
cost.put("fcaFees", fcaFees);
cost.put("fca_fees", fcaFees);
ReportEntryDTO repacking = new ReportEntryDTO();
repacking.setTotal(repackingValues);