FRONTEND/BACKEND: Enriching informations in report. Added route widget to destinations
This commit is contained in:
parent
ca3c15ecd2
commit
abed6b82e5
11 changed files with 888 additions and 169 deletions
97
src/frontend/src/backend.js
Normal file
97
src/frontend/src/backend.js
Normal 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;
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
141
src/frontend/src/components/UI/CollapsibleBox.vue
Normal file
141
src/frontend/src/components/UI/CollapsibleBox.vue
Normal 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>
|
||||
|
|
@ -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) }} €</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>
|
||||
|
|
@ -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 [€]</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>
|
||||
|
|
@ -55,6 +55,7 @@ export default {
|
|||
}
|
||||
},
|
||||
created() {
|
||||
//todo reset the store instead.
|
||||
this.selectedMaterialId = this.reportSearchStore.getMaterial?.id;
|
||||
},
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
@ -59,7 +75,7 @@ export default {
|
|||
}
|
||||
},
|
||||
created() {
|
||||
if(!this.hasReport)
|
||||
if (!this.hasReport)
|
||||
this.showModal = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,135 +1,35 @@
|
|||
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() {
|
||||
return {
|
||||
reports: [],
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
|
||||
},
|
||||
actions: {
|
||||
async fetchReports(materialId, supplierIds) {
|
||||
if (supplierIds == null || materialId == null) return;
|
||||
|
||||
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.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);
|
||||
state() {
|
||||
return {
|
||||
reports: [],
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
getters: {},
|
||||
actions: {
|
||||
async fetchReports(materialId, supplierIds) {
|
||||
if (supplierIds == null || materialId == null) return;
|
||||
|
||||
const request = {url: url, params: params};
|
||||
logger.info("Request:", request);
|
||||
this.loading = true;
|
||||
this.reports = [];
|
||||
|
||||
const response = await fetch(url, params
|
||||
).catch(e => {
|
||||
const error = {
|
||||
code: 'Network error.',
|
||||
message: "Please check your internet connection.",
|
||||
trace: null
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
params.append('material', materialId);
|
||||
params.append('sources', supplierIds);
|
||||
|
||||
logger.error(error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(error, {store: this, request: request});
|
||||
const url = `${config.backendUrl}/reports/view/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||
|
||||
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;
|
||||
this.reports = await performRequest(this,'GET', url, null).catch(e => {
|
||||
this.loading = false;
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
logger.info("Response:", data);
|
||||
return data;
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
entity.setTransportType(TransportType.valueOf(rs.getString("transport_type")));
|
||||
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"));
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
@ -79,9 +86,9 @@ public class ReportTransformer {
|
|||
|
||||
if (section.getPreRun())
|
||||
totalPreRunCost = totalPreRunCost.add(section.getAnnualCost());
|
||||
if(section.getMainRun())
|
||||
if (section.getMainRun())
|
||||
totalMainRunCost = totalMainRunCost.add(section.getAnnualCost());
|
||||
if(section.getPostRun())
|
||||
if (section.getPostRun())
|
||||
totalPostRunCost = totalPostRunCost.add(section.getAnnualCost());
|
||||
|
||||
totalCost = totalCost.add(section.getAnnualCost());
|
||||
|
|
@ -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 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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue