Merge branch 'main' of git.avatic.de:avatic/lcc_tool

This commit is contained in:
Anja Guenther 2025-09-28 10:41:30 +02:00
commit dc29a38230
11 changed files with 67 additions and 218 deletions

View file

@ -1,7 +1,7 @@
<template>
<div class="container-rate-container">
<staged-rates ref="stagedRatesRef"></staged-rates>
<staged-rates ref="stagedRatesRef" @rates-update="reloadTable"></staged-rates>
<div class="container-rate-header">
@ -79,7 +79,8 @@ export default {
},
components: {
StagedRates,
RadioOption, TableView, DataTable, AutosuggestSearchbar, ModalDialog, Tooltip, IconButton, Dropdown},
RadioOption, TableView, DataTable, AutosuggestSearchbar, ModalDialog, Tooltip, IconButton, Dropdown
},
computed: {
...mapStores(useValidityPeriodStore, useMatrixRateStore, useContainerRateStore),
loadingPeriods() {
@ -146,7 +147,7 @@ export default {
rateTypeValue: "container",
selectedTypeColumns: [],
matrixColumns: [
{key: 'source.iso_code', label: 'From Country (ISO code)' },
{key: 'source.iso_code', label: 'From Country (ISO code)'},
{key: 'destination.iso_code', label: 'To Country (ISO code)'},
{key: 'rate', align: 'right', label: 'Rate [EUR/km]'},
],
@ -207,7 +208,17 @@ export default {
this.modalDialogDeleteState = true;
}
},
async reloadTable() {
await this.validityPeriodStore.loadPeriods();
await this.matrixRateStore.setQuery();
await this.containerRateStore.setQuery();
this.pagination = this.rateType === 'container' ? this.containerRateStore.getPagination : this.matrixRateStore.getPagination;
},
deleteModalClick() {
this.modalDialogDeleteState = false;
if (action === 'accept')
this.validPeriodStore.invalidate();
}
}

View file

@ -31,6 +31,7 @@ import {useStagedRatesStore} from "@/store/stagedRates.js";
export default {
name: "StagedRates",
components: {ModalDialog, IconButton},
emits: ['ratesUpdate'],
data() {
return {
modalDialogStagedChangesState: false,
@ -76,7 +77,7 @@ export default {
this.modalDialogStagedChangesState = false;
if (action === 'accept') {
await this.stagedRatesStore.applyChanges();
this.$emit('rates-update');
this.$emit('ratesUpdate');
}
},
checkChanges() {

View file

@ -1,6 +1,6 @@
<template>
<div class="destination-edit-modal-container">
<h3 class="sub-header">Edit Destination</h3>
<h3 class="sub-header">Edit destination</h3>
<div class="destination-edit-container">
<tab-container :tabs="tabsConfig" class="tab-container">
</tab-container >

View file

@ -7,25 +7,25 @@
</div>
<div>
<checkbox :checked="inputFieldsActive" @checkbox-changed="activateInputFields">I want to enter handling and
repackaging costs.
repackaging costs manually.
</checkbox>
</div>
<div class="destination-edit-handling-cost-container" v-show="inputFieldsActive">
<div class="destination-edit-column-caption">Repackaging cost [EUR]</div>
<div class="destination-edit-column-caption">Repackaging cost [EUR/HU]</div>
<div class="destination-edit-column-data">
<div class="text-container">
<input :value="repackaging" @blur="validate('repackaging', $event)" class="input-field"
autocomplete="off"/>
</div>
</div>
<div class="destination-edit-column-caption">Handling cost [EUR]</div>
<div class="destination-edit-column-caption">Handling cost [EUR/HU]</div>
<div class="destination-edit-column-data">
<div class="text-container">
<input :value="handling" @blur="validate('handling', $event)" class="input-field"
autocomplete="off"/>
</div>
</div>
<div class="destination-edit-column-caption">Disposal cost [EUR]</div>
<div class="destination-edit-column-caption">Disposal cost [EUR/HU]</div>
<div class="destination-edit-column-data">
<div class="text-container">
<input :value="disposal" @blur="validate('disposal', $event)" class="input-field"

View file

@ -2,6 +2,7 @@ import {defineStore} from 'pinia'
import {config} from '@/config'
import {useErrorStore} from "@/store/error.js";
import { useStageStore } from './stage.js'
import performRequest from "@/backend.js";
export const usePropertySetsStore = defineStore('propertySets', {
state() {
@ -47,7 +48,7 @@ export const usePropertySetsStore = defineStore('propertySets', {
async invalidate() {
const url = `${config.backendUrl}/properties/periods/${this.getSelectedPeriod}/`;
this.periods = await this.performRequest('DELETE', url, null, false);
this.periods = await performRequest(this,'DELETE', url, null, false);
await this.reload();
@ -55,103 +56,9 @@ export const usePropertySetsStore = defineStore('propertySets', {
async loadPeriods() {
this.loading = true;
const url = `${config.backendUrl}/properties/periods`;
this.periods = await this.performRequest('GET', url, null, );
this.periods = await performRequest(this, 'GET', url, null);
this.selectedPeriod = this.getCurrentPeriodId;
this.loading = false;
},
async performRequest(method, url, body, expectResponse = true) {
const params = {
method: method,
headers: {
'Content-Type': 'application/json'
}
};
if ((body ?? null) !== null) {
params.body = JSON.stringify(body);
}
const request = {url: url, params: params};
console.log("Request:", request);
const response = await fetch(url, params
).catch(e => {
const error = {
code: 'Network error.',
message: "Please check your internet connection.",
trace: null
}
console.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
}
console.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
}
console.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
}
console.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
}
console.error(error);
const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request});
throw new Error('Internal backend error');
}
}
console.log("Response:", data);
return data;
}
}
});

View file

@ -2,6 +2,7 @@ import {defineStore} from 'pinia'
import {config} from '@/config'
import {useErrorStore} from "@/store/error.js";
import { useStageStore } from './stage.js'
import performRequest from "@/backend.js";
export const useValidityPeriodStore = defineStore('validityPeriod', {
state() {
@ -37,12 +38,12 @@ export const useValidityPeriodStore = defineStore('validityPeriod', {
},
actions: {
setSelectedPeriod(periodId) {
this.selectedPeriod = periodId;
this.selectedPeriod = periodId;
},
async invalidate() {
const url = `${config.backendUrl}/rates/periods/${this.getSelectedPeriod}/`;
this.periods = await this.performRequest('DELETE', url, null, false);
this.periods = await performRequest(this,'DELETE', url, null, false);
await this.reload();
@ -50,103 +51,9 @@ export const useValidityPeriodStore = defineStore('validityPeriod', {
async loadPeriods() {
this.loading = true;
const url = `${config.backendUrl}/rates/periods`;
this.periods = await this.performRequest('GET', url, null, );
this.periods = await performRequest(this,'GET', url, null);
this.selectedPeriod = this.getCurrentPeriodId;
this.loading = false;
},
async performRequest(method, url, body, expectResponse = true) {
const params = {
method: method,
headers: {
'Content-Type': 'application/json'
}
};
if ((body ?? null) !== null) {
params.body = JSON.stringify(body);
}
const request = {url: url, params: params};
console.log("Request:", request);
const response = await fetch(url, params
).catch(e => {
const error = {
code: 'Network error.',
message: "Please check your internet connection.",
trace: null
}
console.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
}
console.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
}
console.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
}
console.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
}
console.error(error);
const errorStore = useErrorStore();
void errorStore.addError(error, {store: this, request: request});
throw new Error('Internal backend error');
}
}
console.log("Response:", data);
return data;
}
}
});

View file

@ -62,10 +62,10 @@ public class CalculationJobRepository {
}
@Transactional
public Optional<CalculationJob> getCalculationJob(Integer periodId, Integer nodeId, Integer materialId) {
public Optional<CalculationJob> getCalculationJobWithJobStateValid(Integer periodId, Integer nodeId, Integer materialId) {
/* there should only be one job per period id, node id and material id combination */
String query = "SELECT * FROM calculation_job AS cj INNER JOIN premise AS p ON cj.premise_id = p.id WHERE validity_period_id = ? AND p.supplier_node_id = ? AND material_id = ? LIMIT 1";
String query = "SELECT * FROM calculation_job AS cj INNER JOIN premise AS p ON cj.premise_id = p.id WHERE job_state = 'VALID' AND validity_period_id = ? AND p.supplier_node_id = ? AND material_id = ? LIMIT 1";
var job = jdbcTemplate.query(query, new CalculationJobMapper(), periodId, nodeId, materialId);

View file

@ -9,6 +9,7 @@ import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.repositories.rates.ContainerRateRepository;
import de.avatic.lcc.service.bulk.helper.ConstraintGenerator;
import de.avatic.lcc.service.bulk.helper.HeaderGenerator;
import de.avatic.lcc.util.exception.internalerror.ExcelValidationError;
import org.apache.poi.ss.usermodel.*;
import org.springframework.stereotype.Service;
@ -88,13 +89,35 @@ public class ContainerRateExcelMapper {
return true;
}
private String toExcelLetter(int columnIdx) {
StringBuilder result = new StringBuilder();
columnIdx++; // Convert from 0-based to 1-based for the algorithm
while (columnIdx > 0) {
columnIdx--; // Adjust for 1-based indexing
result.insert(0, (char) ('A' + columnIdx % 26));
columnIdx /= 26;
}
return result.toString();
}
private ContainerRate mapToEntity(Row row) {
ContainerRate entity = new ContainerRate();
validateConstraints(row);
entity.setFromNodeId(nodeRepository.getByExternalMappingId(row.getCell(ContainerRateHeader.FROM_NODE.ordinal()).getStringCellValue()).orElseThrow().getId());
entity.setToNodeId(nodeRepository.getByExternalMappingId(row.getCell(ContainerRateHeader.TO_NODE.ordinal()).getStringCellValue()).orElseThrow().getId());
var fromNode = nodeRepository.getByExternalMappingId(row.getCell(ContainerRateHeader.FROM_NODE.ordinal()).getStringCellValue());
var toNode = nodeRepository.getByExternalMappingId(row.getCell(ContainerRateHeader.TO_NODE.ordinal()).getStringCellValue());
if(fromNode.isEmpty() || fromNode.get().getDeprecated()) {
throw new ExcelValidationError("Unable to validate row " + (row.getRowNum() + 1) + " column " + toExcelLetter( ContainerRateHeader.FROM_NODE.ordinal()) + ": Node with mapping id " + row.getCell(ContainerRateHeader.FROM_NODE.ordinal()).getStringCellValue() + " not found.");
}
if(toNode.isEmpty() || toNode.get().getDeprecated()) {
throw new ExcelValidationError("Unable to validate row " + (row.getRowNum() + 1) + " column " + toExcelLetter( ContainerRateHeader.TO_NODE.ordinal()) + ": Node with mapping id " + row.getCell(ContainerRateHeader.TO_NODE.ordinal()).getStringCellValue() + " not found.");
}
entity.setFromNodeId(fromNode.orElseThrow().getId());
entity.setToNodeId(toNode.orElseThrow().getId());
entity.setType(TransportType.valueOf(row.getCell(ContainerRateHeader.CONTAINER_RATE_TYPE.ordinal()).getStringCellValue()));
entity.setLeadTime(Double.valueOf(row.getCell(ContainerRateHeader.LEAD_TIME.ordinal()).getNumericCellValue()).intValue());
entity.setType(TransportType.valueOf(row.getCell(ContainerRateHeader.CONTAINER_RATE_TYPE.ordinal()).getStringCellValue()));

View file

@ -118,16 +118,16 @@ public class PreCalculationCheckService {
throw new PremiseValidationError("In destination " + node.getName() + ": destination geo location not set. Please contact your administrator.");
}
if (destination.getDisposalCost() != null && destination.getDisposalCost().compareTo(BigDecimal.ZERO) == 0) {
throw new PremiseValidationError("In destination " + node.getName() + ": disposal costs entered as zero.");
if (destination.getDisposalCost() != null && destination.getDisposalCost().compareTo(BigDecimal.ZERO) < 0) {
throw new PremiseValidationError("In destination " + node.getName() + ": disposal costs are not set or entered value is less than zero.");
}
if (destination.getHandlingCost() != null && destination.getHandlingCost().compareTo(BigDecimal.ZERO) == 0) {
throw new PremiseValidationError("In destination " + node.getName() + ": handling costs entered as zero.");
if (destination.getHandlingCost() != null && destination.getHandlingCost().compareTo(BigDecimal.ZERO) < 0) {
throw new PremiseValidationError("In destination " + node.getName() + ": handling costs are not set or entered value is less than zero.");
}
if (destination.getRepackingCost() != null && destination.getRepackingCost().compareTo(BigDecimal.ZERO) == 0) {
throw new PremiseValidationError("In destination " + node.getName() + ": repackaging costs enstered as zero.");
if (destination.getRepackingCost() != null && destination.getRepackingCost().compareTo(BigDecimal.ZERO) < 0) {
throw new PremiseValidationError("In destination " + node.getName() + ": repackaging costs are not set or entered value is less than zero.");
}
}
@ -158,11 +158,11 @@ public class PreCalculationCheckService {
private void packagingCheck(Premise premise) {
if (premise.getHuMixable() == null) {
throw new PremiseValidationError("Mixable not set. Please contact administrator");
throw new PremiseValidationError("Mixable not set. Please contact administrator.");
}
if (premise.getHuStackable() == null) {
throw new PremiseValidationError("Stackable not set. Please contact administrator");
throw new PremiseValidationError("Stackable not set. Please contact administrator.");
}
if (premise.getHuStackable() == false && premise.getHuMixable() == true) {
@ -190,11 +190,11 @@ public class PreCalculationCheckService {
}
if (premise.getHuDisplayedWeightUnit() == null) {
throw new PremiseValidationError("Packaging weight unit not set. Please contact administrator");
throw new PremiseValidationError("Packaging weight unit not set. Please contact administrator.");
}
if (premise.getHuDisplayedDimensionUnit() == null) {
throw new PremiseValidationError("Packaging dimension unit not set. Please contact administrator");
throw new PremiseValidationError("Packaging dimension unit not set. Please contact administrator.");
}
var hu = dimensionTransformer.toDimensionEntity(premise).withTolerance(DIMENSION_TOLERANCE);

View file

@ -57,7 +57,7 @@ public class ReportingService {
var periodId = period.get().getId();
var jobs = nodeIds.stream().map(nodeId -> calculationJobRepository.getCalculationJob(periodId, nodeId,materialId)).filter(Optional::isPresent).map(Optional::get).toList();
var jobs = nodeIds.stream().map(nodeId -> calculationJobRepository.getCalculationJobWithJobStateValid(periodId, nodeId,materialId)).filter(Optional::isPresent).map(Optional::get).toList();
return jobs.stream().map(reportTransformer::toReportDTO).toList();
}

View file

@ -573,7 +573,7 @@ CREATE TABLE IF NOT EXISTS calculation_job_route_section
FOREIGN KEY (calculation_job_destination_id) REFERENCES calculation_job_destination (id),
INDEX idx_premise_route_section_id (premise_route_section_id),
INDEX idx_calculation_job_destination_id (calculation_job_destination_id),
CONSTRAINT chk_stacked CHECK (is_unmixed_price IS FALSE OR is_stacked IS TRUE) -- only unmixed transports can be unstacked
CONSTRAINT chk_stacked CHECK (is_unmixed_price IS TRUE OR is_stacked IS TRUE) -- only unmixed transports can be unstacked
);