From e5bd56d3a9227eb12325c9a16718bb1e42bef45b Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 16 Sep 2025 20:44:41 +0200 Subject: [PATCH] FRONTEND: bulk download working. Added views for nodes, materials and rates --- src/frontend/src/backend.js | 23 +- src/frontend/src/components/UI/Box.vue | 11 +- src/frontend/src/components/UI/DataTable.vue | 265 ++++++++++++++++++ src/frontend/src/components/UI/Pagination.vue | 201 +++++++++++++ src/frontend/src/components/UI/SearchBar.vue | 117 ++++++++ src/frontend/src/components/UI/TableView.vue | 265 ++++++++++++++++++ .../layout/config/BulkOperations.vue | 177 ++++++++---- .../components/layout/config/Materials.vue | 64 +++++ .../src/components/layout/config/Nodes.vue | 75 +++++ .../src/components/layout/config/Rates.vue | 241 ++++++++++++++++ .../layout/edit/DestinationListView.vue | 2 +- .../src/components/layout/node/SelectNode.vue | 2 +- src/frontend/src/main.js | 2 + src/frontend/src/pages/Config.vue | 15 + src/frontend/src/pages/Reporting.vue | 7 +- src/frontend/src/store/bulkOperation.js | 0 src/frontend/src/store/containerRate.js | 63 +++++ src/frontend/src/store/material.js | 56 ++-- src/frontend/src/store/matrixRate.js | 60 ++++ src/frontend/src/store/node.js | 53 ++-- src/frontend/src/store/premiseEdit.js | 14 +- src/frontend/src/store/reports.js | 23 +- src/frontend/src/store/validityPeriod.js | 157 +++++++++++ .../java/de/avatic/lcc/config/CorsConfig.java | 13 +- .../java/de/avatic/lcc/config/CorsFilter.java | 54 ++-- .../de/avatic/lcc/config/SecurityConfig.java | 7 + .../configuration/MaterialController.java | 3 +- .../configuration/NodeController.java | 2 +- .../configuration/RateController.java | 30 +- .../report/ReportingController.java | 2 +- .../matrixrates/MatrixRateDTO.java | 38 +-- .../configuration/rates/ContainerRateDTO.java | 10 +- .../avatic/lcc/dto/generic/MaterialDTO.java | 13 + .../de/avatic/lcc/model/bulk/NodeHeader.java | 2 +- .../avatic/lcc/model/utils/DimensionUnit.java | 2 +- .../lcc/repositories/MaterialRepository.java | 12 + .../rates/ContainerRateRepository.java | 62 +++- .../rates/MatrixRateRepository.java | 57 +++- .../service/access/ContainerRateService.java | 16 +- .../lcc/service/access/MaterialService.java | 18 +- .../lcc/service/access/MatrixRateService.java | 31 +- .../lcc/service/bulk/BulkExportService.java | 9 +- .../service/bulk/TemplateExportService.java | 26 +- .../bulk/helper/ConstraintGenerator.java | 54 +++- .../bulk/helper/HeaderCellStyleProvider.java | 24 +- .../service/bulk/helper/HeaderGenerator.java | 18 ++ .../excelMapper/ContainerRateExcelMapper.java | 1 + .../excelMapper/MaterialExcelMapper.java | 3 +- .../excelMapper/MatrixRateExcelMapper.java | 1 + .../service/excelMapper/NodeExcelMapper.java | 15 +- .../excelMapper/PackagingExcelMapper.java | 42 ++- .../service/report/ExcelReportingService.java | 20 +- .../generic/MaterialTransformer.java | 2 + .../premise/PremiseTransformer.java | 2 +- .../transformer/report/ReportTransformer.java | 2 +- 55 files changed, 2174 insertions(+), 310 deletions(-) create mode 100644 src/frontend/src/components/UI/DataTable.vue create mode 100644 src/frontend/src/components/UI/Pagination.vue create mode 100644 src/frontend/src/components/UI/SearchBar.vue create mode 100644 src/frontend/src/components/UI/TableView.vue create mode 100644 src/frontend/src/components/layout/config/Materials.vue create mode 100644 src/frontend/src/components/layout/config/Nodes.vue create mode 100644 src/frontend/src/components/layout/config/Rates.vue create mode 100644 src/frontend/src/store/bulkOperation.js create mode 100644 src/frontend/src/store/containerRate.js create mode 100644 src/frontend/src/store/matrixRate.js create mode 100644 src/frontend/src/store/validityPeriod.js diff --git a/src/frontend/src/backend.js b/src/frontend/src/backend.js index f1d1007..bd00238 100644 --- a/src/frontend/src/backend.js +++ b/src/frontend/src/backend.js @@ -17,13 +17,13 @@ const performRequest = async (requestingStore, method, url, body, expectResponse const request = {url: url, params: params, expectResponse: expectResponse, expectedException: expectedException}; logger.info("Request:", request); - const data = await executeRequest(requestingStore, request); + const resp = await executeRequest(requestingStore, request); - logger.info("Response:", data); - return data; + logger.info("Response:", resp); + return resp; } -const performDownload = async (url, expectResponse = true, expectedException = null) => { +const performDownload = async (url, toFile, expectResponse = true, expectedException = null) => { const params = { method: 'GET', @@ -32,15 +32,15 @@ const performDownload = async (url, expectResponse = true, expectedException = n const request = {url: url, params: params, expectResponse: expectResponse, expectedException: expectedException, type: 'blob'}; logger.info("Request:", request); - const blob = await executeRequest(null, request); + const resp = await executeRequest(null, request); - const downloadUrl = window.URL.createObjectURL(blob); + const downloadUrl = window.URL.createObjectURL(resp.data); // Create temporary link element and trigger download const link = document.createElement('a'); link.href = downloadUrl; - link.download = `export.xlsx`; // or get filename from response headers + link.download = toFile; document.body.appendChild(link); link.click(); @@ -62,10 +62,10 @@ const performUpload = async (url, file, expectResponse = true, expectedException const request = {url: url, params: params, expectResponse: expectResponse, expectedException: expectedException}; logger.info("Request:", request); - const processId = await executeRequest(null, request); + const resp = await executeRequest(null, request); - logger.info("Response:", processId); - return processId; + logger.info("Response:", resp.data); + return resp.data; } function handleErrorResponse(data, requestingStore, request) { @@ -104,6 +104,7 @@ const executeRequest = async (requestingStore, request) => { throw e; }); + let data = null; if (request.expectResponse) { try { @@ -149,7 +150,7 @@ const executeRequest = async (requestingStore, request) => { } } - return data; + return {data: data, headers: response.headers}; } export default performRequest; diff --git a/src/frontend/src/components/UI/Box.vue b/src/frontend/src/components/UI/Box.vue index 1f07423..693e4d7 100644 --- a/src/frontend/src/components/UI/Box.vue +++ b/src/frontend/src/components/UI/Box.vue @@ -1,5 +1,5 @@ @@ -17,6 +17,10 @@ export default { stretchContent: { type: Boolean, default: false + }, + removePadding: { + type: Boolean, + default: false } } } @@ -35,7 +39,6 @@ export default { .box-bordered { border: 0.1rem solid #E3EDFF; - } .box { @@ -43,6 +46,10 @@ export default { background: white; border-radius: 0.8rem; position: relative; + +} + +.box-padding { padding: 1.5rem; } diff --git a/src/frontend/src/components/UI/DataTable.vue b/src/frontend/src/components/UI/DataTable.vue new file mode 100644 index 0000000..a3a4177 --- /dev/null +++ b/src/frontend/src/components/UI/DataTable.vue @@ -0,0 +1,265 @@ + + + + + \ No newline at end of file diff --git a/src/frontend/src/components/UI/Pagination.vue b/src/frontend/src/components/UI/Pagination.vue new file mode 100644 index 0000000..8768568 --- /dev/null +++ b/src/frontend/src/components/UI/Pagination.vue @@ -0,0 +1,201 @@ +```vue + + + + + \ No newline at end of file diff --git a/src/frontend/src/components/UI/SearchBar.vue b/src/frontend/src/components/UI/SearchBar.vue new file mode 100644 index 0000000..8f2aeac --- /dev/null +++ b/src/frontend/src/components/UI/SearchBar.vue @@ -0,0 +1,117 @@ + + + + + + \ No newline at end of file diff --git a/src/frontend/src/components/UI/TableView.vue b/src/frontend/src/components/UI/TableView.vue new file mode 100644 index 0000000..7f2cc8a --- /dev/null +++ b/src/frontend/src/components/UI/TableView.vue @@ -0,0 +1,265 @@ + + + + + + \ No newline at end of file diff --git a/src/frontend/src/components/layout/config/BulkOperations.vue b/src/frontend/src/components/layout/config/BulkOperations.vue index d274548..9414d05 100644 --- a/src/frontend/src/components/layout/config/BulkOperations.vue +++ b/src/frontend/src/components/layout/config/BulkOperations.vue @@ -2,69 +2,91 @@
- -
+ +
+
Bulk operation status
+
+
-
Export
-
type
-
- empty template - full data export -
-
dataset
-
- nodes - kilometer rates - container rates - materials - packaging -
+ +
- +
Export
+
type
+
+ empty template + full data export +
+
dataset
+
+ nodes + kilometer rates + + container rates + + materials + packaging
- - -
+
validity period
+
+ +
-
Import
-
dataset
-
- nodes - kilometer rates - container rates - materials - packaging -
-
import mode
-
- append existing data - fully replace data -
- -
file
-
-
- - -
- -
{{ selectedFileName }}
- -
- - + + + +
+ +
Import
+
dataset
+
+ nodes + kilometer rates + + container rates + + materials + packaging +
+
import mode
+
+ append existing data + fully replace data +
+ +
file
+
+
+ +
+
{{ selectedFileName }}
- + + + +
+ +
@@ -77,10 +99,13 @@ import BasicButton from "@/components/UI/BasicButton.vue"; import RadioOption from "@/components/UI/RadioOption.vue"; import performRequest, {performUpload, performDownload} from "@/backend.js"; import {config} from "@/config.js"; +import Dropdown from "@/components/UI/Dropdown.vue"; +import {mapStores} from "pinia"; +import {useValidityPeriodStore} from "@/store/validityPeriod.js"; export default { name: "BulkOperations", - components: {RadioOption, BasicButton, Box}, + components: {Dropdown, RadioOption, BasicButton, Box}, data() { return { exportType: "templates", @@ -93,11 +118,49 @@ export default { processId: null, } }, + computed: { + ...mapStores(useValidityPeriodStore), + showValidityPeriod() { + return this.exportType === "download" && (this.exportDataset === "COUNTRY_MATRIX" || this.exportDataset === "CONTAINER_RATE"); + }, + selectedPeriod: { + get() { + return this.validityPeriodStore.getSelectedPeriod + }, + async set(value) { + this.validityPeriodStore.setSelectedPeriod(value); + } + }, + periods() { + const periods = []; + + const ps = this.validityPeriodStore.getPeriods; + const current = this.validityPeriodStore.getCurrentPeriodId; + + if ((ps ?? null) === null) { + return null; + } + + for (const p of ps) { + const value = (p.state === "DRAFT" || p.state === "VALID") ? "CURRENT" : `${this.buildDate(p.start_date)} - ${this.buildDate(p.end_date)} ${p.state === "INVALID" ? "(INVALID)" : ""}`; + const period = {id: p.id, value: value}; + + if (p.state !== "VALID" || p.id === current) + periods.push(period); + } + + return periods; + } + }, + created() { + this.validityPeriodStore.loadPeriods(); + }, methods: { async downloadFile() { + const fileName = `lcc_export_${this.exportDataset.toLowerCase()}_${this.exportType.toLowerCase()}.xlsx`; const url = `${config.backendUrl}/bulk/${this.exportType}/${this.exportDataset}/` - this.processId = await performDownload(url); + this.processId = await performDownload(url, fileName); }, inputFile(event) { const file = event.target.files[0]; @@ -130,6 +193,10 @@ export default { gap: 1rem; } +.bulk-operation-box-status-container { + grid-column: 1 / -1; +} + .bulk-operations-box-container { flex: 1; } diff --git a/src/frontend/src/components/layout/config/Materials.vue b/src/frontend/src/components/layout/config/Materials.vue new file mode 100644 index 0000000..9ba3683 --- /dev/null +++ b/src/frontend/src/components/layout/config/Materials.vue @@ -0,0 +1,64 @@ + + + + + + \ No newline at end of file diff --git a/src/frontend/src/components/layout/config/Nodes.vue b/src/frontend/src/components/layout/config/Nodes.vue new file mode 100644 index 0000000..0ee8363 --- /dev/null +++ b/src/frontend/src/components/layout/config/Nodes.vue @@ -0,0 +1,75 @@ + + + + + + \ No newline at end of file diff --git a/src/frontend/src/components/layout/config/Rates.vue b/src/frontend/src/components/layout/config/Rates.vue new file mode 100644 index 0000000..8e06fad --- /dev/null +++ b/src/frontend/src/components/layout/config/Rates.vue @@ -0,0 +1,241 @@ + + + + + + \ No newline at end of file diff --git a/src/frontend/src/components/layout/edit/DestinationListView.vue b/src/frontend/src/components/layout/edit/DestinationListView.vue index c599753..da9da6f 100644 --- a/src/frontend/src/components/layout/edit/DestinationListView.vue +++ b/src/frontend/src/components/layout/edit/DestinationListView.vue @@ -85,7 +85,7 @@ export default { }, methods: { async fetchDestinations(query) { - await this.nodeStore.setQuery({searchTerm: query, nodeType: "DESTINATION", includeUserNode: false}); + await this.nodeStore.setSearch({searchTerm: query, nodeType: "DESTINATION", includeUserNode: false}); return this.nodeStore.nodes; }, resolveFlag(node) { diff --git a/src/frontend/src/components/layout/node/SelectNode.vue b/src/frontend/src/components/layout/node/SelectNode.vue index bb8f88a..049baf4 100644 --- a/src/frontend/src/components/layout/node/SelectNode.vue +++ b/src/frontend/src/components/layout/node/SelectNode.vue @@ -80,7 +80,7 @@ export default { }, async fetchSupplier(query) { console.log("Fetching supplier for query: " + query); - await this.nodeStore.setQuery({searchTerm: query, nodeType: 'SOURCE', includeUserNode: true}); + await this.nodeStore.setSearch({searchTerm: query, nodeType: 'SOURCE', includeUserNode: true}); return this.nodeStore.nodes; }, resolveFlag(node) { diff --git a/src/frontend/src/main.js b/src/frontend/src/main.js index e7b021e..eae9f1b 100644 --- a/src/frontend/src/main.js +++ b/src/frontend/src/main.js @@ -13,6 +13,7 @@ import { PhX, PhTrain, PhTruckTrailer, + PhTruck, PhBoat, PhPencilSimple, PhLock, @@ -48,6 +49,7 @@ app.component('PhWarning', PhWarning); app.component('PhLock', PhLock); app.component('PhLockOpen', PhLockOpen); app.component('PhTruckTrailer', PhTruckTrailer); +app.component('PhTruck', PhTruck); app.component('PhBoat', PhBoat); app.component('PhTrain', PhTrain); app.component('PhPencilSimple', PhPencilSimple); diff --git a/src/frontend/src/pages/Config.vue b/src/frontend/src/pages/Config.vue index 9ffbb64..3cb4220 100644 --- a/src/frontend/src/pages/Config.vue +++ b/src/frontend/src/pages/Config.vue @@ -23,6 +23,9 @@ import Box from "@/components/UI/Box.vue"; import CountryProperties from "@/components/layout/config/CountryProperties.vue"; import StagedChanges from "@/components/layout/config/StagedChanges.vue"; import BulkOperations from "@/components/layout/config/BulkOperations.vue"; +import Rates from "@/components/layout/config/Rates.vue"; +import Nodes from "@/components/layout/config/Nodes.vue"; +import Materials from "@/components/layout/config/Materials.vue"; export default { name: "Config", @@ -39,6 +42,18 @@ export default { title: 'Countries', component: markRaw(CountryProperties), }, + { + title: 'Materials', + component: markRaw(Materials), + }, + { + title: 'Nodes', + component: markRaw(Nodes), + }, + { + title: 'Rates', + component: markRaw(Rates), + }, { title: 'Bulk operations', component: markRaw(BulkOperations), diff --git a/src/frontend/src/pages/Reporting.vue b/src/frontend/src/pages/Reporting.vue index a58db46..0fbf1c0 100644 --- a/src/frontend/src/pages/Reporting.vue +++ b/src/frontend/src/pages/Reporting.vue @@ -91,15 +91,18 @@ export default { } }, methods: { + downloadReport() { + this.reportsStore.downloadReport(); + }, createReport() { this.showModal = true; }, buildDate(date) { return `${date[0]}-${date[1].toString().padStart(2, '0')}-${date[2].toString().padStart(2, '0')}` }, - closeModal(data) { + async closeModal(data) { if (data.action === 'accept') { - this.reportsStore.fetchReports(data.materialId, data.supplierIds); + await this.reportsStore.fetchReports(data.materialId, data.supplierIds); } this.showModal = false; } diff --git a/src/frontend/src/store/bulkOperation.js b/src/frontend/src/store/bulkOperation.js new file mode 100644 index 0000000..e69de29 diff --git a/src/frontend/src/store/containerRate.js b/src/frontend/src/store/containerRate.js new file mode 100644 index 0000000..2ad6056 --- /dev/null +++ b/src/frontend/src/store/containerRate.js @@ -0,0 +1,63 @@ +import {defineStore} from 'pinia' +import {config} from '@/config' +import {useErrorStore} from "@/store/error.js"; +import performRequest from "@/backend.js"; + +export const useContainerRateStore = defineStore('containerRate', { + state() { + return { + rates: [], + loading: false, + query: {}, + pagination: {} + } + }, + getters: { + isLoading(state) { + return state.loading; + }, + getRates(state) { + return state.rates; + }, + getPagination(state) { + return state.pagination; + } + + }, + actions: { + async setQuery(query = {}) { + this.query = query; + await this.updateMatrixRates(); + }, + async updateMatrixRates() { + this.loading = true; + + this.rates = []; + + const params = new URLSearchParams(); + if (this.query?.searchTerm && this.query.searchTerm !== '') + params.append('filter', this.query.searchTerm); + if(this.query?.periodId) + params.append('valid', this.query.periodId); + + if(this.query?.page) + params.append('page', this.query.page); + + if(this.query?.pageSize) + params.append('limit', this.query.pageSize); + + const url = `${config.backendUrl}/rates/container/${params.size === 0 ? '' : '?'}${params.toString()}`; + + const {data: data, headers: headers} = await performRequest(this, "GET", url, null); + + this.rates = data; + this.pagination = { page: parseInt(headers.get('X-Current-Page')), pageCount: parseInt(headers.get('X-Page-Count')), totalCount: parseInt(headers.get('X-Total-Count'))}; + + console.log(this.pagination) + + this.loading = false; + + } + } + +}); \ No newline at end of file diff --git a/src/frontend/src/store/material.js b/src/frontend/src/store/material.js index 9714e24..8ba9355 100644 --- a/src/frontend/src/store/material.js +++ b/src/frontend/src/store/material.js @@ -1,6 +1,7 @@ import {defineStore} from 'pinia' import {config} from '@/config' import {useErrorStore} from "@/store/error.js"; +import performRequest from "@/backend.js"; export const useMaterialStore = defineStore('material', { state() { @@ -15,6 +16,11 @@ export const useMaterialStore = defineStore('material', { }, getters: {}, actions: { + async setQueryForList(query) { + this.query = query; + this.query.excludeDeprecated = false; + await this.updateMaterials(); + }, async setQuery(query) { this.query = query; await this.updateMaterials(); @@ -30,48 +36,28 @@ export const useMaterialStore = defineStore('material', { if (this.query.searchTerm) params.append('filter', this.query.searchTerm); + if(this.query?.page) + params.append('page', this.query.page); + + if(this.query?.pageSize) + params.append('limit', this.query.pageSize); + + if(this.query?.excludeDeprecated !== null) + params.append('excludeDeprecated', this.query.excludeDeprecated); + const url = `${config.backendUrl}/materials/${params.size === 0 ? '' : '?'}${params.toString()}`; const request = { url: url, params: {method: 'GET'}}; - const response = await fetch(url).catch(e => { - this.error = {code: 'Network error.', message: "Please check your internet connection.", trace: null} - this.loading = false; - console.error(this.error); - const errorStore = useErrorStore(); - void errorStore.addError(this.error, { store: this, request: request}); + const {data: data, headers: headers} = await performRequest(this, "GET", url, null, true); - - throw e; - }); - - const data = await response.json().catch(e => { - this.error = { - code: 'Malformed response', - message: "Malformed server response. Please contact support.", - trace: null - } - this.loading = false; - - console.error(this.error); - const errorStore = useErrorStore(); - void errorStore.addError(this.error, { store: this, request: request}); - - throw e; - }); - - if (!response.ok) { - this.error = {code: data.error.code, title: data.error.title, message: data.error.message, trace: data.error.trace }; - this.loading = false; - - console.error(this.error); - const errorStore = useErrorStore(); - void errorStore.addError(this.error, { store: this, request: request}); - - return; - } + this.pagination = { + page: parseInt(headers.get('X-Current-Page')), + pageCount: parseInt(headers.get('X-Page-Count')), + totalCount: parseInt(headers.get('X-Total-Count')) + }; this.loading = false; this.empty = data.length === 0; diff --git a/src/frontend/src/store/matrixRate.js b/src/frontend/src/store/matrixRate.js new file mode 100644 index 0000000..3366733 --- /dev/null +++ b/src/frontend/src/store/matrixRate.js @@ -0,0 +1,60 @@ +import {defineStore} from 'pinia' +import {config} from '@/config' +import {useErrorStore} from "@/store/error.js"; +import performRequest from "@/backend.js"; + +export const useMatrixRateStore = defineStore('matrixRate', { + state() { + return { + rates: [], + loading: false, + empty: true, + error: null, + query: {}, + pagination: {} + } + }, + getters: { + isLoading(state) { + return state.loading; + }, + getRates(state) { + return state.rates; + }, + getPagination(state) { + return state.pagination; + } + }, + actions: { + async setQuery(query = {}) { + this.query = query; + await this.updateMatrixRates(); + }, + async updateMatrixRates() { + this.loading = true; + + const params = new URLSearchParams(); + if (this.query?.searchTerm && this.query.searchTerm !== '') + params.append('filter', this.query.searchTerm); + if(this.query?.periodId) + params.append('valid', this.query.periodId); + + if(this.query?.page) + params.append('page', this.query.page); + + if(this.query?.pageSize) + params.append('limit', this.query.pageSize); + + const url = `${config.backendUrl}/rates/matrix/${params.size === 0 ? '' : '?'}${params.toString()}`; + + const {data: data, headers: headers} = await performRequest(this, "GET", url, null); + + this.rates = data; + this.pagination = { page: parseInt(headers.get('X-Current-Page')), pageCount: parseInt(headers.get('X-Page-Count')), totalCount: parseInt(headers.get('X-Total-Count'))}; + + this.loading = false; + + } + } + +}); \ No newline at end of file diff --git a/src/frontend/src/store/node.js b/src/frontend/src/store/node.js index 953f106..1682807 100644 --- a/src/frontend/src/store/node.js +++ b/src/frontend/src/store/node.js @@ -1,6 +1,7 @@ import {defineStore} from 'pinia' import {config} from '@/config' import {useErrorStore} from "@/store/error.js"; +import performRequest from "@/backend.js"; export const useNodeStore = defineStore('node', { @@ -18,8 +19,14 @@ export const useNodeStore = defineStore('node', { } }, actions: { + async setSearch(query) { + this.query = query; + this.query.type = 'search'; + await this.loadNodes(); + }, async setQuery(query) { this.query = query; + this.query.type = 'list'; await this.loadNodes(); }, async loadNodes() { @@ -35,47 +42,27 @@ export const useNodeStore = defineStore('node', { if (this.query.includeUserNode) params.append('include_user_node', this.query.includeUserNode); - const url = `${config.backendUrl}/nodes/search/${params.size === 0 ? '' : '?'}${params.toString()}`; + if(this.query?.page) + params.append('page', this.query.page); - const request = { url: url, params: {method: 'GET'}}; + if(this.query?.pageSize) + params.append('limit', this.query.pageSize); - console.log(url) - const response = await fetch(url).catch(e => { - this.error = {code: 'Network error.', message: "Please check your internet connection.", trace: null} - this.loading = false; - console.error(this.error); - const errorStore = useErrorStore(); - void errorStore.addError(this.error, { store: this, request: request}); + const endpoint = this.query.type === 'list' ? '/nodes' : '/nodes/search' + const url = `${config.backendUrl}${endpoint}/${params.size === 0 ? '' : '?'}${params.toString()}`; - throw e; - }); - const data = await response.json().catch(e => { - this.error = { - code: 'Malformed response', - message: "Malformed server response. Please contact support.", - trace: null - } - this.loading = false; + const {data: data, headers: headers} = await performRequest(this, "GET", url, null, true); - console.error(this.error); - const errorStore = useErrorStore(); - void errorStore.addError(this.error, { store: this, request: request}); - throw e; - }); + if (this.query.type === 'list') + this.pagination = { + page: parseInt(headers.get('X-Current-Page')), + pageCount: parseInt(headers.get('X-Page-Count')), + totalCount: parseInt(headers.get('X-Total-Count')) + }; - if (!response.ok) { - this.error = {code: data.error.code, title: data.error.title, message: data.error.message, trace: data.error.trace }; - this.loading = false; - - console.error(this.error); - const errorStore = useErrorStore(); - void errorStore.addError(this.error, { store: this, request: request}); - - return; - } this.loading = false; this.empty = data.length === 0; diff --git a/src/frontend/src/store/premiseEdit.js b/src/frontend/src/store/premiseEdit.js index 3e599d1..ac4cfa4 100644 --- a/src/frontend/src/store/premiseEdit.js +++ b/src/frontend/src/store/premiseEdit.js @@ -396,10 +396,10 @@ export const usePremiseEditStore = defineStore('premiseEdit', { const body = {destinations: destinations, premise_id: this.destinations.premise_ids}; const url = `${config.backendUrl}/calculation/destination/`; - const data = await performRequest(this,'PUT', url, body).catch(e => { + const { data: data, headers: headers } = await performRequest(this,'PUT', url, body).catch(e => { this.destinations = null; this.processDestinationMassEdit = false; - }) + }); if (data) { for (const id of Object.keys(data)) { @@ -605,7 +605,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', { const url = `${config.backendUrl}/calculation/destination/`; - const destinations = await performRequest(this,'POST', url, body).catch(e => { + const {data: destinations } = await performRequest(this,'POST', url, body).catch(e => { this.loading = false; this.selectedLoading = false; throw e; @@ -666,7 +666,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', { body.premise_id = toBeUpdated; logger.info(url, body) - const data = await performRequest(this,'PUT', url, body).catch(e => { + const {data: data} = await performRequest(this,'PUT', url, body).catch(e => { this.loading = false; }); @@ -835,10 +835,11 @@ export const usePremiseEditStore = defineStore('premiseEdit', { params.append('premissIds', `${[id]}`); const url = `${config.backendUrl}/calculation/edit/${params.size === 0 ? '' : '?'}${params.toString()}`; - this.premisses = await performRequest(this,'GET', url, null).catch(e => { + const { data: data, headers: headers } = await performRequest(this,'GET', url, null).catch(e => { this.selectedLoading = false; this.loading = false; }); + this.premisses = data; this.premisses.forEach(p => p.selected = true); this.prepareDestinations(id, [id]); @@ -861,9 +862,10 @@ export const usePremiseEditStore = defineStore('premiseEdit', { params.append('premissIds', ids.join(', ')); const url = `${config.backendUrl}/calculation/edit/${params.size === 0 ? '' : '?'}${params.toString()}`; - this.premisses = await performRequest(this,'GET', url, null).catch(e => { + const { data: data, headers: headers } = await performRequest(this,'GET', url, null).catch(e => { this.loading = false; }); + this.premisses = data; this.premisses.forEach(p => p.selected = false); this.loading = false; diff --git a/src/frontend/src/store/reports.js b/src/frontend/src/store/reports.js index 2ac5a8f..7ed7369 100644 --- a/src/frontend/src/store/reports.js +++ b/src/frontend/src/store/reports.js @@ -1,6 +1,6 @@ import {defineStore} from 'pinia' import {config} from '@/config' -import performRequest from '@/backend.js' +import performRequest, {performDownload} from '@/backend.js' export const useReportsStore = defineStore('reports', { @@ -9,6 +9,8 @@ export const useReportsStore = defineStore('reports', { reports: [], showComparableWarning: false, loading: false, + materialId: null, + supplierIds: [], } }, getters: { @@ -28,6 +30,19 @@ export const useReportsStore = defineStore('reports', { } }, actions: { + async downloadReport() { + + if(this.materialId === null && this.supplierIds?.length === 0) + return; + + const params = new URLSearchParams(); + params.append('material', this.materialId); + params.append('sources', this.supplierIds); + + const url = `${config.backendUrl}/reports/download/${params.size === 0 ? '' : '?'}${params.toString()}`; + const fileName = `report_${this.materialId}_${this.supplierIds.join('_')}.xlsx`; + await performDownload(url,fileName); + }, reset() { this.reports = []; }, @@ -37,15 +52,19 @@ export const useReportsStore = defineStore('reports', { this.loading = true; this.reports = []; + this.materialId = materialId; + this.supplierIds = supplierIds; + 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 performRequest(this, 'GET', url, null).catch(e => { + const {data: data} = await performRequest(this, 'GET', url, null).catch(e => { this.loading = false; }); + this.reports = data; this.showComparableWarning = false; diff --git a/src/frontend/src/store/validityPeriod.js b/src/frontend/src/store/validityPeriod.js new file mode 100644 index 0000000..64d8782 --- /dev/null +++ b/src/frontend/src/store/validityPeriod.js @@ -0,0 +1,157 @@ +import {defineStore} from 'pinia' +import {config} from '@/config' +import {useErrorStore} from "@/store/error.js"; +import { useStageStore } from './stage.js' + +export const useValidityPeriodStore = defineStore('validityPeriod', { + state() { + return { + periods: null, + selectedPeriod: null, + } + }, + getters: { + getPeriods(state) { + return state.periods; + }, + getCurrentPeriodId(state) { + if (state.periods === null) + return null; + + for (const period of state.periods) { + if (period.state === "DRAFT") + return period.id; + } + + for (const period of state.periods) { + if (period.state === "VALID") + return period.id; + } + }, + getSelectedPeriod(state) { + return state.selectedPeriod; + }, + getPeriodState(state) { + return function(periodId) { + if (state.periods === null) + return null; + + return state.periods.find(p => p.id === periodId)?.state; + } + } + }, + actions: { + setSelectedPeriod(periodId) { + this.selectedPeriod = periodId; + }, + async invalidate() { + + const url = `${config.backendUrl}/rates/periods/${this.getSelectedPeriod}/`; + this.periods = await this.performRequest('DELETE', url, null, false); + + await this.reload(); + + }, + async loadPeriods() { + this.loading = true; + const url = `${config.backendUrl}/rates/periods`; + this.periods = await this.performRequest('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; + } + } +}); \ No newline at end of file diff --git a/src/main/java/de/avatic/lcc/config/CorsConfig.java b/src/main/java/de/avatic/lcc/config/CorsConfig.java index b895d56..efe20c8 100644 --- a/src/main/java/de/avatic/lcc/config/CorsConfig.java +++ b/src/main/java/de/avatic/lcc/config/CorsConfig.java @@ -4,6 +4,8 @@ import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.core.env.Environment; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @@ -13,6 +15,7 @@ import java.util.Arrays; @Configuration @EnableWebMvc +@Order(Ordered.HIGHEST_PRECEDENCE) public class CorsConfig implements WebMvcConfigurer { @Autowired @@ -25,19 +28,27 @@ public class CorsConfig implements WebMvcConfigurer { public void addCorsMappings(@NotNull CorsRegistry registry) { String[] activeProfiles = environment.getActiveProfiles(); + System.out.println("Active profiles: " + Arrays.toString(activeProfiles)); + System.out.println("Allowed CORS: " + allowedCors); + if (Arrays.asList(activeProfiles).contains("dev")) { + + System.out.println("Applying DEV CORS configuration"); + // Development CORS configuration registry.addMapping("/api/**") .allowedOriginPatterns("http://localhost:*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") - .allowCredentials(true); + .exposedHeaders("X-Total-Count", "X-Page-Count", "X-Current-Page") + .allowCredentials(false); } else { // Production CORS configuration registry.addMapping("/api/**") .allowedOrigins(allowedCors) .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") + .exposedHeaders("X-Total-Count", "X-Page-Count", "X-Current-Page") .allowCredentials(true); } } diff --git a/src/main/java/de/avatic/lcc/config/CorsFilter.java b/src/main/java/de/avatic/lcc/config/CorsFilter.java index 08c0e56..d614bdc 100644 --- a/src/main/java/de/avatic/lcc/config/CorsFilter.java +++ b/src/main/java/de/avatic/lcc/config/CorsFilter.java @@ -18,29 +18,35 @@ public class CorsFilter implements Filter { private String allowedOrigins; @Override - public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) - throws IOException, ServletException { - - HttpServletRequest request = (HttpServletRequest) req; - HttpServletResponse response = (HttpServletResponse) res; - - String origin = request.getHeader("Origin"); - - // Check if it's a development environment and localhost - if (origin != null && origin.startsWith("http://localhost:")) { - response.setHeader("Access-Control-Allow-Origin", origin); - response.setHeader("Access-Control-Allow-Credentials", "true"); - response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); - response.setHeader("Access-Control-Allow-Headers", "*"); - response.setHeader("Access-Control-Max-Age", "3600"); - } - - // Handle preflight OPTIONS requests - if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { - response.setStatus(HttpServletResponse.SC_OK); - return; - } - - chain.doFilter(req, res); + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + filterChain.doFilter(servletRequest, servletResponse); } + +// @Override +// public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) +// throws IOException, ServletException { +// +// HttpServletRequest request = (HttpServletRequest) req; +// HttpServletResponse response = (HttpServletResponse) res; +// +// String origin = request.getHeader("Origin"); +// +// // Check if it's a development environment and localhost +// if (origin != null && origin.startsWith("http://localhost:")) { +// response.setHeader("Access-Control-Allow-Origin", origin); +// response.setHeader("Access-Control-Allow-Credentials", "true"); +// response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); +// response.setHeader("Access-Control-Allow-Headers", "*"); +// response.setHeader("Access-Control-Expose-Headers", "X-Total-Count, X-Page-Count, X-Current-Page"); +// response.setHeader("Access-Control-Max-Age", "3600"); +// } +// +// // Handle preflight OPTIONS requests +// if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { +// response.setStatus(HttpServletResponse.SC_OK); +// return; +// } +// +// chain.doFilter(req, res); +// } } \ No newline at end of file diff --git a/src/main/java/de/avatic/lcc/config/SecurityConfig.java b/src/main/java/de/avatic/lcc/config/SecurityConfig.java index b990b08..a3b091d 100644 --- a/src/main/java/de/avatic/lcc/config/SecurityConfig.java +++ b/src/main/java/de/avatic/lcc/config/SecurityConfig.java @@ -6,10 +6,17 @@ import org.springframework.context.annotation.Profile; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; +import java.util.List; @Configuration public class SecurityConfig { + @Bean @Profile("!dev & !test") // Only active when NOT in dev profile public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { diff --git a/src/main/java/de/avatic/lcc/controller/configuration/MaterialController.java b/src/main/java/de/avatic/lcc/controller/configuration/MaterialController.java index e97be42..107be63 100644 --- a/src/main/java/de/avatic/lcc/controller/configuration/MaterialController.java +++ b/src/main/java/de/avatic/lcc/controller/configuration/MaterialController.java @@ -37,11 +37,12 @@ public class MaterialController { */ @GetMapping("/") public ResponseEntity> listMaterials( + @RequestParam(defaultValue = "true") String excludeDeprecated, @RequestParam(defaultValue = "20") @Min(1) int limit, @RequestParam(defaultValue = "1") @Min(1) int page, @RequestParam(required = false) Optional filter) { - SearchQueryResult materials = materialService.listMaterial(filter, page, limit); + SearchQueryResult materials = materialService.listMaterial(filter, page, limit,Boolean.parseBoolean(excludeDeprecated)); return ResponseEntity.ok() .header("X-Total-Count", String.valueOf(materials.getTotalElements())) diff --git a/src/main/java/de/avatic/lcc/controller/configuration/NodeController.java b/src/main/java/de/avatic/lcc/controller/configuration/NodeController.java index 443e61c..df227b8 100644 --- a/src/main/java/de/avatic/lcc/controller/configuration/NodeController.java +++ b/src/main/java/de/avatic/lcc/controller/configuration/NodeController.java @@ -33,7 +33,7 @@ public class NodeController { this.userNodeService = userNodeService; } - @GetMapping("/") + @GetMapping({"","/"}) public ResponseEntity> listNodes(@RequestParam(required = false) String filter, @RequestParam(defaultValue = "1") @Min(1) Integer page, @RequestParam(defaultValue = "20") @Min(1) Integer limit) { nodeService.listNodes(filter, page, limit); 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 a6100f0..27bd6b7 100644 --- a/src/main/java/de/avatic/lcc/controller/configuration/RateController.java +++ b/src/main/java/de/avatic/lcc/controller/configuration/RateController.java @@ -51,8 +51,9 @@ public class RateController { * @param validAt the specific date and time to filter container rates, optional * @return a ResponseEntity containing the list of container rates and additional pagination headers */ - @GetMapping("/container") + @GetMapping({"/container", "/container/" }) public ResponseEntity> listContainerRates( + @RequestParam(defaultValue = "") String filter, @RequestParam(defaultValue = "20") @Min(1) int limit, @RequestParam(defaultValue = "1") @Min(1) int page, @RequestParam(name= "valid", required = false) Integer validityPeriodId, @@ -61,13 +62,13 @@ public class RateController { SearchQueryResult containerRates = null; if(validAt != null) { - containerRates = containerRateService.listRates(limit, page, validAt); + containerRates = containerRateService.listRates(filter, limit, page, validAt); } else if(validityPeriodId != null) { - containerRates = containerRateService.listRates(limit, page, validityPeriodId); + containerRates = containerRateService.listRates(filter, limit, page, validityPeriodId); } else { - containerRates = containerRateService.listRates(limit, page); + containerRates = containerRateService.listRates(filter, limit, page); } return ResponseEntity.ok() @@ -83,7 +84,7 @@ public class RateController { * @param id the unique identifier of the container whose rate information is to be retrieved * @return a ResponseEntity containing the ContainerRateDTO with the rate information of the specified container */ - @GetMapping("/container/{id}") + @GetMapping({"/container/{id}", "/container/{id}/"}) public ResponseEntity getContainerRate(@PathVariable Integer id) { return ResponseEntity.ok(containerRateService.getContainerRate(id)); } @@ -98,8 +99,9 @@ public class RateController { * @return a {@link ResponseEntity} containing a list of {@link MatrixRateDTO} wrapped in the response body, * including pagination headers. */ - @GetMapping("/matrix") + @GetMapping({"/matrix","/matrix/"}) public ResponseEntity> listMatrixRates( + @RequestParam(defaultValue = "") String filter, @RequestParam(defaultValue = "20") @Min(1) int limit, @RequestParam(defaultValue = "1") @Min(1) int page, @RequestParam(required = false) Integer valid, @@ -108,13 +110,13 @@ public class RateController { SearchQueryResult rates = null; if(validAt != null) { - rates = matrixRateService.listRates(limit, page, validAt); + rates = matrixRateService.listRates(filter, limit, page, validAt); } else if(valid != null) { - rates = matrixRateService.listRates(limit, page, valid); + rates = matrixRateService.listRates(filter, limit, page, valid); } else { - rates = matrixRateService.listRates(limit, page); + rates = matrixRateService.listRates(filter, limit, page); } return ResponseEntity.ok() @@ -130,7 +132,7 @@ public class RateController { * @param id the unique identifier of the matrix rate to retrieve * @return a ResponseEntity containing the MatrixRateDTO for the specified ID */ - @GetMapping("/matrix/{id}") + @GetMapping({"/matrix/{id}", "/matrix/{id}/"}) public ResponseEntity getMatrixRate(@PathVariable Integer id) { return ResponseEntity.ok(matrixRateService.getRate(id)); } @@ -140,7 +142,7 @@ public class RateController { * * @return ResponseEntity containing the list of ValidityPeriodDTO objects. */ - @GetMapping("/periods") + @GetMapping({"/periods", "/periods/"}) public ResponseEntity> listPeriods() { return ResponseEntity.ok(validityPeriodService.listPeriods()); } @@ -151,7 +153,7 @@ public class RateController { * @param id The ID of the validity period to invalidate. * @return ResponseEntity indicating the operation status. */ - @DeleteMapping("/periods/{id}") + @DeleteMapping({"/periods/{id}", "/periods/{id}/"}) public ResponseEntity invalidatePeriod(@PathVariable Integer id) { validityPeriodService.invalidate(id); return ResponseEntity.ok().build(); @@ -163,7 +165,7 @@ public class RateController { * @return ResponseEntity containing a Boolean value indicating * whether rate drafts exist (true) or not (false). */ - @GetMapping("/staged_changes") + @GetMapping( {"/staged_changes", "/staged_changes/"}) public ResponseEntity checkRateDrafts() { return ResponseEntity.ok(rateApprovalService.hasRateDrafts()); } @@ -173,7 +175,7 @@ public class RateController { * * @return ResponseEntity with HTTP 200 status if the operation is successful. */ - @PutMapping("/staged_changes") + @PutMapping({"/staged_changes", "/staged_changes/"}) public ResponseEntity approveRateDrafts() { rateApprovalService.approveRateDrafts(); return ResponseEntity.ok().build(); diff --git a/src/main/java/de/avatic/lcc/controller/report/ReportingController.java b/src/main/java/de/avatic/lcc/controller/report/ReportingController.java index b16ff93..316a823 100644 --- a/src/main/java/de/avatic/lcc/controller/report/ReportingController.java +++ b/src/main/java/de/avatic/lcc/controller/report/ReportingController.java @@ -67,7 +67,7 @@ public class ReportingController { * @param nodeIds A list of node IDs (sources) to include in the downloaded report. * @return The Excel file as an attachment in the response. */ - @GetMapping("/download") + @GetMapping({"/download","/download/"}) public ResponseEntity downloadReport(@RequestParam(value = "material") Integer materialId, @RequestParam(value = "sources") List nodeIds) { HttpHeaders headers = new HttpHeaders(); diff --git a/src/main/java/de/avatic/lcc/dto/configuration/matrixrates/MatrixRateDTO.java b/src/main/java/de/avatic/lcc/dto/configuration/matrixrates/MatrixRateDTO.java index 0071076..9ab5535 100644 --- a/src/main/java/de/avatic/lcc/dto/configuration/matrixrates/MatrixRateDTO.java +++ b/src/main/java/de/avatic/lcc/dto/configuration/matrixrates/MatrixRateDTO.java @@ -1,11 +1,11 @@ package de.avatic.lcc.dto.configuration.matrixrates; -import de.avatic.lcc.dto.generic.NodeDTO; +import de.avatic.lcc.dto.generic.CountryDTO; public class MatrixRateDTO { private Integer id; - private NodeDTO origin; - private NodeDTO destination; + private CountryDTO source; + private CountryDTO destination; private Number rate; public Integer getId() { @@ -16,22 +16,6 @@ public class MatrixRateDTO { this.id = id; } - public NodeDTO getOrigin() { - return origin; - } - - public void setOrigin(NodeDTO origin) { - this.origin = origin; - } - - public NodeDTO getDestination() { - return destination; - } - - public void setDestination(NodeDTO destination) { - this.destination = destination; - } - public Number getRate() { return rate; } @@ -39,4 +23,20 @@ public class MatrixRateDTO { public void setRate(Number rate) { this.rate = rate; } + + public CountryDTO getSource() { + return source; + } + + public void setSource(CountryDTO source) { + this.source = source; + } + + public CountryDTO getDestination() { + return destination; + } + + public void setDestination(CountryDTO destination) { + this.destination = destination; + } } diff --git a/src/main/java/de/avatic/lcc/dto/configuration/rates/ContainerRateDTO.java b/src/main/java/de/avatic/lcc/dto/configuration/rates/ContainerRateDTO.java index 486901f..09274ef 100644 --- a/src/main/java/de/avatic/lcc/dto/configuration/rates/ContainerRateDTO.java +++ b/src/main/java/de/avatic/lcc/dto/configuration/rates/ContainerRateDTO.java @@ -11,7 +11,7 @@ public class ContainerRateDTO { private Integer id; - private NodeDTO origin; + private NodeDTO source; private NodeDTO destination; @@ -33,12 +33,12 @@ public class ContainerRateDTO { this.id = id; } - public NodeDTO getOrigin() { - return origin; + public NodeDTO getSource() { + return source; } - public void setOrigin(NodeDTO origin) { - this.origin = origin; + public void setSource(NodeDTO source) { + this.source = source; } public NodeDTO getDestination() { diff --git a/src/main/java/de/avatic/lcc/dto/generic/MaterialDTO.java b/src/main/java/de/avatic/lcc/dto/generic/MaterialDTO.java index 10c256f..e35936e 100644 --- a/src/main/java/de/avatic/lcc/dto/generic/MaterialDTO.java +++ b/src/main/java/de/avatic/lcc/dto/generic/MaterialDTO.java @@ -1,6 +1,7 @@ package de.avatic.lcc.dto.generic; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Objects; @@ -15,6 +16,9 @@ public class MaterialDTO { @JsonProperty("hs_code") private String hsCode; + @JsonProperty("is_deprecated") + private Boolean isDeprecated; + public MaterialDTO() { } @@ -57,6 +61,15 @@ public class MaterialDTO { this.name = name; } + @JsonIgnore + public Boolean getDeprecated() { + return isDeprecated; + } + + public void setDeprecated(Boolean deprecated) { + isDeprecated = deprecated; + } + @Override public String toString() { return "MaterialSummaryDTO{" + diff --git a/src/main/java/de/avatic/lcc/model/bulk/NodeHeader.java b/src/main/java/de/avatic/lcc/model/bulk/NodeHeader.java index 0f0fc9c..841299e 100644 --- a/src/main/java/de/avatic/lcc/model/bulk/NodeHeader.java +++ b/src/main/java/de/avatic/lcc/model/bulk/NodeHeader.java @@ -3,7 +3,7 @@ package de.avatic.lcc.model.bulk; public enum NodeHeader implements HeaderProvider { MAPPING_ID("Mapping ID"), NAME("Name"), ADDRESS("Address"), COUNTRY("Country (ISO 3166-1)"), GEO_LATITUDE("Latitude"), GEO_LONGITUDE("Longitude"), - IS_ORIGIN("Origin"), IS_INTERMEDIATE("Intermediate"), IS_DESTINATION("Destination"), + IS_SOURCE("Source"), IS_INTERMEDIATE("Intermediate"), IS_DESTINATION("Destination"), OUTBOUND_COUNTRIES("Outbound countries (ISO 3166-1)"), PREDECESSOR_NODES("Predecessor Nodes (Mapping ID)"), IS_PREDECESSOR_MANDATORY("Predecessors mandatory"); diff --git a/src/main/java/de/avatic/lcc/model/utils/DimensionUnit.java b/src/main/java/de/avatic/lcc/model/utils/DimensionUnit.java index a4e468c..3de5372 100644 --- a/src/main/java/de/avatic/lcc/model/utils/DimensionUnit.java +++ b/src/main/java/de/avatic/lcc/model/utils/DimensionUnit.java @@ -45,7 +45,7 @@ public enum DimensionUnit { } throw new IllegalArgumentException("Unknown DimensionUnit: " + value + - ". Valid values are: t, kg, g (case insensitive)"); + ". Valid values are: mm, cm, m (case insensitive)"); } diff --git a/src/main/java/de/avatic/lcc/repositories/MaterialRepository.java b/src/main/java/de/avatic/lcc/repositories/MaterialRepository.java index abadd15..eef8059 100644 --- a/src/main/java/de/avatic/lcc/repositories/MaterialRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/MaterialRepository.java @@ -106,6 +106,18 @@ public class MaterialRepository { return new SearchQueryResult<>(materials, pagination.getPage(), totalCount, pagination.getLimit()); } + @Transactional + public Optional getByIdIncludeDeprecated(Integer id) { + String query = "SELECT * FROM material WHERE id = ?"; + + var material = jdbcTemplate.query(query, new MaterialMapper(), id); + + if(material.isEmpty()) + return Optional.empty(); + + return Optional.ofNullable(material.getFirst()); + } + @Transactional public Optional getById(Integer id) { String query = "SELECT * FROM material WHERE id = ? AND is_deprecated = FALSE"; 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 d05969e..89d643a 100644 --- a/src/main/java/de/avatic/lcc/repositories/rates/ContainerRateRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/rates/ContainerRateRepository.java @@ -26,12 +26,62 @@ public class ContainerRateRepository { this.jdbcTemplate = jdbcTemplate; } - public SearchQueryResult listRatesByPeriodId(SearchQueryPagination pagination, Integer periodId) { - String query = "SELECT * FROM container_rate WHERE validity_period_id = ? ORDER BY id LIMIT ? OFFSET ?"; - String countQuery = "SELECT COUNT(*) FROM container_rate WHERE validity_period_id = ?"; - Integer totalCount = jdbcTemplate.queryForObject(countQuery, Integer.class, periodId); + /** + * Retrieves a paginated list of {@link ContainerRate} entries filtered by a specific validity period ID. + * Optionally filters results by searching node names and external mapping IDs for both source and destination nodes. + * + * @param filter optional search term to filter by node names or external mapping IDs (case-insensitive partial match). + * Searches across from_node and to_node names and external_mapping_ids. Can be null or empty. + * @param pagination the {@link SearchQueryPagination} object containing limit, offset, and page details + * @param periodId the ID of the validity period to filter the rates by + * @return a {@link SearchQueryResult} containing a list of filtered {@link ContainerRate} entities, + * total count, and pagination details + */ + public SearchQueryResult listRatesByPeriodId(String filter, SearchQueryPagination pagination, Integer periodId) { + StringBuilder queryBuilder = new StringBuilder(); + StringBuilder countQueryBuilder = new StringBuilder(); + List params = new ArrayList<>(); + List countParams = new ArrayList<>(); - return new SearchQueryResult<>(jdbcTemplate.query(query, new ContainerRateMapper(), periodId, pagination.getLimit(), pagination.getOffset()), pagination.getPage(), totalCount, pagination.getLimit()); + // Base query with node joins + String baseQuery = """ + FROM container_rate cr + JOIN node fn ON cr.from_node_id = fn.id + JOIN node tn ON cr.to_node_id = tn.id + WHERE cr.validity_period_id = ? + """; + + queryBuilder.append("SELECT cr.* ").append(baseQuery); + countQueryBuilder.append("SELECT COUNT(*) ").append(baseQuery); + + params.add(periodId); + countParams.add(periodId); + + // Add filter conditions if filter is provided + if (filter != null && !filter.trim().isEmpty()) { + String filterCondition = """ + AND (fn.name LIKE ? OR fn.external_mapping_id LIKE ? + OR tn.name LIKE ? OR tn.external_mapping_id LIKE ?) + """; + + queryBuilder.append(filterCondition); + countQueryBuilder.append(filterCondition); + + String filterParam = "%" + filter.trim() + "%"; + for (int i = 0; i < 4; i++) { + params.add(filterParam); + countParams.add(filterParam); + } + } + + queryBuilder.append(" ORDER BY cr.id LIMIT ? OFFSET ?"); + params.add(pagination.getLimit()); + params.add(pagination.getOffset()); + + Integer totalCount = jdbcTemplate.queryForObject(countQueryBuilder.toString(), Integer.class, countParams.toArray()); + var results = jdbcTemplate.query(queryBuilder.toString(), new ContainerRateMapper(), params.toArray()); + + return new SearchQueryResult<>(results, pagination.getPage(), totalCount, pagination.getLimit()); } public Optional getById(Integer id) { @@ -48,7 +98,7 @@ public class ContainerRateRepository { public List listAllRatesByPeriodId(Integer periodId) { String query = "SELECT * FROM container_rate WHERE validity_period_id = ?"; - return jdbcTemplate.query(query, new ContainerRateMapper()); + return jdbcTemplate.query(query, new ContainerRateMapper(), periodId); } @Transactional 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 1ecd8b1..c91e327 100644 --- a/src/main/java/de/avatic/lcc/repositories/rates/MatrixRateRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/rates/MatrixRateRepository.java @@ -10,6 +10,7 @@ import org.springframework.transaction.annotation.Transactional; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -47,17 +48,61 @@ public class MatrixRateRepository { /** * Retrieves a paginated list of {@link MatrixRate} entries filtered by a specific validity period ID. + * Optionally filters results by searching country names and ISO codes for both source and destination countries. * + * @param filter optional search term to filter by country names or ISO codes (case-insensitive partial match). + * Searches across from_country and to_country names and iso_codes. Can be null or empty. * @param pagination the {@link SearchQueryPagination} object containing limit, offset, and page details * @param periodId the ID of the validity period to filter the rates by * @return a {@link SearchQueryResult} containing a list of filtered {@link MatrixRate} entities, - * total count, and pagination details + * total count, and pagination details */ @Transactional - public SearchQueryResult listRatesByPeriodId(SearchQueryPagination pagination, Integer periodId) { - String query = "SELECT * FROM country_matrix_rate WHERE validity_period_id = ? ORDER BY id LIMIT ? OFFSET ?"; - var totalCount = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM country_matrix_rate WHERE validity_period_id = ?", Integer.class, periodId); - return new SearchQueryResult<>(jdbcTemplate.query(query, new MatrixRateMapper(), periodId, pagination.getLimit(), pagination.getOffset()), pagination.getPage(), totalCount, pagination.getLimit()); + public SearchQueryResult listRatesByPeriodId(String filter, SearchQueryPagination pagination, Integer periodId) { + StringBuilder queryBuilder = new StringBuilder(); + StringBuilder countQueryBuilder = new StringBuilder(); + List params = new ArrayList<>(); + List countParams = new ArrayList<>(); + + // Base query with country joins + String baseQuery = """ + FROM country_matrix_rate cmr + JOIN country fc ON cmr.from_country_id = fc.id + JOIN country tc ON cmr.to_country_id = tc.id + WHERE cmr.validity_period_id = ? + """; + + queryBuilder.append("SELECT cmr.* ").append(baseQuery); + countQueryBuilder.append("SELECT COUNT(*) ").append(baseQuery); + + params.add(periodId); + countParams.add(periodId); + + // Add filter conditions if filter is provided + if (filter != null && !filter.trim().isEmpty()) { + String filterCondition = """ + AND (fc.name LIKE ? OR fc.iso_code LIKE ? + OR tc.name LIKE ? OR tc.iso_code LIKE ?) + """; + + queryBuilder.append(filterCondition); + countQueryBuilder.append(filterCondition); + + String filterParam = "%" + filter.trim() + "%"; + for (int i = 0; i < 4; i++) { + params.add(filterParam); + countParams.add(filterParam); + } + } + + queryBuilder.append(" ORDER BY cmr.id LIMIT ? OFFSET ?"); + params.add(pagination.getLimit()); + params.add(pagination.getOffset()); + + var totalCount = jdbcTemplate.queryForObject(countQueryBuilder.toString(), Integer.class, countParams.toArray()); + var results = jdbcTemplate.query(queryBuilder.toString(), new MatrixRateMapper(), params.toArray()); + + return new SearchQueryResult<>(results, pagination.getPage(), totalCount, pagination.getLimit()); } /** @@ -75,7 +120,7 @@ public class MatrixRateRepository { @Transactional public List listAllRatesByPeriodId(Integer periodId) { String query = "SELECT * FROM country_matrix_rate WHERE validity_period_id = ?"; - return jdbcTemplate.query(query, new MatrixRateMapper()); + return jdbcTemplate.query(query, new MatrixRateMapper(), periodId); } @Transactional diff --git a/src/main/java/de/avatic/lcc/service/access/ContainerRateService.java b/src/main/java/de/avatic/lcc/service/access/ContainerRateService.java index 937698c..41554c2 100644 --- a/src/main/java/de/avatic/lcc/service/access/ContainerRateService.java +++ b/src/main/java/de/avatic/lcc/service/access/ContainerRateService.java @@ -51,9 +51,9 @@ public class ContainerRateService { * @return a {@link SearchQueryResult} containing container rate DTOs with pagination information */ @Transactional - public SearchQueryResult listRates(int limit, int page, LocalDateTime validAt) { + public SearchQueryResult listRates(String filter, int limit, int page, LocalDateTime validAt) { Optional periodId = validityPeriodRepository.getPeriodId(validAt); - return listRates(limit, page, periodId.orElseThrow()); + return listRates(filter, limit, page, periodId.orElseThrow()); } /** @@ -65,11 +65,11 @@ public class ContainerRateService { * @return a {@link SearchQueryResult} containing container rate DTOs with pagination information */ @Transactional - public SearchQueryResult listRates(int limit, int page, Integer periodId) { + public SearchQueryResult listRates(String filter, int limit, int page, Integer periodId) { if(null == periodId) - return listRates(limit, page); + return listRates(filter,limit, page); - return SearchQueryResult.map(containerRateRepository.listRatesByPeriodId(new SearchQueryPagination(limit, page), periodId), this::toContainerRateDTO); + return SearchQueryResult.map(containerRateRepository.listRatesByPeriodId(filter, new SearchQueryPagination(page,limit), periodId), this::toContainerRateDTO); } /** @@ -81,8 +81,8 @@ public class ContainerRateService { * @return a {@link SearchQueryResult} containing container rate DTOs with pagination information */ @Transactional - public SearchQueryResult listRates(int limit, int page) { - var data = validityPeriodRepository.getValidPeriodId().map(id -> containerRateRepository.listRatesByPeriodId(new SearchQueryPagination(page, limit), id)).orElseThrow(() -> new InternalErrorException("No validity period that is VALID")); + public SearchQueryResult listRates(String filter, int limit, int page) { + var data = validityPeriodRepository.getValidPeriodId().map(id -> containerRateRepository.listRatesByPeriodId(filter, new SearchQueryPagination(page, limit), id)).orElseThrow(() -> new InternalErrorException("No validity period that is VALID")); return SearchQueryResult.map(data, this::toContainerRateDTO); } @@ -111,7 +111,7 @@ public class ContainerRateService { dto.setType(TransportType.valueOf(entity.getType().name())); dto.setValidityPeriod(validityPeriodTransformer.toValidityPeriodDTO(validityPeriodRepository.getById(entity.getValidityPeriodId()))); dto.setLeadTime(entity.getLeadTime()); - dto.setOrigin(nodeTransformer.toNodeDTO(nodeRepository.getById(entity.getFromNodeId()).orElseThrow())); + dto.setSource(nodeTransformer.toNodeDTO(nodeRepository.getById(entity.getFromNodeId()).orElseThrow())); dto.setDestination(nodeTransformer.toNodeDTO(nodeRepository.getById(entity.getToNodeId()).orElseThrow())); var rates = new HashMap(); diff --git a/src/main/java/de/avatic/lcc/service/access/MaterialService.java b/src/main/java/de/avatic/lcc/service/access/MaterialService.java index d4cc71c..be19653 100644 --- a/src/main/java/de/avatic/lcc/service/access/MaterialService.java +++ b/src/main/java/de/avatic/lcc/service/access/MaterialService.java @@ -40,15 +40,17 @@ public class MaterialService { } /** - * Lists materials based on a filter and pagination parameters. + * Lists materials based on the provided criteria, including optional filtering, + * pagination, and exclusion of deprecated entries. * - * @param filter the search filter to apply - * @param page the page number for pagination (zero-based) - * @param limit the maximum number of items per page - * @return a {@link SearchQueryResult} containing a list of material DTOs and pagination details + * @param filter an optional filter to apply to the material list, such as a search term + * @param page the page number for pagination + * @param limit the maximum number of materials to include in the result + * @param excludeDeprecated a flag indicating whether to exclude deprecated materials + * @return a {@code SearchQueryResult} containing the paginated list of materials */ - public SearchQueryResult listMaterial(Optional filter, int page, int limit) { - SearchQueryResult queryResult = materialRepository.listMaterials(filter, true, new SearchQueryPagination(page, limit)); + public SearchQueryResult listMaterial(Optional filter, int page, int limit, boolean excludeDeprecated) { + SearchQueryResult queryResult = materialRepository.listMaterials(filter, excludeDeprecated, new SearchQueryPagination(page, limit)); return SearchQueryResult.map(queryResult, materialTransformer::toMaterialDTO); } @@ -60,7 +62,7 @@ public class MaterialService { * @throws MaterialNotFoundException if no material with the given ID is found */ public MaterialDetailDTO getMaterial(Integer id) { - return materialDetailTransformer.toMaterialDetailDTO(materialRepository.getById(id).orElseThrow(() -> new NotFoundException(NotFoundException.NotFoundType.MATERIAL,id.toString()))); + return materialDetailTransformer.toMaterialDetailDTO(materialRepository.getByIdIncludeDeprecated(id).orElseThrow(() -> new NotFoundException(NotFoundException.NotFoundType.MATERIAL,id.toString()))); } /** diff --git a/src/main/java/de/avatic/lcc/service/access/MatrixRateService.java b/src/main/java/de/avatic/lcc/service/access/MatrixRateService.java index 37b4d03..9e2b975 100644 --- a/src/main/java/de/avatic/lcc/service/access/MatrixRateService.java +++ b/src/main/java/de/avatic/lcc/service/access/MatrixRateService.java @@ -2,12 +2,13 @@ package de.avatic.lcc.service.access; import de.avatic.lcc.dto.configuration.matrixrates.MatrixRateDTO; import de.avatic.lcc.model.rates.MatrixRate; -import de.avatic.lcc.model.rates.ValidityPeriod; import de.avatic.lcc.repositories.NodeRepository; +import de.avatic.lcc.repositories.country.CountryRepository; import de.avatic.lcc.repositories.pagination.SearchQueryPagination; import de.avatic.lcc.repositories.pagination.SearchQueryResult; import de.avatic.lcc.repositories.rates.MatrixRateRepository; import de.avatic.lcc.repositories.rates.ValidityPeriodRepository; +import de.avatic.lcc.service.transformer.generic.CountryTransformer; import de.avatic.lcc.service.transformer.generic.NodeTransformer; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,15 +26,15 @@ public class MatrixRateService { private final ValidityPeriodRepository validityPeriodRepository; private final MatrixRateRepository matrixRateRepository; - private final NodeRepository nodeRepository; - private final NodeTransformer nodeTransformer; + private final CountryRepository countryRepository; + private final CountryTransformer countryTransformer; - public MatrixRateService(ValidityPeriodRepository validityPeriodRepository, MatrixRateRepository matrixRateRepository, NodeRepository nodeRepository, NodeTransformer nodeTransformer) { + public MatrixRateService(ValidityPeriodRepository validityPeriodRepository, MatrixRateRepository matrixRateRepository, CountryRepository countryRepository, CountryTransformer countryTransformer) { this.validityPeriodRepository = validityPeriodRepository; this.matrixRateRepository = matrixRateRepository; - this.nodeRepository = nodeRepository; - this.nodeTransformer = nodeTransformer; + this.countryRepository = countryRepository; + this.countryTransformer = countryTransformer; } /** @@ -45,9 +46,9 @@ public class MatrixRateService { * @return a {@link SearchQueryResult} containing matrix rate DTOs with pagination information */ @Transactional - public SearchQueryResult listRates(int limit, int page, LocalDateTime validAt) { + public SearchQueryResult listRates(String filter, int limit, int page, LocalDateTime validAt) { Optional periodId = validityPeriodRepository.getPeriodId(validAt); - return listRates(limit, page, periodId.orElseThrow()); + return listRates(filter, limit, page, periodId.orElseThrow()); } /** @@ -59,11 +60,11 @@ public class MatrixRateService { * @return a {@link SearchQueryResult} containing matrix rate DTOs with pagination information */ @Transactional - public SearchQueryResult listRates(int limit, int page, Integer periodId) { + public SearchQueryResult listRates(String filter, int limit, int page, Integer periodId) { if (null == periodId) - return listRates(limit, page); + return listRates(filter,limit, page); - return SearchQueryResult.map(matrixRateRepository.listRatesByPeriodId(new SearchQueryPagination(page, limit), periodId), this::toMatrixRateDTO); + return SearchQueryResult.map(matrixRateRepository.listRatesByPeriodId(filter, new SearchQueryPagination(page, limit), periodId), this::toMatrixRateDTO); } /** @@ -74,9 +75,9 @@ public class MatrixRateService { * @return a {@link SearchQueryResult} containing matrix rate DTOs with pagination information */ @Transactional - public SearchQueryResult listRates(int limit, int page) { + public SearchQueryResult listRates(String filter, int limit, int page) { Integer id = validityPeriodRepository.getValidPeriodId().orElseThrow(() -> new IllegalStateException("No valid period found that is VALID")); - return SearchQueryResult.map(matrixRateRepository.listRatesByPeriodId(new SearchQueryPagination(page, limit), id), this::toMatrixRateDTO); + return SearchQueryResult.map(matrixRateRepository.listRatesByPeriodId(filter, new SearchQueryPagination(page, limit), id), this::toMatrixRateDTO); } /** @@ -100,8 +101,8 @@ public class MatrixRateService { MatrixRateDTO dto = new MatrixRateDTO(); dto.setId(entity.getId()); - dto.setOrigin(nodeTransformer.toNodeDTO(nodeRepository.getById(entity.getFromCountry()).orElseThrow())); - dto.setDestination(nodeTransformer.toNodeDTO(nodeRepository.getById(entity.getToCountry()).orElseThrow())); + dto.setSource(countryTransformer.toCountryDTO(countryRepository.getById(entity.getFromCountry()).orElseThrow())); + dto.setDestination(countryTransformer.toCountryDTO(countryRepository.getById(entity.getToCountry()).orElseThrow())); dto.setRate(entity.getRate()); return dto; diff --git a/src/main/java/de/avatic/lcc/service/bulk/BulkExportService.java b/src/main/java/de/avatic/lcc/service/bulk/BulkExportService.java index 83e2ed4..8dfb979 100644 --- a/src/main/java/de/avatic/lcc/service/bulk/BulkExportService.java +++ b/src/main/java/de/avatic/lcc/service/bulk/BulkExportService.java @@ -66,30 +66,35 @@ public class BulkExportService { var hiddenCountrySheet = workbook.createSheet(HiddenTableType.COUNTRY_HIDDEN_TABLE.getSheetName()); hiddenCountryExcelMapper.fillSheet(hiddenCountrySheet, style); hiddenCountrySheet.protectSheet(sheetPassword); - workbook.setSheetVisibility(workbook.getSheetIndex(hiddenCountrySheet), SheetVisibility.VERY_HIDDEN); + workbook.setSheetVisibility(workbook.getSheetIndex(hiddenCountrySheet), SheetVisibility.HIDDEN); } else if (bulkFileType.equals(BulkFileType.CONTAINER_RATE) || bulkFileType.equals(BulkFileType.PACKAGING)) { var hiddenNodeSheet = workbook.createSheet(HiddenTableType.NODE_HIDDEN_TABLE.getSheetName()); hiddenNodeExcelMapper.fillSheet(hiddenNodeSheet, style, BulkFileType.PACKAGING.equals(bulkFileType)); hiddenNodeSheet.protectSheet(sheetPassword); - workbook.setSheetVisibility(workbook.getSheetIndex(hiddenNodeSheet), SheetVisibility.VERY_HIDDEN); + workbook.setSheetVisibility(workbook.getSheetIndex(hiddenNodeSheet), SheetVisibility.HIDDEN); } // Create headers based on the bulk file type switch (bulkFileType) { case CONTAINER_RATE: containerRateExcelMapper.fillSheet(worksheet, style, periodId); + containerRateExcelMapper.createConstraints(workbook, worksheet); break; case COUNTRY_MATRIX: matrixRateExcelMapper.fillSheet(worksheet, style, periodId); + matrixRateExcelMapper.createConstraints(workbook, worksheet); break; case MATERIAL: materialExcelMapper.fillSheet(worksheet, style); + materialExcelMapper.createConstraints(worksheet); break; case PACKAGING: packagingExcelMapper.fillSheet(worksheet, style); + packagingExcelMapper.createConstraints(workbook, worksheet); break; case NODE: nodeExcelMapper.fillSheet(worksheet, style); + nodeExcelMapper.createConstraints(workbook, worksheet); break; } diff --git a/src/main/java/de/avatic/lcc/service/bulk/TemplateExportService.java b/src/main/java/de/avatic/lcc/service/bulk/TemplateExportService.java index d329592..1041b6d 100644 --- a/src/main/java/de/avatic/lcc/service/bulk/TemplateExportService.java +++ b/src/main/java/de/avatic/lcc/service/bulk/TemplateExportService.java @@ -5,7 +5,10 @@ import de.avatic.lcc.model.bulk.*; import de.avatic.lcc.service.bulk.helper.HeaderCellStyleProvider; import de.avatic.lcc.service.bulk.helper.HeaderGenerator; import de.avatic.lcc.service.excelMapper.*; -import org.apache.poi.ss.usermodel.*; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.SheetVisibility; +import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ByteArrayResource; @@ -44,26 +47,24 @@ public class TemplateExportService { } public InputStreamSource generateTemplate(BulkFileType bulkFileType) { - try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { - Workbook workbook = new XSSFWorkbook(); + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Workbook workbook = new XSSFWorkbook();) { + Sheet sheet = workbook.createSheet(BulkFileTypes.valueOf(bulkFileType.name()).getSheetName()); CellStyle style = headerCellStyleProvider.createHeaderCellStyle(workbook); - if(bulkFileType.equals(BulkFileType.COUNTRY_MATRIX) || bulkFileType.equals(BulkFileType.NODE)) - { + if (bulkFileType.equals(BulkFileType.COUNTRY_MATRIX) || bulkFileType.equals(BulkFileType.NODE)) { var hiddenCountrySheet = workbook.createSheet(HiddenTableType.COUNTRY_HIDDEN_TABLE.getSheetName()); hiddenCountryExcelMapper.fillSheet(hiddenCountrySheet, style); hiddenCountrySheet.protectSheet(sheetPassword); - workbook.setSheetVisibility(workbook.getSheetIndex(hiddenCountrySheet), SheetVisibility.VERY_HIDDEN); - } - else if(bulkFileType.equals(BulkFileType.CONTAINER_RATE) || bulkFileType.equals(BulkFileType.PACKAGING)) - { + workbook.setSheetVisibility(workbook.getSheetIndex(hiddenCountrySheet), SheetVisibility.HIDDEN); + } else if (bulkFileType.equals(BulkFileType.CONTAINER_RATE) || bulkFileType.equals(BulkFileType.PACKAGING)) { var hiddenNodeSheet = workbook.createSheet(HiddenTableType.NODE_HIDDEN_TABLE.getSheetName()); hiddenNodeExcelMapper.fillSheet(hiddenNodeSheet, style, BulkFileType.PACKAGING.equals(bulkFileType)); hiddenNodeSheet.protectSheet(sheetPassword); - workbook.setSheetVisibility(workbook.getSheetIndex(hiddenNodeSheet), SheetVisibility.VERY_HIDDEN); + workbook.setSheetVisibility(workbook.getSheetIndex(hiddenNodeSheet), SheetVisibility.HIDDEN); } // Create headers and constraints based on the bulk file type @@ -71,22 +72,27 @@ public class TemplateExportService { case CONTAINER_RATE: headerGenerator.generateHeader(sheet, ContainerRateHeader.class, style); containerRateExcelMapper.createConstraints(workbook, sheet); + headerGenerator.fixWidth(sheet, ContainerRateHeader.class); break; case COUNTRY_MATRIX: headerGenerator.generateHeader(sheet, MatrixRateHeader.class, style); matrixRateExcelMapper.createConstraints(workbook, sheet); + headerGenerator.fixWidth(sheet, MatrixRateHeader.class); break; case MATERIAL: headerGenerator.generateHeader(sheet, MaterialHeader.class, style); materialExcelMapper.createConstraints(sheet); + headerGenerator.fixWidth(sheet, MaterialHeader.class); break; case PACKAGING: headerGenerator.generateHeader(sheet, PackagingHeader.class, style); packagingExcelMapper.createConstraints(workbook, sheet); + headerGenerator.fixWidth(sheet, PackagingHeader.class); break; case NODE: headerGenerator.generateHeader(sheet, NodeHeader.class, style); nodeExcelMapper.createConstraints(workbook, sheet); + headerGenerator.fixWidth(sheet, NodeHeader.class); break; default: throw new IllegalArgumentException("Unsupported bulk file type: " + bulkFileType); diff --git a/src/main/java/de/avatic/lcc/service/bulk/helper/ConstraintGenerator.java b/src/main/java/de/avatic/lcc/service/bulk/helper/ConstraintGenerator.java index b6d153c..0393c28 100644 --- a/src/main/java/de/avatic/lcc/service/bulk/helper/ConstraintGenerator.java +++ b/src/main/java/de/avatic/lcc/service/bulk/helper/ConstraintGenerator.java @@ -10,10 +10,13 @@ import org.apache.poi.ss.util.CellRangeAddressList; import org.springframework.stereotype.Service; import java.util.EnumSet; +import java.util.function.Function; @Service public class ConstraintGenerator { + private static final int MAX_ROWS = 10000; + public void createBooleanConstraint(Sheet sheet, Integer columnIdx) { createConstraint(sheet, columnIdx, new String[]{"true", "false"}); @@ -23,12 +26,25 @@ public class ConstraintGenerator { createConstraint(sheet, columnIdx, EnumSet.allOf(values).stream().map(Enum::name).toArray(String[]::new)); } + public > void createEnumConstraint(Sheet sheet, Integer columnIdx, Class values, Function resolver) { + createConstraint(sheet, columnIdx, EnumSet.allOf(values).stream().map(Enum::name).toArray(String[]::new)); + } + private void createConstraint(Sheet sheet, Integer columnIdx, String[] values) { var helper = sheet.getDataValidationHelper(); var constraint = helper.createExplicitListConstraint(values); - var validation = helper.createValidation(constraint, new CellRangeAddressList(1, SpreadsheetVersion.EXCEL2007.getMaxRows(), columnIdx, columnIdx)); - validation.createErrorBox("Invalid Value", values.length > 8 ? "Please check dropdown for allowed values." : String.format("Allowed values are: %s.", String.join(", ", values))); + + + var validation = helper.createValidation(constraint, new CellRangeAddressList(1, MAX_ROWS, columnIdx, columnIdx)); + + String errorMessage = values.length > 8 ? "Please check dropdown for allowed values." : + String.format("Allowed values: %s", String.join(", ", values)); + if (errorMessage.length() > 200) { + errorMessage = "Please check dropdown for allowed values."; + } + + validation.createErrorBox("Invalid Value", errorMessage); validation.setShowErrorBox(true); sheet.addValidationData(validation); } @@ -37,7 +53,7 @@ public class ConstraintGenerator { var helper = sheet.getDataValidationHelper(); var constraint = helper.createFormulaListConstraint(namedReference); - var validation = helper.createValidation(constraint, new CellRangeAddressList(1, SpreadsheetVersion.EXCEL2007.getMaxRows(), columnIdx, columnIdx)); + var validation = helper.createValidation(constraint, new CellRangeAddressList(1, MAX_ROWS, columnIdx, columnIdx)); validation.createErrorBox("Invalid Value", "Please check dropdown for allowed values."); validation.setShowErrorBox(true); sheet.addValidationData(validation); @@ -47,7 +63,7 @@ public class ConstraintGenerator { var helper = sheet.getDataValidationHelper(); var constraint = helper.createTextLengthConstraint(DataValidationConstraint.OperatorType.BETWEEN, String.valueOf(min), String.valueOf(max)); - var validation = helper.createValidation(constraint, new CellRangeAddressList(1, SpreadsheetVersion.EXCEL2007.getMaxRows(), columnIdx, columnIdx)); + var validation = helper.createValidation(constraint, new CellRangeAddressList(1, MAX_ROWS, columnIdx, columnIdx)); validation.createErrorBox("Invalid Value", String.format("Allowed length is between %d and %d characters.", min, max)); validation.setShowErrorBox(true); sheet.addValidationData(validation); @@ -57,7 +73,7 @@ public class ConstraintGenerator { var helper = sheet.getDataValidationHelper(); var constraint = helper.createDecimalConstraint(DataValidationConstraint.OperatorType.BETWEEN, String.valueOf(min), String.valueOf(max)); - var validation = helper.createValidation(constraint, new CellRangeAddressList(1, SpreadsheetVersion.EXCEL2007.getMaxRows(), columnIdx, columnIdx)); + var validation = helper.createValidation(constraint, new CellRangeAddressList(1, MAX_ROWS, columnIdx, columnIdx)); validation.createErrorBox("Invalid Value", String.format("Allowed value is between %f and %f.", min.floatValue(), max.floatValue())); validation.setShowErrorBox(true); sheet.addValidationData(validation); @@ -67,23 +83,39 @@ public class ConstraintGenerator { var helper = sheet.getDataValidationHelper(); var constraint = helper.createIntegerConstraint(DataValidationConstraint.OperatorType.BETWEEN, String.valueOf(min), String.valueOf(max)); - var validation = helper.createValidation(constraint, new CellRangeAddressList(1, SpreadsheetVersion.EXCEL2007.getMaxRows(), columnIdx, columnIdx)); + var validation = helper.createValidation(constraint, new CellRangeAddressList(1, MAX_ROWS, columnIdx, columnIdx)); validation.createErrorBox("Invalid Value", String.format("Allowed value is between %f and %f.", min.floatValue(), max.floatValue())); validation.setShowErrorBox(true); sheet.addValidationData(validation); } - public Name createReference(Workbook workbook, Integer columnIdx, HiddenTableType hiddenTableType ) { + public Name createReference(Workbook workbook, Integer columnIdx, HiddenTableType hiddenTableType) { + // Check if the referenced sheet exists + Sheet hiddenSheet = workbook.getSheet(hiddenTableType.getSheetName()); + if (hiddenSheet == null) { + throw new IllegalStateException("Hidden sheet " + hiddenTableType.getSheetName() + " does not exist"); + } + Name namedRange = workbook.createName(); namedRange.setNameName(hiddenTableType.getReferenceName()); - String reference = hiddenTableType.getSheetName()+ "!$"+toExcelLetter(columnIdx)+"$1:$"+toExcelLetter(columnIdx)+"$" + SpreadsheetVersion.EXCEL2007.getMaxRows(); + + // Create a more conservative range - only to the last row with data + int lastRow = Math.max(hiddenSheet.getLastRowNum(), 1); // At least row 1 + String columnLetter = toExcelLetter(columnIdx); + String reference = hiddenTableType.getSheetName() + "!$" + columnLetter + "$2:$" + columnLetter + "$" + (lastRow + 1); + namedRange.setRefersToFormula(reference); return namedRange; } private String toExcelLetter(int columnIdx) { - int digit = columnIdx % 26; - int quotient = columnIdx / 26; - return (quotient==0 ? "" : toExcelLetter(quotient)) + (char) ('A' + digit); + 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(); } } diff --git a/src/main/java/de/avatic/lcc/service/bulk/helper/HeaderCellStyleProvider.java b/src/main/java/de/avatic/lcc/service/bulk/helper/HeaderCellStyleProvider.java index 712e362..ca666ca 100644 --- a/src/main/java/de/avatic/lcc/service/bulk/helper/HeaderCellStyleProvider.java +++ b/src/main/java/de/avatic/lcc/service/bulk/helper/HeaderCellStyleProvider.java @@ -1,17 +1,37 @@ package de.avatic.lcc.service.bulk.helper; import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFCellStyle; +import org.apache.poi.xssf.usermodel.XSSFColor; +import org.apache.poi.xssf.usermodel.XSSFFont; import org.springframework.stereotype.Service; @Service public class HeaderCellStyleProvider { public CellStyle createHeaderCellStyle(Workbook workbook) { + + + + XSSFFont headerFont = (XSSFFont) workbook.createFont(); + + XSSFColor customTextColor = new XSSFColor(new byte[]{(byte)0, (byte)47, (byte)84}, null); // Blue + headerFont.setColor(customTextColor); + + headerFont.setFontName("Arial"); + headerFont.setFontHeightInPoints((short)10); + headerFont.setBold(true); + + + + XSSFColor customColor = new XSSFColor(new byte[]{(byte)90, (byte)240, (byte)180}, null); + CellStyle headerStyle = workbook.createCellStyle(); - Font headerFont = workbook.createFont(); headerFont.setBold(true); headerStyle.setFont(headerFont); - headerStyle.setFillForegroundColor(IndexedColors.LIGHT_CORNFLOWER_BLUE.getIndex()); + headerStyle.setFillForegroundColor(customColor); + + headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); headerStyle.setBorderBottom(BorderStyle.THIN); headerStyle.setBorderTop(BorderStyle.THIN); diff --git a/src/main/java/de/avatic/lcc/service/bulk/helper/HeaderGenerator.java b/src/main/java/de/avatic/lcc/service/bulk/helper/HeaderGenerator.java index b165994..633fa91 100644 --- a/src/main/java/de/avatic/lcc/service/bulk/helper/HeaderGenerator.java +++ b/src/main/java/de/avatic/lcc/service/bulk/helper/HeaderGenerator.java @@ -1,6 +1,7 @@ package de.avatic.lcc.service.bulk.helper; import de.avatic.lcc.model.bulk.HeaderProvider; +import de.avatic.lcc.model.bulk.NodeHeader; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.Row; @@ -12,6 +13,8 @@ import java.util.EnumSet; @Service public class HeaderGenerator { + private static final int ADD_COLUMN_SIZE = (10*256); + public & HeaderProvider> boolean validateHeader(Sheet sheet, Class headers) { Row row = sheet.getRow(0); for(H header : EnumSet.allOf(headers)){ @@ -57,4 +60,19 @@ public class HeaderGenerator { } + public void fixWidth(Sheet sheet, String[] headers) { + int idx = 0; + for (String header : headers) { + sheet.autoSizeColumn(idx); + sheet.setColumnWidth(idx,sheet.getColumnWidth(idx)+ADD_COLUMN_SIZE); + idx++; + } + } + + public & HeaderProvider> void fixWidth(Sheet sheet, Class headers) { + for (H header : EnumSet.allOf(headers)) { + sheet.autoSizeColumn(header.ordinal()); + sheet.setColumnWidth(header.ordinal(),sheet.getColumnWidth(header.ordinal())+ADD_COLUMN_SIZE); + } + } } diff --git a/src/main/java/de/avatic/lcc/service/excelMapper/ContainerRateExcelMapper.java b/src/main/java/de/avatic/lcc/service/excelMapper/ContainerRateExcelMapper.java index 8c153af..377c830 100644 --- a/src/main/java/de/avatic/lcc/service/excelMapper/ContainerRateExcelMapper.java +++ b/src/main/java/de/avatic/lcc/service/excelMapper/ContainerRateExcelMapper.java @@ -37,6 +37,7 @@ public class ContainerRateExcelMapper { public void fillSheet(Sheet sheet, CellStyle headerStyle, Integer periodId) { headerGenerator.generateHeader(sheet, ContainerRateHeader.class, headerStyle); containerRateRepository.listAllRatesByPeriodId(periodId).forEach(rate -> mapToRow(rate, sheet.createRow(sheet.getLastRowNum()+1))); + headerGenerator.fixWidth(sheet, ContainerRateHeader.class); } private void mapToRow(ContainerRate rate, Row row) { diff --git a/src/main/java/de/avatic/lcc/service/excelMapper/MaterialExcelMapper.java b/src/main/java/de/avatic/lcc/service/excelMapper/MaterialExcelMapper.java index 6ba95da..16b0c1b 100644 --- a/src/main/java/de/avatic/lcc/service/excelMapper/MaterialExcelMapper.java +++ b/src/main/java/de/avatic/lcc/service/excelMapper/MaterialExcelMapper.java @@ -29,6 +29,7 @@ public class MaterialExcelMapper { public void fillSheet(Sheet sheet, CellStyle headerStyle) { headerGenerator.generateHeader(sheet, MaterialHeader.class, headerStyle); materialRepository.listAllMaterials().forEach(material -> mapToRow(material, sheet.createRow(sheet.getLastRowNum()+1))); + headerGenerator.fixWidth(sheet, MaterialHeader.class); } private void mapToRow(Material material, Row row) { @@ -39,7 +40,7 @@ public class MaterialExcelMapper { public void createConstraints(Sheet sheet) { constraintGenerator.createLengthConstraint(sheet, MaterialHeader.PART_NUMBER.ordinal(), 0, 12); - constraintGenerator.createLengthConstraint(sheet, MaterialHeader.HS_CODE.ordinal(), 0, 8); + constraintGenerator.createLengthConstraint(sheet, MaterialHeader.HS_CODE.ordinal(), 0, 11); constraintGenerator.createLengthConstraint(sheet, MaterialHeader.DESCRIPTION.ordinal(), 1, 500); } diff --git a/src/main/java/de/avatic/lcc/service/excelMapper/MatrixRateExcelMapper.java b/src/main/java/de/avatic/lcc/service/excelMapper/MatrixRateExcelMapper.java index 23cdb1d..69af47b 100644 --- a/src/main/java/de/avatic/lcc/service/excelMapper/MatrixRateExcelMapper.java +++ b/src/main/java/de/avatic/lcc/service/excelMapper/MatrixRateExcelMapper.java @@ -35,6 +35,7 @@ public class MatrixRateExcelMapper { public void fillSheet(Sheet sheet, CellStyle headerStyle, Integer periodId) { headerGenerator.generateHeader(sheet, MatrixRateHeader.class, headerStyle); matrixRateRepository.listAllRatesByPeriodId(periodId).forEach(rate -> mapToRow(rate, sheet.createRow(sheet.getLastRowNum()+1))); + headerGenerator.fixWidth(sheet, MatrixRateHeader.class); } public List extractSheet(Sheet sheet) { diff --git a/src/main/java/de/avatic/lcc/service/excelMapper/NodeExcelMapper.java b/src/main/java/de/avatic/lcc/service/excelMapper/NodeExcelMapper.java index 779fd74..44a106c 100644 --- a/src/main/java/de/avatic/lcc/service/excelMapper/NodeExcelMapper.java +++ b/src/main/java/de/avatic/lcc/service/excelMapper/NodeExcelMapper.java @@ -41,6 +41,7 @@ public class NodeExcelMapper { { headerGenerator.generateHeader(sheet, NodeHeader.class, headerStyle); nodeRepository.listAllNodes(false).forEach(node -> mapToRow(node, sheet.createRow(sheet.getLastRowNum()+1))); + headerGenerator.fixWidth(sheet, NodeHeader.class); } private void mapToRow(Node node, Row row) { @@ -50,16 +51,16 @@ public class NodeExcelMapper { row.createCell(NodeHeader.COUNTRY.ordinal()).setCellValue(countryRepository.getById(node.getCountryId()).orElseThrow().getIsoCode().getCode()); row.createCell(NodeHeader.GEO_LATITUDE.ordinal()).setCellValue(node.getGeoLat().doubleValue()); row.createCell(NodeHeader.GEO_LONGITUDE.ordinal()).setCellValue(node.getGeoLng().doubleValue()); - row.createCell(NodeHeader.IS_ORIGIN.ordinal()).setCellValue(node.getSource()); - row.createCell(NodeHeader.IS_INTERMEDIATE.ordinal()).setCellValue(node.getIntermediate()); - row.createCell(NodeHeader.IS_DESTINATION.ordinal()).setCellValue(node.getDestination()); + row.createCell(NodeHeader.IS_SOURCE.ordinal()).setCellValue(node.getSource() ? "true":"false"); + row.createCell(NodeHeader.IS_INTERMEDIATE.ordinal()).setCellValue(node.getIntermediate()? "true":"false"); + row.createCell(NodeHeader.IS_DESTINATION.ordinal()).setCellValue(node.getDestination()? "true":"false"); row.createCell(NodeHeader.OUTBOUND_COUNTRIES.ordinal()).setCellValue(mapOutboundCountriesToCell(node.getOutboundCountries())); row.createCell(NodeHeader.PREDECESSOR_NODES.ordinal()).setCellValue(mapChains(node.getNodePredecessors())); - row.createCell(NodeHeader.IS_PREDECESSOR_MANDATORY.ordinal()).setCellValue(node.getPredecessorRequired()); + row.createCell(NodeHeader.IS_PREDECESSOR_MANDATORY.ordinal()).setCellValue(node.getPredecessorRequired()? "true":"false"); } private String mapChains(List> chains) { - return chains.stream().map(this::mapChain).collect(Collectors.joining(", ")); + return chains.stream().map(this::mapChain).collect(Collectors.joining("; ")); } private String mapChain(Map chain) { @@ -78,7 +79,7 @@ public class NodeExcelMapper { constraintGenerator.createFormulaListConstraint(sheet, NodeHeader.COUNTRY.ordinal(), namedRange.getNameName()); constraintGenerator.createDecimalConstraint(sheet, NodeHeader.GEO_LATITUDE.ordinal(), -90.0, 90.0); constraintGenerator.createDecimalConstraint(sheet, NodeHeader.GEO_LONGITUDE.ordinal(), -180.0, 180.0); - constraintGenerator.createBooleanConstraint(sheet, NodeHeader.IS_ORIGIN.ordinal()); + constraintGenerator.createBooleanConstraint(sheet, NodeHeader.IS_SOURCE.ordinal()); constraintGenerator.createBooleanConstraint(sheet, NodeHeader.IS_INTERMEDIATE.ordinal()); constraintGenerator.createBooleanConstraint(sheet, NodeHeader.IS_DESTINATION.ordinal()); constraintGenerator.createBooleanConstraint(sheet, NodeHeader.IS_PREDECESSOR_MANDATORY.ordinal()); @@ -104,7 +105,7 @@ public class NodeExcelMapper { entity.setCountryId(IsoCode.valueOf(row.getCell(NodeHeader.COUNTRY.ordinal()).getStringCellValue())); entity.setGeoLat(BigDecimal.valueOf(row.getCell(NodeHeader.GEO_LATITUDE.ordinal()).getNumericCellValue())); entity.setGeoLng(BigDecimal.valueOf(row.getCell(NodeHeader.GEO_LONGITUDE.ordinal()).getNumericCellValue())); - entity.setSource(row.getCell(NodeHeader.IS_ORIGIN.ordinal()).getBooleanCellValue()); + entity.setSource(row.getCell(NodeHeader.IS_SOURCE.ordinal()).getBooleanCellValue()); entity.setIntermediate(row.getCell(NodeHeader.IS_INTERMEDIATE.ordinal()).getBooleanCellValue()); entity.setDestination(row.getCell(NodeHeader.IS_DESTINATION.ordinal()).getBooleanCellValue()); entity.setPredecessorRequired(row.getCell(NodeHeader.IS_PREDECESSOR_MANDATORY.ordinal()).getBooleanCellValue()); diff --git a/src/main/java/de/avatic/lcc/service/excelMapper/PackagingExcelMapper.java b/src/main/java/de/avatic/lcc/service/excelMapper/PackagingExcelMapper.java index 918f07b..3a50710 100644 --- a/src/main/java/de/avatic/lcc/service/excelMapper/PackagingExcelMapper.java +++ b/src/main/java/de/avatic/lcc/service/excelMapper/PackagingExcelMapper.java @@ -54,27 +54,28 @@ public class PackagingExcelMapper { headerGenerator.generateHeader(sheet, headers.toArray(String[]::new), headerStyle); packagingRepository.listAllPackaging().forEach(p -> mapToRow(p, headers, sheet.createRow(sheet.getLastRowNum() + 1))); + headerGenerator.fixWidth(sheet, headers.toArray(String[]::new)); } private void mapToRow(Packaging packaging, ArrayList headers, Row row) { Optional shu = packagingDimensionRepository.getById(packaging.getShuId()); Optional hu = packagingDimensionRepository.getById(packaging.getShuId()); - row.createCell(PackagingHeader.PART_NUMBER.ordinal()).setCellValue(materialRepository.getById(packaging.getMaterialId()).orElseThrow().getPartNumber()); + row.createCell(PackagingHeader.PART_NUMBER.ordinal()).setCellValue(materialRepository.getByIdIncludeDeprecated(packaging.getMaterialId()).orElseThrow().getPartNumber()); row.createCell(PackagingHeader.SUPPLIER.ordinal()).setCellValue(nodeRepository.getById(packaging.getSupplierId()).orElseThrow().getExternalMappingId()); - mapToCell(row.createCell(PackagingHeader.SHU_HEIGHT.ordinal()), shu, PackagingDimension::getHeight); - mapToCell(row.createCell(PackagingHeader.SHU_WIDTH.ordinal()), shu, PackagingDimension::getWidth); - mapToCell(row.createCell(PackagingHeader.SHU_LENGTH.ordinal()), shu, PackagingDimension::getLength); - mapToCell(row.createCell(PackagingHeader.SHU_WEIGHT.ordinal()), shu, PackagingDimension::getWeight); + mapDimensionToCell(row.createCell(PackagingHeader.SHU_HEIGHT.ordinal()), shu, PackagingDimension::getHeight); + mapDimensionToCell(row.createCell(PackagingHeader.SHU_WIDTH.ordinal()), shu, PackagingDimension::getWidth); + mapDimensionToCell(row.createCell(PackagingHeader.SHU_LENGTH.ordinal()), shu, PackagingDimension::getLength); + mapWeightToCell(row.createCell(PackagingHeader.SHU_WEIGHT.ordinal()), shu, PackagingDimension::getWeight); mapToCell(row.createCell(PackagingHeader.SHU_UNIT_COUNT.ordinal()), shu, PackagingDimension::getContentUnitCount); mapToCell(row.createCell(PackagingHeader.SHU_DIMENSION_UNIT.ordinal()), shu, PackagingDimension::getDimensionUnit); mapToCell(row.createCell(PackagingHeader.SHU_WEIGHT_UNIT.ordinal()), shu, PackagingDimension::getWeightUnit); - mapToCell(row.createCell(PackagingHeader.HU_HEIGHT.ordinal()), hu, PackagingDimension::getHeight); - mapToCell(row.createCell(PackagingHeader.HU_WIDTH.ordinal()), hu, PackagingDimension::getWidth); - mapToCell(row.createCell(PackagingHeader.HU_LENGTH.ordinal()), hu, PackagingDimension::getLength); - mapToCell(row.createCell(PackagingHeader.HU_WEIGHT.ordinal()), hu, PackagingDimension::getWeight); + mapDimensionToCell(row.createCell(PackagingHeader.HU_HEIGHT.ordinal()), hu, PackagingDimension::getHeight); + mapDimensionToCell(row.createCell(PackagingHeader.HU_WIDTH.ordinal()), hu, PackagingDimension::getWidth); + mapDimensionToCell(row.createCell(PackagingHeader.HU_LENGTH.ordinal()), hu, PackagingDimension::getLength); + mapWeightToCell(row.createCell(PackagingHeader.HU_WEIGHT.ordinal()), hu, PackagingDimension::getWeight); mapToCell(row.createCell(PackagingHeader.HU_UNIT_COUNT.ordinal()), hu, PackagingDimension::getContentUnitCount); mapToCell(row.createCell(PackagingHeader.HU_DIMENSION_UNIT.ordinal()), hu, PackagingDimension::getDimensionUnit); mapToCell(row.createCell(PackagingHeader.HU_WEIGHT_UNIT.ordinal()), hu, PackagingDimension::getWeightUnit); @@ -84,6 +85,18 @@ public class PackagingExcelMapper { } + private void mapDimensionToCell(Cell cell, Optional packaging, Function provider) { + if (packaging.isPresent()) + cell.setCellValue(packaging.get().getDimensionUnit().convertFromMM(provider.apply(packaging.get()))); + else cell.setBlank(); + } + + private void mapWeightToCell(Cell cell, Optional packaging, Function provider) { + if (packaging.isPresent()) + cell.setCellValue(packaging.get().getWeightUnit().convertFromG(provider.apply(packaging.get()))); + else cell.setBlank(); + } + private void mapToCell(Cell cell, Optional dimension, Function resolver) { if (dimension.isPresent()) { if (resolver.apply(dimension.get()) instanceof Integer) @@ -91,16 +104,23 @@ public class PackagingExcelMapper { if (resolver.apply(dimension.get()) instanceof String) cell.setCellValue((String) resolver.apply(dimension.get())); if (resolver.apply(dimension.get()) instanceof DimensionUnit) - cell.setCellValue(((DimensionUnit) resolver.apply(dimension.get())).name()); + cell.setCellValue(((DimensionUnit) resolver.apply(dimension.get())).getDisplayedName()); + if (resolver.apply(dimension.get()) instanceof WeightUnit) + cell.setCellValue(((WeightUnit) resolver.apply(dimension.get())).getDisplayedName()); } else cell.setBlank(); } public void createConstraints(Workbook workbook, Sheet sheet) { - var namedRange = constraintGenerator.createReference(workbook, HiddenNodeHeader.MAPPING_ID.ordinal(), HiddenTableType.ORIGIN_HIDDEN_TABLE); + var namedRange = constraintGenerator.createReference(workbook, HiddenNodeHeader.MAPPING_ID.ordinal(), HiddenTableType.NODE_HIDDEN_TABLE); constraintGenerator.createFormulaListConstraint(sheet, PackagingHeader.SUPPLIER.ordinal(), namedRange.getNameName()); constraintGenerator.createLengthConstraint(sheet, PackagingHeader.PART_NUMBER.ordinal(), 0, 12); + constraintGenerator.createEnumConstraint(sheet, PackagingHeader.SHU_DIMENSION_UNIT.ordinal(), DimensionUnit.class); + constraintGenerator.createEnumConstraint(sheet, PackagingHeader.SHU_WEIGHT_UNIT.ordinal(), WeightUnit.class); + constraintGenerator.createEnumConstraint(sheet, PackagingHeader.HU_DIMENSION_UNIT.ordinal(), DimensionUnit.class); + constraintGenerator.createEnumConstraint(sheet, PackagingHeader.HU_WEIGHT_UNIT.ordinal(), WeightUnit.class); + //TODO: check hu dimensions... diff --git a/src/main/java/de/avatic/lcc/service/report/ExcelReportingService.java b/src/main/java/de/avatic/lcc/service/report/ExcelReportingService.java index 8bd8f1f..56a1930 100644 --- a/src/main/java/de/avatic/lcc/service/report/ExcelReportingService.java +++ b/src/main/java/de/avatic/lcc/service/report/ExcelReportingService.java @@ -42,7 +42,12 @@ public class ExcelReportingService { Sheet sheet = workbook.createSheet("report"); CellStyle headerStyle = headerCellStyleProvider.createHeaderCellStyle(workbook); - headerGenerator.generateHeader(sheet, reports.stream().map(ReportDTO::getSupplier).map(NodeDTO::getName).toArray(String[]::new), headerStyle); + + ArrayList headers = new ArrayList<>(); + headers.add(""); + headers.addAll(reports.stream().map(ReportDTO::getSupplier).map(NodeDTO::getName).toList()); + + headerGenerator.generateHeader(sheet, headers.toArray(String[]::new), headerStyle); List flatterers = reports.stream().map(ReportFlattener::new).toList(); @@ -57,6 +62,7 @@ public class ExcelReportingService { Cell cell = row.createCell(cellIdx); + if(flattener.hasData(row.getRowNum())) { if(!headerWritten) { row.createCell(0).setCellValue(flattener.getHeader(row.getRowNum())); @@ -72,6 +78,8 @@ public class ExcelReportingService { if(!hasData) break; } + headerGenerator.fixWidth(sheet, headers.toArray(String[]::new)); + // Return the Excel file as an InputStreamSource workbook.write(outputStream); return new ByteArrayResource(outputStream.toByteArray()); @@ -116,8 +124,8 @@ public class ExcelReportingService { private final ReportDTO report; - private List data = new ArrayList<>(); - private List dataHeader = new ArrayList<>(); + private final List data = new ArrayList<>(); + private final List dataHeader = new ArrayList<>(); public ReportFlattener(ReportDTO report) { this.report = report; @@ -129,6 +137,7 @@ public class ExcelReportingService { addData(SUPPLIER_NAME, report.getSupplier().getName()); addData(SUPPLIER_ADDRESS, report.getSupplier().getAddress()); + // TODO: hardcoded (otherwise values wont match report.getCost().keySet().forEach(costName -> addData(costName, report.getCost().get(costName))); report.getRisk().keySet().forEach(riskName -> addData(riskName, report.getRisk().get(riskName))); @@ -143,7 +152,8 @@ public class ExcelReportingService { addData(DESTINATION_HS_CODE, destination.getHsCode()); addData(DESTINATION_TARIFF_RATE, destination.getTariffRate().toString()); addData(DESTINATION_OVERSHARE, destination.getOverseaShare().toString()); - addData(DESTINATION_AIR_FREIGHT_SHARE, destination.getAirFreightShare().toString()); + if(destination.getAirFreightShare() != null) + addData(DESTINATION_AIR_FREIGHT_SHARE, destination.getAirFreightShare().toString()); addData(DESTINATION_TRANSPORT_TIME, destination.getTransportTime().toString()); addData(DESTINATION_SAFETY_STOCK, destination.getSafetyStock().toString()); @@ -172,7 +182,7 @@ public class ExcelReportingService { private void addData(String header, ReportEntryDTO data) { this.dataHeader.add(header); - this.data.add(data.getTotal() + " (" + data.getPercentage().doubleValue()*100 + "%)"); + this.data.add(data.getTotal().toString()); // + " (" + data.getPercentage().doubleValue()*100 + "%)"); } public String getCell(int rowIdx) { diff --git a/src/main/java/de/avatic/lcc/service/transformer/generic/MaterialTransformer.java b/src/main/java/de/avatic/lcc/service/transformer/generic/MaterialTransformer.java index 9b09a9a..809f3b7 100644 --- a/src/main/java/de/avatic/lcc/service/transformer/generic/MaterialTransformer.java +++ b/src/main/java/de/avatic/lcc/service/transformer/generic/MaterialTransformer.java @@ -16,6 +16,8 @@ public class MaterialTransformer { dtoEntry.setName(entity.getName()); dtoEntry.setHsCode(entity.getHsCode()); + dtoEntry.setDeprecated(entity.getDeprecated()); + return dtoEntry; diff --git a/src/main/java/de/avatic/lcc/service/transformer/premise/PremiseTransformer.java b/src/main/java/de/avatic/lcc/service/transformer/premise/PremiseTransformer.java index 17aa63e..17574d7 100644 --- a/src/main/java/de/avatic/lcc/service/transformer/premise/PremiseTransformer.java +++ b/src/main/java/de/avatic/lcc/service/transformer/premise/PremiseTransformer.java @@ -91,7 +91,7 @@ public class PremiseTransformer { var dto = new PremiseDetailDTO(); dto.setId(entity.getId()); - dto.setMaterial(materialTransformer.toMaterialDTO(materialRepository.getById(entity.getMaterialId()).orElseThrow())); + dto.setMaterial(materialTransformer.toMaterialDTO(materialRepository.getByIdIncludeDeprecated(entity.getMaterialId()).orElseThrow())); dto.setMixable(entity.getHuMixable()); dto.setStackable(entity.getHuStackable()); diff --git a/src/main/java/de/avatic/lcc/service/transformer/report/ReportTransformer.java b/src/main/java/de/avatic/lcc/service/transformer/report/ReportTransformer.java index 57f6f90..5224476 100644 --- a/src/main/java/de/avatic/lcc/service/transformer/report/ReportTransformer.java +++ b/src/main/java/de/avatic/lcc/service/transformer/report/ReportTransformer.java @@ -78,7 +78,7 @@ public class ReportTransformer { var weightedTotalCost = getWeightedTotalCosts(sections); Premise premise = premiseRepository.getPremiseById(job.getPremiseId()).orElseThrow(); - reportDTO.setMaterial(materialTransformer.toMaterialDTO(materialRepository.getById(premise.getMaterialId()).orElseThrow())); + reportDTO.setMaterial(materialTransformer.toMaterialDTO(materialRepository.getByIdIncludeDeprecated(premise.getMaterialId()).orElseThrow())); var period = getPeriod(job);