From 8698031689b43a4fd9f21edc4231e736a0d84fbd Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 25 Sep 2025 23:31:12 +0200 Subject: [PATCH] FRONTEND: - Introduced `StagedRatesStore` with support for staged changes and expiry handling. - Updated `Rates.vue` to integrate staged rates check and display. - Enhanced `TableView` with flag support for improved visuals. - Adjusted bulk operation timer behavior to fix async issues. - Incorporated Kosovo flag asset and updated styles for better layout. BACKEND: - Added Matrix/Container Rate import service. - Added renewal function for expired rates. --- src/frontend/src/assets/flags/XK.svg | 92 ++++++++++++ src/frontend/src/components/UI/Flag.vue | 10 +- .../src/components/UI/TabContainer.vue | 2 + src/frontend/src/components/UI/TableView.vue | 23 ++- .../layout/config/BulkOperations.vue | 42 ++++-- .../layout/config/CountryProperties.vue | 80 ++++++----- .../components/layout/config/Materials.vue | 22 ++- .../src/components/layout/config/Nodes.vue | 29 +++- .../components/layout/config/Properties.vue | 11 +- .../src/components/layout/config/Rates.vue | 58 +++++--- .../components/layout/config/StagedRates.vue | 135 ++++++++++++++++++ src/frontend/src/main.js | 5 +- src/frontend/src/pages/Config.vue | 25 +++- src/frontend/src/pages/ErrorLog.vue | 37 ++++- src/frontend/src/store/bulkOperation.js | 16 +-- src/frontend/src/store/stagedRates.js | 33 +++++ .../configuration/RateController.java | 5 +- .../configuration/rates/StagedRatesDTO.java | 27 ++++ .../de/avatic/lcc/model/rates/MatrixRate.java | 10 ++ .../lcc/model/rates/ValidityPeriod.java | 9 ++ .../rates/ContainerRateRepository.java | 26 ++++ .../rates/MatrixRateRepository.java | 17 +++ .../rates/ValidityPeriodRepository.java | 39 ++++- .../lcc/service/bulk/BulkImportService.java | 16 ++- .../ContainerRateImportService.java | 30 ++++ .../bulkImport/MatrixRateImportService.java | 30 ++++ .../configuration/RateApprovalService.java | 71 ++++++++- src/main/resources/schema.sql | 15 +- 28 files changed, 790 insertions(+), 125 deletions(-) create mode 100644 src/frontend/src/assets/flags/XK.svg create mode 100644 src/frontend/src/components/layout/config/StagedRates.vue create mode 100644 src/frontend/src/store/stagedRates.js create mode 100644 src/main/java/de/avatic/lcc/dto/configuration/rates/StagedRatesDTO.java create mode 100644 src/main/java/de/avatic/lcc/service/bulk/bulkImport/ContainerRateImportService.java create mode 100644 src/main/java/de/avatic/lcc/service/bulk/bulkImport/MatrixRateImportService.java diff --git a/src/frontend/src/assets/flags/XK.svg b/src/frontend/src/assets/flags/XK.svg new file mode 100644 index 0000000..1e0603f --- /dev/null +++ b/src/frontend/src/assets/flags/XK.svg @@ -0,0 +1,92 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/src/frontend/src/components/UI/Flag.vue b/src/frontend/src/components/UI/Flag.vue index c6c586d..8efffd6 100644 --- a/src/frontend/src/components/UI/Flag.vue +++ b/src/frontend/src/components/UI/Flag.vue @@ -1,12 +1,20 @@ + + \ No newline at end of file diff --git a/src/frontend/src/main.js b/src/frontend/src/main.js index eae9f1b..f1e835c 100644 --- a/src/frontend/src/main.js +++ b/src/frontend/src/main.js @@ -27,7 +27,8 @@ import { PhArchive, PhFloppyDisk, PhArrowCounterClockwise, - PhCheck, PhBug, PhShuffle, PhStack, PhFile, PhFilePlus, PhDownloadSimple + PhCheck, PhBug, PhShuffle, PhStack, PhFile, PhFilePlus, PhDownloadSimple, PhMonitor, PhCpu, PhFileJs, PhFileCloud, + PhCloudX, PhDesktop, PhHardDrives } from "@phosphor-icons/vue"; const app = createApp(App); @@ -62,6 +63,8 @@ app.component('PhBug', PhBug); app.component('PhShuffle', PhShuffle); app.component('PhStack', PhStack ); app.component('PhFile', PhFile); +app.component("PhDesktop", PhDesktop ); +app.component("PhHardDrives", PhHardDrives ); app.use(router); diff --git a/src/frontend/src/pages/Config.vue b/src/frontend/src/pages/Config.vue index ba36dcf..873f561 100644 --- a/src/frontend/src/pages/Config.vue +++ b/src/frontend/src/pages/Config.vue @@ -3,9 +3,8 @@
- - +
@@ -35,32 +34,50 @@ export default { currentTab: null, tabsConfig: [ { - title: 'System properties', + title: 'Properties', component: markRaw(Properties), + props: { isSelected: false}, }, { - title: 'System error log', + title: 'System log', component: markRaw(ErrorLog), + props: { isSelected: false}, }, { title: 'Materials', component: markRaw(Materials), + props: { isSelected: false}, }, { title: 'Nodes', component: markRaw(Nodes), + props: { isSelected: false}, }, { title: 'Rates', component: markRaw(Rates), + props: { isSelected: false}, }, { title: 'Bulk operations', component: markRaw(BulkOperations), + props: { isSelected: false}, } ] } }, + methods: { + handleTabChange(eventData) { + + console.log("handleTabChange") + + const { index, tab } = eventData; + console.log(`Tab ${index} activated:`, tab.title); + + this.tabsConfig.forEach(t => t.props.isSelected = t.title === tab.title); + + } + } } diff --git a/src/frontend/src/pages/ErrorLog.vue b/src/frontend/src/pages/ErrorLog.vue index 543092e..1183fbf 100644 --- a/src/frontend/src/pages/ErrorLog.vue +++ b/src/frontend/src/pages/ErrorLog.vue @@ -25,14 +25,25 @@ export default { return { showModal: false, error: null, - pageSize: 10, + pageSize: 20, pagination: { page: 1, pageCount: 10, totalCount: 1 }, columns: [ + {key: 'type', label: 'Type', align: "center", iconResolver: (rawValue, item) => { + + if (rawValue === "FRONTEND") { + return "PhDesktop"; + } else if (rawValue === "BACKEND") { + return "PhHardDrives" + } else if (rawValue === "BULK") { + return "PhStack" + } else if(rawValue === "CALCULATION") { + return "PhCalculator" + } + }}, {key: 'timestamp', label: 'Timestamp', formatter: (value) => this.buildDate(value) }, {key: 'user_id', label: 'User'}, - {key: 'type', label: 'Type'}, {key: 'title', label: 'Title'}, - {key: 'message', label: 'Message'}, + {key: 'message', label: 'Message', formatter: (value) => (value?.length > 100 ? `${value?.substring(0,100)} ...` : value)}, {key: 'code', label: 'Exception'}, ], @@ -41,8 +52,28 @@ export default { computed: { ...mapStores(useErrorLogStore) }, + props: { + isSelected: { + type: Boolean, + default: false + } + }, + watch: { + async isSelected(newVal) { + if(newVal === true) { + const query = { + searchTerm: '', + page: 1, + pageSize: 10, + } + await this.fetchData(query); + } + } + }, methods: { + async fetchData(query) { + console.log("fetchData") await this.errorLogStore.setQuery(query); this.pagination = this.errorLogStore.getPagination; return this.errorLogStore.getErrors; diff --git a/src/frontend/src/store/bulkOperation.js b/src/frontend/src/store/bulkOperation.js index 89eaa95..94a624e 100644 --- a/src/frontend/src/store/bulkOperation.js +++ b/src/frontend/src/store/bulkOperation.js @@ -34,11 +34,9 @@ export const useBulkOperationStore = defineStore('bulkOperation', { this.startTimer(); }, async timerMethod() { - this.updateStatus(); + await this.updateStatus(); const restart = this.restartNeeded(); - console.log("state " + this.bulkOperations.map(b => b.state).join(", ") + "restarting " + restart); - this.stopTimer(); if(restart) { @@ -46,6 +44,11 @@ export const useBulkOperationStore = defineStore('bulkOperation', { } }, + async manageStatus() { + await this.updateStatus(); + if(this.restartNeeded()) + this.startTimer(); + }, async updateStatus() { this.loading = true; @@ -79,15 +82,12 @@ export const useBulkOperationStore = defineStore('bulkOperation', { startTimer() { if (this.updateTimer) return - console.log("start timer") - this.updateTimer = setTimeout(() => { - this.timerMethod() + this.updateTimer = setTimeout(async () => { + await this.timerMethod() }, this.updateInterval) }, stopTimer() { if (this.updateTimer) { - console.log("stop timer") - clearTimeout(this.updateTimer) this.updateTimer = null } diff --git a/src/frontend/src/store/stagedRates.js b/src/frontend/src/store/stagedRates.js new file mode 100644 index 0000000..d4ea8ff --- /dev/null +++ b/src/frontend/src/store/stagedRates.js @@ -0,0 +1,33 @@ +import {defineStore} from 'pinia' +import {config} from '@/config' +import {useErrorStore} from "@/store/error.js"; +import performRequest from "@/backend.js"; + +export const useStagedRatesStore = defineStore('stagedRates', { + state() { + return { + stagedChanges: {staged_changes: false, expires: null} + } + }, + getters: { + hasStagedChanges(state) { + return state.stagedChanges.staged_changes; + }, + expiresSoon(state) { + return !state.stagedChanges.staged_changes && state.stagedChanges.expires !== null && (state.stagedChanges.expires < 10); + } + }, + actions: { + async checkStagedChanges() { + const url = `${config.backendUrl}/rates/staged_changes`; + const resp = await performRequest(this, 'GET', url, null,); + this.stagedChanges = resp.data; + }, + async applyChanges() { + const url = `${config.backendUrl}/rates/staged_changes`; + await performRequest(this, 'PUT', url, null, false); + this.stagedChanges = {staged_changes: false, expires: null}; + }, + } + +}); \ No newline at end of file diff --git a/src/main/java/de/avatic/lcc/controller/configuration/RateController.java b/src/main/java/de/avatic/lcc/controller/configuration/RateController.java index 27bd6b7..f67c4b7 100644 --- a/src/main/java/de/avatic/lcc/controller/configuration/RateController.java +++ b/src/main/java/de/avatic/lcc/controller/configuration/RateController.java @@ -2,6 +2,7 @@ package de.avatic.lcc.controller.configuration; import de.avatic.lcc.dto.configuration.matrixrates.MatrixRateDTO; import de.avatic.lcc.dto.configuration.rates.ContainerRateDTO; +import de.avatic.lcc.dto.configuration.rates.StagedRatesDTO; import de.avatic.lcc.dto.generic.ValidityPeriodDTO; import de.avatic.lcc.repositories.pagination.SearchQueryResult; import de.avatic.lcc.service.access.ContainerRateService; @@ -166,8 +167,8 @@ public class RateController { * whether rate drafts exist (true) or not (false). */ @GetMapping( {"/staged_changes", "/staged_changes/"}) - public ResponseEntity checkRateDrafts() { - return ResponseEntity.ok(rateApprovalService.hasRateDrafts()); + public ResponseEntity checkRateDrafts() { + return ResponseEntity.ok(rateApprovalService.getStagedRateDTO()); } /** diff --git a/src/main/java/de/avatic/lcc/dto/configuration/rates/StagedRatesDTO.java b/src/main/java/de/avatic/lcc/dto/configuration/rates/StagedRatesDTO.java new file mode 100644 index 0000000..9edc8e7 --- /dev/null +++ b/src/main/java/de/avatic/lcc/dto/configuration/rates/StagedRatesDTO.java @@ -0,0 +1,27 @@ +package de.avatic.lcc.dto.configuration.rates; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class StagedRatesDTO { + + @JsonProperty("staged_changes") + private boolean stagedChanges; + + private Integer expires; + + public boolean isStagedChanges() { + return stagedChanges; + } + + public void setStagedChanges(boolean stagedChanges) { + this.stagedChanges = stagedChanges; + } + + public Integer getExpires() { + return expires; + } + + public void setExpires(Integer expires) { + this.expires = expires; + } +} diff --git a/src/main/java/de/avatic/lcc/model/rates/MatrixRate.java b/src/main/java/de/avatic/lcc/model/rates/MatrixRate.java index 8de5123..3b8e006 100644 --- a/src/main/java/de/avatic/lcc/model/rates/MatrixRate.java +++ b/src/main/java/de/avatic/lcc/model/rates/MatrixRate.java @@ -18,6 +18,8 @@ public class MatrixRate { @NotNull private Integer toCountry; + private Integer validityPeriodId; + public Integer getId() { return id; } @@ -49,4 +51,12 @@ public class MatrixRate { public void setToCountry(Integer toCountry) { this.toCountry = toCountry; } + + public void setValidityPeriodId(Integer validityPeriodId) { + this.validityPeriodId = validityPeriodId; + } + + public Integer getValidityPeriodId() { + return validityPeriodId; + } } diff --git a/src/main/java/de/avatic/lcc/model/rates/ValidityPeriod.java b/src/main/java/de/avatic/lcc/model/rates/ValidityPeriod.java index 9fa7567..27cb2fb 100644 --- a/src/main/java/de/avatic/lcc/model/rates/ValidityPeriod.java +++ b/src/main/java/de/avatic/lcc/model/rates/ValidityPeriod.java @@ -14,6 +14,7 @@ public class ValidityPeriod { private ValidityPeriodState state; + private int renewals; public Integer getId() { return id; @@ -46,4 +47,12 @@ public class ValidityPeriod { public void setState(ValidityPeriodState state) { this.state = state; } + + public void setRenewals(int renewals) { + this.renewals = renewals; + } + + public int getRenewals() { + return renewals; + } } diff --git a/src/main/java/de/avatic/lcc/repositories/rates/ContainerRateRepository.java b/src/main/java/de/avatic/lcc/repositories/rates/ContainerRateRepository.java index 89d643a..5229eab 100644 --- a/src/main/java/de/avatic/lcc/repositories/rates/ContainerRateRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/rates/ContainerRateRepository.java @@ -180,6 +180,32 @@ public class ContainerRateRepository { return Optional.of(route.getFirst()); } + @Transactional + public void insert(ContainerRate containerRate) { + String sql = """ + INSERT INTO container_rate + (from_node_id, to_node_id, container_rate_type, rate_teu, rate_feu, rate_hc, lead_time, validity_period_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + container_rate_type = VALUES(container_rate_type), + rate_teu = VALUES(rate_teu), + rate_feu = VALUES(rate_feu), + rate_hc = VALUES(rate_hc), + lead_time = VALUES(lead_time) + """; + + jdbcTemplate.update(sql, + containerRate.getFromNodeId(), + containerRate.getToNodeId(), + containerRate.getType().name(), + containerRate.getRateTeu(), + containerRate.getRateFeu(), + containerRate.getRateHc(), + containerRate.getLeadTime(), + containerRate.getValidityPeriodId() + ); + } + private static class ContainerRateMapper implements RowMapper { diff --git a/src/main/java/de/avatic/lcc/repositories/rates/MatrixRateRepository.java b/src/main/java/de/avatic/lcc/repositories/rates/MatrixRateRepository.java index c91e327..e8380c4 100644 --- a/src/main/java/de/avatic/lcc/repositories/rates/MatrixRateRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/rates/MatrixRateRepository.java @@ -134,6 +134,22 @@ public class MatrixRateRepository { return Optional.of(rates.getFirst()); } + @Transactional + public void insert(MatrixRate rate) { + String sql = """ + INSERT INTO country_matrix_rate (from_country_id, to_country_id, rate, validity_period_id) + VALUES (?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + rate = VALUES(rate) + """; + + jdbcTemplate.update(sql, + rate.getFromCountry(), + rate.getToCountry(), + rate.getRate(), + rate.getValidityPeriodId()); + } + /** * Maps rows of a {@link ResultSet} to {@link MatrixRate} objects as required by * the {@link JdbcTemplate}. @@ -155,6 +171,7 @@ public class MatrixRateRepository { entity.setRate(rs.getBigDecimal("rate")); entity.setFromCountry(rs.getInt("from_country_id")); entity.setToCountry(rs.getInt("to_country_id")); + entity.setValidityPeriodId(rs.getInt("validity_period_id")); return entity; } diff --git a/src/main/java/de/avatic/lcc/repositories/rates/ValidityPeriodRepository.java b/src/main/java/de/avatic/lcc/repositories/rates/ValidityPeriodRepository.java index 729e7ea..f521ff0 100644 --- a/src/main/java/de/avatic/lcc/repositories/rates/ValidityPeriodRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/rates/ValidityPeriodRepository.java @@ -3,6 +3,8 @@ package de.avatic.lcc.repositories.rates; import de.avatic.lcc.model.ValidityTuple; import de.avatic.lcc.model.rates.ValidityPeriod; import de.avatic.lcc.model.rates.ValidityPeriodState; +import de.avatic.lcc.util.exception.internalerror.DatabaseException; +import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Repository; @@ -80,6 +82,7 @@ public class ValidityPeriodRepository { * @param id the unique identifier of the validity period. * @return the {@link ValidityPeriod} corresponding to the ID. */ + @Transactional public ValidityPeriod getById(Integer id) { String query = "SELECT * FROM validity_period WHERE id = ?"; return jdbcTemplate.queryForObject(query, new ValidityPeriodMapper(), id); @@ -89,7 +92,8 @@ public class ValidityPeriodRepository { * Creates a draft validity period if none exists in the database. */ private void createSet() { - jdbcTemplate.update("INSERT INTO validity_period (state) SELECT ? WHERE NOT EXISTS (SELECT 1 FROM validity_period WHERE state = ?)", ValidityPeriodState.DRAFT.name(), ValidityPeriodState.DRAFT.name()); + final Timestamp currentTimestamp = new Timestamp(System.currentTimeMillis()); + jdbcTemplate.update("INSERT INTO validity_period (state, start_date) SELECT ?, ? WHERE NOT EXISTS (SELECT 1 FROM validity_period WHERE state = ?)", ValidityPeriodState.DRAFT.name(), currentTimestamp, ValidityPeriodState.DRAFT.name()); } /** @@ -97,6 +101,7 @@ public class ValidityPeriodRepository { * * @return the ID of the valid {@link ValidityPeriod}. */ + @Transactional public Optional getValidPeriodId() { return getValidPeriod().map(ValidityPeriod::getId); } @@ -106,6 +111,7 @@ public class ValidityPeriodRepository { * * @return the {@link ValidityPeriod} in the {@code VALID} state. */ + @Transactional public Optional getValidPeriod() { String query = "SELECT * FROM validity_period WHERE state = ?"; var period = jdbcTemplate.query(query, new ValidityPeriodMapper(), ValidityPeriodState.VALID.name()); @@ -121,9 +127,17 @@ public class ValidityPeriodRepository { * * @return the {@link ValidityPeriod} in the {@code DRAFT} state. */ + @Transactional public ValidityPeriod getDraftPeriod() { + createSet(); + String query = "SELECT * FROM validity_period WHERE state = ?"; - return jdbcTemplate.queryForObject(query, new ValidityPeriodMapper(), ValidityPeriodState.DRAFT.name()); + var period = jdbcTemplate.query(query, new ValidityPeriodMapper(), ValidityPeriodState.DRAFT.name()); + + if(period.isEmpty()) + throw new DatabaseException("No draft validity period exists"); + + return period.getFirst(); } /** @@ -131,6 +145,7 @@ public class ValidityPeriodRepository { * * @return the ID of the draft {@link ValidityPeriod}. */ + @Transactional public Integer getDraftPeriodId() { return getDraftPeriod().getId(); } @@ -148,8 +163,14 @@ public class ValidityPeriodRepository { if (id == null) return false; String query = "SELECT COUNT(*) FROM country_matrix_rate WHERE validity_period_id = ?"; - var totalCount = jdbcTemplate.queryForObject(query, Integer.class, id); - return totalCount != null && totalCount > 0; + var matrixCount = jdbcTemplate.queryForObject(query, Integer.class, id); + + query = "SELECT COUNT(*) FROM container_rate WHERE validity_period_id = ?"; + var containerCount = jdbcTemplate.queryForObject(query, Integer.class, id); + + int totalCount = (matrixCount != null ? matrixCount : 0) + (containerCount != null ? containerCount : 0); + + return totalCount > 0; } /** @@ -219,6 +240,7 @@ public class ValidityPeriodRepository { return Optional.of(periods.getFirst()); } + @Transactional public List findValidityPeriodsWithReportByMaterialId(Integer materialId) { String validityPeriodSql = """ @@ -231,7 +253,12 @@ public class ValidityPeriodRepository { return jdbcTemplate.query(validityPeriodSql, (rs, cnt) -> new ValidityTuple(rs.getInt("validity_period_id"), rs.getInt("property_set_id")), materialId); } - ; + @Transactional + public void increaseRenewal(Integer increase) { + String sql = "UPDATE validity_period SET renewals = renewals + ? WHERE state = 'VALID'"; + jdbcTemplate.update(sql, increase); + } + /** * Maps rows of a {@link ResultSet} to {@link ValidityPeriod} objects. @@ -254,6 +281,8 @@ public class ValidityPeriodRepository { String stateStr = rs.getString("state"); period.setState(stateStr != null ? ValidityPeriodState.valueOf(stateStr) : null); + period.setRenewals(rs.getInt("renewals")); + return period; diff --git a/src/main/java/de/avatic/lcc/service/bulk/BulkImportService.java b/src/main/java/de/avatic/lcc/service/bulk/BulkImportService.java index 04769c0..4ec0a45 100644 --- a/src/main/java/de/avatic/lcc/service/bulk/BulkImportService.java +++ b/src/main/java/de/avatic/lcc/service/bulk/BulkImportService.java @@ -3,9 +3,7 @@ package de.avatic.lcc.service.bulk; import de.avatic.lcc.model.bulk.BulkFileTypes; import de.avatic.lcc.model.bulk.BulkOperation; import de.avatic.lcc.repositories.NodeRepository; -import de.avatic.lcc.service.bulk.bulkImport.MaterialBulkImportService; -import de.avatic.lcc.service.bulk.bulkImport.NodeBulkImportService; -import de.avatic.lcc.service.bulk.bulkImport.PackagingBulkImportService; +import de.avatic.lcc.service.bulk.bulkImport.*; import de.avatic.lcc.service.excelMapper.*; import de.avatic.lcc.service.transformer.generic.NodeTransformer; import de.avatic.lcc.util.exception.internalerror.ExcelValidationError; @@ -32,8 +30,10 @@ public class BulkImportService { private final NodeBulkImportService nodeBulkImportService; private final PackagingBulkImportService packagingBulkImportService; private final MaterialBulkImportService materialBulkImportService; + private final MatrixRateImportService matrixRateImportService; + private final ContainerRateImportService containerRateImportService; - public BulkImportService(MatrixRateExcelMapper matrixRateExcelMapper, ContainerRateExcelMapper containerRateExcelMapper, MaterialExcelMapper materialExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper, NodeRepository nodeRepository, NodeTransformer nodeTransformer, NodeBulkImportService nodeBulkImportService, PackagingBulkImportService packagingBulkImportService, MaterialBulkImportService materialBulkImportService) { + public BulkImportService(MatrixRateExcelMapper matrixRateExcelMapper, ContainerRateExcelMapper containerRateExcelMapper, MaterialExcelMapper materialExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper, NodeRepository nodeRepository, NodeTransformer nodeTransformer, NodeBulkImportService nodeBulkImportService, PackagingBulkImportService packagingBulkImportService, MaterialBulkImportService materialBulkImportService, MatrixRateImportService matrixRateImportService, ContainerRateImportService containerRateImportService) { this.matrixRateExcelMapper = matrixRateExcelMapper; this.containerRateExcelMapper = containerRateExcelMapper; this.materialExcelMapper = materialExcelMapper; @@ -44,6 +44,8 @@ public class BulkImportService { this.nodeBulkImportService = nodeBulkImportService; this.packagingBulkImportService = packagingBulkImportService; this.materialBulkImportService = materialBulkImportService; + this.matrixRateImportService = matrixRateImportService; + this.containerRateImportService = containerRateImportService; } public void processOperation(BulkOperation op) throws IOException { @@ -56,14 +58,18 @@ public class BulkImportService { try (Workbook workbook = new XSSFWorkbook(in)) { Sheet sheet = workbook.getSheet(BulkFileTypes.valueOf(type.name()).getSheetName()); + if(sheet == null) + throw new ExcelValidationError("Provided file does not contain a sheet named " + BulkFileTypes.valueOf(type.name()).getSheetName()); + switch (type) { case CONTAINER_RATE: var containerRates = containerRateExcelMapper.extractSheet(sheet); + containerRateImportService.processContainerRates(containerRates); break; case COUNTRY_MATRIX: var matrixRates = matrixRateExcelMapper.extractSheet(sheet); - matrixRates.forEach(System.out::println); + matrixRateImportService.processMatrixRates(matrixRates); break; case MATERIAL: var materials = materialExcelMapper.extractSheet(sheet); diff --git a/src/main/java/de/avatic/lcc/service/bulk/bulkImport/ContainerRateImportService.java b/src/main/java/de/avatic/lcc/service/bulk/bulkImport/ContainerRateImportService.java new file mode 100644 index 0000000..8a0de68 --- /dev/null +++ b/src/main/java/de/avatic/lcc/service/bulk/bulkImport/ContainerRateImportService.java @@ -0,0 +1,30 @@ +package de.avatic.lcc.service.bulk.bulkImport; + +import de.avatic.lcc.model.rates.ContainerRate; +import de.avatic.lcc.repositories.rates.ContainerRateRepository; +import de.avatic.lcc.repositories.rates.ValidityPeriodRepository; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ContainerRateImportService { + + private final ValidityPeriodRepository validityPeriodRepository; + private final ContainerRateRepository containerRateRepository; + + public ContainerRateImportService(ValidityPeriodRepository validityPeriodRepository, ContainerRateRepository containerRateRepository) { + this.validityPeriodRepository = validityPeriodRepository; + this.containerRateRepository = containerRateRepository; + } + + public void processContainerRates(List containerRates) { + Integer periodId = validityPeriodRepository.getDraftPeriodId(); + containerRates.forEach(rate -> processContainerRate(rate,periodId)); + } + + public void processContainerRate(ContainerRate containerRate, Integer periodId) { + containerRate.setValidityPeriodId(periodId); + containerRateRepository.insert(containerRate); + } +} diff --git a/src/main/java/de/avatic/lcc/service/bulk/bulkImport/MatrixRateImportService.java b/src/main/java/de/avatic/lcc/service/bulk/bulkImport/MatrixRateImportService.java new file mode 100644 index 0000000..5681f4c --- /dev/null +++ b/src/main/java/de/avatic/lcc/service/bulk/bulkImport/MatrixRateImportService.java @@ -0,0 +1,30 @@ +package de.avatic.lcc.service.bulk.bulkImport; + +import de.avatic.lcc.model.rates.MatrixRate; +import de.avatic.lcc.repositories.rates.MatrixRateRepository; +import de.avatic.lcc.repositories.rates.ValidityPeriodRepository; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class MatrixRateImportService { + + private final MatrixRateRepository matrixRateRepository; + private final ValidityPeriodRepository validityPeriodRepository; + + public MatrixRateImportService(MatrixRateRepository matrixRateRepository, ValidityPeriodRepository validityPeriodRepository) { + this.matrixRateRepository = matrixRateRepository; + this.validityPeriodRepository = validityPeriodRepository; + } + + public void processMatrixRate(MatrixRate rate, Integer periodId) { + rate.setValidityPeriodId(periodId); + matrixRateRepository.insert(rate); + } + + public void processMatrixRates(List matrixRates) { + Integer periodId = validityPeriodRepository.getDraftPeriodId(); + matrixRates.forEach(rate -> processMatrixRate(rate,periodId)); + } +} diff --git a/src/main/java/de/avatic/lcc/service/configuration/RateApprovalService.java b/src/main/java/de/avatic/lcc/service/configuration/RateApprovalService.java index 2ff8ae2..1f1c62e 100644 --- a/src/main/java/de/avatic/lcc/service/configuration/RateApprovalService.java +++ b/src/main/java/de/avatic/lcc/service/configuration/RateApprovalService.java @@ -1,9 +1,15 @@ package de.avatic.lcc.service.configuration; -import de.avatic.lcc.repositories.rates.MatrixRateRepository; +import de.avatic.lcc.dto.configuration.rates.StagedRatesDTO; +import de.avatic.lcc.model.properties.SystemPropertyMappingId; +import de.avatic.lcc.model.rates.ValidityPeriod; import de.avatic.lcc.repositories.rates.ValidityPeriodRepository; +import de.avatic.lcc.service.access.PropertyService; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Optional; /** * A service class responsible for approving or verifying rate drafts. @@ -15,9 +21,11 @@ public class RateApprovalService { private final ValidityPeriodRepository validityPeriodRepository; + private final PropertyService propertyService; - public RateApprovalService(ValidityPeriodRepository validityPeriodRepository) { + public RateApprovalService(ValidityPeriodRepository validityPeriodRepository, PropertyService propertyService) { this.validityPeriodRepository = validityPeriodRepository; + this.propertyService = propertyService; } /** @@ -25,14 +33,65 @@ public class RateApprovalService { * * @return {@code true} if rate drafts exist, {@code false} otherwise. */ - public boolean hasRateDrafts() { - return validityPeriodRepository.hasRateDrafts(); + public StagedRatesDTO getStagedRateDTO() { + + var optValidPeriod = validityPeriodRepository.getValidPeriod(); + StagedRatesDTO stagedRatesDTO = new StagedRatesDTO(); + + stagedRatesDTO.setExpires(optValidPeriod.map(this::expires).orElse(null)); + stagedRatesDTO.setStagedChanges(validityPeriodRepository.hasRateDrafts()); + + return stagedRatesDTO; + } + + private Integer expires(ValidityPeriod validPeriod) { + Optional validityProperty = propertyService.getProperty(SystemPropertyMappingId.VALID_DAYS); + + if (validityProperty.isPresent()) { + var validDays = validityProperty.get() * (validPeriod.getRenewals() + 1); + return Math.toIntExact(Duration.between(LocalDateTime.now(), validPeriod.getStartDate().plusDays(validDays)).toDays()); + + } + + return null; + } + + private Integer getRenewalIncrease() { + Optional validityProperty = propertyService.getProperty(SystemPropertyMappingId.VALID_DAYS); + var optValidPeriod = validityPeriodRepository.getValidPeriod(); + + if (validityProperty.isPresent() && optValidPeriod.isPresent()) { + var validPeriod = optValidPeriod.get(); + var validDays = validityProperty.get() * (validPeriod.getRenewals() + 1); + + var expiresIn = Math.toIntExact(Duration.between(LocalDateTime.now(), validPeriod.getStartDate().plusDays(validDays)).toDays()) - 10; + + if (expiresIn < 0) { + return ((-1 * expiresIn) / validDays) + 1; + } + return 1; + } + + return null; } /** * Approves and applies all staged (draft) rates. */ public void approveRateDrafts() { - validityPeriodRepository.applyDraft(); + + if (validityPeriodRepository.hasRateDrafts()) { + validityPeriodRepository.applyDraft(); + } else { + Integer increase = getRenewalIncrease(); + + if (increase != null) { + validityPeriodRepository.increaseRenewal(increase); + } + + + } + + } } diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 481a821..6ae21ad 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -66,7 +66,7 @@ CREATE TABLE IF NOT EXISTS `country_property_type` `external_mapping_id` VARCHAR(16), `data_type` VARCHAR(16) NOT NULL, `validation_rule` VARCHAR(64), - `description` VARCHAR(255) NOT NULL, + `description` VARCHAR(255) NOT NULL, `property_group` VARCHAR(32) NOT NULL, `sequence_number` INT NOT NULL, `is_required` BOOLEAN NOT NULL DEFAULT FALSE, @@ -218,6 +218,7 @@ CREATE TABLE IF NOT EXISTS validity_period id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, start_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, end_date TIMESTAMP DEFAULT NULL, + renewals INT UNSIGNED DEFAULT 0, state CHAR(8) NOT NULL CHECK (state IN ('DRAFT', 'VALID', 'INVALID', 'EXPIRED')), CONSTRAINT `chk_validity_date_range` CHECK (`end_date` IS NULL OR `end_date` > `start_date`) ); @@ -237,7 +238,8 @@ CREATE TABLE IF NOT EXISTS container_rate FOREIGN KEY (to_node_id) REFERENCES node (id), FOREIGN KEY (validity_period_id) REFERENCES validity_period (id), INDEX idx_from_to_nodes (from_node_id, to_node_id), - INDEX idx_validity_period_id (validity_period_id) + INDEX idx_validity_period_id (validity_period_id), + CONSTRAINT uk_container_rate_unique UNIQUE (from_node_id, to_node_id, validity_period_id) ); CREATE TABLE IF NOT EXISTS country_matrix_rate @@ -251,7 +253,8 @@ CREATE TABLE IF NOT EXISTS country_matrix_rate FOREIGN KEY (to_country_id) REFERENCES country (id), FOREIGN KEY (validity_period_id) REFERENCES validity_period (id), INDEX idx_from_to_country (from_country_id, to_country_id), - INDEX idx_validity_period_id (validity_period_id) + INDEX idx_validity_period_id (validity_period_id), + CONSTRAINT uk_country_matrix_rate_unique UNIQUE (from_country_id, to_country_id, validity_period_id) ); @@ -311,9 +314,9 @@ CREATE TABLE IF NOT EXISTS packaging_property_type `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, `name` VARCHAR(255) NOT NULL, external_mapping_id VARCHAR(16) NOT NULL, - `description` VARCHAR(255) NOT NULL, - `property_group` VARCHAR(32) NOT NULL, - `sequence_number` INT NOT NULL, + `description` VARCHAR(255) NOT NULL, + `property_group` VARCHAR(32) NOT NULL, + `sequence_number` INT NOT NULL, `data_type` VARCHAR(16), `validation_rule` VARCHAR(64), `is_required` BOOLEAN NOT NULL DEFAULT FALSE,