diff --git a/src/frontend/src/backend.js b/src/frontend/src/backend.js index e1bae63..55bfcfb 100644 --- a/src/frontend/src/backend.js +++ b/src/frontend/src/backend.js @@ -14,10 +14,71 @@ const performRequest = async (requestingStore, method, url, body, expectResponse params.body = JSON.stringify(body); } - const request = {url: url, params: params}; + const request = {url: url, params: params, expectResponse: expectResponse, expectedException: expectedException}; logger.info("Request:", request); - const response = await fetch(url, params + const data = await executeRequest(requestingStore, request); + + logger.info("Response:", data); + return data; +} + +const performDownload = async (requestingStore, url, expectResponse = true, expectedException = null) => { + + const params = { + method: 'GET', + }; + + const request = {url: url, params: params, expectResponse: expectResponse, expectedException: expectedException}; + logger.info("Request:", request); + + const processId = await executeRequest(null, request); + + logger.info("Response:", processId); + return processId; +} + +const performUpload = async (url, file, expectResponse = true, expectedException = null) => { + + const formData = new FormData(); + formData.append('file', file); + + const params = { + method: 'POST', + body: formData + }; + + const request = {url: url, params: params, expectResponse: expectResponse, expectedException: expectedException}; + logger.info("Request:", request); + + const processId = await executeRequest(null, request); + + logger.info("Response:", processId); + return processId; +} + +function handleErrorResponse(data, requestingStore, request) { + const errorObj = { + code: data.error.code, + title: data.error.title, + message: data.error.message, + trace: data.error.trace + } + + const error = new Error('Internal backend error'); + error.errorObj = errorObj; + + if (request.expectedException === null || data.error.title !== request.expectedException) { + logger.error(errorObj); + const errorStore = useErrorStore(); + void errorStore.addError(errorObj, {store: requestingStore, request: request}); + } + + throw error; +} + +const executeRequest = async (requestingStore, request) => { + const response = await fetch(request.url, request.params ).catch(e => { const error = { code: 'Network error.', @@ -33,7 +94,7 @@ const performRequest = async (requestingStore, method, url, body, expectResponse }); let data = null; - if (expectResponse) { + if (request.expectResponse) { data = await response.json().catch(e => { const error = { code: 'Malformed response', @@ -48,23 +109,7 @@ const performRequest = async (requestingStore, method, url, body, expectResponse }); if (!response.ok) { - const errorObj = { - code: data.error.code, - title: data.error.title, - message: data.error.message, - trace: data.error.trace - } - - const error = new Error('Internal backend error'); - error.errorObj = errorObj; - - if (expectedException === null || data.error.title !== expectedException) { - logger.error(errorObj); - const errorStore = useErrorStore(); - void errorStore.addError(errorObj, {store: requestingStore, request: request}); - } - - throw error; + handleErrorResponse(data, requestingStore, request); } } else { if (!response.ok) { @@ -82,28 +127,12 @@ const performRequest = async (requestingStore, method, url, body, expectResponse }); - const errorObj = { - code: data.error.code, - title: data.error.title, - message: data.error.message, - trace: data.error.trace - } - - const error = new Error('Internal backend error'); - error.errorObj = errorObj; - - if (expectedException === null || data.error.title !== expectedException) { - logger.error(errorObj); - const errorStore = useErrorStore(); - void errorStore.addError(errorObj, {store: requestingStore, request: request}); - } - - throw error; + handleErrorResponse(data, requestingStore, request); } } - logger.info("Response:", data); return data; } export default performRequest; +export {performUpload, performDownload}; diff --git a/src/frontend/src/components/layout/config/BulkOperations.vue b/src/frontend/src/components/layout/config/BulkOperations.vue new file mode 100644 index 0000000..dbe3ac5 --- /dev/null +++ b/src/frontend/src/components/layout/config/BulkOperations.vue @@ -0,0 +1,227 @@ + + + + + + \ No newline at end of file diff --git a/src/frontend/src/components/layout/config/BulkUpload.vue b/src/frontend/src/components/layout/config/BulkUpload.vue deleted file mode 100644 index 2c61f78..0000000 --- a/src/frontend/src/components/layout/config/BulkUpload.vue +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/frontend/src/pages/Config.vue b/src/frontend/src/pages/Config.vue index b8964b7..9ffbb64 100644 --- a/src/frontend/src/pages/Config.vue +++ b/src/frontend/src/pages/Config.vue @@ -22,6 +22,7 @@ import Properties from "@/components/layout/config/Properties.vue"; 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"; export default { name: "Config", @@ -38,25 +39,9 @@ export default { title: 'Countries', component: markRaw(CountryProperties), }, - { - title: 'Nodes', - component: (null), - }, - { - title: 'Kilometer rates', - component: (null), - }, - { - title: 'Container rates', - component: (null), - }, - { - title: 'Materials & packaging', - component: (null), - }, { title: 'Bulk operations', - component: (null), + component: markRaw(BulkOperations), } ] } diff --git a/src/frontend/src/pages/Reporting.vue b/src/frontend/src/pages/Reporting.vue index 309d98a..a58db46 100644 --- a/src/frontend/src/pages/Reporting.vue +++ b/src/frontend/src/pages/Reporting.vue @@ -8,6 +8,11 @@ +
+ + Reports may not be comparable! Calculations differ in destinations and/or their annual quantities. +
+
@@ -54,6 +59,9 @@ export default { }, computed: { ...mapStores(useReportsStore), + showComparableWarning() { + return this.reportsStore.getShowComparableWarning; + }, hasReport() { return this.reportsStore.reports?.length > 0; }, @@ -160,4 +168,17 @@ export default { align-items: center; } +.destination-differ-warning { + display: flex; + align-items: center; + font-size: 1.4rem; + gap: 1.6rem; + background-color: #c3cfdf; + color: #002F54; + border-radius: 0.8rem; + padding: 1.6rem; + margin-bottom: 1.6rem; +} + + \ No newline at end of file diff --git a/src/frontend/src/store/reports.js b/src/frontend/src/store/reports.js index d704319..2ac5a8f 100644 --- a/src/frontend/src/store/reports.js +++ b/src/frontend/src/store/reports.js @@ -7,10 +7,14 @@ export const useReportsStore = defineStore('reports', { state() { return { reports: [], + showComparableWarning: false, loading: false, } }, getters: { + getShowComparableWarning(state) { + return state.showComparableWarning; + }, getChartScale(state) { let max = 0; @@ -25,7 +29,7 @@ export const useReportsStore = defineStore('reports', { }, actions: { reset() { - this.reports = []; + this.reports = []; }, async fetchReports(materialId, supplierIds) { if (supplierIds == null || materialId == null) return; @@ -39,10 +43,42 @@ export const useReportsStore = defineStore('reports', { const url = `${config.backendUrl}/reports/view/${params.size === 0 ? '' : '?'}${params.toString()}`; - this.reports = await performRequest(this,'GET', url, null).catch(e => { + this.reports = await performRequest(this, 'GET', url, null).catch(e => { this.loading = false; }); + + this.showComparableWarning = false; + for (const [idx, report] of this.reports.entries()) { + for (const otherReport of this.reports.slice(idx + 1)) { + if (report.premises.length !== otherReport.premises.length) { + this.showComparableWarning = true; + break; + } + + for (const premise of report.premises) { + + const otherPremise = otherReport.premises.find(otherPremise => otherPremise.destination.external_mapping_id === premise.destination.external_mapping_id); + + if((otherPremise ?? null) == null) { + this.showComparableWarning = true; + break; + } + + if(otherPremise.annual_quantity !== premise.annual_quantity) { + this.showComparableWarning = true; + break; + } + + } + + } + + if(this.showComparableWarning) + break; + } + + this.loading = false; } } diff --git a/src/main/java/de/avatic/lcc/controller/bulk/BulkOperationController.java b/src/main/java/de/avatic/lcc/controller/bulk/BulkOperationController.java index 88f62ab..3073d32 100644 --- a/src/main/java/de/avatic/lcc/controller/bulk/BulkOperationController.java +++ b/src/main/java/de/avatic/lcc/controller/bulk/BulkOperationController.java @@ -54,7 +54,7 @@ public class BulkOperationController { * @param file The file to be uploaded, provided as a multipart file. * @return A ResponseEntity indicating whether the upload was processed successfully. */ - @PostMapping("/upload/{type}/{processing_type}") + @PostMapping({"/upload/{type}/{processing_type}","/upload/{type}/{processing_type}/"}) public ResponseEntity uploadFile(@PathVariable("processing_type") BulkProcessingType processingType, @PathVariable BulkFileType type, @RequestParam("file") MultipartFile file) { return ResponseEntity.ok(bulkProcessingService.processFile(type, processingType, file)); } diff --git a/src/main/java/de/avatic/lcc/repositories/premise/RouteRepository.java b/src/main/java/de/avatic/lcc/repositories/premise/RouteRepository.java index e06a3fc..e3e2199 100644 --- a/src/main/java/de/avatic/lcc/repositories/premise/RouteRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/premise/RouteRepository.java @@ -40,8 +40,8 @@ public class RouteRepository { return Optional.empty(); } -/* if(1 < route.size()) - TODO throw something */ + if(1 < route.size()) + throw new DatabaseException("Multiple selected routes for destination with id " + id); return Optional.of(route.getFirst());