Bugfix: fixed reporting for user generated nodes

This commit is contained in:
Jan 2025-10-19 17:24:23 +02:00
parent a2e0029dfe
commit b19e16fbb0
12 changed files with 107 additions and 54 deletions

View file

@ -15,15 +15,15 @@
<div class="caption">Select suppliers to compare:</div>
<div class="content-container" v-if="suppliers.length !== 0">
<transition-group name="fade" tag="ul" class="item-list">
<li class="item-list-element" v-for="supplier in suppliers" :key="supplier.id"
@click="selectSupplier(supplier.id)">
<li class="item-list-element" v-for="supplier in suppliers" :key="supplier.is_user_node? 'u' : 'n'+ String(supplier.id)"
@click="selectSupplier(supplier)">
<supplier-item :id="String(supplier.id)"
:iso-code="supplier.country.iso_code"
:address="supplier.address"
:name="supplier.name"
:show-trash="false"
:is-user-supplier="supplier.isUserSupplier"
:selected="isSelected(supplier.id)"
:selected="isSelected(supplier)"
></supplier-item>
</li>
</transition-group>
@ -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};

View file

@ -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;
}

View file

@ -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();

View file

@ -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()}`;

View file

@ -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;
@ -57,8 +56,8 @@ public class ReportingController {
* @return The generated report details.
*/
@GetMapping({"/view", "/view/"})
public ResponseEntity<List<ReportDTO>> getReport(@RequestParam(value = "material") Integer materialId, @RequestParam(value = "sources") List<Integer> nodeIds) {
return ResponseEntity.ok(reportingService.getReport(materialId, nodeIds));
public ResponseEntity<List<ReportDTO>> getReport(@RequestParam(value = "material") Integer materialId, @RequestParam(value = "sources", required = false) List<Integer> nodeIds, @RequestParam(value = "userSources", required = false) List<Integer> userNodeIds) {
return ResponseEntity.ok(reportingService.getReport(materialId, nodeIds, userNodeIds));
}
/**
@ -69,7 +68,7 @@ public class ReportingController {
* @return The Excel file as an attachment in the response.
*/
@GetMapping({"/download", "/download/"})
public ResponseEntity<InputStreamResource> downloadReport(@RequestParam(value = "material") Integer materialId, @RequestParam(value = "sources") List<Integer> nodeIds) {
public ResponseEntity<InputStreamResource> downloadReport(@RequestParam(value = "material") Integer materialId, @RequestParam(value = "sources", required = false) List<Integer> nodeIds, @RequestParam(value = "userSources", required = false) List<Integer> 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)));
}
}

View file

@ -86,6 +86,22 @@ public class CalculationJobRepository {
return Optional.of(job.getFirst());
}
@Transactional
public Optional<CalculationJob> 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 = ?";

View file

@ -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();
}

View file

@ -189,13 +189,33 @@ public class ValidityPeriodRepository {
}
@Transactional
public Optional<ValidityPeriod> getValidPeriodForReportingByMaterialId(Integer materialId, List<Integer> nodeIds) {
public Optional<ValidityPeriod> getValidPeriodForReportingByMaterialId(Integer materialId, List<Integer> nodeIds, List<Integer> 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<Integer> safeNodeIds = nodeIds != null ? nodeIds : Collections.emptyList();
List<Integer> 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);

View file

@ -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:

View file

@ -34,9 +34,9 @@ public class ExcelReportingService {
this.headerGenerator = headerGenerator;
}
public ByteArrayResource generateExcelReport(Integer materialId, List<Integer> nodeIds) {
public ByteArrayResource generateExcelReport(Integer materialId, List<Integer> nodeIds, List<Integer> 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");

View file

@ -49,15 +49,17 @@ public class ReportingService {
return nodes;
}
public List<ReportDTO> getReport(Integer materialId, List<Integer> nodeIds) {
var period = validityPeriodRepository.getValidPeriodForReportingByMaterialId(materialId, nodeIds);
public List<ReportDTO> getReport(Integer materialId, List<Integer> nodeIds, List<Integer> 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();
}

View file

@ -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