{{ 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