From cbd467d3b0d61d23df811ae8fed80e55065130dd Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 5 Dec 2025 17:36:01 +0100 Subject: [PATCH] Add bulk update functionality for destination data - Introduced `DestinationMassUpdateDTO` to handle bulk update payload. - Added new API endpoint `/destination/all` for mass updates in `PremiseController`. - Updated frontend logic to support mass updating of destinations and enhanced matrix calculations with debounce. - Improved handling of destination and route processing across components. --- .../mass/DestinationMassHandlingCost.vue | 7 +- .../mass/DestinationMassQuantity.vue | 7 +- .../destination/mass/DestinationMassRoute.vue | 67 ++++++++++------ .../src/pages/CalculationMassEdit.vue | 8 +- src/frontend/src/store/destinationEdit.js | 80 ++++++++++++++++++- .../calculation/PremiseController.java | 11 ++- .../destination/DestinationMassUpdateDTO.java | 19 +++++ .../lcc/service/access/PremisesService.java | 4 +- 8 files changed, 168 insertions(+), 35 deletions(-) create mode 100644 src/main/java/de/avatic/lcc/dto/calculation/edit/destination/DestinationMassUpdateDTO.java diff --git a/src/frontend/src/components/layout/edit/destination/mass/DestinationMassHandlingCost.vue b/src/frontend/src/components/layout/edit/destination/mass/DestinationMassHandlingCost.vue index d0b1b08..a1eb043 100644 --- a/src/frontend/src/components/layout/edit/destination/mass/DestinationMassHandlingCost.vue +++ b/src/frontend/src/components/layout/edit/destination/mass/DestinationMassHandlingCost.vue @@ -107,7 +107,7 @@ export default { computed: { ...mapStores(useDestinationEditStore, usePremiseEditStore), rows() { - return this.destinationEditStore.getHandlingCostMatrix + return this.destinationEditStore.getHandlingCostMatrix ?? []; }, allChecked() { return this.rows.every(r => r.selected); @@ -128,7 +128,10 @@ export default { async created() { this.onLoadingChange(true); try { - await this.buildMatrix(); + await new Promise(resolve => setTimeout(() => { + this.buildMatrix(); + resolve(); + }, 10)); } finally { this.onLoadingChange(false); diff --git a/src/frontend/src/components/layout/edit/destination/mass/DestinationMassQuantity.vue b/src/frontend/src/components/layout/edit/destination/mass/DestinationMassQuantity.vue index faac1d2..d4fdb5c 100644 --- a/src/frontend/src/components/layout/edit/destination/mass/DestinationMassQuantity.vue +++ b/src/frontend/src/components/layout/edit/destination/mass/DestinationMassQuantity.vue @@ -77,7 +77,7 @@ export default { computed: { ...mapStores(useDestinationEditStore, usePremiseEditStore), rows() { - return this.destinationEditStore.getQuantityMatrix; + return this.destinationEditStore.getQuantityMatrix ?? []; }, allChecked() { return this.rows.every(r => r.selected); @@ -98,7 +98,10 @@ export default { async created() { this.onLoadingChange(true); try { - await this.buildMatrix(); + await new Promise(resolve => setTimeout(() => { + this.buildMatrix(); + resolve(); + }, 10)); } finally { this.onLoadingChange(false); } diff --git a/src/frontend/src/components/layout/edit/destination/mass/DestinationMassRoute.vue b/src/frontend/src/components/layout/edit/destination/mass/DestinationMassRoute.vue index ef6173b..89d351c 100644 --- a/src/frontend/src/components/layout/edit/destination/mass/DestinationMassRoute.vue +++ b/src/frontend/src/components/layout/edit/destination/mass/DestinationMassRoute.vue @@ -62,7 +62,7 @@ export default { computed: { ...mapStores(useDestinationEditStore, usePremiseEditStore), rows() { - return this.destinationEditStore.getRouteMatrix; + return this.destinationEditStore.getRouteMatrix ?? []; } }, data() { @@ -75,7 +75,10 @@ export default { async created() { this.onLoadingChange(true); try { - await this.buildMatrix(); + await new Promise(resolve => setTimeout(() => { + this.buildMatrix(); + resolve(); + }, 10)); } finally { this.onLoadingChange(false); } @@ -116,13 +119,16 @@ export default { const destOfCurPremise = this.destinationEditStore.getByPremiseId(pId); if (!destOfCurPremise) continue; - /* supplier map collects all destinations for one supplier - * and replaces the destination if the same destination is found - * that already has a selected route. + /* supplier map collects all destinations for one supplier. + * if there is more than one instance of a destination for one supplier + * (more than one part number), a destination instance is chosen by the following priority list: + * 1. instances with d2d rate + * 2. instances with selected routes + * 3. all other instances. */ if (!supplierToDestinationsMap.has(curPremise.supplier.id)) { supplierToDestinationsMap.set(curPremise.supplier.id, { - destinations: destOfCurPremise ?? [] + destinations: [...destOfCurPremise] }); } else { const mapEntry = supplierToDestinationsMap.get(curPremise.supplier.id); @@ -144,8 +150,8 @@ export default { }) } - /* destination map collects all destinations over all - * suppliers for table headers + /* Collects all destinations over all + * suppliers and part numbers for the table headers */ for (const d of destOfCurPremise) { const destId = d.destination_node.id; @@ -166,23 +172,23 @@ export default { const premiseMap = new Map(); this.premiseIds.forEach(pId => { - const premise = this.premiseEditStore.getById(pId); - const destinations = this.destinationEditStore.getByPremiseId(pId); + const curPremise = this.premiseEditStore.getById(pId); + const destOfCurPremise = this.destinationEditStore.getByPremiseId(pId); - if (!premiseMap.has(premise.supplier.id)) { - premiseMap.set(premise.supplier.id, { + if (!premiseMap.has(curPremise.supplier.id)) { + premiseMap.set(curPremise.supplier.id, { ids: [], - supplierNodeId: premise.supplier.id, - supplier: premise.supplier, - destinations: this.buildDestinations(columnHeadersMap, supplierToDestinationsMap.get(premise.supplier.id)?.destinations ?? []) + supplierNodeId: curPremise.supplier.id, + supplier: curPremise.supplier, + destinations: this.buildDestinations(columnHeadersMap, supplierToDestinationsMap.get(curPremise.supplier.id)?.destinations ?? []) }); } - const row = premiseMap.get(premise.supplier.id); + const row = premiseMap.get(curPremise.supplier.id); if (row) { - row.ids.push(premise.id); - this.addDestinationsToRow(row.destinations, destinations) + row.ids.push(curPremise.id); + this.addDestinationsToRow(row.destinations, destOfCurPremise) } }); @@ -215,17 +221,25 @@ export default { } }); }, - addDestinationsToRow(rowDestinations, premiseDestinations) { - premiseDestinations.forEach(premD => { + addDestinationsToRow(rowDestinations, destOfPremises) { + destOfPremises.forEach(curDestOfPremise => { - let existingDest = rowDestinations.find(rowD => rowD.destinationNodeId === premD.destination_node.id) ?? null; + /* rowDestinations contains all here known destinations that are shown + * + * + */ + let existingDest = rowDestinations.find(rowD => rowD.destinationNodeId === curDestOfPremise.destination_node.id) ?? null; if (existingDest) { existingDest.disabled = false; - existingDest.ids.push(premD.id); + + if(existingDest.ids.includes(curDestOfPremise.id)) + console.log("Duplicate id: ", curDestOfPremise.id); + + existingDest.ids.push(curDestOfPremise.id); /* add route ids to routes */ - this.verifyRoutes(existingDest, premD) + this.verifyRoutes(existingDest, curDestOfPremise) } }); }, @@ -242,9 +256,13 @@ export default { premiseRoutes.forEach(route => { const routeString = JSON.stringify(route.transit_nodes.map(n => n.external_mapping_id)); //.join(" > ").replace("_", " "); - if (!(rowDest.routes.some(r => r.routeCompareString === routeString && r.type === route.type))) { + const rowRoute = rowDest.routes.find(r => r.routeCompareString === routeString && r.type === route.type); + + if (!rowRoute) { console.log("no matching route ", routeString, rowDest); rowDest.valid = false; + } else { + rowRoute.ids.push(route.id); } }); }, @@ -252,6 +270,7 @@ export default { return routes?.map(r => { return { + ids: [], type: r.type, selected: r.is_selected, transitNodes: r.transit_nodes.map(n => n.external_mapping_id), diff --git a/src/frontend/src/pages/CalculationMassEdit.vue b/src/frontend/src/pages/CalculationMassEdit.vue index e3fa81d..5cc6634 100644 --- a/src/frontend/src/pages/CalculationMassEdit.vue +++ b/src/frontend/src/pages/CalculationMassEdit.vue @@ -439,6 +439,8 @@ export default { }, async closeEditModalAction(action) { + let massUpdate = false; + if (this.modalType === 'amount' || this.modalType === 'routes' || this.modalType === "destinations") { if (action === 'accept') { @@ -450,7 +452,7 @@ export default { await this.destinationEditStore.massSetDestinations(setMatrix); } } else { - await this.destinationEditStore.massUpdateDestinations(); + massUpdate = true } } @@ -458,6 +460,10 @@ export default { this.fillData(this.modalType); this.modalType = null; + if(massUpdate) { + await this.destinationEditStore.massUpdateDestinations(this.editIds); + } + if (this.modalStash && action === 'accept') { setTimeout(() => { this.openModal(this.modalStash.type, this.modalStash.ids, this.modalStash.dataSource, this.modalStash.massEdit); diff --git a/src/frontend/src/store/destinationEdit.js b/src/frontend/src/store/destinationEdit.js index 122f471..c339843 100644 --- a/src/frontend/src/store/destinationEdit.js +++ b/src/frontend/src/store/destinationEdit.js @@ -108,15 +108,87 @@ export const useDestinationEditStore = defineStore('destinationEdit', { this.loading = false; }, - async massUpdateDestinations() { + async massUpdateDestinations(premiseIds) { this.loading = true; - this.updateQuantity(); - this.updateHandlingCosts(); + await new Promise(resolve => setTimeout(() => { + this.updateQuantity(); + resolve(); + }, 10)); + + await new Promise(resolve => setTimeout(() => { + this.updateHandlingCosts(); + resolve(); + }, 10)); + + await new Promise(resolve => setTimeout(() => { + this.updateRoutes(); + resolve(); + }, 10)); + + const destinationMap = new Map(); + + console.log("premiseids", premiseIds ) + + premiseIds.forEach(premiseId => { + this.destinations.get(premiseId)?.forEach(toUpdate => { + + console.log("toUpdate", toUpdate) + + destinationMap.set(toUpdate.id,{ + annual_amount: toUpdate.annual_amount, + repackaging_costs: toUpdate.repackaging_costs, + handling_costs: toUpdate.handling_costs, + disposal_costs: toUpdate.disposal_costs, + is_d2d: toUpdate.is_d2d, + rate_d2d: toUpdate.rate_d2d, + lead_time_d2d: toUpdate.lead_time_d2d, + route_selected_id: toUpdate.routes.find(r => r.is_selected)?.id ?? null, + }) + } ) + }); + + console.log("destmap",destinationMap) + + await performRequest(this, 'PUT', `${config.backendUrl}/calculation/destination/all`, {destinations: Object.fromEntries(destinationMap)}, false); this.loading = false; }, + updateRoutes() { + this.routeMatrix.forEach(row => { + + row.ids.forEach(premiseId => { + row.destinations.forEach(destinationUpdateInfo => { + const destOfCurPremisses = this.destinations.get(premiseId); + + if ((destOfCurPremisses ?? null) !== null) { + const destOfCurPremise = destOfCurPremisses.find(d => destinationUpdateInfo.destinationNodeId === d.destination_node.id); + + if(destinationUpdateInfo.ids.includes(destOfCurPremise.id)) { + /* set d2d stuff */ + destOfCurPremise.is_d2d = destinationUpdateInfo.isD2d; + destOfCurPremise.rate_d2d = destinationUpdateInfo.rateD2d; + destOfCurPremise.lead_time_d2d = destinationUpdateInfo.leadTimeD2d; + + /* set selected route */ + const selectedRoute = destinationUpdateInfo.routes?.find(r => r.routeCompareString === destinationUpdateInfo.selectedRoute); + + destOfCurPremise.routes.forEach(r => r.is_selected = false); + + if(selectedRoute) { + const routeOfCurPremise = destOfCurPremise.routes.find(r => selectedRoute.ids.includes(r.id)); + + if(routeOfCurPremise) { + routeOfCurPremise.is_selected = true; + } + } + } + } + }); + }); + }); + }, updateHandlingCosts() { this.handlingCostMatrix.forEach(row => { const destinations = this.destinations.get(row.id); @@ -125,7 +197,7 @@ export const useDestinationEditStore = defineStore('destinationEdit', { const destination = destinations.find(dest => dest.id === row.destinationId); - if((destination ?? null) !== null) { + if ((destination ?? null) !== null) { destination.disposal_costs = row.disposal_costs; destination.repackaging_costs = row.repackaging_costs; destination.handling_costs = row.handling_costs; diff --git a/src/main/java/de/avatic/lcc/controller/calculation/PremiseController.java b/src/main/java/de/avatic/lcc/controller/calculation/PremiseController.java index c1a0eb4..015dab5 100644 --- a/src/main/java/de/avatic/lcc/controller/calculation/PremiseController.java +++ b/src/main/java/de/avatic/lcc/controller/calculation/PremiseController.java @@ -9,6 +9,7 @@ import de.avatic.lcc.dto.calculation.create.CreatePremiseDTO; import de.avatic.lcc.dto.calculation.create.PremiseSearchResultDTO; import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO; import de.avatic.lcc.dto.calculation.edit.destination.DestinationCreateDTO; +import de.avatic.lcc.dto.calculation.edit.destination.DestinationMassUpdateDTO; import de.avatic.lcc.dto.calculation.edit.destination.DestinationSetDTO; import de.avatic.lcc.dto.calculation.edit.destination.DestinationUpdateDTO; import de.avatic.lcc.dto.calculation.edit.masterData.MaterialUpdateDTO; @@ -128,7 +129,8 @@ public class PremiseController { @GetMapping({"/edit", "/edit/"}) @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')") public ResponseEntity> getPremises(@RequestParam List premissIds) { - return ResponseEntity.ok(premisesServices.getPremises(premissIds)); + var premisses = premisesServices.getPremises(premissIds); + return ResponseEntity.ok(premisses); } @PutMapping({"/start", "/start/"}) @@ -198,6 +200,13 @@ public class PremiseController { return ResponseEntity.ok().build(); } + @PutMapping({"/destination/all", "/destination/all/"}) + @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')") + public ResponseEntity updateAllDestination(@RequestBody @Valid DestinationMassUpdateDTO destinationUpdateDTO) { + destinationUpdateDTO.getDestinations().forEach(destinationService::updateDestination); + return ResponseEntity.ok().build(); + } + @DeleteMapping({"/destination/{id}", "/destination/{id}/"}) @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')") public ResponseEntity deleteDestination(@PathVariable Integer id) { diff --git a/src/main/java/de/avatic/lcc/dto/calculation/edit/destination/DestinationMassUpdateDTO.java b/src/main/java/de/avatic/lcc/dto/calculation/edit/destination/DestinationMassUpdateDTO.java new file mode 100644 index 0000000..e7c4e53 --- /dev/null +++ b/src/main/java/de/avatic/lcc/dto/calculation/edit/destination/DestinationMassUpdateDTO.java @@ -0,0 +1,19 @@ +package de.avatic.lcc.dto.calculation.edit.destination; + +import jakarta.validation.Valid; + +import java.util.Map; + +public class DestinationMassUpdateDTO { + + Map destinations; + + + public Map getDestinations() { + return destinations; + } + + public void setDestinations(Map destinations) { + this.destinations = destinations; + } +} diff --git a/src/main/java/de/avatic/lcc/service/access/PremisesService.java b/src/main/java/de/avatic/lcc/service/access/PremisesService.java index 516a052..41e353c 100644 --- a/src/main/java/de/avatic/lcc/service/access/PremisesService.java +++ b/src/main/java/de/avatic/lcc/service/access/PremisesService.java @@ -96,7 +96,9 @@ public class PremisesService { if (!admin) premiseRepository.checkOwner(premiseIds, userId); - return premiseRepository.getPremisesById(premiseIds).stream().filter(p -> p.getState().equals(PremiseState.DRAFT)).map(premiseTransformer::toPremiseDetailDTO).toList(); + var premisses = premiseRepository.getPremisesById(premiseIds).stream().filter(p -> p.getState().equals(PremiseState.DRAFT)).toList(); + + return premisses.stream().map(premiseTransformer::toPremiseDetailDTO).toList(); }