Refined distance calculations and enhanced reporting layout:

- **Backend**: Adjusted `getDistance` implementation to retrieve distances via `DistanceApiService`, with fallback to `fast` algorithm. Updated `DistanceService` to utilize API responses for more accurate calculations. Enhanced `ExcelReportingService` handling for main run containers and mixed premises logic.
- **Frontend**: Improved `Report.vue` structure for better readability and modularity. Added logic to conditionally display container details based on the main run's existence.
This commit is contained in:
Jan 2025-11-01 13:27:12 +01:00
parent f7efc9eb81
commit be1ef5091b
14 changed files with 297 additions and 254 deletions

View file

@ -1,7 +1,8 @@
<template>
<div class="report-container">
<div class="box-gap">
<div class="report-header">{{ shorten(report.supplier.name, 35) }}</div></div>
<div class="report-header">{{ shorten(report.supplier.name, 35) }}</div>
</div>
<div class="report-chart">
<report-chart
title=""
@ -14,7 +15,7 @@
</div>
<div class="box-gap">
<collapsible-box :is-collapsable="false" variant="border" title="Overview" size="m" :stretch-content="true" >
<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">
@ -92,7 +93,10 @@
<div class="report-content-row" v-if="((report.costs.air_freight_cost ?? null) !== null)">
<div>Air freight costs</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 class="report-content-data-cell">{{
(report.costs.air_freight_cost.percentage * 100).toFixed(2)
}}
</div>
</div>
@ -143,7 +147,8 @@
<collapsible-box class="report-content-container" variant="border" :title="premise.destination.name"
:stretch-content="true" :initially-collapsed="true">
<div>
<report-route :sections="premise.sections" :destination="premise.destination" :route-section-scale="routeSectionScale[idx]" ></report-route>
<report-route :sections="premise.sections" :destination="premise.destination"
:route-section-scale="routeSectionScale[idx]"></report-route>
<div class="report-sub-header">Premisses</div>
@ -192,42 +197,45 @@
<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 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 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 class="report-content-data-cell">{{ premise.hu_unit_count }}</div>
</div>
<div class="report-content-row">
<div>Mixed container</div>
<div class="report-content-data-cell">{{ premise.mixed ? 'Yes' : 'No' }} </div>
<div class="report-content-data-cell">{{ premise.mixed ? 'Yes' : 'No' }}</div>
</div>
<div class="report-content-row">
<div>Stacked layers</div>
<div class="report-content-data-cell">{{ premise.layer }} </div>
<div class="report-content-data-cell">{{ hasMainRun(premise.sections) ? 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 class="report-content-data-cell">{{hasMainRun(premise.sections) ? (premise.unit_count * premise.hu_unit_count) : '-' }}</div>
</div>
<div class="report-content-row">
<div>Container type</div>
<div class="report-content-data-cell">{{ getContainerTypeName(premise.container_type) }} </div>
<div class="report-content-data-cell">{{hasMainRun(premise.sections) ? getContainerTypeName(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 class="report-content-data-cell">{{hasMainRun(premise.sections) ? premise.weight_exceeded ? 'Weight' : 'Volume' : '-'}}</div>
</div>
</div>
@ -265,9 +273,12 @@ export default {
}
},
methods: {
hasMainRun(sections) {
return sections.some(section => section.transport_type === 'SEA' || section.transport_type === 'RAIL');
},
shorten(text, length) {
if(text !== null && text !== undefined && text.length > length) {
return `${text.substring(0, length - 3)}` ;
if (text !== null && text !== undefined && text.length > length) {
return `${text.substring(0, length - 3)}`;
}
return text;
@ -284,11 +295,11 @@ export default {
return ''
},
toFixedDimension(value, unit) {
if(unit === 'm') {
if (unit === 'm') {
return value.toFixed(2);
} else if(unit === 'cm') {
} else if (unit === 'cm') {
return value.toFixed(2);
} else if(unit === 'mm') {
} else if (unit === 'mm') {
return value.toFixed();
}
},

View file

@ -190,7 +190,7 @@ export default {
return this.modalType ? this.componentsData[this.modalType] : null;
},
showProcessingModal() {
return this.premiseEditStore.showProcessingModal;
return this.premiseEditStore.showProcessingModal || this.showCalculationModal;
}
},
created() {
@ -224,7 +224,8 @@ export default {
},
editIds: null,
dataSourceId: null,
processingMessage: "Please wait. Calculating routes ...",
processingMessage: "Please wait. Calculating ...",
showCalculationModal: false
}
},
methods: {
@ -244,6 +245,7 @@ export default {
}
},
async startCalculation() {
this.showCalculationModal = true;
const error = await this.premiseEditStore.startCalculation();
if (error !== null) {
@ -258,6 +260,7 @@ export default {
} else {
this.closeMassEdit()
}
this.showCalculationModal = false;
},
closeMassEdit() {
this.$router.push({name: "calculation-list"});

View file

@ -86,6 +86,13 @@
<h3 class="sub-header">Destinations & routes</h3>
<destination-list-view></destination-list-view>
<modal :z-index="3000" :state="showProcessingModal">
<div class="edit-calculation-spinner-container space-around">
<spinner></spinner>
<span>{{ processingMessage }}</span>
</div>
</modal>
</div>
</div>
</template>
@ -131,6 +138,8 @@ export default {
traceModal: false,
bulkEditQuery: null,
id: null,
processingMessage: "Please wait. Calculating ...",
showCalculationModal: false,
}
},
computed: {
@ -140,15 +149,18 @@ export default {
},
fromMassEdit() {
return this.bulkEditQuery !== null;
}
},
showProcessingModal() {
return this.premiseEditStore.showProcessingModal || this.showCalculationModal;
},
},
methods: {
async startCalculation() {
this.showCalculationModal = true;
const error = await this.premiseEditStore.startCalculation();
if (error !== null) {
this.$refs.toast.addToast({
icon: 'warning',
message: error.message,
@ -159,6 +171,8 @@ export default {
} else {
this.close();
}
this.showCalculationModal = false;
},
close() {
if (this.bulkEditQuery) {
@ -181,17 +195,6 @@ export default {
success = await this.premiseEditStore.savePackaging();
}
// if(success) {
// this.$refs.toast.addToast({
// icon: 'floppy-disk',
// message: `Changes on ${type} saved.`,
//
// variant: 'primary',
// duration: 3000
// })
// }
},
updateMaterial(id, action) {
console.log(id, action);
@ -287,11 +290,18 @@ export default {
text-decoration: underline;
}
.space-around {
margin: 3rem;
}
.edit-calculation-spinner-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1 1 30rem;
gap: 3.6rem;
flex: 1 1 auto;
font-size: 1.6rem;
}
.edit-calculation-spinner {

View file

@ -599,6 +599,8 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
} else {
const id = node.id;
this.processDestinationMassEdit = true;
const toBeUpdated = this.destinations.fromMassEditView ? this.destinations.premise_ids : this.premisses?.filter(p => p.selected).map(p => p.id);
@ -611,6 +613,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
const {data: destinations} = await performRequest(this, 'POST', url, body).catch(e => {
this.loading = false;
this.selectedLoading = false;
this.processDestinationMassEdit = false;
throw e;
});
@ -624,6 +627,8 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
this.destinations.destinations.push(mappedDestination);
}
this.processDestinationMassEdit = false;
return mappedIds;
}
},

View file

@ -39,9 +39,10 @@ export const useReportsStore = defineStore('reports', {
const params = new URLSearchParams();
params.append('material', this.materialId);
params.append('sources', this.supplierIds);
params.append('userSources', this.userSupplierIds);
const url = `${config.backendUrl}/reports/download/${params.size === 0 ? '' : '?'}${params.toString()}`;
const fileName = `report_${this.materialId}_${this.supplierIds.join('_')}.xlsx`;
const fileName = `report_${this.materialId}_${this.supplierIds.join('_')}_u${this.userSupplierIds.join('_')}.xlsx`;
await performDownload(url,fileName);
},
reset() {

View file

@ -71,7 +71,7 @@ public class SecurityConfig {
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/actuator/**").authenticated()
.requestMatchers("/actuator/**").hasRole("SERVICE")
.requestMatchers("/oauth2/token").permitAll()
.requestMatchers("/api/**").authenticated()
.requestMatchers("/api/dev/**").denyAll()

View file

@ -13,6 +13,7 @@ import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.math.BigDecimal;
import java.net.URI;
import java.time.LocalDateTime;
import java.util.Optional;
@ -35,6 +36,7 @@ public class DistanceApiService {
}
public Optional<Distance> getDistance(Node from, Node to) {
if (from == null || to == null) {
logger.warn("Source or destination node is null");
return Optional.empty();
@ -46,7 +48,6 @@ public class DistanceApiService {
return Optional.empty();
}
// Check if distance exists in database and is valid
Optional<Distance> cachedDistance = distanceMatrixRepository.getDistance(from, to);
if (cachedDistance.isPresent()) {
@ -54,12 +55,10 @@ public class DistanceApiService {
return cachedDistance;
}
// Distance not found or stale, fetch from Azure Maps
logger.info("Fetching distance from Azure Maps for nodes {} to {}", from.getId(), to.getId());
logger.debug("Fetching distance from Azure Maps for nodes {} to {}", from.getId(), to.getId());
Optional<Distance> fetchedDistance = fetchDistanceFromAzureMaps(from, to);
if (fetchedDistance.isPresent()) {
// Store in database
distanceMatrixRepository.saveDistance(fetchedDistance.get());
return fetchedDistance;
}
@ -69,18 +68,19 @@ public class DistanceApiService {
private Optional<Distance> fetchDistanceFromAzureMaps(Node from, Node to) {
try {
String url = UriComponentsBuilder.fromHttpUrl(AZURE_MAPS_ROUTE_API)
String url = UriComponentsBuilder.fromUriString(AZURE_MAPS_ROUTE_API)
.queryParam("api-version", "1.0")
.queryParam("subscription-key", subscriptionKey)
.queryParam("query", String.format("%s,%s:%s,%s",
from.getGeoLat(), from.getGeoLng(),
to.getGeoLat(), to.getGeoLng()))
.encode()
.toUriString();
RouteDirectionsResponse response = restTemplate.getForObject(url, RouteDirectionsResponse.class);
if (response != null && response.getRoutes() != null && !response.getRoutes().isEmpty()) {
Integer distanceInMeters = response.getRoutes().get(0).getSummary().getLengthInMeters();
Integer distanceInMeters = response.getRoutes().getFirst().getSummary().getLengthInMeters();
Distance distance = new Distance();
distance.setFromNodeId(from.getId());

View file

@ -6,6 +6,7 @@ import de.avatic.lcc.model.db.nodes.Location;
import de.avatic.lcc.model.db.nodes.Node;
import de.avatic.lcc.repositories.DistanceMatrixRepository;
import de.avatic.lcc.repositories.country.CountryRepository;
import de.avatic.lcc.service.api.DistanceApiService;
import org.springframework.stereotype.Service;
@Service
@ -14,20 +15,21 @@ public class DistanceService {
private static final double EARTH_RADIUS = 6371.0;
private final DistanceMatrixRepository distanceMatrixRepository;
private final CountryRepository countryRepository;
private final DistanceApiService distanceApiService;
public DistanceService(DistanceMatrixRepository distanceMatrixRepository, CountryRepository countryRepository) {
public DistanceService(DistanceMatrixRepository distanceMatrixRepository, CountryRepository countryRepository, DistanceApiService distanceApiService) {
this.distanceMatrixRepository = distanceMatrixRepository;
this.countryRepository = countryRepository;
this.distanceApiService = distanceApiService;
}
public double getDistance(Node src, Node dest, boolean fast) {
if (fast) return getDistanceFast(src, dest);
var distance = distanceMatrixRepository.getDistance(src, dest);
// TODO do a api call if empty!.
return distance.map(value -> value.getDistance().intValue()).orElse(0);
var distance = distanceApiService.getDistance(src, dest);
if (distance.isEmpty()) return getDistanceFast(src, dest);
return distance.map(value -> value.getDistance().intValue()/1000).orElse(0);
}
public double getDistanceFast(Integer srcCountryId, Location src, Integer destCountryId, Location dest ) {

View file

@ -334,7 +334,7 @@ public class RoutingService {
}
finalSection.setRate(matrixRate);
finalSection.setApproxDistance(distanceService.getDistance(container.getSourceNode(), toNode, true));
finalSection.setApproxDistance(distanceService.getDistance(container.getSourceNode(), toNode, false));
rates.add(finalSection);
}
@ -699,7 +699,7 @@ public class RoutingService {
if (matrixRate.isPresent()) {
matrixRateObj.setRate(matrixRate.get());
matrixRateObj.setApproxDistance(distanceService.getDistance(startNode, endNode, true));
matrixRateObj.setApproxDistance(distanceService.getDistance(startNode, endNode, false));
container.getRates().add(matrixRateObj);
return matrixRateObj;
} else {

View file

@ -67,7 +67,7 @@ public class RouteSectionCostCalculationService {
Node fromNode = nodeRepository.getById(premise.getSupplierNodeId()).orElseThrow();
Node toNode = nodeRepository.getById(destination.getDestinationNodeId()).orElseThrow();
double distance = distanceService.getDistance(fromNode, toNode, true);
double distance = distanceService.getDistance(fromNode, toNode, false);
result.setDistance(BigDecimal.valueOf(distance));
// Get rate and transit time

View file

@ -153,7 +153,7 @@ public class PreCalculationCheckService {
var validDaysInt = Integer.parseInt(validDays.get().getCurrentValue());
if(!period.getStartDate().plusDays((long) validDaysInt * renewals).isAfter(LocalDateTime.now()))
if(!period.getStartDate().plusDays((((long) validDaysInt * renewals)+validDaysInt)).isAfter(LocalDateTime.now()))
throw new PremiseValidationError("There are no valid rates for the given date. Please contact your administrator.");
}

View file

@ -1,6 +1,7 @@
package de.avatic.lcc.service.report;
import de.avatic.lcc.dto.generic.NodeDTO;
import de.avatic.lcc.dto.generic.TransportType;
import de.avatic.lcc.dto.report.ReportDTO;
import de.avatic.lcc.dto.report.ReportDestinationDTO;
import de.avatic.lcc.dto.report.ReportEntryDTO;
@ -145,6 +146,9 @@ public class ExcelReportingService {
}
private void flattenDestination(ReportDestinationDTO destination) {
var hasMainRun = destination.getSections().stream().anyMatch(s -> s.getTransportType().equals(TransportType.RAIL) || s.getTransportType().equals(TransportType.SEA));
addData(DESTINATION_NAME, destination.getDestination().getName());
addData(DESTINATION_ADDRESS, destination.getDestination().getAddress());
@ -152,6 +156,7 @@ public class ExcelReportingService {
addData(DESTINATION_HS_CODE, destination.getHsCode());
addData(DESTINATION_TARIFF_RATE, destination.getTariffRate().toString());
addData(DESTINATION_OVERSHARE, destination.getOverseaShare().toString());
if(destination.getAirFreightShare() != null)
addData(DESTINATION_AIR_FREIGHT_SHARE, destination.getAirFreightShare().toString());
addData(DESTINATION_TRANSPORT_TIME, destination.getTransportTime().toString());
@ -164,14 +169,14 @@ public class ExcelReportingService {
addData(DESTINATION_WEIGHT, destination.getWeight().toString());
addData(DESTINATION_WEIGHT_UNIT, destination.getWeightUnit().toString());
addData(DESTINATION_HU_UNIT_COUNT, destination.getHuUnitCount().toString());
addData(DESTINATION_CONTAINER_LAYER, destination.getLayer().toString());
addData(DESTINATION_CONTAINER_LAYER, !hasMainRun ? "-" : destination.getLayer().toString());
addData(DESTINATION_CONTAINER_UNIT_COUNT, destination.getUnitCount().toString());
addData(DESTINATION_CONTAINER_UTILIZATION, destination.getUtilization().toString());
addData(DESTINATION_CONTAINER_TYPE, destination.getType().toString());
addData(DESTINATION_CONTAINER_WEIGHT_EXCEEDED, destination.getWeightExceeded().toString());
addData(DESTINATION_CONTAINER_RATE, destination.getRate().toString());
addData(DESTINATION_MIXED, destination.getMixed().toString());
addData(DESTINATION_CONTAINER_UNIT_COUNT, !hasMainRun ? "-" : destination.getUnitCount().toString());
addData(DESTINATION_CONTAINER_UTILIZATION, !hasMainRun ? "-" : destination.getUtilization().toString());
addData(DESTINATION_CONTAINER_TYPE, !hasMainRun ? "-" : destination.getType().toString());
addData(DESTINATION_CONTAINER_WEIGHT_EXCEEDED, !hasMainRun ? "-" : destination.getWeightExceeded().toString());
addData(DESTINATION_CONTAINER_RATE, !hasMainRun ? "-" : destination.getRate().toString());
addData(DESTINATION_MIXED, !hasMainRun ? "-" : destination.getMixed().toString());
}

View file

@ -2,6 +2,7 @@ package de.avatic.lcc.service.report;
import de.avatic.lcc.dto.generic.NodeDTO;
import de.avatic.lcc.dto.report.ReportDTO;
import de.avatic.lcc.model.db.calculations.CalculationJob;
import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.repositories.calculation.CalculationJobRepository;
import de.avatic.lcc.repositories.rates.ValidityPeriodRepository;
@ -58,9 +59,13 @@ public class ReportingService {
var periodId = tuple.get().periodId();
var setId = tuple.get().propertySetId();
//TODO check user node id and node id for null
var jobs = new ArrayList<>(nodeIds.stream().map(nodeId -> calculationJobRepository.getCalculationJobWithJobStateValid(periodId, setId, nodeId, materialId)).filter(Optional::isPresent).map(Optional::get).toList());
var jobs = new ArrayList<CalculationJob>();
if(!nodeIds.isEmpty())
jobs.addAll(nodeIds.stream().map(nodeId -> calculationJobRepository.getCalculationJobWithJobStateValid(periodId, setId, nodeId, materialId)).filter(Optional::isPresent).map(Optional::get).toList());
if(!userNodeIds.isEmpty())
jobs.addAll(userNodeIds.stream().map(nodeId -> calculationJobRepository.getCalculationJobWithJobStateValidUserNodeId(periodId, setId,nodeId ,materialId)).filter(Optional::isPresent).map(Optional::get).toList());
return jobs.stream().map(reportTransformer::toReportDTO).toList();

View file

@ -190,6 +190,8 @@ public class ReportTransformer {
destinationDTO.setWeight(weightUnit.convertFromG(premise.getIndividualHuWeight()));
destinationDTO.setHuUnitCount(premise.getHuUnitCount());
CalculationJobRouteSection mainRun = sections.stream().filter(CalculationJobRouteSection::getMainRun).findFirst().orElse(null);
destinationDTO.setLayer(destination.getLayerCount());
destinationDTO.setOverseaShare(premise.getOverseaShare().doubleValue());
@ -199,7 +201,6 @@ public class ReportTransformer {
if (includeAirfreight)
destinationDTO.setAirFreightShare(destination.getAirFreightShare().doubleValue());
CalculationJobRouteSection mainRun = sections.stream().filter(CalculationJobRouteSection::getMainRun).findFirst().orElse(null);
destinationDTO.setMixed(premise.getHuMixable());
destinationDTO.setRate(mainRun == null ? 0 : mainRun.getRate());