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.
This commit is contained in:
Jan 2025-12-05 17:36:01 +01:00
parent a40a8c6bb4
commit cbd467d3b0
8 changed files with 168 additions and 35 deletions

View file

@ -107,7 +107,7 @@ export default {
computed: { computed: {
...mapStores(useDestinationEditStore, usePremiseEditStore), ...mapStores(useDestinationEditStore, usePremiseEditStore),
rows() { rows() {
return this.destinationEditStore.getHandlingCostMatrix return this.destinationEditStore.getHandlingCostMatrix ?? [];
}, },
allChecked() { allChecked() {
return this.rows.every(r => r.selected); return this.rows.every(r => r.selected);
@ -128,7 +128,10 @@ export default {
async created() { async created() {
this.onLoadingChange(true); this.onLoadingChange(true);
try { try {
await this.buildMatrix(); await new Promise(resolve => setTimeout(() => {
this.buildMatrix();
resolve();
}, 10));
} finally { } finally {
this.onLoadingChange(false); this.onLoadingChange(false);

View file

@ -77,7 +77,7 @@ export default {
computed: { computed: {
...mapStores(useDestinationEditStore, usePremiseEditStore), ...mapStores(useDestinationEditStore, usePremiseEditStore),
rows() { rows() {
return this.destinationEditStore.getQuantityMatrix; return this.destinationEditStore.getQuantityMatrix ?? [];
}, },
allChecked() { allChecked() {
return this.rows.every(r => r.selected); return this.rows.every(r => r.selected);
@ -98,7 +98,10 @@ export default {
async created() { async created() {
this.onLoadingChange(true); this.onLoadingChange(true);
try { try {
await this.buildMatrix(); await new Promise(resolve => setTimeout(() => {
this.buildMatrix();
resolve();
}, 10));
} finally { } finally {
this.onLoadingChange(false); this.onLoadingChange(false);
} }

View file

@ -62,7 +62,7 @@ export default {
computed: { computed: {
...mapStores(useDestinationEditStore, usePremiseEditStore), ...mapStores(useDestinationEditStore, usePremiseEditStore),
rows() { rows() {
return this.destinationEditStore.getRouteMatrix; return this.destinationEditStore.getRouteMatrix ?? [];
} }
}, },
data() { data() {
@ -75,7 +75,10 @@ export default {
async created() { async created() {
this.onLoadingChange(true); this.onLoadingChange(true);
try { try {
await this.buildMatrix(); await new Promise(resolve => setTimeout(() => {
this.buildMatrix();
resolve();
}, 10));
} finally { } finally {
this.onLoadingChange(false); this.onLoadingChange(false);
} }
@ -116,13 +119,16 @@ export default {
const destOfCurPremise = this.destinationEditStore.getByPremiseId(pId); const destOfCurPremise = this.destinationEditStore.getByPremiseId(pId);
if (!destOfCurPremise) continue; if (!destOfCurPremise) continue;
/* supplier map collects all destinations for one supplier /* supplier map collects all destinations for one supplier.
* and replaces the destination if the same destination is found * if there is more than one instance of a destination for one supplier
* that already has a selected route. * (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)) { if (!supplierToDestinationsMap.has(curPremise.supplier.id)) {
supplierToDestinationsMap.set(curPremise.supplier.id, { supplierToDestinationsMap.set(curPremise.supplier.id, {
destinations: destOfCurPremise ?? [] destinations: [...destOfCurPremise]
}); });
} else { } else {
const mapEntry = supplierToDestinationsMap.get(curPremise.supplier.id); const mapEntry = supplierToDestinationsMap.get(curPremise.supplier.id);
@ -144,8 +150,8 @@ export default {
}) })
} }
/* destination map collects all destinations over all /* Collects all destinations over all
* suppliers for table headers * suppliers and part numbers for the table headers
*/ */
for (const d of destOfCurPremise) { for (const d of destOfCurPremise) {
const destId = d.destination_node.id; const destId = d.destination_node.id;
@ -166,23 +172,23 @@ export default {
const premiseMap = new Map(); const premiseMap = new Map();
this.premiseIds.forEach(pId => { this.premiseIds.forEach(pId => {
const premise = this.premiseEditStore.getById(pId); const curPremise = this.premiseEditStore.getById(pId);
const destinations = this.destinationEditStore.getByPremiseId(pId); const destOfCurPremise = this.destinationEditStore.getByPremiseId(pId);
if (!premiseMap.has(premise.supplier.id)) { if (!premiseMap.has(curPremise.supplier.id)) {
premiseMap.set(premise.supplier.id, { premiseMap.set(curPremise.supplier.id, {
ids: [], ids: [],
supplierNodeId: premise.supplier.id, supplierNodeId: curPremise.supplier.id,
supplier: premise.supplier, supplier: curPremise.supplier,
destinations: this.buildDestinations(columnHeadersMap, supplierToDestinationsMap.get(premise.supplier.id)?.destinations ?? []) 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) { if (row) {
row.ids.push(premise.id); row.ids.push(curPremise.id);
this.addDestinationsToRow(row.destinations, destinations) this.addDestinationsToRow(row.destinations, destOfCurPremise)
} }
}); });
@ -215,17 +221,25 @@ export default {
} }
}); });
}, },
addDestinationsToRow(rowDestinations, premiseDestinations) { addDestinationsToRow(rowDestinations, destOfPremises) {
premiseDestinations.forEach(premD => { 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) { if (existingDest) {
existingDest.disabled = false; 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 */ /* add route ids to routes */
this.verifyRoutes(existingDest, premD) this.verifyRoutes(existingDest, curDestOfPremise)
} }
}); });
}, },
@ -242,9 +256,13 @@ export default {
premiseRoutes.forEach(route => { premiseRoutes.forEach(route => {
const routeString = JSON.stringify(route.transit_nodes.map(n => n.external_mapping_id)); //.join(" > ").replace("_", " "); 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); console.log("no matching route ", routeString, rowDest);
rowDest.valid = false; rowDest.valid = false;
} else {
rowRoute.ids.push(route.id);
} }
}); });
}, },
@ -252,6 +270,7 @@ export default {
return routes?.map(r => { return routes?.map(r => {
return { return {
ids: [],
type: r.type, type: r.type,
selected: r.is_selected, selected: r.is_selected,
transitNodes: r.transit_nodes.map(n => n.external_mapping_id), transitNodes: r.transit_nodes.map(n => n.external_mapping_id),

View file

@ -439,6 +439,8 @@ export default {
}, },
async closeEditModalAction(action) { async closeEditModalAction(action) {
let massUpdate = false;
if (this.modalType === 'amount' || this.modalType === 'routes' || this.modalType === "destinations") { if (this.modalType === 'amount' || this.modalType === 'routes' || this.modalType === "destinations") {
if (action === 'accept') { if (action === 'accept') {
@ -450,7 +452,7 @@ export default {
await this.destinationEditStore.massSetDestinations(setMatrix); await this.destinationEditStore.massSetDestinations(setMatrix);
} }
} else { } else {
await this.destinationEditStore.massUpdateDestinations(); massUpdate = true
} }
} }
@ -458,6 +460,10 @@ export default {
this.fillData(this.modalType); this.fillData(this.modalType);
this.modalType = null; this.modalType = null;
if(massUpdate) {
await this.destinationEditStore.massUpdateDestinations(this.editIds);
}
if (this.modalStash && action === 'accept') { if (this.modalStash && action === 'accept') {
setTimeout(() => { setTimeout(() => {
this.openModal(this.modalStash.type, this.modalStash.ids, this.modalStash.dataSource, this.modalStash.massEdit); this.openModal(this.modalStash.type, this.modalStash.ids, this.modalStash.dataSource, this.modalStash.massEdit);

View file

@ -108,15 +108,87 @@ export const useDestinationEditStore = defineStore('destinationEdit', {
this.loading = false; this.loading = false;
}, },
async massUpdateDestinations() { async massUpdateDestinations(premiseIds) {
this.loading = true; 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; 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() { updateHandlingCosts() {
this.handlingCostMatrix.forEach(row => { this.handlingCostMatrix.forEach(row => {
const destinations = this.destinations.get(row.id); const destinations = this.destinations.get(row.id);

View file

@ -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.create.PremiseSearchResultDTO;
import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO; 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.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.DestinationSetDTO;
import de.avatic.lcc.dto.calculation.edit.destination.DestinationUpdateDTO; import de.avatic.lcc.dto.calculation.edit.destination.DestinationUpdateDTO;
import de.avatic.lcc.dto.calculation.edit.masterData.MaterialUpdateDTO; import de.avatic.lcc.dto.calculation.edit.masterData.MaterialUpdateDTO;
@ -128,7 +129,8 @@ public class PremiseController {
@GetMapping({"/edit", "/edit/"}) @GetMapping({"/edit", "/edit/"})
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')") @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
public ResponseEntity<List<PremiseDetailDTO>> getPremises(@RequestParam List<Integer> premissIds) { public ResponseEntity<List<PremiseDetailDTO>> getPremises(@RequestParam List<Integer> premissIds) {
return ResponseEntity.ok(premisesServices.getPremises(premissIds)); var premisses = premisesServices.getPremises(premissIds);
return ResponseEntity.ok(premisses);
} }
@PutMapping({"/start", "/start/"}) @PutMapping({"/start", "/start/"})
@ -198,6 +200,13 @@ public class PremiseController {
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@PutMapping({"/destination/all", "/destination/all/"})
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
public ResponseEntity<Void> updateAllDestination(@RequestBody @Valid DestinationMassUpdateDTO destinationUpdateDTO) {
destinationUpdateDTO.getDestinations().forEach(destinationService::updateDestination);
return ResponseEntity.ok().build();
}
@DeleteMapping({"/destination/{id}", "/destination/{id}/"}) @DeleteMapping({"/destination/{id}", "/destination/{id}/"})
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')") @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
public ResponseEntity<Void> deleteDestination(@PathVariable Integer id) { public ResponseEntity<Void> deleteDestination(@PathVariable Integer id) {

View file

@ -0,0 +1,19 @@
package de.avatic.lcc.dto.calculation.edit.destination;
import jakarta.validation.Valid;
import java.util.Map;
public class DestinationMassUpdateDTO {
Map<Integer, @Valid DestinationUpdateDTO> destinations;
public Map<Integer, DestinationUpdateDTO> getDestinations() {
return destinations;
}
public void setDestinations(Map<Integer, DestinationUpdateDTO> destinations) {
this.destinations = destinations;
}
}

View file

@ -96,7 +96,9 @@ public class PremisesService {
if (!admin) if (!admin)
premiseRepository.checkOwner(premiseIds, userId); 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();
} }