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