diff --git a/src/frontend/src/components/layout/report/SelectForReport.vue b/src/frontend/src/components/layout/report/SelectForReport.vue index 7cfc0a8..fffe190 100644 --- a/src/frontend/src/components/layout/report/SelectForReport.vue +++ b/src/frontend/src/components/layout/report/SelectForReport.vue @@ -15,15 +15,15 @@
Select suppliers to compare:
-
  • +
  • @@ -64,7 +64,7 @@ export default { return this.reportSearchStore.getSuppliers; }, hasSelectedSuppliers() { - return this.reportSearchStore.getSelectedIds.length > 0; + return this.reportSearchStore.getSelectedSuppliers.length > 0; }, partNumber() { return this.reportSearchStore.getMaterial?.part_number; @@ -72,14 +72,18 @@ export default { }, methods: { close(action) { - const ids = this.reportSearchStore.getSelectedIds; - this.$emit('close', {action: action, supplierIds: ids, materialId: this.selectedMaterialId}); + const suppliers = this.reportSearchStore.getSelectedSuppliers; + this.$emit('close', { + action: action, + userSupplierIds: suppliers.filter(s => s.is_user_node).map(s => s.id), + supplierIds: suppliers.filter(s => !s.is_user_node).map(s => s.id), + materialId: this.selectedMaterialId}); }, - isSelected(id) { - return this.reportSearchStore.isSelected(id); + isSelected(supplier) { + return this.reportSearchStore.isSelected(supplier); }, - selectSupplier(id) { - this.reportSearchStore.selectSupplier(id); + selectSupplier(supplier) { + this.reportSearchStore.selectSupplier(supplier); }, async getMaterial(query) { const materialQuery = {searchTerm: query}; diff --git a/src/frontend/src/pages/Reporting.vue b/src/frontend/src/pages/Reporting.vue index ac466ce..1bf8377 100644 --- a/src/frontend/src/pages/Reporting.vue +++ b/src/frontend/src/pages/Reporting.vue @@ -123,7 +123,7 @@ export default { }, async closeModal(data) { if (data.action === 'accept') { - await this.reportsStore.fetchReports(data.materialId, data.supplierIds); + await this.reportsStore.fetchReports(data.materialId, data.supplierIds, data.userSupplierIds); } this.showModal = false; } diff --git a/src/frontend/src/store/reportSearch.js b/src/frontend/src/store/reportSearch.js index d99404a..1919e86 100644 --- a/src/frontend/src/store/reportSearch.js +++ b/src/frontend/src/store/reportSearch.js @@ -6,7 +6,7 @@ export const useReportSearchStore = defineStore('reportSearch', { state() { return { suppliers: [], - selectedIds: [], + selectedSuppliers: [], remainingSuppliers: [], material: null, loading: false, @@ -17,12 +17,12 @@ export const useReportSearchStore = defineStore('reportSearch', { return state.remainingSuppliers; }, isSelected(state) { - return function (id) { - return state.selectedIds.includes(id); + return function (supplier) { + return state.selectedSuppliers.some(s => s.id === supplier.id && s.is_user_node === supplier.is_user_node); } }, - getSelectedIds(state) { - return state.selectedIds; + getSelectedSuppliers(state) { + return state.selectedSuppliers; }, getMaterialId(state) { return state.material?.id; @@ -35,16 +35,16 @@ export const useReportSearchStore = defineStore('reportSearch', { async reset() { this.suppliers = []; this.material = null; - this.selectedIds = []; + this.selectedSuppliers = []; this.remainingSuppliers = []; }, - async selectSupplier(id) { + async selectSupplier(supplier) { - if (!this.selectedIds.includes(id)) - this.selectedIds.push(id) + if (!this.selectedSuppliers.some(s => s.id === supplier.id && s.is_user_node === supplier.is_user_node)) + this.selectedSuppliers.push(supplier) else { - const index = this.selectedIds.findIndex(selectedId => selectedId === id); - this.selectedIds.splice(index, 1); + const index = this.selectedSuppliers.findIndex(s => s.id === supplier.id && s.is_user_node === supplier.is_user_node); + this.selectedSuppliers.splice(index, 1); } @@ -54,7 +54,7 @@ export const useReportSearchStore = defineStore('reportSearch', { const toBeShown = [] this.suppliers.forEach(supplierList => { - const shouldInclude = this.selectedIds.every(id => supplierList.some(s => s.id === id)) + const shouldInclude = this.selectedSuppliers.every(selSup => supplierList.some(s => s.id === selSup.id && s.is_user_node === selSup.is_user_node)) if (shouldInclude) toBeShown.push(...supplierList); @@ -62,7 +62,7 @@ export const useReportSearchStore = defineStore('reportSearch', { this.remainingSuppliers = toBeShown.filter((item, index) => - toBeShown.findIndex(obj => obj.id === item.id) === index + toBeShown.findIndex(obj => obj.id === item.id && obj.is_user_node === item.is_user_node) === index ); }, async setMaterial(material) { @@ -74,7 +74,7 @@ export const useReportSearchStore = defineStore('reportSearch', { this.loading = true; this.suppliers = []; - this.selectedIds = []; + this.selectedSuppliers = []; this.remainingSuppliers = []; const params = new URLSearchParams(); diff --git a/src/frontend/src/store/reports.js b/src/frontend/src/store/reports.js index 7ed7369..d805c94 100644 --- a/src/frontend/src/store/reports.js +++ b/src/frontend/src/store/reports.js @@ -11,6 +11,7 @@ export const useReportsStore = defineStore('reports', { loading: false, materialId: null, supplierIds: [], + userSupplierIds: [], } }, getters: { @@ -46,7 +47,7 @@ export const useReportsStore = defineStore('reports', { reset() { this.reports = []; }, - async fetchReports(materialId, supplierIds) { + async fetchReports(materialId, supplierIds, userSupplierIds) { if (supplierIds == null || materialId == null) return; this.loading = true; @@ -54,10 +55,12 @@ export const useReportsStore = defineStore('reports', { this.materialId = materialId; this.supplierIds = supplierIds; + this.userSupplierIds = userSupplierIds; const params = new URLSearchParams(); params.append('material', materialId); params.append('sources', supplierIds); + params.append('userSources', userSupplierIds); const url = `${config.backendUrl}/reports/view/${params.size === 0 ? '' : '?'}${params.toString()}`; 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 445babe..381a9ee 100644 --- a/src/main/java/de/avatic/lcc/controller/report/ReportingController.java +++ b/src/main/java/de/avatic/lcc/controller/report/ReportingController.java @@ -4,7 +4,6 @@ import de.avatic.lcc.dto.generic.NodeDTO; import de.avatic.lcc.dto.report.ReportDTO; import de.avatic.lcc.service.report.ExcelReportingService; import de.avatic.lcc.service.report.ReportingService; -import jakarta.annotation.security.RolesAllowed; import org.springframework.core.io.InputStreamResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -56,9 +55,9 @@ public class ReportingController { * @param nodeIds A list of node IDs (sources) to include in the report. * @return The generated report details. */ - @GetMapping({"/view","/view/"}) - public ResponseEntity> getReport(@RequestParam(value = "material") Integer materialId, @RequestParam(value = "sources") List nodeIds) { - return ResponseEntity.ok(reportingService.getReport(materialId, nodeIds)); + @GetMapping({"/view", "/view/"}) + public ResponseEntity> getReport(@RequestParam(value = "material") Integer materialId, @RequestParam(value = "sources", required = false) List nodeIds, @RequestParam(value = "userSources", required = false) List userNodeIds) { + return ResponseEntity.ok(reportingService.getReport(materialId, nodeIds, userNodeIds)); } /** @@ -68,8 +67,8 @@ 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","/download/"}) - public ResponseEntity downloadReport(@RequestParam(value = "material") Integer materialId, @RequestParam(value = "sources") List nodeIds) { + @GetMapping({"/download", "/download/"}) + public ResponseEntity downloadReport(@RequestParam(value = "material") Integer materialId, @RequestParam(value = "sources", required = false) List nodeIds, @RequestParam(value = "userSources", required = false) List userNodeIds) { HttpHeaders headers = new HttpHeaders(); headers.add("Content-Disposition", "attachment; filename=lcc_report.xlsx"); @@ -78,6 +77,6 @@ public class ReportingController { .ok() .headers(headers) .contentType(MediaType.parseMediaType("application/vnd.ms-excel")) - .body(new InputStreamResource(excelReportingService.generateExcelReport(materialId, nodeIds))); + .body(new InputStreamResource(excelReportingService.generateExcelReport(materialId, nodeIds, userNodeIds))); } } diff --git a/src/main/java/de/avatic/lcc/repositories/calculation/CalculationJobRepository.java b/src/main/java/de/avatic/lcc/repositories/calculation/CalculationJobRepository.java index 4062de8..9aeba04 100644 --- a/src/main/java/de/avatic/lcc/repositories/calculation/CalculationJobRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/calculation/CalculationJobRepository.java @@ -86,6 +86,22 @@ public class CalculationJobRepository { return Optional.of(job.getFirst()); } + @Transactional + public Optional getCalculationJobWithJobStateValidUserNodeId(Integer periodId, Integer userNodeId, Integer materialId) { + + /* there should only be one job per period id, node id and material id combination */ + String query = "SELECT * FROM calculation_job AS cj INNER JOIN premise AS p ON cj.premise_id = p.id WHERE job_state = 'VALID' AND validity_period_id = ? AND p.user_supplier_node_id = ? AND material_id = ? ORDER BY cj.calculation_date DESC LIMIT 1"; + + var job = jdbcTemplate.query(query, new CalculationJobMapper(), periodId, userNodeId, materialId); + + if(job.isEmpty()) + return Optional.empty(); + + return Optional.of(job.getFirst()); + } + + + @Transactional public void setStateTo(Integer id, CalculationJobState calculationJobState) { String sql = "UPDATE calculation_job SET job_state = ? WHERE id = ?"; diff --git a/src/main/java/de/avatic/lcc/repositories/premise/PremiseRepository.java b/src/main/java/de/avatic/lcc/repositories/premise/PremiseRepository.java index 757d516..d91db8e 100644 --- a/src/main/java/de/avatic/lcc/repositories/premise/PremiseRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/premise/PremiseRepository.java @@ -732,7 +732,7 @@ public class PremiseRepository { user_n.country_id as 'user_n.country_id', user_n.geo_lat as 'user_n.geo_lat', user_n.geo_lng as 'user_n.geo_lng' """).append(BASE_JOIN_QUERY); appendConditions(queryBuilder); - queryBuilder.append(" ORDER BY p.updated_at, p.id DESC"); + queryBuilder.append(" ORDER BY p.updated_at DESC, p.id DESC"); queryBuilder.append(" LIMIT ? OFFSET ?"); return queryBuilder.toString(); } diff --git a/src/main/java/de/avatic/lcc/repositories/rates/ValidityPeriodRepository.java b/src/main/java/de/avatic/lcc/repositories/rates/ValidityPeriodRepository.java index f521ff0..025e9d8 100644 --- a/src/main/java/de/avatic/lcc/repositories/rates/ValidityPeriodRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/rates/ValidityPeriodRepository.java @@ -189,13 +189,33 @@ public class ValidityPeriodRepository { } @Transactional - public Optional getValidPeriodForReportingByMaterialId(Integer materialId, List nodeIds) { + public Optional getValidPeriodForReportingByMaterialId(Integer materialId, List nodeIds, List userNodeIds) { - if (nodeIds == null || nodeIds.isEmpty()) { + // Check if both lists are empty + if ((nodeIds == null || nodeIds.isEmpty()) && (userNodeIds == null || userNodeIds.isEmpty())) { return Optional.empty(); } - String placeholders = String.join(",", Collections.nCopies(nodeIds.size(), "?")); + // Handle null lists + List safeNodeIds = nodeIds != null ? nodeIds : Collections.emptyList(); + List safeUserNodeIds = userNodeIds != null ? userNodeIds : Collections.emptyList(); + + int totalNodeCount = safeNodeIds.size() + safeUserNodeIds.size(); + + // Build placeholders for both node types + String nodePlaceholders = safeNodeIds.isEmpty() ? "" : String.join(",", Collections.nCopies(safeNodeIds.size(), "?")); + String userNodePlaceholders = safeUserNodeIds.isEmpty() ? "" : String.join(",", Collections.nCopies(safeUserNodeIds.size(), "?")); + + // Build the WHERE clause dynamically + StringBuilder whereClause = new StringBuilder(); + if (!safeNodeIds.isEmpty() && !safeUserNodeIds.isEmpty()) { + whereClause.append("AND (p.supplier_node_id IN (").append(nodePlaceholders) + .append(") OR p.user_supplier_node_id IN (").append(userNodePlaceholders).append("))"); + } else if (!safeNodeIds.isEmpty()) { + whereClause.append("AND p.supplier_node_id IN (").append(nodePlaceholders).append(")"); + } else { + whereClause.append("AND p.user_supplier_node_id IN (").append(userNodePlaceholders).append(")"); + } String validityPeriodSql = """ SELECT vp.* @@ -203,33 +223,42 @@ public class ValidityPeriodRepository { INNER JOIN ( SELECT cj.validity_period_id, - COUNT(DISTINCT p.supplier_node_id) as node_count + COUNT(DISTINCT COALESCE(p.supplier_node_id, p.user_supplier_node_id)) as node_count FROM premise p INNER JOIN calculation_job cj ON p.id = cj.premise_id WHERE p.material_id = ? - AND p.supplier_node_id IN (""" - + placeholders + """ - ) + """ + + whereClause + """ + GROUP BY cj.validity_period_id HAVING - COUNT(DISTINCT p.supplier_node_id) = ? + COUNT(DISTINCT COALESCE(p.supplier_node_id, p.user_supplier_node_id)) = ? ) matching_periods ON vp.id = matching_periods.validity_period_id ORDER BY vp.start_date DESC LIMIT 1 """; + // Build parameters array + Object[] params = new Object[1 + totalNodeCount + 1]; + int paramIndex = 0; + params[paramIndex++] = materialId; - Object[] params = new Object[1 + nodeIds.size() + 1]; - params[0] = materialId; - for (int i = 0; i < nodeIds.size(); i++) { - params[i + 1] = nodeIds.get(i); + // Add nodeIds + for (Integer nodeId : safeNodeIds) { + params[paramIndex++] = nodeId; } - params[params.length - 1] = nodeIds.size(); + + // Add userNodeIds + for (Integer userNodeId : safeUserNodeIds) { + params[paramIndex++] = userNodeId; + } + + params[paramIndex] = totalNodeCount; var periods = jdbcTemplate.query(validityPeriodSql, new ValidityPeriodMapper(), params); diff --git a/src/main/java/de/avatic/lcc/service/bulk/BulkImportService.java b/src/main/java/de/avatic/lcc/service/bulk/BulkImportService.java index beb95c8..acecd03 100644 --- a/src/main/java/de/avatic/lcc/service/bulk/BulkImportService.java +++ b/src/main/java/de/avatic/lcc/service/bulk/BulkImportService.java @@ -91,7 +91,7 @@ public class BulkImportService { break; case NODE: var nodeInstructions = nodeExcelMapper.extractSheet(sheet); -// batchGeoApiService.geocodeBatch(nodeInstructions); + batchGeoApiService.geocodeBatch(nodeInstructions); nodeInstructions.forEach(nodeBulkImportService::processNodeInstructions); break; default: 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 56a1930..c5b25eb 100644 --- a/src/main/java/de/avatic/lcc/service/report/ExcelReportingService.java +++ b/src/main/java/de/avatic/lcc/service/report/ExcelReportingService.java @@ -34,9 +34,9 @@ public class ExcelReportingService { this.headerGenerator = headerGenerator; } - public ByteArrayResource generateExcelReport(Integer materialId, List nodeIds) { + public ByteArrayResource generateExcelReport(Integer materialId, List nodeIds, List userNodeIds ) { try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { - var reports = reportingService.getReport(materialId, nodeIds); + var reports = reportingService.getReport(materialId, nodeIds, userNodeIds); Workbook workbook = new XSSFWorkbook(); Sheet sheet = workbook.createSheet("report"); diff --git a/src/main/java/de/avatic/lcc/service/report/ReportingService.java b/src/main/java/de/avatic/lcc/service/report/ReportingService.java index f33cffd..be868fd 100644 --- a/src/main/java/de/avatic/lcc/service/report/ReportingService.java +++ b/src/main/java/de/avatic/lcc/service/report/ReportingService.java @@ -49,15 +49,17 @@ public class ReportingService { return nodes; } - public List getReport(Integer materialId, List nodeIds) { - var period = validityPeriodRepository.getValidPeriodForReportingByMaterialId(materialId, nodeIds); + public List getReport(Integer materialId, List nodeIds, List userNodeIds) { + var period = validityPeriodRepository.getValidPeriodForReportingByMaterialId(materialId, nodeIds, userNodeIds); if (period.isEmpty()) throw new IllegalArgumentException("No valid period found"); var periodId = period.get().getId(); - var jobs = nodeIds.stream().map(nodeId -> calculationJobRepository.getCalculationJobWithJobStateValid(periodId, nodeId,materialId)).filter(Optional::isPresent).map(Optional::get).toList(); + var jobs = new ArrayList<>(nodeIds.stream().map(nodeId -> calculationJobRepository.getCalculationJobWithJobStateValid(periodId, nodeId, materialId)).filter(Optional::isPresent).map(Optional::get).toList()); + jobs.addAll(userNodeIds.stream().map(nodeId -> calculationJobRepository.getCalculationJobWithJobStateValidUserNodeId(periodId,nodeId ,materialId)).filter(Optional::isPresent).map(Optional::get).toList()); + return jobs.stream().map(reportTransformer::toReportDTO).toList(); } diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index c40fcb0..ad8dbab 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -239,7 +239,7 @@ CREATE TABLE IF NOT EXISTS container_rate FOREIGN KEY (validity_period_id) REFERENCES validity_period (id), INDEX idx_from_to_nodes (from_node_id, to_node_id), INDEX idx_validity_period_id (validity_period_id), - CONSTRAINT uk_container_rate_unique UNIQUE (from_node_id, to_node_id, validity_period_id) + CONSTRAINT uk_container_rate_unique UNIQUE (from_node_id, to_node_id, validity_period_id, container_rate_type) ); CREATE TABLE IF NOT EXISTS country_matrix_rate