From 0a3d7597331e4cb2f1c5c812708341e751abcdd5 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 17 Nov 2025 10:16:01 +0100 Subject: [PATCH] Added single user fetch to user api --- src/frontend/src/components/UI/TableView.vue | 1 + src/frontend/src/components/UI/Toast.vue | 88 +++++++++++++++++-- .../layout/bulkedit/BulkEditRow.vue | 4 +- .../src/components/layout/config/Rates.vue | 3 +- .../de/avatic/lcc/config/SecurityConfig.java | 2 +- .../lcc/controller/users/UserController.java | 17 ++++ .../java/de/avatic/lcc/dto/users/UserDTO.java | 11 --- .../repositories/users/UserRepository.java | 5 +- .../transformer/users/UserTransformer.java | 3 +- .../avatic/lcc/service/users/UserService.java | 12 +++ src/main/resources/application.properties | 2 - .../resources/db/migration/V9__Groups.sql | 16 ++-- 12 files changed, 127 insertions(+), 37 deletions(-) diff --git a/src/frontend/src/components/UI/TableView.vue b/src/frontend/src/components/UI/TableView.vue index 2197796..8347f18 100644 --- a/src/frontend/src/components/UI/TableView.vue +++ b/src/frontend/src/components/UI/TableView.vue @@ -38,6 +38,7 @@ {{ getCellValue(item, column) }} + {{ getCellValue(item, column) }}
{{ toast.title }}
{{ toast.message }}
+
+ {{ toast.count }} +
@@ -49,8 +52,40 @@ export default { * @param {string} options.variant - Toast variant: 'success', 'error', 'warning', 'info' * @param {number} options.duration - Auto-dismiss duration in ms (0 = no auto-dismiss) * @param {string} options.icon - Icon name (optional) + * @param {boolean} options.stack - Whether to stack identical toasts (default: true) */ addToast(options = {}) { + const shouldStack = options.stack !== undefined ? options.stack : true; + + // Check if an identical toast already exists + if (shouldStack) { + const existingToast = this.toasts.find(t => + t.message === options.message && + t.title === (options.title || null) && + t.variant === this.mapVariant(options.variant || 'primary') + ); + + if (existingToast) { + // Increment count and reset timer + existingToast.count = (existingToast.count || 1) + 1; + + // Clear existing timeout + if (existingToast.timeoutId) { + clearTimeout(existingToast.timeoutId); + } + + // Set new timeout + const duration = options.duration !== undefined ? options.duration : 5000; + if (duration > 0) { + existingToast.timeoutId = setTimeout(() => { + this.removeToast(existingToast.id); + }, duration); + } + + return existingToast.id; + } + } + const toast = { id: this.nextId++, message: options.message || 'Notification', @@ -58,13 +93,15 @@ export default { variant: this.mapVariant(options.variant || 'primary' ), duration: options.duration !== undefined ? options.duration : 5000, icon: options.icon ? `Ph${options.icon.charAt(0).toUpperCase() + options.icon.slice(1)}` : null, + count: 1, + timeoutId: null }; this.toasts.push(toast) // Auto-dismiss if duration is set if (toast.duration > 0) { - setTimeout(() => { + toast.timeoutId = setTimeout(() => { this.removeToast(toast.id) }, toast.duration) } @@ -94,6 +131,10 @@ export default { removeToast(id) { const index = this.toasts.findIndex(toast => toast.id === id) if (index > -1) { + // Clear timeout if exists + if (this.toasts[index].timeoutId) { + clearTimeout(this.toasts[index].timeoutId); + } this.toasts.splice(index, 1) } }, @@ -102,6 +143,12 @@ export default { * Remove all toasts */ clearToasts() { + // Clear all timeouts + this.toasts.forEach(toast => { + if (toast.timeoutId) { + clearTimeout(toast.timeoutId); + } + }); this.toasts = [] }, @@ -150,12 +197,11 @@ export default { cursor: pointer; pointer-events: auto; transition: transform 0.2s ease, box-shadow 0.2s ease; - + position: relative; } .toast:hover { transform: translateY(-2px); - box-shadow: 0 0.5rem 0.9rem rgba(0, 0, 0, 0.1); } @@ -197,22 +243,50 @@ export default { word-break: break-word; } +.toast__badge { + flex-shrink: 0; + width: 2.4rem; + height: 2.4rem; + border-radius: 50%; + background-color: #002F54; + color: #5AF0B4; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; + font-weight: 700; + margin-left: auto; +} .toast-primary { background-color: #5AF0B4; color: #002F54; } +.toast-primary .toast__badge { + background-color: #002F54; + color: #5AF0B4; +} + .toast--secondary { color: #002F54; background-color: #c3cfdf; } +.toast--secondary .toast__badge { + background-color: #002F54; + color: #c3cfdf; +} + .toast--exception{ background-color: #BC2B72; color: #ffffff; } +.toast--exception .toast__badge { + background-color: #ffffff; + color: #BC2B72; +} /* Transition animations */ .toast-enter-active { diff --git a/src/frontend/src/components/layout/bulkedit/BulkEditRow.vue b/src/frontend/src/components/layout/bulkedit/BulkEditRow.vue index b28025c..fe3fa7f 100644 --- a/src/frontend/src/components/layout/bulkedit/BulkEditRow.vue +++ b/src/frontend/src/components/layout/bulkedit/BulkEditRow.vue @@ -13,9 +13,9 @@ {{ premise.hs_code }}
+ v-if="(premise.tariff_rate ?? null) !== null"> Tariff rate: - {{ toPercent(premise.tariff_rate) }} % + {{ toPercent(premise.tariff_rate) }} 
diff --git a/src/frontend/src/components/layout/config/Rates.vue b/src/frontend/src/components/layout/config/Rates.vue index f4164a9..5620377 100644 --- a/src/frontend/src/components/layout/config/Rates.vue +++ b/src/frontend/src/components/layout/config/Rates.vue @@ -51,7 +51,6 @@ import ModalDialog from "@/components/UI/ModalDialog.vue"; import {mapStores} from "pinia"; import {useValidityPeriodStore} from "@/store/validityPeriod.js"; import AutosuggestSearchbar from "@/components/UI/AutoSuggestSearchBar.vue"; -import DataTable from "@/components/UI/DataTable.vue"; import TableView from "@/components/UI/TableView.vue"; import RadioOption from "@/components/UI/RadioOption.vue"; import {useMatrixRateStore} from "@/store/matrixRate.js"; @@ -80,7 +79,7 @@ export default { }, components: { StagedRates, - RadioOption, TableView, DataTable, AutosuggestSearchbar, ModalDialog, Tooltip, IconButton, Dropdown + RadioOption, TableView, AutosuggestSearchbar, ModalDialog, Tooltip, IconButton, Dropdown }, computed: { ...mapStores(useValidityPeriodStore, useMatrixRateStore, useContainerRateStore), diff --git a/src/main/java/de/avatic/lcc/config/SecurityConfig.java b/src/main/java/de/avatic/lcc/config/SecurityConfig.java index 47b25dd..00cdc1a 100644 --- a/src/main/java/de/avatic/lcc/config/SecurityConfig.java +++ b/src/main/java/de/avatic/lcc/config/SecurityConfig.java @@ -279,7 +279,7 @@ public class SecurityConfig { } else if (identifyBy.equals("workday") && workdayId != null && !workdayId.isEmpty()) { log.debug("Fetch user by workday id {}", workdayId); - user = userRepository.getByWorkdayId(workdayId); + user = userRepository.getByWorkdayId(workdayId).orElse(null); } if (user != null) { diff --git a/src/main/java/de/avatic/lcc/controller/users/UserController.java b/src/main/java/de/avatic/lcc/controller/users/UserController.java index 77026c7..ac50c5e 100644 --- a/src/main/java/de/avatic/lcc/controller/users/UserController.java +++ b/src/main/java/de/avatic/lcc/controller/users/UserController.java @@ -12,6 +12,7 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.Optional; /** * REST Controller for managing user-related operations. @@ -51,6 +52,22 @@ public class UserController { .body(users.toList()); } + /** + * Retrieves a single user by its workday id. + * + * @param workdayId The workday id of the user to retrieve. + * @return A ResponseEntity containing the user, along with pagination headers. + */ + @GetMapping({"/{workdayId}/", "/{workdayId}"}) + @PreAuthorize("hasRole('RIGHT-MANAGEMENT')") + public ResponseEntity> getUser(@PathVariable String workdayId) { + + Optional users = userService.getUser(workdayId); + + return ResponseEntity.ok() + .body(users); + } + /** * 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 diff --git a/src/main/java/de/avatic/lcc/dto/users/UserDTO.java b/src/main/java/de/avatic/lcc/dto/users/UserDTO.java index e9029cf..8fc22e5 100644 --- a/src/main/java/de/avatic/lcc/dto/users/UserDTO.java +++ b/src/main/java/de/avatic/lcc/dto/users/UserDTO.java @@ -25,9 +25,6 @@ public class UserDTO { @JsonProperty("workday_id") private String workdayId; - @JsonProperty("is_active") - private boolean isActive; - @NotNull @Size(min = 1) @JsonProperty("groups") @@ -65,14 +62,6 @@ public class UserDTO { this.workdayId = workdayId; } - public boolean isActive() { - return isActive; - } - - public void setActive(boolean active) { - isActive = active; - } - public List getGroups() { return groups; } diff --git a/src/main/java/de/avatic/lcc/repositories/users/UserRepository.java b/src/main/java/de/avatic/lcc/repositories/users/UserRepository.java index 067c610..5cae18b 100644 --- a/src/main/java/de/avatic/lcc/repositories/users/UserRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/users/UserRepository.java @@ -18,6 +18,7 @@ import java.sql.Statement; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; @Repository public class UserRepository { @@ -145,12 +146,12 @@ public class UserRepository { } @Transactional - public User getByWorkdayId(String workdayId) { + public Optional getByWorkdayId(String workdayId) { List results = jdbcTemplate.query("SELECT * FROM sys_user WHERE workday_id = ?", new UserMapper(), workdayId); - return results.isEmpty() ? null : results.getFirst(); + return results.isEmpty() ? Optional.empty() : Optional.of(results.getFirst()); } @Transactional diff --git a/src/main/java/de/avatic/lcc/service/transformer/users/UserTransformer.java b/src/main/java/de/avatic/lcc/service/transformer/users/UserTransformer.java index 7a3e1d3..3cf59c9 100644 --- a/src/main/java/de/avatic/lcc/service/transformer/users/UserTransformer.java +++ b/src/main/java/de/avatic/lcc/service/transformer/users/UserTransformer.java @@ -11,7 +11,6 @@ public class UserTransformer { public UserDTO toUserDTO(User entity) { UserDTO dto = new UserDTO(); - dto.setActive(entity.getActive()); dto.setEmail(entity.getEmail()); dto.setFirstName(entity.getFirstName()); dto.setLastName(entity.getLastName()); @@ -25,7 +24,7 @@ public class UserTransformer { public User fromUserDTO(UserDTO dto) { User entity = new User(); - entity.setActive(dto.isActive()); + entity.setActive(true); entity.setEmail(dto.getEmail()); entity.setFirstName(dto.getFirstName()); entity.setLastName(dto.getLastName()); diff --git a/src/main/java/de/avatic/lcc/service/users/UserService.java b/src/main/java/de/avatic/lcc/service/users/UserService.java index 0941157..28dad3e 100644 --- a/src/main/java/de/avatic/lcc/service/users/UserService.java +++ b/src/main/java/de/avatic/lcc/service/users/UserService.java @@ -10,6 +10,8 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; +import java.util.Optional; + /** * Service class responsible for handling business logic related to users. * Provides methods to retrieve and update user information. @@ -38,6 +40,16 @@ public class UserService { return SearchQueryResult.map(userRepository.listUsers(new SearchQueryPagination(page, limit)), userTransformer::toUserDTO); } + /** + * Retrieves a single user by its workday id. + * + * @param workdayId The workday id of the user to retrieve. + * @return A ResponseEntity containing the user, along with pagination headers. + */ + public Optional getUser(String workdayId) { + return userRepository.getByWorkdayId(workdayId).map(userTransformer::toUserDTO); + } + /** * Updates an existing user's information. * diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index dd67a10..dfce181 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -25,8 +25,6 @@ spring.sql.init.mode=never lcc.allowed_cors= lcc.allowed_oauth_token_cors=* -logging.level.org.springframework.ws=DEBUG -logging.level.org.springframework.oxm=DEBUG lcc.auth.identify.by=workday lcc.auth.claim.workday=employeeid diff --git a/src/main/resources/db/migration/V9__Groups.sql b/src/main/resources/db/migration/V9__Groups.sql index 3332df8..c70bf7e 100644 --- a/src/main/resources/db/migration/V9__Groups.sql +++ b/src/main/resources/db/migration/V9__Groups.sql @@ -1,20 +1,20 @@ INSERT INTO sys_group(group_name, group_description) VALUES ('none', 'no rights'); INSERT INTO sys_group(group_name, group_description) -VALUES ('basic', 'Login, generate reports'); +VALUES ('basic', 'can generate reports'); INSERT INTO sys_group(group_name, group_description) -VALUES ('calculation', 'Login, generate reports, do calculations'); +VALUES ('calculation', 'can generate reports, do calculations'); INSERT INTO sys_group(group_name, group_description) -VALUES ('freight', 'Login, generate reports, edit freight rates'); +VALUES ('freight', 'manage freight rates'); INSERT INTO sys_group(group_name, group_description) -VALUES ('packaging', 'Login, generate reports, edit packaging data'); +VALUES ('packaging', 'manage packaging data'); INSERT INTO sys_group(group_name, group_description) -VALUES ('material', 'Login, generate reports, edit material data'); +VALUES ('material', 'manage material data'); INSERT INTO sys_group(group_name, group_description) VALUES ('super', - 'Login, generate reports, do calculations, edit freight rates, edit packaging data'); + 'can generate reports, do calculations, manage freight rates, manage packaging data, manage material data, manage general system settings'); INSERT INTO sys_group(group_name, group_description) -VALUES ('service', 'Register API Tokens'); +VALUES ('service', 'register external applications'); INSERT INTO sys_group(group_name, group_description) VALUES ('right-management', - 'Add/Remove users, groups, etc.'); \ No newline at end of file + 'add users, manage user groups'); \ No newline at end of file