1. Import
This commit is contained in:
23
vaadin/frontend/index.html
Normal file
23
vaadin/frontend/index.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This file is auto-generated by Vaadin.
|
||||
-->
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style>
|
||||
body, #outlet {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<!-- index.ts is included here automatically (either by the dev server or during the build) -->
|
||||
</head>
|
||||
<body>
|
||||
<!-- This outlet div is where the views are rendered -->
|
||||
<div id="outlet"></div>
|
||||
</body>
|
||||
</html>
|
||||
39
vaadin/frontend/themes/votian/styles.css
Normal file
39
vaadin/frontend/themes/votian/styles.css
Normal file
@@ -0,0 +1,39 @@
|
||||
/* Votian Theme - Stadtbote GmbH */
|
||||
|
||||
html {
|
||||
--lumo-primary-color: #1b12b9;
|
||||
--lumo-primary-color-50pct: rgba(27, 18, 185, 0.5);
|
||||
--lumo-primary-text-color: #1b12b9;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
vaadin-app-layout::part(navbar) {
|
||||
background: linear-gradient(135deg, #1b12b9 0%, #2a1fd4 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
vaadin-app-layout::part(navbar) h1 {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* Drawer */
|
||||
vaadin-app-layout::part(drawer) {
|
||||
background: #f8f9fa;
|
||||
border-right: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
/* Grid styling */
|
||||
vaadin-grid {
|
||||
--lumo-row-stripe-color: rgba(27, 18, 185, 0.03);
|
||||
}
|
||||
|
||||
/* Button primary */
|
||||
vaadin-button[theme~="primary"] {
|
||||
background-color: #1b12b9;
|
||||
}
|
||||
|
||||
/* Login page */
|
||||
.login-form {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
3
vaadin/frontend/themes/votian/theme.json
Normal file
3
vaadin/frontend/themes/votian/theme.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"lumoImports": ["typography", "color", "spacing", "badge", "utility"]
|
||||
}
|
||||
61
vaadin/pom.xml
Normal file
61
vaadin/pom.xml
Normal file
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.2.5</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>de.votian</groupId>
|
||||
<artifactId>votian-web</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<name>Votian Web Application</name>
|
||||
<description>Vaadin Flow Web Application for Votian Logistics (Stadtbote GmbH)</description>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<vaadin.version>24.3.11</vaadin.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.vaadin</groupId>
|
||||
<artifactId>vaadin-bom</artifactId>
|
||||
<version>${vaadin.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.vaadin</groupId>
|
||||
<artifactId>vaadin-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
15
vaadin/src/main/java/de/votian/web/VotianWebApplication.java
Normal file
15
vaadin/src/main/java/de/votian/web/VotianWebApplication.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package de.votian.web;
|
||||
|
||||
import com.vaadin.flow.component.page.AppShellConfigurator;
|
||||
import com.vaadin.flow.theme.Theme;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
@Theme("votian")
|
||||
public class VotianWebApplication implements AppShellConfigurator {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(VotianWebApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.votian.web.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Configuration
|
||||
public class RestClientConfig {
|
||||
|
||||
@Value("${votian.services.url}")
|
||||
private String servicesUrl;
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
return new RestTemplate();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public String servicesBaseUrl() {
|
||||
return servicesUrl;
|
||||
}
|
||||
}
|
||||
389
vaadin/src/main/java/de/votian/web/model/SessionData.java
Normal file
389
vaadin/src/main/java/de/votian/web/model/SessionData.java
Normal file
@@ -0,0 +1,389 @@
|
||||
package de.votian.web.model;
|
||||
|
||||
import com.vaadin.flow.spring.annotation.VaadinSessionScope;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@VaadinSessionScope
|
||||
public class SessionData {
|
||||
|
||||
private Long userId;
|
||||
private Long headquartersId;
|
||||
private Long employeeId;
|
||||
private Long customerId;
|
||||
private Long costCenterId;
|
||||
private Long courierId;
|
||||
private String userName;
|
||||
private String userFirstname;
|
||||
private String authToken;
|
||||
private String pendingTotpToken;
|
||||
private Integer userType;
|
||||
private String legacyRights;
|
||||
private List<Long> accessibleCostCenterIds = new ArrayList<>();
|
||||
private List<Long> rightIds = new ArrayList<>();
|
||||
private List<String> rightNames = new ArrayList<>();
|
||||
private boolean authenticated;
|
||||
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public Long getHeadquartersId() { return headquartersId; }
|
||||
public void setHeadquartersId(Long headquartersId) { this.headquartersId = headquartersId; }
|
||||
public Long getEmployeeId() { return employeeId; }
|
||||
public void setEmployeeId(Long employeeId) { this.employeeId = employeeId; }
|
||||
public Long getCustomerId() { return customerId; }
|
||||
public void setCustomerId(Long customerId) { this.customerId = customerId; }
|
||||
public Long getCostCenterId() { return costCenterId; }
|
||||
public void setCostCenterId(Long costCenterId) { this.costCenterId = costCenterId; }
|
||||
public Long getCourierId() { return courierId; }
|
||||
public void setCourierId(Long courierId) { this.courierId = courierId; }
|
||||
public String getUserName() { return userName; }
|
||||
public void setUserName(String userName) { this.userName = userName; }
|
||||
public String getUserFirstname() { return userFirstname; }
|
||||
public void setUserFirstname(String userFirstname) { this.userFirstname = userFirstname; }
|
||||
public String getAuthToken() { return authToken; }
|
||||
public void setAuthToken(String authToken) { this.authToken = authToken; }
|
||||
public String getPendingTotpToken() { return pendingTotpToken; }
|
||||
public void setPendingTotpToken(String pendingTotpToken) { this.pendingTotpToken = pendingTotpToken; }
|
||||
public Integer getUserType() { return userType; }
|
||||
public void setUserType(Integer userType) { this.userType = userType; }
|
||||
public String getLegacyRights() { return legacyRights; }
|
||||
public void setLegacyRights(String legacyRights) { this.legacyRights = legacyRights; }
|
||||
public List<Long> getAccessibleCostCenterIds() { return accessibleCostCenterIds; }
|
||||
public void setAccessibleCostCenterIds(List<Long> accessibleCostCenterIds) {
|
||||
this.accessibleCostCenterIds = accessibleCostCenterIds != null ? new ArrayList<>(accessibleCostCenterIds) : new ArrayList<>();
|
||||
}
|
||||
public List<Long> getRightIds() { return rightIds; }
|
||||
public void setRightIds(List<Long> rightIds) {
|
||||
this.rightIds = rightIds != null ? new ArrayList<>(rightIds) : new ArrayList<>();
|
||||
}
|
||||
public List<String> getRightNames() { return rightNames; }
|
||||
public void setRightNames(List<String> rightNames) {
|
||||
this.rightNames = rightNames != null ? new ArrayList<>(rightNames) : new ArrayList<>();
|
||||
}
|
||||
public boolean isAuthenticated() { return authenticated; }
|
||||
public void setAuthenticated(boolean authenticated) { this.authenticated = authenticated; }
|
||||
|
||||
public boolean isHeadquartersUser() { return Integer.valueOf(1).equals(userType); }
|
||||
public boolean isCustomerUser() { return Integer.valueOf(2).equals(userType); }
|
||||
public boolean isCourierUser() { return Integer.valueOf(3).equals(userType); }
|
||||
public boolean isWarehouseUser() { return Integer.valueOf(4).equals(userType); }
|
||||
public boolean hasCostCenterAccess(Long costCenterId) {
|
||||
return costCenterId == null || accessibleCostCenterIds.contains(costCenterId);
|
||||
}
|
||||
|
||||
public boolean canViewCustomers() {
|
||||
return isHeadquartersUser() && hqRight(0);
|
||||
}
|
||||
|
||||
public boolean canManageCustomers() {
|
||||
return canViewCustomers();
|
||||
}
|
||||
|
||||
public boolean canViewCouriers() {
|
||||
return isHeadquartersUser() && hqRight(1);
|
||||
}
|
||||
|
||||
public boolean canManageCouriers() {
|
||||
return canViewCouriers();
|
||||
}
|
||||
|
||||
public boolean canViewEmployees() {
|
||||
if (isHeadquartersUser()) {
|
||||
return hqRight(3);
|
||||
}
|
||||
if (isCustomerUser()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean canManageEmployees() {
|
||||
if (isHeadquartersUser()) {
|
||||
return hqRight(3);
|
||||
}
|
||||
return isCustomerUser() && customerRight(2);
|
||||
}
|
||||
|
||||
public boolean canManageEmployeeMatrixRights() {
|
||||
return isHeadquartersUser() && hqRight(3);
|
||||
}
|
||||
|
||||
public boolean canManageEmployeeCostCenterMatrix() {
|
||||
if (isHeadquartersUser()) {
|
||||
return canManageEmployees();
|
||||
}
|
||||
return isCustomerUser() && customerRight(2) && customerRight(12);
|
||||
}
|
||||
|
||||
public boolean canViewCostCenters() {
|
||||
if (isHeadquartersUser()) {
|
||||
return canViewCustomers();
|
||||
}
|
||||
return isCustomerUser() && customerRight(0);
|
||||
}
|
||||
|
||||
public boolean canManageCostCenters() {
|
||||
if (isHeadquartersUser()) {
|
||||
return canManageCustomers();
|
||||
}
|
||||
return isCustomerUser() && customerRight(1);
|
||||
}
|
||||
|
||||
public boolean canViewJobs() {
|
||||
if (isHeadquartersUser()) {
|
||||
return hqRight(4) || hqRight(7);
|
||||
}
|
||||
if (isCustomerUser()) {
|
||||
return customerRight(4);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean canManageJobs() {
|
||||
if (isHeadquartersUser()) {
|
||||
return hqRight(7);
|
||||
}
|
||||
return isCustomerUser() && customerRight(4);
|
||||
}
|
||||
|
||||
public boolean canBatchJobs() {
|
||||
return canManageJobs();
|
||||
}
|
||||
|
||||
public boolean canExportJobs() {
|
||||
if (isHeadquartersUser()) {
|
||||
return canViewJobs() && hqRight(42);
|
||||
}
|
||||
return isCustomerUser() && canViewJobs() && customerRight(7);
|
||||
}
|
||||
|
||||
public boolean canViewInvoices() {
|
||||
if (isHeadquartersUser()) {
|
||||
return hqRight(5);
|
||||
}
|
||||
if (isCustomerUser()) {
|
||||
return customerRight(5);
|
||||
}
|
||||
return isCourierUser();
|
||||
}
|
||||
|
||||
public boolean canExportInvoices() {
|
||||
if (isHeadquartersUser()) {
|
||||
return hqRight(5);
|
||||
}
|
||||
if (isCustomerUser()) {
|
||||
return customerRight(7);
|
||||
}
|
||||
return isCourierUser();
|
||||
}
|
||||
|
||||
public boolean canViewStatistics() {
|
||||
if (isHeadquartersUser()) {
|
||||
return hqRight(8);
|
||||
}
|
||||
return isCustomerUser() && customerRight(8);
|
||||
}
|
||||
|
||||
public boolean canViewDisposition() {
|
||||
return isHeadquartersUser() && (hqRight(7) || hqRight(19));
|
||||
}
|
||||
|
||||
public boolean canManageDisposition() {
|
||||
return isHeadquartersUser() && hqRight(7);
|
||||
}
|
||||
|
||||
public boolean canViewLonghaul() {
|
||||
return isHeadquartersUser() && hqRight(10);
|
||||
}
|
||||
|
||||
public boolean canManageLonghaul() {
|
||||
return canViewLonghaul();
|
||||
}
|
||||
|
||||
public boolean canViewGroupware() {
|
||||
return isHeadquartersUser() && hqRight(11);
|
||||
}
|
||||
|
||||
public boolean canManageGroupware() {
|
||||
return canViewGroupware();
|
||||
}
|
||||
|
||||
public boolean canViewGenericExports() {
|
||||
if (isHeadquartersUser()) {
|
||||
return hqRight(6);
|
||||
}
|
||||
return isCustomerUser() && customerRight(7);
|
||||
}
|
||||
|
||||
public boolean canManageGenericExports() {
|
||||
return canViewGenericExports();
|
||||
}
|
||||
|
||||
public boolean canViewCustomerReports() {
|
||||
return isHeadquartersUser() && hqRight(16) && hqRight(0);
|
||||
}
|
||||
|
||||
public boolean canViewCourierReports() {
|
||||
return isHeadquartersUser() && hqRight(16) && hqRight(1);
|
||||
}
|
||||
|
||||
public boolean canViewProspectReports() {
|
||||
return canViewCustomerReports();
|
||||
}
|
||||
|
||||
public boolean canViewReports() {
|
||||
return canViewCustomerReports() || canViewCourierReports() || canViewProspectReports();
|
||||
}
|
||||
|
||||
public boolean canManageReports() {
|
||||
return canViewReports();
|
||||
}
|
||||
|
||||
public boolean canViewConfidentialReports() {
|
||||
return isHeadquartersUser() && hqRight(10);
|
||||
}
|
||||
|
||||
public boolean canManageCourierMessageGroups() {
|
||||
return isHeadquartersUser() && hqRight(1);
|
||||
}
|
||||
|
||||
public boolean canManageDeviceMessages() {
|
||||
return isHeadquartersUser() && hqRight(4);
|
||||
}
|
||||
|
||||
public boolean canManageTicker() {
|
||||
return isHeadquartersUser() && hqRight(18);
|
||||
}
|
||||
|
||||
public boolean canManageNewsletter() {
|
||||
return isHeadquartersUser() && hqRight(17);
|
||||
}
|
||||
|
||||
public boolean canViewCommunication() {
|
||||
return canManageCourierMessageGroups()
|
||||
|| canManageDeviceMessages()
|
||||
|| canManageTicker()
|
||||
|| canManageNewsletter();
|
||||
}
|
||||
|
||||
public boolean canManageCommunication() {
|
||||
return canViewCommunication();
|
||||
}
|
||||
|
||||
public boolean canViewGlobalMap() {
|
||||
return isHeadquartersUser() && hqRight(19);
|
||||
}
|
||||
|
||||
public boolean canViewSimilaritySearch() {
|
||||
return isHeadquartersUser() && (hqRight(0) || hqRight(1));
|
||||
}
|
||||
|
||||
public boolean canManageAddressDirectory() {
|
||||
return isHeadquartersUser() && hqRight(2);
|
||||
}
|
||||
|
||||
public boolean canManageTravelTimes() {
|
||||
return canManageAddressDirectory();
|
||||
}
|
||||
|
||||
public boolean canViewGeoOperations() {
|
||||
return canViewGlobalMap()
|
||||
|| canViewSimilaritySearch()
|
||||
|| canManageAddressDirectory();
|
||||
}
|
||||
|
||||
public boolean canManageGeoOperations() {
|
||||
return canViewGeoOperations();
|
||||
}
|
||||
|
||||
public boolean canManagePricing() {
|
||||
return isHeadquartersUser() && hqRight(2) && hqRight(29);
|
||||
}
|
||||
|
||||
public boolean canManageHeadquarters() {
|
||||
return isHeadquartersUser() && hqRight(2);
|
||||
}
|
||||
|
||||
public boolean canManageMetaFields() {
|
||||
return canManageHeadquarters();
|
||||
}
|
||||
|
||||
public boolean canManageImports() {
|
||||
return isHeadquartersUser() && (hqRight(15) || hqRight(22) || hqRight(23));
|
||||
}
|
||||
|
||||
public boolean canManageEmployeeWarehouseConfig() {
|
||||
return isHeadquartersUser() && canManageEmployees();
|
||||
}
|
||||
|
||||
public boolean canViewWarehouse() {
|
||||
return isWarehouseUser() || (isHeadquartersUser() && hqRight(14));
|
||||
}
|
||||
|
||||
public boolean canEditOwnEmployeeAccount() {
|
||||
return isCustomerUser() && customerRight(10);
|
||||
}
|
||||
|
||||
public boolean canViewEmployee(Long targetEmployeeId) {
|
||||
if (isHeadquartersUser()) {
|
||||
return canViewEmployees();
|
||||
}
|
||||
return isCustomerUser()
|
||||
&& (canManageEmployees()
|
||||
|| (employeeId != null && employeeId.equals(targetEmployeeId)));
|
||||
}
|
||||
|
||||
public boolean canEditEmployee(Long targetEmployeeId) {
|
||||
if (isHeadquartersUser()) {
|
||||
return canManageEmployees();
|
||||
}
|
||||
return isCustomerUser()
|
||||
&& (canManageEmployees()
|
||||
|| ((employeeId != null && employeeId.equals(targetEmployeeId))
|
||||
&& canEditOwnEmployeeAccount()));
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return (userFirstname != null ? userFirstname : "") + " " + (userName != null ? userName : "");
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
userId = null;
|
||||
headquartersId = null;
|
||||
employeeId = null;
|
||||
customerId = null;
|
||||
costCenterId = null;
|
||||
courierId = null;
|
||||
userName = null;
|
||||
userFirstname = null;
|
||||
authToken = null;
|
||||
pendingTotpToken = null;
|
||||
userType = null;
|
||||
legacyRights = null;
|
||||
accessibleCostCenterIds = new ArrayList<>();
|
||||
rightIds = new ArrayList<>();
|
||||
rightNames = new ArrayList<>();
|
||||
authenticated = false;
|
||||
}
|
||||
|
||||
private boolean hqRight(int index) {
|
||||
return legacyRight(index, true);
|
||||
}
|
||||
|
||||
private boolean customerRight(int index) {
|
||||
return legacyRight(index, false);
|
||||
}
|
||||
|
||||
private boolean legacyRight(int index, boolean defaultIfMissing) {
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
if (legacyRights == null || legacyRights.isBlank()) {
|
||||
return defaultIfMissing;
|
||||
}
|
||||
return legacyRights.length() > index && legacyRights.charAt(index) == '1';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package de.votian.web.service;
|
||||
|
||||
import com.vaadin.flow.spring.annotation.VaadinSessionScope;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Session-scoped parameter provider for the Vaadin web application.
|
||||
* <p>
|
||||
* Equivalent to PHP's defineGlobalParameters() which loads all parameters
|
||||
* for the current headquarters and makes them available as constants.
|
||||
* <p>
|
||||
* Parameters are loaded once per session (at login) and cached.
|
||||
* Individual parameters can also be fetched on-demand via the REST API
|
||||
* with full PHP-compatible fallback logic.
|
||||
* <p>
|
||||
* Usage in views:
|
||||
* <pre>
|
||||
* @Autowired
|
||||
* private ParameterProvider parameterProvider;
|
||||
*
|
||||
* String value = parameterProvider.get("MASK_MULTI_JOBLIST");
|
||||
* boolean enabled = parameterProvider.is("CS_TRACKING_ENABLED");
|
||||
* int count = parameterProvider.getInt("MASK_MULTI_JOBLIST", 4);
|
||||
* </pre>
|
||||
*/
|
||||
@Component
|
||||
@VaadinSessionScope
|
||||
public class ParameterProvider {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ParameterProvider.class);
|
||||
|
||||
private final RestClientService restClient;
|
||||
|
||||
/** Cached parameters: key -> value */
|
||||
private final Map<String, String> cache = new ConcurrentHashMap<>();
|
||||
|
||||
private boolean loaded = false;
|
||||
private Long headquartersId;
|
||||
private Long employeeId;
|
||||
|
||||
public ParameterProvider(RestClientService restClient) {
|
||||
this.restClient = restClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the parameter cache for the current session.
|
||||
* Should be called after successful login.
|
||||
*
|
||||
* @param hqId Headquarters ID of the logged-in user
|
||||
* @param empId Employee ID of the logged-in user
|
||||
*/
|
||||
public void init(Long hqId, Long empId) {
|
||||
this.headquartersId = hqId;
|
||||
this.employeeId = empId != null ? empId : 0L;
|
||||
reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload all parameters from the backend.
|
||||
* Calls the /api/parameters/bulk endpoint which returns all global + employee-specific parameters.
|
||||
*/
|
||||
public void reload() {
|
||||
if (headquartersId == null) {
|
||||
log.warn("Cannot load parameters: headquartersId not set. Call init() first.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, Object> params = restClient.getMap(
|
||||
"/parameters/bulk?hqId=" + headquartersId + "&empId=" + employeeId
|
||||
);
|
||||
cache.clear();
|
||||
if (params != null) {
|
||||
for (Map.Entry<String, Object> entry : params.entrySet()) {
|
||||
cache.put(entry.getKey(), entry.getValue() != null ? entry.getValue().toString() : "");
|
||||
}
|
||||
}
|
||||
loaded = true;
|
||||
log.info("Loaded {} parameters for hq={}, emp={}", cache.size(), headquartersId, employeeId);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load parameters for hq={}, emp={}", headquartersId, employeeId, e);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Parameter access methods
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Get a parameter value from the cache.
|
||||
*
|
||||
* @param key Parameter key (use constants from ParameterKeys)
|
||||
* @return Parameter value or empty string if not found
|
||||
*/
|
||||
public String get(String key) {
|
||||
return cache.getOrDefault(key, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a parameter value with a default fallback.
|
||||
*/
|
||||
public String get(String key, String defaultValue) {
|
||||
String value = cache.get(key);
|
||||
return (value != null && !value.isEmpty()) ? value : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a parameter value as integer.
|
||||
*/
|
||||
public int getInt(String key, int defaultValue) {
|
||||
String value = cache.get(key);
|
||||
if (value != null && !value.isEmpty()) {
|
||||
try {
|
||||
return Integer.parseInt(value.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a parameter value as long.
|
||||
*/
|
||||
public long getLong(String key, long defaultValue) {
|
||||
String value = cache.get(key);
|
||||
if (value != null && !value.isEmpty()) {
|
||||
try {
|
||||
return Long.parseLong(value.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a parameter is enabled (value is "1").
|
||||
* This is the most common boolean check in the PHP code.
|
||||
*/
|
||||
public boolean is(String key) {
|
||||
return "1".equals(cache.get(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a parameter has a non-empty value.
|
||||
*/
|
||||
public boolean has(String key) {
|
||||
String value = cache.get(key);
|
||||
return value != null && !value.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an object-based parameter (dynamic key with object ID suffix).
|
||||
* First checks key_objId, then falls back to the base key.
|
||||
*
|
||||
* @param baseKey Base parameter key (e.g., "CUSTOMER_MASK_JOBLIST_FIELDS")
|
||||
* @param objId Object ID (e.g., customer ID)
|
||||
* @return Parameter value or empty string
|
||||
*/
|
||||
public String getForObject(String baseKey, Long objId) {
|
||||
if (objId != null && objId > 0) {
|
||||
String objValue = cache.get(baseKey + objId);
|
||||
if (objValue != null && !objValue.isEmpty()) {
|
||||
return objValue;
|
||||
}
|
||||
// Also try with underscore separator (KEY_123456)
|
||||
if (!baseKey.endsWith("_")) {
|
||||
objValue = cache.get(baseKey + "_" + objId);
|
||||
if (objValue != null && !objValue.isEmpty()) {
|
||||
return objValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback to base key
|
||||
return get(baseKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an object-based parameter is enabled.
|
||||
*/
|
||||
public boolean isForObject(String baseKey, Long objId) {
|
||||
return "1".equals(getForObject(baseKey, objId));
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Fetch individual parameters via REST (bypassing cache)
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Fetch a parameter value directly from the backend with full fallback logic.
|
||||
* Use this for parameters that may not be in the bulk-loaded cache
|
||||
* (e.g., employee-specific parameters for other employees).
|
||||
*/
|
||||
public String fetchValue(String key, Long hqId, Long empId) {
|
||||
try {
|
||||
String url = "/parameters/value-full-fallback?key=" + key
|
||||
+ "&hqId=" + hqId + "&empId=" + (empId != null ? empId : 0);
|
||||
String value = restClient.get(url, String.class);
|
||||
return value != null ? value : "";
|
||||
} catch (Exception e) {
|
||||
log.debug("Parameter not found via REST: key={}, hq={}, emp={}", key, hqId, empId);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a parameter with the current session's headquarters.
|
||||
*/
|
||||
public String fetchValue(String key) {
|
||||
return fetchValue(key, headquartersId, employeeId);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// State
|
||||
// ========================================================================
|
||||
|
||||
public boolean isLoaded() {
|
||||
return loaded;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return cache.size();
|
||||
}
|
||||
|
||||
public Long getHeadquartersId() {
|
||||
return headquartersId;
|
||||
}
|
||||
|
||||
public Long getEmployeeId() {
|
||||
return employeeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all cached parameters as an unmodifiable map.
|
||||
*/
|
||||
public Map<String, String> getAll() {
|
||||
return Collections.unmodifiableMap(cache);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package de.votian.web.service;
|
||||
|
||||
import de.votian.web.model.SessionData;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.support.ScopeNotActiveException;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.http.client.MultipartBodyBuilder;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class RestClientService {
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final String baseUrl;
|
||||
private final ObjectProvider<SessionData> sessionDataProvider;
|
||||
|
||||
public RestClientService(RestTemplate restTemplate,
|
||||
@Qualifier("servicesBaseUrl") String baseUrl,
|
||||
ObjectProvider<SessionData> sessionDataProvider) {
|
||||
this.restTemplate = restTemplate;
|
||||
this.baseUrl = baseUrl;
|
||||
this.sessionDataProvider = sessionDataProvider;
|
||||
}
|
||||
|
||||
public <T> T get(String path, Class<T> type) {
|
||||
ResponseEntity<T> response = restTemplate.exchange(
|
||||
baseUrl + path, HttpMethod.GET, authorizedEntity(null), type);
|
||||
return response.getBody();
|
||||
}
|
||||
|
||||
public <T> List<T> getList(String path, ParameterizedTypeReference<List<T>> typeRef) {
|
||||
ResponseEntity<List<T>> response = restTemplate.exchange(
|
||||
baseUrl + path, HttpMethod.GET, authorizedEntity(null), typeRef);
|
||||
return response.getBody() != null ? response.getBody() : Collections.emptyList();
|
||||
}
|
||||
|
||||
public <T> T post(String path, Object body, Class<T> type) {
|
||||
ResponseEntity<T> response = restTemplate.exchange(
|
||||
baseUrl + path, HttpMethod.POST, authorizedEntity(body), type);
|
||||
return response.getBody();
|
||||
}
|
||||
|
||||
public <T> T postMultipart(String path, byte[] fileBytes, String filename, String contentType,
|
||||
Map<String, String> fields, Class<T> type) {
|
||||
MultipartBodyBuilder builder = new MultipartBodyBuilder();
|
||||
ByteArrayResource resource = new ByteArrayResource(fileBytes) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
};
|
||||
builder.part("file", resource)
|
||||
.filename(filename)
|
||||
.contentType(contentType != null && !contentType.isBlank()
|
||||
? MediaType.parseMediaType(contentType)
|
||||
: MediaType.APPLICATION_OCTET_STREAM);
|
||||
for (Map.Entry<String, String> entry : fields.entrySet()) {
|
||||
builder.part(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
ResponseEntity<T> response = restTemplate.exchange(
|
||||
baseUrl + path,
|
||||
HttpMethod.POST,
|
||||
authorizedMultipartEntity(builder.build()),
|
||||
type
|
||||
);
|
||||
return response.getBody();
|
||||
}
|
||||
|
||||
public void put(String path, Object body) {
|
||||
restTemplate.exchange(baseUrl + path, HttpMethod.PUT, authorizedEntity(body), Void.class);
|
||||
}
|
||||
|
||||
public void delete(String path) {
|
||||
restTemplate.exchange(baseUrl + path, HttpMethod.DELETE, authorizedEntity(null), Void.class);
|
||||
}
|
||||
|
||||
public Map<String, Object> getMap(String path) {
|
||||
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
|
||||
baseUrl + path, HttpMethod.GET, authorizedEntity(null),
|
||||
new ParameterizedTypeReference<Map<String, Object>>() {});
|
||||
return response.getBody() != null ? response.getBody() : Collections.emptyMap();
|
||||
}
|
||||
|
||||
public byte[] getBytes(String path) {
|
||||
ResponseEntity<byte[]> response = restTemplate.exchange(
|
||||
baseUrl + path, HttpMethod.GET, authorizedEntity(null), byte[].class);
|
||||
return response.getBody() != null ? response.getBody() : new byte[0];
|
||||
}
|
||||
|
||||
private HttpEntity<Object> authorizedEntity(Object body) {
|
||||
return new HttpEntity<>(body, authorizedHeaders(MediaType.APPLICATION_JSON));
|
||||
}
|
||||
|
||||
private HttpEntity<MultiValueMap<String, HttpEntity<?>>> authorizedMultipartEntity(MultiValueMap<String, HttpEntity<?>> body) {
|
||||
return new HttpEntity<>(body, authorizedHeaders(MediaType.MULTIPART_FORM_DATA));
|
||||
}
|
||||
|
||||
private HttpHeaders authorizedHeaders(MediaType contentType) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(contentType);
|
||||
SessionData sessionData = currentSessionData();
|
||||
if (sessionData != null && sessionData.getAuthToken() != null && !sessionData.getAuthToken().isBlank()) {
|
||||
headers.setBearerAuth(sessionData.getAuthToken());
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
private SessionData currentSessionData() {
|
||||
try {
|
||||
return sessionDataProvider.getIfAvailable();
|
||||
} catch (ScopeNotActiveException | IllegalStateException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
195
vaadin/src/main/java/de/votian/web/view/CostCenterView.java
Normal file
195
vaadin/src/main/java/de/votian/web/view/CostCenterView.java
Normal file
@@ -0,0 +1,195 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.dialog.Dialog;
|
||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.votian.web.model.SessionData;
|
||||
import de.votian.web.service.RestClientService;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Route(value = "costcenters", layout = MainLayout.class)
|
||||
@PageTitle("Votian - Kostenstellen")
|
||||
public class CostCenterView extends VerticalLayout {
|
||||
|
||||
private final RestClientService restClient;
|
||||
private final SessionData sessionData;
|
||||
private final Grid<Map<String, Object>> grid;
|
||||
private final TextField customerIdField;
|
||||
|
||||
public CostCenterView(RestClientService restClient, SessionData sessionData) {
|
||||
this.restClient = restClient;
|
||||
this.sessionData = sessionData;
|
||||
setSizeFull();
|
||||
setPadding(true);
|
||||
|
||||
customerIdField = new TextField("Kunden-ID");
|
||||
customerIdField.setPlaceholder("Kunden-ID eingeben...");
|
||||
|
||||
Button loadButton = new Button("Laden", VaadinIcon.DOWNLOAD.create(), e -> loadCostCenters());
|
||||
loadButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
|
||||
Button addButton = new Button("Neue Kostenstelle", VaadinIcon.PLUS.create(), e -> showAddDialog());
|
||||
addButton.setVisible(sessionData.canManageCostCenters());
|
||||
|
||||
HorizontalLayout toolbar = new HorizontalLayout(customerIdField, loadButton, addButton);
|
||||
toolbar.setAlignItems(Alignment.BASELINE);
|
||||
|
||||
grid = new Grid<>();
|
||||
grid.addColumn(m -> m.get("id")).setHeader("ID").setAutoWidth(true);
|
||||
grid.addColumn(m -> m.get("name")).setHeader("Name").setFlexGrow(2);
|
||||
grid.addColumn(m -> m.get("path")).setHeader("Pfad").setFlexGrow(2);
|
||||
grid.addColumn(m -> "1".equals(str(m.get("visible"))) ? "Ja" : "Nein").setHeader("Sichtbar").setAutoWidth(true);
|
||||
grid.addColumn(m -> "1".equals(str(m.get("isExtern"))) ? "Ja" : "Nein").setHeader("Extern").setAutoWidth(true);
|
||||
grid.setSizeFull();
|
||||
|
||||
grid.addItemDoubleClickListener(e -> showEditDialog(e.getItem()));
|
||||
|
||||
add(toolbar, grid);
|
||||
|
||||
if (sessionData.isCustomerUser() && sessionData.getCustomerId() != null) {
|
||||
customerIdField.setValue(String.valueOf(sessionData.getCustomerId()));
|
||||
customerIdField.setReadOnly(true);
|
||||
loadCostCenters();
|
||||
}
|
||||
}
|
||||
|
||||
private void loadCostCenters() {
|
||||
String csId = customerIdField.getValue().trim();
|
||||
if (csId.isEmpty()) {
|
||||
Notification.show("Bitte Kunden-ID eingeben.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Object[] centers = restClient.get("/costcenters/customer/" + csId, Object[].class);
|
||||
if (centers != null) {
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (Object c : centers) {
|
||||
if (c instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> map = (Map<String, Object>) c;
|
||||
Long id = map.get("id") instanceof Number number ? number.longValue() : null;
|
||||
if (!sessionData.isCustomerUser() || sessionData.hasCostCenterAccess(id)) {
|
||||
items.add(map);
|
||||
}
|
||||
}
|
||||
}
|
||||
grid.setItems(items);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Notification.show("Fehler: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void showAddDialog() {
|
||||
if (!sessionData.canManageCostCenters()) {
|
||||
Notification.show("Keine Berechtigung zum Anlegen von Kostenstellen.");
|
||||
return;
|
||||
}
|
||||
String csId = customerIdField.getValue().trim();
|
||||
if (csId.isEmpty()) {
|
||||
Notification.show("Bitte zuerst Kunden-ID eingeben.");
|
||||
return;
|
||||
}
|
||||
|
||||
Dialog dialog = new Dialog();
|
||||
dialog.setHeaderTitle("Neue Kostenstelle");
|
||||
dialog.setWidth("500px");
|
||||
|
||||
TextField nameField = new TextField("Name");
|
||||
nameField.setWidthFull();
|
||||
TextField pathField = new TextField("Pfad");
|
||||
pathField.setWidthFull();
|
||||
ComboBox<String> visibleField = new ComboBox<>("Sichtbar");
|
||||
visibleField.setItems("0", "1");
|
||||
visibleField.setItemLabelGenerator(v -> "1".equals(v) ? "Ja" : "Nein");
|
||||
visibleField.setValue("1");
|
||||
|
||||
FormLayout form = new FormLayout(nameField, pathField, visibleField);
|
||||
dialog.add(form);
|
||||
|
||||
Button saveBtn = new Button("Speichern", ev -> {
|
||||
try {
|
||||
Map<String, Object> csc = new HashMap<>();
|
||||
csc.put("customerId", Long.parseLong(csId));
|
||||
csc.put("name", nameField.getValue());
|
||||
csc.put("path", pathField.getValue());
|
||||
csc.put("visible", visibleField.getValue());
|
||||
restClient.post("/costcenters", csc, LinkedHashMap.class);
|
||||
Notification.show("Kostenstelle gespeichert!", 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
dialog.close();
|
||||
loadCostCenters();
|
||||
} catch (Exception ex) {
|
||||
Notification.show("Fehler: " + ex.getMessage());
|
||||
}
|
||||
});
|
||||
saveBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
|
||||
Button cancelBtn = new Button("Abbrechen", ev -> dialog.close());
|
||||
dialog.getFooter().add(cancelBtn, saveBtn);
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
private void showEditDialog(Map<String, Object> item) {
|
||||
if (!sessionData.canManageCostCenters()) {
|
||||
Notification.show("Keine Berechtigung zum Bearbeiten von Kostenstellen.");
|
||||
return;
|
||||
}
|
||||
Dialog dialog = new Dialog();
|
||||
dialog.setHeaderTitle("Kostenstelle bearbeiten");
|
||||
dialog.setWidth("500px");
|
||||
|
||||
TextField nameField = new TextField("Name");
|
||||
nameField.setValue(str(item.get("name")));
|
||||
nameField.setWidthFull();
|
||||
|
||||
TextField pathField = new TextField("Pfad");
|
||||
pathField.setValue(str(item.get("path")));
|
||||
pathField.setWidthFull();
|
||||
|
||||
ComboBox<String> visibleField = new ComboBox<>("Sichtbar");
|
||||
visibleField.setItems("0", "1");
|
||||
visibleField.setItemLabelGenerator(v -> "1".equals(v) ? "Ja" : "Nein");
|
||||
visibleField.setValue(str(item.get("visible")));
|
||||
|
||||
FormLayout form = new FormLayout(nameField, pathField, visibleField);
|
||||
dialog.add(form);
|
||||
|
||||
Button saveBtn = new Button("Speichern", ev -> {
|
||||
try {
|
||||
Map<String, Object> updates = new HashMap<>();
|
||||
updates.put("name", nameField.getValue());
|
||||
updates.put("path", pathField.getValue());
|
||||
updates.put("visible", visibleField.getValue());
|
||||
restClient.put("/costcenters/" + item.get("id"), updates);
|
||||
Notification.show("Gespeichert!", 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
dialog.close();
|
||||
loadCostCenters();
|
||||
} catch (Exception ex) {
|
||||
Notification.show("Fehler: " + ex.getMessage());
|
||||
}
|
||||
});
|
||||
saveBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
|
||||
Button cancelBtn = new Button("Abbrechen", ev -> dialog.close());
|
||||
dialog.getFooter().add(cancelBtn, saveBtn);
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
private String str(Object obj) {
|
||||
return obj != null ? obj.toString() : "";
|
||||
}
|
||||
}
|
||||
731
vaadin/src/main/java/de/votian/web/view/CourierDetailView.java
Normal file
731
vaadin/src/main/java/de/votian/web/view/CourierDetailView.java
Normal file
@@ -0,0 +1,731 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.checkbox.CheckboxGroup;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.datepicker.DatePicker;
|
||||
import com.vaadin.flow.component.dialog.Dialog;
|
||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.html.Paragraph;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.tabs.Tab;
|
||||
import com.vaadin.flow.component.tabs.Tabs;
|
||||
import com.vaadin.flow.component.textfield.PasswordField;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.router.*;
|
||||
import de.votian.web.model.SessionData;
|
||||
import de.votian.web.service.RestClientService;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Route(value = "couriers/:id?", layout = MainLayout.class)
|
||||
@PageTitle("Votian - Kurierdetail")
|
||||
public class CourierDetailView extends VerticalLayout implements HasUrlParameter<String> {
|
||||
|
||||
private final RestClientService restClient;
|
||||
private final SessionData sessionData;
|
||||
private Long courierId;
|
||||
private boolean isNew = true;
|
||||
|
||||
private final TextField compField = new TextField("Firma");
|
||||
private final TextField comp2Field = new TextField("Firma 2");
|
||||
private final TextField hsnoField = new TextField("Hausnummer");
|
||||
private final TextField streetField = new TextField("Strasse");
|
||||
private final TextField zipcodeField = new TextField("PLZ");
|
||||
private final TextField cityField = new TextField("Ort");
|
||||
private final TextField ilnField = new TextField("ILN");
|
||||
private final TextField taxIdField = new TextField("Steuer-Nr.");
|
||||
|
||||
private final TextField eidField = new TextField("Kurier-Nr. (EID)");
|
||||
private final TextField sidField = new TextField("SID");
|
||||
private final ComboBox<String> availableField = new ComboBox<>("Verfuegbar");
|
||||
private final TextField mobilePdaField = new TextField("Mobile/PDA");
|
||||
private final TextField trackingZipcodeField = new TextField("Tracking-PLZ");
|
||||
private final TextField trackingGpsTimeField = new TextField("Letzte Ortung");
|
||||
private final TextField trackingGpsTypeField = new TextField("Ortungstyp");
|
||||
private final TextField trackingGpsCoordinatesField = new TextField("Koordinaten");
|
||||
private final TextField trackingAvailableTimeField = new TextField("Verfuegbar seit");
|
||||
private final CheckboxGroup<GroupOption> groupField = new CheckboxGroup<>();
|
||||
private final CheckboxGroup<FilterOption> filterField = new CheckboxGroup<>();
|
||||
|
||||
private final TextField accountField = new TextField("Benutzerkonto");
|
||||
private final TextField nameField = new TextField("Name");
|
||||
private final TextField firstnameField = new TextField("Vorname");
|
||||
private final TextField emailField = new TextField("E-Mail");
|
||||
private final TextField phoneField = new TextField("Telefon");
|
||||
private final TextField phone2Field = new TextField("Telefon 2");
|
||||
private final TextField faxField = new TextField("Fax");
|
||||
private final DatePicker birthdateField = new DatePicker("Geburtsdatum");
|
||||
private final TextField countryField = new TextField("Land");
|
||||
private final PasswordField passwordField = new PasswordField("Passwort");
|
||||
|
||||
private final VerticalLayout vehicleContent = new VerticalLayout();
|
||||
private final Grid<VehicleRow> vehicleGrid = new Grid<>(VehicleRow.class, false);
|
||||
private final Button addVehicleButton = new Button("Fahrzeug anlegen", VaadinIcon.PLUS.create());
|
||||
private final Button editVehicleButton = new Button("Fahrzeug bearbeiten", VaadinIcon.EDIT.create());
|
||||
private final Button deleteVehicleButton = new Button("Fahrzeug loeschen", VaadinIcon.TRASH.create());
|
||||
|
||||
private final List<VehicleTypeOption> vehicleTypes = new ArrayList<>();
|
||||
private final List<VehicleRow> vehicles = new ArrayList<>();
|
||||
private final List<GroupOption> groupOptions = new ArrayList<>();
|
||||
private final List<FilterOption> filterOptions = new ArrayList<>();
|
||||
|
||||
public CourierDetailView(RestClientService restClient, SessionData sessionData) {
|
||||
this.restClient = restClient;
|
||||
this.sessionData = sessionData;
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
setWidthFull();
|
||||
|
||||
availableField.setItems("0", "1");
|
||||
availableField.setItemLabelGenerator(v -> "1".equals(v) ? "Ja" : "Nein");
|
||||
mobilePdaField.setWidthFull();
|
||||
trackingZipcodeField.setReadOnly(true);
|
||||
trackingGpsTimeField.setReadOnly(true);
|
||||
trackingGpsTypeField.setReadOnly(true);
|
||||
trackingGpsCoordinatesField.setReadOnly(true);
|
||||
trackingAvailableTimeField.setReadOnly(true);
|
||||
groupField.setLabel("Gruppen");
|
||||
groupField.setWidthFull();
|
||||
groupField.setItemLabelGenerator(GroupOption::label);
|
||||
filterField.setLabel("Filter");
|
||||
filterField.setWidthFull();
|
||||
filterField.setItemLabelGenerator(FilterOption::label);
|
||||
|
||||
vehicleContent.setPadding(false);
|
||||
vehicleContent.setSpacing(true);
|
||||
vehicleContent.setWidthFull();
|
||||
|
||||
configureVehicleGrid();
|
||||
configureVehicleActions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setParameter(BeforeEvent event, @OptionalParameter String parameter) {
|
||||
removeAll();
|
||||
resetState();
|
||||
loadVehicleTypes();
|
||||
loadGroupFilterOptions();
|
||||
if (parameter != null && !parameter.isEmpty() && !"new".equals(parameter)) {
|
||||
try {
|
||||
courierId = Long.parseLong(parameter);
|
||||
isNew = false;
|
||||
loadCourier();
|
||||
loadVehicles();
|
||||
} catch (NumberFormatException e) {
|
||||
isNew = true;
|
||||
}
|
||||
}
|
||||
buildForm();
|
||||
}
|
||||
|
||||
private void buildForm() {
|
||||
H3 title = new H3(isNew ? "Neuer Kurier" : "Kurier bearbeiten");
|
||||
|
||||
Tab companyTab = new Tab("Firma");
|
||||
Tab contactTab = new Tab("Kontakt");
|
||||
Tab courierTab = new Tab("Kurier-Daten");
|
||||
Tab vehiclesTab = new Tab("Fahrzeuge");
|
||||
Tabs tabs = new Tabs(companyTab, contactTab, courierTab, vehiclesTab);
|
||||
|
||||
FormLayout companyForm = new FormLayout();
|
||||
companyForm.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1),
|
||||
new FormLayout.ResponsiveStep("500px", 2));
|
||||
companyForm.add(compField, comp2Field, streetField, hsnoField, zipcodeField, cityField, ilnField, taxIdField);
|
||||
|
||||
FormLayout contactForm = new FormLayout();
|
||||
contactForm.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1),
|
||||
new FormLayout.ResponsiveStep("500px", 2));
|
||||
contactForm.add(nameField, firstnameField, emailField, phoneField, phone2Field, faxField,
|
||||
birthdateField, countryField, accountField, passwordField);
|
||||
contactForm.setVisible(false);
|
||||
|
||||
FormLayout courierForm = new FormLayout();
|
||||
courierForm.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1),
|
||||
new FormLayout.ResponsiveStep("500px", 2));
|
||||
courierForm.add(
|
||||
eidField,
|
||||
sidField,
|
||||
availableField,
|
||||
mobilePdaField,
|
||||
trackingZipcodeField,
|
||||
trackingGpsTimeField,
|
||||
trackingGpsTypeField,
|
||||
trackingGpsCoordinatesField,
|
||||
trackingAvailableTimeField,
|
||||
groupField,
|
||||
filterField
|
||||
);
|
||||
courierForm.setColspan(trackingGpsCoordinatesField, 2);
|
||||
courierForm.setColspan(groupField, 2);
|
||||
courierForm.setColspan(filterField, 2);
|
||||
courierForm.setVisible(false);
|
||||
|
||||
buildVehicleContent();
|
||||
vehicleContent.setVisible(false);
|
||||
|
||||
tabs.addSelectedChangeListener(e -> {
|
||||
companyForm.setVisible(e.getSelectedTab() == companyTab);
|
||||
contactForm.setVisible(e.getSelectedTab() == contactTab);
|
||||
courierForm.setVisible(e.getSelectedTab() == courierTab);
|
||||
vehicleContent.setVisible(e.getSelectedTab() == vehiclesTab);
|
||||
});
|
||||
|
||||
Button saveButton = new Button("Speichern", VaadinIcon.CHECK.create(), e -> save());
|
||||
saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
saveButton.setEnabled(sessionData.canManageCouriers());
|
||||
Button cancelButton = new Button("Abbrechen", e ->
|
||||
getUI().ifPresent(ui -> ui.navigate(CourierListView.class)));
|
||||
|
||||
HorizontalLayout buttons = new HorizontalLayout(saveButton, cancelButton);
|
||||
add(title, tabs, companyForm, contactForm, courierForm, vehicleContent, buttons);
|
||||
}
|
||||
|
||||
private void configureVehicleGrid() {
|
||||
vehicleGrid.addColumn(VehicleRow::sid).setHeader("SID").setAutoWidth(true);
|
||||
vehicleGrid.addColumn(VehicleRow::vehicleTypeName).setHeader("Fahrzeugtyp").setAutoWidth(true);
|
||||
vehicleGrid.setWidthFull();
|
||||
vehicleGrid.setMinHeight("240px");
|
||||
vehicleGrid.asSingleSelect().addValueChangeListener(e -> refreshVehicleActionState());
|
||||
}
|
||||
|
||||
private void configureVehicleActions() {
|
||||
addVehicleButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
editVehicleButton.addClickListener(e -> {
|
||||
VehicleRow selectedVehicle = vehicleGrid.asSingleSelect().getValue();
|
||||
if (selectedVehicle != null) {
|
||||
openVehicleDialog(selectedVehicle);
|
||||
}
|
||||
});
|
||||
deleteVehicleButton.addThemeVariants(ButtonVariant.LUMO_ERROR);
|
||||
addVehicleButton.addClickListener(e -> openVehicleDialog(null));
|
||||
deleteVehicleButton.addClickListener(e -> deleteSelectedVehicle());
|
||||
refreshVehicleActionState();
|
||||
}
|
||||
|
||||
private void buildVehicleContent() {
|
||||
vehicleContent.removeAll();
|
||||
vehicleGrid.setItems(vehicles);
|
||||
vehicleGrid.asSingleSelect().clear();
|
||||
|
||||
String message = courierId == null
|
||||
? "Fahrzeuge koennen nach dem ersten Speichern des Kuriers gepflegt werden."
|
||||
: "Zusatzfahrzeuge des Kuriers inklusive Fahrzeugtyp pflegen.";
|
||||
Paragraph info = new Paragraph(message);
|
||||
HorizontalLayout actions = new HorizontalLayout(addVehicleButton, editVehicleButton, deleteVehicleButton);
|
||||
vehicleContent.add(info, actions, vehicleGrid);
|
||||
refreshVehicleActionState();
|
||||
}
|
||||
|
||||
private void openVehicleDialog(VehicleRow existingVehicle) {
|
||||
if (!sessionData.canManageCouriers()) {
|
||||
Notification.show("Keine Berechtigung zur Fahrzeugpflege.", 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
return;
|
||||
}
|
||||
if (courierId == null) {
|
||||
Notification.show("Kurier zuerst speichern.", 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
Dialog dialog = new Dialog();
|
||||
dialog.setHeaderTitle(existingVehicle == null ? "Fahrzeug anlegen" : "Fahrzeug bearbeiten");
|
||||
|
||||
TextField vehicleSidField = new TextField("SID");
|
||||
vehicleSidField.setWidthFull();
|
||||
ComboBox<VehicleTypeOption> vehicleTypeField = new ComboBox<>("Fahrzeugtyp");
|
||||
vehicleTypeField.setItems(vehicleTypes);
|
||||
vehicleTypeField.setItemLabelGenerator(VehicleTypeOption::label);
|
||||
vehicleTypeField.setWidthFull();
|
||||
|
||||
if (existingVehicle != null) {
|
||||
vehicleSidField.setValue(existingVehicle.sid());
|
||||
vehicleTypeField.setValue(findVehicleType(existingVehicle.vehicleTypeId()));
|
||||
}
|
||||
|
||||
Button saveButton = new Button("Speichern", e -> {
|
||||
if (vehicleTypeField.getValue() == null) {
|
||||
Notification.show("Bitte einen Fahrzeugtyp waehlen.", 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, Object> vehicle = new HashMap<>();
|
||||
vehicle.put("sid", vehicleSidField.getValue());
|
||||
vehicle.put("vehicleTypeId", vehicleTypeField.getValue().id());
|
||||
|
||||
if (existingVehicle == null) {
|
||||
restClient.post("/couriers/" + courierId + "/vehicles", vehicle, LinkedHashMap.class);
|
||||
} else {
|
||||
restClient.put("/couriers/" + courierId + "/vehicles/" + existingVehicle.id(), vehicle);
|
||||
}
|
||||
|
||||
loadVehicles();
|
||||
vehicleGrid.setItems(vehicles);
|
||||
vehicleGrid.asSingleSelect().clear();
|
||||
refreshVehicleActionState();
|
||||
dialog.close();
|
||||
} catch (Exception ex) {
|
||||
Notification.show("Fehler: " + ex.getMessage(), 5000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
});
|
||||
saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
|
||||
Button cancelButton = new Button("Abbrechen", e -> dialog.close());
|
||||
HorizontalLayout footer = new HorizontalLayout(saveButton, cancelButton);
|
||||
VerticalLayout content = new VerticalLayout(vehicleSidField, vehicleTypeField, footer);
|
||||
content.setPadding(false);
|
||||
content.setSpacing(true);
|
||||
|
||||
dialog.add(content);
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
private void deleteSelectedVehicle() {
|
||||
if (!sessionData.canManageCouriers()) {
|
||||
return;
|
||||
}
|
||||
VehicleRow selectedVehicle = vehicleGrid.asSingleSelect().getValue();
|
||||
if (selectedVehicle == null || courierId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
restClient.delete("/couriers/" + courierId + "/vehicles/" + selectedVehicle.id());
|
||||
loadVehicles();
|
||||
vehicleGrid.setItems(vehicles);
|
||||
vehicleGrid.asSingleSelect().clear();
|
||||
refreshVehicleActionState();
|
||||
Notification.show("Fahrzeug geloescht.", 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
} catch (Exception e) {
|
||||
Notification.show("Fehler: " + e.getMessage(), 5000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadCourier() {
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> courier = restClient.get("/couriers/" + courierId, LinkedHashMap.class);
|
||||
if (courier != null) {
|
||||
eidField.setValue(str(courier.get("eid")));
|
||||
sidField.setValue(str(courier.get("sid")));
|
||||
String available = str(courier.get("available"));
|
||||
if (!available.isBlank()) {
|
||||
availableField.setValue(available);
|
||||
}
|
||||
mobilePdaField.setValue(str(courier.get("mobilePda")));
|
||||
trackingZipcodeField.setValue(str(courier.get("locationZipcode")));
|
||||
trackingGpsTimeField.setValue(dateTimeText(courier.get("gpsTime")));
|
||||
trackingGpsTypeField.setValue(gpsTypeLabel(toInteger(courier.get("gpsType"))));
|
||||
trackingGpsCoordinatesField.setValue(trackingCoordinates(courier));
|
||||
trackingAvailableTimeField.setValue(dateTimeText(courier.get("availableTime")));
|
||||
compField.setValue(str(courier.get("companyName")));
|
||||
comp2Field.setValue(str(courier.get("companyName2")));
|
||||
hsnoField.setValue(str(courier.get("hsno")));
|
||||
streetField.setValue(str(courier.get("street")));
|
||||
zipcodeField.setValue(str(courier.get("zipcode")));
|
||||
cityField.setValue(str(courier.get("city")));
|
||||
ilnField.setValue(str(courier.get("iln")));
|
||||
taxIdField.setValue(str(courier.get("taxIdNo")));
|
||||
accountField.setValue(str(courier.get("account")));
|
||||
nameField.setValue(str(courier.get("name")));
|
||||
firstnameField.setValue(str(courier.get("firstname")));
|
||||
emailField.setValue(str(courier.get("email")));
|
||||
phoneField.setValue(str(courier.get("phone")));
|
||||
phone2Field.setValue(str(courier.get("phone2")));
|
||||
faxField.setValue(str(courier.get("fax")));
|
||||
countryField.setValue(str(courier.get("country")));
|
||||
groupField.setValue(selectGroups(str(courier.get("group"))));
|
||||
filterField.setValue(selectFilters(str(courier.get("filter"))));
|
||||
if (courier.get("birthdate") != null) {
|
||||
birthdateField.setValue(java.time.LocalDate.parse(courier.get("birthdate").toString()));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Notification.show("Fehler: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void loadVehicleTypes() {
|
||||
try {
|
||||
List<LinkedHashMap<String, Object>> result = restClient.getList(
|
||||
"/metatypes/vehicletypes",
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
for (Map<String, Object> row : result) {
|
||||
Integer id = toInteger(row.get("id"));
|
||||
if (id != null) {
|
||||
vehicleTypes.add(new VehicleTypeOption(id, str(row.get("value"))));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Notification.show("Fahrzeugtypen konnten nicht geladen werden: " + e.getMessage(),
|
||||
5000, Notification.Position.MIDDLE).addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadGroupFilterOptions() {
|
||||
if (sessionData.getHeadquartersId() == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
groupOptions.clear();
|
||||
List<LinkedHashMap<String, Object>> groups = restClient.getList(
|
||||
"/group-filter-admin/groups?hqId=" + sessionData.getHeadquartersId(),
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
for (Map<String, Object> row : groups) {
|
||||
Long id = toLong(row.get("id"));
|
||||
if (id != null) {
|
||||
groupOptions.add(new GroupOption(id, str(row.get("name"))));
|
||||
}
|
||||
}
|
||||
groupField.setItems(groupOptions);
|
||||
|
||||
filterOptions.clear();
|
||||
List<LinkedHashMap<String, Object>> filters = restClient.getList(
|
||||
"/group-filter-admin/filters?hqId=" + sessionData.getHeadquartersId(),
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
for (Map<String, Object> row : filters) {
|
||||
String shortCode = str(row.get("shortCode"));
|
||||
if (!shortCode.isBlank()) {
|
||||
filterOptions.add(new FilterOption(shortCode, str(row.get("text"))));
|
||||
}
|
||||
}
|
||||
filterField.setItems(filterOptions);
|
||||
} catch (Exception e) {
|
||||
Notification.show("Gruppen- und Filterkatalog konnte nicht geladen werden: " + e.getMessage(),
|
||||
5000, Notification.Position.MIDDLE).addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadVehicles() {
|
||||
vehicles.clear();
|
||||
if (courierId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
List<LinkedHashMap<String, Object>> result = restClient.getList(
|
||||
"/couriers/" + courierId + "/vehicles",
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
for (Map<String, Object> row : result) {
|
||||
Long id = toLong(row.get("id"));
|
||||
Integer vehicleTypeId = toInteger(row.get("vehicleTypeId"));
|
||||
vehicles.add(new VehicleRow(
|
||||
id,
|
||||
str(row.get("sid")),
|
||||
vehicleTypeId,
|
||||
findVehicleTypeLabel(vehicleTypeId)
|
||||
));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Notification.show("Kurierfahrzeuge konnten nicht geladen werden: " + e.getMessage(),
|
||||
5000, Notification.Position.MIDDLE).addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void save() {
|
||||
if (!sessionData.canManageCouriers()) {
|
||||
Notification.show("Keine Berechtigung zum Bearbeiten von Kurieren.", 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
boolean createdNow = isNew;
|
||||
Map<String, Object> request = new HashMap<>();
|
||||
Map<String, Object> courier = new HashMap<>();
|
||||
courier.put("headquartersId", sessionData.getHeadquartersId());
|
||||
courier.put("eid", eidField.getValue());
|
||||
courier.put("sid", sidField.getValue());
|
||||
courier.put("available", availableField.getValue());
|
||||
courier.put("mobilePda", mobilePdaField.getValue());
|
||||
courier.put("group", toGroupString(groupField.getValue()));
|
||||
courier.put("filter", toFilterString(filterField.getValue()));
|
||||
request.put("courier", courier);
|
||||
|
||||
Map<String, Object> company = new HashMap<>();
|
||||
company.put("comp", compField.getValue());
|
||||
company.put("comp2", comp2Field.getValue());
|
||||
company.put("hsno", hsnoField.getValue());
|
||||
company.put("iln", ilnField.getValue());
|
||||
company.put("taxIdNo", taxIdField.getValue());
|
||||
request.put("company", company);
|
||||
|
||||
Map<String, Object> address = new HashMap<>();
|
||||
address.put("street", streetField.getValue());
|
||||
address.put("zipcode", zipcodeField.getValue());
|
||||
address.put("city", cityField.getValue());
|
||||
address.put("country", countryField.getValue());
|
||||
request.put("address", address);
|
||||
|
||||
Map<String, Object> user = new HashMap<>();
|
||||
user.put("account", accountField.getValue());
|
||||
user.put("name", nameField.getValue());
|
||||
user.put("firstname", firstnameField.getValue());
|
||||
user.put("email", emailField.getValue());
|
||||
user.put("phone", phoneField.getValue());
|
||||
user.put("phone2", phone2Field.getValue());
|
||||
user.put("fax", faxField.getValue());
|
||||
user.put("birthdate", birthdateField.getValue());
|
||||
user.put("country", countryField.getValue());
|
||||
request.put("user", user);
|
||||
|
||||
if (isNew) {
|
||||
request.put("password", passwordField.getValue());
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> created = restClient.post("/couriers", request, LinkedHashMap.class);
|
||||
courierId = created != null ? toLong(created.get("id")) : null;
|
||||
isNew = false;
|
||||
} else {
|
||||
restClient.put("/couriers/" + courierId, request);
|
||||
}
|
||||
|
||||
Notification.show("Kurier gespeichert!", 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
|
||||
if (createdNow && courierId != null && getUI().isPresent()) {
|
||||
getUI().get().navigate("couriers/" + courierId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Notification.show("Fehler: " + e.getMessage(), 5000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshVehicleActionState() {
|
||||
VehicleRow selectedVehicle = vehicleGrid.asSingleSelect().getValue();
|
||||
boolean editable = sessionData.canManageCouriers() && courierId != null && !isNew;
|
||||
addVehicleButton.setEnabled(editable);
|
||||
editVehicleButton.setEnabled(editable && selectedVehicle != null);
|
||||
deleteVehicleButton.setEnabled(editable && selectedVehicle != null);
|
||||
}
|
||||
|
||||
private void resetState() {
|
||||
courierId = null;
|
||||
isNew = true;
|
||||
vehicleTypes.clear();
|
||||
vehicles.clear();
|
||||
groupOptions.clear();
|
||||
filterOptions.clear();
|
||||
|
||||
compField.clear();
|
||||
comp2Field.clear();
|
||||
hsnoField.clear();
|
||||
streetField.clear();
|
||||
zipcodeField.clear();
|
||||
cityField.clear();
|
||||
ilnField.clear();
|
||||
taxIdField.clear();
|
||||
|
||||
eidField.clear();
|
||||
sidField.clear();
|
||||
availableField.clear();
|
||||
mobilePdaField.clear();
|
||||
trackingZipcodeField.clear();
|
||||
trackingGpsTimeField.clear();
|
||||
trackingGpsTypeField.clear();
|
||||
trackingGpsCoordinatesField.clear();
|
||||
trackingAvailableTimeField.clear();
|
||||
groupField.clear();
|
||||
groupField.setItems(groupOptions);
|
||||
filterField.clear();
|
||||
filterField.setItems(filterOptions);
|
||||
|
||||
accountField.clear();
|
||||
nameField.clear();
|
||||
firstnameField.clear();
|
||||
emailField.clear();
|
||||
phoneField.clear();
|
||||
phone2Field.clear();
|
||||
faxField.clear();
|
||||
birthdateField.clear();
|
||||
countryField.clear();
|
||||
passwordField.clear();
|
||||
|
||||
vehicleGrid.setItems(vehicles);
|
||||
vehicleGrid.asSingleSelect().clear();
|
||||
refreshVehicleActionState();
|
||||
}
|
||||
|
||||
private VehicleTypeOption findVehicleType(Integer typeId) {
|
||||
if (typeId == null) {
|
||||
return null;
|
||||
}
|
||||
for (VehicleTypeOption option : vehicleTypes) {
|
||||
if (option.id().equals(typeId)) {
|
||||
return option;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String findVehicleTypeLabel(Integer typeId) {
|
||||
VehicleTypeOption option = findVehicleType(typeId);
|
||||
return option != null ? option.label() : "";
|
||||
}
|
||||
|
||||
private LinkedHashSet<GroupOption> selectGroups(String value) {
|
||||
Set<Long> selectedIds = parseLongTokens(value);
|
||||
LinkedHashSet<GroupOption> selected = new LinkedHashSet<>();
|
||||
for (GroupOption option : groupOptions) {
|
||||
if (selectedIds.contains(option.id())) {
|
||||
selected.add(option);
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
private LinkedHashSet<FilterOption> selectFilters(String value) {
|
||||
Set<String> selectedCodes = parseStringTokens(value);
|
||||
LinkedHashSet<FilterOption> selected = new LinkedHashSet<>();
|
||||
for (FilterOption option : filterOptions) {
|
||||
if (selectedCodes.contains(option.shortCode())) {
|
||||
selected.add(option);
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
private String toGroupString(Set<GroupOption> selected) {
|
||||
if (selected == null || selected.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return "," + selected.stream()
|
||||
.map(GroupOption::id)
|
||||
.sorted()
|
||||
.map(String::valueOf)
|
||||
.reduce((left, right) -> left + "," + right)
|
||||
.orElse("") + ",";
|
||||
}
|
||||
|
||||
private String toFilterString(Set<FilterOption> selected) {
|
||||
if (selected == null || selected.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return "," + selected.stream()
|
||||
.map(FilterOption::shortCode)
|
||||
.sorted(String::compareToIgnoreCase)
|
||||
.reduce((left, right) -> left + "," + right)
|
||||
.orElse("") + ",";
|
||||
}
|
||||
|
||||
private Set<Long> parseLongTokens(String value) {
|
||||
Set<Long> result = new LinkedHashSet<>();
|
||||
if (value == null || value.isBlank()) {
|
||||
return result;
|
||||
}
|
||||
for (String token : value.split(",")) {
|
||||
if (token == null || token.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
result.add(Long.parseLong(token.trim()));
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Set<String> parseStringTokens(String value) {
|
||||
Set<String> result = new LinkedHashSet<>();
|
||||
if (value == null || value.isBlank()) {
|
||||
return result;
|
||||
}
|
||||
for (String token : value.split(",")) {
|
||||
if (token != null && !token.isBlank()) {
|
||||
result.add(token.trim());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Long toLong(Object obj) {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
if (obj instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(obj.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Integer toInteger(Object obj) {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
if (obj instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(obj.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String str(Object obj) {
|
||||
return obj != null ? obj.toString() : "";
|
||||
}
|
||||
|
||||
private String dateTimeText(Object obj) {
|
||||
String value = str(obj);
|
||||
return value.isBlank() ? "" : value.replace("T", " ");
|
||||
}
|
||||
|
||||
private String trackingCoordinates(Map<String, Object> courier) {
|
||||
String latitude = str(courier.get("gpsLatitude"));
|
||||
String longitude = str(courier.get("gpsLongitude"));
|
||||
if (latitude.isBlank() || longitude.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
return latitude + ", " + longitude;
|
||||
}
|
||||
|
||||
private String gpsTypeLabel(Integer gpsType) {
|
||||
if (gpsType == null) {
|
||||
return "";
|
||||
}
|
||||
return switch (gpsType) {
|
||||
case 0 -> "unbestimmt";
|
||||
case 1 -> "LBS";
|
||||
case 2 -> "GPS";
|
||||
case 3 -> "Network";
|
||||
case 9 -> "Ortung aus";
|
||||
default -> "Typ " + gpsType;
|
||||
};
|
||||
}
|
||||
|
||||
private record VehicleTypeOption(Integer id, String label) { }
|
||||
|
||||
private record VehicleRow(Long id, String sid, Integer vehicleTypeId, String vehicleTypeName) { }
|
||||
|
||||
private record GroupOption(Long id, String label) { }
|
||||
|
||||
private record FilterOption(String shortCode, String label) { }
|
||||
}
|
||||
401
vaadin/src/main/java/de/votian/web/view/CourierInvoiceView.java
Normal file
401
vaadin/src/main/java/de/votian/web/view/CourierInvoiceView.java
Normal file
@@ -0,0 +1,401 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.html.Anchor;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.html.H4;
|
||||
import com.vaadin.flow.component.html.Pre;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.datepicker.DatePicker;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import com.vaadin.flow.server.StreamResource;
|
||||
import de.votian.web.service.RestClientService;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Route(value = "courier-invoices", layout = MainLayout.class)
|
||||
@PageTitle("Votian - Kurierauftraege")
|
||||
public class CourierInvoiceView extends VerticalLayout {
|
||||
|
||||
private final RestClientService restClient;
|
||||
|
||||
private final DatePicker fromDate = new DatePicker("Von");
|
||||
private final DatePicker toDate = new DatePicker("Bis");
|
||||
private final ComboBox<HistoryMode> modeField = new ComboBox<>("Ansicht");
|
||||
|
||||
private final Span totalJobs = new Span("-");
|
||||
private final Span totalRevenue = new Span("-");
|
||||
private final Span totalCourierPrice = new Span("-");
|
||||
|
||||
private final Grid<CourierRow> grid = new Grid<>(CourierRow.class, false);
|
||||
private final Anchor csvExportLink = new Anchor();
|
||||
private final Anchor pdfExportLink = new Anchor();
|
||||
private final Button csvExportButton = new Button("CSV exportieren");
|
||||
private final Button pdfExportButton = new Button("PDF exportieren");
|
||||
|
||||
private final Span detailJob = new Span("-");
|
||||
private final Span detailCustomer = new Span("-");
|
||||
private final Span detailRoute = new Span("-");
|
||||
private final Span detailTime = new Span("-");
|
||||
private final Span detailStatus = new Span("-");
|
||||
private final Pre invoiceText = new Pre();
|
||||
|
||||
private CourierRow selectedRow;
|
||||
|
||||
public CourierInvoiceView(RestClientService restClient) {
|
||||
this.restClient = restClient;
|
||||
|
||||
setSizeFull();
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
|
||||
configureFilters();
|
||||
configureGrid();
|
||||
configureExportLinks();
|
||||
configureDetails();
|
||||
|
||||
HorizontalLayout filterBar = buildFilterBar();
|
||||
HorizontalLayout cards = buildCards();
|
||||
VerticalLayout gridPanel = buildGridPanel();
|
||||
VerticalLayout detailPanel = buildDetailPanel();
|
||||
HorizontalLayout workspace = new HorizontalLayout(gridPanel, detailPanel);
|
||||
workspace.setSizeFull();
|
||||
workspace.expand(gridPanel);
|
||||
|
||||
add(new H3("Kurierauftraege"), filterBar, cards, workspace);
|
||||
expand(workspace);
|
||||
|
||||
loadRows();
|
||||
}
|
||||
|
||||
private void configureFilters() {
|
||||
fromDate.setValue(LocalDate.now().withDayOfMonth(1));
|
||||
toDate.setValue(LocalDate.now());
|
||||
modeField.setItems(HistoryMode.values());
|
||||
modeField.setItemLabelGenerator(HistoryMode::label);
|
||||
modeField.setValue(HistoryMode.FINISHED);
|
||||
fromDate.addValueChangeListener(event -> updateExportLinks());
|
||||
toDate.addValueChangeListener(event -> updateExportLinks());
|
||||
modeField.addValueChangeListener(event -> updateExportLinks());
|
||||
}
|
||||
|
||||
private void configureGrid() {
|
||||
grid.addColumn(CourierRow::jobId).setHeader("Auftrag").setAutoWidth(true);
|
||||
grid.addColumn(CourierRow::customerName).setHeader("Kunde").setFlexGrow(1);
|
||||
grid.addColumn(CourierRow::pickupName).setHeader("Abholung").setFlexGrow(1);
|
||||
grid.addColumn(CourierRow::deliveryName).setHeader("Zustellung").setFlexGrow(1);
|
||||
grid.addColumn(CourierRow::displayTime).setHeader("Zeit").setAutoWidth(true);
|
||||
grid.addColumn(CourierRow::statusLabel).setHeader("Status").setAutoWidth(true);
|
||||
grid.addColumn(row -> formatAmount(row.totalPrice())).setHeader("Umsatz").setAutoWidth(true);
|
||||
grid.addColumn(row -> formatAmount(row.courierPrice())).setHeader("Kurierpreis").setAutoWidth(true);
|
||||
grid.addColumn(row -> row.exported() ? "Ja" : "Nein").setHeader("Export").setAutoWidth(true);
|
||||
grid.setSizeFull();
|
||||
grid.asSingleSelect().addValueChangeListener(event -> {
|
||||
selectedRow = event.getValue();
|
||||
updateDetail();
|
||||
});
|
||||
}
|
||||
|
||||
private void configureExportLinks() {
|
||||
csvExportButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
pdfExportButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
csvExportLink.add(csvExportButton);
|
||||
pdfExportLink.add(pdfExportButton);
|
||||
csvExportLink.getElement().setAttribute("download", true);
|
||||
pdfExportLink.getElement().setAttribute("download", true);
|
||||
updateExportLinks();
|
||||
}
|
||||
|
||||
private void configureDetails() {
|
||||
invoiceText.setWidthFull();
|
||||
invoiceText.getStyle()
|
||||
.set("background", "var(--lumo-contrast-5pct)")
|
||||
.set("padding", "var(--lumo-space-m)")
|
||||
.set("border-radius", "var(--lumo-border-radius-m)")
|
||||
.set("white-space", "pre-wrap")
|
||||
.set("margin", "0");
|
||||
clearDetail();
|
||||
}
|
||||
|
||||
private HorizontalLayout buildFilterBar() {
|
||||
Button reloadButton = new Button("Neu laden", event -> loadRows());
|
||||
reloadButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
|
||||
HorizontalLayout filterBar = new HorizontalLayout(fromDate, toDate, modeField, reloadButton, csvExportLink, pdfExportLink);
|
||||
filterBar.setAlignItems(Alignment.BASELINE);
|
||||
return filterBar;
|
||||
}
|
||||
|
||||
private HorizontalLayout buildCards() {
|
||||
HorizontalLayout cards = new HorizontalLayout(
|
||||
createCard("Auftraege", totalJobs),
|
||||
createCard("Umsatz", totalRevenue),
|
||||
createCard("Kurierpreis", totalCourierPrice)
|
||||
);
|
||||
cards.setWidthFull();
|
||||
return cards;
|
||||
}
|
||||
|
||||
private VerticalLayout buildGridPanel() {
|
||||
VerticalLayout panel = new VerticalLayout(new H4("Historie"), grid);
|
||||
panel.setPadding(false);
|
||||
panel.setSpacing(true);
|
||||
panel.setSizeFull();
|
||||
panel.expand(grid);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private VerticalLayout buildDetailPanel() {
|
||||
VerticalLayout panel = new VerticalLayout();
|
||||
panel.setPadding(true);
|
||||
panel.setSpacing(true);
|
||||
panel.setWidth("420px");
|
||||
panel.getStyle()
|
||||
.set("border", "1px solid var(--lumo-contrast-20pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-l)");
|
||||
|
||||
panel.add(
|
||||
new H4("Details"),
|
||||
detailLine("Auftrag", detailJob),
|
||||
detailLine("Kunde", detailCustomer),
|
||||
detailLine("Route", detailRoute),
|
||||
detailLine("Zeit", detailTime),
|
||||
detailLine("Status", detailStatus),
|
||||
new H4("Rechnungstext"),
|
||||
invoiceText
|
||||
);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private HorizontalLayout detailLine(String label, Span value) {
|
||||
Span caption = new Span(label + ":");
|
||||
caption.getStyle().set("font-weight", "600");
|
||||
return new HorizontalLayout(caption, value);
|
||||
}
|
||||
|
||||
private VerticalLayout createCard(String title, Span value) {
|
||||
VerticalLayout card = new VerticalLayout();
|
||||
card.setPadding(true);
|
||||
card.setSpacing(false);
|
||||
card.getStyle()
|
||||
.set("border", "1px solid var(--lumo-contrast-20pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-l)")
|
||||
.set("min-width", "180px");
|
||||
H3 headline = new H3(title);
|
||||
headline.getStyle().set("margin", "0");
|
||||
value.getStyle().set("font-size", "var(--lumo-font-size-xl)").set("font-weight", "700");
|
||||
card.add(headline, value);
|
||||
return card;
|
||||
}
|
||||
|
||||
private void loadRows() {
|
||||
try {
|
||||
List<LinkedHashMap<String, Object>> data = restClient.getList(
|
||||
"/invoices/courier-items?from=" + fromDate.getValue()
|
||||
+ "&to=" + toDate.getValue()
|
||||
+ "&mode=" + modeField.getValue().apiValue(),
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
List<CourierRow> rows = new ArrayList<>();
|
||||
BigDecimal revenueSum = BigDecimal.ZERO;
|
||||
BigDecimal courierSum = BigDecimal.ZERO;
|
||||
for (Map<String, Object> row : data) {
|
||||
BigDecimal totalPrice = toBigDecimal(row.get("totalPrice"));
|
||||
BigDecimal courierPrice = toBigDecimal(row.get("courierPrice"));
|
||||
rows.add(new CourierRow(
|
||||
toLong(row.get("jobId")),
|
||||
str(row.get("customerName")),
|
||||
str(row.get("pickupName")),
|
||||
str(row.get("deliveryName")),
|
||||
str(row.get("orderTime")),
|
||||
str(row.get("finishTime")),
|
||||
toInteger(row.get("status")),
|
||||
toInteger(row.get("incomplete")),
|
||||
Boolean.TRUE.equals(row.get("exported")) || "true".equalsIgnoreCase(str(row.get("exported"))),
|
||||
totalPrice,
|
||||
courierPrice,
|
||||
str(row.get("invoiceText"))
|
||||
));
|
||||
if (totalPrice != null) {
|
||||
revenueSum = revenueSum.add(totalPrice);
|
||||
}
|
||||
if (courierPrice != null) {
|
||||
courierSum = courierSum.add(courierPrice);
|
||||
}
|
||||
}
|
||||
grid.setItems(rows);
|
||||
totalJobs.setText(String.valueOf(rows.size()));
|
||||
totalRevenue.setText(formatAmount(revenueSum));
|
||||
totalCourierPrice.setText(formatAmount(courierSum));
|
||||
grid.asSingleSelect().clear();
|
||||
if (!rows.isEmpty()) {
|
||||
grid.select(rows.get(0));
|
||||
} else {
|
||||
clearDetail();
|
||||
}
|
||||
updateExportLinks();
|
||||
} catch (Exception e) {
|
||||
Notification.show("Kurierhistorie konnte nicht geladen werden.", 4000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateExportLinks() {
|
||||
StreamResource csv = new StreamResource(
|
||||
"courier_jobs_" + modeField.getValue().apiValue() + "_" + fromDate.getValue() + "_" + toDate.getValue() + ".csv",
|
||||
() -> new ByteArrayInputStream(restClient.getBytes(
|
||||
"/invoices/courier-export/csv?from=" + fromDate.getValue()
|
||||
+ "&to=" + toDate.getValue()
|
||||
+ "&mode=" + modeField.getValue().apiValue()
|
||||
))
|
||||
);
|
||||
StreamResource pdf = new StreamResource(
|
||||
"courier_jobs_" + modeField.getValue().apiValue() + "_" + fromDate.getValue() + "_" + toDate.getValue() + ".pdf",
|
||||
() -> new ByteArrayInputStream(restClient.getBytes(
|
||||
"/invoices/courier-export/pdf?from=" + fromDate.getValue()
|
||||
+ "&to=" + toDate.getValue()
|
||||
+ "&mode=" + modeField.getValue().apiValue()
|
||||
))
|
||||
);
|
||||
csvExportLink.setHref(csv);
|
||||
pdfExportLink.setHref(pdf);
|
||||
}
|
||||
|
||||
private void updateDetail() {
|
||||
if (selectedRow == null) {
|
||||
clearDetail();
|
||||
return;
|
||||
}
|
||||
detailJob.setText(String.valueOf(selectedRow.jobId()));
|
||||
detailCustomer.setText(selectedRow.customerName());
|
||||
detailRoute.setText(selectedRow.pickupName() + " -> " + selectedRow.deliveryName());
|
||||
detailTime.setText(selectedRow.displayTime());
|
||||
detailStatus.setText(selectedRow.statusLabel());
|
||||
invoiceText.setText(selectedRow.invoiceText().isBlank() ? "(kein Rechnungstext)" : selectedRow.invoiceText());
|
||||
}
|
||||
|
||||
private void clearDetail() {
|
||||
detailJob.setText("-");
|
||||
detailCustomer.setText("-");
|
||||
detailRoute.setText("-");
|
||||
detailTime.setText("-");
|
||||
detailStatus.setText("-");
|
||||
invoiceText.setText("(kein Auftrag ausgewaehlt)");
|
||||
}
|
||||
|
||||
private String formatAmount(BigDecimal value) {
|
||||
return value != null ? value.toPlainString() : "-";
|
||||
}
|
||||
|
||||
private BigDecimal toBigDecimal(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof BigDecimal decimal) {
|
||||
return decimal;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(value.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Long toLong(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(value.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Integer toInteger(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(value.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String str(Object value) {
|
||||
return value != null ? value.toString() : "";
|
||||
}
|
||||
|
||||
private record CourierRow(Long jobId, String customerName, String pickupName, String deliveryName,
|
||||
String orderTime, String finishTime, Integer status, Integer incomplete,
|
||||
boolean exported, BigDecimal totalPrice, BigDecimal courierPrice,
|
||||
String invoiceText) {
|
||||
|
||||
private String displayTime() {
|
||||
String value = finishTime != null && !finishTime.isBlank() ? finishTime : orderTime;
|
||||
return value != null ? value.replace("T", " ") : "-";
|
||||
}
|
||||
|
||||
private String statusLabel() {
|
||||
String base = switch (status != null ? status : -1) {
|
||||
case 0 -> "Blockiert";
|
||||
case 1 -> "Zugewiesen";
|
||||
case 2 -> "In Zustellung";
|
||||
case 8 -> "Zugestellt";
|
||||
case 9 -> "Abgeschlossen";
|
||||
case 10 -> "Storniert";
|
||||
default -> status != null ? String.valueOf(status) : "-";
|
||||
};
|
||||
if (exported) {
|
||||
return base + ", exportiert";
|
||||
}
|
||||
if (incomplete != null && incomplete > 0) {
|
||||
return base + ", unvollstaendig";
|
||||
}
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
||||
private enum HistoryMode {
|
||||
FINISHED("Erledigt", "finished"),
|
||||
RUNNING("Laufend", "running");
|
||||
|
||||
private final String label;
|
||||
private final String apiValue;
|
||||
|
||||
HistoryMode(String label, String apiValue) {
|
||||
this.label = label;
|
||||
this.apiValue = apiValue;
|
||||
}
|
||||
|
||||
private String label() {
|
||||
return label;
|
||||
}
|
||||
|
||||
private String apiValue() {
|
||||
return apiValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
111
vaadin/src/main/java/de/votian/web/view/CourierListView.java
Normal file
111
vaadin/src/main/java/de/votian/web/view/CourierListView.java
Normal file
@@ -0,0 +1,111 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.data.value.ValueChangeMode;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.votian.web.model.SessionData;
|
||||
import de.votian.web.service.RestClientService;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Route(value = "couriers", layout = MainLayout.class)
|
||||
@PageTitle("Votian - Kuriere")
|
||||
public class CourierListView extends VerticalLayout {
|
||||
|
||||
private final RestClientService restClient;
|
||||
private final SessionData sessionData;
|
||||
private final Grid<Map<String, Object>> grid;
|
||||
|
||||
public CourierListView(RestClientService restClient, SessionData sessionData) {
|
||||
this.restClient = restClient;
|
||||
this.sessionData = sessionData;
|
||||
setSizeFull();
|
||||
setPadding(true);
|
||||
|
||||
TextField searchField = new TextField();
|
||||
searchField.setPlaceholder("Kurier suchen...");
|
||||
searchField.setPrefixComponent(VaadinIcon.SEARCH.create());
|
||||
searchField.setValueChangeMode(ValueChangeMode.LAZY);
|
||||
searchField.addValueChangeListener(e -> search(e.getValue()));
|
||||
|
||||
Button newButton = new Button("Neuer Kurier", VaadinIcon.PLUS.create(), e ->
|
||||
getUI().ifPresent(ui -> ui.navigate(CourierDetailView.class)));
|
||||
newButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
newButton.setVisible(sessionData.canManageCouriers());
|
||||
|
||||
HorizontalLayout toolbar = new HorizontalLayout(searchField, newButton);
|
||||
toolbar.setWidthFull();
|
||||
toolbar.expand(searchField);
|
||||
|
||||
grid = new Grid<>();
|
||||
grid.addColumn(m -> m.get("eid")).setHeader("Kurier-Nr.").setSortable(true).setAutoWidth(true);
|
||||
grid.addColumn(m -> m.get("sid")).setHeader("SID").setAutoWidth(true);
|
||||
grid.addColumn(m -> m.get("companyName")).setHeader("Firma").setFlexGrow(2);
|
||||
grid.addColumn(m -> m.get("contact")).setHeader("Ansprechpartner").setFlexGrow(1);
|
||||
grid.addColumn(m -> m.get("available")).setHeader("Verfuegbar").setAutoWidth(true);
|
||||
grid.setSizeFull();
|
||||
|
||||
grid.addItemClickListener(e -> {
|
||||
Object id = e.getItem().get("id");
|
||||
if (id != null) {
|
||||
getUI().ifPresent(ui -> ui.navigate("couriers/" + id));
|
||||
}
|
||||
});
|
||||
|
||||
add(toolbar, grid);
|
||||
loadCouriers();
|
||||
}
|
||||
|
||||
private void loadCouriers() {
|
||||
if (sessionData.getHeadquartersId() == null) return;
|
||||
try {
|
||||
Object[] couriers = restClient.get("/couriers/hq/" + sessionData.getHeadquartersId(), Object[].class);
|
||||
if (couriers != null) {
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (Object c : couriers) {
|
||||
if (c instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> map = (Map<String, Object>) c;
|
||||
items.add(map);
|
||||
}
|
||||
}
|
||||
grid.setItems(items);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Notification.show("Fehler: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void search(String term) {
|
||||
if (term == null || term.trim().isEmpty()) {
|
||||
loadCouriers();
|
||||
return;
|
||||
}
|
||||
if (sessionData.getHeadquartersId() == null) return;
|
||||
try {
|
||||
Object[] results = restClient.get(
|
||||
"/couriers/search?hqId=" + sessionData.getHeadquartersId() + "&term=" + term, Object[].class);
|
||||
if (results != null) {
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (Object c : results) {
|
||||
if (c instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> map = (Map<String, Object>) c;
|
||||
items.add(map);
|
||||
}
|
||||
}
|
||||
grid.setItems(items);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Notification.show("Suchfehler: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
1991
vaadin/src/main/java/de/votian/web/view/CustomerDetailView.java
Normal file
1991
vaadin/src/main/java/de/votian/web/view/CustomerDetailView.java
Normal file
File diff suppressed because it is too large
Load Diff
114
vaadin/src/main/java/de/votian/web/view/CustomerListView.java
Normal file
114
vaadin/src/main/java/de/votian/web/view/CustomerListView.java
Normal file
@@ -0,0 +1,114 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.data.value.ValueChangeMode;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.votian.web.model.SessionData;
|
||||
import de.votian.web.service.RestClientService;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Route(value = "customers", layout = MainLayout.class)
|
||||
@PageTitle("Votian - Kunden")
|
||||
public class CustomerListView extends VerticalLayout {
|
||||
|
||||
private final RestClientService restClient;
|
||||
private final SessionData sessionData;
|
||||
private final Grid<Map<String, Object>> grid;
|
||||
|
||||
public CustomerListView(RestClientService restClient, SessionData sessionData) {
|
||||
this.restClient = restClient;
|
||||
this.sessionData = sessionData;
|
||||
setSizeFull();
|
||||
setPadding(true);
|
||||
|
||||
// Toolbar
|
||||
TextField searchField = new TextField();
|
||||
searchField.setPlaceholder("Kunde suchen...");
|
||||
searchField.setPrefixComponent(VaadinIcon.SEARCH.create());
|
||||
searchField.setValueChangeMode(ValueChangeMode.LAZY);
|
||||
searchField.addValueChangeListener(e -> search(e.getValue()));
|
||||
|
||||
Button newButton = new Button("Neuer Kunde", VaadinIcon.PLUS.create(), e -> {
|
||||
getUI().ifPresent(ui -> ui.navigate(CustomerDetailView.class));
|
||||
});
|
||||
newButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
newButton.setVisible(sessionData.canManageCustomers());
|
||||
|
||||
HorizontalLayout toolbar = new HorizontalLayout(searchField, newButton);
|
||||
toolbar.setWidthFull();
|
||||
toolbar.expand(searchField);
|
||||
|
||||
// Grid
|
||||
grid = new Grid<>();
|
||||
grid.addColumn(m -> m.get("eid")).setHeader("Kunden-Nr.").setSortable(true).setAutoWidth(true);
|
||||
grid.addColumn(m -> m.get("companyName")).setHeader("Firma").setSortable(true).setFlexGrow(2);
|
||||
grid.addColumn(m -> m.get("contact")).setHeader("Ansprechpartner").setFlexGrow(1);
|
||||
grid.addColumn(m -> m.get("email")).setHeader("E-Mail").setFlexGrow(1);
|
||||
grid.addColumn(m -> m.get("phone")).setHeader("Telefon").setAutoWidth(true);
|
||||
grid.setSizeFull();
|
||||
|
||||
grid.addItemClickListener(e -> {
|
||||
Object id = e.getItem().get("id");
|
||||
if (id != null) {
|
||||
getUI().ifPresent(ui -> ui.navigate("customers/" + id));
|
||||
}
|
||||
});
|
||||
|
||||
add(toolbar, grid);
|
||||
loadCustomers();
|
||||
}
|
||||
|
||||
private void loadCustomers() {
|
||||
if (sessionData.getHeadquartersId() == null) return;
|
||||
try {
|
||||
Object[] customers = restClient.get("/customers/hq/" + sessionData.getHeadquartersId(), Object[].class);
|
||||
if (customers != null) {
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (Object c : customers) {
|
||||
if (c instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> map = (Map<String, Object>) c;
|
||||
items.add(map);
|
||||
}
|
||||
}
|
||||
grid.setItems(items);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Notification.show("Fehler beim Laden der Kunden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void search(String term) {
|
||||
if (term == null || term.trim().isEmpty()) {
|
||||
loadCustomers();
|
||||
return;
|
||||
}
|
||||
if (sessionData.getHeadquartersId() == null) return;
|
||||
try {
|
||||
Object[] results = restClient.get(
|
||||
"/customers/search?hqId=" + sessionData.getHeadquartersId() + "&term=" + term, Object[].class);
|
||||
if (results != null) {
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (Object c : results) {
|
||||
if (c instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> map = (Map<String, Object>) c;
|
||||
items.add(map);
|
||||
}
|
||||
}
|
||||
grid.setItems(items);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Notification.show("Suchfehler: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
102
vaadin/src/main/java/de/votian/web/view/DashboardView.java
Normal file
102
vaadin/src/main/java/de/votian/web/view/DashboardView.java
Normal file
@@ -0,0 +1,102 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.html.H2;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.html.Paragraph;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import com.vaadin.flow.router.RouteAlias;
|
||||
import de.votian.web.model.SessionData;
|
||||
import de.votian.web.service.ParameterProvider;
|
||||
import de.votian.web.service.RestClientService;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Route(value = "dashboard", layout = MainLayout.class)
|
||||
@RouteAlias(value = "", layout = MainLayout.class)
|
||||
@PageTitle("Votian - Dashboard")
|
||||
public class DashboardView extends VerticalLayout {
|
||||
|
||||
public DashboardView(RestClientService restClient, SessionData sessionData, ParameterProvider parameterProvider) {
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
|
||||
H2 welcome = new H2("Willkommen, " + sessionData.getDisplayName());
|
||||
Paragraph dateInfo = new Paragraph(LocalDate.now().format(
|
||||
DateTimeFormatter.ofPattern("EEEE, dd. MMMM yyyy", java.util.Locale.GERMAN)));
|
||||
|
||||
// Info cards
|
||||
HorizontalLayout cards = new HorizontalLayout();
|
||||
cards.setWidthFull();
|
||||
cards.setSpacing(true);
|
||||
|
||||
// Load HQ info
|
||||
String hqName = "Niederlassung";
|
||||
if (sessionData.getHeadquartersId() != null) {
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> hq = restClient.get("/headquarters/" + sessionData.getHeadquartersId(),
|
||||
LinkedHashMap.class);
|
||||
if (hq != null) {
|
||||
hqName = (String) hq.getOrDefault("name", "Niederlassung");
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
cards.add(createInfoCard("Niederlassung", hqName));
|
||||
|
||||
String userTypeLabel;
|
||||
switch (sessionData.getUserType() != null ? sessionData.getUserType() : 0) {
|
||||
case 1: userTypeLabel = "Mitarbeiter (HQ)"; break;
|
||||
case 2: userTypeLabel = "Kundenbenutzer"; break;
|
||||
case 3: userTypeLabel = "Kurier"; break;
|
||||
case 4: userTypeLabel = "Lager"; break;
|
||||
default: userTypeLabel = "Unbekannt";
|
||||
}
|
||||
cards.add(createInfoCard("Benutzertyp", userTypeLabel));
|
||||
|
||||
// Today's jobs count
|
||||
try {
|
||||
Object[] todayJobs = restClient.get("/jobs/today", Object[].class);
|
||||
cards.add(createInfoCard("Heutige Auftraege", String.valueOf(todayJobs != null ? todayJobs.length : 0)));
|
||||
} catch (Exception e) {
|
||||
cards.add(createInfoCard("Heutige Auftraege", "N/A"));
|
||||
}
|
||||
|
||||
// Parameter info
|
||||
if (parameterProvider.isLoaded()) {
|
||||
cards.add(createInfoCard("Parameter", String.valueOf(parameterProvider.size())));
|
||||
}
|
||||
|
||||
add(welcome, dateInfo, cards);
|
||||
}
|
||||
|
||||
private VerticalLayout createInfoCard(String title, String value) {
|
||||
VerticalLayout card = new VerticalLayout();
|
||||
card.setPadding(true);
|
||||
card.setSpacing(false);
|
||||
card.getStyle()
|
||||
.set("border", "1px solid var(--lumo-contrast-20pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-l)")
|
||||
.set("background", "var(--lumo-base-color)")
|
||||
.set("box-shadow", "var(--lumo-box-shadow-s)")
|
||||
.set("min-width", "200px");
|
||||
|
||||
H3 cardTitle = new H3(title);
|
||||
cardTitle.getStyle().set("margin", "0").set("font-size", "var(--lumo-font-size-s)")
|
||||
.set("color", "var(--lumo-secondary-text-color)");
|
||||
|
||||
Span cardValue = new Span(value);
|
||||
cardValue.getStyle().set("font-size", "var(--lumo-font-size-xl)")
|
||||
.set("font-weight", "bold").set("color", "#1b12b9");
|
||||
|
||||
card.add(cardTitle, cardValue);
|
||||
return card;
|
||||
}
|
||||
}
|
||||
800
vaadin/src/main/java/de/votian/web/view/DispositionView.java
Normal file
800
vaadin/src/main/java/de/votian/web/view/DispositionView.java
Normal file
@@ -0,0 +1,800 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.datepicker.DatePicker;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.html.Anchor;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.html.H4;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.tabs.Tab;
|
||||
import com.vaadin.flow.component.tabs.Tabs;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.votian.web.model.SessionData;
|
||||
import de.votian.web.service.RestClientService;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Route(value = "disposition", layout = MainLayout.class)
|
||||
@PageTitle("Votian - Disposition")
|
||||
public class DispositionView extends VerticalLayout {
|
||||
|
||||
private final RestClientService restClient;
|
||||
private final SessionData sessionData;
|
||||
|
||||
private final DatePicker dayField = new DatePicker("Tag");
|
||||
|
||||
private final Span totalJobs = new Span("-");
|
||||
private final Span assignedJobs = new Span("-");
|
||||
private final Span unassignedJobs = new Span("-");
|
||||
private final Span availableVehicles = new Span("-");
|
||||
|
||||
private final Grid<JobRow> jobGrid = new Grid<>(JobRow.class, false);
|
||||
private final Grid<VehicleRow> vehicleGrid = new Grid<>(VehicleRow.class, false);
|
||||
private final Grid<StationRow> stationGrid = new Grid<>(StationRow.class, false);
|
||||
|
||||
private final Button assignButton = new Button("Zuordnen");
|
||||
private final Button clearAssignmentButton = new Button("Zuordnung loesen");
|
||||
private final Button openJobButton = new Button("Auftrag oeffnen");
|
||||
|
||||
private final Span detailJob = new Span("-");
|
||||
private final Span detailCustomer = new Span("-");
|
||||
private final Span detailCostCenter = new Span("-");
|
||||
private final Span detailStatus = new Span("-");
|
||||
private final Span detailCourier = new Span("-");
|
||||
private final Span detailVehicle = new Span("-");
|
||||
private final Span detailPickup = new Span("-");
|
||||
private final Span detailDelivery = new Span("-");
|
||||
private final Anchor routeLink = createLink("Route in Google Maps");
|
||||
private final Anchor pickupMapLink = createLink("Abholung auf Karte");
|
||||
private final Anchor deliveryMapLink = createLink("Zustellung auf Karte");
|
||||
|
||||
private final Span stationJob = new Span("-");
|
||||
private final Span stationCustomer = new Span("-");
|
||||
private final Span stationName = new Span("-");
|
||||
private final Span stationStatus = new Span("-");
|
||||
private final Span stationAddress = new Span("-");
|
||||
private final Span stationCourier = new Span("-");
|
||||
private final Span stationRemark = new Span("-");
|
||||
private final Span stationPhone = new Span("-");
|
||||
private final Anchor stationMapLink = createLink("Station auf Karte");
|
||||
private final Anchor stationRouteLink = createLink("Auftragsroute");
|
||||
|
||||
private final List<JobRow> jobs = new ArrayList<>();
|
||||
private final List<VehicleRow> vehicles = new ArrayList<>();
|
||||
private final List<StationRow> stations = new ArrayList<>();
|
||||
private final Map<Long, JobRow> jobsById = new HashMap<>();
|
||||
|
||||
private JobRow selectedJob;
|
||||
private VehicleRow selectedVehicle;
|
||||
private StationRow selectedStation;
|
||||
|
||||
public DispositionView(RestClientService restClient, SessionData sessionData) {
|
||||
this.restClient = restClient;
|
||||
this.sessionData = sessionData;
|
||||
|
||||
setSizeFull();
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
|
||||
dayField.setValue(LocalDate.now());
|
||||
|
||||
configureJobGrid();
|
||||
configureVehicleGrid();
|
||||
configureStationGrid();
|
||||
configureActions();
|
||||
clearJobDetail();
|
||||
clearStationDetail();
|
||||
|
||||
Button reloadButton = new Button("Neu laden", event -> loadWorkspace(selectedJob != null ? selectedJob.jobId() : null));
|
||||
reloadButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
HorizontalLayout filterBar = new HorizontalLayout(dayField, reloadButton);
|
||||
filterBar.setAlignItems(Alignment.BASELINE);
|
||||
|
||||
HorizontalLayout cards = new HorizontalLayout(
|
||||
createCard("Auftraege", totalJobs),
|
||||
createCard("Zugeordnet", assignedJobs),
|
||||
createCard("Offen", unassignedJobs),
|
||||
createCard("Fahrzeuge", availableVehicles)
|
||||
);
|
||||
cards.setWidthFull();
|
||||
|
||||
VerticalLayout dispositionContent = buildDispositionContent();
|
||||
VerticalLayout locatingContent = buildLocatingContent();
|
||||
locatingContent.setVisible(false);
|
||||
|
||||
Tab dispositionTab = new Tab("Disposition");
|
||||
Tab locatingTab = new Tab("Locating");
|
||||
Tabs tabs = new Tabs(dispositionTab, locatingTab);
|
||||
Map<Tab, Component> tabContent = Map.of(
|
||||
dispositionTab, dispositionContent,
|
||||
locatingTab, locatingContent
|
||||
);
|
||||
tabs.addSelectedChangeListener(event -> {
|
||||
for (Component content : tabContent.values()) {
|
||||
content.setVisible(false);
|
||||
}
|
||||
tabContent.get(event.getSelectedTab()).setVisible(true);
|
||||
});
|
||||
|
||||
add(new H3("Disposition"), filterBar, cards, tabs, dispositionContent, locatingContent);
|
||||
expand(dispositionContent, locatingContent);
|
||||
|
||||
loadWorkspace(null);
|
||||
}
|
||||
|
||||
private void configureJobGrid() {
|
||||
jobGrid.addColumn(JobRow::jobId).setHeader("Auftrag").setAutoWidth(true).setFrozen(true);
|
||||
jobGrid.addColumn(row -> formatDateTime(row.orderTime())).setHeader("Zeit").setAutoWidth(true);
|
||||
jobGrid.addColumn(JobRow::customerName).setHeader("Kunde").setFlexGrow(1);
|
||||
jobGrid.addColumn(JobRow::pickupName).setHeader("Abholung").setFlexGrow(1);
|
||||
jobGrid.addColumn(JobRow::deliveryName).setHeader("Zustellung").setFlexGrow(1);
|
||||
jobGrid.addColumn(row -> statusLabel(row.status(), false)).setHeader("Status").setAutoWidth(true);
|
||||
jobGrid.addColumn(JobRow::courierSid).setHeader("Kurier").setAutoWidth(true);
|
||||
jobGrid.addColumn(JobRow::vehicleDisplay).setHeader("Fahrzeug").setAutoWidth(true);
|
||||
jobGrid.addColumn(row -> formatAmount(row.totalPrice())).setHeader("Umsatz").setAutoWidth(true);
|
||||
jobGrid.setSizeFull();
|
||||
jobGrid.asSingleSelect().addValueChangeListener(event -> {
|
||||
selectedJob = event.getValue();
|
||||
refreshActionState();
|
||||
updateJobDetail();
|
||||
});
|
||||
}
|
||||
|
||||
private void configureVehicleGrid() {
|
||||
vehicleGrid.addColumn(VehicleRow::courierSid).setHeader("Kurier").setAutoWidth(true);
|
||||
vehicleGrid.addColumn(VehicleRow::courierEid).setHeader("EID").setAutoWidth(true);
|
||||
vehicleGrid.addColumn(VehicleRow::vehicleDisplay).setHeader("Fahrzeug").setAutoWidth(true);
|
||||
vehicleGrid.addColumn(row -> vehicleTypeLabel(row.vehicleTypeId())).setHeader("Typ").setAutoWidth(true);
|
||||
vehicleGrid.addColumn(row -> availabilityLabel(row.available())).setHeader("Verfuegbar").setAutoWidth(true);
|
||||
vehicleGrid.addColumn(VehicleRow::assignedJobs).setHeader("Auftraege").setAutoWidth(true);
|
||||
vehicleGrid.setWidthFull();
|
||||
vehicleGrid.setMinHeight("260px");
|
||||
vehicleGrid.asSingleSelect().addValueChangeListener(event -> {
|
||||
selectedVehicle = event.getValue();
|
||||
refreshActionState();
|
||||
});
|
||||
}
|
||||
|
||||
private void configureStationGrid() {
|
||||
stationGrid.addColumn(StationRow::jobId).setHeader("Auftrag").setAutoWidth(true).setFrozen(true);
|
||||
stationGrid.addColumn(StationRow::stopSort).setHeader("Stop").setAutoWidth(true);
|
||||
stationGrid.addColumn(row -> formatDateTime(row.orderTime())).setHeader("Zeit").setAutoWidth(true);
|
||||
stationGrid.addColumn(StationRow::customerName).setHeader("Kunde").setFlexGrow(1);
|
||||
stationGrid.addColumn(StationRow::stopName).setHeader("Station").setFlexGrow(1);
|
||||
stationGrid.addColumn(StationRow::addressLabel).setHeader("Adresse").setFlexGrow(1);
|
||||
stationGrid.addColumn(row -> statusLabel(row.stopStatus(), true)).setHeader("Status").setAutoWidth(true);
|
||||
stationGrid.addColumn(StationRow::courierSid).setHeader("Kurier").setAutoWidth(true);
|
||||
stationGrid.addComponentColumn(row -> mapAnchor(row.mapUrl(), "Karte")).setHeader("Karte").setAutoWidth(true);
|
||||
stationGrid.setSizeFull();
|
||||
stationGrid.asSingleSelect().addValueChangeListener(event -> {
|
||||
selectedStation = event.getValue();
|
||||
updateStationDetail();
|
||||
});
|
||||
}
|
||||
|
||||
private void configureActions() {
|
||||
assignButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
assignButton.addClickListener(event -> assignSelectedVehicle());
|
||||
|
||||
clearAssignmentButton.addThemeVariants(ButtonVariant.LUMO_ERROR);
|
||||
clearAssignmentButton.addClickListener(event -> clearSelectedAssignment());
|
||||
|
||||
openJobButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
openJobButton.addClickListener(event -> {
|
||||
if (selectedJob != null && sessionData.canViewJobs()) {
|
||||
getUI().ifPresent(ui -> ui.navigate("jobs/" + selectedJob.jobId()));
|
||||
}
|
||||
});
|
||||
|
||||
refreshActionState();
|
||||
}
|
||||
|
||||
private VerticalLayout buildDispositionContent() {
|
||||
VerticalLayout jobPanel = new VerticalLayout(new H4("Tagesauftraege"), jobGrid);
|
||||
jobPanel.setPadding(false);
|
||||
jobPanel.setSpacing(true);
|
||||
jobPanel.setSizeFull();
|
||||
jobPanel.expand(jobGrid);
|
||||
|
||||
HorizontalLayout actions = new HorizontalLayout(assignButton, clearAssignmentButton, openJobButton);
|
||||
actions.setAlignItems(Alignment.BASELINE);
|
||||
|
||||
VerticalLayout vehiclePanel = new VerticalLayout(new H4("Fahrzeuge"), vehicleGrid);
|
||||
vehiclePanel.setPadding(false);
|
||||
vehiclePanel.setSpacing(true);
|
||||
vehiclePanel.setWidthFull();
|
||||
|
||||
VerticalLayout detailPanel = buildJobDetailPanel();
|
||||
|
||||
VerticalLayout sideColumn = new VerticalLayout(actions, vehiclePanel, detailPanel);
|
||||
sideColumn.setPadding(false);
|
||||
sideColumn.setSpacing(true);
|
||||
sideColumn.setWidth("520px");
|
||||
sideColumn.setHeightFull();
|
||||
sideColumn.expand(vehiclePanel);
|
||||
|
||||
HorizontalLayout workspace = new HorizontalLayout(jobPanel, sideColumn);
|
||||
workspace.setSizeFull();
|
||||
workspace.expand(jobPanel);
|
||||
|
||||
VerticalLayout content = new VerticalLayout(workspace);
|
||||
content.setPadding(false);
|
||||
content.setSpacing(false);
|
||||
content.setSizeFull();
|
||||
content.expand(workspace);
|
||||
return content;
|
||||
}
|
||||
|
||||
private VerticalLayout buildLocatingContent() {
|
||||
VerticalLayout stationPanel = new VerticalLayout(new H4("Stationen"), stationGrid);
|
||||
stationPanel.setPadding(false);
|
||||
stationPanel.setSpacing(true);
|
||||
stationPanel.setSizeFull();
|
||||
stationPanel.expand(stationGrid);
|
||||
|
||||
VerticalLayout detailPanel = buildStationDetailPanel();
|
||||
detailPanel.setWidth("520px");
|
||||
|
||||
HorizontalLayout workspace = new HorizontalLayout(stationPanel, detailPanel);
|
||||
workspace.setSizeFull();
|
||||
workspace.expand(stationPanel);
|
||||
|
||||
VerticalLayout content = new VerticalLayout(workspace);
|
||||
content.setPadding(false);
|
||||
content.setSpacing(false);
|
||||
content.setSizeFull();
|
||||
content.expand(workspace);
|
||||
return content;
|
||||
}
|
||||
|
||||
private VerticalLayout buildJobDetailPanel() {
|
||||
VerticalLayout panel = new VerticalLayout();
|
||||
panel.setPadding(true);
|
||||
panel.setSpacing(true);
|
||||
panel.getStyle()
|
||||
.set("border", "1px solid var(--lumo-contrast-20pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-l)");
|
||||
|
||||
panel.add(
|
||||
new H4("Auftragsdetail"),
|
||||
detailLine("Auftrag", detailJob),
|
||||
detailLine("Kunde", detailCustomer),
|
||||
detailLine("Kostenstelle", detailCostCenter),
|
||||
detailLine("Status", detailStatus),
|
||||
detailLine("Kurier", detailCourier),
|
||||
detailLine("Fahrzeug", detailVehicle),
|
||||
detailLine("Abholung", detailPickup),
|
||||
detailLine("Zustellung", detailDelivery),
|
||||
routeLink,
|
||||
pickupMapLink,
|
||||
deliveryMapLink
|
||||
);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private VerticalLayout buildStationDetailPanel() {
|
||||
VerticalLayout panel = new VerticalLayout();
|
||||
panel.setPadding(true);
|
||||
panel.setSpacing(true);
|
||||
panel.getStyle()
|
||||
.set("border", "1px solid var(--lumo-contrast-20pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-l)");
|
||||
|
||||
panel.add(
|
||||
new H4("Stationsdetail"),
|
||||
detailLine("Auftrag", stationJob),
|
||||
detailLine("Kunde", stationCustomer),
|
||||
detailLine("Station", stationName),
|
||||
detailLine("Status", stationStatus),
|
||||
detailLine("Adresse", stationAddress),
|
||||
detailLine("Kurier", stationCourier),
|
||||
detailLine("Telefon", stationPhone),
|
||||
detailLine("Bemerkung", stationRemark),
|
||||
stationMapLink,
|
||||
stationRouteLink
|
||||
);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private HorizontalLayout detailLine(String label, Span value) {
|
||||
Span caption = new Span(label + ":");
|
||||
caption.getStyle().set("font-weight", "600");
|
||||
HorizontalLayout line = new HorizontalLayout(caption, value);
|
||||
line.setPadding(false);
|
||||
line.setSpacing(true);
|
||||
return line;
|
||||
}
|
||||
|
||||
private VerticalLayout createCard(String title, Span value) {
|
||||
VerticalLayout card = new VerticalLayout();
|
||||
card.setPadding(true);
|
||||
card.setSpacing(false);
|
||||
card.getStyle()
|
||||
.set("border", "1px solid var(--lumo-contrast-20pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-l)")
|
||||
.set("min-width", "180px");
|
||||
|
||||
H4 headline = new H4(title);
|
||||
headline.getStyle().set("margin", "0");
|
||||
value.getStyle().set("font-size", "var(--lumo-font-size-xl)").set("font-weight", "700");
|
||||
card.add(headline, value);
|
||||
return card;
|
||||
}
|
||||
|
||||
private void loadWorkspace(Long preferredJobId) {
|
||||
if (sessionData.getHeadquartersId() == null) {
|
||||
return;
|
||||
}
|
||||
loadJobs(preferredJobId);
|
||||
loadVehicles();
|
||||
loadStations();
|
||||
}
|
||||
|
||||
private void loadJobs(Long preferredJobId) {
|
||||
try {
|
||||
jobs.clear();
|
||||
jobsById.clear();
|
||||
List<LinkedHashMap<String, Object>> data = restClient.getList(
|
||||
"/disposition/jobs?hqId=" + sessionData.getHeadquartersId() + "&day=" + dayField.getValue(),
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
long assignedCount = 0L;
|
||||
for (Map<String, Object> row : data) {
|
||||
JobRow item = new JobRow(
|
||||
toLong(row.get("jobId")),
|
||||
toInteger(row.get("status")),
|
||||
str(row.get("orderTime")),
|
||||
str(row.get("customerName")),
|
||||
str(row.get("costCenterName")),
|
||||
str(row.get("pickupName")),
|
||||
str(row.get("pickupStreet")),
|
||||
str(row.get("pickupZipcode")),
|
||||
str(row.get("pickupCity")),
|
||||
str(row.get("deliveryName")),
|
||||
str(row.get("deliveryStreet")),
|
||||
str(row.get("deliveryZipcode")),
|
||||
str(row.get("deliveryCity")),
|
||||
toLong(row.get("courierId")),
|
||||
str(row.get("courierSid")),
|
||||
toInteger(row.get("vehicleTypeId")),
|
||||
toLong(row.get("courierVehicleId")),
|
||||
str(row.get("courierVehicleSid")),
|
||||
toBigDecimal(row.get("totalPrice")),
|
||||
Boolean.TRUE.equals(row.get("exported")) || "true".equalsIgnoreCase(str(row.get("exported")))
|
||||
);
|
||||
jobs.add(item);
|
||||
jobsById.put(item.jobId(), item);
|
||||
if (item.courierId() != null || item.courierVehicleId() != null) {
|
||||
assignedCount++;
|
||||
}
|
||||
}
|
||||
jobGrid.setItems(jobs);
|
||||
totalJobs.setText(String.valueOf(jobs.size()));
|
||||
assignedJobs.setText(String.valueOf(assignedCount));
|
||||
unassignedJobs.setText(String.valueOf(Math.max(0, jobs.size() - assignedCount)));
|
||||
selectedJob = null;
|
||||
jobGrid.asSingleSelect().clear();
|
||||
if (preferredJobId != null) {
|
||||
jobs.stream()
|
||||
.filter(job -> preferredJobId.equals(job.jobId()))
|
||||
.findFirst()
|
||||
.ifPresent(jobGrid::select);
|
||||
}
|
||||
if (jobGrid.asSingleSelect().getValue() == null && !jobs.isEmpty()) {
|
||||
jobGrid.select(jobs.get(0));
|
||||
}
|
||||
if (jobs.isEmpty()) {
|
||||
clearJobDetail();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Notification.show("Disposition konnte nicht geladen werden.", 4000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadVehicles() {
|
||||
try {
|
||||
vehicles.clear();
|
||||
List<LinkedHashMap<String, Object>> data = restClient.getList(
|
||||
"/disposition/vehicles?hqId=" + sessionData.getHeadquartersId() + "&day=" + dayField.getValue(),
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
long availableCount = 0L;
|
||||
for (Map<String, Object> row : data) {
|
||||
VehicleRow item = new VehicleRow(
|
||||
toLong(row.get("courierId")),
|
||||
str(row.get("courierSid")),
|
||||
str(row.get("courierEid")),
|
||||
str(row.get("available")),
|
||||
toLong(row.get("courierVehicleId")),
|
||||
str(row.get("courierVehicleSid")),
|
||||
toInteger(row.get("vehicleTypeId")),
|
||||
toLong(row.get("assignedJobs")) != null ? toLong(row.get("assignedJobs")) : 0L
|
||||
);
|
||||
vehicles.add(item);
|
||||
if ("1".equals(item.available())) {
|
||||
availableCount++;
|
||||
}
|
||||
}
|
||||
vehicleGrid.setItems(vehicles);
|
||||
availableVehicles.setText(String.valueOf(availableCount));
|
||||
selectedVehicle = null;
|
||||
vehicleGrid.asSingleSelect().clear();
|
||||
refreshActionState();
|
||||
} catch (Exception e) {
|
||||
Notification.show("Fahrzeuge konnten nicht geladen werden.", 4000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadStations() {
|
||||
try {
|
||||
stations.clear();
|
||||
List<LinkedHashMap<String, Object>> data = restClient.getList(
|
||||
"/disposition/stations?hqId=" + sessionData.getHeadquartersId() + "&day=" + dayField.getValue(),
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
for (Map<String, Object> row : data) {
|
||||
stations.add(new StationRow(
|
||||
toLong(row.get("jobId")),
|
||||
str(row.get("orderTime")),
|
||||
toInteger(row.get("stopSort")),
|
||||
toInteger(row.get("stopStatus")),
|
||||
str(row.get("customerName")),
|
||||
str(row.get("commissionNo")),
|
||||
str(row.get("stopName")),
|
||||
str(row.get("street")),
|
||||
str(row.get("zipcode")),
|
||||
str(row.get("city")),
|
||||
str(row.get("country")),
|
||||
str(row.get("remark")),
|
||||
str(row.get("phone")),
|
||||
str(row.get("courierSid"))
|
||||
));
|
||||
}
|
||||
stationGrid.setItems(stations);
|
||||
selectedStation = null;
|
||||
stationGrid.asSingleSelect().clear();
|
||||
if (!stations.isEmpty()) {
|
||||
stationGrid.select(stations.get(0));
|
||||
} else {
|
||||
clearStationDetail();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Notification.show("Stationsdaten konnten nicht geladen werden.", 4000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void assignSelectedVehicle() {
|
||||
if (!sessionData.canManageDisposition()) {
|
||||
return;
|
||||
}
|
||||
if (selectedJob == null || selectedVehicle == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, Object> request = new LinkedHashMap<>();
|
||||
request.put("courierId", selectedVehicle.courierId());
|
||||
request.put("courierVehicleId", selectedVehicle.courierVehicleId());
|
||||
restClient.put(
|
||||
"/disposition/jobs/" + selectedJob.jobId() + "/assignment?hqId=" + sessionData.getHeadquartersId(),
|
||||
request
|
||||
);
|
||||
loadWorkspace(selectedJob.jobId());
|
||||
} catch (Exception e) {
|
||||
Notification.show("Zuordnung konnte nicht gespeichert werden: " + e.getMessage(),
|
||||
5000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearSelectedAssignment() {
|
||||
if (!sessionData.canManageDisposition()) {
|
||||
return;
|
||||
}
|
||||
if (selectedJob == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
restClient.delete(
|
||||
"/disposition/jobs/" + selectedJob.jobId() + "/assignment?hqId=" + sessionData.getHeadquartersId()
|
||||
);
|
||||
loadWorkspace(selectedJob.jobId());
|
||||
} catch (Exception e) {
|
||||
Notification.show("Zuordnung konnte nicht geloest werden: " + e.getMessage(),
|
||||
5000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshActionState() {
|
||||
assignButton.setEnabled(sessionData.canManageDisposition() && selectedJob != null && selectedVehicle != null);
|
||||
clearAssignmentButton.setEnabled(sessionData.canManageDisposition() && selectedJob != null
|
||||
&& (selectedJob.courierId() != null || selectedJob.courierVehicleId() != null));
|
||||
openJobButton.setEnabled(sessionData.canViewJobs() && selectedJob != null);
|
||||
}
|
||||
|
||||
private void updateJobDetail() {
|
||||
if (selectedJob == null) {
|
||||
clearJobDetail();
|
||||
return;
|
||||
}
|
||||
|
||||
detailJob.setText(String.valueOf(selectedJob.jobId()));
|
||||
detailCustomer.setText(selectedJob.customerName());
|
||||
detailCostCenter.setText(selectedJob.costCenterName());
|
||||
detailStatus.setText(statusLabel(selectedJob.status(), false));
|
||||
detailCourier.setText(selectedJob.courierSid().isBlank() ? "-" : selectedJob.courierSid());
|
||||
detailVehicle.setText(selectedJob.vehicleDisplay());
|
||||
detailPickup.setText(selectedJob.pickupAddressLabel());
|
||||
detailDelivery.setText(selectedJob.deliveryAddressLabel());
|
||||
updateLink(routeLink, directionsUrl(selectedJob.pickupAddressLabel(), selectedJob.deliveryAddressLabel()), true);
|
||||
updateLink(pickupMapLink, mapUrl(selectedJob.pickupAddressLabel()), true);
|
||||
updateLink(deliveryMapLink, mapUrl(selectedJob.deliveryAddressLabel()), true);
|
||||
}
|
||||
|
||||
private void clearJobDetail() {
|
||||
detailJob.setText("-");
|
||||
detailCustomer.setText("-");
|
||||
detailCostCenter.setText("-");
|
||||
detailStatus.setText("-");
|
||||
detailCourier.setText("-");
|
||||
detailVehicle.setText("-");
|
||||
detailPickup.setText("-");
|
||||
detailDelivery.setText("-");
|
||||
updateLink(routeLink, "", false);
|
||||
updateLink(pickupMapLink, "", false);
|
||||
updateLink(deliveryMapLink, "", false);
|
||||
}
|
||||
|
||||
private void updateStationDetail() {
|
||||
if (selectedStation == null) {
|
||||
clearStationDetail();
|
||||
return;
|
||||
}
|
||||
|
||||
stationJob.setText(String.valueOf(selectedStation.jobId()));
|
||||
stationCustomer.setText(selectedStation.customerName());
|
||||
stationName.setText(selectedStation.stopName());
|
||||
stationStatus.setText(statusLabel(selectedStation.stopStatus(), true));
|
||||
stationAddress.setText(selectedStation.addressLabel());
|
||||
stationCourier.setText(selectedStation.courierSid().isBlank() ? "-" : selectedStation.courierSid());
|
||||
stationRemark.setText(selectedStation.remark().isBlank() ? "-" : selectedStation.remark());
|
||||
stationPhone.setText(selectedStation.phone().isBlank() ? "-" : selectedStation.phone());
|
||||
updateLink(stationMapLink, selectedStation.mapUrl(), true);
|
||||
JobRow job = jobsById.get(selectedStation.jobId());
|
||||
updateLink(stationRouteLink,
|
||||
job != null ? directionsUrl(job.pickupAddressLabel(), job.deliveryAddressLabel()) : "",
|
||||
job != null);
|
||||
}
|
||||
|
||||
private void clearStationDetail() {
|
||||
stationJob.setText("-");
|
||||
stationCustomer.setText("-");
|
||||
stationName.setText("-");
|
||||
stationStatus.setText("-");
|
||||
stationAddress.setText("-");
|
||||
stationCourier.setText("-");
|
||||
stationRemark.setText("-");
|
||||
stationPhone.setText("-");
|
||||
updateLink(stationMapLink, "", false);
|
||||
updateLink(stationRouteLink, "", false);
|
||||
}
|
||||
|
||||
private Anchor mapAnchor(String href, String label) {
|
||||
Anchor anchor = createLink(label);
|
||||
updateLink(anchor, href, href != null && !href.isBlank());
|
||||
return anchor;
|
||||
}
|
||||
|
||||
private Anchor createLink(String text) {
|
||||
Anchor anchor = new Anchor();
|
||||
anchor.setText(text);
|
||||
anchor.setTarget("_blank");
|
||||
anchor.setVisible(false);
|
||||
return anchor;
|
||||
}
|
||||
|
||||
private void updateLink(Anchor anchor, String href, boolean visible) {
|
||||
if (visible && href != null && !href.isBlank()) {
|
||||
anchor.setHref(href);
|
||||
anchor.setVisible(true);
|
||||
} else {
|
||||
anchor.setHref("");
|
||||
anchor.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
private String mapUrl(String address) {
|
||||
if (address == null || address.isBlank() || "-".equals(address)) {
|
||||
return "";
|
||||
}
|
||||
return "https://www.google.com/maps/search/?api=1&query="
|
||||
+ URLEncoder.encode(address, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private String directionsUrl(String origin, String destination) {
|
||||
if (origin == null || origin.isBlank() || destination == null || destination.isBlank()
|
||||
|| "-".equals(origin) || "-".equals(destination)) {
|
||||
return "";
|
||||
}
|
||||
return "https://www.google.com/maps/dir/?api=1&origin="
|
||||
+ URLEncoder.encode(origin, StandardCharsets.UTF_8)
|
||||
+ "&destination="
|
||||
+ URLEncoder.encode(destination, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private String formatDateTime(String value) {
|
||||
return value == null || value.isBlank() ? "-" : value.replace("T", " ");
|
||||
}
|
||||
|
||||
private String formatAmount(BigDecimal value) {
|
||||
return value != null ? value.toPlainString() : "-";
|
||||
}
|
||||
|
||||
private String statusLabel(Integer status, boolean station) {
|
||||
if (station) {
|
||||
return Integer.valueOf(1).equals(status) ? "Erledigt" : "Offen";
|
||||
}
|
||||
return switch (status != null ? status : -1) {
|
||||
case 0 -> "Blockiert";
|
||||
case 1 -> "Zugewiesen";
|
||||
case 2 -> "In Zustellung";
|
||||
case 8 -> "Zugestellt";
|
||||
case 9 -> "Abgeschlossen";
|
||||
case 10 -> "Storniert";
|
||||
default -> status != null ? String.valueOf(status) : "-";
|
||||
};
|
||||
}
|
||||
|
||||
private String vehicleTypeLabel(Integer vehicleTypeId) {
|
||||
return vehicleTypeId != null ? "Typ " + vehicleTypeId : "-";
|
||||
}
|
||||
|
||||
private String availabilityLabel(String value) {
|
||||
return "1".equals(value) ? "Ja" : "Nein";
|
||||
}
|
||||
|
||||
private Long toLong(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(value.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Integer toInteger(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(value.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal toBigDecimal(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof BigDecimal decimal) {
|
||||
return decimal;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(value.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String str(Object value) {
|
||||
return value != null ? value.toString() : "";
|
||||
}
|
||||
|
||||
private record JobRow(Long jobId, Integer status, String orderTime, String customerName, String costCenterName,
|
||||
String pickupName, String pickupStreet, String pickupZipcode, String pickupCity,
|
||||
String deliveryName, String deliveryStreet, String deliveryZipcode, String deliveryCity,
|
||||
Long courierId, String courierSid, Integer vehicleTypeId, Long courierVehicleId,
|
||||
String courierVehicleSid, BigDecimal totalPrice, boolean exported) {
|
||||
|
||||
private String vehicleDisplay() {
|
||||
if (courierVehicleSid != null && !courierVehicleSid.isBlank()) {
|
||||
return courierVehicleSid;
|
||||
}
|
||||
if (vehicleTypeId != null) {
|
||||
return "Typ " + vehicleTypeId;
|
||||
}
|
||||
return "-";
|
||||
}
|
||||
|
||||
private String pickupAddressLabel() {
|
||||
return joinAddress(pickupStreet, pickupZipcode, pickupCity);
|
||||
}
|
||||
|
||||
private String deliveryAddressLabel() {
|
||||
return joinAddress(deliveryStreet, deliveryZipcode, deliveryCity);
|
||||
}
|
||||
|
||||
private String joinAddress(String street, String zipcode, String city) {
|
||||
List<String> parts = new ArrayList<>();
|
||||
if (street != null && !street.isBlank()) {
|
||||
parts.add(street);
|
||||
}
|
||||
String zipcodeCity = ((zipcode != null ? zipcode : "") + " " + (city != null ? city : "")).trim();
|
||||
if (!zipcodeCity.isBlank()) {
|
||||
parts.add(zipcodeCity);
|
||||
}
|
||||
return parts.isEmpty() ? "-" : String.join(", ", parts);
|
||||
}
|
||||
}
|
||||
|
||||
private record VehicleRow(Long courierId, String courierSid, String courierEid, String available,
|
||||
Long courierVehicleId, String courierVehicleSid, Integer vehicleTypeId,
|
||||
Long assignedJobs) {
|
||||
|
||||
private String vehicleDisplay() {
|
||||
if (courierVehicleSid != null && !courierVehicleSid.isBlank()) {
|
||||
return courierVehicleSid;
|
||||
}
|
||||
return "Hauptfahrzeug";
|
||||
}
|
||||
}
|
||||
|
||||
private record StationRow(Long jobId, String orderTime, Integer stopSort, Integer stopStatus, String customerName,
|
||||
String commissionNo, String stopName, String street, String zipcode, String city,
|
||||
String country, String remark, String phone, String courierSid) {
|
||||
|
||||
private String addressLabel() {
|
||||
List<String> parts = new ArrayList<>();
|
||||
if (street != null && !street.isBlank()) {
|
||||
parts.add(street);
|
||||
}
|
||||
String zipcodeCity = ((zipcode != null ? zipcode : "") + " " + (city != null ? city : "")).trim();
|
||||
if (!zipcodeCity.isBlank()) {
|
||||
parts.add(zipcodeCity);
|
||||
}
|
||||
if (country != null && !country.isBlank()) {
|
||||
parts.add(country);
|
||||
}
|
||||
return parts.isEmpty() ? "-" : String.join(", ", parts);
|
||||
}
|
||||
|
||||
private String mapUrl() {
|
||||
if (addressLabel().equals("-")) {
|
||||
return "";
|
||||
}
|
||||
return "https://www.google.com/maps/search/?api=1&query="
|
||||
+ URLEncoder.encode(addressLabel(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
}
|
||||
1298
vaadin/src/main/java/de/votian/web/view/EmployeeDetailView.java
Normal file
1298
vaadin/src/main/java/de/votian/web/view/EmployeeDetailView.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,86 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.votian.web.model.SessionData;
|
||||
import de.votian.web.service.RestClientService;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Route(value = "employees", layout = MainLayout.class)
|
||||
@PageTitle("Votian - Mitarbeiter")
|
||||
public class EmployeeListView extends VerticalLayout {
|
||||
|
||||
private final RestClientService restClient;
|
||||
private final Grid<Map<String, Object>> grid;
|
||||
|
||||
public EmployeeListView(RestClientService restClient, SessionData sessionData) {
|
||||
this.restClient = restClient;
|
||||
setSizeFull();
|
||||
setPadding(true);
|
||||
|
||||
Button newButton = new Button("Neuer Mitarbeiter", VaadinIcon.PLUS.create(), e ->
|
||||
getUI().ifPresent(ui -> ui.navigate(EmployeeDetailView.class)));
|
||||
newButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
newButton.setVisible(sessionData.canManageEmployees());
|
||||
|
||||
HorizontalLayout toolbar = new HorizontalLayout(newButton);
|
||||
toolbar.setWidthFull();
|
||||
|
||||
grid = new Grid<>();
|
||||
grid.addColumn(m -> m.get("id")).setHeader("ID").setAutoWidth(true);
|
||||
grid.addColumn(m -> m.get("userName")).setHeader("Name").setFlexGrow(1);
|
||||
grid.addColumn(m -> m.get("userFirstname")).setHeader("Vorname").setFlexGrow(1);
|
||||
grid.addColumn(m -> m.get("userEmail")).setHeader("E-Mail").setFlexGrow(1);
|
||||
grid.addColumn(m -> userTypeLabel(m.get("userType"))).setHeader("Art").setAutoWidth(true);
|
||||
grid.addColumn(m -> m.get("headquarters")).setHeader("Niederlassung").setAutoWidth(true);
|
||||
grid.setSizeFull();
|
||||
|
||||
grid.addItemClickListener(e -> {
|
||||
Object id = e.getItem().get("id");
|
||||
if (id != null) {
|
||||
getUI().ifPresent(ui -> ui.navigate("employees/" + id));
|
||||
}
|
||||
});
|
||||
|
||||
add(toolbar, grid);
|
||||
loadEmployees();
|
||||
}
|
||||
|
||||
private void loadEmployees() {
|
||||
try {
|
||||
Object[] employees = restClient.get("/employees", Object[].class);
|
||||
if (employees != null) {
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (Object e : employees) {
|
||||
if (e instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> map = (Map<String, Object>) e;
|
||||
items.add(map);
|
||||
}
|
||||
}
|
||||
grid.setItems(items);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Notification.show("Fehler: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String userTypeLabel(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return switch (number.intValue()) {
|
||||
case 2 -> "Kunde";
|
||||
case 4 -> "Lager";
|
||||
default -> "HQ";
|
||||
};
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,517 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.checkbox.Checkbox;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.datepicker.DatePicker;
|
||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.html.Anchor;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.html.H4;
|
||||
import com.vaadin.flow.component.html.Paragraph;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.textfield.TextArea;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import com.vaadin.flow.server.StreamResource;
|
||||
import de.votian.web.model.SessionData;
|
||||
import de.votian.web.service.RestClientService;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Route(value = "exports", layout = MainLayout.class)
|
||||
@PageTitle("Votian - Datenexport")
|
||||
public class GenericExportWorkspaceView extends VerticalLayout {
|
||||
|
||||
private final RestClientService restClient;
|
||||
private final SessionData sessionData;
|
||||
|
||||
private final ComboBox<CategoryOption> categoryField = new ComboBox<>("Kategorie");
|
||||
private final TextField customerFilterField = new TextField("Kundenfilter");
|
||||
private final DatePicker fromDate = new DatePicker("Von");
|
||||
private final DatePicker toDate = new DatePicker("Bis");
|
||||
private final ComboBox<StatusOption> statusFilterField = new ComboBox<>("Status");
|
||||
private final TextField delimiterField = new TextField("Delimiter");
|
||||
private final TextField fileNameField = new TextField("Dateiname");
|
||||
private final Checkbox includeHeadlineField = new Checkbox("Kopfzeile einbauen");
|
||||
private final Button runExportButton = new Button("Export erzeugen");
|
||||
|
||||
private final Grid<ProfileRow> profileGrid = new Grid<>(ProfileRow.class, false);
|
||||
private final TextField profileNameField = new TextField("Profilname");
|
||||
private final TextField fileExtensionField = new TextField("Dateiendung");
|
||||
private final TextArea parameterStringField = new TextArea("Parameterstring");
|
||||
private final Checkbox profileHeadlineField = new Checkbox("Profil-Kopfzeile");
|
||||
private final TextField beginOfLineField = new TextField("BOL");
|
||||
private final TextField endOfLineField = new TextField("EOL");
|
||||
private final TextField beginOfFileField = new TextField("BOF");
|
||||
private final TextField endOfFileField = new TextField("EOF");
|
||||
private final Button profileNewButton = new Button("Neu");
|
||||
private final Button profileSaveButton = new Button("Speichern");
|
||||
private final Button profileDeleteButton = new Button("Loeschen");
|
||||
|
||||
private final Grid<ExportFileRow> fileGrid = new Grid<>(ExportFileRow.class, false);
|
||||
|
||||
private final List<CategoryOption> categories = new ArrayList<>();
|
||||
private final List<ProfileRow> profiles = new ArrayList<>();
|
||||
|
||||
private Long selectedProfileId;
|
||||
private boolean customerScoped;
|
||||
private Long scopedCustomerId;
|
||||
|
||||
public GenericExportWorkspaceView(RestClientService restClient, SessionData sessionData) {
|
||||
this.restClient = restClient;
|
||||
this.sessionData = sessionData;
|
||||
|
||||
setSizeFull();
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
|
||||
configureFilters();
|
||||
configureProfileGrid();
|
||||
configureFileGrid();
|
||||
configureProfileEditor();
|
||||
|
||||
add(
|
||||
new H3("Generischer Datenexport"),
|
||||
buildFilterBar(),
|
||||
buildProfileSection(),
|
||||
buildFilesSection()
|
||||
);
|
||||
expand(profileGrid, fileGrid);
|
||||
|
||||
loadBootstrap();
|
||||
}
|
||||
|
||||
private void configureFilters() {
|
||||
categoryField.setItemLabelGenerator(CategoryOption::label);
|
||||
categoryField.setWidth("320px");
|
||||
categoryField.addValueChangeListener(event -> {
|
||||
clearProfileForm();
|
||||
loadProfiles();
|
||||
});
|
||||
|
||||
customerFilterField.setWidth("220px");
|
||||
customerFilterField.setClearButtonVisible(true);
|
||||
fromDate.setValue(LocalDate.now().withDayOfMonth(1));
|
||||
toDate.setValue(LocalDate.now());
|
||||
|
||||
statusFilterField.setItems(
|
||||
new StatusOption(null, "Alle"),
|
||||
new StatusOption(1, "Nur nicht exportierte"),
|
||||
new StatusOption(2, "Nur exportierte")
|
||||
);
|
||||
statusFilterField.setItemLabelGenerator(StatusOption::label);
|
||||
statusFilterField.setValue(statusFilterField.getListDataView().getItems().findFirst().orElse(null));
|
||||
statusFilterField.setWidth("220px");
|
||||
|
||||
delimiterField.setValue(";");
|
||||
delimiterField.setWidth("100px");
|
||||
fileNameField.setWidth("260px");
|
||||
|
||||
runExportButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
runExportButton.addClickListener(event -> runExport());
|
||||
}
|
||||
|
||||
private void configureProfileGrid() {
|
||||
profileGrid.addColumn(ProfileRow::name).setHeader("Profil").setFlexGrow(1).setFrozen(true);
|
||||
profileGrid.addColumn(ProfileRow::fileExtension).setHeader("Ext.").setAutoWidth(true);
|
||||
profileGrid.addColumn(row -> row.headline() ? "Ja" : "").setHeader("Headline").setAutoWidth(true);
|
||||
profileGrid.setHeight("220px");
|
||||
profileGrid.asSingleSelect().addValueChangeListener(event -> fillProfileForm(event.getValue()));
|
||||
}
|
||||
|
||||
private void configureFileGrid() {
|
||||
fileGrid.addColumn(ExportFileRow::storedAt).setHeader("Zeitpunkt").setAutoWidth(true).setFrozen(true);
|
||||
fileGrid.addColumn(ExportFileRow::fileName).setHeader("Datei").setFlexGrow(1);
|
||||
fileGrid.addColumn(ExportFileRow::customerCode).setHeader("Kunde").setAutoWidth(true);
|
||||
fileGrid.addColumn(row -> row.ftpUploaded() ? "Ja" : "").setHeader("FTP").setAutoWidth(true);
|
||||
fileGrid.addComponentColumn(this::downloadAnchor).setHeader("Download").setAutoWidth(true);
|
||||
fileGrid.addComponentColumn(row -> {
|
||||
Button delete = new Button("Loeschen");
|
||||
delete.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
|
||||
delete.addClickListener(event -> deleteFile(row));
|
||||
return delete;
|
||||
}).setHeader("Aktion").setAutoWidth(true);
|
||||
fileGrid.setSizeFull();
|
||||
}
|
||||
|
||||
private void configureProfileEditor() {
|
||||
parameterStringField.setWidthFull();
|
||||
parameterStringField.setMinHeight("180px");
|
||||
|
||||
profileSaveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
profileDeleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR);
|
||||
|
||||
profileNewButton.addClickListener(event -> clearProfileForm());
|
||||
profileSaveButton.addClickListener(event -> saveProfile());
|
||||
profileDeleteButton.addClickListener(event -> deleteProfile());
|
||||
}
|
||||
|
||||
private HorizontalLayout buildFilterBar() {
|
||||
HorizontalLayout layout = new HorizontalLayout(
|
||||
categoryField,
|
||||
customerFilterField,
|
||||
fromDate,
|
||||
toDate,
|
||||
statusFilterField,
|
||||
delimiterField,
|
||||
fileNameField,
|
||||
includeHeadlineField,
|
||||
runExportButton
|
||||
);
|
||||
layout.setAlignItems(Alignment.BASELINE);
|
||||
layout.getStyle().set("flex-wrap", "wrap");
|
||||
return layout;
|
||||
}
|
||||
|
||||
private VerticalLayout buildProfileSection() {
|
||||
FormLayout form = new FormLayout();
|
||||
form.setResponsiveSteps(
|
||||
new FormLayout.ResponsiveStep("0", 1),
|
||||
new FormLayout.ResponsiveStep("900px", 4)
|
||||
);
|
||||
form.add(profileNameField, fileExtensionField, profileHeadlineField, beginOfLineField, endOfLineField, beginOfFileField, endOfFileField, parameterStringField);
|
||||
form.setColspan(parameterStringField, 4);
|
||||
|
||||
HorizontalLayout actions = new HorizontalLayout(profileNewButton, profileSaveButton, profileDeleteButton);
|
||||
actions.setAlignItems(Alignment.BASELINE);
|
||||
|
||||
VerticalLayout section = new VerticalLayout(
|
||||
new Paragraph("Profile arbeiten direkt auf `exportparameters`. Der Parameterstring bleibt bewusst editierbar, waehrend der NG-Workspace die Exportdateien nativ erzeugt und in `exportfiles` archiviert."),
|
||||
new H4("Exportprofile"),
|
||||
profileGrid,
|
||||
form,
|
||||
actions
|
||||
);
|
||||
section.setPadding(false);
|
||||
section.setSpacing(true);
|
||||
section.setSizeFull();
|
||||
return section;
|
||||
}
|
||||
|
||||
private VerticalLayout buildFilesSection() {
|
||||
VerticalLayout section = new VerticalLayout(
|
||||
new H4("Exportdateien"),
|
||||
fileGrid
|
||||
);
|
||||
section.setPadding(false);
|
||||
section.setSpacing(true);
|
||||
section.setSizeFull();
|
||||
return section;
|
||||
}
|
||||
|
||||
private void loadBootstrap() {
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
if (headquartersId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> bootstrap = restClient.get(
|
||||
"/export-workspace/bootstrap?hqId=" + headquartersId,
|
||||
LinkedHashMap.class
|
||||
);
|
||||
customerScoped = Boolean.TRUE.equals(bootstrap.get("customerScoped"));
|
||||
scopedCustomerId = toLong(bootstrap.get("customerId"));
|
||||
customerFilterField.setEnabled(!customerScoped);
|
||||
categories.clear();
|
||||
Object rawCategories = bootstrap.get("categories");
|
||||
if (rawCategories instanceof List<?> list) {
|
||||
for (Object item : list) {
|
||||
if (item instanceof Map<?, ?> row) {
|
||||
Long id = toLong(row.get("id"));
|
||||
if (id != null) {
|
||||
categories.add(new CategoryOption(id, str(row.get("name"))));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
categoryField.setItems(categories);
|
||||
if (!categories.isEmpty()) {
|
||||
categoryField.setValue(categories.get(0));
|
||||
}
|
||||
loadProfiles();
|
||||
loadFiles();
|
||||
} catch (Exception e) {
|
||||
showError("Exportworkspace konnte nicht initialisiert werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void loadProfiles() {
|
||||
CategoryOption category = categoryField.getValue();
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
if (category == null || headquartersId == null) {
|
||||
profileGrid.setItems(List.of());
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String path = "/export-workspace/profiles?hqId=" + headquartersId
|
||||
+ "&categoryId=" + category.id()
|
||||
+ customerScopeQuery();
|
||||
List<LinkedHashMap<String, Object>> result = restClient.getList(path, new ParameterizedTypeReference<>() {});
|
||||
profiles.clear();
|
||||
for (Map<String, Object> row : result) {
|
||||
Long id = toLong(row.get("id"));
|
||||
if (id == null) {
|
||||
continue;
|
||||
}
|
||||
profiles.add(new ProfileRow(
|
||||
id,
|
||||
str(row.get("name")),
|
||||
str(row.get("parameterString")),
|
||||
Boolean.TRUE.equals(row.get("headline")),
|
||||
str(row.get("beginOfLineChars")),
|
||||
str(row.get("endOfLineChars")),
|
||||
str(row.get("beginOfFileChars")),
|
||||
str(row.get("endOfFileChars")),
|
||||
str(row.get("fileExtension"))
|
||||
));
|
||||
}
|
||||
profileGrid.setItems(profiles);
|
||||
} catch (Exception e) {
|
||||
showError("Exportprofile konnten nicht geladen werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFiles() {
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
if (headquartersId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String path = "/export-workspace/files?hqId=" + headquartersId + customerScopeQuery();
|
||||
List<LinkedHashMap<String, Object>> result = restClient.getList(path, new ParameterizedTypeReference<>() {});
|
||||
List<ExportFileRow> rows = new ArrayList<>();
|
||||
for (Map<String, Object> row : result) {
|
||||
Long id = toLong(row.get("id"));
|
||||
if (id == null) {
|
||||
continue;
|
||||
}
|
||||
rows.add(new ExportFileRow(
|
||||
id,
|
||||
str(row.get("fileName")),
|
||||
str(row.get("storedAt")),
|
||||
Boolean.TRUE.equals(row.get("ftpUploaded")),
|
||||
str(row.get("customerCode"))
|
||||
));
|
||||
}
|
||||
fileGrid.setItems(rows);
|
||||
} catch (Exception e) {
|
||||
showError("Exportdateien konnten nicht geladen werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void runExport() {
|
||||
CategoryOption category = categoryField.getValue();
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
if (category == null || headquartersId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("hqId", headquartersId);
|
||||
body.put("customerId", scopedCustomerId);
|
||||
body.put("categoryId", category.id());
|
||||
body.put("profileId", selectedProfileId);
|
||||
body.put("from", fromDate.getValue());
|
||||
body.put("to", toDate.getValue());
|
||||
body.put("statusFilter", statusFilterField.getValue() != null ? statusFilterField.getValue().id() : null);
|
||||
body.put("customerCodeFilter", customerFilterField.getValue());
|
||||
body.put("delimiter", delimiterField.getValue());
|
||||
body.put("fileName", fileNameField.getValue());
|
||||
body.put("includeHeadline", includeHeadlineField.getValue());
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> result = restClient.post("/export-workspace/run", body, LinkedHashMap.class);
|
||||
showInfo("Export erzeugt: " + toInteger(result.get("rowCount")) + " Zeilen.");
|
||||
loadFiles();
|
||||
} catch (Exception e) {
|
||||
showError("Export konnte nicht erzeugt werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void saveProfile() {
|
||||
CategoryOption category = categoryField.getValue();
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
if (category == null || headquartersId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("hqId", headquartersId);
|
||||
body.put("customerId", scopedCustomerId);
|
||||
body.put("categoryId", category.id());
|
||||
body.put("name", profileNameField.getValue());
|
||||
body.put("parameterString", parameterStringField.getValue());
|
||||
body.put("headline", profileHeadlineField.getValue());
|
||||
body.put("beginOfLineChars", beginOfLineField.getValue());
|
||||
body.put("endOfLineChars", endOfLineField.getValue());
|
||||
body.put("beginOfFileChars", beginOfFileField.getValue());
|
||||
body.put("endOfFileChars", endOfFileField.getValue());
|
||||
body.put("fileExtension", fileExtensionField.getValue());
|
||||
|
||||
if (selectedProfileId != null) {
|
||||
restClient.put("/export-workspace/profiles/" + selectedProfileId, body);
|
||||
} else {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> saved = restClient.post("/export-workspace/profiles", body, LinkedHashMap.class);
|
||||
selectedProfileId = toLong(saved.get("id"));
|
||||
}
|
||||
loadProfiles();
|
||||
showInfo("Exportprofil gespeichert.");
|
||||
} catch (Exception e) {
|
||||
showError("Exportprofil konnte nicht gespeichert werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteProfile() {
|
||||
CategoryOption category = categoryField.getValue();
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
if (selectedProfileId == null || category == null || headquartersId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
restClient.delete("/export-workspace/profiles/" + selectedProfileId
|
||||
+ "?hqId=" + headquartersId
|
||||
+ "&categoryId=" + category.id()
|
||||
+ customerScopeQuery());
|
||||
clearProfileForm();
|
||||
loadProfiles();
|
||||
showInfo("Exportprofil geloescht.");
|
||||
} catch (Exception e) {
|
||||
showError("Exportprofil konnte nicht geloescht werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteFile(ExportFileRow row) {
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
if (headquartersId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
restClient.delete("/export-workspace/files/" + row.id()
|
||||
+ "?hqId=" + headquartersId
|
||||
+ customerScopeQuery());
|
||||
loadFiles();
|
||||
showInfo("Exportdatei geloescht.");
|
||||
} catch (Exception e) {
|
||||
showError("Exportdatei konnte nicht geloescht werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private Anchor downloadAnchor(ExportFileRow row) {
|
||||
String path = "/export-workspace/files/" + row.id()
|
||||
+ "/content?hqId=" + sessionData.getHeadquartersId()
|
||||
+ customerScopeQuery();
|
||||
StreamResource resource = new StreamResource(
|
||||
row.fileName(),
|
||||
() -> new ByteArrayInputStream(restClient.getBytes(path))
|
||||
);
|
||||
Anchor anchor = new Anchor(resource, "Download");
|
||||
anchor.getElement().setAttribute("download", true);
|
||||
return anchor;
|
||||
}
|
||||
|
||||
private void fillProfileForm(ProfileRow row) {
|
||||
if (row == null) {
|
||||
clearProfileForm();
|
||||
return;
|
||||
}
|
||||
selectedProfileId = row.id();
|
||||
profileNameField.setValue(row.name());
|
||||
fileExtensionField.setValue(row.fileExtension());
|
||||
parameterStringField.setValue(row.parameterString());
|
||||
profileHeadlineField.setValue(row.headline());
|
||||
beginOfLineField.setValue(row.beginOfLineChars());
|
||||
endOfLineField.setValue(row.endOfLineChars());
|
||||
beginOfFileField.setValue(row.beginOfFileChars());
|
||||
endOfFileField.setValue(row.endOfFileChars());
|
||||
}
|
||||
|
||||
private void clearProfileForm() {
|
||||
selectedProfileId = null;
|
||||
profileGrid.deselectAll();
|
||||
profileNameField.clear();
|
||||
fileExtensionField.clear();
|
||||
parameterStringField.clear();
|
||||
profileHeadlineField.setValue(false);
|
||||
beginOfLineField.clear();
|
||||
endOfLineField.clear();
|
||||
beginOfFileField.clear();
|
||||
endOfFileField.clear();
|
||||
}
|
||||
|
||||
private String customerScopeQuery() {
|
||||
return scopedCustomerId != null ? "&customerId=" + scopedCustomerId : "";
|
||||
}
|
||||
|
||||
private void showInfo(String message) {
|
||||
Notification notification = Notification.show(message, 3000, Notification.Position.TOP_END);
|
||||
notification.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
}
|
||||
|
||||
private void showError(String message) {
|
||||
Notification notification = Notification.show(message, 5000, Notification.Position.TOP_END);
|
||||
notification.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
|
||||
private static String str(Object value) {
|
||||
return value != null ? String.valueOf(value) : "";
|
||||
}
|
||||
|
||||
private static Long toLong(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
if (value != null && !String.valueOf(value).isBlank()) {
|
||||
return Long.parseLong(String.valueOf(value));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Integer toInteger(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
if (value != null && !String.valueOf(value).isBlank()) {
|
||||
return Integer.parseInt(String.valueOf(value));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private record CategoryOption(Long id, String label) {
|
||||
}
|
||||
|
||||
private record StatusOption(Integer id, String label) {
|
||||
}
|
||||
|
||||
private record ProfileRow(Long id,
|
||||
String name,
|
||||
String parameterString,
|
||||
boolean headline,
|
||||
String beginOfLineChars,
|
||||
String endOfLineChars,
|
||||
String beginOfFileChars,
|
||||
String endOfFileChars,
|
||||
String fileExtension) {
|
||||
}
|
||||
|
||||
private record ExportFileRow(Long id,
|
||||
String fileName,
|
||||
String storedAt,
|
||||
boolean ftpUploaded,
|
||||
String customerCode) {
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,493 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.checkbox.Checkbox;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.html.Paragraph;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.tabs.Tab;
|
||||
import com.vaadin.flow.component.tabs.Tabs;
|
||||
import com.vaadin.flow.component.textfield.IntegerField;
|
||||
import com.vaadin.flow.component.textfield.TextArea;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.votian.web.model.SessionData;
|
||||
import de.votian.web.service.RestClientService;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Route(value = "groups-filters", layout = MainLayout.class)
|
||||
@PageTitle("Votian - Gruppen und Filter")
|
||||
public class GroupFilterAdminView extends VerticalLayout {
|
||||
|
||||
private final RestClientService restClient;
|
||||
private final SessionData sessionData;
|
||||
|
||||
private final Grid<GroupRow> groupGrid = new Grid<>(GroupRow.class, false);
|
||||
private final TextField groupNameField = new TextField("Gruppenname");
|
||||
private final Checkbox groupGlobalField = new Checkbox("Fuer alle Niederlassungen sichtbar");
|
||||
private final Paragraph groupInfo = new Paragraph();
|
||||
private final Button groupNewButton = new Button("Neu");
|
||||
private final Button groupSaveButton = new Button("Speichern");
|
||||
private final Button groupDeleteButton = new Button("Loeschen");
|
||||
private final List<GroupRow> groups = new ArrayList<>();
|
||||
|
||||
private final Grid<FilterRow> filterGrid = new Grid<>(FilterRow.class, false);
|
||||
private final TextField filterShortField = new TextField("Kuerzel");
|
||||
private final TextField filterTextField = new TextField("Bezeichnung");
|
||||
private final ComboBox<FilterTypeOption> filterTypeField = new ComboBox<>("Art");
|
||||
private final ComboBox<FilterStatusOption> filterStatusField = new ComboBox<>("Typ");
|
||||
private final IntegerField filterSortField = new IntegerField("Sortierung");
|
||||
private final TextArea filterLongTextField = new TextArea("Langtext");
|
||||
private final Paragraph filterInfo = new Paragraph();
|
||||
private final Button filterNewButton = new Button("Neu");
|
||||
private final Button filterSaveButton = new Button("Speichern");
|
||||
private final Button filterDeleteButton = new Button("Loeschen");
|
||||
private final List<FilterRow> filters = new ArrayList<>();
|
||||
|
||||
private Long selectedGroupId;
|
||||
private boolean selectedGroupReadonly;
|
||||
private Long selectedFilterId;
|
||||
|
||||
public GroupFilterAdminView(RestClientService restClient, SessionData sessionData) {
|
||||
this.restClient = restClient;
|
||||
this.sessionData = sessionData;
|
||||
|
||||
setSizeFull();
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
|
||||
configureGroupGrid();
|
||||
configureFilterGrid();
|
||||
configureForms();
|
||||
configureActions();
|
||||
|
||||
Tab groupsTab = new Tab("Gruppen");
|
||||
Tab filtersTab = new Tab("Filter");
|
||||
Tabs tabs = new Tabs(groupsTab, filtersTab);
|
||||
|
||||
VerticalLayout groupsContent = buildGroupsContent();
|
||||
VerticalLayout filtersContent = buildFiltersContent();
|
||||
filtersContent.setVisible(false);
|
||||
|
||||
tabs.addSelectedChangeListener(event -> {
|
||||
groupsContent.setVisible(event.getSelectedTab() == groupsTab);
|
||||
filtersContent.setVisible(event.getSelectedTab() == filtersTab);
|
||||
});
|
||||
|
||||
add(new H3("Gruppen und Filter"), tabs, groupsContent, filtersContent);
|
||||
expand(groupsContent, filtersContent);
|
||||
|
||||
loadGroups();
|
||||
loadFilters();
|
||||
}
|
||||
|
||||
private void configureGroupGrid() {
|
||||
groupGrid.addColumn(GroupRow::name).setHeader("Gruppe").setAutoWidth(true).setFrozen(true);
|
||||
groupGrid.addColumn(GroupRow::visibilityLabel).setHeader("Sichtbarkeit").setAutoWidth(true);
|
||||
groupGrid.addColumn(GroupRow::usageCount).setHeader("Zuordnungen").setAutoWidth(true);
|
||||
groupGrid.addColumn(GroupRow::usageSummary).setHeader("Verwendung").setFlexGrow(1);
|
||||
groupGrid.addColumn(row -> row.readonly() ? "Ja" : "").setHeader("Readonly").setAutoWidth(true);
|
||||
groupGrid.setSizeFull();
|
||||
groupGrid.asSingleSelect().addValueChangeListener(event -> fillGroupForm(event.getValue()));
|
||||
}
|
||||
|
||||
private void configureFilterGrid() {
|
||||
filterGrid.addColumn(FilterRow::shortCode).setHeader("Kuerzel").setAutoWidth(true).setFrozen(true);
|
||||
filterGrid.addColumn(FilterRow::text).setHeader("Bezeichnung").setAutoWidth(true);
|
||||
filterGrid.addColumn(FilterRow::typeLabel).setHeader("Art").setAutoWidth(true);
|
||||
filterGrid.addColumn(FilterRow::statusLabel).setHeader("Typ").setAutoWidth(true);
|
||||
filterGrid.addColumn(FilterRow::sort).setHeader("Sort.").setAutoWidth(true);
|
||||
filterGrid.addColumn(FilterRow::usageSummary).setHeader("Verwendung").setFlexGrow(1);
|
||||
filterGrid.setSizeFull();
|
||||
filterGrid.asSingleSelect().addValueChangeListener(event -> fillFilterForm(event.getValue()));
|
||||
}
|
||||
|
||||
private void configureForms() {
|
||||
groupNameField.setWidthFull();
|
||||
groupInfo.getStyle().set("margin", "0");
|
||||
|
||||
filterTypeField.setItems(
|
||||
new FilterTypeOption(0, "Fahrzeugfilter"),
|
||||
new FilterTypeOption(1, "Transporteursfilter")
|
||||
);
|
||||
filterTypeField.setItemLabelGenerator(FilterTypeOption::label);
|
||||
filterTypeField.setValue(new FilterTypeOption(0, "Fahrzeugfilter"));
|
||||
|
||||
filterStatusField.setItems(
|
||||
new FilterStatusOption(1, "Pflichtfilter"),
|
||||
new FilterStatusOption(2, "Optional")
|
||||
);
|
||||
filterStatusField.setItemLabelGenerator(FilterStatusOption::label);
|
||||
filterStatusField.setValue(new FilterStatusOption(2, "Optional"));
|
||||
|
||||
filterLongTextField.setWidthFull();
|
||||
filterLongTextField.setMinHeight("140px");
|
||||
filterInfo.getStyle().set("margin", "0");
|
||||
}
|
||||
|
||||
private void configureActions() {
|
||||
groupSaveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
groupDeleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR);
|
||||
filterSaveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
filterDeleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR);
|
||||
|
||||
groupNewButton.addClickListener(event -> clearGroupForm());
|
||||
groupSaveButton.addClickListener(event -> saveGroup());
|
||||
groupDeleteButton.addClickListener(event -> deleteGroup());
|
||||
|
||||
filterNewButton.addClickListener(event -> clearFilterForm());
|
||||
filterSaveButton.addClickListener(event -> saveFilter());
|
||||
filterDeleteButton.addClickListener(event -> deleteFilter());
|
||||
|
||||
refreshGroupButtons();
|
||||
refreshFilterButtons();
|
||||
}
|
||||
|
||||
private VerticalLayout buildGroupsContent() {
|
||||
FormLayout form = new FormLayout();
|
||||
form.setResponsiveSteps(
|
||||
new FormLayout.ResponsiveStep("0", 1),
|
||||
new FormLayout.ResponsiveStep("700px", 2)
|
||||
);
|
||||
form.add(groupNameField, groupGlobalField);
|
||||
|
||||
HorizontalLayout actions = new HorizontalLayout(groupNewButton, groupSaveButton, groupDeleteButton);
|
||||
VerticalLayout content = new VerticalLayout(
|
||||
new Paragraph("Der Gruppenkatalog entspricht der Legacy-Tabelle `groups`. Nicht schreibbare Legacy-Gruppen bleiben sichtbar, aber gesperrt."),
|
||||
form,
|
||||
groupInfo,
|
||||
actions,
|
||||
groupGrid
|
||||
);
|
||||
content.setPadding(false);
|
||||
content.setSpacing(true);
|
||||
content.setSizeFull();
|
||||
content.expand(groupGrid);
|
||||
return content;
|
||||
}
|
||||
|
||||
private VerticalLayout buildFiltersContent() {
|
||||
FormLayout form = new FormLayout();
|
||||
form.setResponsiveSteps(
|
||||
new FormLayout.ResponsiveStep("0", 1),
|
||||
new FormLayout.ResponsiveStep("900px", 2)
|
||||
);
|
||||
form.add(filterShortField, filterTextField, filterTypeField, filterStatusField, filterSortField, filterLongTextField);
|
||||
form.setColspan(filterLongTextField, 2);
|
||||
|
||||
HorizontalLayout actions = new HorizontalLayout(filterNewButton, filterSaveButton, filterDeleteButton);
|
||||
VerticalLayout content = new VerticalLayout(
|
||||
new Paragraph("Filter pflegen den Legacy-Katalog `courierfilter`. Beim Loeschen werden Kunden-, Kurier- und Fahrzeugzuordnungen bereinigt."),
|
||||
form,
|
||||
filterInfo,
|
||||
actions,
|
||||
filterGrid
|
||||
);
|
||||
content.setPadding(false);
|
||||
content.setSpacing(true);
|
||||
content.setSizeFull();
|
||||
content.expand(filterGrid);
|
||||
return content;
|
||||
}
|
||||
|
||||
private void loadGroups() {
|
||||
if (sessionData.getHeadquartersId() == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
List<LinkedHashMap<String, Object>> result = restClient.getList(
|
||||
"/group-filter-admin/groups?hqId=" + sessionData.getHeadquartersId(),
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
groups.clear();
|
||||
for (Map<String, Object> row : result) {
|
||||
Long id = toLong(row.get("id"));
|
||||
if (id == null) {
|
||||
continue;
|
||||
}
|
||||
groups.add(new GroupRow(
|
||||
id,
|
||||
str(row.get("name")),
|
||||
bool(row.get("globalVisible")),
|
||||
bool(row.get("readonly")),
|
||||
toLong(row.get("usageCount")) != null ? toLong(row.get("usageCount")) : 0L,
|
||||
str(row.get("usageSummary"))
|
||||
));
|
||||
}
|
||||
groupGrid.setItems(groups);
|
||||
if (selectedGroupId != null) {
|
||||
groups.stream()
|
||||
.filter(row -> row.id().equals(selectedGroupId))
|
||||
.findFirst()
|
||||
.ifPresent(row -> groupGrid.asSingleSelect().setValue(row));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
showError("Gruppen konnten nicht geladen werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFilters() {
|
||||
if (sessionData.getHeadquartersId() == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
List<LinkedHashMap<String, Object>> result = restClient.getList(
|
||||
"/group-filter-admin/filters?hqId=" + sessionData.getHeadquartersId(),
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
filters.clear();
|
||||
for (Map<String, Object> row : result) {
|
||||
Long id = toLong(row.get("id"));
|
||||
if (id == null) {
|
||||
continue;
|
||||
}
|
||||
filters.add(new FilterRow(
|
||||
id,
|
||||
toInteger(row.get("type")) != null ? toInteger(row.get("type")) : 0,
|
||||
str(row.get("typeLabel")),
|
||||
toInteger(row.get("status")) != null ? toInteger(row.get("status")) : 2,
|
||||
str(row.get("statusLabel")),
|
||||
str(row.get("shortCode")),
|
||||
str(row.get("text")),
|
||||
toInteger(row.get("sort")) != null ? toInteger(row.get("sort")) : 0,
|
||||
str(row.get("longText")),
|
||||
toLong(row.get("usageCount")) != null ? toLong(row.get("usageCount")) : 0L,
|
||||
str(row.get("usageSummary"))
|
||||
));
|
||||
}
|
||||
filterGrid.setItems(filters);
|
||||
if (selectedFilterId != null) {
|
||||
filters.stream()
|
||||
.filter(row -> row.id().equals(selectedFilterId))
|
||||
.findFirst()
|
||||
.ifPresent(row -> filterGrid.asSingleSelect().setValue(row));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
showError("Filter konnten nicht geladen werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void fillGroupForm(GroupRow row) {
|
||||
if (row == null) {
|
||||
clearGroupForm();
|
||||
return;
|
||||
}
|
||||
selectedGroupId = row.id();
|
||||
selectedGroupReadonly = row.readonly();
|
||||
groupNameField.setValue(row.name());
|
||||
groupGlobalField.setValue(row.globalVisible());
|
||||
groupInfo.setText(row.usageSummary());
|
||||
refreshGroupButtons();
|
||||
}
|
||||
|
||||
private void clearGroupForm() {
|
||||
selectedGroupId = null;
|
||||
selectedGroupReadonly = false;
|
||||
groupGrid.asSingleSelect().clear();
|
||||
groupNameField.clear();
|
||||
groupGlobalField.setValue(false);
|
||||
groupInfo.setText("Neue Gruppe anlegen oder bestehende Gruppe aus der Liste bearbeiten.");
|
||||
refreshGroupButtons();
|
||||
}
|
||||
|
||||
private void saveGroup() {
|
||||
if (sessionData.getHeadquartersId() == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Map<String, Object> request = new LinkedHashMap<>();
|
||||
request.put("hqId", sessionData.getHeadquartersId());
|
||||
request.put("name", groupNameField.getValue());
|
||||
request.put("globalVisible", groupGlobalField.getValue());
|
||||
|
||||
if (selectedGroupId == null) {
|
||||
restClient.post("/group-filter-admin/groups", request, LinkedHashMap.class);
|
||||
} else {
|
||||
restClient.put("/group-filter-admin/groups/" + selectedGroupId, request);
|
||||
}
|
||||
Notification.show("Gruppe gespeichert.", 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
clearGroupForm();
|
||||
loadGroups();
|
||||
} catch (Exception e) {
|
||||
showError("Gruppe konnte nicht gespeichert werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteGroup() {
|
||||
if (selectedGroupId == null || sessionData.getHeadquartersId() == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
restClient.delete("/group-filter-admin/groups/" + selectedGroupId + "?hqId=" + sessionData.getHeadquartersId());
|
||||
Notification.show("Gruppe geloescht.", 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
clearGroupForm();
|
||||
loadGroups();
|
||||
} catch (Exception e) {
|
||||
showError("Gruppe konnte nicht geloescht werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void fillFilterForm(FilterRow row) {
|
||||
if (row == null) {
|
||||
clearFilterForm();
|
||||
return;
|
||||
}
|
||||
selectedFilterId = row.id();
|
||||
filterShortField.setValue(row.shortCode());
|
||||
filterShortField.setReadOnly(true);
|
||||
filterTextField.setValue(row.text());
|
||||
filterTypeField.setValue(new FilterTypeOption(row.type(), row.typeLabel()));
|
||||
filterStatusField.setValue(new FilterStatusOption(row.status(), row.statusLabel()));
|
||||
filterSortField.setValue(row.sort());
|
||||
filterLongTextField.setValue(row.longText());
|
||||
filterInfo.setText(row.usageSummary());
|
||||
refreshFilterButtons();
|
||||
}
|
||||
|
||||
private void clearFilterForm() {
|
||||
selectedFilterId = null;
|
||||
filterGrid.asSingleSelect().clear();
|
||||
filterShortField.clear();
|
||||
filterShortField.setReadOnly(false);
|
||||
filterTextField.clear();
|
||||
filterTypeField.setValue(new FilterTypeOption(0, "Fahrzeugfilter"));
|
||||
filterStatusField.setValue(new FilterStatusOption(2, "Optional"));
|
||||
filterSortField.clear();
|
||||
filterLongTextField.clear();
|
||||
filterInfo.setText("Neuen Filter anlegen oder bestehenden Filter aus der Liste bearbeiten.");
|
||||
refreshFilterButtons();
|
||||
}
|
||||
|
||||
private void saveFilter() {
|
||||
if (sessionData.getHeadquartersId() == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Map<String, Object> request = new LinkedHashMap<>();
|
||||
request.put("hqId", sessionData.getHeadquartersId());
|
||||
request.put("shortCode", filterShortField.getValue());
|
||||
request.put("text", filterTextField.getValue());
|
||||
request.put("type", filterTypeField.getValue() != null ? filterTypeField.getValue().code() : 0);
|
||||
request.put("status", filterStatusField.getValue() != null ? filterStatusField.getValue().code() : 2);
|
||||
request.put("sort", filterSortField.getValue());
|
||||
request.put("longText", filterLongTextField.getValue());
|
||||
|
||||
if (selectedFilterId == null) {
|
||||
restClient.post("/group-filter-admin/filters", request, LinkedHashMap.class);
|
||||
} else {
|
||||
restClient.put("/group-filter-admin/filters/" + selectedFilterId, request);
|
||||
}
|
||||
Notification.show("Filter gespeichert.", 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
clearFilterForm();
|
||||
loadFilters();
|
||||
} catch (Exception e) {
|
||||
showError("Filter konnte nicht gespeichert werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteFilter() {
|
||||
if (selectedFilterId == null || sessionData.getHeadquartersId() == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
restClient.delete("/group-filter-admin/filters/" + selectedFilterId + "?hqId=" + sessionData.getHeadquartersId());
|
||||
Notification.show("Filter geloescht.", 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
clearFilterForm();
|
||||
loadFilters();
|
||||
} catch (Exception e) {
|
||||
showError("Filter konnte nicht geloescht werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshGroupButtons() {
|
||||
boolean editable = !selectedGroupReadonly;
|
||||
groupSaveButton.setEnabled(editable);
|
||||
groupDeleteButton.setEnabled(selectedGroupId != null && editable);
|
||||
}
|
||||
|
||||
private void refreshFilterButtons() {
|
||||
filterDeleteButton.setEnabled(selectedFilterId != null);
|
||||
}
|
||||
|
||||
private void showError(String message) {
|
||||
Notification.show(message, 5000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
|
||||
private Long toLong(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
try {
|
||||
return value != null ? Long.parseLong(value.toString()) : null;
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Integer toInteger(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
try {
|
||||
return value != null ? Integer.parseInt(value.toString()) : null;
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean bool(Object value) {
|
||||
return Boolean.TRUE.equals(value) || (value != null && "true".equalsIgnoreCase(value.toString()));
|
||||
}
|
||||
|
||||
private String str(Object value) {
|
||||
return value != null ? value.toString() : "";
|
||||
}
|
||||
|
||||
private record GroupRow(Long id,
|
||||
String name,
|
||||
boolean globalVisible,
|
||||
boolean readonly,
|
||||
long usageCount,
|
||||
String usageSummary) {
|
||||
private String visibilityLabel() {
|
||||
return globalVisible ? "Alle Niederlassungen" : "Nur aktuelle Niederlassung";
|
||||
}
|
||||
}
|
||||
|
||||
private record FilterRow(Long id,
|
||||
int type,
|
||||
String typeLabel,
|
||||
int status,
|
||||
String statusLabel,
|
||||
String shortCode,
|
||||
String text,
|
||||
int sort,
|
||||
String longText,
|
||||
long usageCount,
|
||||
String usageSummary) {
|
||||
}
|
||||
|
||||
private record FilterTypeOption(int code, String label) {
|
||||
}
|
||||
|
||||
private record FilterStatusOption(int code, String label) {
|
||||
}
|
||||
}
|
||||
1063
vaadin/src/main/java/de/votian/web/view/GroupwareView.java
Normal file
1063
vaadin/src/main/java/de/votian/web/view/GroupwareView.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,143 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.textfield.IntegerField;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.votian.web.model.SessionData;
|
||||
import de.votian.web.service.RestClientService;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Route(value = "headquarters-admin", layout = MainLayout.class)
|
||||
@PageTitle("Votian - Niederlassung")
|
||||
public class HeadquartersAdminView extends VerticalLayout {
|
||||
|
||||
private final RestClientService restClient;
|
||||
private final SessionData sessionData;
|
||||
|
||||
private final TextField mnemonicField = new TextField("Kuerzel");
|
||||
private final TextField nameField = new TextField("Name");
|
||||
private final IntegerField workmodeField = new IntegerField("Workmode");
|
||||
private final TextField companyNameField = new TextField("Firma");
|
||||
private final TextField companyName2Field = new TextField("Firma 2");
|
||||
private final TextField companyName3Field = new TextField("Firma 3");
|
||||
private final TextField companyName4Field = new TextField("Firma 4");
|
||||
private final TextField streetField = new TextField("Strasse");
|
||||
private final TextField hsnoField = new TextField("Hausnummer");
|
||||
private final TextField zipcodeField = new TextField("PLZ");
|
||||
private final TextField cityField = new TextField("Ort");
|
||||
private final TextField bwvPhoneField = new TextField("BWV-Hotline");
|
||||
private final TextField glnField = new TextField("GLN");
|
||||
private final TextField dunsField = new TextField("DUNS");
|
||||
|
||||
public HeadquartersAdminView(RestClientService restClient, SessionData sessionData) {
|
||||
this.restClient = restClient;
|
||||
this.sessionData = sessionData;
|
||||
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
|
||||
FormLayout form = new FormLayout();
|
||||
form.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1),
|
||||
new FormLayout.ResponsiveStep("700px", 2));
|
||||
form.add(
|
||||
mnemonicField, nameField, workmodeField, companyNameField, companyName2Field,
|
||||
companyName3Field, companyName4Field, streetField, hsnoField, zipcodeField,
|
||||
cityField, bwvPhoneField, glnField, dunsField
|
||||
);
|
||||
|
||||
Button saveButton = new Button("Speichern", event -> save());
|
||||
saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
saveButton.setEnabled(sessionData.canManageHeadquarters());
|
||||
|
||||
add(new H3("Niederlassung"), form, new HorizontalLayout(saveButton));
|
||||
load();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void load() {
|
||||
if (sessionData.getHeadquartersId() == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Map<String, Object> data = restClient.get("/headquarters/" + sessionData.getHeadquartersId(), LinkedHashMap.class);
|
||||
mnemonicField.setValue(str(data.get("mnemonic")));
|
||||
nameField.setValue(str(data.get("name")));
|
||||
workmodeField.setValue(data.get("workmode") instanceof Number ? ((Number) data.get("workmode")).intValue() : null);
|
||||
companyNameField.setValue(str(data.get("companyName")));
|
||||
companyName2Field.setValue(str(data.get("companyName2")));
|
||||
companyName3Field.setValue(str(data.get("companyName3")));
|
||||
companyName4Field.setValue(str(data.get("companyName4")));
|
||||
streetField.setValue(str(data.get("street")));
|
||||
hsnoField.setValue(str(data.get("hsno")));
|
||||
zipcodeField.setValue(str(data.get("zipcode")));
|
||||
cityField.setValue(str(data.get("city")));
|
||||
bwvPhoneField.setValue(str(data.get("bwvPhone")));
|
||||
glnField.setValue(str(data.get("gln")));
|
||||
dunsField.setValue(str(data.get("duns")));
|
||||
} catch (Exception e) {
|
||||
Notification.show("Niederlassung konnte nicht geladen werden.", 4000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void save() {
|
||||
if (!sessionData.canManageHeadquarters()) {
|
||||
Notification.show("Keine Berechtigung zum Bearbeiten der Niederlassung.", 4000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
return;
|
||||
}
|
||||
if (sessionData.getHeadquartersId() == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Map<String, Object> request = new LinkedHashMap<>();
|
||||
Map<String, Object> headquarters = new LinkedHashMap<>();
|
||||
headquarters.put("mnemonic", mnemonicField.getValue());
|
||||
headquarters.put("name", nameField.getValue());
|
||||
headquarters.put("workmode", workmodeField.getValue());
|
||||
request.put("headquarters", headquarters);
|
||||
|
||||
Map<String, Object> company = new LinkedHashMap<>();
|
||||
company.put("comp", companyNameField.getValue());
|
||||
company.put("comp2", companyName2Field.getValue());
|
||||
company.put("comp3", companyName3Field.getValue());
|
||||
company.put("comp4", companyName4Field.getValue());
|
||||
company.put("hsno", hsnoField.getValue());
|
||||
request.put("company", company);
|
||||
|
||||
Map<String, Object> address = new LinkedHashMap<>();
|
||||
address.put("street", streetField.getValue());
|
||||
address.put("zipcode", zipcodeField.getValue());
|
||||
address.put("city", cityField.getValue());
|
||||
address.put("country", "DE");
|
||||
request.put("address", address);
|
||||
|
||||
request.put("bwvPhone", bwvPhoneField.getValue());
|
||||
request.put("gln", glnField.getValue());
|
||||
request.put("duns", dunsField.getValue());
|
||||
|
||||
restClient.put("/headquarters/" + sessionData.getHeadquartersId(), request);
|
||||
Notification.show("Niederlassung gespeichert.", 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
load();
|
||||
} catch (Exception e) {
|
||||
Notification.show("Niederlassung konnte nicht gespeichert werden.", 5000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private String str(Object value) {
|
||||
return value != null ? value.toString() : "";
|
||||
}
|
||||
}
|
||||
429
vaadin/src/main/java/de/votian/web/view/ImportView.java
Normal file
429
vaadin/src/main/java/de/votian/web/view/ImportView.java
Normal file
@@ -0,0 +1,429 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.html.Anchor;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.html.Pre;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.upload.Upload;
|
||||
import com.vaadin.flow.component.upload.receivers.MemoryBuffer;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import com.vaadin.flow.server.StreamResource;
|
||||
import de.votian.web.service.RestClientService;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Route(value = "imports", layout = MainLayout.class)
|
||||
@PageTitle("Votian - Importe")
|
||||
public class ImportView extends VerticalLayout {
|
||||
|
||||
private static final DateTimeFormatter FILE_TIME_FORMAT = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
|
||||
|
||||
private final RestClientService restClient;
|
||||
|
||||
private final ComboBox<ImportProcessRow> processField = new ComboBox<>("Prozess");
|
||||
private final ComboBox<CustomerOption> customerField = new ComboBox<>("Kunde");
|
||||
private final Button refreshButton = new Button("Aktualisieren");
|
||||
private final Button executeButton = new Button("Nativen Import ausfuehren");
|
||||
private final Grid<ImportFileRow> fileGrid = new Grid<>(ImportFileRow.class, false);
|
||||
private final MemoryBuffer uploadBuffer = new MemoryBuffer();
|
||||
private final Upload upload = new Upload(uploadBuffer);
|
||||
private final Pre processInfo = new Pre();
|
||||
private final Pre resultBox = new Pre();
|
||||
|
||||
private List<ImportProcessRow> processes = List.of();
|
||||
private List<CustomerOption> customers = List.of();
|
||||
|
||||
public ImportView(RestClientService restClient) {
|
||||
this.restClient = restClient;
|
||||
|
||||
setSizeFull();
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
|
||||
configureSelectors();
|
||||
configureUpload();
|
||||
configureGrid();
|
||||
configureResultBox();
|
||||
|
||||
add(new H3("Importe"), buildToolbar(), buildWorkspace());
|
||||
expand(fileGrid);
|
||||
|
||||
loadProcesses();
|
||||
}
|
||||
|
||||
private void configureSelectors() {
|
||||
processField.setWidth("420px");
|
||||
processField.setItemLabelGenerator(ImportProcessRow::comboLabel);
|
||||
processField.addValueChangeListener(event -> {
|
||||
updateProcessPresentation();
|
||||
refreshFiles();
|
||||
});
|
||||
|
||||
customerField.setWidth("360px");
|
||||
customerField.setItemLabelGenerator(CustomerOption::label);
|
||||
customerField.setVisible(false);
|
||||
customerField.addValueChangeListener(event -> updateExecuteState());
|
||||
|
||||
refreshButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
refreshButton.addClickListener(event -> refreshFiles());
|
||||
|
||||
executeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
executeButton.addClickListener(event -> executeSelectedImport());
|
||||
executeButton.setEnabled(false);
|
||||
}
|
||||
|
||||
private void configureUpload() {
|
||||
upload.setMaxFiles(1);
|
||||
upload.setMaxFileSize(25 * 1024 * 1024);
|
||||
upload.setDropAllowed(true);
|
||||
upload.setUploadButton(new Button("Datei hochladen"));
|
||||
upload.addSucceededListener(event -> uploadSelectedFile(event.getFileName(), event.getMIMEType()));
|
||||
upload.addFileRejectedListener(event -> showError(event.getErrorMessage()));
|
||||
}
|
||||
|
||||
private void configureGrid() {
|
||||
fileGrid.addColumn(ImportFileRow::fileName).setHeader("Datei").setFlexGrow(1);
|
||||
fileGrid.addColumn(row -> formatBytes(row.size())).setHeader("Groesse").setAutoWidth(true);
|
||||
fileGrid.addColumn(row -> row.modifiedAt() != null ? FILE_TIME_FORMAT.format(row.modifiedAt()) : "")
|
||||
.setHeader("Geaendert")
|
||||
.setAutoWidth(true);
|
||||
fileGrid.addComponentColumn(this::buildDownloadLink).setHeader("Download").setAutoWidth(true);
|
||||
fileGrid.addComponentColumn(this::buildDeleteButton).setHeader("Loeschen").setAutoWidth(true);
|
||||
fileGrid.setSizeFull();
|
||||
fileGrid.asSingleSelect().addValueChangeListener(event -> updateExecuteState());
|
||||
}
|
||||
|
||||
private void configureResultBox() {
|
||||
processInfo.getStyle()
|
||||
.set("background", "var(--lumo-contrast-5pct)")
|
||||
.set("padding", "var(--lumo-space-m)")
|
||||
.set("margin", "0");
|
||||
resultBox.getStyle()
|
||||
.set("background", "var(--lumo-contrast-5pct)")
|
||||
.set("padding", "var(--lumo-space-m)")
|
||||
.set("margin", "0");
|
||||
processInfo.setText("Prozess auswaehlen");
|
||||
resultBox.setText("Noch kein Import ausgefuehrt.");
|
||||
}
|
||||
|
||||
private HorizontalLayout buildToolbar() {
|
||||
HorizontalLayout toolbar = new HorizontalLayout(processField, customerField, upload, refreshButton, executeButton);
|
||||
toolbar.setAlignItems(Alignment.END);
|
||||
toolbar.setWidthFull();
|
||||
return toolbar;
|
||||
}
|
||||
|
||||
private VerticalLayout buildWorkspace() {
|
||||
VerticalLayout workspace = new VerticalLayout(processInfo, fileGrid, resultBox);
|
||||
workspace.setPadding(false);
|
||||
workspace.setSpacing(true);
|
||||
workspace.setSizeFull();
|
||||
workspace.expand(fileGrid);
|
||||
return workspace;
|
||||
}
|
||||
|
||||
private void loadProcesses() {
|
||||
try {
|
||||
processes = restClient.getList("/imports/processes", new ParameterizedTypeReference<List<ImportProcessRow>>() {});
|
||||
processField.setItems(processes);
|
||||
if (!processes.isEmpty()) {
|
||||
processField.setValue(processes.getFirst());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
showError("Importprozesse konnten nicht geladen werden: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void loadCustomersIfNeeded() {
|
||||
ImportProcessRow selectedProcess = processField.getValue();
|
||||
if (selectedProcess == null || !selectedProcess.nativeExecution() || !selectedProcess.customerRequired()) {
|
||||
customers = List.of();
|
||||
customerField.clear();
|
||||
customerField.setItems(List.of());
|
||||
customerField.setVisible(false);
|
||||
return;
|
||||
}
|
||||
if (!customers.isEmpty()) {
|
||||
customerField.setVisible(true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
customers = restClient.getList("/imports/customers", new ParameterizedTypeReference<List<CustomerOption>>() {});
|
||||
customers = customers != null ? customers : List.of();
|
||||
customerField.setItems(customers);
|
||||
customerField.setVisible(true);
|
||||
if (!customers.isEmpty()) {
|
||||
customerField.setValue(customers.getFirst());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
customers = List.of();
|
||||
customerField.setItems(List.of());
|
||||
customerField.setVisible(true);
|
||||
showError("Kunden fuer den Import konnten nicht geladen werden: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateProcessPresentation() {
|
||||
ImportProcessRow selectedProcess = processField.getValue();
|
||||
loadCustomersIfNeeded();
|
||||
if (selectedProcess == null) {
|
||||
processInfo.setText("Prozess auswaehlen");
|
||||
fileGrid.setItems(List.of());
|
||||
return;
|
||||
}
|
||||
String executionMode = selectedProcess.nativeExecution()
|
||||
? "Nativer NG-Import verfuegbar"
|
||||
: "Archivierter Legacy-Sonderfall ohne nativen Parser";
|
||||
executeButton.setText(selectedProcess.nativeExecution() ? "Nativen Import ausfuehren" : "Archivierter Sonderfall");
|
||||
processInfo.setText(selectedProcess.label()
|
||||
+ "\n\n"
|
||||
+ "Status: " + selectedProcess.status()
|
||||
+ "\n"
|
||||
+ "Prioritaet: " + selectedProcess.priority()
|
||||
+ "\n"
|
||||
+ "Legacy-Quellen: " + joinValues(selectedProcess.legacySources())
|
||||
+ "\n"
|
||||
+ "NG-Nachfolger: " + fallback(selectedProcess.successorWorkspace(), "Import-Workspace")
|
||||
+ "\n\n"
|
||||
+ selectedProcess.description()
|
||||
+ "\n\n"
|
||||
+ "Entscheidung: " + fallback(selectedProcess.decisionSummary(), "-")
|
||||
+ "\n\n"
|
||||
+ executionMode
|
||||
+ "\n\n"
|
||||
+ selectedProcess.schemaHint());
|
||||
updateExecuteState();
|
||||
}
|
||||
|
||||
private void refreshFiles() {
|
||||
ImportProcessRow selectedProcess = processField.getValue();
|
||||
if (selectedProcess == null) {
|
||||
fileGrid.setItems(List.of());
|
||||
updateExecuteState();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
List<ImportFileRow> files = restClient.getList(
|
||||
"/imports/processes/" + selectedProcess.key() + "/files",
|
||||
new ParameterizedTypeReference<List<ImportFileRow>>() {});
|
||||
fileGrid.setItems(files != null ? files : List.of());
|
||||
} catch (Exception ex) {
|
||||
fileGrid.setItems(List.of());
|
||||
showError("Dateiliste konnte nicht geladen werden: " + ex.getMessage());
|
||||
}
|
||||
updateExecuteState();
|
||||
}
|
||||
|
||||
private void uploadSelectedFile(String fileName, String mimeType) {
|
||||
ImportProcessRow selectedProcess = processField.getValue();
|
||||
if (selectedProcess == null) {
|
||||
showError("Bitte zuerst einen Prozess auswaehlen.");
|
||||
upload.clearFileList();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
byte[] content = uploadBuffer.getInputStream().readAllBytes();
|
||||
restClient.postMultipart(
|
||||
"/imports/processes/" + selectedProcess.key() + "/files",
|
||||
content,
|
||||
fileName,
|
||||
mimeType,
|
||||
Collections.emptyMap(),
|
||||
ImportFileRow.class
|
||||
);
|
||||
showSuccess("Datei gespeichert");
|
||||
refreshFiles();
|
||||
} catch (Exception ex) {
|
||||
showError("Upload fehlgeschlagen: " + ex.getMessage());
|
||||
} finally {
|
||||
upload.clearFileList();
|
||||
}
|
||||
}
|
||||
|
||||
private void executeSelectedImport() {
|
||||
ImportProcessRow selectedProcess = processField.getValue();
|
||||
ImportFileRow selectedFile = fileGrid.asSingleSelect().getValue();
|
||||
CustomerOption selectedCustomer = customerField.getValue();
|
||||
if (selectedProcess == null || selectedFile == null) {
|
||||
showError("Bitte Prozess und Datei auswaehlen.");
|
||||
return;
|
||||
}
|
||||
if (selectedProcess.customerRequired() && selectedCustomer == null) {
|
||||
showError("Bitte einen Kunden auswaehlen.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("fileName", selectedFile.fileName());
|
||||
if (selectedCustomer != null) {
|
||||
payload.put("customerId", selectedCustomer.id());
|
||||
}
|
||||
ImportExecutionResult executionResult = restClient.post(
|
||||
"/imports/processes/" + selectedProcess.key() + "/execute",
|
||||
payload,
|
||||
ImportExecutionResult.class
|
||||
);
|
||||
showExecutionResult(executionResult);
|
||||
refreshFiles();
|
||||
} catch (Exception ex) {
|
||||
showError("Import konnte nicht gestartet werden: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private Anchor buildDownloadLink(ImportFileRow row) {
|
||||
ImportProcessRow selectedProcess = processField.getValue();
|
||||
String encodedName = URLEncoder.encode(row.fileName(), StandardCharsets.UTF_8);
|
||||
StreamResource resource = new StreamResource(
|
||||
row.fileName(),
|
||||
() -> new ByteArrayInputStream(restClient.getBytes(
|
||||
"/imports/processes/" + selectedProcess.key() + "/files/content?name=" + encodedName
|
||||
))
|
||||
);
|
||||
Anchor anchor = new Anchor(resource, "");
|
||||
anchor.getElement().setAttribute("download", true);
|
||||
Button button = new Button("Download");
|
||||
button.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
anchor.add(button);
|
||||
return anchor;
|
||||
}
|
||||
|
||||
private Button buildDeleteButton(ImportFileRow row) {
|
||||
Button button = new Button("Loeschen");
|
||||
button.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
|
||||
button.addClickListener(event -> {
|
||||
ImportProcessRow selectedProcess = processField.getValue();
|
||||
if (selectedProcess == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String encodedName = URLEncoder.encode(row.fileName(), StandardCharsets.UTF_8);
|
||||
restClient.delete("/imports/processes/" + selectedProcess.key() + "/files?name=" + encodedName);
|
||||
showSuccess("Datei geloescht");
|
||||
refreshFiles();
|
||||
} catch (Exception ex) {
|
||||
showError("Datei konnte nicht geloescht werden: " + ex.getMessage());
|
||||
}
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
private void updateExecuteState() {
|
||||
ImportProcessRow selectedProcess = processField.getValue();
|
||||
ImportFileRow selectedFile = fileGrid.asSingleSelect().getValue();
|
||||
boolean enabled = selectedProcess != null
|
||||
&& selectedProcess.nativeExecution()
|
||||
&& selectedFile != null
|
||||
&& (!selectedProcess.customerRequired() || customerField.getValue() != null);
|
||||
executeButton.setEnabled(enabled);
|
||||
}
|
||||
|
||||
private void showExecutionResult(ImportExecutionResult result) {
|
||||
if (result == null) {
|
||||
resultBox.setText("Keine Rueckgabe vom Importdienst erhalten.");
|
||||
return;
|
||||
}
|
||||
StringBuilder text = new StringBuilder();
|
||||
text.append(result.success ? "Erfolg" : "Fehler").append('\n');
|
||||
if (result.summary != null) {
|
||||
text.append(result.summary).append('\n');
|
||||
}
|
||||
text.append('\n')
|
||||
.append("Angelegt: ").append(result.createdCount).append('\n')
|
||||
.append("Aktualisiert: ").append(result.updatedCount).append('\n')
|
||||
.append("Geloescht: ").append(result.deletedCount).append('\n')
|
||||
.append("Ignoriert: ").append(result.ignoredCount);
|
||||
if (result.messages != null && !result.messages.isEmpty()) {
|
||||
text.append("\n\nHinweise:\n");
|
||||
result.messages.forEach(message -> text.append("- ").append(message).append('\n'));
|
||||
}
|
||||
resultBox.setText(text.toString().trim());
|
||||
if (result.success) {
|
||||
showSuccess("Import ausgefuehrt");
|
||||
} else {
|
||||
showError(result.summary != null ? result.summary : "Import fehlgeschlagen");
|
||||
}
|
||||
}
|
||||
|
||||
private String formatBytes(long size) {
|
||||
if (size >= 1024 * 1024) {
|
||||
return String.format("%.1f MB", size / (1024d * 1024d));
|
||||
}
|
||||
if (size >= 1024) {
|
||||
return String.format("%.1f KB", size / 1024d);
|
||||
}
|
||||
return size + " B";
|
||||
}
|
||||
|
||||
private void showSuccess(String message) {
|
||||
Notification notification = Notification.show(message, 2500, Notification.Position.BOTTOM_START);
|
||||
notification.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
}
|
||||
|
||||
private void showError(String message) {
|
||||
Notification notification = Notification.show(message, 4000, Notification.Position.BOTTOM_START);
|
||||
notification.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
|
||||
private String joinValues(List<String> values) {
|
||||
if (values == null || values.isEmpty()) {
|
||||
return "-";
|
||||
}
|
||||
return String.join(", ", values);
|
||||
}
|
||||
|
||||
private String fallback(String value, String fallback) {
|
||||
return value == null || value.isBlank() ? fallback : value;
|
||||
}
|
||||
|
||||
private record ImportProcessRow(String key,
|
||||
String label,
|
||||
String description,
|
||||
String schemaHint,
|
||||
boolean nativeExecution,
|
||||
boolean customerRequired,
|
||||
String status,
|
||||
String priority,
|
||||
String decisionSummary,
|
||||
String successorWorkspace,
|
||||
List<String> legacySources) {
|
||||
private String comboLabel() {
|
||||
return label + " [" + status + " | " + priority + "]";
|
||||
}
|
||||
}
|
||||
|
||||
private record ImportFileRow(String fileName, long size, LocalDateTime modifiedAt) {
|
||||
}
|
||||
|
||||
private record CustomerOption(Long id, String eid, String companyName) {
|
||||
private String label() {
|
||||
return (eid != null ? eid + " - " : "") + (companyName != null ? companyName : "Kunde " + id);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ImportExecutionResult {
|
||||
public boolean success;
|
||||
public String summary;
|
||||
public int createdCount;
|
||||
public int updatedCount;
|
||||
public int deletedCount;
|
||||
public int ignoredCount;
|
||||
public List<String> messages;
|
||||
}
|
||||
}
|
||||
574
vaadin/src/main/java/de/votian/web/view/InvoiceOverviewView.java
Normal file
574
vaadin/src/main/java/de/votian/web/view/InvoiceOverviewView.java
Normal file
@@ -0,0 +1,574 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.datepicker.DatePicker;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.html.Anchor;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.html.H4;
|
||||
import com.vaadin.flow.component.html.Pre;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import com.vaadin.flow.server.StreamResource;
|
||||
import de.votian.web.model.SessionData;
|
||||
import de.votian.web.service.RestClientService;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Route(value = "invoices", layout = MainLayout.class)
|
||||
@PageTitle("Votian - Rechnungen")
|
||||
public class InvoiceOverviewView extends VerticalLayout {
|
||||
|
||||
private static final long ALL_COST_CENTERS_ID = -1L;
|
||||
|
||||
private final RestClientService restClient;
|
||||
private final SessionData sessionData;
|
||||
|
||||
private final DatePicker fromDate = new DatePicker("Von");
|
||||
private final DatePicker toDate = new DatePicker("Bis");
|
||||
private final ComboBox<CostCenterOption> costCenterField = new ComboBox<>("Kostenstelle");
|
||||
|
||||
private final Span totalJobs = new Span("-");
|
||||
private final Span billableJobs = new Span("-");
|
||||
private final Span totalRevenue = new Span("-");
|
||||
private final Span totalCourierCosts = new Span("-");
|
||||
private final Span margin = new Span("-");
|
||||
|
||||
private final Grid<CustomerSummaryRow> customerGrid = new Grid<>(CustomerSummaryRow.class, false);
|
||||
private final Grid<InvoiceRow> invoiceGrid = new Grid<>(InvoiceRow.class, false);
|
||||
|
||||
private final Span detailJob = new Span("-");
|
||||
private final Span detailCustomer = new Span("-");
|
||||
private final Span detailCostCenter = new Span("-");
|
||||
private final Span detailTimes = new Span("-");
|
||||
private final Span detailRoute = new Span("-");
|
||||
private final Span detailCourier = new Span("-");
|
||||
private final Span detailStatus = new Span("-");
|
||||
private final Pre serviceDetails = new Pre();
|
||||
private final Pre invoiceText = new Pre();
|
||||
private final Button openJobButton = new Button("Auftrag oeffnen");
|
||||
private final Anchor csvExportLink = new Anchor();
|
||||
private final Anchor pdfExportLink = new Anchor();
|
||||
private final Button csvExportButton = new Button("CSV exportieren");
|
||||
private final Button pdfExportButton = new Button("PDF exportieren");
|
||||
|
||||
private CustomerSummaryRow selectedCustomer;
|
||||
private InvoiceRow selectedInvoice;
|
||||
|
||||
public InvoiceOverviewView(RestClientService restClient, SessionData sessionData) {
|
||||
this.restClient = restClient;
|
||||
this.sessionData = sessionData;
|
||||
|
||||
setSizeFull();
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
|
||||
configureFilters();
|
||||
configureCustomerGrid();
|
||||
configureInvoiceGrid();
|
||||
configureDetails();
|
||||
configureExportLinks();
|
||||
|
||||
HorizontalLayout filterBar = buildFilterBar();
|
||||
HorizontalLayout cards = buildCards();
|
||||
HorizontalLayout workspace = buildWorkspace();
|
||||
|
||||
add(new H3("Rechnungen"), filterBar, cards, workspace);
|
||||
expand(workspace);
|
||||
|
||||
loadWorkspace();
|
||||
}
|
||||
|
||||
private void configureExportLinks() {
|
||||
csvExportButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
pdfExportButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
csvExportLink.add(csvExportButton);
|
||||
pdfExportLink.add(pdfExportButton);
|
||||
csvExportLink.getElement().setAttribute("download", true);
|
||||
pdfExportLink.getElement().setAttribute("download", true);
|
||||
updateExportLinks();
|
||||
}
|
||||
|
||||
private void configureFilters() {
|
||||
fromDate.setValue(LocalDate.now().withDayOfMonth(1));
|
||||
toDate.setValue(LocalDate.now());
|
||||
costCenterField.setItemLabelGenerator(CostCenterOption::label);
|
||||
costCenterField.setWidth("360px");
|
||||
costCenterField.addValueChangeListener(event -> {
|
||||
if (selectedCustomer != null) {
|
||||
loadInvoiceItems();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private HorizontalLayout buildFilterBar() {
|
||||
Button reloadButton = new Button("Neu laden", event -> loadWorkspace());
|
||||
reloadButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
|
||||
HorizontalLayout filterBar = new HorizontalLayout(fromDate, toDate, reloadButton);
|
||||
filterBar.setAlignItems(Alignment.BASELINE);
|
||||
return filterBar;
|
||||
}
|
||||
|
||||
private HorizontalLayout buildCards() {
|
||||
HorizontalLayout cards = new HorizontalLayout(
|
||||
createCard("Auftraege", totalJobs),
|
||||
createCard("Abrechenbar", billableJobs),
|
||||
createCard("Umsatz", totalRevenue),
|
||||
createCard("Kurierkosten", totalCourierCosts),
|
||||
createCard("Deckungsbeitrag", margin)
|
||||
);
|
||||
cards.setWidthFull();
|
||||
return cards;
|
||||
}
|
||||
|
||||
private HorizontalLayout buildWorkspace() {
|
||||
VerticalLayout customerPanel = new VerticalLayout(new H4("Kunden mit Rechnungsfaellen"), customerGrid);
|
||||
customerPanel.setPadding(false);
|
||||
customerPanel.setSpacing(true);
|
||||
customerPanel.setWidth("360px");
|
||||
customerPanel.setHeightFull();
|
||||
customerPanel.expand(customerGrid);
|
||||
|
||||
Button refreshItemsButton = new Button("Details laden", event -> loadInvoiceItems());
|
||||
HorizontalLayout invoiceToolbar = new HorizontalLayout(costCenterField, refreshItemsButton, csvExportLink, pdfExportLink);
|
||||
invoiceToolbar.setAlignItems(Alignment.BASELINE);
|
||||
|
||||
VerticalLayout detailPanel = buildDetailPanel();
|
||||
|
||||
VerticalLayout invoicePanel = new VerticalLayout(new H4("Rechnungsdetails"), invoiceToolbar, invoiceGrid, detailPanel);
|
||||
invoicePanel.setPadding(false);
|
||||
invoicePanel.setSpacing(true);
|
||||
invoicePanel.setSizeFull();
|
||||
invoicePanel.expand(invoiceGrid);
|
||||
|
||||
HorizontalLayout workspace = new HorizontalLayout(customerPanel, invoicePanel);
|
||||
workspace.setSizeFull();
|
||||
workspace.expand(invoicePanel);
|
||||
return workspace;
|
||||
}
|
||||
|
||||
private void configureCustomerGrid() {
|
||||
customerGrid.addColumn(CustomerSummaryRow::customerName).setHeader("Kunde").setFlexGrow(1);
|
||||
customerGrid.addColumn(CustomerSummaryRow::customerEid).setHeader("EID").setAutoWidth(true);
|
||||
customerGrid.addColumn(CustomerSummaryRow::invoiceableJobs).setHeader("Faelle").setAutoWidth(true);
|
||||
customerGrid.addColumn(row -> formatAmount(row.totalRevenue())).setHeader("Umsatz").setAutoWidth(true);
|
||||
customerGrid.setSizeFull();
|
||||
customerGrid.asSingleSelect().addValueChangeListener(event -> {
|
||||
selectedCustomer = event.getValue();
|
||||
selectedInvoice = null;
|
||||
clearDetail();
|
||||
if (selectedCustomer != null) {
|
||||
loadCostCenters(selectedCustomer.customerId());
|
||||
loadInvoiceItems();
|
||||
} else {
|
||||
costCenterField.clear();
|
||||
costCenterField.setItems(List.of());
|
||||
invoiceGrid.setItems(List.of());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void configureInvoiceGrid() {
|
||||
invoiceGrid.addColumn(InvoiceRow::jobId).setHeader("Auftrag").setAutoWidth(true).setFrozen(true);
|
||||
invoiceGrid.addColumn(InvoiceRow::finishTime).setHeader("Abschluss").setAutoWidth(true);
|
||||
invoiceGrid.addColumn(InvoiceRow::costCenterName).setHeader("Kostenstelle").setAutoWidth(true);
|
||||
invoiceGrid.addColumn(InvoiceRow::commissionNo).setHeader("Kommission").setAutoWidth(true);
|
||||
invoiceGrid.addColumn(InvoiceRow::pickupName).setHeader("Abholung").setFlexGrow(1);
|
||||
invoiceGrid.addColumn(InvoiceRow::deliveryName).setHeader("Zustellung").setFlexGrow(1);
|
||||
invoiceGrid.addColumn(InvoiceRow::courierSid).setHeader("Kurier").setAutoWidth(true);
|
||||
invoiceGrid.addColumn(row -> formatAmount(row.totalPrice())).setHeader("Umsatz").setAutoWidth(true);
|
||||
invoiceGrid.addColumn(row -> formatAmount(row.courierPrice())).setHeader("Kurierkosten").setAutoWidth(true);
|
||||
invoiceGrid.addColumn(row -> formatAmount(row.margin())).setHeader("Marge").setAutoWidth(true);
|
||||
invoiceGrid.addColumn(row -> row.exported() ? "Ja" : "Nein").setHeader("Export").setAutoWidth(true);
|
||||
invoiceGrid.setSizeFull();
|
||||
invoiceGrid.asSingleSelect().addValueChangeListener(event -> {
|
||||
selectedInvoice = event.getValue();
|
||||
updateDetail();
|
||||
});
|
||||
}
|
||||
|
||||
private void configureDetails() {
|
||||
configureTextBlock(serviceDetails);
|
||||
configureTextBlock(invoiceText);
|
||||
openJobButton.setEnabled(false);
|
||||
openJobButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
openJobButton.addClickListener(event -> {
|
||||
if (selectedInvoice != null && sessionData.canViewJobs()) {
|
||||
getUI().ifPresent(ui -> ui.navigate("jobs/" + selectedInvoice.jobId()));
|
||||
}
|
||||
});
|
||||
clearDetail();
|
||||
}
|
||||
|
||||
private VerticalLayout buildDetailPanel() {
|
||||
VerticalLayout detailPanel = new VerticalLayout();
|
||||
detailPanel.setPadding(true);
|
||||
detailPanel.setSpacing(true);
|
||||
detailPanel.getStyle()
|
||||
.set("border", "1px solid var(--lumo-contrast-20pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-l)");
|
||||
|
||||
detailPanel.add(
|
||||
new H4("Ausgewaehlter Rechnungsfall"),
|
||||
detailLine("Auftrag", detailJob),
|
||||
detailLine("Kunde", detailCustomer),
|
||||
detailLine("Kostenstelle", detailCostCenter),
|
||||
detailLine("Zeiten", detailTimes),
|
||||
detailLine("Route", detailRoute),
|
||||
detailLine("Kurier", detailCourier),
|
||||
detailLine("Status", detailStatus),
|
||||
new H4("Leistungen"),
|
||||
serviceDetails,
|
||||
new H4("Rechnungstext"),
|
||||
invoiceText,
|
||||
openJobButton
|
||||
);
|
||||
return detailPanel;
|
||||
}
|
||||
|
||||
private HorizontalLayout detailLine(String label, Span value) {
|
||||
Span headline = new Span(label + ":");
|
||||
headline.getStyle().set("font-weight", "600");
|
||||
HorizontalLayout line = new HorizontalLayout(headline, value);
|
||||
line.setPadding(false);
|
||||
line.setSpacing(true);
|
||||
return line;
|
||||
}
|
||||
|
||||
private VerticalLayout createCard(String title, Span value) {
|
||||
VerticalLayout card = new VerticalLayout();
|
||||
card.setPadding(true);
|
||||
card.setSpacing(false);
|
||||
card.getStyle()
|
||||
.set("border", "1px solid var(--lumo-contrast-20pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-l)")
|
||||
.set("min-width", "180px");
|
||||
|
||||
H3 headline = new H3(title);
|
||||
headline.getStyle().set("margin", "0");
|
||||
value.getStyle().set("font-size", "var(--lumo-font-size-xl)").set("font-weight", "700");
|
||||
card.add(headline, value);
|
||||
return card;
|
||||
}
|
||||
|
||||
private void loadWorkspace() {
|
||||
if (sessionData.getHeadquartersId() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadSummary();
|
||||
loadCustomers();
|
||||
}
|
||||
|
||||
private void loadSummary() {
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> data = restClient.get(
|
||||
"/jobs/invoice-summary?hqId=" + sessionData.getHeadquartersId()
|
||||
+ "&from=" + fromDate.getValue() + "&to=" + toDate.getValue(),
|
||||
LinkedHashMap.class
|
||||
);
|
||||
totalJobs.setText(str(data.get("totalJobs")));
|
||||
billableJobs.setText(str(data.get("billableJobs")));
|
||||
totalRevenue.setText(str(data.get("totalRevenue")));
|
||||
totalCourierCosts.setText(str(data.get("totalCourierCosts")));
|
||||
margin.setText(str(data.get("contributionMargin")));
|
||||
} catch (Exception e) {
|
||||
Notification.show("Rechnungssummary konnte nicht geladen werden.", 4000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadCustomers() {
|
||||
try {
|
||||
List<LinkedHashMap<String, Object>> data = restClient.getList(
|
||||
"/invoices/customers?hqId=" + sessionData.getHeadquartersId()
|
||||
+ "&from=" + fromDate.getValue() + "&to=" + toDate.getValue(),
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
List<CustomerSummaryRow> rows = new ArrayList<>();
|
||||
for (Map<String, Object> row : data) {
|
||||
rows.add(new CustomerSummaryRow(
|
||||
toLong(row.get("customerId")),
|
||||
str(row.get("customerName")),
|
||||
str(row.get("customerEid")),
|
||||
toLong(row.get("invoiceableJobs")) != null ? toLong(row.get("invoiceableJobs")) : 0L,
|
||||
toBigDecimal(row.get("totalRevenue"))
|
||||
));
|
||||
}
|
||||
rows.sort(Comparator.comparing(CustomerSummaryRow::customerName, String.CASE_INSENSITIVE_ORDER));
|
||||
customerGrid.setItems(rows);
|
||||
if (!rows.isEmpty()) {
|
||||
customerGrid.select(rows.get(0));
|
||||
} else {
|
||||
selectedCustomer = null;
|
||||
invoiceGrid.setItems(List.of());
|
||||
costCenterField.setItems(List.of());
|
||||
clearDetail();
|
||||
}
|
||||
updateExportLinks();
|
||||
} catch (Exception e) {
|
||||
Notification.show("Rechnungskunden konnten nicht geladen werden.", 4000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadCostCenters(Long customerId) {
|
||||
try {
|
||||
List<LinkedHashMap<String, Object>> data = restClient.getList(
|
||||
"/customers/" + customerId + "/costcenters/visible",
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
List<CostCenterOption> options = new ArrayList<>();
|
||||
options.add(new CostCenterOption(ALL_COST_CENTERS_ID, "Alle sichtbaren Kostenstellen"));
|
||||
for (Map<String, Object> row : data) {
|
||||
Long id = toLong(row.get("id"));
|
||||
if (id != null && (!sessionData.isCustomerUser() || sessionData.hasCostCenterAccess(id))) {
|
||||
options.add(new CostCenterOption(id, str(row.get("name"))));
|
||||
}
|
||||
}
|
||||
costCenterField.setItems(options);
|
||||
costCenterField.setValue(options.get(0));
|
||||
updateExportLinks();
|
||||
} catch (Exception e) {
|
||||
Notification.show("Kostenstellen konnten nicht geladen werden.", 4000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadInvoiceItems() {
|
||||
if (selectedCustomer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
CostCenterOption selectedCostCenter = costCenterField.getValue();
|
||||
StringBuilder path = new StringBuilder("/invoices/items?hqId=")
|
||||
.append(sessionData.getHeadquartersId())
|
||||
.append("&customerId=").append(selectedCustomer.customerId())
|
||||
.append("&from=").append(fromDate.getValue())
|
||||
.append("&to=").append(toDate.getValue());
|
||||
if (selectedCostCenter != null && selectedCostCenter.id() != ALL_COST_CENTERS_ID) {
|
||||
path.append("&costCenterId=").append(selectedCostCenter.id());
|
||||
}
|
||||
|
||||
List<LinkedHashMap<String, Object>> data = restClient.getList(
|
||||
path.toString(),
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
List<InvoiceRow> rows = new ArrayList<>();
|
||||
for (Map<String, Object> row : data) {
|
||||
rows.add(new InvoiceRow(
|
||||
toLong(row.get("jobId")),
|
||||
toLong(row.get("costCenterId")),
|
||||
str(row.get("costCenterName")),
|
||||
str(row.get("customerName")),
|
||||
str(row.get("orderTime")),
|
||||
str(row.get("finishTime")),
|
||||
str(row.get("courierSid")),
|
||||
str(row.get("tourName")),
|
||||
str(row.get("commissionNo")),
|
||||
str(row.get("pickupName")),
|
||||
str(row.get("deliveryName")),
|
||||
toInteger(row.get("status")),
|
||||
toInteger(row.get("incomplete")),
|
||||
toBigDecimal(row.get("totalPrice")),
|
||||
toBigDecimal(row.get("courierPrice")),
|
||||
toBigDecimal(row.get("margin")),
|
||||
Boolean.TRUE.equals(row.get("exported")) || "true".equalsIgnoreCase(str(row.get("exported"))),
|
||||
blank(row.get("serviceDetails")),
|
||||
blank(row.get("invoiceText"))
|
||||
));
|
||||
}
|
||||
invoiceGrid.setItems(rows);
|
||||
invoiceGrid.asSingleSelect().clear();
|
||||
clearDetail();
|
||||
if (!rows.isEmpty()) {
|
||||
invoiceGrid.select(rows.get(0));
|
||||
}
|
||||
updateExportLinks();
|
||||
} catch (Exception e) {
|
||||
Notification.show("Rechnungsdetails konnten nicht geladen werden.", 4000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateExportLinks() {
|
||||
boolean enabled = selectedCustomer != null && sessionData.canExportInvoices();
|
||||
csvExportButton.setEnabled(enabled);
|
||||
pdfExportButton.setEnabled(enabled);
|
||||
if (!enabled) {
|
||||
csvExportLink.setHref("");
|
||||
pdfExportLink.setHref("");
|
||||
return;
|
||||
}
|
||||
|
||||
csvExportLink.setHref(exportResource("csv"));
|
||||
pdfExportLink.setHref(exportResource("pdf"));
|
||||
}
|
||||
|
||||
private StreamResource exportResource(String format) {
|
||||
String fileName = "invoices_" + selectedCustomer.customerId() + "_" + fromDate.getValue() + "_" + toDate.getValue() + "." + format;
|
||||
return new StreamResource(fileName, () -> new ByteArrayInputStream(restClient.getBytes(buildExportPath(format))));
|
||||
}
|
||||
|
||||
private String buildExportPath(String format) {
|
||||
StringBuilder path = new StringBuilder("/invoices/export/")
|
||||
.append(format)
|
||||
.append("?hqId=").append(sessionData.getHeadquartersId())
|
||||
.append("&customerId=").append(selectedCustomer.customerId())
|
||||
.append("&from=").append(fromDate.getValue())
|
||||
.append("&to=").append(toDate.getValue());
|
||||
CostCenterOption selectedCostCenter = costCenterField.getValue();
|
||||
if (selectedCostCenter != null && selectedCostCenter.id() != ALL_COST_CENTERS_ID) {
|
||||
path.append("&costCenterId=").append(selectedCostCenter.id());
|
||||
}
|
||||
return path.toString();
|
||||
}
|
||||
|
||||
private void updateDetail() {
|
||||
if (selectedInvoice == null) {
|
||||
clearDetail();
|
||||
return;
|
||||
}
|
||||
|
||||
detailJob.setText(str(selectedInvoice.jobId()));
|
||||
detailCustomer.setText(selectedInvoice.customerName());
|
||||
detailCostCenter.setText(selectedInvoice.costCenterName());
|
||||
detailTimes.setText(formatDateTime(selectedInvoice.orderTime()) + " / " + formatDateTime(selectedInvoice.finishTime()));
|
||||
detailRoute.setText(selectedInvoice.pickupName() + " -> " + selectedInvoice.deliveryName());
|
||||
detailCourier.setText(str(selectedInvoice.courierSid()));
|
||||
detailStatus.setText(statusLabel(selectedInvoice.status(), selectedInvoice.incomplete(), selectedInvoice.exported()));
|
||||
serviceDetails.setText(selectedInvoice.serviceDetails().isBlank() ? "(keine Leistungen)" : selectedInvoice.serviceDetails());
|
||||
invoiceText.setText(selectedInvoice.invoiceText().isBlank() ? "(kein Rechnungstext)" : selectedInvoice.invoiceText());
|
||||
openJobButton.setEnabled(sessionData.canViewJobs());
|
||||
}
|
||||
|
||||
private void clearDetail() {
|
||||
detailJob.setText("-");
|
||||
detailCustomer.setText("-");
|
||||
detailCostCenter.setText("-");
|
||||
detailTimes.setText("-");
|
||||
detailRoute.setText("-");
|
||||
detailCourier.setText("-");
|
||||
detailStatus.setText("-");
|
||||
serviceDetails.setText("(keine Leistungen)");
|
||||
invoiceText.setText("(kein Rechnungsfall ausgewaehlt)");
|
||||
openJobButton.setEnabled(false);
|
||||
}
|
||||
|
||||
private void configureTextBlock(Pre block) {
|
||||
block.setWidthFull();
|
||||
block.getStyle()
|
||||
.set("background", "var(--lumo-contrast-5pct)")
|
||||
.set("padding", "var(--lumo-space-m)")
|
||||
.set("border-radius", "var(--lumo-border-radius-m)")
|
||||
.set("white-space", "pre-wrap")
|
||||
.set("margin", "0");
|
||||
}
|
||||
|
||||
private String statusLabel(Integer status, Integer incomplete, boolean exported) {
|
||||
String base = switch (status != null ? status : -1) {
|
||||
case 0 -> "Blockiert";
|
||||
case 1 -> "Zugewiesen";
|
||||
case 2 -> "In Zustellung";
|
||||
case 8 -> "Zugestellt";
|
||||
case 9 -> "Abgeschlossen";
|
||||
case 10 -> "Storniert";
|
||||
default -> status != null ? String.valueOf(status) : "-";
|
||||
};
|
||||
if (Boolean.TRUE.equals(exported)) {
|
||||
return base + ", exportiert";
|
||||
}
|
||||
if (incomplete != null && incomplete > 0) {
|
||||
return base + ", unvollstaendig";
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
private String formatDateTime(String value) {
|
||||
return value == null || value.isBlank() ? "-" : value.replace("T", " ");
|
||||
}
|
||||
|
||||
private String formatAmount(BigDecimal value) {
|
||||
return value != null ? value.toPlainString() : "-";
|
||||
}
|
||||
|
||||
private BigDecimal toBigDecimal(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof BigDecimal decimal) {
|
||||
return decimal;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(value.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Long toLong(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(value.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Integer toInteger(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(value.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String str(Object value) {
|
||||
return value != null ? value.toString() : "-";
|
||||
}
|
||||
|
||||
private String blank(Object value) {
|
||||
return value != null ? value.toString() : "";
|
||||
}
|
||||
|
||||
private record CustomerSummaryRow(Long customerId, String customerName, String customerEid,
|
||||
long invoiceableJobs, BigDecimal totalRevenue) { }
|
||||
|
||||
private record CostCenterOption(Long id, String label) { }
|
||||
|
||||
private record InvoiceRow(Long jobId, Long costCenterId, String costCenterName, String customerName,
|
||||
String orderTime, String finishTime, String courierSid, String tourName,
|
||||
String commissionNo, String pickupName, String deliveryName,
|
||||
Integer status, Integer incomplete, BigDecimal totalPrice,
|
||||
BigDecimal courierPrice, BigDecimal margin, boolean exported,
|
||||
String serviceDetails, String invoiceText) { }
|
||||
}
|
||||
369
vaadin/src/main/java/de/votian/web/view/JobBatchView.java
Normal file
369
vaadin/src/main/java/de/votian/web/view/JobBatchView.java
Normal file
@@ -0,0 +1,369 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.datetimepicker.DateTimePicker;
|
||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.html.Paragraph;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.textfield.NumberField;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.votian.web.model.SessionData;
|
||||
import de.votian.web.service.RestClientService;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Route(value = "jobs/batch", layout = MainLayout.class)
|
||||
@PageTitle("Votian - Listenerfassung")
|
||||
public class JobBatchView extends VerticalLayout {
|
||||
|
||||
private final RestClientService restClient;
|
||||
private final SessionData sessionData;
|
||||
|
||||
private final ComboBox<CustomerOption> customerField = new ComboBox<>("Kunde");
|
||||
private final ComboBox<CostCenterOption> costCenterField = new ComboBox<>("Kostenstelle");
|
||||
private final ComboBox<CourierOption> defaultCourierField = new ComboBox<>("Standard-Kurier");
|
||||
private final VerticalLayout rowsLayout = new VerticalLayout();
|
||||
private final List<BatchRowEditor> rowEditors = new ArrayList<>();
|
||||
|
||||
public JobBatchView(RestClientService restClient, SessionData sessionData) {
|
||||
this.restClient = restClient;
|
||||
this.sessionData = sessionData;
|
||||
|
||||
setSizeFull();
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
|
||||
customerField.setItemLabelGenerator(CustomerOption::label);
|
||||
customerField.setWidth("360px");
|
||||
customerField.addValueChangeListener(event -> loadCostCenters(event.getValue() != null ? event.getValue().id() : null));
|
||||
|
||||
costCenterField.setItemLabelGenerator(CostCenterOption::label);
|
||||
costCenterField.setWidth("360px");
|
||||
|
||||
defaultCourierField.setItemLabelGenerator(CourierOption::label);
|
||||
defaultCourierField.setWidth("260px");
|
||||
|
||||
rowsLayout.setPadding(false);
|
||||
rowsLayout.setSpacing(true);
|
||||
rowsLayout.setWidthFull();
|
||||
|
||||
Button addRowButton = new Button("Zeile hinzufuegen", event -> addRow(null));
|
||||
Button saveButton = new Button("Liste buchen", event -> saveBatch());
|
||||
saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
Button cancelButton = new Button("Zurueck", event -> getUI().ifPresent(ui -> ui.navigate(JobListView.class)));
|
||||
|
||||
HorizontalLayout actions = new HorizontalLayout(addRowButton, saveButton, cancelButton);
|
||||
|
||||
add(
|
||||
new H3("Listenerfassung"),
|
||||
new Paragraph("Erzeugt mehrere abgeschlossene Listenbuchungen fuer einen Kunden und eine Kostenstelle."),
|
||||
new HorizontalLayout(customerField, costCenterField, defaultCourierField),
|
||||
rowsLayout,
|
||||
actions
|
||||
);
|
||||
|
||||
loadCustomers();
|
||||
loadCouriers();
|
||||
addInitialRows();
|
||||
}
|
||||
|
||||
private void loadCustomers() {
|
||||
try {
|
||||
List<CustomerOption> customers = new ArrayList<>();
|
||||
if (sessionData.isCustomerUser() && sessionData.getCustomerId() != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> customer = restClient.get("/customers/" + sessionData.getCustomerId(), LinkedHashMap.class);
|
||||
customers.add(new CustomerOption(
|
||||
sessionData.getCustomerId(),
|
||||
str(customer.get("companyName")).isBlank() ? str(customer.get("eid")) : str(customer.get("companyName"))
|
||||
));
|
||||
} else {
|
||||
List<LinkedHashMap<String, Object>> data = restClient.getList(
|
||||
"/customers/hq/" + sessionData.getHeadquartersId(),
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
for (Map<String, Object> row : data) {
|
||||
customers.add(new CustomerOption(
|
||||
toLong(row.get("id")),
|
||||
str(row.get("companyName")).isBlank() ? str(row.get("eid")) : str(row.get("companyName"))
|
||||
));
|
||||
}
|
||||
}
|
||||
customerField.setItems(customers);
|
||||
if (!customers.isEmpty()) {
|
||||
customerField.setValue(customers.get(0));
|
||||
}
|
||||
customerField.setEnabled(!sessionData.isCustomerUser());
|
||||
} catch (Exception e) {
|
||||
Notification.show("Kunden konnten nicht geladen werden.", 4000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadCostCenters(Long customerId) {
|
||||
if (customerId == null) {
|
||||
costCenterField.clear();
|
||||
costCenterField.setItems(List.of());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
List<LinkedHashMap<String, Object>> data = restClient.getList(
|
||||
"/customers/" + customerId + "/costcenters/visible",
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
List<CostCenterOption> options = new ArrayList<>();
|
||||
for (Map<String, Object> row : data) {
|
||||
options.add(new CostCenterOption(
|
||||
toLong(row.get("id")),
|
||||
str(row.get("path")).isBlank()
|
||||
? str(row.get("name"))
|
||||
: str(row.get("path")) + " / " + str(row.get("name"))
|
||||
));
|
||||
}
|
||||
costCenterField.setItems(options);
|
||||
if (!options.isEmpty()) {
|
||||
costCenterField.setValue(options.get(0));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Notification.show("Kostenstellen konnten nicht geladen werden.", 4000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadCouriers() {
|
||||
try {
|
||||
List<LinkedHashMap<String, Object>> data = restClient.getList(
|
||||
"/couriers/hq/" + sessionData.getHeadquartersId(),
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
List<CourierOption> options = new ArrayList<>();
|
||||
options.add(new CourierOption(null, "Keine Zuordnung"));
|
||||
for (Map<String, Object> row : data) {
|
||||
options.add(new CourierOption(
|
||||
toLong(row.get("id")),
|
||||
str(row.get("sid")).isBlank() ? str(row.get("eid")) : str(row.get("sid"))
|
||||
));
|
||||
}
|
||||
defaultCourierField.setItems(options);
|
||||
defaultCourierField.setValue(options.get(0));
|
||||
} catch (Exception e) {
|
||||
Notification.show("Kuriere konnten nicht geladen werden.", 4000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void addInitialRows() {
|
||||
if (rowEditors.isEmpty()) {
|
||||
addRow(null);
|
||||
addRow(null);
|
||||
addRow(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void addRow(BatchRowState initialState) {
|
||||
BatchRowEditor editor = new BatchRowEditor(initialState);
|
||||
rowEditors.add(editor);
|
||||
rowsLayout.add(editor);
|
||||
}
|
||||
|
||||
private void removeRow(BatchRowEditor editor) {
|
||||
if (rowEditors.size() == 1) {
|
||||
editor.clear();
|
||||
return;
|
||||
}
|
||||
rowEditors.remove(editor);
|
||||
rowsLayout.remove(editor);
|
||||
}
|
||||
|
||||
private void saveBatch() {
|
||||
if (customerField.getValue() == null || costCenterField.getValue() == null) {
|
||||
Notification.show("Bitte Kunde und Kostenstelle waehlen.", 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
for (BatchRowEditor editor : rowEditors) {
|
||||
if (!editor.hasContent()) {
|
||||
continue;
|
||||
}
|
||||
if (!editor.isValid()) {
|
||||
Notification.show("Bitte alle ausgefuellten Zeilen vollstaendig erfassen.",
|
||||
4000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
return;
|
||||
}
|
||||
rows.add(editor.toRequest(defaultCourierField.getValue()));
|
||||
}
|
||||
|
||||
if (rows.isEmpty()) {
|
||||
Notification.show("Bitte mindestens eine Zeile erfassen.", 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, Object> request = new LinkedHashMap<>();
|
||||
request.put("hqId", sessionData.getHeadquartersId());
|
||||
request.put("customerId", customerField.getValue().id());
|
||||
request.put("costCenterId", costCenterField.getValue().id());
|
||||
request.put("rows", rows);
|
||||
restClient.post("/jobs/batch", request, Object[].class);
|
||||
Notification.show("Listenerfassung gespeichert.", 3000, Notification.Position.BOTTOM_START);
|
||||
getUI().ifPresent(ui -> ui.navigate(JobListView.class));
|
||||
} catch (Exception e) {
|
||||
Notification.show("Listenerfassung konnte nicht gespeichert werden: " + e.getMessage(),
|
||||
5000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private Long toLong(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(value.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String str(Object value) {
|
||||
return value != null ? value.toString() : "";
|
||||
}
|
||||
|
||||
private record CustomerOption(Long id, String label) {
|
||||
}
|
||||
|
||||
private record CostCenterOption(Long id, String label) {
|
||||
}
|
||||
|
||||
private record CourierOption(Long id, String label) {
|
||||
}
|
||||
|
||||
private record BatchRowState(LocalDateTime orderTime,
|
||||
String description,
|
||||
String commissionNo,
|
||||
Double totalPrice,
|
||||
Double courierPrice,
|
||||
Long courierId) {
|
||||
}
|
||||
|
||||
private class BatchRowEditor extends VerticalLayout {
|
||||
|
||||
private final DateTimePicker orderTimeField = new DateTimePicker("Zeit");
|
||||
private final TextField descriptionField = new TextField("Auftrag / Tourenverlauf");
|
||||
private final TextField commissionField = new TextField("Kst./Referenz");
|
||||
private final NumberField totalPriceField = new NumberField("Fuhrbetrag");
|
||||
private final NumberField courierPriceField = new NumberField("Kurierpreis");
|
||||
private final ComboBox<CourierOption> courierField = new ComboBox<>("Kurier");
|
||||
|
||||
private BatchRowEditor(BatchRowState initialState) {
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
getStyle()
|
||||
.set("border", "1px solid var(--lumo-contrast-20pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-l)");
|
||||
|
||||
orderTimeField.setWidth("260px");
|
||||
orderTimeField.setValue(initialState != null && initialState.orderTime() != null
|
||||
? initialState.orderTime()
|
||||
: LocalDateTime.now().withSecond(0).withNano(0));
|
||||
|
||||
descriptionField.setWidthFull();
|
||||
commissionField.setWidth("220px");
|
||||
totalPriceField.setWidth("180px");
|
||||
courierPriceField.setWidth("180px");
|
||||
|
||||
courierField.setItems(defaultCourierField.getListDataView().getItems().toList());
|
||||
courierField.setItemLabelGenerator(CourierOption::label);
|
||||
courierField.setWidth("220px");
|
||||
courierField.setValue(resolveCourier(initialState != null ? initialState.courierId() : null));
|
||||
|
||||
if (initialState != null) {
|
||||
descriptionField.setValue(initialState.description() != null ? initialState.description() : "");
|
||||
commissionField.setValue(initialState.commissionNo() != null ? initialState.commissionNo() : "");
|
||||
totalPriceField.setValue(initialState.totalPrice());
|
||||
courierPriceField.setValue(initialState.courierPrice());
|
||||
}
|
||||
|
||||
Button removeButton = new Button("Zeile entfernen", event -> removeRow(this));
|
||||
removeButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
|
||||
|
||||
FormLayout form = new FormLayout();
|
||||
form.setWidthFull();
|
||||
form.setResponsiveSteps(
|
||||
new FormLayout.ResponsiveStep("0", 1),
|
||||
new FormLayout.ResponsiveStep("720px", 2),
|
||||
new FormLayout.ResponsiveStep("1080px", 3)
|
||||
);
|
||||
form.add(orderTimeField, descriptionField, commissionField, totalPriceField, courierPriceField, courierField);
|
||||
|
||||
add(form, removeButton);
|
||||
}
|
||||
|
||||
private void clear() {
|
||||
descriptionField.clear();
|
||||
commissionField.clear();
|
||||
totalPriceField.clear();
|
||||
courierPriceField.clear();
|
||||
courierField.setValue(resolveCourier(null));
|
||||
}
|
||||
|
||||
private boolean hasContent() {
|
||||
return !descriptionField.getValue().isBlank()
|
||||
|| !commissionField.getValue().isBlank()
|
||||
|| totalPriceField.getValue() != null
|
||||
|| courierPriceField.getValue() != null
|
||||
|| (courierField.getValue() != null && courierField.getValue().id() != null);
|
||||
}
|
||||
|
||||
private boolean isValid() {
|
||||
return orderTimeField.getValue() != null
|
||||
&& !descriptionField.getValue().isBlank()
|
||||
&& totalPriceField.getValue() != null;
|
||||
}
|
||||
|
||||
private Map<String, Object> toRequest(CourierOption defaultCourier) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("orderTime", orderTimeField.getValue().toString());
|
||||
row.put("description", descriptionField.getValue());
|
||||
row.put("commissionNo", commissionField.getValue());
|
||||
row.put("totalPrice", decimal(totalPriceField.getValue()));
|
||||
row.put("courierPrice", decimal(courierPriceField.getValue()));
|
||||
CourierOption selectedCourier = courierField.getValue() != null ? courierField.getValue() : defaultCourier;
|
||||
row.put("courierId", selectedCourier != null ? selectedCourier.id() : null);
|
||||
return row;
|
||||
}
|
||||
|
||||
private CourierOption resolveCourier(Long courierId) {
|
||||
return defaultCourierField.getListDataView().getItems()
|
||||
.filter(option -> option.id() == null ? courierId == null : option.id().equals(courierId))
|
||||
.findFirst()
|
||||
.orElse(defaultCourierField.getListDataView().getItems().findFirst().orElse(null));
|
||||
}
|
||||
|
||||
private BigDecimal decimal(Double value) {
|
||||
return value != null ? BigDecimal.valueOf(value) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
2115
vaadin/src/main/java/de/votian/web/view/JobDetailView.java
Normal file
2115
vaadin/src/main/java/de/votian/web/view/JobDetailView.java
Normal file
File diff suppressed because it is too large
Load Diff
438
vaadin/src/main/java/de/votian/web/view/JobListView.java
Normal file
438
vaadin/src/main/java/de/votian/web/view/JobListView.java
Normal file
@@ -0,0 +1,438 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.datepicker.DatePicker;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.html.Anchor;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.server.StreamResource;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.data.value.ValueChangeMode;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.votian.web.model.SessionData;
|
||||
import de.votian.web.service.RestClientService;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Route(value = "jobs", layout = MainLayout.class)
|
||||
@PageTitle("Votian - Auftraege")
|
||||
public class JobListView extends VerticalLayout {
|
||||
|
||||
private final RestClientService restClient;
|
||||
private final SessionData sessionData;
|
||||
|
||||
private final TextField searchField = new TextField();
|
||||
private final DatePicker fromDate = new DatePicker("Von");
|
||||
private final DatePicker toDate = new DatePicker("Bis");
|
||||
private final ComboBox<StatusOption> statusField = new ComboBox<>("Status");
|
||||
private final ComboBox<ExportOption> exportField = new ComboBox<>("Export");
|
||||
private final ComboBox<CostCenterOption> costCenterField = new ComboBox<>("Kostenstelle");
|
||||
private final TextField courierField = new TextField("Kurier");
|
||||
private final Anchor csvExportLink = new Anchor();
|
||||
private final Button csvExportButton = new Button("CSV exportieren");
|
||||
|
||||
private final Grid<JobRow> grid = new Grid<>(JobRow.class, false);
|
||||
|
||||
public JobListView(RestClientService restClient, SessionData sessionData) {
|
||||
this.restClient = restClient;
|
||||
this.sessionData = sessionData;
|
||||
|
||||
setSizeFull();
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
|
||||
configureFilters();
|
||||
configureGrid();
|
||||
configureExportLink();
|
||||
|
||||
Button searchButton = new Button("Suchen", VaadinIcon.SEARCH.create(), event -> {
|
||||
updateExportLink();
|
||||
loadJobs();
|
||||
});
|
||||
searchButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
|
||||
Button resetButton = new Button("Zuruecksetzen", event -> resetFilters());
|
||||
Button newButton = new Button("Neuer Auftrag", VaadinIcon.PLUS.create(),
|
||||
event -> getUI().ifPresent(ui -> ui.navigate(JobDetailView.class)));
|
||||
newButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
newButton.setVisible(sessionData.canManageJobs());
|
||||
|
||||
Button batchButton = new Button("Listenerfassung", VaadinIcon.TABLE.create(),
|
||||
event -> getUI().ifPresent(ui -> ui.navigate(JobBatchView.class)));
|
||||
batchButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
batchButton.setVisible(sessionData.canBatchJobs());
|
||||
|
||||
HorizontalLayout toolbar = new HorizontalLayout(
|
||||
searchField,
|
||||
fromDate,
|
||||
toDate,
|
||||
statusField,
|
||||
exportField,
|
||||
costCenterField,
|
||||
courierField,
|
||||
csvExportLink,
|
||||
searchButton,
|
||||
resetButton,
|
||||
newButton,
|
||||
batchButton
|
||||
);
|
||||
toolbar.setAlignItems(Alignment.BASELINE);
|
||||
toolbar.setWidthFull();
|
||||
|
||||
add(toolbar, grid);
|
||||
expand(grid);
|
||||
|
||||
loadCostCenters();
|
||||
loadJobs();
|
||||
}
|
||||
|
||||
private void configureFilters() {
|
||||
searchField.setPlaceholder("Auftrag, Kommission, Kunde ...");
|
||||
searchField.setPrefixComponent(VaadinIcon.SEARCH.create());
|
||||
searchField.setValueChangeMode(ValueChangeMode.LAZY);
|
||||
searchField.setWidth("260px");
|
||||
searchField.addValueChangeListener(event -> {
|
||||
updateExportLink();
|
||||
loadJobs();
|
||||
});
|
||||
|
||||
fromDate.setValue(LocalDate.now());
|
||||
fromDate.addValueChangeListener(event -> updateExportLink());
|
||||
toDate.setValue(LocalDate.now());
|
||||
toDate.addValueChangeListener(event -> updateExportLink());
|
||||
|
||||
statusField.setItems(StatusOption.values());
|
||||
statusField.setItemLabelGenerator(StatusOption::label);
|
||||
statusField.setValue(StatusOption.ALL);
|
||||
statusField.setWidth("170px");
|
||||
statusField.addValueChangeListener(event -> updateExportLink());
|
||||
|
||||
exportField.setItems(ExportOption.values());
|
||||
exportField.setItemLabelGenerator(ExportOption::label);
|
||||
exportField.setValue(ExportOption.ALL);
|
||||
exportField.setWidth("160px");
|
||||
exportField.addValueChangeListener(event -> updateExportLink());
|
||||
|
||||
costCenterField.setItemLabelGenerator(CostCenterOption::label);
|
||||
costCenterField.setWidth("260px");
|
||||
costCenterField.addValueChangeListener(event -> updateExportLink());
|
||||
|
||||
courierField.setWidth("180px");
|
||||
courierField.setValueChangeMode(ValueChangeMode.LAZY);
|
||||
courierField.addValueChangeListener(event -> updateExportLink());
|
||||
}
|
||||
|
||||
private void configureExportLink() {
|
||||
csvExportButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
csvExportLink.add(csvExportButton);
|
||||
csvExportLink.getElement().setAttribute("download", true);
|
||||
csvExportLink.setVisible(sessionData.canExportJobs());
|
||||
updateExportLink();
|
||||
}
|
||||
|
||||
private void configureGrid() {
|
||||
grid.addColumn(JobRow::id).setHeader("Auftrag").setSortable(true).setAutoWidth(true).setFrozen(true);
|
||||
grid.addColumn(row -> statusLabel(row.status())).setHeader("Status").setAutoWidth(true);
|
||||
grid.addColumn(row -> formatDateTime(row.orderTime())).setHeader("Auftragszeit").setSortable(true).setAutoWidth(true);
|
||||
grid.addColumn(JobRow::customerName).setHeader("Kunde").setFlexGrow(1);
|
||||
grid.addColumn(JobRow::costCenterName).setHeader("Kostenstelle").setAutoWidth(true);
|
||||
grid.addColumn(JobRow::commissionNo).setHeader("Kommission").setAutoWidth(true);
|
||||
grid.addColumn(JobRow::pickupName).setHeader("Abholung").setFlexGrow(1);
|
||||
grid.addColumn(JobRow::deliveryName).setHeader("Zustellung").setFlexGrow(1);
|
||||
grid.addColumn(JobRow::courierSid).setHeader("Kurier SID").setAutoWidth(true);
|
||||
grid.addColumn(row -> formatAmount(row.totalPrice())).setHeader("Gesamtpreis").setAutoWidth(true);
|
||||
grid.addColumn(row -> row.exported() ? "Ja" : "Nein").setHeader("Export").setAutoWidth(true);
|
||||
grid.addColumn(row -> vehicleLabel(row.vehicleTypeId())).setHeader("Fahrzeug").setAutoWidth(true);
|
||||
grid.setSizeFull();
|
||||
|
||||
grid.addItemClickListener(event -> {
|
||||
if (sessionData.canViewJobs()) {
|
||||
getUI().ifPresent(ui -> ui.navigate("jobs/" + event.getItem().id()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadCostCenters() {
|
||||
try {
|
||||
List<LinkedHashMap<String, Object>> data = restClient.getList(
|
||||
"/jobs/filter-costcenters?hqId=" + sessionData.getHeadquartersId(),
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
List<CostCenterOption> options = new ArrayList<>();
|
||||
options.add(new CostCenterOption(null, "Alle Kostenstellen"));
|
||||
for (Map<String, Object> row : data) {
|
||||
options.add(new CostCenterOption(
|
||||
toLong(row.get("id")),
|
||||
str(row.get("path")).isBlank()
|
||||
? str(row.get("name"))
|
||||
: str(row.get("path")) + " / " + str(row.get("name"))
|
||||
));
|
||||
}
|
||||
costCenterField.setItems(options);
|
||||
costCenterField.setValue(options.get(0));
|
||||
updateExportLink();
|
||||
} catch (Exception e) {
|
||||
Notification.show("Kostenstellenfilter konnte nicht geladen werden.", 4000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadJobs() {
|
||||
if (sessionData.getHeadquartersId() == null || fromDate.getValue() == null || toDate.getValue() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
StringBuilder path = new StringBuilder("/jobs/list?hqId=")
|
||||
.append(sessionData.getHeadquartersId())
|
||||
.append("&from=").append(fromDate.getValue())
|
||||
.append("&to=").append(toDate.getValue());
|
||||
|
||||
if (!searchField.getValue().isBlank()) {
|
||||
path.append("&term=").append(encode(searchField.getValue()));
|
||||
}
|
||||
if (statusField.getValue() != null && statusField.getValue().value() != null) {
|
||||
path.append("&status=").append(statusField.getValue().value());
|
||||
}
|
||||
if (costCenterField.getValue() != null && costCenterField.getValue().id() != null) {
|
||||
path.append("&costCenterId=").append(costCenterField.getValue().id());
|
||||
}
|
||||
if (!courierField.getValue().isBlank()) {
|
||||
path.append("&courierSid=").append(encode(courierField.getValue()));
|
||||
}
|
||||
if (exportField.getValue() != null && exportField.getValue().value() != null) {
|
||||
path.append("&exported=").append(exportField.getValue().value());
|
||||
}
|
||||
|
||||
List<LinkedHashMap<String, Object>> data = restClient.getList(
|
||||
path.toString(),
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
List<JobRow> rows = new ArrayList<>();
|
||||
for (Map<String, Object> row : data) {
|
||||
rows.add(new JobRow(
|
||||
toLong(row.get("id")),
|
||||
toInteger(row.get("status")),
|
||||
str(row.get("orderTime")),
|
||||
str(row.get("customerName")),
|
||||
str(row.get("costCenterName")),
|
||||
str(row.get("commissionNo")),
|
||||
str(row.get("pickupName")),
|
||||
str(row.get("deliveryName")),
|
||||
str(row.get("courierSid")),
|
||||
toInteger(row.get("vehicleTypeId")),
|
||||
toBigDecimal(row.get("totalPrice")),
|
||||
Boolean.TRUE.equals(row.get("exported")) || "true".equalsIgnoreCase(str(row.get("exported")))
|
||||
));
|
||||
}
|
||||
grid.setItems(rows);
|
||||
} catch (Exception e) {
|
||||
Notification.show("Auftragsliste konnte nicht geladen werden: " + e.getMessage(),
|
||||
5000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void resetFilters() {
|
||||
searchField.clear();
|
||||
fromDate.setValue(LocalDate.now());
|
||||
toDate.setValue(LocalDate.now());
|
||||
statusField.setValue(StatusOption.ALL);
|
||||
exportField.setValue(ExportOption.ALL);
|
||||
if (!costCenterField.getListDataView().getItems().toList().isEmpty()) {
|
||||
costCenterField.setValue(costCenterField.getListDataView().getItems().findFirst().orElse(null));
|
||||
}
|
||||
courierField.clear();
|
||||
updateExportLink();
|
||||
loadJobs();
|
||||
}
|
||||
|
||||
private void updateExportLink() {
|
||||
boolean enabled = sessionData.canExportJobs()
|
||||
&& sessionData.getHeadquartersId() != null
|
||||
&& fromDate.getValue() != null
|
||||
&& toDate.getValue() != null;
|
||||
csvExportButton.setEnabled(enabled);
|
||||
if (!enabled) {
|
||||
csvExportLink.setHref("");
|
||||
return;
|
||||
}
|
||||
|
||||
StreamResource csv = new StreamResource(
|
||||
"jobs_" + fromDate.getValue() + "_" + toDate.getValue() + ".csv",
|
||||
() -> new ByteArrayInputStream(restClient.getBytes(buildExportPath()))
|
||||
);
|
||||
csvExportLink.setHref(csv);
|
||||
}
|
||||
|
||||
private String buildExportPath() {
|
||||
StringBuilder path = new StringBuilder("/jobs/export/csv?hqId=")
|
||||
.append(sessionData.getHeadquartersId())
|
||||
.append("&from=").append(fromDate.getValue())
|
||||
.append("&to=").append(toDate.getValue());
|
||||
|
||||
if (!searchField.getValue().isBlank()) {
|
||||
path.append("&term=").append(encode(searchField.getValue()));
|
||||
}
|
||||
if (statusField.getValue() != null && statusField.getValue().value() != null) {
|
||||
path.append("&status=").append(statusField.getValue().value());
|
||||
}
|
||||
if (costCenterField.getValue() != null && costCenterField.getValue().id() != null) {
|
||||
path.append("&costCenterId=").append(costCenterField.getValue().id());
|
||||
}
|
||||
if (!courierField.getValue().isBlank()) {
|
||||
path.append("&courierSid=").append(encode(courierField.getValue()));
|
||||
}
|
||||
if (exportField.getValue() != null && exportField.getValue().value() != null) {
|
||||
path.append("&exported=").append(exportField.getValue().value());
|
||||
}
|
||||
return path.toString();
|
||||
}
|
||||
|
||||
private String encode(String value) {
|
||||
return java.net.URLEncoder.encode(value, java.nio.charset.StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private String statusLabel(Integer status) {
|
||||
if (status == null) {
|
||||
return "";
|
||||
}
|
||||
return switch (status) {
|
||||
case 0 -> "Blockiert";
|
||||
case 1 -> "Zugewiesen";
|
||||
case 2 -> "In Zustellung";
|
||||
case 8 -> "Zugestellt";
|
||||
case 9 -> "Abgeschlossen";
|
||||
case 10 -> "Storniert";
|
||||
default -> String.valueOf(status);
|
||||
};
|
||||
}
|
||||
|
||||
private String vehicleLabel(Integer vehicleTypeId) {
|
||||
return vehicleTypeId != null ? "Typ " + vehicleTypeId : "";
|
||||
}
|
||||
|
||||
private String formatDateTime(String value) {
|
||||
return value == null || value.isBlank() ? "" : value.replace("T", " ");
|
||||
}
|
||||
|
||||
private String formatAmount(BigDecimal value) {
|
||||
return value != null ? value.toPlainString() : "";
|
||||
}
|
||||
|
||||
private Long toLong(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(value.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Integer toInteger(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(value.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal toBigDecimal(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof BigDecimal decimal) {
|
||||
return decimal;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(value.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String str(Object value) {
|
||||
return value != null ? value.toString() : "";
|
||||
}
|
||||
|
||||
private record JobRow(Long id, Integer status, String orderTime, String customerName, String costCenterName,
|
||||
String commissionNo, String pickupName, String deliveryName, String courierSid,
|
||||
Integer vehicleTypeId, BigDecimal totalPrice, boolean exported) {
|
||||
}
|
||||
|
||||
private record CostCenterOption(Long id, String label) {
|
||||
}
|
||||
|
||||
private enum StatusOption {
|
||||
ALL("Alle", null),
|
||||
BLOCKED("Blockiert", 0),
|
||||
ASSIGNED("Zugewiesen", 1),
|
||||
ON_DELIVERY("In Zustellung", 2),
|
||||
DELIVERED("Zugestellt", 8),
|
||||
COMPLETED("Abgeschlossen", 9),
|
||||
CANCELLED("Storniert", 10);
|
||||
|
||||
private final String label;
|
||||
private final Integer value;
|
||||
|
||||
StatusOption(String label, Integer value) {
|
||||
this.label = label;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
private String label() {
|
||||
return label;
|
||||
}
|
||||
|
||||
private Integer value() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
private enum ExportOption {
|
||||
ALL("Alle", null),
|
||||
NOT_EXPORTED("Nicht exportiert", false),
|
||||
EXPORTED("Exportiert", true);
|
||||
|
||||
private final String label;
|
||||
private final Boolean value;
|
||||
|
||||
ExportOption(String label, Boolean value) {
|
||||
this.label = label;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
private String label() {
|
||||
return label;
|
||||
}
|
||||
|
||||
private Boolean value() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
220
vaadin/src/main/java/de/votian/web/view/LoginView.java
Normal file
220
vaadin/src/main/java/de/votian/web/view/LoginView.java
Normal file
@@ -0,0 +1,220 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.Key;
|
||||
import com.vaadin.flow.component.UI;
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.html.H2;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.textfield.IntegerField;
|
||||
import com.vaadin.flow.component.textfield.PasswordField;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.votian.web.model.SessionData;
|
||||
import de.votian.web.service.ParameterProvider;
|
||||
import de.votian.web.service.RestClientService;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Route("login")
|
||||
@PageTitle("Votian - Login")
|
||||
public class LoginView extends VerticalLayout {
|
||||
|
||||
private final RestClientService restClient;
|
||||
private final SessionData sessionData;
|
||||
private final ParameterProvider parameterProvider;
|
||||
|
||||
private final TextField accountField = new TextField("Benutzerkonto");
|
||||
private final PasswordField passwordField = new PasswordField("Passwort");
|
||||
private final IntegerField totpField = new IntegerField("TOTP-Code");
|
||||
private final Button loginButton = new Button("Anmelden", event -> submit());
|
||||
|
||||
public LoginView(RestClientService restClient, SessionData sessionData, ParameterProvider parameterProvider) {
|
||||
this.restClient = restClient;
|
||||
this.sessionData = sessionData;
|
||||
this.parameterProvider = parameterProvider;
|
||||
|
||||
if (sessionData.isAuthenticated()) {
|
||||
UI.getCurrent().navigate(DashboardView.class);
|
||||
return;
|
||||
}
|
||||
|
||||
setSizeFull();
|
||||
setAlignItems(FlexComponent.Alignment.CENTER);
|
||||
setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
|
||||
|
||||
VerticalLayout loginForm = new VerticalLayout();
|
||||
loginForm.setWidth("400px");
|
||||
loginForm.setPadding(true);
|
||||
loginForm.setSpacing(true);
|
||||
loginForm.setAlignItems(FlexComponent.Alignment.CENTER);
|
||||
loginForm.getStyle()
|
||||
.set("border", "1px solid var(--lumo-contrast-20pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-l)")
|
||||
.set("background", "var(--lumo-base-color)")
|
||||
.set("box-shadow", "var(--lumo-box-shadow-m)");
|
||||
|
||||
H2 title = new H2("Stadtbote GmbH");
|
||||
title.getStyle().set("color", "#1b12b9");
|
||||
|
||||
accountField.setWidthFull();
|
||||
accountField.setAutofocus(true);
|
||||
|
||||
passwordField.setWidthFull();
|
||||
|
||||
totpField.setWidthFull();
|
||||
totpField.setMin(0);
|
||||
totpField.setMax(999999);
|
||||
totpField.setVisible(false);
|
||||
|
||||
loginButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
loginButton.setWidthFull();
|
||||
loginButton.addClickShortcut(Key.ENTER);
|
||||
|
||||
loginForm.add(title, accountField, passwordField, totpField, loginButton);
|
||||
add(loginForm);
|
||||
}
|
||||
|
||||
private void submit() {
|
||||
if (sessionData.getPendingTotpToken() != null && totpField.isVisible()) {
|
||||
verifyTotp();
|
||||
} else {
|
||||
login();
|
||||
}
|
||||
}
|
||||
|
||||
private void login() {
|
||||
String account = accountField.getValue().trim();
|
||||
String password = passwordField.getValue();
|
||||
if (account.isEmpty() || password.isEmpty()) {
|
||||
showError("Bitte Benutzerkonto und Passwort eingeben.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, Object> request = new LinkedHashMap<>();
|
||||
request.put("account", account);
|
||||
request.put("password", password);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> response = restClient.post("/auth/login", request, LinkedHashMap.class);
|
||||
handleAuthResponse(response);
|
||||
} catch (Exception e) {
|
||||
showError("Anmeldung fehlgeschlagen. Bitte pruefen Sie Ihre Eingaben.");
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyTotp() {
|
||||
if (totpField.getValue() == null) {
|
||||
showError("Bitte den TOTP-Code eingeben.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, Object> request = new LinkedHashMap<>();
|
||||
request.put("pendingToken", sessionData.getPendingTotpToken());
|
||||
request.put("code", String.format("%06d", totpField.getValue()));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> response = restClient.post("/auth/verify-totp", request, LinkedHashMap.class);
|
||||
handleAuthResponse(response);
|
||||
} catch (Exception e) {
|
||||
showError("TOTP-Pruefung fehlgeschlagen.");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void handleAuthResponse(Map<String, Object> response) {
|
||||
if (response == null) {
|
||||
showError("Anmeldung fehlgeschlagen.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(response.get("requiresTotp"))) {
|
||||
sessionData.setPendingTotpToken((String) response.get("pendingToken"));
|
||||
totpField.clear();
|
||||
totpField.setVisible(true);
|
||||
loginButton.setText("Code pruefen");
|
||||
totpField.focus();
|
||||
Notification.show("TOTP-Code eingeben.", 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_CONTRAST);
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Object> user = (Map<String, Object>) response.get("user");
|
||||
if (user == null) {
|
||||
showError("Die Anmeldedaten konnten nicht geladen werden.");
|
||||
return;
|
||||
}
|
||||
|
||||
sessionData.clear();
|
||||
sessionData.setAuthToken((String) response.get("authToken"));
|
||||
sessionData.setUserId(asLong(user.get("userId")));
|
||||
sessionData.setHeadquartersId(asLong(user.get("headquartersId")));
|
||||
sessionData.setEmployeeId(asLong(user.get("employeeId")));
|
||||
sessionData.setCustomerId(asLong(user.get("customerId")));
|
||||
sessionData.setCostCenterId(asLong(user.get("costCenterId")));
|
||||
sessionData.setCourierId(asLong(user.get("courierId")));
|
||||
sessionData.setUserName(asString(user.get("name"), user.get("userName")));
|
||||
sessionData.setUserFirstname(asString(user.get("firstname"), user.get("userFirstname")));
|
||||
sessionData.setUserType(user.get("userType") instanceof Number ? ((Number) user.get("userType")).intValue() : 1);
|
||||
sessionData.setLegacyRights(asString(user.get("legacyRights"), null));
|
||||
sessionData.setAccessibleCostCenterIds(asLongList(user.get("accessibleCostCenterIds")));
|
||||
sessionData.setRightIds(asLongList(user.get("rightIds")));
|
||||
sessionData.setRightNames(asStringList(user.get("rightNames")));
|
||||
sessionData.setAuthenticated(true);
|
||||
|
||||
parameterProvider.init(sessionData.getHeadquartersId(), sessionData.getEmployeeId());
|
||||
UI.getCurrent().navigate(DashboardView.class);
|
||||
}
|
||||
|
||||
private Long asLong(Object value) {
|
||||
return value instanceof Number ? ((Number) value).longValue() : null;
|
||||
}
|
||||
|
||||
private List<Long> asLongList(Object value) {
|
||||
List<Long> result = new java.util.ArrayList<>();
|
||||
if (value instanceof Iterable<?> iterable) {
|
||||
for (Object item : iterable) {
|
||||
Long parsed = asLong(item);
|
||||
if (parsed != null) {
|
||||
result.add(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String asString(Object primary, Object fallback) {
|
||||
if (primary instanceof String primaryString && !primaryString.isBlank()) {
|
||||
return primaryString;
|
||||
}
|
||||
return fallback instanceof String fallbackString ? fallbackString : null;
|
||||
}
|
||||
|
||||
private List<String> asStringList(Object value) {
|
||||
List<String> result = new java.util.ArrayList<>();
|
||||
if (value instanceof Iterable<?> iterable) {
|
||||
for (Object item : iterable) {
|
||||
if (item != null) {
|
||||
String text = item.toString();
|
||||
if (!text.isBlank()) {
|
||||
result.add(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void showError(String message) {
|
||||
Notification.show(message, 5000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
1129
vaadin/src/main/java/de/votian/web/view/LonghaulView.java
Normal file
1129
vaadin/src/main/java/de/votian/web/view/LonghaulView.java
Normal file
File diff suppressed because it is too large
Load Diff
258
vaadin/src/main/java/de/votian/web/view/MainLayout.java
Normal file
258
vaadin/src/main/java/de/votian/web/view/MainLayout.java
Normal file
@@ -0,0 +1,258 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.UI;
|
||||
import com.vaadin.flow.component.applayout.AppLayout;
|
||||
import com.vaadin.flow.component.applayout.DrawerToggle;
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.html.H1;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.sidenav.SideNav;
|
||||
import com.vaadin.flow.component.sidenav.SideNavItem;
|
||||
import com.vaadin.flow.router.BeforeEnterEvent;
|
||||
import com.vaadin.flow.router.BeforeEnterObserver;
|
||||
import de.votian.web.model.SessionData;
|
||||
import de.votian.web.service.ParameterProvider;
|
||||
import de.votian.web.service.RestClientService;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class MainLayout extends AppLayout implements BeforeEnterObserver {
|
||||
|
||||
private final SessionData sessionData;
|
||||
private final ParameterProvider parameterProvider;
|
||||
private final RestClientService restClient;
|
||||
|
||||
public MainLayout(SessionData sessionData, ParameterProvider parameterProvider, RestClientService restClient) {
|
||||
this.sessionData = sessionData;
|
||||
this.parameterProvider = parameterProvider;
|
||||
this.restClient = restClient;
|
||||
createHeader();
|
||||
createDrawer();
|
||||
}
|
||||
|
||||
private void createHeader() {
|
||||
H1 logo = new H1("Votian");
|
||||
logo.getStyle()
|
||||
.set("font-size", "var(--lumo-font-size-l)")
|
||||
.set("margin", "0")
|
||||
.set("color", "#1b12b9");
|
||||
|
||||
Span userInfo = new Span();
|
||||
if (sessionData.isAuthenticated()) {
|
||||
userInfo.setText(sessionData.getDisplayName());
|
||||
}
|
||||
userInfo.getStyle().set("font-size", "var(--lumo-font-size-s)");
|
||||
|
||||
Button logoutButton = new Button("Abmelden", VaadinIcon.SIGN_OUT.create(), e -> logout());
|
||||
logoutButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SMALL);
|
||||
|
||||
HorizontalLayout header = new HorizontalLayout(new DrawerToggle(), logo, userInfo, logoutButton);
|
||||
header.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||
header.expand(userInfo);
|
||||
header.setWidthFull();
|
||||
header.setPadding(true);
|
||||
header.setSpacing(true);
|
||||
|
||||
addToNavbar(header);
|
||||
}
|
||||
|
||||
private void createDrawer() {
|
||||
SideNav nav = new SideNav();
|
||||
nav.addItem(new SideNavItem("Dashboard", DashboardView.class, VaadinIcon.DASHBOARD.create()));
|
||||
|
||||
if (sessionData.canViewJobs()) {
|
||||
nav.addItem(new SideNavItem("Auftraege", JobListView.class, VaadinIcon.CLIPBOARD_TEXT.create()));
|
||||
}
|
||||
if (sessionData.isCourierUser() && sessionData.canViewInvoices()) {
|
||||
nav.addItem(new SideNavItem("Auftraege", CourierInvoiceView.class, VaadinIcon.CLIPBOARD_TEXT.create()));
|
||||
}
|
||||
if (sessionData.canViewDisposition()) {
|
||||
nav.addItem(new SideNavItem("Disposition", DispositionView.class, VaadinIcon.MAP_MARKER.create()));
|
||||
}
|
||||
if (canAccessTracking()) {
|
||||
nav.addItem(new SideNavItem("Tracking", TrackingView.class, VaadinIcon.MAP_MARKER.create()));
|
||||
}
|
||||
if (sessionData.canViewLonghaul()) {
|
||||
nav.addItem(new SideNavItem("Ferntouren", LonghaulView.class, VaadinIcon.GLOBE.create()));
|
||||
}
|
||||
if (sessionData.canViewGroupware()) {
|
||||
nav.addItem(new SideNavItem("Vertrieb", GroupwareView.class, VaadinIcon.CALENDAR_USER.create()));
|
||||
}
|
||||
if (sessionData.canViewGenericExports()) {
|
||||
nav.addItem(new SideNavItem("Datenexport", GenericExportWorkspaceView.class, VaadinIcon.DOWNLOAD_ALT.create()));
|
||||
}
|
||||
if (sessionData.canViewReports()) {
|
||||
nav.addItem(new SideNavItem("Berichte", ReportWorkspaceView.class, VaadinIcon.CLIPBOARD_PULSE.create()));
|
||||
}
|
||||
if (sessionData.canViewCommunication()) {
|
||||
nav.addItem(new SideNavItem("Kommunikation", CommunicationWorkspaceView.class, VaadinIcon.COMMENT.create()));
|
||||
}
|
||||
if (sessionData.canViewGeoOperations()) {
|
||||
nav.addItem(new SideNavItem("Karten/Adressen", GeoOperationsWorkspaceView.class, VaadinIcon.MAP_MARKER.create()));
|
||||
}
|
||||
if (sessionData.canViewCostCenters()) {
|
||||
nav.addItem(new SideNavItem("Kostenstellen", CostCenterView.class, VaadinIcon.MONEY.create()));
|
||||
}
|
||||
if (sessionData.canViewCustomers()) {
|
||||
nav.addItem(new SideNavItem("Kunden", CustomerListView.class, VaadinIcon.GROUP.create()));
|
||||
}
|
||||
if (sessionData.canViewCouriers()) {
|
||||
nav.addItem(new SideNavItem("Kuriere", CourierListView.class, VaadinIcon.CAR.create()));
|
||||
}
|
||||
if (sessionData.canViewEmployees()) {
|
||||
nav.addItem(new SideNavItem("Mitarbeiter", EmployeeListView.class, VaadinIcon.USER.create()));
|
||||
}
|
||||
if (sessionData.canViewStatistics()) {
|
||||
nav.addItem(new SideNavItem("Statistik", StatisticsView.class, VaadinIcon.CHART.create()));
|
||||
}
|
||||
if (!sessionData.isCourierUser() && sessionData.canViewInvoices()) {
|
||||
nav.addItem(new SideNavItem("Rechnungen", InvoiceOverviewView.class, VaadinIcon.INVOICE.create()));
|
||||
}
|
||||
if (sessionData.canManagePricing()) {
|
||||
nav.addItem(new SideNavItem("Preise", PricingView.class, VaadinIcon.COIN_PILES.create()));
|
||||
}
|
||||
if (sessionData.canManageHeadquarters()) {
|
||||
nav.addItem(new SideNavItem("Niederlassung", HeadquartersAdminView.class, VaadinIcon.BUILDING.create()));
|
||||
nav.addItem(new SideNavItem("Feiertage", PublicHolidayView.class, VaadinIcon.CALENDAR.create()));
|
||||
nav.addItem(new SideNavItem("Gruppen/Filter", GroupFilterAdminView.class, VaadinIcon.TAGS.create()));
|
||||
}
|
||||
if (sessionData.canManageMetaFields()) {
|
||||
nav.addItem(new SideNavItem("Formulare", MetaFieldAdminView.class, VaadinIcon.FORM.create()));
|
||||
}
|
||||
if (sessionData.canManageImports()) {
|
||||
nav.addItem(new SideNavItem("Importe", ImportView.class, VaadinIcon.UPLOAD.create()));
|
||||
}
|
||||
if (sessionData.canViewWarehouse()) {
|
||||
nav.addItem(new SideNavItem("Lager", WarehouseView.class, VaadinIcon.ARCHIVES.create()));
|
||||
}
|
||||
|
||||
nav.addItem(new SideNavItem("Passwort", PasswordChangeView.class, VaadinIcon.LOCK.create()));
|
||||
|
||||
VerticalLayout drawerContent = new VerticalLayout(nav);
|
||||
drawerContent.setSizeFull();
|
||||
drawerContent.setPadding(false);
|
||||
drawerContent.setSpacing(false);
|
||||
|
||||
addToDrawer(drawerContent);
|
||||
}
|
||||
|
||||
private void logout() {
|
||||
try {
|
||||
restClient.post("/auth/logout", Map.of(), LinkedHashMap.class);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
sessionData.clear();
|
||||
UI.getCurrent().navigate(LoginView.class);
|
||||
UI.getCurrent().getPage().reload();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeEnter(BeforeEnterEvent event) {
|
||||
if (!sessionData.isAuthenticated()) {
|
||||
event.rerouteTo(LoginView.class);
|
||||
return;
|
||||
}
|
||||
if (!canAccessTarget(event.getNavigationTarget())) {
|
||||
event.rerouteTo(DashboardView.class);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean canAccessTarget(Class<?> target) {
|
||||
if (target == DashboardView.class || target == PasswordChangeView.class) {
|
||||
return true;
|
||||
}
|
||||
if (target == CourierInvoiceView.class) {
|
||||
return sessionData.isCourierUser() && sessionData.canViewInvoices();
|
||||
}
|
||||
if (target == JobListView.class) {
|
||||
return sessionData.canViewJobs();
|
||||
}
|
||||
if (target == JobDetailView.class) {
|
||||
return sessionData.canViewJobs();
|
||||
}
|
||||
if (target == JobBatchView.class) {
|
||||
return sessionData.canBatchJobs();
|
||||
}
|
||||
if (target == DispositionView.class) {
|
||||
return sessionData.canViewDisposition();
|
||||
}
|
||||
if (target == TrackingView.class) {
|
||||
return canAccessTracking();
|
||||
}
|
||||
if (target == LonghaulView.class) {
|
||||
return sessionData.canViewLonghaul();
|
||||
}
|
||||
if (target == GroupwareView.class) {
|
||||
return sessionData.canViewGroupware();
|
||||
}
|
||||
if (target == GenericExportWorkspaceView.class) {
|
||||
return sessionData.canViewGenericExports();
|
||||
}
|
||||
if (target == ReportWorkspaceView.class) {
|
||||
return sessionData.canViewReports();
|
||||
}
|
||||
if (target == CommunicationWorkspaceView.class) {
|
||||
return sessionData.canViewCommunication();
|
||||
}
|
||||
if (target == GeoOperationsWorkspaceView.class) {
|
||||
return sessionData.canViewGeoOperations();
|
||||
}
|
||||
if (target == CostCenterView.class) {
|
||||
return sessionData.canViewCostCenters();
|
||||
}
|
||||
if (target == CustomerListView.class || target == CustomerDetailView.class) {
|
||||
return sessionData.canViewCustomers();
|
||||
}
|
||||
if (target == CourierListView.class || target == CourierDetailView.class) {
|
||||
return sessionData.canViewCouriers();
|
||||
}
|
||||
if (target == EmployeeListView.class || target == EmployeeDetailView.class) {
|
||||
return sessionData.canViewEmployees();
|
||||
}
|
||||
if (target == StatisticsView.class) {
|
||||
return sessionData.canViewStatistics();
|
||||
}
|
||||
if (target == InvoiceOverviewView.class) {
|
||||
return !sessionData.isCourierUser() && sessionData.canViewInvoices();
|
||||
}
|
||||
if (target == PricingView.class) {
|
||||
return sessionData.canManagePricing();
|
||||
}
|
||||
if (target == HeadquartersAdminView.class) {
|
||||
return sessionData.canManageHeadquarters();
|
||||
}
|
||||
if (target == PublicHolidayView.class) {
|
||||
return sessionData.canManageHeadquarters();
|
||||
}
|
||||
if (target == GroupFilterAdminView.class) {
|
||||
return sessionData.canManageHeadquarters();
|
||||
}
|
||||
if (target == MetaFieldAdminView.class) {
|
||||
return sessionData.canManageMetaFields();
|
||||
}
|
||||
if (target == ImportView.class) {
|
||||
return sessionData.canManageImports();
|
||||
}
|
||||
if (target == WarehouseView.class) {
|
||||
return sessionData.canViewWarehouse();
|
||||
}
|
||||
return sessionData.isHeadquartersUser();
|
||||
}
|
||||
|
||||
private boolean canAccessTracking() {
|
||||
if (sessionData.isHeadquartersUser()) {
|
||||
return sessionData.canViewDisposition() || sessionData.canViewJobs();
|
||||
}
|
||||
if (!sessionData.isCustomerUser() || !sessionData.canViewJobs() || sessionData.getCustomerId() == null) {
|
||||
return false;
|
||||
}
|
||||
return parameterProvider.isForObject("TRACKING_ENABLED_CS_", sessionData.getCustomerId())
|
||||
|| parameterProvider.is("CS_TRACKING_ENABLED");
|
||||
}
|
||||
}
|
||||
741
vaadin/src/main/java/de/votian/web/view/MetaFieldAdminView.java
Normal file
741
vaadin/src/main/java/de/votian/web/view/MetaFieldAdminView.java
Normal file
@@ -0,0 +1,741 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.html.H4;
|
||||
import com.vaadin.flow.component.html.Paragraph;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.tabs.Tab;
|
||||
import com.vaadin.flow.component.tabs.Tabs;
|
||||
import com.vaadin.flow.component.textfield.TextArea;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.votian.web.model.SessionData;
|
||||
import de.votian.web.service.RestClientService;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
@Route(value = "meta-admin", layout = MainLayout.class)
|
||||
@PageTitle("Votian - Formulare")
|
||||
public class MetaFieldAdminView extends VerticalLayout {
|
||||
|
||||
private final RestClientService restClient;
|
||||
private final SessionData sessionData;
|
||||
|
||||
private final Grid<CategoryRow> categoryGrid = new Grid<>(CategoryRow.class, false);
|
||||
private final Grid<FieldRow> fieldGrid = new Grid<>(FieldRow.class, false);
|
||||
private final Grid<TemplateRow> templateGrid = new Grid<>(TemplateRow.class, false);
|
||||
private final Grid<CategoryFieldRow> categoryFieldGrid = new Grid<>(CategoryFieldRow.class, false);
|
||||
|
||||
private final TextField categoryMnemonicField = new TextField("Kuerzel");
|
||||
private final TextField categoryDescriptionField = new TextField("Bezeichnung");
|
||||
private final ComboBox<TemplateOption> categoryTemplateField = new ComboBox<>("Vorlage");
|
||||
private final ComboBox<FieldOption> categoryFieldSelection = new ComboBox<>("Globales Feld");
|
||||
|
||||
private final TextField fieldFilter = new TextField("Anzeigefilter");
|
||||
private final TextField fieldTypeField = new TextField("Typ");
|
||||
private final TextField fieldNameField = new TextField("Name");
|
||||
|
||||
private final TextField templateNameField = new TextField("Name");
|
||||
private final TextArea templateContentField = new TextArea("Inhalt");
|
||||
|
||||
private final Button categoryNewButton = new Button("Neu");
|
||||
private final Button categorySaveButton = new Button("Speichern");
|
||||
private final Button categoryFieldAddButton = new Button("Feld hinzufuegen");
|
||||
private final Button categoryFieldUpButton = new Button("Nach oben");
|
||||
private final Button categoryFieldDownButton = new Button("Nach unten");
|
||||
private final Button categoryFieldRemoveButton = new Button("Entfernen");
|
||||
|
||||
private final Button fieldNewButton = new Button("Neu");
|
||||
private final Button fieldSaveButton = new Button("Speichern");
|
||||
private final Button fieldLoadButton = new Button("Anzeigen");
|
||||
|
||||
private final Button templateNewButton = new Button("Neu");
|
||||
private final Button templateSaveButton = new Button("Speichern");
|
||||
|
||||
private final List<CategoryRow> categories = new ArrayList<>();
|
||||
private final List<FieldRow> fieldGridRows = new ArrayList<>();
|
||||
private final List<FieldOption> allFieldOptions = new ArrayList<>();
|
||||
private final List<TemplateRow> templates = new ArrayList<>();
|
||||
private final List<TemplateOption> templateOptions = new ArrayList<>();
|
||||
private final List<CategoryFieldRow> categoryFieldRows = new ArrayList<>();
|
||||
|
||||
private Long selectedCategoryId;
|
||||
private Long selectedFieldId;
|
||||
private Long selectedTemplateId;
|
||||
|
||||
public MetaFieldAdminView(RestClientService restClient, SessionData sessionData) {
|
||||
this.restClient = restClient;
|
||||
this.sessionData = sessionData;
|
||||
|
||||
setSizeFull();
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
|
||||
configureGrids();
|
||||
configureFields();
|
||||
configureButtons();
|
||||
|
||||
Tab categoriesTab = new Tab("Formulare");
|
||||
Tab fieldsTab = new Tab("Globale Felder");
|
||||
Tab templatesTab = new Tab("Vorlagen");
|
||||
Tabs tabs = new Tabs(categoriesTab, fieldsTab, templatesTab);
|
||||
|
||||
VerticalLayout categoriesContent = buildCategoriesContent();
|
||||
VerticalLayout fieldsContent = buildFieldsContent();
|
||||
VerticalLayout templatesContent = buildTemplatesContent();
|
||||
fieldsContent.setVisible(false);
|
||||
templatesContent.setVisible(false);
|
||||
|
||||
tabs.addSelectedChangeListener(event -> {
|
||||
categoriesContent.setVisible(event.getSelectedTab() == categoriesTab);
|
||||
fieldsContent.setVisible(event.getSelectedTab() == fieldsTab);
|
||||
templatesContent.setVisible(event.getSelectedTab() == templatesTab);
|
||||
});
|
||||
|
||||
add(new H3("Formulare und Metafelder"), tabs, categoriesContent, fieldsContent, templatesContent);
|
||||
expand(categoriesContent, fieldsContent, templatesContent);
|
||||
|
||||
loadTemplates();
|
||||
loadFieldOptions();
|
||||
loadFieldGrid();
|
||||
loadCategories();
|
||||
}
|
||||
|
||||
private void configureGrids() {
|
||||
categoryGrid.addColumn(CategoryRow::mnemonic).setHeader("Kuerzel").setAutoWidth(true).setFrozen(true);
|
||||
categoryGrid.addColumn(CategoryRow::description).setHeader("Bezeichnung").setFlexGrow(1);
|
||||
categoryGrid.addColumn(CategoryRow::templateName).setHeader("Vorlage").setAutoWidth(true);
|
||||
categoryGrid.addColumn(CategoryRow::fieldCount).setHeader("Felder").setAutoWidth(true);
|
||||
categoryGrid.setHeight("260px");
|
||||
categoryGrid.asSingleSelect().addValueChangeListener(event -> fillCategoryForm(event.getValue()));
|
||||
|
||||
categoryFieldGrid.addColumn(CategoryFieldRow::sort).setHeader("Sort.").setAutoWidth(true).setFrozen(true);
|
||||
categoryFieldGrid.addColumn(CategoryFieldRow::fieldName).setHeader("Feld").setFlexGrow(1);
|
||||
categoryFieldGrid.addColumn(CategoryFieldRow::fieldType).setHeader("Typ").setAutoWidth(true);
|
||||
categoryFieldGrid.setHeight("240px");
|
||||
|
||||
fieldGrid.addColumn(FieldRow::id).setHeader("Nr.").setAutoWidth(true).setFrozen(true);
|
||||
fieldGrid.addColumn(FieldRow::name).setHeader("Name").setFlexGrow(1);
|
||||
fieldGrid.addColumn(FieldRow::type).setHeader("Typ").setFlexGrow(1);
|
||||
fieldGrid.setHeight("320px");
|
||||
fieldGrid.asSingleSelect().addValueChangeListener(event -> fillFieldForm(event.getValue()));
|
||||
|
||||
templateGrid.addColumn(TemplateRow::name).setHeader("Name").setFlexGrow(1).setFrozen(true);
|
||||
templateGrid.addColumn(TemplateRow::usageCount).setHeader("Verwendung").setAutoWidth(true);
|
||||
templateGrid.setHeight("260px");
|
||||
templateGrid.asSingleSelect().addValueChangeListener(event -> fillTemplateForm(event.getValue()));
|
||||
}
|
||||
|
||||
private void configureFields() {
|
||||
categoryTemplateField.setItemLabelGenerator(TemplateOption::label);
|
||||
categoryTemplateField.setWidthFull();
|
||||
|
||||
categoryFieldSelection.setItemLabelGenerator(FieldOption::label);
|
||||
categoryFieldSelection.setWidth("420px");
|
||||
|
||||
fieldFilter.setClearButtonVisible(true);
|
||||
fieldFilter.setWidth("320px");
|
||||
fieldTypeField.setWidthFull();
|
||||
fieldNameField.setWidthFull();
|
||||
|
||||
templateNameField.setWidthFull();
|
||||
templateContentField.setWidthFull();
|
||||
templateContentField.setMinHeight("260px");
|
||||
}
|
||||
|
||||
private void configureButtons() {
|
||||
categorySaveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
categoryFieldAddButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
fieldSaveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
templateSaveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
|
||||
categoryNewButton.addClickListener(event -> clearCategoryForm());
|
||||
categorySaveButton.addClickListener(event -> saveCategory());
|
||||
categoryFieldAddButton.addClickListener(event -> addCategoryField());
|
||||
categoryFieldUpButton.addClickListener(event -> moveCategoryField(-1));
|
||||
categoryFieldDownButton.addClickListener(event -> moveCategoryField(1));
|
||||
categoryFieldRemoveButton.addClickListener(event -> removeCategoryField());
|
||||
|
||||
fieldNewButton.addClickListener(event -> clearFieldForm());
|
||||
fieldSaveButton.addClickListener(event -> saveField());
|
||||
fieldLoadButton.addClickListener(event -> loadFieldGrid());
|
||||
|
||||
templateNewButton.addClickListener(event -> clearTemplateForm());
|
||||
templateSaveButton.addClickListener(event -> saveTemplate());
|
||||
}
|
||||
|
||||
private VerticalLayout buildCategoriesContent() {
|
||||
FormLayout categoryForm = new FormLayout();
|
||||
categoryForm.setResponsiveSteps(
|
||||
new FormLayout.ResponsiveStep("0", 1),
|
||||
new FormLayout.ResponsiveStep("850px", 3)
|
||||
);
|
||||
categoryForm.add(categoryMnemonicField, categoryDescriptionField, categoryTemplateField);
|
||||
|
||||
HorizontalLayout categoryActions = new HorizontalLayout(categoryNewButton, categorySaveButton);
|
||||
HorizontalLayout fieldActions = new HorizontalLayout(
|
||||
categoryFieldSelection,
|
||||
categoryFieldAddButton,
|
||||
categoryFieldUpButton,
|
||||
categoryFieldDownButton,
|
||||
categoryFieldRemoveButton
|
||||
);
|
||||
fieldActions.setAlignItems(Alignment.BASELINE);
|
||||
|
||||
VerticalLayout content = new VerticalLayout(
|
||||
new Paragraph("Der Workspace pflegt die Legacy-Tabellen `metafieldcategory`, `metafieldkey`, `metafieldtemplate` und `metafieldcategorykey` direkt kompatibel weiter."),
|
||||
new H4("Formulare"),
|
||||
categoryGrid,
|
||||
categoryForm,
|
||||
categoryActions,
|
||||
new H4("Formularfelder"),
|
||||
fieldActions,
|
||||
categoryFieldGrid
|
||||
);
|
||||
content.setPadding(false);
|
||||
content.setSpacing(true);
|
||||
content.setSizeFull();
|
||||
return content;
|
||||
}
|
||||
|
||||
private VerticalLayout buildFieldsContent() {
|
||||
FormLayout form = new FormLayout();
|
||||
form.setResponsiveSteps(
|
||||
new FormLayout.ResponsiveStep("0", 1),
|
||||
new FormLayout.ResponsiveStep("900px", 2)
|
||||
);
|
||||
form.add(fieldTypeField, fieldNameField);
|
||||
|
||||
HorizontalLayout filterBar = new HorizontalLayout(fieldFilter, fieldLoadButton);
|
||||
filterBar.setAlignItems(Alignment.BASELINE);
|
||||
HorizontalLayout actions = new HorizontalLayout(fieldNewButton, fieldSaveButton);
|
||||
|
||||
VerticalLayout content = new VerticalLayout(
|
||||
new Paragraph("Globale Felder bleiben mandantenunabhaengig. Die Filterung entspricht dem Legacy-Editor nach Feldnamenpraefix."),
|
||||
filterBar,
|
||||
fieldGrid,
|
||||
form,
|
||||
actions
|
||||
);
|
||||
content.setPadding(false);
|
||||
content.setSpacing(true);
|
||||
content.setSizeFull();
|
||||
return content;
|
||||
}
|
||||
|
||||
private VerticalLayout buildTemplatesContent() {
|
||||
FormLayout form = new FormLayout();
|
||||
form.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1));
|
||||
form.add(templateNameField, templateContentField);
|
||||
|
||||
HorizontalLayout actions = new HorizontalLayout(templateNewButton, templateSaveButton);
|
||||
VerticalLayout content = new VerticalLayout(
|
||||
new Paragraph("Vorlagen editieren direkt `mtft_content` und koennen anschliessend den Formularen zugewiesen werden."),
|
||||
templateGrid,
|
||||
form,
|
||||
actions
|
||||
);
|
||||
content.setPadding(false);
|
||||
content.setSpacing(true);
|
||||
content.setSizeFull();
|
||||
return content;
|
||||
}
|
||||
|
||||
private void loadCategories() {
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
if (headquartersId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
List<LinkedHashMap<String, Object>> result = restClient.getList(
|
||||
"/meta-admin/categories?hqId=" + headquartersId,
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
categories.clear();
|
||||
for (Map<String, Object> row : result) {
|
||||
Long id = toLong(row.get("id"));
|
||||
if (id == null) {
|
||||
continue;
|
||||
}
|
||||
categories.add(new CategoryRow(
|
||||
id,
|
||||
str(row.get("mnemonic")),
|
||||
str(row.get("description")),
|
||||
toLong(row.get("templateId")),
|
||||
str(row.get("templateName")),
|
||||
toLong(row.get("fieldCount")) != null ? toLong(row.get("fieldCount")) : 0L
|
||||
));
|
||||
}
|
||||
categoryGrid.setItems(categories);
|
||||
reselectCategory();
|
||||
if (selectedCategoryId == null && !categories.isEmpty()) {
|
||||
categoryGrid.select(categories.get(0));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
showError("Formulare konnten nicht geladen werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFieldGrid() {
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
if (headquartersId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String path = "/meta-admin/fields?hqId=" + headquartersId;
|
||||
if (!fieldFilter.getValue().isBlank()) {
|
||||
path += "&filter=" + url(fieldFilter.getValue().trim());
|
||||
}
|
||||
List<LinkedHashMap<String, Object>> result = restClient.getList(path, new ParameterizedTypeReference<>() {});
|
||||
fieldGridRows.clear();
|
||||
for (Map<String, Object> row : result) {
|
||||
Long id = toLong(row.get("id"));
|
||||
if (id == null) {
|
||||
continue;
|
||||
}
|
||||
fieldGridRows.add(new FieldRow(
|
||||
id,
|
||||
str(row.get("type")),
|
||||
str(row.get("name"))
|
||||
));
|
||||
}
|
||||
fieldGrid.setItems(fieldGridRows);
|
||||
reselectField();
|
||||
} catch (Exception e) {
|
||||
showError("Globale Felder konnten nicht geladen werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFieldOptions() {
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
if (headquartersId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
List<LinkedHashMap<String, Object>> result = restClient.getList(
|
||||
"/meta-admin/fields?hqId=" + headquartersId,
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
allFieldOptions.clear();
|
||||
for (Map<String, Object> row : result) {
|
||||
Long id = toLong(row.get("id"));
|
||||
if (id == null) {
|
||||
continue;
|
||||
}
|
||||
allFieldOptions.add(new FieldOption(
|
||||
id,
|
||||
str(row.get("name")),
|
||||
str(row.get("type"))
|
||||
));
|
||||
}
|
||||
refreshCategoryFieldSelection();
|
||||
} catch (Exception e) {
|
||||
showError("Feldoptionen konnten nicht geladen werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void loadTemplates() {
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
if (headquartersId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
List<LinkedHashMap<String, Object>> result = restClient.getList(
|
||||
"/meta-admin/templates?hqId=" + headquartersId,
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
templates.clear();
|
||||
templateOptions.clear();
|
||||
templateOptions.add(new TemplateOption(null, "(keine Vorlage)"));
|
||||
for (Map<String, Object> row : result) {
|
||||
Long id = toLong(row.get("id"));
|
||||
if (id == null) {
|
||||
continue;
|
||||
}
|
||||
TemplateRow template = new TemplateRow(
|
||||
id,
|
||||
str(row.get("name")),
|
||||
str(row.get("content")),
|
||||
toLong(row.get("usageCount")) != null ? toLong(row.get("usageCount")) : 0L
|
||||
);
|
||||
templates.add(template);
|
||||
templateOptions.add(new TemplateOption(id, template.name()));
|
||||
}
|
||||
templateGrid.setItems(templates);
|
||||
categoryTemplateField.setItems(templateOptions);
|
||||
categoryTemplateField.setValue(templateOptions.get(0));
|
||||
reselectTemplate();
|
||||
} catch (Exception e) {
|
||||
showError("Vorlagen konnten nicht geladen werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void loadCategoryFields() {
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
if (selectedCategoryId == null || headquartersId == null) {
|
||||
categoryFieldRows.clear();
|
||||
categoryFieldGrid.setItems(categoryFieldRows);
|
||||
refreshCategoryFieldSelection();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
List<LinkedHashMap<String, Object>> result = restClient.getList(
|
||||
"/meta-admin/categories/" + selectedCategoryId + "/fields?hqId=" + headquartersId,
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
categoryFieldRows.clear();
|
||||
for (Map<String, Object> row : result) {
|
||||
Long assignmentId = toLong(row.get("assignmentId"));
|
||||
Long fieldId = toLong(row.get("fieldId"));
|
||||
if (assignmentId == null || fieldId == null) {
|
||||
continue;
|
||||
}
|
||||
categoryFieldRows.add(new CategoryFieldRow(
|
||||
assignmentId,
|
||||
fieldId,
|
||||
str(row.get("fieldName")),
|
||||
str(row.get("fieldType")),
|
||||
toInteger(row.get("sort")) != null ? toInteger(row.get("sort")) : 0
|
||||
));
|
||||
}
|
||||
categoryFieldGrid.setItems(categoryFieldRows);
|
||||
refreshCategoryFieldSelection();
|
||||
} catch (Exception e) {
|
||||
showError("Formularfelder konnten nicht geladen werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void saveCategory() {
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
if (headquartersId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Map<String, Object> request = new LinkedHashMap<>();
|
||||
request.put("hqId", headquartersId);
|
||||
request.put("mnemonic", categoryMnemonicField.getValue());
|
||||
request.put("description", categoryDescriptionField.getValue());
|
||||
TemplateOption template = categoryTemplateField.getValue();
|
||||
request.put("templateId", template != null ? template.id() : null);
|
||||
|
||||
if (selectedCategoryId == null) {
|
||||
restClient.post("/meta-admin/categories", request, LinkedHashMap.class);
|
||||
} else {
|
||||
restClient.put("/meta-admin/categories/" + selectedCategoryId, request);
|
||||
}
|
||||
showSuccess("Formular gespeichert.");
|
||||
loadTemplates();
|
||||
loadCategories();
|
||||
loadCategoryFields();
|
||||
} catch (Exception e) {
|
||||
showError("Formular konnte nicht gespeichert werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void saveField() {
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
if (headquartersId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Map<String, Object> request = new LinkedHashMap<>();
|
||||
request.put("hqId", headquartersId);
|
||||
request.put("type", fieldTypeField.getValue());
|
||||
request.put("name", fieldNameField.getValue());
|
||||
|
||||
if (selectedFieldId == null) {
|
||||
restClient.post("/meta-admin/fields", request, LinkedHashMap.class);
|
||||
} else {
|
||||
restClient.put("/meta-admin/fields/" + selectedFieldId, request);
|
||||
}
|
||||
showSuccess("Globales Feld gespeichert.");
|
||||
loadFieldOptions();
|
||||
loadFieldGrid();
|
||||
loadCategoryFields();
|
||||
} catch (Exception e) {
|
||||
showError("Globales Feld konnte nicht gespeichert werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void saveTemplate() {
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
if (headquartersId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Map<String, Object> request = new LinkedHashMap<>();
|
||||
request.put("hqId", headquartersId);
|
||||
request.put("name", templateNameField.getValue());
|
||||
request.put("content", templateContentField.getValue());
|
||||
|
||||
if (selectedTemplateId == null) {
|
||||
restClient.post("/meta-admin/templates", request, LinkedHashMap.class);
|
||||
} else {
|
||||
restClient.put("/meta-admin/templates/" + selectedTemplateId, request);
|
||||
}
|
||||
showSuccess("Vorlage gespeichert.");
|
||||
loadTemplates();
|
||||
loadCategories();
|
||||
} catch (Exception e) {
|
||||
showError("Vorlage konnte nicht gespeichert werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void addCategoryField() {
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
FieldOption selectedField = categoryFieldSelection.getValue();
|
||||
if (selectedCategoryId == null || headquartersId == null || selectedField == null) {
|
||||
showError("Bitte Formular und Feld auswaehlen.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Map<String, Object> request = new LinkedHashMap<>();
|
||||
request.put("hqId", headquartersId);
|
||||
request.put("fieldId", selectedField.id());
|
||||
restClient.post("/meta-admin/categories/" + selectedCategoryId + "/fields", request, LinkedHashMap.class);
|
||||
showSuccess("Formularfeld hinzugefuegt.");
|
||||
loadCategoryFields();
|
||||
loadCategories();
|
||||
} catch (Exception e) {
|
||||
showError("Formularfeld konnte nicht hinzugefuegt werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void moveCategoryField(int direction) {
|
||||
CategoryFieldRow selected = categoryFieldGrid.asSingleSelect().getValue();
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
if (selectedCategoryId == null || headquartersId == null || selected == null) {
|
||||
return;
|
||||
}
|
||||
int index = categoryFieldRows.indexOf(selected);
|
||||
int newIndex = index + direction;
|
||||
if (index < 0 || newIndex < 0 || newIndex >= categoryFieldRows.size()) {
|
||||
return;
|
||||
}
|
||||
List<CategoryFieldRow> reordered = new ArrayList<>(categoryFieldRows);
|
||||
reordered.remove(index);
|
||||
reordered.add(newIndex, selected);
|
||||
try {
|
||||
Map<String, Object> request = new LinkedHashMap<>();
|
||||
request.put("hqId", headquartersId);
|
||||
request.put("assignmentIds", reordered.stream().map(CategoryFieldRow::assignmentId).toList());
|
||||
restClient.put("/meta-admin/categories/" + selectedCategoryId + "/fields/order", request);
|
||||
loadCategoryFields();
|
||||
loadCategories();
|
||||
} catch (Exception e) {
|
||||
showError("Sortierung konnte nicht gespeichert werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void removeCategoryField() {
|
||||
CategoryFieldRow selected = categoryFieldGrid.asSingleSelect().getValue();
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
if (selected == null || headquartersId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
restClient.delete("/meta-admin/category-fields/" + selected.assignmentId() + "?hqId=" + headquartersId);
|
||||
showSuccess("Formularfeld entfernt.");
|
||||
loadCategoryFields();
|
||||
loadCategories();
|
||||
} catch (Exception e) {
|
||||
showError("Formularfeld konnte nicht entfernt werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void fillCategoryForm(CategoryRow row) {
|
||||
if (row == null) {
|
||||
clearCategoryForm();
|
||||
return;
|
||||
}
|
||||
selectedCategoryId = row.id();
|
||||
categoryMnemonicField.setValue(row.mnemonic());
|
||||
categoryDescriptionField.setValue(row.description());
|
||||
categoryTemplateField.setValue(templateOptions.stream()
|
||||
.filter(option -> Objects.equals(option.id(), row.templateId()))
|
||||
.findFirst()
|
||||
.orElse(templateOptions.get(0)));
|
||||
loadCategoryFields();
|
||||
}
|
||||
|
||||
private void fillFieldForm(FieldRow row) {
|
||||
if (row == null) {
|
||||
clearFieldForm();
|
||||
return;
|
||||
}
|
||||
selectedFieldId = row.id();
|
||||
fieldTypeField.setValue(row.type());
|
||||
fieldNameField.setValue(row.name());
|
||||
}
|
||||
|
||||
private void fillTemplateForm(TemplateRow row) {
|
||||
if (row == null) {
|
||||
clearTemplateForm();
|
||||
return;
|
||||
}
|
||||
selectedTemplateId = row.id();
|
||||
templateNameField.setValue(row.name());
|
||||
templateContentField.setValue(row.content());
|
||||
}
|
||||
|
||||
private void clearCategoryForm() {
|
||||
selectedCategoryId = null;
|
||||
categoryGrid.asSingleSelect().clear();
|
||||
categoryMnemonicField.clear();
|
||||
categoryDescriptionField.clear();
|
||||
if (!templateOptions.isEmpty()) {
|
||||
categoryTemplateField.setValue(templateOptions.get(0));
|
||||
} else {
|
||||
categoryTemplateField.clear();
|
||||
}
|
||||
loadCategoryFields();
|
||||
}
|
||||
|
||||
private void clearFieldForm() {
|
||||
selectedFieldId = null;
|
||||
fieldGrid.asSingleSelect().clear();
|
||||
fieldTypeField.clear();
|
||||
fieldNameField.clear();
|
||||
}
|
||||
|
||||
private void clearTemplateForm() {
|
||||
selectedTemplateId = null;
|
||||
templateGrid.asSingleSelect().clear();
|
||||
templateNameField.clear();
|
||||
templateContentField.clear();
|
||||
}
|
||||
|
||||
private void refreshCategoryFieldSelection() {
|
||||
if (selectedCategoryId == null) {
|
||||
categoryFieldSelection.clear();
|
||||
categoryFieldSelection.setItems(List.of());
|
||||
return;
|
||||
}
|
||||
List<Long> assignedFieldIds = categoryFieldRows.stream().map(CategoryFieldRow::fieldId).toList();
|
||||
List<FieldOption> available = allFieldOptions.stream()
|
||||
.filter(option -> !assignedFieldIds.contains(option.id()))
|
||||
.toList();
|
||||
categoryFieldSelection.setItems(available);
|
||||
if (!available.contains(categoryFieldSelection.getValue())) {
|
||||
categoryFieldSelection.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void reselectCategory() {
|
||||
if (selectedCategoryId == null) {
|
||||
return;
|
||||
}
|
||||
categories.stream()
|
||||
.filter(row -> Objects.equals(row.id(), selectedCategoryId))
|
||||
.findFirst()
|
||||
.ifPresent(row -> categoryGrid.select(row));
|
||||
}
|
||||
|
||||
private void reselectField() {
|
||||
if (selectedFieldId == null) {
|
||||
return;
|
||||
}
|
||||
fieldGridRows.stream()
|
||||
.filter(row -> Objects.equals(row.id(), selectedFieldId))
|
||||
.findFirst()
|
||||
.ifPresent(row -> fieldGrid.select(row));
|
||||
}
|
||||
|
||||
private void reselectTemplate() {
|
||||
if (selectedTemplateId == null) {
|
||||
return;
|
||||
}
|
||||
templates.stream()
|
||||
.filter(row -> Objects.equals(row.id(), selectedTemplateId))
|
||||
.findFirst()
|
||||
.ifPresent(row -> templateGrid.select(row));
|
||||
}
|
||||
|
||||
private void showSuccess(String message) {
|
||||
Notification.show(message, 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
}
|
||||
|
||||
private void showError(String message) {
|
||||
Notification.show(message, 5000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
|
||||
private String url(String value) {
|
||||
return URLEncoder.encode(value, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private String str(Object value) {
|
||||
return value != null ? value.toString() : "";
|
||||
}
|
||||
|
||||
private Long toLong(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
if (value instanceof String string && !string.isBlank()) {
|
||||
try {
|
||||
return Long.parseLong(string);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Integer toInteger(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
if (value instanceof String string && !string.isBlank()) {
|
||||
try {
|
||||
return Integer.parseInt(string);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private record CategoryRow(Long id,
|
||||
String mnemonic,
|
||||
String description,
|
||||
Long templateId,
|
||||
String templateName,
|
||||
long fieldCount) {
|
||||
}
|
||||
|
||||
private record FieldRow(Long id, String type, String name) {
|
||||
}
|
||||
|
||||
private record TemplateRow(Long id, String name, String content, long usageCount) {
|
||||
}
|
||||
|
||||
private record CategoryFieldRow(Long assignmentId, Long fieldId, String fieldName, String fieldType, int sort) {
|
||||
}
|
||||
|
||||
private record TemplateOption(Long id, String label) {
|
||||
}
|
||||
|
||||
private record FieldOption(Long id, String name, String type) {
|
||||
private String label() {
|
||||
return name + (type == null || type.isBlank() ? "" : " [" + type + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.textfield.PasswordField;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.votian.web.service.RestClientService;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Route(value = "password", layout = MainLayout.class)
|
||||
@PageTitle("Votian - Passwort")
|
||||
public class PasswordChangeView extends VerticalLayout {
|
||||
|
||||
public PasswordChangeView(RestClientService restClient) {
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
|
||||
PasswordField oldPasswordField = new PasswordField("Aktuelles Passwort");
|
||||
PasswordField newPasswordField = new PasswordField("Neues Passwort");
|
||||
PasswordField confirmPasswordField = new PasswordField("Neues Passwort wiederholen");
|
||||
|
||||
FormLayout form = new FormLayout();
|
||||
form.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1));
|
||||
form.add(oldPasswordField, newPasswordField, confirmPasswordField);
|
||||
|
||||
Button saveButton = new Button("Passwort aendern", event -> {
|
||||
if (!newPasswordField.getValue().equals(confirmPasswordField.getValue())) {
|
||||
Notification.show("Die neuen Passwoerter stimmen nicht ueberein.", 4000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, Object> request = new LinkedHashMap<>();
|
||||
request.put("oldPassword", oldPasswordField.getValue());
|
||||
request.put("newPassword", newPasswordField.getValue());
|
||||
restClient.post("/auth/change-password", request, LinkedHashMap.class);
|
||||
|
||||
oldPasswordField.clear();
|
||||
newPasswordField.clear();
|
||||
confirmPasswordField.clear();
|
||||
Notification.show("Passwort geaendert.", 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
} catch (Exception e) {
|
||||
Notification.show("Passwort konnte nicht geaendert werden.", 5000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
});
|
||||
saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
|
||||
add(new H3("Passwort aendern"), form, new HorizontalLayout(saveButton));
|
||||
}
|
||||
}
|
||||
291
vaadin/src/main/java/de/votian/web/view/PricingView.java
Normal file
291
vaadin/src/main/java/de/votian/web/view/PricingView.java
Normal file
@@ -0,0 +1,291 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.datepicker.DatePicker;
|
||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.textfield.IntegerField;
|
||||
import com.vaadin.flow.component.textfield.NumberField;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.votian.web.model.SessionData;
|
||||
import de.votian.web.service.RestClientService;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Route(value = "pricing", layout = MainLayout.class)
|
||||
@PageTitle("Votian - Preise")
|
||||
public class PricingView extends VerticalLayout {
|
||||
|
||||
private final RestClientService restClient;
|
||||
private final SessionData sessionData;
|
||||
|
||||
private final ComboBox<Map<String, Object>> customerBox = new ComboBox<>("Kunde");
|
||||
private final ComboBox<Map<String, Object>> typeBox = new ComboBox<>("Leistungstyp");
|
||||
private final Grid<Map<String, Object>> grid = new Grid<>();
|
||||
private final NumberField priceField = new NumberField("Preis");
|
||||
private final NumberField courierPriceField = new NumberField("Kurierpreis");
|
||||
private final NumberField discountField = new NumberField("Rabatt");
|
||||
private final DatePicker validFromField = new DatePicker("Gueltig ab");
|
||||
private Long selectedServiceId;
|
||||
|
||||
public PricingView(RestClientService restClient, SessionData sessionData) {
|
||||
this.restClient = restClient;
|
||||
this.sessionData = sessionData;
|
||||
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
|
||||
customerBox.setItemLabelGenerator(item -> (String) item.getOrDefault("companyName", "Global"));
|
||||
customerBox.addValueChangeListener(event -> loadPrices());
|
||||
|
||||
typeBox.setItemLabelGenerator(item -> (String) item.getOrDefault("name", ""));
|
||||
|
||||
Button reloadButton = new Button("Neu laden", event -> loadPrices());
|
||||
reloadButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
|
||||
HorizontalLayout filterBar = new HorizontalLayout(customerBox, reloadButton);
|
||||
filterBar.setAlignItems(Alignment.BASELINE);
|
||||
|
||||
grid.addColumn(item -> item.get("serviceTypeName")).setHeader("Typ").setAutoWidth(true);
|
||||
grid.addColumn(item -> item.get("serviceName")).setHeader("Leistung").setFlexGrow(1);
|
||||
grid.addColumn(item -> item.get("price")).setHeader("Preis").setAutoWidth(true);
|
||||
grid.addColumn(item -> item.get("courierPrice")).setHeader("Kurierpreis").setAutoWidth(true);
|
||||
grid.addColumn(item -> item.get("discount")).setHeader("Rabatt").setAutoWidth(true);
|
||||
grid.addColumn(item -> item.get("validFrom")).setHeader("Gueltig ab").setAutoWidth(true);
|
||||
grid.setHeight("320px");
|
||||
grid.addSelectionListener(event -> event.getFirstSelectedItem().ifPresent(this::selectPrice));
|
||||
|
||||
validFromField.setValue(LocalDate.now());
|
||||
|
||||
Button savePriceButton = new Button("Preis speichern", event -> savePrice());
|
||||
savePriceButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
savePriceButton.setEnabled(sessionData.canManagePricing());
|
||||
|
||||
FormLayout priceForm = new FormLayout();
|
||||
priceForm.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1),
|
||||
new FormLayout.ResponsiveStep("700px", 2));
|
||||
priceForm.add(priceField, courierPriceField, discountField, validFromField, savePriceButton);
|
||||
|
||||
TextField newTypeNameField = new TextField("Neuer Leistungstyp");
|
||||
IntegerField newTypeModeField = new IntegerField("Typ-Modus");
|
||||
newTypeModeField.setValue(0);
|
||||
Button addTypeButton = new Button("Typ anlegen", event -> addType(newTypeNameField, newTypeModeField));
|
||||
|
||||
TextField newServiceNameField = new TextField("Neue Leistung");
|
||||
IntegerField newServiceModeField = new IntegerField("Leistungs-Modus");
|
||||
newServiceModeField.setValue(0);
|
||||
Button addServiceButton = new Button("Leistung anlegen", event -> addService(newServiceNameField, newServiceModeField));
|
||||
addTypeButton.setEnabled(sessionData.canManagePricing());
|
||||
addServiceButton.setEnabled(sessionData.canManagePricing());
|
||||
|
||||
FormLayout catalogForm = new FormLayout();
|
||||
catalogForm.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1),
|
||||
new FormLayout.ResponsiveStep("900px", 3));
|
||||
catalogForm.add(newTypeNameField, newTypeModeField, addTypeButton, newServiceNameField, typeBox, newServiceModeField, addServiceButton);
|
||||
|
||||
add(new H3("Preisverwaltung"), filterBar, grid, new H3("Preis bearbeiten"), priceForm,
|
||||
new H3("Leistungen und Typen"), catalogForm);
|
||||
|
||||
loadCustomers();
|
||||
loadTypes();
|
||||
loadPrices();
|
||||
}
|
||||
|
||||
private void loadCustomers() {
|
||||
if (sessionData.getHeadquartersId() == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Object[] result = restClient.get("/customers/hq/" + sessionData.getHeadquartersId(), Object[].class);
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
Map<String, Object> global = new LinkedHashMap<>();
|
||||
global.put("id", 0L);
|
||||
global.put("companyName", "Globale Preise");
|
||||
items.add(global);
|
||||
if (result != null) {
|
||||
for (Object obj : result) {
|
||||
if (obj instanceof Map<?, ?> map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> typed = (Map<String, Object>) map;
|
||||
items.add(typed);
|
||||
}
|
||||
}
|
||||
}
|
||||
customerBox.setItems(items);
|
||||
customerBox.setValue(items.get(0));
|
||||
} catch (Exception e) {
|
||||
Notification.show("Kunden konnten nicht geladen werden.", 4000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadTypes() {
|
||||
if (sessionData.getHeadquartersId() == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Object[] result = restClient.get("/services/types/hq/" + sessionData.getHeadquartersId(), Object[].class);
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
if (result != null) {
|
||||
for (Object obj : result) {
|
||||
if (obj instanceof Map<?, ?> map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> typed = (Map<String, Object>) map;
|
||||
items.add(typed);
|
||||
}
|
||||
}
|
||||
}
|
||||
typeBox.setItems(items);
|
||||
} catch (Exception e) {
|
||||
Notification.show("Leistungstypen konnten nicht geladen werden.", 4000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadPrices() {
|
||||
if (sessionData.getHeadquartersId() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String customerParam = "";
|
||||
Map<String, Object> selectedCustomer = customerBox.getValue();
|
||||
if (selectedCustomer != null && selectedCustomer.get("id") instanceof Number number && number.longValue() > 0) {
|
||||
customerParam = "&customerId=" + number.longValue();
|
||||
}
|
||||
|
||||
try {
|
||||
Object[] result = restClient.get("/services/prices?hqId=" + sessionData.getHeadquartersId() + customerParam, Object[].class);
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
if (result != null) {
|
||||
for (Object obj : result) {
|
||||
if (obj instanceof Map<?, ?> map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> typed = (Map<String, Object>) map;
|
||||
items.add(typed);
|
||||
}
|
||||
}
|
||||
}
|
||||
grid.setItems(items);
|
||||
} catch (Exception e) {
|
||||
Notification.show("Preise konnten nicht geladen werden.", 4000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void selectPrice(Map<String, Object> item) {
|
||||
selectedServiceId = item.get("serviceId") instanceof Number number ? number.longValue() : null;
|
||||
priceField.setValue(asDouble(item.get("price")));
|
||||
courierPriceField.setValue(asDouble(item.get("courierPrice")));
|
||||
discountField.setValue(asDouble(item.get("discount")));
|
||||
if (item.get("validFrom") != null) {
|
||||
validFromField.setValue(LocalDate.parse(item.get("validFrom").toString()));
|
||||
}
|
||||
}
|
||||
|
||||
private void savePrice() {
|
||||
if (!sessionData.canManagePricing()) {
|
||||
Notification.show("Keine Berechtigung zur Preispflege.", 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
return;
|
||||
}
|
||||
if (selectedServiceId == null || sessionData.getHeadquartersId() == null) {
|
||||
Notification.show("Bitte zuerst eine Leistung auswaehlen.", 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, Object> request = new LinkedHashMap<>();
|
||||
request.put("serviceId", selectedServiceId);
|
||||
request.put("headquartersId", sessionData.getHeadquartersId());
|
||||
request.put("customerId", selectedCustomerId());
|
||||
request.put("date", validFromField.getValue() != null ? validFromField.getValue().toString() : LocalDate.now().toString());
|
||||
request.put("price", decimal(priceField.getValue()));
|
||||
request.put("courierPrice", decimal(courierPriceField.getValue()));
|
||||
request.put("discount", decimal(discountField.getValue()));
|
||||
|
||||
restClient.post("/services/history", request, LinkedHashMap.class);
|
||||
Notification.show("Preis gespeichert.", 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
loadPrices();
|
||||
} catch (Exception e) {
|
||||
Notification.show("Preis konnte nicht gespeichert werden.", 5000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void addType(TextField nameField, IntegerField modeField) {
|
||||
if (!sessionData.canManagePricing()) {
|
||||
return;
|
||||
}
|
||||
if (sessionData.getHeadquartersId() == null || nameField.getValue().isBlank()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Map<String, Object> request = new LinkedHashMap<>();
|
||||
request.put("name", nameField.getValue());
|
||||
request.put("mode", modeField.getValue());
|
||||
request.put("headquartersId", sessionData.getHeadquartersId());
|
||||
request.put("customerId", selectedCustomerId());
|
||||
restClient.post("/services/types", request, LinkedHashMap.class);
|
||||
nameField.clear();
|
||||
loadTypes();
|
||||
} catch (Exception e) {
|
||||
Notification.show("Leistungstyp konnte nicht gespeichert werden.", 4000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void addService(TextField nameField, IntegerField modeField) {
|
||||
if (!sessionData.canManagePricing()) {
|
||||
return;
|
||||
}
|
||||
if (sessionData.getHeadquartersId() == null || nameField.getValue().isBlank() || typeBox.getValue() == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Map<String, Object> request = new LinkedHashMap<>();
|
||||
request.put("name", nameField.getValue());
|
||||
request.put("mode", modeField.getValue());
|
||||
request.put("headquartersId", sessionData.getHeadquartersId());
|
||||
request.put("customerId", selectedCustomerId());
|
||||
request.put("serviceTypeId", typeBox.getValue().get("id"));
|
||||
restClient.post("/services/entity", request, LinkedHashMap.class);
|
||||
nameField.clear();
|
||||
loadPrices();
|
||||
} catch (Exception e) {
|
||||
Notification.show("Leistung konnte nicht gespeichert werden.", 4000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private Long selectedCustomerId() {
|
||||
Map<String, Object> selectedCustomer = customerBox.getValue();
|
||||
if (selectedCustomer != null && selectedCustomer.get("id") instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
return 0L;
|
||||
}
|
||||
|
||||
private Double asDouble(Object value) {
|
||||
return value instanceof Number ? ((Number) value).doubleValue() : null;
|
||||
}
|
||||
|
||||
private BigDecimal decimal(Double value) {
|
||||
return value != null ? BigDecimal.valueOf(value) : null;
|
||||
}
|
||||
}
|
||||
343
vaadin/src/main/java/de/votian/web/view/PublicHolidayView.java
Normal file
343
vaadin/src/main/java/de/votian/web/view/PublicHolidayView.java
Normal file
@@ -0,0 +1,343 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.checkbox.Checkbox;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.html.Paragraph;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.votian.web.model.SessionData;
|
||||
import de.votian.web.service.RestClientService;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Route(value = "public-holidays", layout = MainLayout.class)
|
||||
@PageTitle("Votian - Feiertage")
|
||||
public class PublicHolidayView extends VerticalLayout {
|
||||
|
||||
private final RestClientService restClient;
|
||||
private final SessionData sessionData;
|
||||
|
||||
private final ComboBox<Integer> yearSelect = new ComboBox<>("Jahr");
|
||||
private final ComboBox<Integer> mergeYearSelect = new ComboBox<>("Vorjahr integrieren");
|
||||
private final Button mergeButton = new Button("Eintraege uebernehmen");
|
||||
private final Button saveButton = new Button("Speichern");
|
||||
private final Paragraph info = new Paragraph();
|
||||
private final Grid<HolidayRow> grid = new Grid<>(HolidayRow.class, false);
|
||||
|
||||
private final List<HolidayRow> rows = new ArrayList<>();
|
||||
private boolean loading;
|
||||
|
||||
public PublicHolidayView(RestClientService restClient, SessionData sessionData) {
|
||||
this.restClient = restClient;
|
||||
this.sessionData = sessionData;
|
||||
|
||||
setSizeFull();
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
|
||||
configureToolbar();
|
||||
configureGrid();
|
||||
|
||||
add(new H3("Feiertage"), new HorizontalLayout(yearSelect, mergeYearSelect, mergeButton, saveButton), info, grid);
|
||||
expand(grid);
|
||||
|
||||
loadYear(LocalDate.now().getYear());
|
||||
}
|
||||
|
||||
private void configureToolbar() {
|
||||
yearSelect.setWidth("160px");
|
||||
yearSelect.addValueChangeListener(event -> {
|
||||
if (!loading && event.getValue() != null) {
|
||||
loadYear(event.getValue());
|
||||
}
|
||||
});
|
||||
|
||||
mergeYearSelect.setWidth("220px");
|
||||
mergeYearSelect.addValueChangeListener(event -> refreshButtons());
|
||||
|
||||
mergeButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
mergeButton.addClickListener(event -> mergeSelectedYear());
|
||||
|
||||
saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
saveButton.addClickListener(event -> save());
|
||||
}
|
||||
|
||||
private void configureGrid() {
|
||||
grid.addColumn(HolidayRow::isoDate).setHeader("Datum").setAutoWidth(true).setFrozen(true);
|
||||
grid.addColumn(HolidayRow::weekdayName).setHeader("Wochentag").setAutoWidth(true);
|
||||
grid.addComponentColumn(this::buildHolidayCheckbox).setHeader("Feiertag").setAutoWidth(true);
|
||||
grid.addComponentColumn(this::buildNameField).setHeader("Bezeichnung").setFlexGrow(1);
|
||||
grid.setSizeFull();
|
||||
}
|
||||
|
||||
private Checkbox buildHolidayCheckbox(HolidayRow row) {
|
||||
Checkbox checkbox = new Checkbox(row.holiday());
|
||||
checkbox.setEnabled(row.editable());
|
||||
checkbox.addValueChangeListener(event -> {
|
||||
row.setHoliday(event.getValue());
|
||||
if (!event.getValue()) {
|
||||
row.setName("");
|
||||
}
|
||||
grid.getDataProvider().refreshItem(row);
|
||||
});
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
private TextField buildNameField(HolidayRow row) {
|
||||
TextField field = new TextField();
|
||||
field.setWidthFull();
|
||||
field.setValue(row.name());
|
||||
field.setEnabled(row.editable() && row.holiday());
|
||||
field.setPlaceholder("Feiertagsname");
|
||||
field.addValueChangeListener(event -> row.setName(event.getValue()));
|
||||
return field;
|
||||
}
|
||||
|
||||
private void loadYear(int year) {
|
||||
if (sessionData.getHeadquartersId() == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
loading = true;
|
||||
HolidayResponse response = parseResponse(restClient.getMap(
|
||||
"/public-holidays?hqId=" + sessionData.getHeadquartersId() + "&year=" + year
|
||||
));
|
||||
yearSelect.setItems(response.availableYears());
|
||||
yearSelect.setValue(response.year());
|
||||
mergeYearSelect.setItems(response.copySourceYears());
|
||||
if (!response.copySourceYears().contains(mergeYearSelect.getValue())) {
|
||||
mergeYearSelect.clear();
|
||||
}
|
||||
rows.clear();
|
||||
rows.addAll(response.days());
|
||||
grid.setItems(rows);
|
||||
info.setText(response.editable()
|
||||
? "Nur zukuenftige Tage sind bearbeitbar. Vergangene Feiertage bleiben beim Speichern unveraendert."
|
||||
: "Dieses Jahr ist nur noch lesend verfuegbar.");
|
||||
refreshButtons();
|
||||
} catch (Exception e) {
|
||||
Notification.show("Feiertage konnten nicht geladen werden: " + e.getMessage(), 5000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void mergeSelectedYear() {
|
||||
Integer sourceYear = mergeYearSelect.getValue();
|
||||
Integer targetYear = yearSelect.getValue();
|
||||
if (sourceYear == null || targetYear == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
HolidayResponse source = parseResponse(restClient.getMap(
|
||||
"/public-holidays?hqId=" + sessionData.getHeadquartersId() + "&year=" + sourceYear
|
||||
));
|
||||
Map<String, HolidayRow> sourceByDate = new LinkedHashMap<>();
|
||||
for (HolidayRow row : source.days()) {
|
||||
if (row.holiday()) {
|
||||
sourceByDate.put(row.isoDate().substring(5), row);
|
||||
}
|
||||
}
|
||||
|
||||
int merged = 0;
|
||||
for (HolidayRow targetRow : rows) {
|
||||
if (!targetRow.editable() || targetRow.holiday()) {
|
||||
continue;
|
||||
}
|
||||
HolidayRow sourceRow = sourceByDate.get(targetRow.isoDate().substring(5));
|
||||
if (sourceRow == null) {
|
||||
continue;
|
||||
}
|
||||
targetRow.setHoliday(true);
|
||||
targetRow.setName(sourceRow.name());
|
||||
merged++;
|
||||
}
|
||||
grid.getDataProvider().refreshAll();
|
||||
Notification.show(merged + " Feiertage integriert.", 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
} catch (Exception e) {
|
||||
Notification.show("Vorjahreseintraege konnten nicht geladen werden: " + e.getMessage(), 5000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void save() {
|
||||
Integer year = yearSelect.getValue();
|
||||
if (year == null || sessionData.getHeadquartersId() == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
List<Map<String, Object>> days = rows.stream().map(HolidayRow::toMap).toList();
|
||||
restClient.put(
|
||||
"/public-holidays?hqId=" + sessionData.getHeadquartersId() + "&year=" + year,
|
||||
Map.of("days", days)
|
||||
);
|
||||
Notification.show("Feiertage gespeichert.", 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
loadYear(year);
|
||||
} catch (Exception e) {
|
||||
Notification.show("Feiertage konnten nicht gespeichert werden: " + e.getMessage(), 5000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshButtons() {
|
||||
boolean editable = rows.stream().anyMatch(HolidayRow::editable);
|
||||
mergeButton.setEnabled(editable && mergeYearSelect.getValue() != null);
|
||||
saveButton.setEnabled(editable && sessionData.canManageHeadquarters());
|
||||
}
|
||||
|
||||
private HolidayResponse parseResponse(Map<String, Object> data) {
|
||||
List<Integer> availableYears = new ArrayList<>();
|
||||
Object rawYears = data.get("availableYears");
|
||||
if (rawYears instanceof List<?> list) {
|
||||
for (Object entry : list) {
|
||||
Integer year = toInteger(entry);
|
||||
if (year != null) {
|
||||
availableYears.add(year);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Integer> copySourceYears = new ArrayList<>();
|
||||
Object rawCopyYears = data.get("copySourceYears");
|
||||
if (rawCopyYears instanceof List<?> list) {
|
||||
for (Object entry : list) {
|
||||
Integer year = toInteger(entry);
|
||||
if (year != null) {
|
||||
copySourceYears.add(year);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<HolidayRow> parsedRows = new ArrayList<>();
|
||||
Object rawDays = data.get("days");
|
||||
if (rawDays instanceof List<?> list) {
|
||||
for (Object entry : list) {
|
||||
if (!(entry instanceof Map<?, ?> rawMap)) {
|
||||
continue;
|
||||
}
|
||||
parsedRows.add(new HolidayRow(
|
||||
str(rawMap.get("isoDate")),
|
||||
str(rawMap.get("weekdayName")),
|
||||
toInteger(rawMap.get("month")),
|
||||
toInteger(rawMap.get("day")),
|
||||
bool(rawMap.get("holiday")),
|
||||
str(rawMap.get("name")),
|
||||
bool(rawMap.get("editable"))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return new HolidayResponse(
|
||||
toInteger(data.get("year")) != null ? toInteger(data.get("year")) : LocalDate.now().getYear(),
|
||||
availableYears,
|
||||
copySourceYears,
|
||||
parsedRows,
|
||||
bool(data.get("editable"))
|
||||
);
|
||||
}
|
||||
|
||||
private Integer toInteger(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
try {
|
||||
return value != null ? Integer.parseInt(value.toString()) : null;
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean bool(Object value) {
|
||||
return Boolean.TRUE.equals(value) || (value != null && "true".equalsIgnoreCase(value.toString()));
|
||||
}
|
||||
|
||||
private String str(Object value) {
|
||||
return value != null ? value.toString() : "";
|
||||
}
|
||||
|
||||
private record HolidayResponse(int year,
|
||||
List<Integer> availableYears,
|
||||
List<Integer> copySourceYears,
|
||||
List<HolidayRow> days,
|
||||
boolean editable) {
|
||||
}
|
||||
|
||||
private static final class HolidayRow {
|
||||
private final String isoDate;
|
||||
private final String weekdayName;
|
||||
private final Integer month;
|
||||
private final Integer day;
|
||||
private boolean holiday;
|
||||
private String name;
|
||||
private final boolean editable;
|
||||
|
||||
private HolidayRow(String isoDate,
|
||||
String weekdayName,
|
||||
Integer month,
|
||||
Integer day,
|
||||
boolean holiday,
|
||||
String name,
|
||||
boolean editable) {
|
||||
this.isoDate = isoDate;
|
||||
this.weekdayName = weekdayName;
|
||||
this.month = month;
|
||||
this.day = day;
|
||||
this.holiday = holiday;
|
||||
this.name = name != null ? name : "";
|
||||
this.editable = editable;
|
||||
}
|
||||
|
||||
private String isoDate() {
|
||||
return isoDate;
|
||||
}
|
||||
|
||||
private String weekdayName() {
|
||||
return weekdayName;
|
||||
}
|
||||
|
||||
private boolean holiday() {
|
||||
return holiday;
|
||||
}
|
||||
|
||||
private void setHoliday(boolean holiday) {
|
||||
this.holiday = holiday;
|
||||
}
|
||||
|
||||
private String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
private void setName(String name) {
|
||||
this.name = name != null ? name : "";
|
||||
}
|
||||
|
||||
private boolean editable() {
|
||||
return editable;
|
||||
}
|
||||
|
||||
private Map<String, Object> toMap() {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("month", month);
|
||||
map.put("day", day);
|
||||
map.put("holiday", holiday);
|
||||
map.put("name", name);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
}
|
||||
721
vaadin/src/main/java/de/votian/web/view/ReportWorkspaceView.java
Normal file
721
vaadin/src/main/java/de/votian/web/view/ReportWorkspaceView.java
Normal file
@@ -0,0 +1,721 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.checkbox.Checkbox;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.datepicker.DatePicker;
|
||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.html.Anchor;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.html.H4;
|
||||
import com.vaadin.flow.component.html.Paragraph;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.tabs.Tab;
|
||||
import com.vaadin.flow.component.tabs.Tabs;
|
||||
import com.vaadin.flow.component.textfield.TextArea;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import com.vaadin.flow.server.StreamResource;
|
||||
import de.votian.web.model.SessionData;
|
||||
import de.votian.web.service.RestClientService;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
@Route(value = "reports", layout = MainLayout.class)
|
||||
@PageTitle("Votian - Berichte")
|
||||
public class ReportWorkspaceView extends VerticalLayout {
|
||||
|
||||
private final RestClientService restClient;
|
||||
private final SessionData sessionData;
|
||||
|
||||
private final ComboBox<ObjectTypeOption> objectTypeField = new ComboBox<>("Bereich");
|
||||
private final TextField objectSearchField = new TextField("Objektsuche");
|
||||
private final Button objectSearchButton = new Button("Suchen");
|
||||
private final ComboBox<ObjectOption> objectField = new ComboBox<>("Objekt");
|
||||
private final DatePicker fromDate = new DatePicker("Von");
|
||||
private final DatePicker toDate = new DatePicker("Bis");
|
||||
|
||||
private final ComboBox<ReportTypeOption> historyReportTypeField = new ComboBox<>("Berichtstyp");
|
||||
private final TextField historySearchField = new TextField("Textsuche");
|
||||
private final Button historyLoadButton = new Button("Historie laden");
|
||||
private final Anchor historyExportLink = new Anchor();
|
||||
private final Button historyExportButton = new Button("CSV exportieren");
|
||||
|
||||
private final Grid<ReportRow> historyGrid = new Grid<>(ReportRow.class, false);
|
||||
private final ComboBox<ReportTypeOption> editorReportTypeField = new ComboBox<>("Berichtstyp");
|
||||
private final Checkbox editorConfidentialField = new Checkbox("Vertraulich");
|
||||
private final TextArea editorTextField = new TextArea("Berichtstext");
|
||||
private final Button editorNewButton = new Button("Neu");
|
||||
private final Button editorSaveButton = new Button("Speichern");
|
||||
private final Button editorDeleteButton = new Button("Loeschen");
|
||||
|
||||
private final Checkbox splitByReportTypeField = new Checkbox("Nach Berichtstyp aufteilen");
|
||||
private final Checkbox splitByCustomerTypeField = new Checkbox("Nach Kundentyp aufteilen");
|
||||
private final Button statisticLoadButton = new Button("Statistik laden");
|
||||
private final Anchor statisticExportLink = new Anchor();
|
||||
private final Button statisticExportButton = new Button("CSV exportieren");
|
||||
private final Grid<StatisticRow> statisticGrid = new Grid<>(StatisticRow.class, false);
|
||||
|
||||
private final List<ObjectTypeOption> objectTypes = new ArrayList<>();
|
||||
private final List<ObjectOption> objectOptions = new ArrayList<>();
|
||||
private final List<ReportTypeOption> historyTypeOptions = new ArrayList<>();
|
||||
private final List<ReportTypeOption> editorTypeOptions = new ArrayList<>();
|
||||
private final List<ReportRow> historyRows = new ArrayList<>();
|
||||
|
||||
private Long selectedReportId;
|
||||
private boolean selectedReportEditable;
|
||||
private boolean selectedReportDeletable;
|
||||
|
||||
public ReportWorkspaceView(RestClientService restClient, SessionData sessionData) {
|
||||
this.restClient = restClient;
|
||||
this.sessionData = sessionData;
|
||||
|
||||
setSizeFull();
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
|
||||
configureFilters();
|
||||
configureHistoryGrid();
|
||||
configureStatisticGrid();
|
||||
configureEditor();
|
||||
configureExportLinks();
|
||||
|
||||
Tab historyTab = new Tab("Historie");
|
||||
Tab statisticsTab = new Tab("Statistik");
|
||||
Tabs tabs = new Tabs(historyTab, statisticsTab);
|
||||
|
||||
VerticalLayout historyContent = buildHistoryContent();
|
||||
VerticalLayout statisticContent = buildStatisticContent();
|
||||
statisticContent.setVisible(false);
|
||||
|
||||
tabs.addSelectedChangeListener(event -> {
|
||||
historyContent.setVisible(event.getSelectedTab() == historyTab);
|
||||
statisticContent.setVisible(event.getSelectedTab() == statisticsTab);
|
||||
});
|
||||
|
||||
add(new H3("Berichtsarbeitsbereich"), buildObjectFilterBar(), tabs, historyContent, statisticContent);
|
||||
expand(historyContent, statisticContent);
|
||||
|
||||
loadBootstrap();
|
||||
}
|
||||
|
||||
private void configureFilters() {
|
||||
objectTypeField.setItemLabelGenerator(ObjectTypeOption::label);
|
||||
objectTypeField.setWidth("220px");
|
||||
objectTypeField.addValueChangeListener(event -> {
|
||||
clearObjectSelection();
|
||||
clearEditor();
|
||||
loadReportTypes();
|
||||
updateSplitCustomerTypeState();
|
||||
});
|
||||
|
||||
objectSearchField.setWidth("260px");
|
||||
objectSearchField.setClearButtonVisible(true);
|
||||
objectSearchButton.addClickListener(event -> searchObjects());
|
||||
|
||||
objectField.setItemLabelGenerator(ObjectOption::labelWithSecondary);
|
||||
objectField.setWidth("420px");
|
||||
objectField.addValueChangeListener(event -> updateExportLinks());
|
||||
|
||||
fromDate.setValue(LocalDate.now().withDayOfMonth(1));
|
||||
toDate.setValue(LocalDate.now());
|
||||
fromDate.addValueChangeListener(event -> updateExportLinks());
|
||||
toDate.addValueChangeListener(event -> updateExportLinks());
|
||||
|
||||
historyReportTypeField.setItemLabelGenerator(ReportTypeOption::label);
|
||||
historyReportTypeField.setWidth("260px");
|
||||
historyReportTypeField.addValueChangeListener(event -> updateExportLinks());
|
||||
editorReportTypeField.setItemLabelGenerator(ReportTypeOption::label);
|
||||
editorReportTypeField.setWidthFull();
|
||||
|
||||
historySearchField.setWidth("260px");
|
||||
historySearchField.setClearButtonVisible(true);
|
||||
historySearchField.addValueChangeListener(event -> updateExportLinks());
|
||||
historyLoadButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
historyLoadButton.addClickListener(event -> loadHistory());
|
||||
|
||||
splitByReportTypeField.setValue(true);
|
||||
splitByCustomerTypeField.setValue(false);
|
||||
splitByReportTypeField.addValueChangeListener(event -> updateExportLinks());
|
||||
splitByCustomerTypeField.addValueChangeListener(event -> updateExportLinks());
|
||||
statisticLoadButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
statisticLoadButton.addClickListener(event -> loadStatistics());
|
||||
}
|
||||
|
||||
private void configureHistoryGrid() {
|
||||
historyGrid.addColumn(ReportRow::createdAt).setHeader("Zeitpunkt").setAutoWidth(true).setFrozen(true);
|
||||
historyGrid.addColumn(ReportRow::objectLabel).setHeader("Objekt").setFlexGrow(1);
|
||||
historyGrid.addColumn(ReportRow::reportTypeLabel).setHeader("Berichtstyp").setAutoWidth(true);
|
||||
historyGrid.addColumn(ReportRow::authorName).setHeader("Mitarbeiter").setAutoWidth(true);
|
||||
historyGrid.addColumn(row -> row.confidential() ? "Ja" : "").setHeader("Vertraulich").setAutoWidth(true);
|
||||
historyGrid.addColumn(ReportRow::text).setHeader("Eintrag").setFlexGrow(2);
|
||||
historyGrid.setSizeFull();
|
||||
historyGrid.asSingleSelect().addValueChangeListener(event -> fillEditor(event.getValue()));
|
||||
}
|
||||
|
||||
private void configureStatisticGrid() {
|
||||
statisticGrid.addColumn(StatisticRow::headquartersName).setHeader("Zentrale").setAutoWidth(true).setFrozen(true);
|
||||
statisticGrid.addColumn(StatisticRow::authorFirstname).setHeader("Vorname").setAutoWidth(true);
|
||||
statisticGrid.addColumn(StatisticRow::authorName).setHeader("Name").setAutoWidth(true);
|
||||
statisticGrid.addColumn(StatisticRow::reportTypeLabel).setHeader("Berichtstyp").setAutoWidth(true);
|
||||
statisticGrid.addColumn(StatisticRow::companyTypeLabel).setHeader("Kundentyp").setAutoWidth(true);
|
||||
statisticGrid.addColumn(StatisticRow::reports).setHeader("Berichte").setAutoWidth(true);
|
||||
statisticGrid.setSizeFull();
|
||||
}
|
||||
|
||||
private void configureEditor() {
|
||||
editorSaveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
editorDeleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR);
|
||||
editorTextField.setWidthFull();
|
||||
editorTextField.setMinHeight("180px");
|
||||
|
||||
editorNewButton.addClickListener(event -> clearEditor());
|
||||
editorSaveButton.addClickListener(event -> saveReport());
|
||||
editorDeleteButton.addClickListener(event -> deleteReport());
|
||||
refreshEditorButtons();
|
||||
}
|
||||
|
||||
private void configureExportLinks() {
|
||||
historyExportLink.add(historyExportButton);
|
||||
statisticExportLink.add(statisticExportButton);
|
||||
historyExportLink.getElement().setAttribute("download", true);
|
||||
statisticExportLink.getElement().setAttribute("download", true);
|
||||
updateExportLinks();
|
||||
}
|
||||
|
||||
private HorizontalLayout buildObjectFilterBar() {
|
||||
HorizontalLayout bar = new HorizontalLayout(
|
||||
objectTypeField,
|
||||
objectSearchField,
|
||||
objectSearchButton,
|
||||
objectField,
|
||||
fromDate,
|
||||
toDate
|
||||
);
|
||||
bar.setAlignItems(Alignment.BASELINE);
|
||||
return bar;
|
||||
}
|
||||
|
||||
private VerticalLayout buildHistoryContent() {
|
||||
HorizontalLayout filters = new HorizontalLayout(historyReportTypeField, historySearchField, historyLoadButton, historyExportLink);
|
||||
filters.setAlignItems(Alignment.BASELINE);
|
||||
|
||||
FormLayout editorForm = new FormLayout();
|
||||
editorForm.setResponsiveSteps(
|
||||
new FormLayout.ResponsiveStep("0", 1),
|
||||
new FormLayout.ResponsiveStep("900px", 2)
|
||||
);
|
||||
editorForm.add(editorReportTypeField, editorConfidentialField, editorTextField);
|
||||
editorForm.setColspan(editorTextField, 2);
|
||||
|
||||
HorizontalLayout actions = new HorizontalLayout(editorNewButton, editorSaveButton, editorDeleteButton);
|
||||
|
||||
VerticalLayout content = new VerticalLayout(
|
||||
new Paragraph("Historie und CRUD arbeiten direkt auf `phoenix_group.report_process`. Vertrauliche Berichte folgen der Legacy-Sichtbarkeit."),
|
||||
filters,
|
||||
historyGrid,
|
||||
new H4("Eintrag bearbeiten"),
|
||||
editorForm,
|
||||
actions
|
||||
);
|
||||
content.setPadding(false);
|
||||
content.setSpacing(true);
|
||||
content.setSizeFull();
|
||||
content.expand(historyGrid);
|
||||
return content;
|
||||
}
|
||||
|
||||
private VerticalLayout buildStatisticContent() {
|
||||
HorizontalLayout controls = new HorizontalLayout(
|
||||
splitByReportTypeField,
|
||||
splitByCustomerTypeField,
|
||||
statisticLoadButton,
|
||||
statisticExportLink
|
||||
);
|
||||
controls.setAlignItems(Alignment.CENTER);
|
||||
|
||||
VerticalLayout content = new VerticalLayout(
|
||||
new Paragraph("Die Statistik gruppiert die Berichte nach Mitarbeiter und optional nach Berichtstyp sowie Kundentyp."),
|
||||
controls,
|
||||
statisticGrid
|
||||
);
|
||||
content.setPadding(false);
|
||||
content.setSpacing(true);
|
||||
content.setSizeFull();
|
||||
content.expand(statisticGrid);
|
||||
return content;
|
||||
}
|
||||
|
||||
private void loadBootstrap() {
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
if (headquartersId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> bootstrap = restClient.get(
|
||||
"/report-workspace/bootstrap?hqId=" + headquartersId,
|
||||
LinkedHashMap.class
|
||||
);
|
||||
objectTypes.clear();
|
||||
Object raw = bootstrap.get("objectTypes");
|
||||
if (raw instanceof List<?> list) {
|
||||
for (Object item : list) {
|
||||
if (item instanceof Map<?, ?> row) {
|
||||
objectTypes.add(new ObjectTypeOption(
|
||||
str(row.get("code")),
|
||||
str(row.get("label"))
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
objectTypeField.setItems(objectTypes);
|
||||
if (!objectTypes.isEmpty()) {
|
||||
objectTypeField.setValue(objectTypes.get(0));
|
||||
}
|
||||
updateSplitCustomerTypeState();
|
||||
loadReportTypes();
|
||||
} catch (Exception e) {
|
||||
showError("Berichtsworkspace konnte nicht initialisiert werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void loadReportTypes() {
|
||||
ObjectTypeOption objectType = objectTypeField.getValue();
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
if (objectType == null || headquartersId == null) {
|
||||
historyReportTypeField.setItems(List.of());
|
||||
editorReportTypeField.setItems(List.of());
|
||||
return;
|
||||
}
|
||||
try {
|
||||
List<LinkedHashMap<String, Object>> result = restClient.getList(
|
||||
"/report-workspace/report-types?hqId=" + headquartersId + "&objectType=" + objectType.code(),
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
historyTypeOptions.clear();
|
||||
editorTypeOptions.clear();
|
||||
historyTypeOptions.add(new ReportTypeOption(0, "Alle"));
|
||||
editorTypeOptions.add(new ReportTypeOption(0, "(ohne Typ)"));
|
||||
for (Map<String, Object> row : result) {
|
||||
Integer id = toInteger(row.get("id"));
|
||||
if (id == null) {
|
||||
continue;
|
||||
}
|
||||
String label = str(row.get("label"));
|
||||
historyTypeOptions.add(new ReportTypeOption(id, label));
|
||||
editorTypeOptions.add(new ReportTypeOption(id, label));
|
||||
}
|
||||
historyReportTypeField.setItems(historyTypeOptions);
|
||||
editorReportTypeField.setItems(editorTypeOptions);
|
||||
historyReportTypeField.setValue(historyTypeOptions.get(0));
|
||||
editorReportTypeField.setValue(editorTypeOptions.get(0));
|
||||
updateExportLinks();
|
||||
} catch (Exception e) {
|
||||
showError("Berichtstypen konnten nicht geladen werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void searchObjects() {
|
||||
ObjectTypeOption objectType = objectTypeField.getValue();
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
if (objectType == null || headquartersId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String path = "/report-workspace/search?hqId=" + headquartersId
|
||||
+ "&objectType=" + objectType.code()
|
||||
+ "&query=" + url(objectSearchField.getValue());
|
||||
List<LinkedHashMap<String, Object>> result = restClient.getList(path, new ParameterizedTypeReference<>() {});
|
||||
objectOptions.clear();
|
||||
for (Map<String, Object> row : result) {
|
||||
Long id = toLong(row.get("id"));
|
||||
if (id == null) {
|
||||
continue;
|
||||
}
|
||||
objectOptions.add(new ObjectOption(
|
||||
id,
|
||||
str(row.get("label")),
|
||||
str(row.get("secondaryLabel"))
|
||||
));
|
||||
}
|
||||
objectField.setItems(objectOptions);
|
||||
if (!objectOptions.isEmpty()) {
|
||||
objectField.setValue(objectOptions.get(0));
|
||||
}
|
||||
updateExportLinks();
|
||||
} catch (Exception e) {
|
||||
showError("Objekte konnten nicht gesucht werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void loadHistory() {
|
||||
ObjectTypeOption objectType = objectTypeField.getValue();
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
if (objectType == null || headquartersId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
StringBuilder path = new StringBuilder("/report-workspace/reports?hqId=")
|
||||
.append(headquartersId)
|
||||
.append("&objectType=").append(objectType.code());
|
||||
if (objectField.getValue() != null) {
|
||||
path.append("&objectId=").append(objectField.getValue().id());
|
||||
}
|
||||
if (fromDate.getValue() != null) {
|
||||
path.append("&from=").append(fromDate.getValue());
|
||||
}
|
||||
if (toDate.getValue() != null) {
|
||||
path.append("&to=").append(toDate.getValue());
|
||||
}
|
||||
if (historyReportTypeField.getValue() != null && historyReportTypeField.getValue().id() != null
|
||||
&& historyReportTypeField.getValue().id() > 0) {
|
||||
path.append("&reportType=").append(historyReportTypeField.getValue().id());
|
||||
}
|
||||
if (!historySearchField.getValue().isBlank()) {
|
||||
path.append("&search=").append(url(historySearchField.getValue()));
|
||||
}
|
||||
List<LinkedHashMap<String, Object>> result = restClient.getList(path.toString(), new ParameterizedTypeReference<>() {});
|
||||
historyRows.clear();
|
||||
for (Map<String, Object> row : result) {
|
||||
Long id = toLong(row.get("id"));
|
||||
if (id == null) {
|
||||
continue;
|
||||
}
|
||||
historyRows.add(new ReportRow(
|
||||
id,
|
||||
str(row.get("objectType")),
|
||||
toLong(row.get("objectId")),
|
||||
str(row.get("objectLabel")),
|
||||
str(row.get("headquartersName")),
|
||||
toLong(row.get("authorUserId")),
|
||||
str(row.get("authorName")),
|
||||
str(row.get("authorPhone")),
|
||||
str(row.get("createdAt")).replace("T", " "),
|
||||
toInteger(row.get("reportType")),
|
||||
str(row.get("reportTypeLabel")),
|
||||
str(row.get("text")),
|
||||
Boolean.TRUE.equals(row.get("confidential")) || "true".equalsIgnoreCase(str(row.get("confidential"))),
|
||||
Boolean.TRUE.equals(row.get("editable")) || "true".equalsIgnoreCase(str(row.get("editable"))),
|
||||
Boolean.TRUE.equals(row.get("deletable")) || "true".equalsIgnoreCase(str(row.get("deletable")))
|
||||
));
|
||||
}
|
||||
historyGrid.setItems(historyRows);
|
||||
historyGrid.asSingleSelect().clear();
|
||||
clearEditor();
|
||||
updateExportLinks();
|
||||
} catch (Exception e) {
|
||||
showError("Historie konnte nicht geladen werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void loadStatistics() {
|
||||
ObjectTypeOption objectType = objectTypeField.getValue();
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
if (objectType == null || headquartersId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
StringBuilder path = new StringBuilder("/report-workspace/statistics?hqId=")
|
||||
.append(headquartersId)
|
||||
.append("&objectType=").append(objectType.code())
|
||||
.append("&splitByReportType=").append(splitByReportTypeField.getValue())
|
||||
.append("&splitByCustomerType=").append(splitByCustomerTypeField.getValue());
|
||||
if (fromDate.getValue() != null) {
|
||||
path.append("&from=").append(fromDate.getValue());
|
||||
}
|
||||
if (toDate.getValue() != null) {
|
||||
path.append("&to=").append(toDate.getValue());
|
||||
}
|
||||
List<LinkedHashMap<String, Object>> result = restClient.getList(path.toString(), new ParameterizedTypeReference<>() {});
|
||||
List<StatisticRow> rows = new ArrayList<>();
|
||||
for (Map<String, Object> row : result) {
|
||||
rows.add(new StatisticRow(
|
||||
str(row.get("headquartersName")),
|
||||
str(row.get("authorFirstname")),
|
||||
str(row.get("authorName")),
|
||||
str(row.get("reportTypeLabel")),
|
||||
str(row.get("companyTypeLabel")),
|
||||
toLong(row.get("reports")) != null ? toLong(row.get("reports")) : 0L
|
||||
));
|
||||
}
|
||||
statisticGrid.setItems(rows);
|
||||
updateExportLinks();
|
||||
} catch (Exception e) {
|
||||
showError("Statistik konnte nicht geladen werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void saveReport() {
|
||||
ObjectTypeOption objectType = objectTypeField.getValue();
|
||||
ObjectOption object = objectField.getValue();
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
if (objectType == null || object == null || headquartersId == null) {
|
||||
showError("Bitte zuerst Bereich und Objekt waehlen.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Map<String, Object> request = new LinkedHashMap<>();
|
||||
request.put("hqId", headquartersId);
|
||||
request.put("objectType", objectType.code());
|
||||
request.put("objectId", object.id());
|
||||
request.put("reportType", editorReportTypeField.getValue() != null ? editorReportTypeField.getValue().id() : 0);
|
||||
request.put("text", editorTextField.getValue());
|
||||
request.put("confidential", editorConfidentialField.getValue());
|
||||
|
||||
if (selectedReportId == null) {
|
||||
restClient.post("/report-workspace/reports", request, LinkedHashMap.class);
|
||||
} else {
|
||||
restClient.put("/report-workspace/reports/" + selectedReportId, request);
|
||||
}
|
||||
showSuccess("Bericht gespeichert.");
|
||||
loadHistory();
|
||||
} catch (Exception e) {
|
||||
showError("Bericht konnte nicht gespeichert werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteReport() {
|
||||
Long headquartersId = sessionData.getHeadquartersId();
|
||||
if (selectedReportId == null || headquartersId == null || !selectedReportDeletable) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
restClient.delete("/report-workspace/reports/" + selectedReportId + "?hqId=" + headquartersId);
|
||||
showSuccess("Bericht geloescht.");
|
||||
loadHistory();
|
||||
} catch (Exception e) {
|
||||
showError("Bericht konnte nicht geloescht werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void fillEditor(ReportRow row) {
|
||||
if (row == null) {
|
||||
clearEditor();
|
||||
return;
|
||||
}
|
||||
selectedReportId = row.id();
|
||||
selectedReportEditable = row.editable();
|
||||
selectedReportDeletable = row.deletable();
|
||||
editorTextField.setValue(row.text());
|
||||
editorConfidentialField.setValue(row.confidential());
|
||||
editorReportTypeField.setValue(editorTypeOptions.stream()
|
||||
.filter(option -> Objects.equals(option.id(), row.reportType()))
|
||||
.findFirst()
|
||||
.orElse(editorTypeOptions.isEmpty() ? null : editorTypeOptions.get(0)));
|
||||
|
||||
if (row.objectId() != null) {
|
||||
ObjectOption reportObject = new ObjectOption(row.objectId(), row.objectLabel(), "");
|
||||
if (objectOptions.stream().noneMatch(option -> Objects.equals(option.id(), reportObject.id()))) {
|
||||
objectOptions.add(0, reportObject);
|
||||
objectField.setItems(objectOptions);
|
||||
}
|
||||
objectField.setValue(objectOptions.stream()
|
||||
.filter(option -> Objects.equals(option.id(), reportObject.id()))
|
||||
.findFirst()
|
||||
.orElse(reportObject));
|
||||
}
|
||||
refreshEditorButtons();
|
||||
}
|
||||
|
||||
private void clearEditor() {
|
||||
selectedReportId = null;
|
||||
selectedReportEditable = true;
|
||||
selectedReportDeletable = false;
|
||||
historyGrid.asSingleSelect().clear();
|
||||
editorTextField.clear();
|
||||
editorConfidentialField.clear();
|
||||
if (!editorTypeOptions.isEmpty()) {
|
||||
editorReportTypeField.setValue(editorTypeOptions.get(0));
|
||||
} else {
|
||||
editorReportTypeField.clear();
|
||||
}
|
||||
refreshEditorButtons();
|
||||
}
|
||||
|
||||
private void clearObjectSelection() {
|
||||
objectOptions.clear();
|
||||
objectField.clear();
|
||||
objectField.setItems(List.of());
|
||||
updateExportLinks();
|
||||
}
|
||||
|
||||
private void updateSplitCustomerTypeState() {
|
||||
boolean customerMode = objectTypeField.getValue() != null && "cs".equals(objectTypeField.getValue().code());
|
||||
splitByCustomerTypeField.setEnabled(customerMode);
|
||||
if (!customerMode) {
|
||||
splitByCustomerTypeField.setValue(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshEditorButtons() {
|
||||
editorSaveButton.setEnabled(selectedReportId == null || selectedReportEditable);
|
||||
editorDeleteButton.setEnabled(selectedReportId != null && selectedReportDeletable);
|
||||
}
|
||||
|
||||
private void updateExportLinks() {
|
||||
boolean historyEnabled = objectTypeField.getValue() != null && sessionData.canManageReports();
|
||||
boolean statisticEnabled = objectTypeField.getValue() != null && sessionData.canManageReports();
|
||||
historyExportButton.setEnabled(historyEnabled);
|
||||
statisticExportButton.setEnabled(statisticEnabled);
|
||||
if (historyEnabled) {
|
||||
historyExportLink.setHref(historyExportResource());
|
||||
} else {
|
||||
historyExportLink.setHref("");
|
||||
}
|
||||
if (statisticEnabled) {
|
||||
statisticExportLink.setHref(statisticExportResource());
|
||||
} else {
|
||||
statisticExportLink.setHref("");
|
||||
}
|
||||
}
|
||||
|
||||
private StreamResource historyExportResource() {
|
||||
String filename = "berichte_" + objectTypeField.getValue().code() + ".csv";
|
||||
return new StreamResource(filename, () -> new ByteArrayInputStream(restClient.getBytes(buildHistoryExportPath())));
|
||||
}
|
||||
|
||||
private StreamResource statisticExportResource() {
|
||||
String filename = "berichtsstatistik_" + objectTypeField.getValue().code() + ".csv";
|
||||
return new StreamResource(filename, () -> new ByteArrayInputStream(restClient.getBytes(buildStatisticExportPath())));
|
||||
}
|
||||
|
||||
private String buildHistoryExportPath() {
|
||||
StringBuilder path = new StringBuilder("/report-workspace/export/history?hqId=")
|
||||
.append(sessionData.getHeadquartersId())
|
||||
.append("&objectType=").append(objectTypeField.getValue().code());
|
||||
if (objectField.getValue() != null) {
|
||||
path.append("&objectId=").append(objectField.getValue().id());
|
||||
}
|
||||
if (fromDate.getValue() != null) {
|
||||
path.append("&from=").append(fromDate.getValue());
|
||||
}
|
||||
if (toDate.getValue() != null) {
|
||||
path.append("&to=").append(toDate.getValue());
|
||||
}
|
||||
if (historyReportTypeField.getValue() != null && historyReportTypeField.getValue().id() != null
|
||||
&& historyReportTypeField.getValue().id() > 0) {
|
||||
path.append("&reportType=").append(historyReportTypeField.getValue().id());
|
||||
}
|
||||
if (!historySearchField.getValue().isBlank()) {
|
||||
path.append("&search=").append(url(historySearchField.getValue()));
|
||||
}
|
||||
return path.toString();
|
||||
}
|
||||
|
||||
private String buildStatisticExportPath() {
|
||||
StringBuilder path = new StringBuilder("/report-workspace/export/statistics?hqId=")
|
||||
.append(sessionData.getHeadquartersId())
|
||||
.append("&objectType=").append(objectTypeField.getValue().code())
|
||||
.append("&splitByReportType=").append(splitByReportTypeField.getValue())
|
||||
.append("&splitByCustomerType=").append(splitByCustomerTypeField.getValue());
|
||||
if (fromDate.getValue() != null) {
|
||||
path.append("&from=").append(fromDate.getValue());
|
||||
}
|
||||
if (toDate.getValue() != null) {
|
||||
path.append("&to=").append(toDate.getValue());
|
||||
}
|
||||
return path.toString();
|
||||
}
|
||||
|
||||
private void showSuccess(String message) {
|
||||
Notification.show(message, 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
}
|
||||
|
||||
private void showError(String message) {
|
||||
Notification.show(message, 5000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
|
||||
private String url(String value) {
|
||||
return URLEncoder.encode(value != null ? value : "", StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private String str(Object value) {
|
||||
return value != null ? value.toString() : "";
|
||||
}
|
||||
|
||||
private Long toLong(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
if (value instanceof String string && !string.isBlank()) {
|
||||
try {
|
||||
return Long.parseLong(string);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Integer toInteger(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
if (value instanceof String string && !string.isBlank()) {
|
||||
try {
|
||||
return Integer.parseInt(string);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private record ObjectTypeOption(String code, String label) {
|
||||
}
|
||||
|
||||
private record ObjectOption(Long id, String label, String secondaryLabel) {
|
||||
private String labelWithSecondary() {
|
||||
if (secondaryLabel == null || secondaryLabel.isBlank()) {
|
||||
return label;
|
||||
}
|
||||
return label + " [" + secondaryLabel + "]";
|
||||
}
|
||||
}
|
||||
|
||||
private record ReportTypeOption(Integer id, String label) {
|
||||
}
|
||||
|
||||
private record ReportRow(Long id,
|
||||
String objectType,
|
||||
Long objectId,
|
||||
String objectLabel,
|
||||
String headquartersName,
|
||||
Long authorUserId,
|
||||
String authorName,
|
||||
String authorPhone,
|
||||
String createdAt,
|
||||
Integer reportType,
|
||||
String reportTypeLabel,
|
||||
String text,
|
||||
boolean confidential,
|
||||
boolean editable,
|
||||
boolean deletable) {
|
||||
}
|
||||
|
||||
private record StatisticRow(String headquartersName,
|
||||
String authorFirstname,
|
||||
String authorName,
|
||||
String reportTypeLabel,
|
||||
String companyTypeLabel,
|
||||
long reports) {
|
||||
}
|
||||
}
|
||||
107
vaadin/src/main/java/de/votian/web/view/StatisticsView.java
Normal file
107
vaadin/src/main/java/de/votian/web/view/StatisticsView.java
Normal file
@@ -0,0 +1,107 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.datepicker.DatePicker;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.votian.web.model.SessionData;
|
||||
import de.votian.web.service.RestClientService;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Route(value = "statistics", layout = MainLayout.class)
|
||||
@PageTitle("Votian - Statistik")
|
||||
public class StatisticsView extends VerticalLayout {
|
||||
|
||||
private final RestClientService restClient;
|
||||
private final SessionData sessionData;
|
||||
|
||||
private final Span totalJobs = new Span("-");
|
||||
private final Span openJobs = new Span("-");
|
||||
private final Span deliveredJobs = new Span("-");
|
||||
private final Span cancelledJobs = new Span("-");
|
||||
private final Span revenue = new Span("-");
|
||||
private final Span courierCosts = new Span("-");
|
||||
|
||||
public StatisticsView(RestClientService restClient, SessionData sessionData) {
|
||||
this.restClient = restClient;
|
||||
this.sessionData = sessionData;
|
||||
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
|
||||
DatePicker fromDate = new DatePicker("Von");
|
||||
fromDate.setValue(LocalDate.now().withDayOfMonth(1));
|
||||
DatePicker toDate = new DatePicker("Bis");
|
||||
toDate.setValue(LocalDate.now());
|
||||
|
||||
Button loadButton = new Button("Auswerten", event -> load(fromDate.getValue(), toDate.getValue()));
|
||||
loadButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
|
||||
HorizontalLayout filterBar = new HorizontalLayout(fromDate, toDate, loadButton);
|
||||
HorizontalLayout cards = new HorizontalLayout(
|
||||
createCard("Auftraege gesamt", totalJobs),
|
||||
createCard("Offen", openJobs),
|
||||
createCard("Zugestellt", deliveredJobs),
|
||||
createCard("Storniert", cancelledJobs),
|
||||
createCard("Umsatz", revenue),
|
||||
createCard("Kurierkosten", courierCosts)
|
||||
);
|
||||
cards.setWidthFull();
|
||||
|
||||
add(new H3("Statistik"), filterBar, cards);
|
||||
load(fromDate.getValue(), toDate.getValue());
|
||||
}
|
||||
|
||||
private void load(LocalDate fromDate, LocalDate toDate) {
|
||||
if (sessionData.getHeadquartersId() == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> data = restClient.get(
|
||||
"/reporting/statistics?hqId=" + sessionData.getHeadquartersId()
|
||||
+ "&from=" + fromDate + "&to=" + toDate,
|
||||
LinkedHashMap.class
|
||||
);
|
||||
totalJobs.setText(str(data.get("totalJobs")));
|
||||
openJobs.setText(str(data.get("openJobs")));
|
||||
deliveredJobs.setText(str(data.get("deliveredJobs")));
|
||||
cancelledJobs.setText(str(data.get("cancelledJobs")));
|
||||
revenue.setText(str(data.get("totalRevenue")));
|
||||
courierCosts.setText(str(data.get("totalCourierCosts")));
|
||||
} catch (Exception e) {
|
||||
Notification.show("Statistik konnte nicht geladen werden.", 4000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private VerticalLayout createCard(String title, Span value) {
|
||||
VerticalLayout card = new VerticalLayout();
|
||||
card.setPadding(true);
|
||||
card.setSpacing(false);
|
||||
card.getStyle()
|
||||
.set("border", "1px solid var(--lumo-contrast-20pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-l)")
|
||||
.set("min-width", "180px");
|
||||
|
||||
H3 headline = new H3(title);
|
||||
headline.getStyle().set("margin", "0");
|
||||
value.getStyle().set("font-size", "var(--lumo-font-size-xl)").set("font-weight", "700");
|
||||
card.add(headline, value);
|
||||
return card;
|
||||
}
|
||||
|
||||
private String str(Object value) {
|
||||
return value != null ? value.toString() : "-";
|
||||
}
|
||||
}
|
||||
916
vaadin/src/main/java/de/votian/web/view/TrackingView.java
Normal file
916
vaadin/src/main/java/de/votian/web/view/TrackingView.java
Normal file
@@ -0,0 +1,916 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.checkbox.Checkbox;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.datepicker.DatePicker;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.html.H4;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.votian.web.model.SessionData;
|
||||
import de.votian.web.service.RestClientService;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
@Route(value = "tracking", layout = MainLayout.class)
|
||||
@PageTitle("Votian - Tracking")
|
||||
public class TrackingView extends VerticalLayout {
|
||||
|
||||
private static final long ALL_CUSTOMERS_ID = -1L;
|
||||
|
||||
private final RestClientService restClient;
|
||||
private final SessionData sessionData;
|
||||
|
||||
private final DatePicker dayField = new DatePicker("Tag");
|
||||
private final Checkbox includeCompletedField = new Checkbox("Abgeschlossene anzeigen");
|
||||
private final ComboBox<CustomerOption> customerField = new ComboBox<>("Kunde");
|
||||
private final Button reloadButton = new Button("Neu laden", VaadinIcon.REFRESH.create());
|
||||
private final Button openJobButton = new Button("Auftrag oeffnen", VaadinIcon.EXTERNAL_LINK.create());
|
||||
|
||||
private final Span totalJobs = new Span("-");
|
||||
private final Span assignedJobs = new Span("-");
|
||||
private final Span trackedJobs = new Span("-");
|
||||
private final Span totalCouriers = new Span("-");
|
||||
private final Span gpsCouriers = new Span("-");
|
||||
private final Span fallbackCouriers = new Span("-");
|
||||
|
||||
private final Span mapInfo = new Span("-");
|
||||
private final Div mapView = new Div();
|
||||
|
||||
private final Grid<TrackingJobRow> jobGrid = new Grid<>(TrackingJobRow.class, false);
|
||||
private final Grid<TrackingCourierRow> courierGrid = new Grid<>(TrackingCourierRow.class, false);
|
||||
|
||||
private final Span detailJobId = new Span("-");
|
||||
private final Span detailJobStatus = new Span("-");
|
||||
private final Span detailJobCustomer = new Span("-");
|
||||
private final Span detailJobCostCenter = new Span("-");
|
||||
private final Span detailJobRoute = new Span("-");
|
||||
private final Span detailJobCourier = new Span("-");
|
||||
private final Span detailJobTimes = new Span("-");
|
||||
private final Span detailJobTracking = new Span("-");
|
||||
private final Span detailJobPrice = new Span("-");
|
||||
|
||||
private final Span detailCourierSid = new Span("-");
|
||||
private final Span detailCourierCompany = new Span("-");
|
||||
private final Span detailCourierVehicle = new Span("-");
|
||||
private final Span detailCourierJobs = new Span("-");
|
||||
private final Span detailCourierMobile = new Span("-");
|
||||
private final Span detailCourierTracking = new Span("-");
|
||||
private final Span detailCourierAvailable = new Span("-");
|
||||
|
||||
private final List<TrackingJobRow> jobs = new ArrayList<>();
|
||||
private final List<TrackingCourierRow> couriers = new ArrayList<>();
|
||||
|
||||
private TrackingJobRow selectedJob;
|
||||
private TrackingCourierRow selectedCourier;
|
||||
|
||||
public TrackingView(RestClientService restClient, SessionData sessionData) {
|
||||
this.restClient = restClient;
|
||||
this.sessionData = sessionData;
|
||||
|
||||
setSizeFull();
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
|
||||
dayField.setValue(LocalDate.now());
|
||||
customerField.setItemLabelGenerator(CustomerOption::label);
|
||||
customerField.setWidth("320px");
|
||||
customerField.setVisible(sessionData.isHeadquartersUser() && sessionData.canViewCustomers());
|
||||
customerField.addValueChangeListener(event -> loadDashboard());
|
||||
|
||||
includeCompletedField.addValueChangeListener(event -> loadDashboard());
|
||||
|
||||
reloadButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
reloadButton.addClickListener(event -> loadDashboard());
|
||||
|
||||
openJobButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
openJobButton.addClickListener(event -> {
|
||||
if (selectedJob != null && sessionData.canViewJobs()) {
|
||||
getUI().ifPresent(ui -> ui.navigate("jobs/" + selectedJob.jobId()));
|
||||
}
|
||||
});
|
||||
|
||||
configureMapView();
|
||||
configureJobGrid();
|
||||
configureCourierGrid();
|
||||
refreshActionState();
|
||||
clearDetail();
|
||||
|
||||
HorizontalLayout workspace = buildWorkspace();
|
||||
add(
|
||||
new H3("Tracking"),
|
||||
buildFilterBar(),
|
||||
buildCards(),
|
||||
buildMapPanel(),
|
||||
workspace,
|
||||
buildDetailPanels()
|
||||
);
|
||||
expand(workspace);
|
||||
|
||||
if (customerField.isVisible()) {
|
||||
loadCustomers();
|
||||
}
|
||||
loadDashboard();
|
||||
}
|
||||
|
||||
private HorizontalLayout buildFilterBar() {
|
||||
HorizontalLayout filterBar = new HorizontalLayout(dayField, includeCompletedField, customerField, reloadButton, openJobButton);
|
||||
filterBar.setAlignItems(Alignment.BASELINE);
|
||||
filterBar.getStyle().set("flex-wrap", "wrap");
|
||||
return filterBar;
|
||||
}
|
||||
|
||||
private HorizontalLayout buildCards() {
|
||||
HorizontalLayout cards = new HorizontalLayout(
|
||||
createCard("Auftraege", totalJobs),
|
||||
createCard("Mit Kurier", assignedJobs),
|
||||
createCard("Mit Tracking", trackedJobs),
|
||||
createCard("Kuriere", totalCouriers),
|
||||
createCard("GPS", gpsCouriers),
|
||||
createCard("PLZ-Fallback", fallbackCouriers)
|
||||
);
|
||||
cards.setWidthFull();
|
||||
cards.getStyle().set("flex-wrap", "wrap");
|
||||
return cards;
|
||||
}
|
||||
|
||||
private Component buildMapPanel() {
|
||||
VerticalLayout panel = new VerticalLayout(new H4("Karte"), mapInfo, mapView);
|
||||
panel.setPadding(false);
|
||||
panel.setSpacing(true);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private HorizontalLayout buildWorkspace() {
|
||||
VerticalLayout jobPanel = new VerticalLayout(new H4("Auftraege"), jobGrid);
|
||||
jobPanel.setPadding(false);
|
||||
jobPanel.setSpacing(true);
|
||||
jobPanel.setSizeFull();
|
||||
jobPanel.expand(jobGrid);
|
||||
|
||||
VerticalLayout courierPanel = new VerticalLayout(new H4("Kuriere"), courierGrid);
|
||||
courierPanel.setPadding(false);
|
||||
courierPanel.setSpacing(true);
|
||||
courierPanel.setSizeFull();
|
||||
courierPanel.expand(courierGrid);
|
||||
|
||||
HorizontalLayout workspace = new HorizontalLayout(jobPanel, courierPanel);
|
||||
workspace.setSizeFull();
|
||||
workspace.expand(jobPanel, courierPanel);
|
||||
return workspace;
|
||||
}
|
||||
|
||||
private HorizontalLayout buildDetailPanels() {
|
||||
VerticalLayout jobDetailPanel = new VerticalLayout(
|
||||
new H4("Auftragsdetails"),
|
||||
detailLine("Auftrag", detailJobId),
|
||||
detailLine("Status", detailJobStatus),
|
||||
detailLine("Kunde", detailJobCustomer),
|
||||
detailLine("Kostenstelle", detailJobCostCenter),
|
||||
detailLine("Route", detailJobRoute),
|
||||
detailLine("Kurier", detailJobCourier),
|
||||
detailLine("Zeiten", detailJobTimes),
|
||||
detailLine("Tracking", detailJobTracking),
|
||||
detailLine("Preis", detailJobPrice)
|
||||
);
|
||||
jobDetailPanel.setPadding(true);
|
||||
jobDetailPanel.setSpacing(true);
|
||||
jobDetailPanel.getStyle()
|
||||
.set("border", "1px solid var(--lumo-contrast-10pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-m)");
|
||||
|
||||
VerticalLayout courierDetailPanel = new VerticalLayout(
|
||||
new H4("Kurierdetails"),
|
||||
detailLine("Kurier", detailCourierSid),
|
||||
detailLine("Firma", detailCourierCompany),
|
||||
detailLine("Fahrzeug", detailCourierVehicle),
|
||||
detailLine("Aktive Auftraege", detailCourierJobs),
|
||||
detailLine("Mobile/PDA", detailCourierMobile),
|
||||
detailLine("Tracking", detailCourierTracking),
|
||||
detailLine("Verfuegbar seit", detailCourierAvailable)
|
||||
);
|
||||
courierDetailPanel.setPadding(true);
|
||||
courierDetailPanel.setSpacing(true);
|
||||
courierDetailPanel.getStyle()
|
||||
.set("border", "1px solid var(--lumo-contrast-10pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-m)");
|
||||
|
||||
HorizontalLayout panels = new HorizontalLayout(jobDetailPanel, courierDetailPanel);
|
||||
panels.setWidthFull();
|
||||
panels.expand(jobDetailPanel, courierDetailPanel);
|
||||
return panels;
|
||||
}
|
||||
|
||||
private HorizontalLayout detailLine(String label, Span value) {
|
||||
Span title = new Span(label + ":");
|
||||
title.getStyle().set("font-weight", "600").set("min-width", "110px");
|
||||
HorizontalLayout line = new HorizontalLayout(title, value);
|
||||
line.setAlignItems(Alignment.BASELINE);
|
||||
line.setWidthFull();
|
||||
return line;
|
||||
}
|
||||
|
||||
private VerticalLayout createCard(String title, Span value) {
|
||||
Span heading = new Span(title);
|
||||
heading.getStyle().set("font-size", "var(--lumo-font-size-s)");
|
||||
value.getStyle().set("font-size", "var(--lumo-font-size-l)").set("font-weight", "700");
|
||||
VerticalLayout card = new VerticalLayout(heading, value);
|
||||
card.setPadding(true);
|
||||
card.setSpacing(false);
|
||||
card.getStyle()
|
||||
.set("border", "1px solid var(--lumo-contrast-10pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-m)")
|
||||
.set("background", "var(--lumo-base-color)")
|
||||
.set("min-width", "160px");
|
||||
return card;
|
||||
}
|
||||
|
||||
private void configureMapView() {
|
||||
mapView.setWidthFull();
|
||||
mapView.setHeight("360px");
|
||||
mapView.getStyle()
|
||||
.set("border", "1px solid var(--lumo-contrast-10pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-m)")
|
||||
.set("overflow", "hidden")
|
||||
.set("background", "linear-gradient(180deg, #edf6ff 0%, #f7f9fc 100%)");
|
||||
mapInfo.getStyle()
|
||||
.set("font-size", "var(--lumo-font-size-s)")
|
||||
.set("color", "var(--lumo-secondary-text-color)");
|
||||
}
|
||||
|
||||
private void configureJobGrid() {
|
||||
jobGrid.addColumn(TrackingJobRow::jobId).setHeader("Auftrag").setAutoWidth(true).setFrozen(true);
|
||||
jobGrid.addColumn(TrackingJobRow::statusLabel).setHeader("Status").setAutoWidth(true);
|
||||
jobGrid.addColumn(row -> formatDateTime(row.orderTime())).setHeader("Zeit").setAutoWidth(true);
|
||||
jobGrid.addColumn(TrackingJobRow::customerName).setHeader("Kunde").setFlexGrow(1);
|
||||
jobGrid.addColumn(TrackingJobRow::costCenterName).setHeader("Kostenstelle").setFlexGrow(1);
|
||||
jobGrid.addColumn(TrackingJobRow::pickupLabel).setHeader("Start").setFlexGrow(1);
|
||||
jobGrid.addColumn(TrackingJobRow::deliveryLabel).setHeader("Ziel").setFlexGrow(1);
|
||||
jobGrid.addColumn(TrackingJobRow::courierSid).setHeader("Kurier").setAutoWidth(true);
|
||||
jobGrid.addColumn(TrackingJobRow::trackingLabel).setHeader("Tracking").setAutoWidth(true);
|
||||
jobGrid.addColumn(row -> formatAmount(row.totalPrice())).setHeader("Preis").setAutoWidth(true);
|
||||
jobGrid.setSizeFull();
|
||||
jobGrid.asSingleSelect().addValueChangeListener(event -> {
|
||||
selectedJob = event.getValue();
|
||||
if (selectedJob != null && selectedJob.courierId() != null) {
|
||||
TrackingCourierRow matchingCourier = couriers.stream()
|
||||
.filter(courier -> selectedJob.courierId().equals(courier.courierId()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
courierGrid.asSingleSelect().setValue(matchingCourier);
|
||||
selectedCourier = matchingCourier;
|
||||
}
|
||||
if (selectedJob == null) {
|
||||
selectedCourier = courierGrid.asSingleSelect().getValue();
|
||||
}
|
||||
updateDetail();
|
||||
updateMap();
|
||||
refreshActionState();
|
||||
});
|
||||
}
|
||||
|
||||
private void configureCourierGrid() {
|
||||
courierGrid.addColumn(TrackingCourierRow::courierSid).setHeader("Kurier").setAutoWidth(true).setFrozen(true);
|
||||
courierGrid.addColumn(TrackingCourierRow::companyName).setHeader("Firma").setFlexGrow(1);
|
||||
courierGrid.addColumn(TrackingCourierRow::vehicleDisplay).setHeader("Fahrzeug").setAutoWidth(true);
|
||||
courierGrid.addColumn(TrackingCourierRow::activeJobs).setHeader("Jobs").setAutoWidth(true);
|
||||
courierGrid.addColumn(TrackingCourierRow::locationZipcode).setHeader("PLZ").setAutoWidth(true);
|
||||
courierGrid.addColumn(TrackingCourierRow::trackingLabel).setHeader("Tracking").setAutoWidth(true);
|
||||
courierGrid.addColumn(row -> formatDateTime(row.gpsTime())).setHeader("Zeit").setAutoWidth(true);
|
||||
courierGrid.setSizeFull();
|
||||
courierGrid.asSingleSelect().addValueChangeListener(event -> {
|
||||
selectedCourier = event.getValue();
|
||||
updateDetail();
|
||||
updateMap();
|
||||
});
|
||||
}
|
||||
|
||||
private void loadCustomers() {
|
||||
try {
|
||||
List<LinkedHashMap<String, Object>> rows = restClient.getList(
|
||||
"/customers/hq/" + sessionData.getHeadquartersId(),
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
List<CustomerOption> options = new ArrayList<>();
|
||||
options.add(new CustomerOption(ALL_CUSTOMERS_ID, "Alle Kunden"));
|
||||
for (Map<String, Object> row : rows) {
|
||||
Long id = toLong(row.get("id"));
|
||||
if (id != null) {
|
||||
options.add(new CustomerOption(id, str(row.get("companyName"))));
|
||||
}
|
||||
}
|
||||
options.sort(Comparator.comparing(CustomerOption::label, String.CASE_INSENSITIVE_ORDER));
|
||||
if (!options.isEmpty()) {
|
||||
CustomerOption allOption = options.stream()
|
||||
.filter(option -> option.id().equals(ALL_CUSTOMERS_ID))
|
||||
.findFirst()
|
||||
.orElse(new CustomerOption(ALL_CUSTOMERS_ID, "Alle Kunden"));
|
||||
options.removeIf(option -> option.id().equals(ALL_CUSTOMERS_ID));
|
||||
options.add(0, allOption);
|
||||
}
|
||||
customerField.setItems(options);
|
||||
customerField.setValue(options.getFirst());
|
||||
} catch (Exception e) {
|
||||
Notification.show("Kundenfilter konnte nicht geladen werden: " + e.getMessage(), 5000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadDashboard() {
|
||||
if (sessionData.getHeadquartersId() == null || dayField.getValue() == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
StringBuilder path = new StringBuilder("/tracking/dashboard?hqId=")
|
||||
.append(sessionData.getHeadquartersId())
|
||||
.append("&day=").append(dayField.getValue())
|
||||
.append("&includeCompleted=").append(includeCompletedField.getValue());
|
||||
CustomerOption customer = customerField.getValue();
|
||||
if (customerField.isVisible() && customer != null && !customer.id().equals(ALL_CUSTOMERS_ID)) {
|
||||
path.append("&customerId=").append(customer.id());
|
||||
}
|
||||
|
||||
Map<String, Object> data = restClient.getMap(path.toString());
|
||||
jobs.clear();
|
||||
jobs.addAll(parseJobs(data.get("jobs")));
|
||||
couriers.clear();
|
||||
couriers.addAll(parseCouriers(data.get("couriers")));
|
||||
|
||||
totalJobs.setText(str(data.get("totalJobs"), jobs.size()));
|
||||
assignedJobs.setText(str(data.get("assignedJobs"), 0));
|
||||
trackedJobs.setText(str(data.get("trackedJobs"), 0));
|
||||
totalCouriers.setText(str(data.get("totalCouriers"), couriers.size()));
|
||||
gpsCouriers.setText(str(data.get("gpsCouriers"), 0));
|
||||
fallbackCouriers.setText(str(data.get("fallbackCouriers"), 0));
|
||||
|
||||
jobGrid.setItems(jobs);
|
||||
courierGrid.setItems(couriers);
|
||||
|
||||
if (selectedJob != null) {
|
||||
TrackingJobRow persistedJob = jobs.stream()
|
||||
.filter(row -> row.jobId().equals(selectedJob.jobId()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
jobGrid.asSingleSelect().setValue(persistedJob);
|
||||
selectedJob = persistedJob;
|
||||
} else {
|
||||
jobGrid.asSingleSelect().clear();
|
||||
}
|
||||
|
||||
if (selectedCourier != null) {
|
||||
TrackingCourierRow persistedCourier = couriers.stream()
|
||||
.filter(row -> row.courierId().equals(selectedCourier.courierId()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
courierGrid.asSingleSelect().setValue(persistedCourier);
|
||||
selectedCourier = persistedCourier;
|
||||
} else {
|
||||
courierGrid.asSingleSelect().clear();
|
||||
}
|
||||
|
||||
if (selectedJob == null && !jobs.isEmpty()) {
|
||||
jobGrid.asSingleSelect().setValue(jobs.getFirst());
|
||||
selectedJob = jobs.getFirst();
|
||||
}
|
||||
|
||||
if (selectedCourier == null && !couriers.isEmpty()) {
|
||||
selectedCourier = couriers.getFirst();
|
||||
courierGrid.asSingleSelect().setValue(selectedCourier);
|
||||
}
|
||||
|
||||
updateDetail();
|
||||
updateMap();
|
||||
refreshActionState();
|
||||
} catch (Exception e) {
|
||||
Notification.show("Tracking konnte nicht geladen werden: " + e.getMessage(), 5000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private List<TrackingJobRow> parseJobs(Object rawValue) {
|
||||
if (!(rawValue instanceof List<?> rawList)) {
|
||||
return List.of();
|
||||
}
|
||||
List<TrackingJobRow> rows = new ArrayList<>();
|
||||
for (Object entry : rawList) {
|
||||
if (!(entry instanceof Map<?, ?> rawMap)) {
|
||||
continue;
|
||||
}
|
||||
rows.add(new TrackingJobRow(
|
||||
toLong(rawMap.get("jobId")),
|
||||
toInteger(rawMap.get("status")),
|
||||
str(rawMap.get("statusLabel")),
|
||||
str(rawMap.get("orderTime")),
|
||||
str(rawMap.get("takeTime")),
|
||||
str(rawMap.get("finishTime")),
|
||||
toLong(rawMap.get("customerId")),
|
||||
str(rawMap.get("customerName")),
|
||||
str(rawMap.get("costCenterName")),
|
||||
str(rawMap.get("commissionNo")),
|
||||
str(rawMap.get("pickupName")),
|
||||
str(rawMap.get("pickupZipcode")),
|
||||
str(rawMap.get("pickupCity")),
|
||||
toDouble(rawMap.get("pickupLatitude")),
|
||||
toDouble(rawMap.get("pickupLongitude")),
|
||||
str(rawMap.get("deliveryName")),
|
||||
str(rawMap.get("deliveryZipcode")),
|
||||
str(rawMap.get("deliveryCity")),
|
||||
toDouble(rawMap.get("deliveryLatitude")),
|
||||
toDouble(rawMap.get("deliveryLongitude")),
|
||||
toLong(rawMap.get("courierId")),
|
||||
str(rawMap.get("courierSid")),
|
||||
str(rawMap.get("courierCompanyName")),
|
||||
str(rawMap.get("courierVehicleSid")),
|
||||
str(rawMap.get("vehicleTypeName")),
|
||||
toDouble(rawMap.get("courierLatitude")),
|
||||
toDouble(rawMap.get("courierLongitude")),
|
||||
str(rawMap.get("courierGpsTime")),
|
||||
str(rawMap.get("courierGpsTypeLabel")),
|
||||
str(rawMap.get("courierLocationZipcode")),
|
||||
bool(rawMap.get("courierTracked")),
|
||||
bool(rawMap.get("courierGpsBased")),
|
||||
toBigDecimal(rawMap.get("totalPrice"))
|
||||
));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private List<TrackingCourierRow> parseCouriers(Object rawValue) {
|
||||
if (!(rawValue instanceof List<?> rawList)) {
|
||||
return List.of();
|
||||
}
|
||||
List<TrackingCourierRow> rows = new ArrayList<>();
|
||||
for (Object entry : rawList) {
|
||||
if (!(entry instanceof Map<?, ?> rawMap)) {
|
||||
continue;
|
||||
}
|
||||
rows.add(new TrackingCourierRow(
|
||||
toLong(rawMap.get("courierId")),
|
||||
str(rawMap.get("courierSid")),
|
||||
str(rawMap.get("companyName")),
|
||||
str(rawMap.get("vehicleDisplay")),
|
||||
toInteger(rawMap.get("activeJobs")) != null ? toInteger(rawMap.get("activeJobs")) : 0,
|
||||
str(rawMap.get("locationZipcode")),
|
||||
toDouble(rawMap.get("latitude")),
|
||||
toDouble(rawMap.get("longitude")),
|
||||
str(rawMap.get("gpsTime")),
|
||||
str(rawMap.get("gpsTypeLabel")),
|
||||
str(rawMap.get("mobilePda")),
|
||||
str(rawMap.get("availableTime")),
|
||||
bool(rawMap.get("tracked")),
|
||||
bool(rawMap.get("gpsBased"))
|
||||
));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private void refreshActionState() {
|
||||
openJobButton.setEnabled(selectedJob != null && sessionData.canViewJobs());
|
||||
}
|
||||
|
||||
private void updateDetail() {
|
||||
if (selectedJob == null) {
|
||||
detailJobId.setText("-");
|
||||
detailJobStatus.setText("-");
|
||||
detailJobCustomer.setText("-");
|
||||
detailJobCostCenter.setText("-");
|
||||
detailJobRoute.setText("-");
|
||||
detailJobCourier.setText("-");
|
||||
detailJobTimes.setText("-");
|
||||
detailJobTracking.setText("-");
|
||||
detailJobPrice.setText("-");
|
||||
} else {
|
||||
detailJobId.setText(str(selectedJob.jobId()));
|
||||
detailJobStatus.setText(orDash(selectedJob.statusLabel()));
|
||||
detailJobCustomer.setText(orDash(selectedJob.customerName()));
|
||||
detailJobCostCenter.setText(orDash(selectedJob.costCenterName()));
|
||||
detailJobRoute.setText(orDash(selectedJob.pickupLabel() + " -> " + selectedJob.deliveryLabel()));
|
||||
detailJobCourier.setText(orDash(selectedJob.courierSid()));
|
||||
detailJobTimes.setText(orDash(formatJobTimes(selectedJob)));
|
||||
detailJobTracking.setText(orDash(selectedJob.trackingLabel()));
|
||||
detailJobPrice.setText(orDash(formatAmount(selectedJob.totalPrice())));
|
||||
}
|
||||
|
||||
if (selectedCourier == null) {
|
||||
detailCourierSid.setText("-");
|
||||
detailCourierCompany.setText("-");
|
||||
detailCourierVehicle.setText("-");
|
||||
detailCourierJobs.setText("-");
|
||||
detailCourierMobile.setText("-");
|
||||
detailCourierTracking.setText("-");
|
||||
detailCourierAvailable.setText("-");
|
||||
} else {
|
||||
detailCourierSid.setText(orDash(selectedCourier.courierSid()));
|
||||
detailCourierCompany.setText(orDash(selectedCourier.companyName()));
|
||||
detailCourierVehicle.setText(orDash(selectedCourier.vehicleDisplay()));
|
||||
detailCourierJobs.setText(String.valueOf(selectedCourier.activeJobs()));
|
||||
detailCourierMobile.setText(orDash(selectedCourier.mobilePda()));
|
||||
detailCourierTracking.setText(orDash(selectedCourier.trackingLabel()));
|
||||
detailCourierAvailable.setText(orDash(formatDateTime(selectedCourier.availableTime())));
|
||||
}
|
||||
}
|
||||
|
||||
private void clearDetail() {
|
||||
selectedJob = null;
|
||||
selectedCourier = null;
|
||||
updateDetail();
|
||||
}
|
||||
|
||||
private void updateMap() {
|
||||
long mappedJobs = jobs.stream().filter(TrackingJobRow::hasMapRoute).count();
|
||||
long trackedCouriers = couriers.stream().filter(TrackingCourierRow::hasPoint).count();
|
||||
if (jobs.isEmpty() && couriers.isEmpty()) {
|
||||
mapInfo.setText("Keine Trackingdaten fuer den gewaehlten Tag.");
|
||||
mapView.getElement().setProperty("innerHTML", buildEmptyMapSvg("Keine Trackingdaten"));
|
||||
return;
|
||||
}
|
||||
mapInfo.setText(mappedJobs + " geocodierte Auftragsrouten, " + trackedCouriers + " ortbare Kuriere");
|
||||
mapView.getElement().setProperty("innerHTML", buildMapSvg());
|
||||
}
|
||||
|
||||
private String buildMapSvg() {
|
||||
List<PointSource> points = new ArrayList<>();
|
||||
for (TrackingJobRow row : jobs) {
|
||||
if (row.hasMapRoute()) {
|
||||
points.add(new PointSource(row.pickupLatitude(), row.pickupLongitude()));
|
||||
points.add(new PointSource(row.deliveryLatitude(), row.deliveryLongitude()));
|
||||
}
|
||||
}
|
||||
for (TrackingCourierRow row : couriers) {
|
||||
if (row.hasPoint()) {
|
||||
points.add(new PointSource(row.latitude(), row.longitude()));
|
||||
}
|
||||
}
|
||||
if (points.isEmpty()) {
|
||||
return buildEmptyMapSvg("Keine geocodierten Trackingdaten");
|
||||
}
|
||||
|
||||
double minLongitude = points.stream().mapToDouble(PointSource::longitude).min().orElse(5.5d);
|
||||
double maxLongitude = points.stream().mapToDouble(PointSource::longitude).max().orElse(15.5d);
|
||||
double minLatitude = points.stream().mapToDouble(PointSource::latitude).min().orElse(47.0d);
|
||||
double maxLatitude = points.stream().mapToDouble(PointSource::latitude).max().orElse(55.0d);
|
||||
|
||||
double longitudeSpan = Math.max(maxLongitude - minLongitude, 4.0d);
|
||||
double latitudeSpan = Math.max(maxLatitude - minLatitude, 3.0d);
|
||||
minLongitude -= longitudeSpan * 0.12d;
|
||||
maxLongitude += longitudeSpan * 0.12d;
|
||||
minLatitude -= latitudeSpan * 0.12d;
|
||||
maxLatitude += latitudeSpan * 0.12d;
|
||||
|
||||
int width = 820;
|
||||
int height = 360;
|
||||
int padding = 26;
|
||||
StringBuilder svg = new StringBuilder();
|
||||
svg.append("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 ").append(width).append(' ').append(height).append("'>");
|
||||
svg.append("<defs>");
|
||||
svg.append("<linearGradient id='tracking-bg' x1='0' y1='0' x2='0' y2='1'>")
|
||||
.append("<stop offset='0%' stop-color='#edf6ff'/>")
|
||||
.append("<stop offset='100%' stop-color='#f7f9fc'/>")
|
||||
.append("</linearGradient>");
|
||||
svg.append("</defs>");
|
||||
svg.append("<rect width='").append(width).append("' height='").append(height).append("' fill='url(#tracking-bg)'/>");
|
||||
svg.append("<rect x='").append(padding).append("' y='").append(padding).append("' width='")
|
||||
.append(width - (padding * 2)).append("' height='").append(height - (padding * 2))
|
||||
.append("' rx='18' fill='#ffffff' fill-opacity='0.55' stroke='#0f172a' stroke-opacity='0.10'/>");
|
||||
|
||||
for (int step = 1; step < 5; step++) {
|
||||
double verticalX = padding + ((width - (padding * 2)) / 5.0d) * step;
|
||||
double horizontalY = padding + ((height - (padding * 2)) / 5.0d) * step;
|
||||
svg.append("<line x1='").append(roundSvg(verticalX)).append("' y1='").append(padding)
|
||||
.append("' x2='").append(roundSvg(verticalX)).append("' y2='").append(height - padding)
|
||||
.append("' stroke='#0f172a' stroke-opacity='0.08' stroke-dasharray='4 8'/>");
|
||||
svg.append("<line x1='").append(padding).append("' y1='").append(roundSvg(horizontalY))
|
||||
.append("' x2='").append(width - padding).append("' y2='").append(roundSvg(horizontalY))
|
||||
.append("' stroke='#0f172a' stroke-opacity='0.08' stroke-dasharray='4 8'/>");
|
||||
}
|
||||
|
||||
for (TrackingJobRow row : jobs) {
|
||||
appendRoute(svg, row, minLongitude, maxLongitude, minLatitude, maxLatitude, width, height, padding);
|
||||
}
|
||||
for (TrackingCourierRow row : couriers) {
|
||||
appendCourierMarker(svg, row, minLongitude, maxLongitude, minLatitude, maxLatitude, width, height, padding);
|
||||
}
|
||||
|
||||
svg.append("</svg>");
|
||||
return svg.toString();
|
||||
}
|
||||
|
||||
private void appendRoute(StringBuilder svg,
|
||||
TrackingJobRow row,
|
||||
double minLongitude,
|
||||
double maxLongitude,
|
||||
double minLatitude,
|
||||
double maxLatitude,
|
||||
int width,
|
||||
int height,
|
||||
int padding) {
|
||||
if (!row.hasMapRoute()) {
|
||||
return;
|
||||
}
|
||||
double x1 = projectLongitude(row.pickupLongitude(), minLongitude, maxLongitude, width, padding);
|
||||
double y1 = projectLatitude(row.pickupLatitude(), minLatitude, maxLatitude, height, padding);
|
||||
double x2 = projectLongitude(row.deliveryLongitude(), minLongitude, maxLongitude, width, padding);
|
||||
double y2 = projectLatitude(row.deliveryLatitude(), minLatitude, maxLatitude, height, padding);
|
||||
String color = statusColor(row.status());
|
||||
boolean selected = selectedJob != null && row.jobId().equals(selectedJob.jobId());
|
||||
String tooltip = escapeSvgText("Auftrag " + row.jobId() + " | " + row.pickupLabel() + " -> " + row.deliveryLabel());
|
||||
|
||||
if (selected) {
|
||||
svg.append("<line x1='").append(roundSvg(x1)).append("' y1='").append(roundSvg(y1))
|
||||
.append("' x2='").append(roundSvg(x2)).append("' y2='").append(roundSvg(y2))
|
||||
.append("' stroke='#101828' stroke-opacity='0.22' stroke-width='8' stroke-linecap='round'/>");
|
||||
}
|
||||
|
||||
svg.append("<line x1='").append(roundSvg(x1)).append("' y1='").append(roundSvg(y1))
|
||||
.append("' x2='").append(roundSvg(x2)).append("' y2='").append(roundSvg(y2))
|
||||
.append("' stroke='").append(color).append("' stroke-width='").append(selected ? "4.5" : "3")
|
||||
.append("' stroke-linecap='round' stroke-opacity='0.88'>");
|
||||
svg.append("<title>").append(tooltip).append("</title></line>");
|
||||
svg.append("<circle cx='").append(roundSvg(x1)).append("' cy='").append(roundSvg(y1))
|
||||
.append("' r='").append(selected ? "5.5" : "4").append("' fill='#ffffff' stroke='")
|
||||
.append(color).append("' stroke-width='2'/>");
|
||||
svg.append("<circle cx='").append(roundSvg(x2)).append("' cy='").append(roundSvg(y2))
|
||||
.append("' r='").append(selected ? "5.5" : "4.5").append("' fill='").append(color)
|
||||
.append("' stroke='#ffffff' stroke-width='1.5'/>");
|
||||
}
|
||||
|
||||
private void appendCourierMarker(StringBuilder svg,
|
||||
TrackingCourierRow row,
|
||||
double minLongitude,
|
||||
double maxLongitude,
|
||||
double minLatitude,
|
||||
double maxLatitude,
|
||||
int width,
|
||||
int height,
|
||||
int padding) {
|
||||
if (!row.hasPoint()) {
|
||||
return;
|
||||
}
|
||||
double x = projectLongitude(row.longitude(), minLongitude, maxLongitude, width, padding);
|
||||
double y = projectLatitude(row.latitude(), minLatitude, maxLatitude, height, padding);
|
||||
boolean selected = selectedCourier != null && row.courierId().equals(selectedCourier.courierId());
|
||||
String fill = row.gpsBased() ? "#175cd3" : "#b54708";
|
||||
String tooltip = escapeSvgText("Kurier " + row.courierSid() + " | " + row.trackingLabel());
|
||||
|
||||
if (selected) {
|
||||
svg.append("<circle cx='").append(roundSvg(x)).append("' cy='").append(roundSvg(y))
|
||||
.append("' r='12' fill='#101828' fill-opacity='0.15'/>");
|
||||
}
|
||||
svg.append("<circle cx='").append(roundSvg(x)).append("' cy='").append(roundSvg(y))
|
||||
.append("' r='").append(selected ? "7" : "5.5").append("' fill='").append(fill)
|
||||
.append("' stroke='#ffffff' stroke-width='2'>");
|
||||
svg.append("<title>").append(tooltip).append("</title></circle>");
|
||||
}
|
||||
|
||||
private String buildEmptyMapSvg(String message) {
|
||||
return "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 820 360'>"
|
||||
+ "<rect width='820' height='360' fill='#f7f9fc'/>"
|
||||
+ "<rect x='26' y='26' width='768' height='308' rx='18' fill='#ffffff' stroke='#0f172a' stroke-opacity='0.10'/>"
|
||||
+ "<text x='410' y='180' text-anchor='middle' fill='#475467' font-size='16' font-family='Arial, sans-serif'>"
|
||||
+ escapeSvgText(message)
|
||||
+ "</text></svg>";
|
||||
}
|
||||
|
||||
private double projectLongitude(Double longitude, double minLongitude, double maxLongitude, int width, int padding) {
|
||||
double usableWidth = width - (padding * 2.0d);
|
||||
return padding + ((longitude - minLongitude) / (maxLongitude - minLongitude)) * usableWidth;
|
||||
}
|
||||
|
||||
private double projectLatitude(Double latitude, double minLatitude, double maxLatitude, int height, int padding) {
|
||||
double usableHeight = height - (padding * 2.0d);
|
||||
return padding + (1.0d - ((latitude - minLatitude) / (maxLatitude - minLatitude))) * usableHeight;
|
||||
}
|
||||
|
||||
private String roundSvg(double value) {
|
||||
return String.format(Locale.ROOT, "%.2f", value);
|
||||
}
|
||||
|
||||
private String escapeSvgText(String value) {
|
||||
return value
|
||||
.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\"", """)
|
||||
.replace("'", "'");
|
||||
}
|
||||
|
||||
private String statusColor(Integer status) {
|
||||
if (status == null) {
|
||||
return "#667085";
|
||||
}
|
||||
return switch (status) {
|
||||
case 1 -> "#175cd3";
|
||||
case 2 -> "#087443";
|
||||
case 8 -> "#087443";
|
||||
case 9 -> "#b42318";
|
||||
default -> "#667085";
|
||||
};
|
||||
}
|
||||
|
||||
private String formatJobTimes(TrackingJobRow row) {
|
||||
List<String> values = new ArrayList<>();
|
||||
if (!row.orderTime().isBlank()) {
|
||||
values.add("Auftrag " + formatDateTime(row.orderTime()));
|
||||
}
|
||||
if (!row.takeTime().isBlank()) {
|
||||
values.add("Uebernommen " + formatDateTime(row.takeTime()));
|
||||
}
|
||||
if (!row.finishTime().isBlank()) {
|
||||
values.add("Abschluss " + formatDateTime(row.finishTime()));
|
||||
}
|
||||
return values.isEmpty() ? "" : String.join(" / ", values);
|
||||
}
|
||||
|
||||
private String formatDateTime(String value) {
|
||||
return value == null || value.isBlank() ? "" : value.replace("T", " ");
|
||||
}
|
||||
|
||||
private String formatAmount(BigDecimal value) {
|
||||
return value != null ? value.toPlainString() : "";
|
||||
}
|
||||
|
||||
private String orDash(String value) {
|
||||
return value == null || value.isBlank() ? "-" : value;
|
||||
}
|
||||
|
||||
private Long toLong(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
try {
|
||||
return value != null ? Long.parseLong(value.toString()) : null;
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Integer toInteger(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
try {
|
||||
return value != null ? Integer.parseInt(value.toString()) : null;
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Double toDouble(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return number.doubleValue();
|
||||
}
|
||||
try {
|
||||
return value != null ? Double.parseDouble(value.toString()) : null;
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal toBigDecimal(Object value) {
|
||||
if (value instanceof BigDecimal decimal) {
|
||||
return decimal;
|
||||
}
|
||||
try {
|
||||
return value != null ? new BigDecimal(value.toString()) : null;
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean bool(Object value) {
|
||||
return Boolean.TRUE.equals(value) || "true".equalsIgnoreCase(str(value));
|
||||
}
|
||||
|
||||
private String str(Object value) {
|
||||
return value != null ? value.toString() : "";
|
||||
}
|
||||
|
||||
private String str(Object value, Object fallback) {
|
||||
String text = str(value);
|
||||
return text.isBlank() ? String.valueOf(fallback) : text;
|
||||
}
|
||||
|
||||
private record CustomerOption(Long id, String label) {
|
||||
}
|
||||
|
||||
private record PointSource(Double latitude, Double longitude) {
|
||||
}
|
||||
|
||||
private record TrackingJobRow(Long jobId,
|
||||
Integer status,
|
||||
String statusLabel,
|
||||
String orderTime,
|
||||
String takeTime,
|
||||
String finishTime,
|
||||
Long customerId,
|
||||
String customerName,
|
||||
String costCenterName,
|
||||
String commissionNo,
|
||||
String pickupName,
|
||||
String pickupZipcode,
|
||||
String pickupCity,
|
||||
Double pickupLatitude,
|
||||
Double pickupLongitude,
|
||||
String deliveryName,
|
||||
String deliveryZipcode,
|
||||
String deliveryCity,
|
||||
Double deliveryLatitude,
|
||||
Double deliveryLongitude,
|
||||
Long courierId,
|
||||
String courierSid,
|
||||
String courierCompanyName,
|
||||
String courierVehicleSid,
|
||||
String vehicleTypeName,
|
||||
Double courierLatitude,
|
||||
Double courierLongitude,
|
||||
String courierGpsTime,
|
||||
String courierGpsTypeLabel,
|
||||
String courierLocationZipcode,
|
||||
boolean courierTracked,
|
||||
boolean courierGpsBased,
|
||||
BigDecimal totalPrice) {
|
||||
|
||||
private boolean hasMapRoute() {
|
||||
return pickupLatitude != null && pickupLongitude != null && deliveryLatitude != null && deliveryLongitude != null;
|
||||
}
|
||||
|
||||
private String pickupLabel() {
|
||||
return joinNonBlank(pickupName, joinNonBlank(pickupZipcode, pickupCity));
|
||||
}
|
||||
|
||||
private String deliveryLabel() {
|
||||
return joinNonBlank(deliveryName, joinNonBlank(deliveryZipcode, deliveryCity));
|
||||
}
|
||||
|
||||
private String trackingLabel() {
|
||||
if (!courierTracked) {
|
||||
return "";
|
||||
}
|
||||
String mode = courierGpsBased ? "GPS" : "PLZ";
|
||||
String location = courierLocationZipcode != null && !courierLocationZipcode.isBlank() ? " / " + courierLocationZipcode : "";
|
||||
String time = courierGpsTime != null && !courierGpsTime.isBlank() ? " / " + courierGpsTime.replace("T", " ") : "";
|
||||
return mode + location + time;
|
||||
}
|
||||
}
|
||||
|
||||
private record TrackingCourierRow(Long courierId,
|
||||
String courierSid,
|
||||
String companyName,
|
||||
String vehicleDisplay,
|
||||
int activeJobs,
|
||||
String locationZipcode,
|
||||
Double latitude,
|
||||
Double longitude,
|
||||
String gpsTime,
|
||||
String gpsTypeLabel,
|
||||
String mobilePda,
|
||||
String availableTime,
|
||||
boolean tracked,
|
||||
boolean gpsBased) {
|
||||
|
||||
private boolean hasPoint() {
|
||||
return latitude != null && longitude != null;
|
||||
}
|
||||
|
||||
private String trackingLabel() {
|
||||
if (!tracked) {
|
||||
return "";
|
||||
}
|
||||
String mode = gpsBased ? "GPS" : "PLZ";
|
||||
String zipcode = locationZipcode != null && !locationZipcode.isBlank() ? " / " + locationZipcode : "";
|
||||
String time = gpsTime != null && !gpsTime.isBlank() ? " / " + gpsTime.replace("T", " ") : "";
|
||||
return mode + zipcode + time;
|
||||
}
|
||||
}
|
||||
|
||||
private static String joinNonBlank(String left, String right) {
|
||||
String leftValue = left != null ? left : "";
|
||||
String rightValue = right != null ? right : "";
|
||||
if (leftValue.isBlank()) {
|
||||
return rightValue;
|
||||
}
|
||||
if (rightValue.isBlank()) {
|
||||
return leftValue;
|
||||
}
|
||||
return leftValue + " / " + rightValue;
|
||||
}
|
||||
}
|
||||
843
vaadin/src/main/java/de/votian/web/view/WarehouseView.java
Normal file
843
vaadin/src/main/java/de/votian/web/view/WarehouseView.java
Normal file
@@ -0,0 +1,843 @@
|
||||
package de.votian.web.view;
|
||||
|
||||
import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.checkbox.Checkbox;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.datepicker.DatePicker;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.html.H4;
|
||||
import com.vaadin.flow.component.html.Paragraph;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.tabs.Tab;
|
||||
import com.vaadin.flow.component.tabs.Tabs;
|
||||
import com.vaadin.flow.component.textfield.IntegerField;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.votian.web.service.RestClientService;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Route(value = "warehouse", layout = MainLayout.class)
|
||||
@PageTitle("Votian - Lager")
|
||||
public class WarehouseView extends VerticalLayout {
|
||||
|
||||
private final RestClientService restClient;
|
||||
|
||||
private final ComboBox<RootOption> rootStockField = new ComboBox<>("Hauptlager");
|
||||
private final Span selectedStockText = new Span("-");
|
||||
private final Span selectedCapacityText = new Span("-");
|
||||
private final Span accessText = new Span("-");
|
||||
|
||||
private final Checkbox includeSubstocksField = new Checkbox("Unterlager einbeziehen", true);
|
||||
private final TextField inventorySearchField = new TextField("Bestandssuche");
|
||||
|
||||
private final Grid<StockRow> stockGrid = new Grid<>(StockRow.class, false);
|
||||
private final Grid<InventoryRow> inventoryGrid = new Grid<>(InventoryRow.class, false);
|
||||
private final Grid<SerialRow> serialGrid = new Grid<>(SerialRow.class, false);
|
||||
private final Grid<JournalRow> journalGrid = new Grid<>(JournalRow.class, false);
|
||||
|
||||
private final ComboBox<ArticleOption> movementArticleField = new ComboBox<>("Artikel");
|
||||
private final ComboBox<MovementStockOption> movementSourceField = new ComboBox<>("Quelle");
|
||||
private final ComboBox<MovementStockOption> movementTargetField = new ComboBox<>("Ziel");
|
||||
private final IntegerField movementQuantityField = new IntegerField("Menge");
|
||||
private final TextField movementSerialField = new TextField("Seriennummer");
|
||||
private final TextField movementTanField = new TextField("TAN");
|
||||
private final TextField movementJobField = new TextField("Auftrag");
|
||||
private final TextField movementRemarkField = new TextField("Bemerkung");
|
||||
private final Button moveButton = new Button("Buchen");
|
||||
|
||||
private final DatePicker journalFromField = new DatePicker("Von");
|
||||
private final DatePicker journalToField = new DatePicker("Bis");
|
||||
private final ComboBox<MovementStockOption> journalSourceField = new ComboBox<>("Quelle");
|
||||
private final ComboBox<MovementStockOption> journalTargetField = new ComboBox<>("Ziel");
|
||||
private final TextField journalSearchField = new TextField("Journalfilter");
|
||||
|
||||
private final List<RootOption> rootOptions = new ArrayList<>();
|
||||
private final List<StockRow> stockRows = new ArrayList<>();
|
||||
private final List<ArticleOption> articleOptions = new ArrayList<>();
|
||||
|
||||
private boolean canMove;
|
||||
private StockRow selectedStock;
|
||||
private InventoryRow selectedInventory;
|
||||
private SerialRow selectedSerial;
|
||||
|
||||
public WarehouseView(RestClientService restClient) {
|
||||
this.restClient = restClient;
|
||||
|
||||
setSizeFull();
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
|
||||
configureSelectors();
|
||||
configureGrids();
|
||||
configureMovementForm();
|
||||
configureJournalFilters();
|
||||
|
||||
VerticalLayout inventoryContent = buildInventoryContent();
|
||||
VerticalLayout movementContent = buildMovementContent();
|
||||
VerticalLayout journalContent = buildJournalContent();
|
||||
movementContent.setVisible(false);
|
||||
journalContent.setVisible(false);
|
||||
|
||||
Tab inventoryTab = new Tab("Bestand");
|
||||
Tab movementTab = new Tab("Bewegung");
|
||||
Tab journalTab = new Tab("Journal");
|
||||
Tabs tabs = new Tabs(inventoryTab, movementTab, journalTab);
|
||||
Map<Tab, Component> tabContent = Map.of(
|
||||
inventoryTab, inventoryContent,
|
||||
movementTab, movementContent,
|
||||
journalTab, journalContent
|
||||
);
|
||||
tabs.addSelectedChangeListener(event -> {
|
||||
for (Component content : tabContent.values()) {
|
||||
content.setVisible(false);
|
||||
}
|
||||
tabContent.get(event.getSelectedTab()).setVisible(true);
|
||||
});
|
||||
|
||||
add(new H3("Lager"), buildTopBar(), buildSummary(), tabs, inventoryContent, movementContent, journalContent);
|
||||
expand(inventoryContent, movementContent, journalContent);
|
||||
|
||||
loadBootstrap();
|
||||
}
|
||||
|
||||
private void configureSelectors() {
|
||||
rootStockField.setItemLabelGenerator(RootOption::label);
|
||||
rootStockField.addValueChangeListener(event -> {
|
||||
if (event.getValue() != null && !event.isFromClient()) {
|
||||
return;
|
||||
}
|
||||
if (event.getValue() != null) {
|
||||
loadRootWorkspace();
|
||||
} else {
|
||||
clearWorkspace();
|
||||
}
|
||||
});
|
||||
inventorySearchField.setClearButtonVisible(true);
|
||||
journalSearchField.setClearButtonVisible(true);
|
||||
}
|
||||
|
||||
private void configureGrids() {
|
||||
stockGrid.addColumn(StockRow::indentedName).setHeader("Lagerort").setFlexGrow(1);
|
||||
stockGrid.addColumn(StockRow::capacityLabel).setHeader("Kapazitaet").setAutoWidth(true);
|
||||
stockGrid.addColumn(row -> row.writeAllowed() ? "Ja" : "Nein").setHeader("Buchbar").setAutoWidth(true);
|
||||
stockGrid.addColumn(StockRow::addressLabel).setHeader("Adresse").setFlexGrow(1);
|
||||
stockGrid.setWidth("420px");
|
||||
stockGrid.setHeightFull();
|
||||
stockGrid.asSingleSelect().addValueChangeListener(event -> {
|
||||
selectedStock = event.getValue();
|
||||
selectedInventory = null;
|
||||
selectedSerial = null;
|
||||
updateSummary();
|
||||
refreshMovementStockDefaults();
|
||||
loadInventory();
|
||||
loadJournal();
|
||||
});
|
||||
|
||||
inventoryGrid.addColumn(InventoryRow::articleCode).setHeader("Artikel").setAutoWidth(true).setFrozen(true);
|
||||
inventoryGrid.addColumn(InventoryRow::name).setHeader("Bezeichnung").setFlexGrow(1);
|
||||
inventoryGrid.addColumn(InventoryRow::match).setHeader("Zusatz").setFlexGrow(1);
|
||||
inventoryGrid.addColumn(InventoryRow::itemQuantity).setHeader("Menge").setAutoWidth(true);
|
||||
inventoryGrid.addColumn(InventoryRow::areaQuantity).setHeader("Plaetze").setAutoWidth(true);
|
||||
inventoryGrid.addColumn(InventoryRow::stockCount).setHeader("Lagerorte").setAutoWidth(true);
|
||||
inventoryGrid.addColumn(row -> row.serialTracked() ? "Ja" : "Nein").setHeader("Seriennr.").setAutoWidth(true);
|
||||
inventoryGrid.setSizeFull();
|
||||
inventoryGrid.asSingleSelect().addValueChangeListener(event -> {
|
||||
selectedInventory = event.getValue();
|
||||
if (selectedInventory != null) {
|
||||
selectMovementArticle(selectedInventory.articleId());
|
||||
movementQuantityField.setValue(selectedInventory.serialTracked() ? 1 : movementQuantityField.getValue());
|
||||
if (selectedStock != null && selectedStock.writeAllowed()) {
|
||||
movementSourceField.setValue(findMovementSourceOption(selectedStock.id()));
|
||||
}
|
||||
}
|
||||
loadSerials();
|
||||
});
|
||||
|
||||
serialGrid.addColumn(SerialRow::stockName).setHeader("Lagerort").setFlexGrow(1);
|
||||
serialGrid.addColumn(SerialRow::serialNo).setHeader("Seriennummer").setAutoWidth(true);
|
||||
serialGrid.addColumn(SerialRow::dataPreview).setHeader("Daten").setFlexGrow(1);
|
||||
serialGrid.setWidth("420px");
|
||||
serialGrid.setHeightFull();
|
||||
serialGrid.asSingleSelect().addValueChangeListener(event -> {
|
||||
selectedSerial = event.getValue();
|
||||
if (selectedSerial != null) {
|
||||
movementSerialField.setValue(selectedSerial.serialNo());
|
||||
movementQuantityField.setValue(1);
|
||||
movementSourceField.setValue(findMovementSourceOption(selectedSerial.stockId()));
|
||||
}
|
||||
});
|
||||
|
||||
journalGrid.addColumn(JournalRow::bookingTime).setHeader("Zeit").setAutoWidth(true).setFrozen(true);
|
||||
journalGrid.addColumn(JournalRow::sourceStock).setHeader("Quelle").setFlexGrow(1);
|
||||
journalGrid.addColumn(JournalRow::targetStock).setHeader("Ziel").setFlexGrow(1);
|
||||
journalGrid.addColumn(JournalRow::articleCode).setHeader("Artikel").setAutoWidth(true);
|
||||
journalGrid.addColumn(JournalRow::articleName).setHeader("Bezeichnung").setFlexGrow(1);
|
||||
journalGrid.addColumn(JournalRow::itemQuantity).setHeader("Menge").setAutoWidth(true);
|
||||
journalGrid.addColumn(JournalRow::serialNo).setHeader("Seriennummer").setAutoWidth(true);
|
||||
journalGrid.addColumn(JournalRow::tan).setHeader("TAN").setAutoWidth(true);
|
||||
journalGrid.addColumn(JournalRow::remark).setHeader("Bemerkung").setFlexGrow(1);
|
||||
journalGrid.addColumn(row -> row.jobId() != null && row.jobId() > 0 ? row.jobId() : null).setHeader("Auftrag").setAutoWidth(true);
|
||||
journalGrid.setSizeFull();
|
||||
}
|
||||
|
||||
private void configureMovementForm() {
|
||||
movementArticleField.setItemLabelGenerator(ArticleOption::label);
|
||||
movementSourceField.setItemLabelGenerator(MovementStockOption::label);
|
||||
movementTargetField.setItemLabelGenerator(MovementStockOption::label);
|
||||
movementQuantityField.setMin(1);
|
||||
movementQuantityField.setValue(1);
|
||||
movementArticleField.addValueChangeListener(event -> updateMovementSerialState());
|
||||
moveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
moveButton.addClickListener(event -> moveArticle());
|
||||
}
|
||||
|
||||
private void configureJournalFilters() {
|
||||
journalFromField.setValue(LocalDate.now().minusDays(7));
|
||||
journalToField.setValue(LocalDate.now());
|
||||
journalSourceField.setItemLabelGenerator(MovementStockOption::label);
|
||||
journalTargetField.setItemLabelGenerator(MovementStockOption::label);
|
||||
}
|
||||
|
||||
private HorizontalLayout buildTopBar() {
|
||||
Button reloadButton = new Button("Neu laden", event -> loadRootWorkspace());
|
||||
reloadButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
HorizontalLayout bar = new HorizontalLayout(rootStockField, reloadButton);
|
||||
bar.setAlignItems(Alignment.BASELINE);
|
||||
return bar;
|
||||
}
|
||||
|
||||
private HorizontalLayout buildSummary() {
|
||||
HorizontalLayout layout = new HorizontalLayout(
|
||||
detailLine("Lagerort", selectedStockText),
|
||||
detailLine("Kapazitaet", selectedCapacityText),
|
||||
detailLine("Zugriff", accessText)
|
||||
);
|
||||
layout.setWidthFull();
|
||||
layout.setAlignItems(Alignment.BASELINE);
|
||||
layout.getStyle().set("flex-wrap", "wrap");
|
||||
return layout;
|
||||
}
|
||||
|
||||
private VerticalLayout buildInventoryContent() {
|
||||
Button loadInventoryButton = new Button("Bestand laden", event -> loadInventory());
|
||||
HorizontalLayout filterBar = new HorizontalLayout(includeSubstocksField, inventorySearchField, loadInventoryButton);
|
||||
filterBar.setAlignItems(Alignment.BASELINE);
|
||||
|
||||
VerticalLayout stockPanel = new VerticalLayout(new H4("Lagerbaum"), stockGrid);
|
||||
stockPanel.setPadding(false);
|
||||
stockPanel.setSpacing(true);
|
||||
stockPanel.setWidth("420px");
|
||||
stockPanel.setHeightFull();
|
||||
stockPanel.expand(stockGrid);
|
||||
|
||||
VerticalLayout inventoryPanel = new VerticalLayout(new H4("Bestand"), filterBar, inventoryGrid);
|
||||
inventoryPanel.setPadding(false);
|
||||
inventoryPanel.setSpacing(true);
|
||||
inventoryPanel.setSizeFull();
|
||||
inventoryPanel.expand(inventoryGrid);
|
||||
|
||||
VerticalLayout serialPanel = new VerticalLayout(new H4("Seriennummern"), serialGrid);
|
||||
serialPanel.setPadding(false);
|
||||
serialPanel.setSpacing(true);
|
||||
serialPanel.setWidth("420px");
|
||||
serialPanel.setHeightFull();
|
||||
serialPanel.expand(serialGrid);
|
||||
|
||||
HorizontalLayout workspace = new HorizontalLayout(stockPanel, inventoryPanel, serialPanel);
|
||||
workspace.setSizeFull();
|
||||
workspace.expand(inventoryPanel);
|
||||
|
||||
VerticalLayout content = new VerticalLayout(workspace);
|
||||
content.setPadding(false);
|
||||
content.setSpacing(false);
|
||||
content.setSizeFull();
|
||||
content.expand(workspace);
|
||||
return content;
|
||||
}
|
||||
|
||||
private VerticalLayout buildMovementContent() {
|
||||
FormLayout form = new FormLayout();
|
||||
form.setResponsiveSteps(
|
||||
new FormLayout.ResponsiveStep("0", 1),
|
||||
new FormLayout.ResponsiveStep("720px", 2)
|
||||
);
|
||||
form.add(movementArticleField, movementQuantityField, movementSourceField, movementTargetField,
|
||||
movementSerialField, movementTanField, movementJobField, movementRemarkField);
|
||||
|
||||
Paragraph hint = new Paragraph("Quelle `Eingangslieferung` bucht Ware ins Lager. Ziel `Ausgangslager` bucht Ware aus dem Lagerbestand aus.");
|
||||
hint.getStyle().set("margin", "0");
|
||||
|
||||
VerticalLayout content = new VerticalLayout(new H4("Artikelbewegung"), hint, form, moveButton);
|
||||
content.setPadding(false);
|
||||
content.setSpacing(true);
|
||||
content.setWidthFull();
|
||||
return content;
|
||||
}
|
||||
|
||||
private VerticalLayout buildJournalContent() {
|
||||
Button loadJournalButton = new Button("Journal laden", event -> loadJournal());
|
||||
HorizontalLayout filters = new HorizontalLayout(journalFromField, journalToField, journalSourceField, journalTargetField,
|
||||
journalSearchField, loadJournalButton);
|
||||
filters.setAlignItems(Alignment.BASELINE);
|
||||
filters.getStyle().set("flex-wrap", "wrap");
|
||||
|
||||
VerticalLayout content = new VerticalLayout(new H4("Lagerjournal"), filters, journalGrid);
|
||||
content.setPadding(false);
|
||||
content.setSpacing(true);
|
||||
content.setSizeFull();
|
||||
content.expand(journalGrid);
|
||||
return content;
|
||||
}
|
||||
|
||||
private HorizontalLayout detailLine(String label, Span value) {
|
||||
Span title = new Span(label + ":");
|
||||
title.getStyle().set("font-weight", "600");
|
||||
return new HorizontalLayout(title, value);
|
||||
}
|
||||
|
||||
private void loadBootstrap() {
|
||||
try {
|
||||
Map<String, Object> data = restClient.getMap("/warehouse/bootstrap");
|
||||
rootOptions.clear();
|
||||
canMove = bool(data.get("canMove"));
|
||||
|
||||
for (Object row : asList(data.get("roots"))) {
|
||||
if (row instanceof Map<?, ?> map) {
|
||||
Long id = toLong(map.get("id"));
|
||||
if (id != null) {
|
||||
rootOptions.add(new RootOption(id, str(map.get("name"))));
|
||||
}
|
||||
}
|
||||
}
|
||||
rootStockField.setItems(rootOptions);
|
||||
moveButton.setEnabled(canMove);
|
||||
|
||||
Long defaultRootStockId = toLong(data.get("defaultRootStockId"));
|
||||
RootOption defaultRoot = rootOptions.stream()
|
||||
.filter(option -> option.id().equals(defaultRootStockId))
|
||||
.findFirst()
|
||||
.orElse(rootOptions.isEmpty() ? null : rootOptions.get(0));
|
||||
|
||||
if (defaultRoot != null) {
|
||||
rootStockField.setValue(defaultRoot);
|
||||
loadRootWorkspace();
|
||||
} else {
|
||||
clearWorkspace();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
showError("Lagermodul konnte nicht geladen werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void loadRootWorkspace() {
|
||||
RootOption root = rootStockField.getValue();
|
||||
if (root == null) {
|
||||
clearWorkspace();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
loadStocks(root.id());
|
||||
loadArticleOptions(root.id());
|
||||
refreshStockSelectors();
|
||||
selectInitialStock(root.id());
|
||||
loadInventory();
|
||||
loadJournal();
|
||||
} catch (Exception e) {
|
||||
showError("Lagerdaten konnten nicht geladen werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void loadStocks(Long rootStockId) {
|
||||
List<LinkedHashMap<String, Object>> data = restClient.getList(
|
||||
"/warehouse/stocks?rootStockId=" + rootStockId,
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
stockRows.clear();
|
||||
for (Map<String, Object> row : data) {
|
||||
stockRows.add(new StockRow(
|
||||
toLong(row.get("id")),
|
||||
toLong(row.get("parentId")),
|
||||
toInt(row.get("level")),
|
||||
str(row.get("name")),
|
||||
bool(row.get("planning")),
|
||||
toInt(row.get("maxQuantity")),
|
||||
toInt(row.get("usedCapacity")),
|
||||
toInt(row.get("reservedCapacity")),
|
||||
toInt(row.get("freeCapacity")),
|
||||
str(row.get("barcode")),
|
||||
str(row.get("addressLabel")),
|
||||
bool(row.get("writeAllowed"))
|
||||
));
|
||||
}
|
||||
stockGrid.setItems(stockRows);
|
||||
}
|
||||
|
||||
private void loadArticleOptions(Long rootStockId) {
|
||||
List<LinkedHashMap<String, Object>> data = restClient.getList(
|
||||
"/warehouse/articles?rootStockId=" + rootStockId,
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
articleOptions.clear();
|
||||
for (Map<String, Object> row : data) {
|
||||
Long articleId = toLong(row.get("articleId"));
|
||||
if (articleId != null) {
|
||||
articleOptions.add(new ArticleOption(
|
||||
articleId,
|
||||
str(row.get("articleCode")),
|
||||
str(row.get("name")),
|
||||
str(row.get("match")),
|
||||
bool(row.get("serialTracked"))
|
||||
));
|
||||
}
|
||||
}
|
||||
movementArticleField.setItems(articleOptions);
|
||||
}
|
||||
|
||||
private void refreshStockSelectors() {
|
||||
List<MovementStockOption> writableStocks = stockRows.stream()
|
||||
.filter(StockRow::writeAllowed)
|
||||
.map(row -> new MovementStockOption(row.id(), row.indentedName(), true))
|
||||
.toList();
|
||||
|
||||
List<MovementStockOption> movementSourceOptions = new ArrayList<>();
|
||||
movementSourceOptions.add(new MovementStockOption(0L, "Eingangslieferung", canMove));
|
||||
movementSourceOptions.addAll(writableStocks);
|
||||
|
||||
List<MovementStockOption> movementTargetOptions = new ArrayList<>(writableStocks);
|
||||
movementTargetOptions.add(new MovementStockOption(0L, "Ausgangslager", canMove));
|
||||
|
||||
movementSourceField.setItems(movementSourceOptions);
|
||||
movementTargetField.setItems(movementTargetOptions);
|
||||
journalSourceField.setItems(stockRows.stream()
|
||||
.map(row -> new MovementStockOption(row.id(), row.indentedName(), row.writeAllowed()))
|
||||
.toList());
|
||||
journalTargetField.setItems(stockRows.stream()
|
||||
.map(row -> new MovementStockOption(row.id(), row.indentedName(), row.writeAllowed()))
|
||||
.toList());
|
||||
}
|
||||
|
||||
private void selectInitialStock(Long rootStockId) {
|
||||
StockRow preferred = selectedStock != null
|
||||
? stockRows.stream().filter(row -> row.id().equals(selectedStock.id())).findFirst().orElse(null)
|
||||
: null;
|
||||
if (preferred == null) {
|
||||
preferred = stockRows.stream().filter(row -> row.id().equals(rootStockId)).findFirst().orElse(null);
|
||||
}
|
||||
if (preferred == null && !stockRows.isEmpty()) {
|
||||
preferred = stockRows.get(0);
|
||||
}
|
||||
selectedStock = preferred;
|
||||
if (preferred != null) {
|
||||
stockGrid.select(preferred);
|
||||
} else {
|
||||
stockGrid.deselectAll();
|
||||
updateSummary();
|
||||
}
|
||||
}
|
||||
|
||||
private void loadInventory() {
|
||||
if (selectedStock == null) {
|
||||
inventoryGrid.setItems(List.of());
|
||||
serialGrid.setItems(List.of());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
List<LinkedHashMap<String, Object>> data = restClient.getList(
|
||||
"/warehouse/inventory?stockId=" + selectedStock.id()
|
||||
+ "&includeSubstocks=" + includeSubstocksField.getValue()
|
||||
+ "&search=" + encode(inventorySearchField.getValue()),
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
List<InventoryRow> rows = new ArrayList<>();
|
||||
for (Map<String, Object> row : data) {
|
||||
rows.add(new InventoryRow(
|
||||
toLong(row.get("articleId")),
|
||||
str(row.get("articleCode")),
|
||||
str(row.get("name")),
|
||||
str(row.get("match")),
|
||||
str(row.get("barcode")),
|
||||
bool(row.get("serialTracked")),
|
||||
toInt(row.get("itemQuantity")),
|
||||
toInt(row.get("areaQuantity")),
|
||||
toInt(row.get("stockCount"))
|
||||
));
|
||||
}
|
||||
inventoryGrid.setItems(rows);
|
||||
InventoryRow updatedSelection = selectedInventory != null
|
||||
? rows.stream().filter(row -> row.articleId().equals(selectedInventory.articleId())).findFirst().orElse(null)
|
||||
: null;
|
||||
selectedInventory = updatedSelection;
|
||||
if (updatedSelection != null) {
|
||||
inventoryGrid.select(updatedSelection);
|
||||
} else {
|
||||
inventoryGrid.deselectAll();
|
||||
loadSerials();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
showError("Bestand konnte nicht geladen werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void loadSerials() {
|
||||
if (selectedStock == null || selectedInventory == null || !selectedInventory.serialTracked()) {
|
||||
serialGrid.setItems(List.of());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
List<LinkedHashMap<String, Object>> data = restClient.getList(
|
||||
"/warehouse/serials?stockId=" + selectedStock.id()
|
||||
+ "&includeSubstocks=" + includeSubstocksField.getValue()
|
||||
+ "&articleId=" + selectedInventory.articleId(),
|
||||
new ParameterizedTypeReference<>() {}
|
||||
);
|
||||
List<SerialRow> rows = new ArrayList<>();
|
||||
for (Map<String, Object> row : data) {
|
||||
rows.add(new SerialRow(
|
||||
toLong(row.get("id")),
|
||||
toLong(row.get("stockId")),
|
||||
str(row.get("stockName")),
|
||||
str(row.get("serialNo")),
|
||||
extractStringList(row.get("dataFields"))
|
||||
));
|
||||
}
|
||||
serialGrid.setItems(rows);
|
||||
} catch (Exception e) {
|
||||
showError("Seriennummern konnten nicht geladen werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void loadJournal() {
|
||||
RootOption root = rootStockField.getValue();
|
||||
if (root == null) {
|
||||
journalGrid.setItems(List.of());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
String path = "/warehouse/journal?rootStockId=" + root.id()
|
||||
+ optionalLongParam("stockFromId", journalSourceField.getValue() != null ? journalSourceField.getValue().stockId() : null)
|
||||
+ optionalLongParam("stockToId", journalTargetField.getValue() != null ? journalTargetField.getValue().stockId() : null)
|
||||
+ "&from=" + journalFromField.getValue()
|
||||
+ "&to=" + journalToField.getValue()
|
||||
+ "&search=" + encode(journalSearchField.getValue());
|
||||
|
||||
List<LinkedHashMap<String, Object>> data = restClient.getList(path, new ParameterizedTypeReference<>() {});
|
||||
List<JournalRow> rows = new ArrayList<>();
|
||||
for (Map<String, Object> row : data) {
|
||||
rows.add(new JournalRow(
|
||||
toLong(row.get("id")),
|
||||
str(row.get("bookingTime")),
|
||||
str(row.get("sourceStock")),
|
||||
str(row.get("targetStock")),
|
||||
str(row.get("articleCode")),
|
||||
str(row.get("articleName")),
|
||||
toInt(row.get("itemQuantity")),
|
||||
str(row.get("serialNo")),
|
||||
str(row.get("tan")),
|
||||
str(row.get("remark")),
|
||||
toLong(row.get("jobId"))
|
||||
));
|
||||
}
|
||||
journalGrid.setItems(rows);
|
||||
} catch (Exception e) {
|
||||
showError("Journal konnte nicht geladen werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void moveArticle() {
|
||||
if (!canMove) {
|
||||
showError("Lagerbuchungen sind fuer den angemeldeten Benutzer gesperrt.");
|
||||
return;
|
||||
}
|
||||
if (movementArticleField.getValue() == null || movementSourceField.getValue() == null || movementTargetField.getValue() == null) {
|
||||
showError("Bitte Artikel, Quelle und Ziel waehlen.");
|
||||
return;
|
||||
}
|
||||
if (movementArticleField.getValue().serialTracked() && movementSerialField.getValue().isBlank()) {
|
||||
showError("Fuer diesen Artikel ist eine Seriennummer erforderlich.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, Object> request = new LinkedHashMap<>();
|
||||
request.put("articleId", movementArticleField.getValue().articleId());
|
||||
request.put("sourceStockId", movementSourceField.getValue().stockId());
|
||||
request.put("targetStockId", movementTargetField.getValue().stockId());
|
||||
request.put("itemQuantity", movementQuantityField.getValue());
|
||||
request.put("serialNo", movementSerialField.getValue());
|
||||
request.put("tan", movementTanField.getValue());
|
||||
request.put("remark", movementRemarkField.getValue());
|
||||
request.put("jobId", parseLongOrNull(movementJobField.getValue()));
|
||||
restClient.post("/warehouse/moves", request, LinkedHashMap.class);
|
||||
Notification.show("Lagerbewegung gespeichert.", 3000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
|
||||
movementRemarkField.clear();
|
||||
movementTanField.clear();
|
||||
movementSerialField.clear();
|
||||
movementJobField.clear();
|
||||
movementQuantityField.setValue(1);
|
||||
|
||||
loadRootWorkspace();
|
||||
} catch (Exception e) {
|
||||
showError("Lagerbewegung konnte nicht gespeichert werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSummary() {
|
||||
if (selectedStock == null) {
|
||||
selectedStockText.setText("-");
|
||||
selectedCapacityText.setText("-");
|
||||
accessText.setText(canMove ? "Buchbar" : "Nur Lesen");
|
||||
return;
|
||||
}
|
||||
selectedStockText.setText(selectedStock.name());
|
||||
if (selectedStock.planning()) {
|
||||
selectedCapacityText.setText(selectedStock.usedCapacity() + " belegt / "
|
||||
+ selectedStock.reservedCapacity() + " reserviert / "
|
||||
+ selectedStock.freeCapacity() + " frei");
|
||||
} else {
|
||||
selectedCapacityText.setText("Kapazitaetsplanung deaktiviert");
|
||||
}
|
||||
accessText.setText(selectedStock.writeAllowed() && canMove ? "Buchbar" : "Nur Lesen");
|
||||
}
|
||||
|
||||
private void updateMovementSerialState() {
|
||||
ArticleOption article = movementArticleField.getValue();
|
||||
boolean serialTracked = article != null && article.serialTracked();
|
||||
movementSerialField.setRequired(serialTracked);
|
||||
if (serialTracked && (movementQuantityField.getValue() == null || movementQuantityField.getValue() != 1)) {
|
||||
movementQuantityField.setValue(1);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshMovementStockDefaults() {
|
||||
updateSummary();
|
||||
if (selectedStock != null && selectedStock.writeAllowed()) {
|
||||
movementSourceField.setValue(findMovementSourceOption(selectedStock.id()));
|
||||
} else {
|
||||
movementSourceField.clear();
|
||||
}
|
||||
movementTargetField.clear();
|
||||
}
|
||||
|
||||
private void selectMovementArticle(Long articleId) {
|
||||
if (articleId == null) {
|
||||
return;
|
||||
}
|
||||
articleOptions.stream()
|
||||
.filter(option -> option.articleId().equals(articleId))
|
||||
.findFirst()
|
||||
.ifPresent(movementArticleField::setValue);
|
||||
}
|
||||
|
||||
private MovementStockOption findMovementSourceOption(Long stockId) {
|
||||
return movementSourceField.getListDataView().getItems()
|
||||
.filter(option -> option.stockId().equals(stockId))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private void clearWorkspace() {
|
||||
stockRows.clear();
|
||||
articleOptions.clear();
|
||||
selectedStock = null;
|
||||
selectedInventory = null;
|
||||
selectedSerial = null;
|
||||
stockGrid.setItems(List.of());
|
||||
inventoryGrid.setItems(List.of());
|
||||
serialGrid.setItems(List.of());
|
||||
journalGrid.setItems(List.of());
|
||||
updateSummary();
|
||||
}
|
||||
|
||||
private void showError(String message) {
|
||||
Notification.show(message, 5000, Notification.Position.MIDDLE)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
|
||||
private String optionalLongParam(String key, Long value) {
|
||||
return value != null ? "&" + key + "=" + value : "";
|
||||
}
|
||||
|
||||
private String encode(String value) {
|
||||
return value != null ? URLEncoder.encode(value, StandardCharsets.UTF_8) : "";
|
||||
}
|
||||
|
||||
private List<?> asList(Object value) {
|
||||
if (value instanceof List<?> list) {
|
||||
return list;
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private List<String> extractStringList(Object value) {
|
||||
List<String> result = new ArrayList<>();
|
||||
if (value instanceof Iterable<?> iterable) {
|
||||
for (Object item : iterable) {
|
||||
result.add(str(item));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Long parseLongOrNull(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(value.trim());
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Long toLong(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
if (value == null || value.toString().isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(value.toString());
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private int toInt(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
if (value == null || value.toString().isBlank()) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(value.toString());
|
||||
} catch (NumberFormatException ignored) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean bool(Object value) {
|
||||
if (value instanceof Boolean bool) {
|
||||
return bool;
|
||||
}
|
||||
return "true".equalsIgnoreCase(str(value)) || "1".equals(str(value));
|
||||
}
|
||||
|
||||
private String str(Object value) {
|
||||
return value != null ? value.toString() : "";
|
||||
}
|
||||
|
||||
private record RootOption(Long id, String name) {
|
||||
private String label() {
|
||||
return name + " (" + id + ")";
|
||||
}
|
||||
}
|
||||
|
||||
private record StockRow(
|
||||
Long id,
|
||||
Long parentId,
|
||||
int level,
|
||||
String name,
|
||||
boolean planning,
|
||||
int maxQuantity,
|
||||
int usedCapacity,
|
||||
int reservedCapacity,
|
||||
int freeCapacity,
|
||||
String barcode,
|
||||
String addressLabel,
|
||||
boolean writeAllowed
|
||||
) {
|
||||
private String indentedName() {
|
||||
return " ".repeat(Math.max(0, level)) + name;
|
||||
}
|
||||
|
||||
private String capacityLabel() {
|
||||
if (!planning) {
|
||||
return "-";
|
||||
}
|
||||
return maxQuantity + " / " + freeCapacity + " frei";
|
||||
}
|
||||
}
|
||||
|
||||
private record InventoryRow(
|
||||
Long articleId,
|
||||
String articleCode,
|
||||
String name,
|
||||
String match,
|
||||
String barcode,
|
||||
boolean serialTracked,
|
||||
int itemQuantity,
|
||||
int areaQuantity,
|
||||
int stockCount
|
||||
) {
|
||||
}
|
||||
|
||||
private record SerialRow(Long id, Long stockId, String stockName, String serialNo, List<String> dataFields) {
|
||||
private String dataPreview() {
|
||||
return dataFields.stream()
|
||||
.filter(value -> value != null && !value.isBlank())
|
||||
.limit(3)
|
||||
.collect(Collectors.joining(" | "));
|
||||
}
|
||||
}
|
||||
|
||||
private record JournalRow(
|
||||
Long id,
|
||||
String bookingTime,
|
||||
String sourceStock,
|
||||
String targetStock,
|
||||
String articleCode,
|
||||
String articleName,
|
||||
int itemQuantity,
|
||||
String serialNo,
|
||||
String tan,
|
||||
String remark,
|
||||
Long jobId
|
||||
) {
|
||||
}
|
||||
|
||||
private record ArticleOption(Long articleId, String articleCode, String name, String match, boolean serialTracked) {
|
||||
private String label() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (articleCode != null && !articleCode.isBlank()) {
|
||||
builder.append(articleCode).append(" - ");
|
||||
}
|
||||
builder.append(name);
|
||||
if (match != null && !match.isBlank()) {
|
||||
builder.append(" (").append(match).append(")");
|
||||
}
|
||||
if (serialTracked) {
|
||||
builder.append(" [SN]");
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private record MovementStockOption(Long stockId, String label, boolean writeAllowed) {
|
||||
}
|
||||
}
|
||||
9
vaadin/src/main/resources/application.properties
Normal file
9
vaadin/src/main/resources/application.properties
Normal file
@@ -0,0 +1,9 @@
|
||||
# Votian Vaadin Web Application
|
||||
server.port=8080
|
||||
|
||||
# REST Service Backend URL
|
||||
votian.services.url=http://localhost:8081/api
|
||||
|
||||
# Vaadin
|
||||
vaadin.launch-browser=true
|
||||
spring.mustache.check-template-location=false
|
||||
Reference in New Issue
Block a user