diff --git a/src/frontend/public/apple-touch-icon.png b/src/frontend/public/apple-touch-icon.png index 92b9fe6..d0ce292 100644 Binary files a/src/frontend/public/apple-touch-icon.png and b/src/frontend/public/apple-touch-icon.png differ diff --git a/src/frontend/public/favicon-96x96.png b/src/frontend/public/favicon-96x96.png index 27edd58..afb1ea1 100644 Binary files a/src/frontend/public/favicon-96x96.png and b/src/frontend/public/favicon-96x96.png differ diff --git a/src/frontend/public/favicon.ico b/src/frontend/public/favicon.ico index 1b2abbf..f29e836 100644 Binary files a/src/frontend/public/favicon.ico and b/src/frontend/public/favicon.ico differ diff --git a/src/frontend/public/favicon.svg b/src/frontend/public/favicon.svg index 0245ba4..13ed733 100644 --- a/src/frontend/public/favicon.svg +++ b/src/frontend/public/favicon.svg @@ -1,4 +1,4 @@ - + - + - \ No newline at end of file + \ No newline at end of file diff --git a/src/frontend/src/App.vue b/src/frontend/src/App.vue index ef68e40..66c8e16 100644 --- a/src/frontend/src/App.vue +++ b/src/frontend/src/App.vue @@ -29,6 +29,14 @@ export default { padding: 0; } + + + + +html.modal-open { + background: #f8fafc; +} + .page-header { font-weight: normal; margin-bottom: 3rem; diff --git a/src/frontend/src/components/UI/Checkbox.vue b/src/frontend/src/components/UI/Checkbox.vue index d1189b6..2463b18 100644 --- a/src/frontend/src/components/UI/Checkbox.vue +++ b/src/frontend/src/components/UI/Checkbox.vue @@ -1,24 +1,24 @@ - \ No newline at end of file diff --git a/src/frontend/src/components/layout/edit/destination/mass/DestMassCreateRow.vue b/src/frontend/src/components/layout/edit/destination/mass/DestMassCreateRow.vue new file mode 100644 index 0000000..e054457 --- /dev/null +++ b/src/frontend/src/components/layout/edit/destination/mass/DestMassCreateRow.vue @@ -0,0 +1,110 @@ + + + + + + \ No newline at end of file diff --git a/src/frontend/src/components/layout/edit/destination/mass/DestinationMassCreate.vue b/src/frontend/src/components/layout/edit/destination/mass/DestinationMassCreate.vue deleted file mode 100644 index e55ab2e..0000000 --- a/src/frontend/src/components/layout/edit/destination/mass/DestinationMassCreate.vue +++ /dev/null @@ -1,13 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/frontend/src/components/layout/edit/destination/mass/DestinationMassEdit.vue b/src/frontend/src/components/layout/edit/destination/mass/DestinationMassEdit.vue index 12648ba..63eece3 100644 --- a/src/frontend/src/components/layout/edit/destination/mass/DestinationMassEdit.vue +++ b/src/frontend/src/components/layout/edit/destination/mass/DestinationMassEdit.vue @@ -1,14 +1,104 @@ - - + + + \ No newline at end of file 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 6805a9a..2736f99 100644 --- a/src/frontend/src/components/layout/edit/destination/mass/DestinationMassHandlingCost.vue +++ b/src/frontend/src/components/layout/edit/destination/mass/DestinationMassHandlingCost.vue @@ -1,15 +1,446 @@ - - \ No newline at end of file diff --git a/src/frontend/src/components/layout/edit/destination/mass/DestinationMassHandlingCostRow.vue b/src/frontend/src/components/layout/edit/destination/mass/DestinationMassHandlingCostRow.vue new file mode 100644 index 0000000..cafaf9c --- /dev/null +++ b/src/frontend/src/components/layout/edit/destination/mass/DestinationMassHandlingCostRow.vue @@ -0,0 +1,266 @@ + + + + + \ No newline at end of file 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 e206831..ebfcb32 100644 --- a/src/frontend/src/components/layout/edit/destination/mass/DestinationMassQuantity.vue +++ b/src/frontend/src/components/layout/edit/destination/mass/DestinationMassQuantity.vue @@ -1,15 +1,414 @@ - - \ No newline at end of file diff --git a/src/frontend/src/components/layout/edit/destination/mass/DestinationMassQuantityRow.vue b/src/frontend/src/components/layout/edit/destination/mass/DestinationMassQuantityRow.vue new file mode 100644 index 0000000..28a7722 --- /dev/null +++ b/src/frontend/src/components/layout/edit/destination/mass/DestinationMassQuantityRow.vue @@ -0,0 +1,216 @@ + + + + + + \ No newline at end of file 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 aa8c9fe..29ccec9 100644 --- a/src/frontend/src/components/layout/edit/destination/mass/DestinationMassRoute.vue +++ b/src/frontend/src/components/layout/edit/destination/mass/DestinationMassRoute.vue @@ -1,14 +1,192 @@ - - \ No newline at end of file + diff --git a/src/frontend/src/pages/CalculationSingleEdit.vue b/src/frontend/src/pages/CalculationSingleEdit.vue index adbab33..f13a3f8 100644 --- a/src/frontend/src/pages/CalculationSingleEdit.vue +++ b/src/frontend/src/pages/CalculationSingleEdit.vue @@ -174,11 +174,10 @@ export default { }, close() { if (this.bulkEditQuery) { - //TODO: deselect and save - // this.premiseEditStore.deselectPremise(); + //TODO: deselect element and save this.$router.push({name: 'bulk', params: {ids: this.bulkEditQuery}}); } else { - //TODO: deselect and save + //TODO: deselect element and save this.$router.push({name: 'home'}); } }, diff --git a/src/frontend/src/store/destinationEdit.js b/src/frontend/src/store/destinationEdit.js index ce60655..896657a 100644 --- a/src/frontend/src/store/destinationEdit.js +++ b/src/frontend/src/store/destinationEdit.js @@ -1,14 +1,53 @@ import {defineStore} from 'pinia' -import {toRaw} from "vue"; +import performRequest from "@/backend.js"; +import {config} from '@/config' export const useDestinationEditStore = defineStore('destinationEdit', { state: () => ({ destinations: null, loading: false, }), - getters: {}, - actions: { + getters: { + checkDestinationAssignment(state) { + return (ids) => { + let some = false; + let all = true; + ids.forEach(id => { + const dest = state.destinations?.get(id); + + if ((dest ?? null) === null || dest.length === 0) + all = false; + else + some = true; + + }); + + if (all) + return "all"; + else if (some) + return "some"; + else + return "none"; + } + }, + getByPremiseId(state) { + return (id) => { + return state.destinations?.get(id); + } + }, + getByPremiseIds(state) { + return (ids) => { + return new Map( + [...state.destinations].filter(([premiseId, destinations]) => ids.includes(premiseId)) + ); + } + }, + showProcessingModal(state) { + return state.loading; + } + }, + actions: { setupDestinations(premisses) { this.loading = true; @@ -18,312 +57,39 @@ export const useDestinationEditStore = defineStore('destinationEdit', { this.loading = false; }, + async massSetDestinations(updateMatrix) { + this.loading = true; + + const toBeAdded = {}; + const toBeDeletedMap = new Map(); + + updateMatrix.forEach(row => { + toBeAdded[row.id] = row.destinations.filter(d => d.selected).map(d => d.id); + toBeDeletedMap.set(row.id, row.destinations.filter(d => !d.selected).map(d => d.id)); + }); + + const url = `${config.backendUrl}/calculation/destination`; + const { + data: data, + headers: headers + } = await performRequest(this, 'POST', url, {'destination_node_ids': toBeAdded}); + + this.destinations.forEach((destinations, premiseId) => { + const toBeDeleted = toBeDeletedMap.get(premiseId); + + const filtered = destinations !== null ? destinations.filter(d => !toBeDeleted?.includes(d.destination_node.id)) : []; + + this.destinations.set(premiseId, [...filtered, ...data[premiseId]]); + }); - /** - * DESTINATION stuff - * ================= - */ - - prepareDestinations(dataSourcePremiseId, editedPremiseIds, massEdit = false, fromMassEditView = false) { - if (this.premisses === null) return; - if (!editedPremiseIds || !dataSourcePremiseId || editedPremiseIds.length === 0) return; - - this.destinations = { - premise_ids: editedPremiseIds, - massEdit: massEdit, - fromMassEditView: fromMassEditView, - destinations: this.premisses.find(p => String(p.id) === String(dataSourcePremiseId))?.destinations.map(d => this.copyAllFromPremises(d, !massEdit)) ?? [], - }; - - this.selectedDestination = null; - + this.loading = false; }, - async executeDestinationsMassEdit() { + async massUpdateDestinations(updateMatrix) { + this.loading = true; - if (!this.destinations.massEdit) { - - this.destinations.premise_ids.forEach(premiseId => { - const toPremise = this.getById(premiseId); - - this.destinations.destinations.forEach(fromDest => { - const toDest = toPremise.destinations.find(to => fromDest.id.substring(1) === String(to.id)); - - if ((toDest ?? null) === null) { - throw new Error("Destination not found in premise: " + premiseId + " -> " + d.id); - } - - this.copyAllToPremise(fromDest, toDest); - - const body = { - annual_amount: toDest.annual_amount, - repackaging_costs: toDest.repackaging_costs, - handling_costs: toDest.handling_costs, - disposal_costs: toDest.disposal_costs, - is_d2d: toDest.is_d2d, - rate_d2d: toDest.rate_d2d, - lead_time_d2d: toDest.lead_time_d2d, - route_selected_id: toDest.routes.find(r => r.is_selected)?.id ?? null, - }; - - const url = `${config.backendUrl}/calculation/destination/${toDest.id}`; - performRequest(this, 'PUT', url, body, false); - - }); - }); - - - } else { - this.processDestinationMassEdit = true; - - const destinations = []; - - this.destinations.destinations.forEach(d => { - const dest = { - destination_node_id: d.destination_node.id, - annual_amount: d.annual_amount, - disposal_costs: d.userDefinedHandlingCosts ? d.disposal_costs : null, - repackaging_costs: d.userDefinedHandlingCosts ? d.repackaging_costs : null, - handling_costs: d.userDefinedHandlingCosts ? d.handling_costs : null, - } - destinations.push(dest); - }) - - const body = {destinations: destinations, premise_id: this.destinations.premise_ids}; - const url = `${config.backendUrl}/calculation/destination/`; - - const {data: data, headers: headers} = await performRequest(this, 'PUT', url, body).catch(e => { - this.destinations = null; - this.processDestinationMassEdit = false; - }); - - if (data) { - for (const id of Object.keys(data)) { - this.premisses.find(p => String(p.id) === id).destinations = data[id]; - } - } - - this.destinations = null; - this.processDestinationMassEdit = false; - } - }, - cancelMassEdit() { - this.destinations = null; + this.loading = false; }, - copyAllFromPremises(from, fullCopy = true) { - - const d = {}; - - d.id = `e${from.id}`; - d.destination_node = structuredClone(toRaw(from.destination_node)); - d.routes = fullCopy ? structuredClone(toRaw(from.routes)) : null; - - d.annual_amount = from.annual_amount; - d.is_d2d = from.is_d2d; - d.rate_d2d = from.is_d2d ? from.rate_d2d : null; - d.lead_time_d2d = from.is_d2d ? from.lead_time_d2d : null; - d.handling_costs = from.handling_costs; - d.disposal_costs = from.disposal_costs; - d.repackaging_costs = from.repackaging_costs; - d.userDefinedHandlingCosts = from.handling_costs !== null || from.disposal_costs !== null || from.repackaging_costs !== null; - - return d; - }, - copyAllToPremise(from, to, fullCopy = true) { - - const d = to ?? {}; - - d.annual_amount = from.annual_amount; - d.is_d2d = from.is_d2d; - d.rate_d2d = from.is_d2d ? from.rate_d2d : null; - d.lead_time_d2d = from.is_d2d ? from.lead_time_d2d : null; - - if (from.userDefinedHandlingCosts) { - d.disposal_costs = from.disposal_costs; - d.repackaging_costs = from.repackaging_costs; - d.handling_costs = from.handling_costs; - } else { - d.disposal_costs = null; - d.repackaging_costs = null; - d.handling_costs = null; - } - - if (fullCopy && (from.routes ?? null) !== null) { - to.routes.forEach(route => route.is_selected = from.routes.find(r => r.id === route.id)?.is_selected ?? false); - } - - return d; - }, - - /** - * Selects all destinations for the given "ids" for editing. - * This creates a copy of the destination with id "id". - * They are written back as soon as the user closes the dialog. - */ - selectDestination(id) { - if (this.premisses === null) return; - - logger.info("selectDestination:", id) - - const dest = this.destinations.destinations.find(d => d.id === id); - - - if ((dest ?? null) == null) { - const error = { - code: 'Frontend error.', - message: `Destination not found: ${id}. Please contact support.`, - trace: null - } - throw new Error("Internal frontend error: Destination not found: " + id); - } - - this.selectedDestination = structuredClone(toRaw(dest)); - }, - async deselectDestinations(save = false) { - if (this.premisses === null) return; - - - if (save) { - const idx = this.destinations.destinations.findIndex(d => d.id === this.selectedDestination.id); - this.destinations.destinations.splice(idx, 1, this.selectedDestination); - - if (!this.destinations.fromMassEditView) { - //TODO write trough backend if no massEdit - - const toDest = this.singleSelectedPremise.destinations.find(to => this.selectedDestination.id.substring(1) === String(to.id)); - this.copyAllToPremise(this.selectedDestination, toDest); - - const body = { - annual_amount: toDest.annual_amount, - repackaging_costs: toDest.repackaging_costs, - handling_costs: toDest.handling_costs, - disposal_costs: toDest.disposal_costs, - is_d2d: toDest.is_d2d, - rate_d2d: toDest.rate_d2d, - lead_time_d2d: toDest.lead_time_d2d, - route_selected_id: toDest.routes.find(r => r.is_selected)?.id ?? null, - }; - - logger.info(body) - - const url = `${config.backendUrl}/calculation/destination/${toDest.id}`; - await performRequest(this, 'PUT', url, body, false); - - } - - } - - this.selectedDestination = null; - }, - async deleteDestination(id) { - - - /* - * 1. delete from destinations copy - */ - const idx = this.destinations.destinations.findIndex(d => d.id === id); - - if (idx === -1) { - logger.info("Destination not found in mass edit: , id)"); - return; - } - - this.destinations.destinations.splice(idx, 1); - - /* - * 2. delete from backend if not mass edit - */ - - if (!this.destinations.massEdit && id.startsWith('e')) { /* 'v'-ids cannot be deleted because they only exist in the frontend */ - if (this.premisses === null) return; - - const origId = id.substring(1); - - const url = `${config.backendUrl}/calculation/destination/${origId}`; - await performRequest(this, 'DELETE', url, null, false).catch(async e => { - logger.error("Unable to delete destination: " + origId + ""); - logger.error(e); - await this.loadPremissesIfNeeded(this.premisses.map(p => p.id)); - }); - - for (const p of this.premisses) { - const toBeDeleted = p.destinations.findIndex(d => String(d.id) === String(origId)) - - logger.info(toBeDeleted) - - if (toBeDeleted !== -1) { - p.destinations.splice(toBeDeleted, 1) - break; - } - } - } - }, - async addDestination(node) { - - if (this.destinations.massEdit) { - - const existing = this.destinations.destinations.find(d => d.destination_node.id === node.id); - logger.info(existing) - - if ((existing ?? null) !== null) { - logger.info("Destination already exists", node.id); - return [existing.id]; - } - - const destination = { - id: `v${node.id}`, - destination_node: structuredClone(toRaw(node)), - massEdit: true, - annual_amount: 0, - is_d2d: false, - rate_d2d: null, - lead_time_d2d: null, - disposal_costs: null, - repackaging_costs: null, - handling_costs: null, - userDefinedHandlingCosts: false, - }; - - this.destinations.destinations.push(destination); - - return [destination.id]; - - } else { - const id = node.id; - - this.processDestinationMassEdit = true; - - - const toBeUpdated = this.destinations.fromMassEditView ? this.destinations.premise_ids : this.premisses?.filter(p => this.selectedIds.includes(p.id)).map(p => p.id); - - if (toBeUpdated === null || toBeUpdated.length === 0) return; - - const body = {destination_node_id: id, premise_id: toBeUpdated}; - const url = `${config.backendUrl}/calculation/destination/`; - - - const {data: destinations} = await performRequest(this, 'POST', url, body).catch(e => { - this.loading = false; - this.selectedLoading = false; - this.processDestinationMassEdit = false; - throw e; - }); - - const mappedIds = [] - - for (const id of Object.keys(destinations)) { - const premise = this.premisses.find(p => String(p.id) === id) - premise.destinations.push(destinations[id]); - const mappedDestination = this.copyAllFromPremises(destinations[id], true); - mappedIds.push(mappedDestination.id); - this.destinations.destinations.push(mappedDestination); - } - - this.processDestinationMassEdit = false; - - return mappedIds; - } - }, } }); \ No newline at end of file diff --git a/src/frontend/src/store/premiseEdit.js b/src/frontend/src/store/premiseEdit.js index d440372..02749ab 100644 --- a/src/frontend/src/store/premiseEdit.js +++ b/src/frontend/src/store/premiseEdit.js @@ -1,7 +1,5 @@ import {defineStore} from 'pinia' import {config} from '@/config' -import {toRaw} from "vue"; -import {useNotificationStore} from "@/store/notification.js"; import logger from "@/logger.js" import performRequest from '@/backend.js' @@ -164,7 +162,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', { this.loading = true; - const direction = (type !== this.sortedBy) ? 'desc' : (this.order.get(type) === 'asc' ? 'desc' : 'asc'); + const direction = (type !== this.sortedBy) ? this.order.get(type) : (this.order.get(type) === 'asc' ? 'desc' : 'asc'); const temp = this.premisses.slice(); temp.sort((a, b) => { @@ -179,7 +177,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', { else return a.id - b.id; }); - console.log("sort", this.sortedBy, direction, type); + this.premisses = temp; this.sortedBy = type; this.order.set(type, direction); diff --git a/src/frontend/src/store/premiseSingleEdit.js b/src/frontend/src/store/premiseSingleEdit.js index bc5ba56..1a01135 100644 --- a/src/frontend/src/store/premiseSingleEdit.js +++ b/src/frontend/src/store/premiseSingleEdit.js @@ -90,9 +90,13 @@ export const usePremiseSingleEditStore = defineStore('premiseSingleEdit', { if (this.premise === null) return; this.routing = true; - const body = {destination_node_id: node.id, premise_id: [this.premise.id]}; + const destinationNodeIds = {}; + destinationNodeIds[this.premise.id] = [node.id, ...this.premise.destinations.map(d => d.destination_node.id)]; + + const body = {destination_node_ids: destinationNodeIds}; const url = `${config.backendUrl}/calculation/destination/`; + logger.info("addDestination", body, url); const {data: destinations} = await performRequest(this, 'POST', url, body).catch(e => { this.routing = false; @@ -101,10 +105,11 @@ export const usePremiseSingleEditStore = defineStore('premiseSingleEdit', { const ids = [] - for (const destId of Object.keys(destinations)) { - this.premise.destinations.push(destinations[destId]); - ids.push(destinations[destId].id); - } + if (destinations[this.premise.id]?.length !== 0) + for (const destId of Object.keys(destinations[this.premise.id])) { + this.premise.destinations.push(destinations[this.premise.id][destId]); + ids.push(destinations[this.premise.id][destId].id); + } this.routing = false; 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 24bc8bd..c1a0eb4 100644 --- a/src/main/java/de/avatic/lcc/controller/calculation/PremiseController.java +++ b/src/main/java/de/avatic/lcc/controller/calculation/PremiseController.java @@ -29,8 +29,6 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; import java.util.Map; @@ -176,14 +174,14 @@ public class PremiseController { @PostMapping({"/destination", "/destination/"}) @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')") - public ResponseEntity> createDestination(@RequestBody @Valid DestinationCreateDTO destinationCreateDTO) { - return ResponseEntity.ok(destinationService.createDestination(destinationCreateDTO)); + public ResponseEntity>> createDestination(@RequestBody @Valid DestinationCreateDTO destinationCreateDTO) { + return ResponseEntity.ok(destinationService.massSetDestinations(destinationCreateDTO)); } @PutMapping({"/destination", "/destination/"}) @PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')") public ResponseEntity>> setDestination(@RequestBody DestinationSetDTO destinationSetDTO) { - return ResponseEntity.ok(destinationService.setDestination(destinationSetDTO)); + return ResponseEntity.ok(destinationService.massSetDestinationProperties(destinationSetDTO)); } @GetMapping({"/destination/{id}", "/destination/{id}/"}) diff --git a/src/main/java/de/avatic/lcc/dto/calculation/edit/destination/DestinationCreateDTO.java b/src/main/java/de/avatic/lcc/dto/calculation/edit/destination/DestinationCreateDTO.java index e2fa2b2..7ffff82 100644 --- a/src/main/java/de/avatic/lcc/dto/calculation/edit/destination/DestinationCreateDTO.java +++ b/src/main/java/de/avatic/lcc/dto/calculation/edit/destination/DestinationCreateDTO.java @@ -6,32 +6,20 @@ import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.util.List; +import java.util.Map; public class DestinationCreateDTO { - @NotEmpty (message = "At least one premise must be selected") - @NotNull (message = "At least one premise must be selected") - @JsonProperty("premise_id") - List<@Min(value = 1, message = "Invalid premise id") Integer> premiseId; + @NotEmpty(message = "At least one premise must be selected") + @NotNull(message = "At least one premise must be selected") + @JsonProperty("destination_node_ids") + Map> destinationNodeIds; - @Min(value = 1, message = "Invalid destination node id") - @NotNull (message = "Destination node id must be provided") - @JsonProperty("destination_node_id") - Integer destinationNodeId; - - public List getPremiseId() { - return premiseId; + public Map> getDestinationNodeIds() { + return destinationNodeIds; } - public void setPremiseId(List premiseId) { - this.premiseId = premiseId; - } - - public Integer getDestinationNodeId() { - return destinationNodeId; - } - - public void setDestinationNodeId(Integer destinationNodeId) { - this.destinationNodeId = destinationNodeId; + public void setDestinationNodeIds(Map> destinationNodeIds) { + this.destinationNodeIds = destinationNodeIds; } } diff --git a/src/main/java/de/avatic/lcc/dto/calculation/edit/destination/DestinationSetDTO.java b/src/main/java/de/avatic/lcc/dto/calculation/edit/destination/DestinationSetDTO.java index 024ca56..adce6cb 100644 --- a/src/main/java/de/avatic/lcc/dto/calculation/edit/destination/DestinationSetDTO.java +++ b/src/main/java/de/avatic/lcc/dto/calculation/edit/destination/DestinationSetDTO.java @@ -1,11 +1,10 @@ package de.avatic.lcc.dto.calculation.edit.destination; import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.validation.constraints.DecimalMin; -import jakarta.validation.constraints.Digits; -import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.*; import java.util.List; +import java.util.Map; public class DestinationSetDTO { @@ -15,6 +14,7 @@ public class DestinationSetDTO { @JsonProperty("destinations") List destinations; + public List getPremiseId() { return premiseId; } diff --git a/src/main/java/de/avatic/lcc/repositories/premise/DestinationRepository.java b/src/main/java/de/avatic/lcc/repositories/premise/DestinationRepository.java index cedb55a..3b47aa3 100644 --- a/src/main/java/de/avatic/lcc/repositories/premise/DestinationRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/premise/DestinationRepository.java @@ -17,6 +17,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.*; +import java.util.stream.Collectors; @Service public class DestinationRepository { @@ -50,6 +51,21 @@ public class DestinationRepository { return jdbcTemplate.query(query, new DestinationMapper(), id); } + @Transactional + public List getByPremiseIdAndUserId(Integer premiseId, Integer userId) { + + String premiseCheckQuery = "SELECT COUNT(*) FROM premise WHERE id = ? AND user_id = ?"; + + Integer count = jdbcTemplate.queryForObject(premiseCheckQuery, Integer.class, premiseId, userId); + + if (count == null || count == 0) { + return Collections.emptyList(); + } + + String query = "SELECT * FROM premise_destination WHERE premise_id = ?"; + return jdbcTemplate.query(query, new DestinationMapper(), premiseId); + } + @Transactional public void update(Integer id, Integer annualAmount, BigDecimal repackingCost, BigDecimal disposalCost, BigDecimal handlingCost, Boolean isD2d, BigDecimal d2dRate, BigDecimal d2dLeadTime) { if (id == null) { @@ -73,7 +89,7 @@ public class DestinationRepository { setClauses.add("handling_cost = :handlingCost"); parameters.put("handlingCost", handlingCost); - var setD2d = isD2d != null ? isD2d : false; + var setD2d = isD2d != null ? isD2d : false; setClauses.add("is_d2d = :isD2d"); parameters.put("isD2d", setD2d); @@ -143,10 +159,10 @@ public class DestinationRepository { String placeholders = String.join(",", Collections.nCopies(ids.size(), "?")); String query = String.format(""" - SELECT pd.id AS pd_id, p.user_id AS user_id - FROM premise_destination pd - JOIN premise p ON pd.premise_id = p.id - WHERE pd.id IN (%s)""", placeholders); + SELECT pd.id AS pd_id, p.user_id AS user_id + FROM premise_destination pd + JOIN premise p ON pd.premise_id = p.id + WHERE pd.id IN (%s)""", placeholders); return jdbcTemplate.query(query, rs -> { Map result = new HashMap<>(); @@ -157,20 +173,73 @@ public class DestinationRepository { }, ids.toArray()); } +// @Transactional +// public List getByPremiseIdsAndNodeId(List premiseId, Integer nodeId, Integer userId) { +// String placeholder = String.join(",", Collections.nCopies(premiseId.size(), "?")); +// String query = "SELECT * FROM premise_destination JOIN premise ON premise_destination.premise_id = premise.id WHERE premise_destination.premise_id IN (" + placeholder + ") AND premise_destination.destination_node_id = ? AND premise.user_id = ?"; +// +// // Create array with all parameters +// Object[] params = new Object[premiseId.size() + 2]; +// for (int i = 0; i < premiseId.size(); i++) { +// params[i] = premiseId.get(i); +// } +// params[premiseId.size()] = nodeId; +// params[premiseId.size() + 1] = userId; +// +// return jdbcTemplate.query(query, new DestinationMapper(), params); +// } + @Transactional - public List getByPremiseIdsAndNodeId(List premiseId, Integer nodeId, Integer userId) { - String placeholder = String.join(",", Collections.nCopies(premiseId.size(), "?")); - String query = "SELECT * FROM premise_destination JOIN premise ON premise_destination.premise_id = premise.id WHERE premise_destination.premise_id IN (" + placeholder + ") AND premise_destination.destination_node_id = ? AND premise.user_id = ?"; + public Map> getByPremiseIdsAndNodeIds(Map> premiseToNodes, Integer userId) { + if (premiseToNodes.isEmpty()) { + return new HashMap<>(); + } + + // Flatten all premise IDs and node IDs for the query + List allPremiseIds = new ArrayList<>(premiseToNodes.keySet()); + Set allNodeIds = premiseToNodes.values().stream() + .flatMap(List::stream) + .collect(Collectors.toSet()); + + String premisePlaceholder = String.join(",", Collections.nCopies(allPremiseIds.size(), "?")); + String nodePlaceholder = String.join(",", Collections.nCopies(allNodeIds.size(), "?")); + + String query = "SELECT * FROM premise_destination " + + "JOIN premise ON premise_destination.premise_id = premise.id " + + "WHERE premise_destination.premise_id IN (" + premisePlaceholder + ") " + + "AND premise_destination.destination_node_id IN (" + nodePlaceholder + ") " + + "AND premise.user_id = ?"; // Create array with all parameters - Object[] params = new Object[premiseId.size() + 2]; - for (int i = 0; i < premiseId.size(); i++) { - params[i] = premiseId.get(i); - } - params[premiseId.size()] = nodeId; - params[premiseId.size() + 1] = userId; + Object[] params = new Object[allPremiseIds.size() + allNodeIds.size() + 1]; + int index = 0; - return jdbcTemplate.query(query, new DestinationMapper(), params); + for (Integer premiseId : allPremiseIds) { + params[index++] = premiseId; + } + for (Integer nodeId : allNodeIds) { + params[index++] = nodeId; + } + params[index] = userId; + + List allDestinations = jdbcTemplate.query(query, new DestinationMapper(), params); + + // Group destinations by premise ID and filter by the requested node IDs + Map> result = new HashMap<>(); + + for (Map.Entry> entry : premiseToNodes.entrySet()) { + Integer premiseId = entry.getKey(); + Set requestedNodeIds = new HashSet<>(entry.getValue()); + + List filteredDestinations = allDestinations.stream() + .filter(d -> d.getPremiseId().equals(premiseId) && + requestedNodeIds.contains(d.getDestinationNodeId())) + .collect(Collectors.toList()); + + result.put(premiseId, filteredDestinations); + } + + return result; } @Transactional diff --git a/src/main/java/de/avatic/lcc/service/access/DestinationService.java b/src/main/java/de/avatic/lcc/service/access/DestinationService.java index 384978f..283fd8d 100644 --- a/src/main/java/de/avatic/lcc/service/access/DestinationService.java +++ b/src/main/java/de/avatic/lcc/service/access/DestinationService.java @@ -3,14 +3,12 @@ package de.avatic.lcc.service.access; import de.avatic.lcc.dto.calculation.DestinationDTO; import de.avatic.lcc.dto.calculation.edit.destination.DestinationCreateDTO; import de.avatic.lcc.dto.calculation.edit.destination.DestinationSetDTO; -import de.avatic.lcc.dto.calculation.edit.destination.DestinationSetListItemDTO; import de.avatic.lcc.dto.calculation.edit.destination.DestinationUpdateDTO; import de.avatic.lcc.model.db.nodes.Node; import de.avatic.lcc.model.db.premises.Premise; import de.avatic.lcc.model.db.premises.route.*; import de.avatic.lcc.repositories.NodeRepository; import de.avatic.lcc.repositories.premise.*; -import de.avatic.lcc.repositories.properties.PropertyRepository; import de.avatic.lcc.repositories.users.UserNodeRepository; import de.avatic.lcc.service.calculation.RoutingService; import de.avatic.lcc.service.transformer.premise.DestinationTransformer; @@ -51,8 +49,52 @@ public class DestinationService { this.authorizationService = authorizationService; } + + private Map> processDestinations(List premisesToProcess, Map> destinationNodeIds, Integer annualAmount, Number repackingCost, Number disposalCost, Number handlingCost, Map> routes) { + + var destMap = new HashMap>(); + + for (var premise : premisesToProcess) { + + var destinations = new ArrayList(); + + for (var destinationNodeId : destinationNodeIds.get(premise.getId())) { + + Node destinationNode = nodeRepository.getById(destinationNodeId).orElseThrow(); + + var destination = new Destination(); + destination.setDestinationNodeId(destinationNodeId); + destination.setPremiseId(premise.getId()); + destination.setAnnualAmount(annualAmount); + destination.setD2d(false); + destination.setLeadTimeD2d(null); + destination.setRateD2d(null); + destination.setDisposalCost(disposalCost == null ? null : BigDecimal.valueOf(disposalCost.doubleValue())); + destination.setHandlingCost(handlingCost == null ? null : BigDecimal.valueOf(handlingCost.doubleValue())); + destination.setRepackingCost(repackingCost == null ? null : BigDecimal.valueOf(repackingCost.doubleValue())); + destination.setCountryId(destinationNode.getCountryId()); + destination.setGeoLat(destinationNode.getGeoLat()); + destination.setGeoLng(destinationNode.getGeoLng()); + destination.setId(destinationRepository.insert(destination)); + + Node source = premise.getSupplierNodeId() == null ? userNodeRepository.getById(premise.getUserSupplierNodeId()).orElseThrow() : nodeRepository.getById(premise.getSupplierNodeId()).orElseThrow(); + + if (routes != null) + //noinspection SpringTransactionalMethodCallsInspection + saveRoute(routes.get(new RouteIds(source.getId(), destinationNodeId, premise.getSupplierNodeId() == null)), destination.getId()); + + + destinations.add(destination); + } + + destMap.put(premise.getId(), destinations); + } + + return destMap; + } + @Transactional - public Map> setDestination(DestinationSetDTO dto) { + public Map> massSetDestinationProperties(DestinationSetDTO dto) { var admin = authorizationService.isSuper(); Integer userId = authorizationService.getUserId(); @@ -62,77 +104,69 @@ public class DestinationService { deleteAllDestinationsByPremiseId(dto.getPremiseId(), false); var premisses = premiseRepository.getPremisesById(dto.getPremiseId()); - Map> routes = findRoutes(premisses, dto.getDestinations().stream().map(DestinationSetListItemDTO::getDestinationNodeId).toList()); + // TODO no routing in set ... only props. +// Map> routes = findRoutes(premisses, dto.getDestinations().stream().map(DestinationSetListItemDTO::getDestinationNodeId).toList()); - Map> destinations = dto.getDestinations().stream() - .flatMap(destination -> createDestination(premisses, destination.getDestinationNodeId(), destination.getAnnualAmount(), destination.getRepackingCost(), destination.getDisposalCost(), destination.getHandlingCost(), routes).stream()) - .collect(Collectors.groupingBy(Destination::getPremiseId)); +// Map> destinations = dto.getDestinations().stream() +// .flatMap(destination -> processDestinations(premisses, destination.getDestinationNodeId(), destination.getAnnualAmount(), destination.getRepackingCost(), destination.getDisposalCost(), destination.getHandlingCost(), null).stream()) +// .collect(Collectors.groupingBy(Destination::getPremiseId)); - return destinations.entrySet().stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - entry -> entry.getValue().stream().map(destinationTransformer::toDestinationDTO).toList())); +// return destinations.entrySet().stream() +// .collect(Collectors.toMap( +// Map.Entry::getKey, +// entry -> entry.getValue().stream().map(destinationTransformer::toDestinationDTO).toList())); + + return null; } + private List getDestinationToRemove(List oldDestinations, List newIds) { + return oldDestinations.stream().filter(dest -> !newIds.contains(dest.getDestinationNodeId())).map(Destination::getId).collect(Collectors.toList()); + } - private List createDestination(List premisesToProcess, Integer destinationNodeId, Integer annualAmount, Number repackingCost, Number disposalCost, Number handlingCost, Map> routes) { - - Node destinationNode = nodeRepository.getById(destinationNodeId).orElseThrow(); - - var destinations = new ArrayList(); - - for (var premise : premisesToProcess) { - var destination = new Destination(); - destination.setDestinationNodeId(destinationNodeId); - destination.setPremiseId(premise.getId()); - destination.setAnnualAmount(annualAmount); - destination.setD2d(false); - destination.setLeadTimeD2d(null); - destination.setRateD2d(null); - destination.setDisposalCost(disposalCost == null ? null : BigDecimal.valueOf(disposalCost.doubleValue())); - destination.setHandlingCost(handlingCost == null ? null : BigDecimal.valueOf(handlingCost.doubleValue())); - destination.setRepackingCost(repackingCost == null ? null : BigDecimal.valueOf(repackingCost.doubleValue())); - destination.setCountryId(destinationNode.getCountryId()); - destination.setGeoLat(destinationNode.getGeoLat()); - destination.setGeoLng(destinationNode.getGeoLng()); - destination.setId(destinationRepository.insert(destination)); - - Node source = premise.getSupplierNodeId() == null ? userNodeRepository.getById(premise.getUserSupplierNodeId()).orElseThrow() : nodeRepository.getById(premise.getSupplierNodeId()).orElseThrow(); - - //noinspection SpringTransactionalMethodCallsInspection - saveRoute(routes.get(new RouteIds(source.getId(), destinationNodeId, premise.getSupplierNodeId() == null)), destination.getId()); - - destinations.add(destination); - } - - return destinations; - + private List getNodeIdsToAdd(List oldDestinations, List newIds) { + var oldIds = oldDestinations.stream().map(Destination::getDestinationNodeId).toList(); + return newIds.stream().filter(id -> !oldIds.contains(id)).collect(Collectors.toList()); } @Transactional - public Map createDestination(DestinationCreateDTO dto) { + public Map> massSetDestinations(DestinationCreateDTO dto) { Integer userId = authorizationService.getUserId(); - var existingDestinations = destinationRepository.getByPremiseIdsAndNodeId(dto.getPremiseId(), dto.getDestinationNodeId(), userId); + Map> destinationsToProcess = new HashMap<>(); - var premisesIdsToProcess = new ArrayList(); - for (var premiseId : dto.getPremiseId()) { - if (existingDestinations.stream().map(Destination::getPremiseId).noneMatch(id -> id.equals(premiseId))) { - premisesIdsToProcess.add(premiseId); - } + var requestedDestMap = dto.getDestinationNodeIds(); + var existingDestMap = dto.getDestinationNodeIds().keySet().stream().collect(Collectors.toMap(id -> id, id -> destinationRepository.getByPremiseIdAndUserId(id, userId))); + + for (Integer premiseId : requestedDestMap.keySet()) { + var requestedDestinations = requestedDestMap.get(premiseId); + var existingDestinations = existingDestMap.getOrDefault(premiseId, Collections.emptyList()); + + /* remove deselected */ + var toRemove = getDestinationToRemove(existingDestinations, requestedDestinations); + deleteDestinationsById(toRemove, false); + + /* find new selected */ + var toAdd = getNodeIdsToAdd(existingDestinations, requestedDestinations); + destinationsToProcess.put(premiseId, toAdd); } - if (premisesIdsToProcess.isEmpty()) + if (destinationsToProcess.isEmpty()) return new HashMap<>(); - var premisses = premiseRepository.getPremisesById(premisesIdsToProcess); - Map> routes = findRoutes(premisses, Collections.singletonList(dto.getDestinationNodeId())); + var premisses = premiseRepository.getPremisesById(new ArrayList<>(destinationsToProcess.keySet())); + Map> routes = findRoutes(premisses, destinationsToProcess); - var destinations = createDestination(premisses, dto.getDestinationNodeId(), null, null, null, null, routes); - return destinations.stream().collect(Collectors.toMap(Destination::getPremiseId, destinationTransformer::toDestinationDTO)); + var destinations = processDestinations(premisses, destinationsToProcess, null, null, null, null, routes); + return destinations.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> entry.getValue().stream() + .map(destinationTransformer::toDestinationDTO) + .toList() + )); } public DestinationDTO getDestination(Integer id) { @@ -170,14 +204,16 @@ public class DestinationService { } - private Map> findRoutes(List premisses, List destinationIds) { + private Map> findRoutes(List premisses, Map> routingRequest) { Map> routes = new HashMap<>(); Map nodes = new HashMap<>(); Map userNodes = new HashMap<>(); for (var premise : premisses) { - for (var destinationId : destinationIds) { + + for (var destinationId : routingRequest.get(premise.getId())) { + boolean isUserSupplierNode = (premise.getSupplierNodeId() == null); var ids = new RouteIds(isUserSupplierNode ? premise.getUserSupplierNodeId() : premise.getSupplierNodeId(), destinationId, isUserSupplierNode); if (routes.containsKey(ids)) continue; @@ -194,6 +230,8 @@ public class DestinationService { userNodes.put(premise.getUserSupplierNodeId(), userNodeRepository.getById(premise.getUserSupplierNodeId()).orElseThrow()); } + + //TODO in parallel routes.put(ids, routingService.findRoutes(nodes.get(destinationId), isUserSupplierNode ? userNodes.get(premise.getUserSupplierNodeId()) : nodes.get(premise.getSupplierNodeId()), isUserSupplierNode)); } } @@ -255,6 +293,11 @@ public class DestinationService { destinations.forEach(destination -> deleteDestinationById(destination.getId(), deleteRoutesOnly)); } + @Transactional + public void deleteDestinationsById(List ids, boolean deleteRoutesOnly) { + ids.forEach(id -> deleteDestinationById(id, deleteRoutesOnly)); + } + @Transactional public void deleteDestinationById(Integer id, boolean deleteRoutesOnly) { var admin = authorizationService.isSuper(); diff --git a/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java b/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java index e4767ae..26e4f9a 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java @@ -334,7 +334,7 @@ public class RoutingService { } finalSection.setRate(matrixRate); - finalSection.setApproxDistance(distanceService.getDistance(container.getSourceNode(), toNode, false)); + finalSection.setApproxDistance(distanceService.getDistance(container.getSourceNode(), toNode, true)); rates.add(finalSection); } @@ -699,7 +699,7 @@ public class RoutingService { if (matrixRate.isPresent()) { matrixRateObj.setRate(matrixRate.get()); - matrixRateObj.setApproxDistance(distanceService.getDistance(startNode, endNode, false)); + matrixRateObj.setApproxDistance(distanceService.getDistance(startNode, endNode, true)); container.getRates().add(matrixRateObj); return matrixRateObj; } else { diff --git a/src/test/java/de/avatic/lcc/controller/calculation/CalculationIntegrationTests.java b/src/test/java/de/avatic/lcc/controller/calculation/CalculationIntegrationTests.java index 2212ba4..921bee6 100644 --- a/src/test/java/de/avatic/lcc/controller/calculation/CalculationIntegrationTests.java +++ b/src/test/java/de/avatic/lcc/controller/calculation/CalculationIntegrationTests.java @@ -17,9 +17,7 @@ import org.springframework.test.context.jdbc.Sql; import org.springframework.test.web.servlet.MockMvc; import org.springframework.transaction.annotation.Transactional; -import java.util.Arrays; -import java.util.Collections; -import java.util.Map; +import java.util.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; @@ -62,8 +60,11 @@ public class CalculationIntegrationTests { var premise3 = premisesBeforeUpdate.stream().filter(p -> p.getHuUnitCount() == 3).findFirst().orElseThrow(); var createDto = new DestinationCreateDTO(); - createDto.setPremiseId(Collections.singletonList(premise1.getId())); - createDto.setDestinationNodeId(nodeId); + + var map = new HashMap>(); + map.put(premise1.getId(), List.of(nodeId)); + createDto.setDestinationNodeIds(map); + var response = mockMvc.perform(post("/api/calculation/destination") .content(objectMapper.writeValueAsString(createDto)) diff --git a/src/test/java/de/avatic/lcc/controller/calculation/DestinationIntegrationTest.java b/src/test/java/de/avatic/lcc/controller/calculation/DestinationIntegrationTest.java index 6c0cdb3..043b2b2 100644 --- a/src/test/java/de/avatic/lcc/controller/calculation/DestinationIntegrationTest.java +++ b/src/test/java/de/avatic/lcc/controller/calculation/DestinationIntegrationTest.java @@ -18,6 +18,7 @@ import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; @@ -61,8 +62,11 @@ public class DestinationIntegrationTest { var premise3 = premisesBeforeUpdate.stream().filter(p -> p.getHuUnitCount() == 3).findFirst().orElseThrow(); var dto = new DestinationCreateDTO(); - dto.setPremiseId(Arrays.asList(premise1.getId(), premise3.getId())); - dto.setDestinationNodeId(nodeId); + + var map = new HashMap>(); + map.put(premise1.getId(), List.of(nodeId)); + map.put(premise3.getId(), List.of(nodeId)); + dto.setDestinationNodeIds(map); mockMvc.perform(post("/api/calculation/destination") .content(objectMapper.writeValueAsString(dto))