Removed unused customs store and related functionality, deprecated Toast component, optimized tariff rate modal behavior, and added HS code index updates in the database schema. Enhanced user group management APIs, refined material update handling, improved input validation in user management UI, reorganized modal styles, and addressed redundant logic across services and components.

This commit is contained in:
Jan 2025-11-20 10:47:05 +01:00
parent a381ac3299
commit be7b94753e
18 changed files with 178 additions and 214 deletions

View file

@ -3,20 +3,21 @@
<div class="edit-user"> <div class="edit-user">
<div>Workday ID</div> <div>Workday ID</div>
<div> <div>
<div class="text-container" :class="{disabled: !isNewUser}"><input :disabled="!isNewUser" class="input-field" <div class="text-container" :class="{disabled: !isNewUser}"><input type="text" autocomplete="off" :disabled="!isNewUser" class="input-field"
v-model="user.workday_id"/></div> v-model="user.workday_id" maxlength="32"/>
</div>
</div> </div>
<div>Firstname</div> <div>Firstname</div>
<div> <div>
<div class="text-container"><input class="input-field" v-model="user.firstname"/></div> <div class="text-container"><input type="text" autocomplete="off" class="input-field" v-model="user.firstname" maxlength="100"/></div>
</div> </div>
<div>Lastname</div> <div>Lastname</div>
<div> <div>
<div class="text-container"><input class="input-field" v-model="user.lastname"/></div> <div class="text-container"><input type="text" autocomplete="off" class="input-field" v-model="user.lastname" maxlength="100"/></div>
</div> </div>
<div>E-Mail</div> <div>E-Mail</div>
<div> <div>
<div class="text-container"><input class="input-field" v-model="user.mail"/></div> <div class="text-container"><input autocomplete="off" type="email" class="input-field" v-model="user.mail" maxlength="254"/></div>
</div> </div>
<div class="group-header">Groups</div> <div class="group-header">Groups</div>
<div class="groups-container"> <div class="groups-container">
@ -62,6 +63,11 @@ export default {
} }
}, },
methods: { methods: {
validate(type) {
if (type === "firstname") {
firstName
}
},
closeModal(save) { closeModal(save) {
this.$emit("close", save); this.$emit("close", save);
}, },
@ -82,7 +88,7 @@ export default {
} else { } else {
if ((idx ?? null) !== null && idx !== -1) if ((idx ?? null) !== null && idx !== -1)
this.user.groups.splice(idx,1); this.user.groups.splice(idx, 1);
} }
}, },
@ -122,22 +128,13 @@ export default {
<style scoped> <style scoped>
.text-container.disabled {
background-color: #f3f4f6; .add-app-footer {
cursor: not-allowed; display: flex;
border-color: #f3f4f6; justify-content: flex-end;
gap: 1.6rem;
} }
.text-container.disabled input {
cursor: not-allowed;
}
.groups-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
gap: 1.8rem;
}
.edit-user-container { .edit-user-container {
min-width: 100rem; min-width: 100rem;
@ -153,23 +150,17 @@ export default {
align-items: center; align-items: center;
} }
.groups-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
gap: 1.8rem;
}
.group-header { .group-header {
align-self: start; align-self: start;
} }
.text-container {
display: flex;
align-items: center;
background: white;
border-radius: 0.4rem;
padding: 0.6rem 1.2rem;
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);*/
border: 0.2rem solid #E3EDFF;
transition: all 0.1s ease;
flex: 1 0 auto;
}
.input-field { .input-field {
border: none; border: none;
@ -181,12 +172,39 @@ export default {
color: #002F54; color: #002F54;
width: 100%; width: 100%;
min-width: 5rem; min-width: 5rem;
padding: 0.6rem 1.2rem;
} }
.add-app-footer {
display: flex; .text-container.disabled {
justify-content: flex-end; background-color: #f3f4f6;
gap: 1.6rem; cursor: not-allowed;
border-color: #f3f4f6;
} }
.text-container.disabled input {
cursor: not-allowed;
}
.text-container:hover:not(.disabled) {
background: #EEF4FF;
border: 0.2rem solid #8DB3FE;
transform: scale(1.01);
}
.text-container {
display: flex;
align-items: center;
background: white;
border-radius: 0.4rem;
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);*/
border: 0.2rem solid #E3EDFF;
transition: all 0.1s ease;
flex: 1 0 auto;
}
</style> </style>

View file

@ -18,7 +18,6 @@
</div> </div>
</div> </div>
<Toast ref="toast"/>
<transition name="list-edit-container" tag="div"> <transition name="list-edit-container" tag="div">
<transition-group name="list-edit" mode="out-in" class="edit-calculation-list-container" tag="div"> <transition-group name="list-edit" mode="out-in" class="edit-calculation-list-container" tag="div">
@ -64,6 +63,7 @@
v-model:partNumber="componentProps.partNumber" v-model:partNumber="componentProps.partNumber"
v-model:hsCode="componentProps.hsCode" v-model:hsCode="componentProps.hsCode"
v-model:tariffRate="componentProps.tariffRate" v-model:tariffRate="componentProps.tariffRate"
v-model:tariffUnlocked="componentProps.tariffUnlocked"
v-model:description="componentProps.description" v-model:description="componentProps.description"
v-model:price="componentProps.price" v-model:price="componentProps.price"
v-model:overSeaShare="componentProps.overSeaShare" v-model:overSeaShare="componentProps.overSeaShare"
@ -81,13 +81,12 @@
:countryId=null :countryId=null
:responsive="false" :responsive="false"
@close="closeEditModalAction('cancel')" @close="closeEditModalAction('cancel')"
@start-lookup="doLookupOnClose"
> >
</component> </component>
<div class="modal-content-footer" v-if="showModalFooter"> <div class="modal-content-footer" >
<basic-button :show-icon="false" @click="closeEditModalAction('accept')">OK</basic-button> <basic-button v-if="!modalCloseOnly" :show-icon="false" @click="closeEditModalAction('accept')">OK</basic-button>
<basic-button variant="secondary" :show-icon="false" @click="closeEditModalAction('cancel')">Cancel <basic-button variant="secondary" :show-icon="false" @click="closeEditModalAction('cancel')"> {{ modalCloseOnly ? "Close" : "Cancel" }}
</basic-button> </basic-button>
</div> </div>
</div> </div>
@ -113,9 +112,7 @@ import PriceEdit from "@/components/layout/edit/PriceEdit.vue";
import MaterialEdit from "@/components/layout/edit/MaterialEdit.vue"; import MaterialEdit from "@/components/layout/edit/MaterialEdit.vue";
import PackagingEdit from "@/components/layout/edit/PackagingEdit.vue"; import PackagingEdit from "@/components/layout/edit/PackagingEdit.vue";
import DestinationListView from "@/components/layout/edit/DestinationListView.vue"; import DestinationListView from "@/components/layout/edit/DestinationListView.vue";
import Toast from "@/components/UI/Toast.vue";
import logger from "@/logger.js"; import logger from "@/logger.js";
import {useCustomsStore} from "@/store/customs.js";
import {useNotificationStore} from "@/store/notification.js"; import {useNotificationStore} from "@/store/notification.js";
@ -129,7 +126,6 @@ const COMPONENT_TYPES = {
export default { export default {
name: "MassEdit", name: "MassEdit",
components: { components: {
Toast,
Modal, Modal,
MassEditDialog, MassEditDialog,
ListEdit, ListEdit,
@ -140,7 +136,7 @@ export default {
BasicButton BasicButton
}, },
computed: { computed: {
...mapStores(usePremiseEditStore, useCustomsStore, useNotificationStore), ...mapStores(usePremiseEditStore, useNotificationStore),
hasSelection() { hasSelection() {
if (this.premiseEditStore.isLoading || this.premiseEditStore.selectedLoading) { if (this.premiseEditStore.isLoading || this.premiseEditStore.selectedLoading) {
return false; return false;
@ -168,8 +164,8 @@ export default {
showMultiselectAction() { showMultiselectAction() {
return this.selectCount > 0; return this.selectCount > 0;
}, },
showModalFooter() { modalCloseOnly() {
return this.modalType !== 'supplier'; return this.modalType === 'material' && !this.componentProps.tariffUnlocked;
}, },
showEditModal() { showEditModal() {
return ((this.modalType ?? null) !== null); return ((this.modalType ?? null) !== null);
@ -208,13 +204,12 @@ export default {
data() { data() {
return { return {
doLookup: false,
ids: [], ids: [],
bulkQuery: null, bulkQuery: null,
modalType: null, modalType: null,
componentsData: { componentsData: {
price: {props: {price: 0, overSeaShare: 0, includeFcaFee: false}}, price: {props: {price: 0, overSeaShare: 0, includeFcaFee: false}},
material: {props: {partNumber: "", hsCode: null, tariffRate: 0.00, description: "", hideDescription: false}}, material: {props: {partNumber: "", hsCode: null, tariffRate: null, tariffUnlocked: false, description: "", hideDescription: false}},
packaging: { packaging: {
props: { props: {
length: 0, length: 0,
@ -256,16 +251,7 @@ export default {
this.showCalculationModal = true; this.showCalculationModal = true;
const error = await this.premiseEditStore.startCalculation(); const error = await this.premiseEditStore.startCalculation();
if (error !== null) { if (error === null) {
this.$refs.toast.addToast({
icon: 'warning',
message: error.message,
title: "Cannot start calculation",
variant: 'exception',
duration: 8000
})
} else {
this.closeMassEdit() this.closeMassEdit()
} }
this.showCalculationModal = false; this.showCalculationModal = false;
@ -298,9 +284,6 @@ export default {
logger.info("open modal", massEdit, this.modalType, this.editIds, this.dataSourceId) logger.info("open modal", massEdit, this.modalType, this.editIds, this.dataSourceId)
}, },
doLookupOnClose(doLookup) {
this.doLookup = doLookup;
},
async closeEditModalAction(action) { async closeEditModalAction(action) {
if (this.modalType === "destinations") { if (this.modalType === "destinations") {
if (action === "accept") { if (action === "accept") {
@ -316,11 +299,7 @@ export default {
await this.premiseEditStore.batchUpdatePrice(this.editIds, props); await this.premiseEditStore.batchUpdatePrice(this.editIds, props);
break; break;
case "material": case "material":
let tariffRates = null; await this.premiseEditStore.batchUpdateMaterial(this.editIds, props);
if (this.doLookup) {
tariffRates = await this.tariffLookUp(props.hsCode);
}
await this.premiseEditStore.batchUpdateMaterial(this.editIds, props, tariffRates);
break; break;
case "packaging": case "packaging":
await this.premiseEditStore.batchUpdatePackaging(this.editIds, props); await this.premiseEditStore.batchUpdatePackaging(this.editIds, props);
@ -332,24 +311,6 @@ export default {
this.fillData(this.modalType); this.fillData(this.modalType);
this.modalType = null; this.modalType = null;
}, },
async tariffLookUp(hsCode) {
const countryIds = this.premiseEditStore.getCountryIdByPremiseIds(this.editIds);
const query = {hsCode: hsCode, countryIds: [...new Set(Array.from(countryIds.values()))]};
const tariffRates = await this.customsStore.findTariffRate(query);
const found = new Map();
for(const editId of this.editIds) {
const countryId = countryIds.get(editId);
found.set(editId, tariffRates.find(rate => rate.countryId === countryId).value);
}
this.doLookup = false;
return found;
},
fillData(type, id = -1, hideDescription = false) { fillData(type, id = -1, hideDescription = false) {
if (id === -1) { if (id === -1) {
@ -361,6 +322,7 @@ export default {
partNumber: "", partNumber: "",
hsCode: null, hsCode: null,
tariffRate: null, tariffRate: null,
tariffUnlocked: false,
description: null, description: null,
hideDescription: hideDescription hideDescription: hideDescription
} }
@ -394,7 +356,8 @@ export default {
this.componentsData.material.props = { this.componentsData.material.props = {
partNumber: premise.material.part_number, partNumber: premise.material.part_number,
hsCode: premise.hs_code, hsCode: premise.hs_code,
tariffRate: premise.tariff_rate ?? 0.00, tariffRate: premise.tariff_rate ?? null,
tariffUnlocked: premise.tariff_unlocked,
description: premise.material.name ?? "", description: premise.material.name ?? "",
hideDescription: hideDescription hideDescription: hideDescription
} }
@ -431,20 +394,13 @@ export default {
margin: 3rem; margin: 3rem;
} }
.edit-calculation-spinner-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3.6rem;
flex: 1 1 auto;
font-size: 1.6rem;
}
.modal-content-container { .modal-content-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.6rem; gap: 1.6rem;
margin-top: 1.6rem;
min-width: 50rem;
} }
.modal-content-footer { .modal-content-footer {
@ -512,23 +468,6 @@ export default {
letter-spacing: 0.08rem; letter-spacing: 0.08rem;
} }
.edit-calculation-spinner-container {
display: flex;
align-items: center;
justify-content: center;
flex: 1 1 30rem
}
.spinner-box {
font-size: 1.6rem;
width: 24rem;
height: 12rem;
display: flex;
justify-content: center;
align-items: center;
}
.edit-calculation-container { .edit-calculation-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View file

@ -1,56 +0,0 @@
import {defineStore} from 'pinia'
import {config} from '@/config'
import performRequest from "@/backend.js";
export const useCustomsStore = defineStore('customs', {
state() {
return {
hsCodes: [],
tariffInfo: null,
loadingTariff: false,
}
},
getters: {
showWarning(state) {
return function (countryId) {
const info = state.tariffInfo?.find(t => t.countryId === countryId);
if ((info ?? null) !== null) {
return info.default || info.measures?.length > 1;
}
return false;
}
},
findByCountryId(state) {
return function (countryId) {
return state.tariffInfo?.find(t => t.countryId === countryId);
}
}
},
actions: {
async findTariffRate(query) {
this.loadingTariff = true;
const url = `${config.backendUrl}/customs?hs_code=${query.hsCode}&country_ids=${query.countryIds}`;
const resp = await performRequest(this, 'GET', url, null).catch(() => {
this.tariffInfo = null;
this.loadingTariff = false;
})
this.tariffInfo = resp.data;
this.tariffInfo.forEach(info => info.measures.sort((m1, m2) => { return parseInt(m1.code) - parseInt(m2.code); }));
this.loadingTariff = false;
return this.tariffInfo;
},
async findHsCode(query) {
const url = `${config.backendUrl}/customs/search?hs_code=${query}`;
const resp = await performRequest(this, 'GET', url, null);
this.hsCodes = resp.data;
return this.hsCodes;
}
}
});

View file

@ -263,7 +263,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
return await this.savePrice(ids, priceData); return await this.savePrice(ids, priceData);
}, },
async batchUpdateMaterial(ids, materialData, tariffRates = null) { async batchUpdateMaterial(ids, materialData) {
const updatedPremises = this.premisses.map(p => { const updatedPremises = this.premisses.map(p => {
if (ids.includes(p.id)) { if (ids.includes(p.id)) {
return { return {
@ -272,7 +272,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
...p.material, ...p.material,
}, },
...(materialData.hsCode !== null && {hs_code: materialData.hsCode}), ...(materialData.hsCode !== null && {hs_code: materialData.hsCode}),
...((tariffRates !== null) ? {tariff_rate: tariffRates.get(p.id)} : (materialData.tariffRate !== null && {tariff_rate: materialData.tariffRate})) ...((materialData.tariffRate !== null && {tariff_rate: materialData.tariffRate}))
}; };
} }
return p; return p;
@ -281,7 +281,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
return await this.saveMaterial(ids, materialData, tariffRates); return await this.saveMaterial(ids, materialData);
}, },
async batchUpdatePackaging(ids, packagingData) { async batchUpdatePackaging(ids, packagingData) {
@ -706,7 +706,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
return success; return success;
}, },
async saveMaterial(ids = null, materialData = null, tariffRates = null) { async saveMaterial(ids = null, materialData = null) {
let success = true; let success = true;
const toBeUpdated = this.premisses ? (ids ? (ids.map(id => this.premisses.find(p => String(p.id) === String(id)))) : (this.premisses.filter(p => p.selected))) : null; const toBeUpdated = this.premisses ? (ids ? (ids.map(id => this.premisses.find(p => String(p.id) === String(id)))) : (this.premisses.filter(p => p.selected))) : null;
@ -717,8 +717,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
premise_ids: toBeUpdated.map(p => p.id), premise_ids: toBeUpdated.map(p => p.id),
hs_code: materialData === null ? toBeUpdated[0].hs_code : materialData.hsCode, hs_code: materialData === null ? toBeUpdated[0].hs_code : materialData.hsCode,
tariff_rate: (null !== tariffRates) ? null : materialData === null ? toBeUpdated[0].tariff_rate : materialData.tariffRate, tariff_rate: (materialData === null ? toBeUpdated[0].tariff_rate : materialData.tariffRate),
tariff_rates: (null === tariffRates) ? null : Object.fromEntries(tariffRates),
}; };
await performRequest(this, 'POST', `${config.backendUrl}/calculation/material/`, body, false).catch(() => { await performRequest(this, 'POST', `${config.backendUrl}/calculation/material/`, body, false).catch(() => {

View file

@ -1,23 +0,0 @@
package de.avatic.lcc.controller;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/api/session")
public class SessionController {
@GetMapping("/keepalive")
public Map<String, Object> keepalive(Authentication authentication) {
return Map.of(
"status", "ok",
"user", authentication.getName(),
"timestamp", System.currentTimeMillis()
);
}
}

View file

@ -78,8 +78,8 @@ public class PremiseController {
public ResponseEntity<PremiseSearchResultDTO> findMaterialsAndSuppliers(@RequestParam String search) { public ResponseEntity<PremiseSearchResultDTO> findMaterialsAndSuppliers(@RequestParam String search) {
try { try {
String decodedValue = URLDecoder.decode(search, StandardCharsets.UTF_8); // String decodedValue = URLDecoder.decode(search, StandardCharsets.UTF_8);
return ResponseEntity.ok(premiseSearchStringAnalyzerService.findMaterialAndSuppliers(decodedValue)); return ResponseEntity.ok(premiseSearchStringAnalyzerService.findMaterialAndSuppliers(search));
} catch (Exception e) { } catch (Exception e) {
throw new BadRequestException("Bad string encoding", "Unable to decode request", e); throw new BadRequestException("Bad string encoding", "Unable to decode request", e);
} }

View file

@ -6,6 +6,8 @@ import de.avatic.lcc.repositories.pagination.SearchQueryResult;
import de.avatic.lcc.service.users.UserService; import de.avatic.lcc.service.users.UserService;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import org.hibernate.validator.constraints.Length;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
@ -52,6 +54,24 @@ public class UserController {
.body(users.toList()); .body(users.toList());
} }
@PostMapping({"/{workdayId}/groups/{group}/", "/{workdayId}/groups/{group}"})
@PreAuthorize("hasRole('RIGHT-MANAGEMENT')")
public ResponseEntity<Void> appendGroups(@PathVariable String workdayId, @PathVariable String group) {
userService.appendGroups(workdayId, group);
return ResponseEntity.ok().build();
}
@DeleteMapping({"/{workdayId}/groups/{group}/", "/{workdayId}/groups/{group}"})
@PreAuthorize("hasRole('RIGHT-MANAGEMENT')")
public ResponseEntity<Void> deleteGroups(@PathVariable String workdayId, @PathVariable String group) {
userService.deleteGroups(workdayId, group);
return ResponseEntity.ok().build();
}
/** /**
* Retrieves a single user by its workday id. * Retrieves a single user by its workday id.
* *
@ -68,6 +88,8 @@ public class UserController {
.body(users); .body(users);
} }
/** /**
* Updates the details of an existing user or creates a new one if it does not exist. * Updates the details of an existing user or creates a new one if it does not exist.
* Users are identified by its workday id. If a group from the group membership does not exist * Users are identified by its workday id. If a group from the group membership does not exist

View file

@ -5,6 +5,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
public class GroupDTO { public class GroupDTO {
private Integer id;
@JsonProperty("group_name") @JsonProperty("group_name")
private String name; private String name;
@ -26,4 +28,12 @@ public class GroupDTO {
public void setDescription(String description) { public void setDescription(String description) {
this.description = description; this.description = description;
} }
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
} }

View file

@ -4,6 +4,7 @@ package de.avatic.lcc.dto.users;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.*; import jakarta.validation.constraints.*;
import org.hibernate.validator.constraints.Length;
import java.util.List; import java.util.List;
@ -11,18 +12,22 @@ public class UserDTO {
@NotNull @NotNull
@JsonProperty("firstname") @JsonProperty("firstname")
@Length(max = 100, min= 1, message = "Firstname must be max 100 characters long")
private String firstName; private String firstName;
@NotNull @NotNull
@JsonProperty("lastname") @JsonProperty("lastname")
@Length(max = 100, min= 1, message = "Lastname must be max 100 characters long")
private String lastName; private String lastName;
@Email @Email(message = "Mail must be valid")
@JsonProperty("mail") @JsonProperty("mail")
@Length(max = 254, min = 1, message = "Email must be max 254 characters long")
private String email; private String email;
@NotNull @NotNull
@JsonProperty("workday_id") @JsonProperty("workday_id")
@Length(max = 32, message = "Workday ID must be max 32 characters long")
private String workdayId; private String workdayId;
@NotNull @NotNull

View file

@ -214,6 +214,8 @@ public class PremisesService {
var unlockedIds = premiseRepository.getIdsWithUnlockedTariffs(materialUpdateDTO.getPremiseIds()); var unlockedIds = premiseRepository.getIdsWithUnlockedTariffs(materialUpdateDTO.getPremiseIds());
if (unlockedIds.isEmpty()) return;
var tariffRate = materialUpdateDTO.getTariffRate() == null ? null : BigDecimal.valueOf(materialUpdateDTO.getTariffRate().doubleValue()); var tariffRate = materialUpdateDTO.getTariffRate() == null ? null : BigDecimal.valueOf(materialUpdateDTO.getTariffRate().doubleValue());
premiseRepository.updateMaterial(unlockedIds, materialUpdateDTO.getHsCode(), tariffRate); premiseRepository.updateMaterial(unlockedIds, materialUpdateDTO.getHsCode(), tariffRate);

View file

@ -128,7 +128,7 @@ public class PremiseCreationService {
} }
tariffs.stream() tariffs.stream()
.filter(r -> r.material().getId().equals(p.getMaterialId())) .filter(r -> r.material().getId().equals(p.getMaterialId()) && r.countryId().equals(p.getCountryId()))
.findFirst() .findFirst()
.ifPresent(value -> premiseRepository.updateMaterial(Collections.singletonList( .ifPresent(value -> premiseRepository.updateMaterial(Collections.singletonList(
p.getId()), p.getId()),

View file

@ -26,6 +26,7 @@ import java.util.regex.Pattern;
@Service @Service
public class PremiseSearchStringAnalyzerService { public class PremiseSearchStringAnalyzerService {
private static final String PART_NUMBER_REGEX_NUMBER_ONLY = "(\\d{4,11})";
private static final String PART_NUMBER_REGEX = "([a-zA-Z0-9][a-zA-Z0-9\\-]{4,11})"; private static final String PART_NUMBER_REGEX = "([a-zA-Z0-9][a-zA-Z0-9\\-]{4,11})";
private static final Pattern PART_NUMBER_PATTERN = Pattern.compile(PART_NUMBER_REGEX); private static final Pattern PART_NUMBER_PATTERN = Pattern.compile(PART_NUMBER_REGEX);
private final MaterialRepository materialRepository; private final MaterialRepository materialRepository;
@ -98,6 +99,8 @@ public class PremiseSearchStringAnalyzerService {
Set<String> partNumbers = new HashSet<>(); Set<String> partNumbers = new HashSet<>();
Matcher matcher = PART_NUMBER_PATTERN.matcher(search); Matcher matcher = PART_NUMBER_PATTERN.matcher(search);
Matcher numbersOnlyMatcher = Pattern.compile(PART_NUMBER_REGEX_NUMBER_ONLY).matcher(search);
// Find all matches // Find all matches
while (matcher.find()) { while (matcher.find()) {
// Get the match from group 1 (inside the lookahead) // Get the match from group 1 (inside the lookahead)
@ -105,6 +108,12 @@ public class PremiseSearchStringAnalyzerService {
partNumbers.add(normalizePartNumber(partNumber)); partNumbers.add(normalizePartNumber(partNumber));
} }
while (numbersOnlyMatcher.find()) {
// Get the match from group 1 (inside the lookahead)
String partNumber = numbersOnlyMatcher.group(1);
partNumbers.add(normalizePartNumber(partNumber));
}
return partNumbers; return partNumbers;
} }

View file

@ -34,7 +34,7 @@ public class UserTransformer {
return entity; return entity;
} }
private Group fromGroupDTO(String name) { public Group fromGroupDTO(String name) {
var group = new Group(); var group = new Group();
group.setName(name); group.setName(name);
return group; return group;

View file

@ -63,6 +63,7 @@ public class GroupService {
private GroupDTO toGroupDTO(Group group) { private GroupDTO toGroupDTO(Group group) {
var dto = new GroupDTO(); var dto = new GroupDTO();
dto.setId(group.getId());
dto.setDescription( group.getDescription()); dto.setDescription( group.getDescription());
dto.setName(group.getName()); dto.setName(group.getName());

View file

@ -2,15 +2,21 @@ package de.avatic.lcc.service.users;
import de.avatic.lcc.config.LccOidcUser; import de.avatic.lcc.config.LccOidcUser;
import de.avatic.lcc.dto.users.UserDTO; import de.avatic.lcc.dto.users.UserDTO;
import de.avatic.lcc.model.db.users.Group;
import de.avatic.lcc.repositories.pagination.SearchQueryPagination; import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
import de.avatic.lcc.repositories.pagination.SearchQueryResult; import de.avatic.lcc.repositories.pagination.SearchQueryResult;
import de.avatic.lcc.repositories.users.UserRepository; import de.avatic.lcc.repositories.users.UserRepository;
import de.avatic.lcc.service.transformer.users.UserTransformer; import de.avatic.lcc.service.transformer.users.UserTransformer;
import de.avatic.lcc.util.exception.badrequest.NotFoundException;
import de.avatic.lcc.util.exception.base.BadRequestException;
import jakarta.validation.constraints.NotNull;
import org.hibernate.validator.constraints.Length;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Optional; import java.util.*;
import java.util.stream.Collectors;
/** /**
* Service class responsible for handling business logic related to users. * Service class responsible for handling business logic related to users.
@ -60,20 +66,43 @@ public class UserService {
userAuthorityCacheService.invalidateUserAuthorities(userId); userAuthorityCacheService.invalidateUserAuthorities(userId);
} }
public boolean isSuper() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//todo make a service. and simulate user rights in dev profile. public void appendGroups(String workdayId, String group) {
if (authentication != null && authentication.getPrincipal() instanceof LccOidcUser oidcUser) { var optUser = userRepository.getByWorkdayId(workdayId);
return oidcUser.getAuthorities().stream().anyMatch(authority -> authority.getAuthority().equals("ROLE_SUPER")); if (optUser.isEmpty()) {
throw new NotFoundException(NotFoundException.NotFoundType.USER, "workday id", workdayId);
}
var user = optUser.get();
var existingGroups = new TreeSet<>(Comparator.comparing(Group::getName));
existingGroups.addAll(user.getGroups());
existingGroups.add(userTransformer.fromGroupDTO(group));
user.setGroups(new ArrayList<>(existingGroups));
userRepository.update(user);
} }
return false; public void deleteGroups(String workdayId, String groupToDelete) {
} var optUser = userRepository.getByWorkdayId(workdayId);
// public boolean canCalculate() { if (optUser.isEmpty()) throw new NotFoundException(NotFoundException.NotFoundType.USER, "workday id", workdayId);
// Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); var user = optUser.get();
// }
List<Group> uniqueGroups = user.getGroups().stream()
.filter(group -> !group.getName().equals(groupToDelete))
.collect(Collectors.toMap(
Group::getName,
group -> group,
(existing, replacement) -> existing,
LinkedHashMap::new
))
.values()
.stream()
.toList();
user.setGroups(uniqueGroups);
userRepository.update(user);
}
} }

View file

@ -29,7 +29,8 @@ public class NotFoundException extends BadRequestException {
COUNTRY_PROPERTY("Country property"), COUNTRY_PROPERTY("Country property"),
CONTAINER_RATE("Container rate"), CONTAINER_RATE("Container rate"),
EXPIRED_VALIDITY_PERIOD("Expired validity period"), EXPIRED_VALIDITY_PERIOD("Expired validity period"),
USER_NODE("User node"); USER_NODE("User node"),
USER("User");
private final String identifier; private final String identifier;

View file

@ -300,6 +300,9 @@ CREATE TABLE IF NOT EXISTS material
hs_code CHAR(11), hs_code CHAR(11),
name VARCHAR(500) NOT NULL, name VARCHAR(500) NOT NULL,
is_deprecated BOOLEAN NOT NULL DEFAULT FALSE, is_deprecated BOOLEAN NOT NULL DEFAULT FALSE,
INDEX idx_part_number (part_number),
INDEX idx_normalized_part_number (normalized_part_number),
INDEX idx_hs_code (hs_code),
CONSTRAINT `uq_normalized_part_number` UNIQUE (`normalized_part_number`) CONSTRAINT `uq_normalized_part_number` UNIQUE (`normalized_part_number`)
); );

View file

@ -300,6 +300,9 @@ CREATE TABLE IF NOT EXISTS material
hs_code CHAR(11), hs_code CHAR(11),
name VARCHAR(500) NOT NULL, name VARCHAR(500) NOT NULL,
is_deprecated BOOLEAN NOT NULL DEFAULT FALSE, is_deprecated BOOLEAN NOT NULL DEFAULT FALSE,
INDEX idx_part_number (part_number),
INDEX idx_normalized_part_number (normalized_part_number),
INDEX idx_hs_code (hs_code),
CONSTRAINT `uq_normalized_part_number` UNIQUE (`normalized_part_number`) CONSTRAINT `uq_normalized_part_number` UNIQUE (`normalized_part_number`)
); );
@ -521,7 +524,9 @@ CREATE TABLE IF NOT EXISTS calculation_job
FOREIGN KEY (user_id) REFERENCES sys_user (id), FOREIGN KEY (user_id) REFERENCES sys_user (id),
INDEX idx_premise_id (premise_id), INDEX idx_premise_id (premise_id),
INDEX idx_validity_period_id (validity_period_id), INDEX idx_validity_period_id (validity_period_id),
INDEX idx_property_set_id (property_set_id) INDEX idx_property_set_id (property_set_id),
INDEX idx_user_id (user_id)
); );