From 751836e8a402745b21077aa93f374e803f12d826 Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Mon, 15 Sep 2025 18:02:18 +0200 Subject: [PATCH] Erweiterungen --- .claude/settings.local.json | 12 + .../assecutor/votianlt/config/MailConfig.java | 36 - .../votianlt/config/MongoConfig.java | 128 ++-- .../votianlt/config/MqttProperties.java | 119 ++- .../controller/MessageController.java | 163 ++-- .../votianlt/dto/JobWithRelatedDataDTO.java | 4 +- .../de/assecutor/votianlt/model/AppUser.java | 4 +- .../de/assecutor/votianlt/model/Barcode.java | 4 +- .../assecutor/votianlt/model/CargoItem.java | 5 +- .../de/assecutor/votianlt/model/Company.java | 3 +- .../de/assecutor/votianlt/model/Customer.java | 3 +- .../de/assecutor/votianlt/model/Invoice.java | 1 - .../java/de/assecutor/votianlt/model/Job.java | 4 +- .../assecutor/votianlt/model/JobHistory.java | 11 +- .../de/assecutor/votianlt/model/Photo.java | 4 +- .../assecutor/votianlt/model/TaskEntry.java | 12 +- .../de/assecutor/votianlt/model/User.java | 14 +- .../votianlt/model/task/BaseTask.java | 12 +- .../votianlt/model/task/SignatureTask.java | 1 - .../votianlt/model/task/TaskType.java | 6 +- .../votianlt/mqtt/MqttClientRunner.java | 4 +- .../votianlt/mqtt/MqttPublisher.java | 6 +- .../votianlt/mqtt/MqttV5ClientManager.java | 56 +- .../pages/base/ui/view/MainLayout.java | 27 +- .../pages/domain/AddCompanyRepository.java | 1 - .../pages/domain/AddCustomerRepository.java | 1 - .../pages/domain/LoginRepository.java | 2 - .../pages/service/AddCompanyService.java | 2 - .../pages/service/AddCustomerService.java | 2 - .../votianlt/pages/service/AddJobService.java | 29 +- .../pages/service/AppDeviceService.java | 4 +- .../pages/service/AppUserService.java | 20 +- .../pages/service/CustomerService.java | 2 - .../pages/service/PasswordResetService.java | 131 ++-- .../pages/service/RegisterService.java | 3 +- .../votianlt/pages/view/AddAppDeviceView.java | 61 +- .../votianlt/pages/view/AddAppUserView.java | 86 +-- .../votianlt/pages/view/AddCompanyView.java | 11 +- .../votianlt/pages/view/AddCustomerView.java | 69 +- .../votianlt/pages/view/AddJobView.java | 710 +++++++++--------- .../votianlt/pages/view/AppDevicesView.java | 25 +- .../votianlt/pages/view/AppUserView.java | 16 +- .../pages/view/AuthenticatedStartView.java | 28 +- .../pages/view/EditAppDeviceView.java | 115 +-- .../votianlt/pages/view/EditAppUserView.java | 83 +- .../votianlt/pages/view/EditCustomerView.java | 24 +- .../votianlt/pages/view/EditProfileView.java | 101 ++- .../pages/view/ForgetPasswordView.java | 6 +- .../pages/view/ForgotPasswordRequestView.java | 7 +- .../votianlt/pages/view/ImprintView.java | 3 +- .../votianlt/pages/view/InvoicesView.java | 23 +- .../votianlt/pages/view/JobHistoryView.java | 226 +++--- .../votianlt/pages/view/JobSummaryView.java | 400 ++++------ .../votianlt/pages/view/LoginView.java | 35 +- .../votianlt/pages/view/MyInvoicesView.java | 42 +- .../votianlt/pages/view/RegisterView.java | 43 +- .../pages/view/ShowCustomersView.java | 36 +- .../votianlt/pages/view/ShowJobsView.java | 45 +- .../votianlt/pages/view/StartView.java | 89 +-- .../votianlt/pages/view/StatisticsView.java | 320 ++++---- .../votianlt/pages/view/VerwaltungView.java | 8 +- .../repository/AppDeviceRepository.java | 2 +- .../repository/AppUserRepository.java | 4 +- .../repository/CargoItemRepository.java | 1 - .../repository/JobHistoryRepository.java | 13 +- .../votianlt/repository/JobRepository.java | 10 +- .../votianlt/repository/PhotoRepository.java | 12 +- .../votianlt/repository/TaskRepository.java | 1 - .../votianlt/repository/UserRepository.java | 6 +- .../security/CustomUserPrincipal.java | 29 +- .../votianlt/security/SecurityConfig.java | 45 +- .../votianlt/security/SecurityService.java | 26 +- .../security/UserDetailsServiceImpl.java | 1 - .../security/totp/TwoFactorService.java | 31 +- .../votianlt/service/EmailService.java | 85 ++- .../votianlt/service/JobHistoryService.java | 98 +-- .../de/assecutor/votianlt/util/MailUtil.java | 54 -- .../java/de/assecutor/votianlt/util/Util.java | 5 +- .../votianlt/zeroconf/ZeroconfPublisher.java | 3 +- 79 files changed, 1855 insertions(+), 2019 deletions(-) create mode 100644 .claude/settings.local.json delete mode 100644 src/main/java/de/assecutor/votianlt/config/MailConfig.java delete mode 100644 src/main/java/de/assecutor/votianlt/util/MailUtil.java diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..1986750 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "Bash(./mvnw clean compile -q)", + "mcp__ide__getDiagnostics", + "Bash(find:*)", + "Bash(./mvnw:*)" + ], + "deny": [], + "ask": [] + } +} \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/config/MailConfig.java b/src/main/java/de/assecutor/votianlt/config/MailConfig.java deleted file mode 100644 index c6ecf24..0000000 --- a/src/main/java/de/assecutor/votianlt/config/MailConfig.java +++ /dev/null @@ -1,36 +0,0 @@ -package de.assecutor.votianlt.config; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -@Component -public class MailConfig { - - @Value("${mail.smtp.username}") - private String username; - - @Value("${mail.smtp.password}") - private String password; - - @Value("${mail.smtp.host:smtp.gmail.com}") - private String host; - - @Value("${mail.smtp.port:587}") - private int port; - - public String getUsername() { - return username; - } - - public String getPassword() { - return password; - } - - public String getHost() { - return host; - } - - public int getPort() { - return port; - } -} \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/config/MongoConfig.java b/src/main/java/de/assecutor/votianlt/config/MongoConfig.java index afdc787..1f0e060 100644 --- a/src/main/java/de/assecutor/votianlt/config/MongoConfig.java +++ b/src/main/java/de/assecutor/votianlt/config/MongoConfig.java @@ -50,55 +50,55 @@ public class MongoConfig { BaseTask task; switch (className) { - case "de.assecutor.votianlt.model.task.ConfirmationTask": - case "ConfirmationTask": - log.debug("Creating ConfirmationTask"); - task = new ConfirmationTask(); - if (source.containsKey("button_text")) { - ((ConfirmationTask) task).setButtonText(source.getString("button_text")); - } - break; - case "de.assecutor.votianlt.model.task.SignatureTask": - case "SignatureTask": - log.debug("Creating SignatureTask"); - task = new SignatureTask(); - break; - case "de.assecutor.votianlt.model.task.PhotoTask": - case "PhotoTask": - log.debug("Creating PhotoTask"); - task = new PhotoTask(); - if (source.containsKey("min_photo_count")) { - ((PhotoTask) task).setMinPhotoCount(source.getInteger("min_photo_count")); - } - if (source.containsKey("max_photo_count")) { - ((PhotoTask) task).setMaxPhotoCount(source.getInteger("max_photo_count")); - } - break; - case "de.assecutor.votianlt.model.task.TodoListTask": - case "TodoListTask": - log.debug("Creating TodoListTask"); - task = new TodoListTask(); - if (source.containsKey("todo_items")) { - @SuppressWarnings("unchecked") - List todoItems = (List) source.get("todo_items"); - ((TodoListTask) task).setTodoItems(todoItems); - } - break; - case "de.assecutor.votianlt.model.task.BarcodeTask": - case "BarcodeTask": - log.debug("Creating BarcodeTask"); - task = new BarcodeTask(); - if (source.containsKey("min_barcode_count")) { - ((BarcodeTask) task).setMinBarcodeCount(source.getInteger("min_barcode_count")); - } - if (source.containsKey("max_barcode_count")) { - ((BarcodeTask) task).setMaxBarcodeCount(source.getInteger("max_barcode_count")); - } - break; - default: - log.warn("Unknown className '{}', falling back to ConfirmationTask", className); - task = new ConfirmationTask(); // fallback - break; + case "de.assecutor.votianlt.model.task.ConfirmationTask": + case "ConfirmationTask": + log.debug("Creating ConfirmationTask"); + task = new ConfirmationTask(); + if (source.containsKey("button_text")) { + ((ConfirmationTask) task).setButtonText(source.getString("button_text")); + } + break; + case "de.assecutor.votianlt.model.task.SignatureTask": + case "SignatureTask": + log.debug("Creating SignatureTask"); + task = new SignatureTask(); + break; + case "de.assecutor.votianlt.model.task.PhotoTask": + case "PhotoTask": + log.debug("Creating PhotoTask"); + task = new PhotoTask(); + if (source.containsKey("min_photo_count")) { + ((PhotoTask) task).setMinPhotoCount(source.getInteger("min_photo_count")); + } + if (source.containsKey("max_photo_count")) { + ((PhotoTask) task).setMaxPhotoCount(source.getInteger("max_photo_count")); + } + break; + case "de.assecutor.votianlt.model.task.TodoListTask": + case "TodoListTask": + log.debug("Creating TodoListTask"); + task = new TodoListTask(); + if (source.containsKey("todo_items")) { + @SuppressWarnings("unchecked") + List todoItems = (List) source.get("todo_items"); + ((TodoListTask) task).setTodoItems(todoItems); + } + break; + case "de.assecutor.votianlt.model.task.BarcodeTask": + case "BarcodeTask": + log.debug("Creating BarcodeTask"); + task = new BarcodeTask(); + if (source.containsKey("min_barcode_count")) { + ((BarcodeTask) task).setMinBarcodeCount(source.getInteger("min_barcode_count")); + } + if (source.containsKey("max_barcode_count")) { + ((BarcodeTask) task).setMaxBarcodeCount(source.getInteger("max_barcode_count")); + } + break; + default: + log.warn("Unknown className '{}', falling back to ConfirmationTask", className); + task = new ConfirmationTask(); // fallback + break; } // Set common fields @@ -120,9 +120,11 @@ public class MongoConfig { if (source.containsKey("completed_at") && source.get("completed_at") != null) { Object completedAtObj = source.get("completed_at"); if (completedAtObj instanceof String) { - task.setCompletedAt(LocalDateTime.parse((String) completedAtObj, DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + task.setCompletedAt( + LocalDateTime.parse((String) completedAtObj, DateTimeFormatter.ISO_LOCAL_DATE_TIME)); } else if (completedAtObj instanceof java.util.Date) { - task.setCompletedAt(((java.util.Date) completedAtObj).toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime()); + task.setCompletedAt(((java.util.Date) completedAtObj).toInstant() + .atZone(java.time.ZoneId.systemDefault()).toLocalDateTime()); } } if (source.containsKey("completed_by")) { @@ -137,18 +139,18 @@ public class MongoConfig { return "de.assecutor.votianlt.model.task.ConfirmationTask"; } switch (taskType) { - case "CONFIRMATION": - return "de.assecutor.votianlt.model.task.ConfirmationTask"; - case "SIGNATURE": - return "de.assecutor.votianlt.model.task.SignatureTask"; - case "PHOTO": - return "de.assecutor.votianlt.model.task.PhotoTask"; - case "TODOLIST": - return "de.assecutor.votianlt.model.task.TodoListTask"; - case "BARCODE": - return "de.assecutor.votianlt.model.task.BarcodeTask"; - default: - return "de.assecutor.votianlt.model.task.ConfirmationTask"; + case "CONFIRMATION": + return "de.assecutor.votianlt.model.task.ConfirmationTask"; + case "SIGNATURE": + return "de.assecutor.votianlt.model.task.SignatureTask"; + case "PHOTO": + return "de.assecutor.votianlt.model.task.PhotoTask"; + case "TODOLIST": + return "de.assecutor.votianlt.model.task.TodoListTask"; + case "BARCODE": + return "de.assecutor.votianlt.model.task.BarcodeTask"; + default: + return "de.assecutor.votianlt.model.task.ConfirmationTask"; } } } diff --git a/src/main/java/de/assecutor/votianlt/config/MqttProperties.java b/src/main/java/de/assecutor/votianlt/config/MqttProperties.java index 442c893..9910de4 100644 --- a/src/main/java/de/assecutor/votianlt/config/MqttProperties.java +++ b/src/main/java/de/assecutor/votianlt/config/MqttProperties.java @@ -31,28 +31,99 @@ public class MqttProperties { /** Default retained flag for publishing */ private boolean defaultRetained = false; - public boolean isEnabled() { return enabled; } - public void setEnabled(boolean enabled) { this.enabled = enabled; } - public String getBrokerUri() { return brokerUri; } - public void setBrokerUri(String brokerUri) { this.brokerUri = brokerUri; } - public String getClientId() { return clientId; } - public void setClientId(String clientId) { this.clientId = clientId; } - public String getUsername() { return username; } - public void setUsername(String username) { this.username = username; } - public String getPassword() { return password; } - public void setPassword(String password) { this.password = password; } - public boolean isCleanStart() { return cleanStart; } - public void setCleanStart(boolean cleanStart) { this.cleanStart = cleanStart; } - public long getSessionExpiryInterval() { return sessionExpiryInterval; } - public void setSessionExpiryInterval(long sessionExpiryInterval) { this.sessionExpiryInterval = sessionExpiryInterval; } - public int getKeepAlive() { return keepAlive; } - public void setKeepAlive(int keepAlive) { this.keepAlive = keepAlive; } - public int getMaxInflight() { return maxInflight; } - public void setMaxInflight(int maxInflight) { this.maxInflight = maxInflight; } - public boolean isAutomaticReconnect() { return automaticReconnect; } - public void setAutomaticReconnect(boolean automaticReconnect) { this.automaticReconnect = automaticReconnect; } - public int getDefaultQos() { return defaultQos; } - public void setDefaultQos(int defaultQos) { this.defaultQos = defaultQos; } - public boolean isDefaultRetained() { return defaultRetained; } - public void setDefaultRetained(boolean defaultRetained) { this.defaultRetained = defaultRetained; } + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getBrokerUri() { + return brokerUri; + } + + public void setBrokerUri(String brokerUri) { + this.brokerUri = brokerUri; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public boolean isCleanStart() { + return cleanStart; + } + + public void setCleanStart(boolean cleanStart) { + this.cleanStart = cleanStart; + } + + public long getSessionExpiryInterval() { + return sessionExpiryInterval; + } + + public void setSessionExpiryInterval(long sessionExpiryInterval) { + this.sessionExpiryInterval = sessionExpiryInterval; + } + + public int getKeepAlive() { + return keepAlive; + } + + public void setKeepAlive(int keepAlive) { + this.keepAlive = keepAlive; + } + + public int getMaxInflight() { + return maxInflight; + } + + public void setMaxInflight(int maxInflight) { + this.maxInflight = maxInflight; + } + + public boolean isAutomaticReconnect() { + return automaticReconnect; + } + + public void setAutomaticReconnect(boolean automaticReconnect) { + this.automaticReconnect = automaticReconnect; + } + + public int getDefaultQos() { + return defaultQos; + } + + public void setDefaultQos(int defaultQos) { + this.defaultQos = defaultQos; + } + + public boolean isDefaultRetained() { + return defaultRetained; + } + + public void setDefaultRetained(boolean defaultRetained) { + this.defaultRetained = defaultRetained; + } } diff --git a/src/main/java/de/assecutor/votianlt/controller/MessageController.java b/src/main/java/de/assecutor/votianlt/controller/MessageController.java index eb4aec2..29a8292 100644 --- a/src/main/java/de/assecutor/votianlt/controller/MessageController.java +++ b/src/main/java/de/assecutor/votianlt/controller/MessageController.java @@ -60,7 +60,10 @@ public class MessageController { private final JobHistoryService jobHistoryService; private final EmailService emailService; - public MessageController(MqttPublisher mqttPublisher, AppUserRepository appUserRepository, AppUserService appUserService, JobRepository jobRepository, CargoItemRepository cargoItemRepository, TaskRepository taskRepository, PhotoRepository photoRepository, BarcodeRepository barcodeRepository, SignatureRepository signatureRepository, JobHistoryService jobHistoryService, EmailService emailService) { + public MessageController(MqttPublisher mqttPublisher, AppUserRepository appUserRepository, + AppUserService appUserService, JobRepository jobRepository, CargoItemRepository cargoItemRepository, + TaskRepository taskRepository, PhotoRepository photoRepository, BarcodeRepository barcodeRepository, + SignatureRepository signatureRepository, JobHistoryService jobHistoryService, EmailService emailService) { this.mqttPublisher = mqttPublisher; this.appUserRepository = appUserRepository; this.appUserService = appUserService; @@ -75,20 +78,21 @@ public class MessageController { } /** - * Authentication endpoint for mobile app users via MQTT. - * Client sends to /server/login with payload { email, password, clientId }. - * The response is sent back to the requesting client on /client/{clientId}/auth + * Authentication endpoint for mobile app users via MQTT. Client sends to + * /server/login with payload { email, password, clientId }. The response is + * sent back to the requesting client on /client/{clientId}/auth */ public void handleAppLogin(AppLoginRequest request) { log.info("MQTT Endpoint '/server/login' called with email: {}, clientId: {}", - request != null ? request.getEmail() : "null", - request != null ? request.getClientId() : "null"); + request != null ? request.getEmail() : "null", request != null ? request.getClientId() : "null"); AppLoginResponse response; - if (request == null || request.getEmail() == null || request.getPassword() == null || request.getClientId() == null - || request.getEmail().isBlank() || request.getPassword().isBlank() || request.getClientId().isBlank()) { - response = new AppLoginResponse(false, "E-Mail, Passwort und Client-ID sind erforderlich", null, null, null); + if (request == null || request.getEmail() == null || request.getPassword() == null + || request.getClientId() == null || request.getEmail().isBlank() || request.getPassword().isBlank() + || request.getClientId().isBlank()) { + response = new AppLoginResponse(false, "E-Mail, Passwort und Client-ID sind erforderlich", null, null, + null); } else { AppUser user = appUserRepository.findByEmail(request.getEmail()); if (user == null) { @@ -108,20 +112,21 @@ public class MessageController { // Send response via MQTT to specific client if (request != null && request.getClientId() != null && !request.getClientId().isBlank()) { mqttPublisher.publishAsJson("/client/" + request.getClientId() + "/auth", response, false); - log.info("MQTT Response sent to '/client/{}/auth': success={}, message='{}'", - request.getClientId(), response.isSuccess(), response.getMessage()); + log.info("MQTT Response sent to '/client/{}/auth': success={}, message='{}'", request.getClientId(), + response.isSuccess(), response.getMessage()); } } /** - * Endpoint to retrieve jobs assigned to a specific app user with related cargo items and tasks. - * Client sends to /server/{clientId}/jobs/assigned with payload { appUserId }. - * The response is sent back to the requesting client on /client/{clientId}/jobs + * Endpoint to retrieve jobs assigned to a specific app user with related cargo + * items and tasks. Client sends to /server/{clientId}/jobs/assigned with + * payload { appUserId }. The response is sent back to the requesting client on + * /client/{clientId}/jobs */ public void handleGetAssignedJobs(Map request) { log.info("MQTT Endpoint '/server/{clientId}/jobs/assigned' called with data: {}", request); log.debug("Starting to process jobs request for MQTT endpoint"); - + if (request == null || !request.containsKey("appUserId")) { log.info("Assigned jobs request missing appUserId; returning empty list"); return; // Return empty list if no appUserId provided @@ -133,12 +138,15 @@ public class MessageController { return; // Return empty list if appUserId is blank } - // Attempt to get clientId from request (injected from topic) or from stored mapping + // Attempt to get clientId from request (injected from topic) or from stored + // mapping String clientId = null; try { Object cid = request.get("clientId"); - if (cid != null) clientId = cid.toString(); - } catch (Exception ignored) {} + if (cid != null) + clientId = cid.toString(); + } catch (Exception ignored) { + } if (clientId == null || clientId.isBlank()) { clientId = getClientIdForUserId(appUserId); } @@ -148,26 +156,26 @@ public class MessageController { log.debug("Found {} jobs for appUserId: {}", assignedJobs.size(), appUserId); // For each job, fetch related cargo items and tasks (ordered by task order) - List jobsWithRelatedData = assignedJobs.stream() - .map(job -> { - List cargoItems = cargoItemRepository.findByJobId(job.getId()); - List tasks = taskRepository.findByJobIdOrderByTaskOrderAsc(job.getId()); + List jobsWithRelatedData = assignedJobs.stream().map(job -> { + List cargoItems = cargoItemRepository.findByJobId(job.getId()); + List tasks = taskRepository.findByJobIdOrderByTaskOrderAsc(job.getId()); - // Log task details for debugging - tasks.forEach(task -> log.info("Task details for job {}: type={}, order={}", - job.getId(), task.getTaskType(), task.getTaskOrder())); + // Log task details for debugging + tasks.forEach(task -> log.info("Task details for job {}: type={}, order={}", job.getId(), + task.getTaskType(), task.getTaskOrder())); + + return new JobWithRelatedDataDTO(job, cargoItems, tasks); + }).toList(); - return new JobWithRelatedDataDTO(job, cargoItems, tasks); - }) - .toList(); - // Publish to the requesting client's topic if clientId is known if (clientId != null && !clientId.isBlank()) { String topic = "/client/" + clientId + "/jobs"; mqttPublisher.publishAsJson(topic, jobsWithRelatedData, false); - log.info("Published {} assigned jobs for appUserId='{}' to topic '{}'", jobsWithRelatedData.size(), appUserId, topic); + log.info("Published {} assigned jobs for appUserId='{}' to topic '{}'", jobsWithRelatedData.size(), + appUserId, topic); } else { - log.warn("No clientId available to publish assigned jobs for appUserId='{}'. Skipping MQTT publish.", appUserId); + log.warn("No clientId available to publish assigned jobs for appUserId='{}'. Skipping MQTT publish.", + appUserId); } // Log complete JSON for debugging @@ -189,10 +197,10 @@ public class MessageController { } /** - * Report generic task completion from apps. - * Client sends to /app/task/completed with payload { taskId, completedBy?, note? }. - * Broadcasts to /topic/task-updates and /topic/tasks/{taskId}. - * This endpoint accepts any task type (fallback for GENERIC or unknown types). + * Report generic task completion from apps. Client sends to /app/task/completed + * with payload { taskId, completedBy?, note? }. Broadcasts to + * /topic/task-updates and /topic/tasks/{taskId}. This endpoint accepts any task + * type (fallback for GENERIC or unknown types). */ public void handleTaskCompleted(Map payload) { // Backward-compatible entry point: extract taskType from payload (if present) @@ -200,14 +208,17 @@ public class MessageController { String taskType = null; try { Object tt = payload != null ? payload.get("taskType") : null; - if (tt != null) taskType = tt.toString(); - } catch (Exception ignored) {} + if (tt != null) + taskType = tt.toString(); + } catch (Exception ignored) { + } handleTaskCompleted(payload, taskType); } /** - * Central dispatcher for task_completed messages. Decides handling based on taskType. - * PHOTO and CONFIRMATION are routed to specialized handlers; others go to generic processing. + * Central dispatcher for task_completed messages. Decides handling based on + * taskType. PHOTO and CONFIRMATION are routed to specialized handlers; others + * go to generic processing. */ public void handleTaskCompleted(Map payload, String taskType) { String key = taskType == null ? "" : taskType.trim().toUpperCase(); @@ -215,24 +226,24 @@ public class MessageController { log.info("handleTaskCompleted called with taskType={}, data: {}", taskType, payload); switch (key) { - case "PHOTO" -> { - processPhotoTaskCompletion(payload); - } - case "CONFIRMATION" -> { - processConfirmationTaskCompletion(payload); - } - case "SIGNATURE" -> { - processSignatureTaskCompletion(payload); - } - case "TODOLIST" -> { - processTodoListTaskCompletion(payload); - } - case "BARCODE" -> { - processBarcodeTaskCompletion(payload); - } - default -> { - log.info("ERROR: handleTaskCompleted called with taskType={}, data: {}", taskType, payload); - } + case "PHOTO" -> { + processPhotoTaskCompletion(payload); + } + case "CONFIRMATION" -> { + processConfirmationTaskCompletion(payload); + } + case "SIGNATURE" -> { + processSignatureTaskCompletion(payload); + } + case "TODOLIST" -> { + processTodoListTaskCompletion(payload); + } + case "BARCODE" -> { + processBarcodeTaskCompletion(payload); + } + default -> { + log.info("ERROR: handleTaskCompleted called with taskType={}, data: {}", taskType, payload); + } } } @@ -266,16 +277,15 @@ public class MessageController { if (!barcodes.isEmpty()) { for (String barcodeString : barcodes) { - Barcode barcodeEntry = new Barcode( - new ObjectId(taskId.toString()), - barcodeString, - task.getCompletedBy() - ); + Barcode barcodeEntry = new Barcode(new ObjectId(taskId.toString()), barcodeString, + task.getCompletedBy()); barcodeRepository.save(barcodeEntry); } - extraDataSummary = barcodes.size() + " Barcode(s) gescannt: " + String.join(", ", barcodes.subList(0, Math.min(3, barcodes.size()))) + (barcodes.size() > 3 ? "..." : ""); + extraDataSummary = barcodes.size() + " Barcode(s) gescannt: " + + String.join(", ", barcodes.subList(0, Math.min(3, barcodes.size()))) + + (barcodes.size() > 3 ? "..." : ""); log.info("Saved {} barcodes for taskId={}", barcodes.size(), taskId); } else { extraDataSummary = "Keine Barcodes gescannt"; @@ -315,11 +325,8 @@ public class MessageController { Object signatureSvgObj = extraData.get("signatureSvg"); if (signatureSvgObj instanceof String signatureSvg) { if (!signatureSvg.isBlank()) { - Signature signatureEntry = new Signature( - new ObjectId(taskId.toString()), - signatureSvg, - task.getCompletedBy() - ); + Signature signatureEntry = new Signature(new ObjectId(taskId.toString()), signatureSvg, + task.getCompletedBy()); signatureRepository.save(signatureEntry); extraDataSummary = "Unterschrift erfasst (SVG, " + signatureSvg.length() + " Zeichen)"; @@ -366,12 +373,9 @@ public class MessageController { List photos = (List) photosList; if (!photos.isEmpty()) { - for (String photoString: photos) { - Photo photoEntry = new Photo( - new ObjectId(taskId.toString()), - photoString, - task.getCompletedBy() - ); + for (String photoString : photos) { + Photo photoEntry = new Photo(new ObjectId(taskId.toString()), photoString, + task.getCompletedBy()); photoRepository.save(photoEntry); } @@ -424,8 +428,8 @@ public class MessageController { String taskType = task.getTaskType() != null ? task.getTaskType().toString() : "Unknown"; String taskDisplayName = task.getDisplayName() != null ? task.getDisplayName() : taskType; - jobHistoryService.logTaskCompletion(jobId, taskType, taskIdStr, task.getCompletedBy(), - taskDisplayName, extraDataSummary); + jobHistoryService.logTaskCompletion(jobId, taskType, taskIdStr, task.getCompletedBy(), taskDisplayName, + extraDataSummary); } catch (Exception e) { log.warn("Failed to log task completion history for task {}: {}", taskIdStr, e.getMessage()); } @@ -441,11 +445,12 @@ public class MessageController { // Check if this was the last task and send job completion notification emailService.checkAndSendJobCompletionNotification(jobId, completedBy); } catch (Exception e) { - log.warn("Failed to send task completion email notification for task {}: {}", taskIdStr, e.getMessage()); + log.warn("Failed to send task completion email notification for task {}: {}", taskIdStr, + e.getMessage()); } - log.info("Task marked completed. taskId={}, completedBy={}, extraData={}", - taskIdStr, task.getCompletedBy(), extraDataSummary); + log.info("Task marked completed. taskId={}, completedBy={}, extraData={}", taskIdStr, task.getCompletedBy(), + extraDataSummary); } catch (IllegalArgumentException ex) { log.error("Invalid taskId format for completion: {}", taskIdStr); } catch (Exception ex) { diff --git a/src/main/java/de/assecutor/votianlt/dto/JobWithRelatedDataDTO.java b/src/main/java/de/assecutor/votianlt/dto/JobWithRelatedDataDTO.java index 7489702..35a266e 100644 --- a/src/main/java/de/assecutor/votianlt/dto/JobWithRelatedDataDTO.java +++ b/src/main/java/de/assecutor/votianlt/dto/JobWithRelatedDataDTO.java @@ -10,8 +10,8 @@ import lombok.NoArgsConstructor; import java.util.List; /** - * DTO for returning job data with related cargo items and tasks. - * This combines Job entity with its associated CargoItems and TaskEntries. + * DTO for returning job data with related cargo items and tasks. This combines + * Job entity with its associated CargoItems and TaskEntries. */ @Data @NoArgsConstructor diff --git a/src/main/java/de/assecutor/votianlt/model/AppUser.java b/src/main/java/de/assecutor/votianlt/model/AppUser.java index 7217159..408e814 100644 --- a/src/main/java/de/assecutor/votianlt/model/AppUser.java +++ b/src/main/java/de/assecutor/votianlt/model/AppUser.java @@ -74,8 +74,8 @@ public class AppUser { } /** - * Returns the ObjectId as string for JSON serialization. - * This ensures that the app user id is returned as a string when users are retrieved via API. + * Returns the ObjectId as string for JSON serialization. This ensures that the + * app user id is returned as a string when users are retrieved via API. */ @JsonGetter("id") public String getIdAsString() { diff --git a/src/main/java/de/assecutor/votianlt/model/Barcode.java b/src/main/java/de/assecutor/votianlt/model/Barcode.java index f07b334..e7af51c 100644 --- a/src/main/java/de/assecutor/votianlt/model/Barcode.java +++ b/src/main/java/de/assecutor/votianlt/model/Barcode.java @@ -8,8 +8,8 @@ import org.bson.types.ObjectId; import java.time.LocalDateTime; /** - * Barcode entity for storing barcode data from task completions. - * References the task ObjectId and stores barcode strings. + * Barcode entity for storing barcode data from task completions. References the + * task ObjectId and stores barcode strings. */ @Data @Document(collection = "barcodes") diff --git a/src/main/java/de/assecutor/votianlt/model/CargoItem.java b/src/main/java/de/assecutor/votianlt/model/CargoItem.java index beb165e..1f266d4 100644 --- a/src/main/java/de/assecutor/votianlt/model/CargoItem.java +++ b/src/main/java/de/assecutor/votianlt/model/CargoItem.java @@ -41,12 +41,11 @@ public class CargoItem { private Double heightMm; /** - * Returns the ObjectId as string for JSON serialization. - * This ensures that the cargo item id is returned as a string when items are retrieved via API. + * Returns the ObjectId as string for JSON serialization. This ensures that the + * cargo item id is returned as a string when items are retrieved via API. */ @JsonGetter("id") public String getIdAsString() { return id != null ? id.toString() : null; } } - diff --git a/src/main/java/de/assecutor/votianlt/model/Company.java b/src/main/java/de/assecutor/votianlt/model/Company.java index e05e762..6878892 100644 --- a/src/main/java/de/assecutor/votianlt/model/Company.java +++ b/src/main/java/de/assecutor/votianlt/model/Company.java @@ -4,8 +4,7 @@ import lombok.Data; import org.bson.types.ObjectId; @Data -public class Company -{ +public class Company { private ObjectId id; private String name; diff --git a/src/main/java/de/assecutor/votianlt/model/Customer.java b/src/main/java/de/assecutor/votianlt/model/Customer.java index 31cddc1..45a87f5 100644 --- a/src/main/java/de/assecutor/votianlt/model/Customer.java +++ b/src/main/java/de/assecutor/votianlt/model/Customer.java @@ -8,8 +8,7 @@ import org.springframework.data.mongodb.core.mapping.Field; @Data @Document(collection = "customers") -public class Customer -{ +public class Customer { @Id private ObjectId id; diff --git a/src/main/java/de/assecutor/votianlt/model/Invoice.java b/src/main/java/de/assecutor/votianlt/model/Invoice.java index 6e493ba..8d315f6 100644 --- a/src/main/java/de/assecutor/votianlt/model/Invoice.java +++ b/src/main/java/de/assecutor/votianlt/model/Invoice.java @@ -16,4 +16,3 @@ public class Invoice { private double betrag; private String beschreibung; } - diff --git a/src/main/java/de/assecutor/votianlt/model/Job.java b/src/main/java/de/assecutor/votianlt/model/Job.java index eb0878f..1ea2e8e 100644 --- a/src/main/java/de/assecutor/votianlt/model/Job.java +++ b/src/main/java/de/assecutor/votianlt/model/Job.java @@ -127,8 +127,8 @@ public class Job { private BigDecimal price; /** - * Returns the ObjectId as string for JSON serialization. - * This ensures that the job id is returned as a string when jobs are retrieved via API. + * Returns the ObjectId as string for JSON serialization. This ensures that the + * job id is returned as a string when jobs are retrieved via API. */ @JsonGetter("id") public String getIdAsString() { diff --git a/src/main/java/de/assecutor/votianlt/model/JobHistory.java b/src/main/java/de/assecutor/votianlt/model/JobHistory.java index 593bcf5..711c101 100644 --- a/src/main/java/de/assecutor/votianlt/model/JobHistory.java +++ b/src/main/java/de/assecutor/votianlt/model/JobHistory.java @@ -8,8 +8,8 @@ import org.bson.types.ObjectId; import java.time.LocalDateTime; /** - * Job History entity for tracking all changes made to a job. - * Each entry represents a single change or action performed on a job. + * Job History entity for tracking all changes made to a job. Each entry + * represents a single change or action performed on a job. */ @Data @Document(collection = "job_history") @@ -34,7 +34,8 @@ public class JobHistory { private String reason; /** - * Description of what was changed (e.g., "Status changed from CREATED to IN_PROGRESS") + * Description of what was changed (e.g., "Status changed from CREATED to + * IN_PROGRESS") */ private String description; @@ -78,8 +79,8 @@ public class JobHistory { } // Constructor for detailed history entry - public JobHistory(ObjectId jobId, String reason, String description, String changedBy, - JobHistoryType changeType, String oldValue, String newValue) { + public JobHistory(ObjectId jobId, String reason, String description, String changedBy, JobHistoryType changeType, + String oldValue, String newValue) { this(jobId, reason, description, changedBy); this.changeType = changeType; this.oldValue = oldValue; diff --git a/src/main/java/de/assecutor/votianlt/model/Photo.java b/src/main/java/de/assecutor/votianlt/model/Photo.java index 517ccb9..f102606 100644 --- a/src/main/java/de/assecutor/votianlt/model/Photo.java +++ b/src/main/java/de/assecutor/votianlt/model/Photo.java @@ -9,8 +9,8 @@ import java.time.LocalDateTime; import java.util.List; /** - * Photo entity for storing photo data from task completions. - * References the job ObjectId and stores base64 encoded photos. + * Photo entity for storing photo data from task completions. References the job + * ObjectId and stores base64 encoded photos. */ @Data @Document(collection = "photos") diff --git a/src/main/java/de/assecutor/votianlt/model/TaskEntry.java b/src/main/java/de/assecutor/votianlt/model/TaskEntry.java index 48bdbb4..f67ddf4 100644 --- a/src/main/java/de/assecutor/votianlt/model/TaskEntry.java +++ b/src/main/java/de/assecutor/votianlt/model/TaskEntry.java @@ -45,8 +45,8 @@ public class TaskEntry { private String completedBy; /** - * Returns the ObjectId as string for JSON serialization. - * This ensures that the task id is returned as a string when jobs are retrieved via API. + * Returns the ObjectId as string for JSON serialization. This ensures that the + * task id is returned as a string when jobs are retrieved via API. */ @JsonGetter("id") public String getIdAsString() { @@ -54,8 +54,8 @@ public class TaskEntry { } /** - * Returns the job ObjectId as string for JSON serialization. - * This ensures that the job id is returned as a string instead of ObjectId object. + * Returns the job ObjectId as string for JSON serialization. This ensures that + * the job id is returned as a string instead of ObjectId object. */ @JsonGetter("jobId") public String getJobIdAsString() { @@ -66,7 +66,8 @@ public class TaskEntry { * Enum for different task types */ public enum TaskType { - CONFIRMATION("Bestätigung"), + CONFIRMATION( + "Bestätigung"), SIGNATURE("Unterschrift"), TODOLIST("To-Do Liste"), PHOTO("Foto"), @@ -108,4 +109,3 @@ public class TaskEntry { private Map additionalConfig; } } - diff --git a/src/main/java/de/assecutor/votianlt/model/User.java b/src/main/java/de/assecutor/votianlt/model/User.java index 363da6d..675bdbf 100644 --- a/src/main/java/de/assecutor/votianlt/model/User.java +++ b/src/main/java/de/assecutor/votianlt/model/User.java @@ -18,16 +18,16 @@ public class User { private int usrId; private String title; - private String name; // Nachname - private String firstname; // Vorname + private String name; // Nachname + private String firstname; // Vorname // Firmen-/Adressdaten - private String company; // Firma - private String street; // Straße - private String houseNumber; // Hausnr + private String company; // Firma + private String street; // Straße + private String houseNumber; // Hausnr private String addressAddition; // Adresszusatz (optional) - private String zip; // Postleitzahl - private String city; // Stadt + private String zip; // Postleitzahl + private String city; // Stadt @Indexed(unique = true) private String email; diff --git a/src/main/java/de/assecutor/votianlt/model/task/BaseTask.java b/src/main/java/de/assecutor/votianlt/model/task/BaseTask.java index 7661e2e..766f2e7 100644 --- a/src/main/java/de/assecutor/votianlt/model/task/BaseTask.java +++ b/src/main/java/de/assecutor/votianlt/model/task/BaseTask.java @@ -17,13 +17,11 @@ import java.time.LocalDateTime; @NoArgsConstructor @Document(collection = "tasks") @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "taskType") -@JsonSubTypes({ - @JsonSubTypes.Type(value = ConfirmationTask.class, name = "CONFIRMATION"), - @JsonSubTypes.Type(value = SignatureTask.class, name = "SIGNATURE"), - @JsonSubTypes.Type(value = TodoListTask.class, name = "TODOLIST"), - @JsonSubTypes.Type(value = PhotoTask.class, name = "PHOTO"), - @JsonSubTypes.Type(value = BarcodeTask.class, name = "BARCODE") -}) +@JsonSubTypes({ @JsonSubTypes.Type(value = ConfirmationTask.class, name = "CONFIRMATION"), + @JsonSubTypes.Type(value = SignatureTask.class, name = "SIGNATURE"), + @JsonSubTypes.Type(value = TodoListTask.class, name = "TODOLIST"), + @JsonSubTypes.Type(value = PhotoTask.class, name = "PHOTO"), + @JsonSubTypes.Type(value = BarcodeTask.class, name = "BARCODE") }) public abstract class BaseTask { @Id @JsonIgnore diff --git a/src/main/java/de/assecutor/votianlt/model/task/SignatureTask.java b/src/main/java/de/assecutor/votianlt/model/task/SignatureTask.java index e1dfc04..2914803 100644 --- a/src/main/java/de/assecutor/votianlt/model/task/SignatureTask.java +++ b/src/main/java/de/assecutor/votianlt/model/task/SignatureTask.java @@ -9,7 +9,6 @@ import lombok.NoArgsConstructor; @EqualsAndHashCode(callSuper = true) public class SignatureTask extends BaseTask { - @Override public String getTaskType() { return "SIGNATURE"; diff --git a/src/main/java/de/assecutor/votianlt/model/task/TaskType.java b/src/main/java/de/assecutor/votianlt/model/task/TaskType.java index 33eaa87..6355c6d 100644 --- a/src/main/java/de/assecutor/votianlt/model/task/TaskType.java +++ b/src/main/java/de/assecutor/votianlt/model/task/TaskType.java @@ -1,11 +1,7 @@ package de.assecutor.votianlt.model.task; public enum TaskType { - CONFIRMATION("Bestätigung"), - SIGNATURE("Unterschrift"), - TODOLIST("To-Do Liste"), - PHOTO("Foto"), - BARCODE("Barcode"); + CONFIRMATION("Bestätigung"), SIGNATURE("Unterschrift"), TODOLIST("To-Do Liste"), PHOTO("Foto"), BARCODE("Barcode"); private final String displayName; diff --git a/src/main/java/de/assecutor/votianlt/mqtt/MqttClientRunner.java b/src/main/java/de/assecutor/votianlt/mqtt/MqttClientRunner.java index 246ad6b..1c8ade9 100644 --- a/src/main/java/de/assecutor/votianlt/mqtt/MqttClientRunner.java +++ b/src/main/java/de/assecutor/votianlt/mqtt/MqttClientRunner.java @@ -6,8 +6,8 @@ import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; /** - * Kept for compatibility: The actual MQTT v5 lifecycle is managed by MqttV5ClientManager. - * This runner only logs application readiness. + * Kept for compatibility: The actual MQTT v5 lifecycle is managed by + * MqttV5ClientManager. This runner only logs application readiness. */ @Component @Slf4j diff --git a/src/main/java/de/assecutor/votianlt/mqtt/MqttPublisher.java b/src/main/java/de/assecutor/votianlt/mqtt/MqttPublisher.java index 6b59506..6ce435d 100644 --- a/src/main/java/de/assecutor/votianlt/mqtt/MqttPublisher.java +++ b/src/main/java/de/assecutor/votianlt/mqtt/MqttPublisher.java @@ -9,11 +9,13 @@ import org.springframework.context.annotation.Lazy; /** * Simple MQTT publishing helper to send JSON payloads. * - * Note: In environments where Spring Integration MQTT is unavailable (e.g., offline CI), - * this implementation degrades to a no-op publisher that logs the intended message. + * Note: In environments where Spring Integration MQTT is unavailable (e.g., + * offline CI), this implementation degrades to a no-op publisher that logs the + * intended message. */ public interface MqttPublisher { void publishAsJson(String topic, Object payload); + void publishAsJson(String topic, Object payload, boolean retained); } diff --git a/src/main/java/de/assecutor/votianlt/mqtt/MqttV5ClientManager.java b/src/main/java/de/assecutor/votianlt/mqtt/MqttV5ClientManager.java index f35f1ed..d56e180 100644 --- a/src/main/java/de/assecutor/votianlt/mqtt/MqttV5ClientManager.java +++ b/src/main/java/de/assecutor/votianlt/mqtt/MqttV5ClientManager.java @@ -20,7 +20,8 @@ import java.util.Map; import java.util.UUID; /** - * Manages a single MQTT v5 client connection with Spring lifecycle using HiveMQ MQTT Client. + * Manages a single MQTT v5 client connection with Spring lifecycle using HiveMQ + * MQTT Client. */ @Service @Slf4j @@ -50,23 +51,15 @@ public class MqttV5ClientManager implements SmartLifecycle { String host = uri.getHost(); int port = 42099; - var builder = Mqtt5Client.builder() - .identifier(clientId) - .serverHost(host) - .serverPort(port); + var builder = Mqtt5Client.builder().identifier(clientId).serverHost(host).serverPort(port); if (props.isAutomaticReconnect()) { builder = builder.automaticReconnectWithDefaultConfig(); } client = builder.buildAsync(); - var connect = client.connectWith() - .cleanStart(props.isCleanStart()) - .keepAlive(props.getKeepAlive()) - .sessionExpiryInterval(props.getSessionExpiryInterval()) - .simpleAuth() - .username("app") - .password("apppwd".getBytes(StandardCharsets.UTF_8)) - .applySimpleAuth(); + var connect = client.connectWith().cleanStart(props.isCleanStart()).keepAlive(props.getKeepAlive()) + .sessionExpiryInterval(props.getSessionExpiryInterval()).simpleAuth().username("app") + .password("apppwd".getBytes(StandardCharsets.UTF_8)).applySimpleAuth(); log.info("[MQTT] Connecting to {} with clientId={} ...", props.getBrokerUri(), clientId); connect.send().join(); @@ -86,15 +79,9 @@ public class MqttV5ClientManager implements SmartLifecycle { }); // Subscribe to topics with QoS - String[] topics = new String[]{ - "/server/+/task/photo/completed", - "/server/+/task/confirm", - "/server/+/task/completed", - "/server/+/task_completed", - "/server/+/job/status", - "/server/+/jobs/assigned", - "/server/login" - }; + String[] topics = new String[] { "/server/+/task/photo/completed", "/server/+/task/confirm", + "/server/+/task/completed", "/server/+/task_completed", "/server/+/job/status", + "/server/+/jobs/assigned", "/server/login" }; MqttQos qos = mapQos(props.getDefaultQos()); for (String topic : topics) { client.subscribeWith().topicFilter(topic).qos(qos).send().join(); @@ -123,7 +110,8 @@ public class MqttV5ClientManager implements SmartLifecycle { private void handleInbound(String topic, byte[] payload) { String json = new String(payload, StandardCharsets.UTF_8); try { - Map map = objectMapper.readValue(json, new TypeReference>(){}); + Map map = objectMapper.readValue(json, new TypeReference>() { + }); routeInbound(topic, map); } catch (Exception ex) { log.error("Failed to parse inbound MQTT JSON on {}: {}", topic, ex.getMessage()); @@ -134,8 +122,10 @@ public class MqttV5ClientManager implements SmartLifecycle { try { // The consolidated topic /server/{clientId}/task_completed is used by apps to // report completion of any task type. Only PHOTO and CONFIRMATION require - // specialized processing on the server side. All other task types are handled by the - // generic handler handleTaskCompleted(). This keeps routing simple while allowing + // specialized processing on the server side. All other task types are handled + // by the + // generic handler handleTaskCompleted(). This keeps routing simple while + // allowing // special logic (e.g., photo persistence) where necessary. if (topic.matches("/server/.+/task_completed")) { try { @@ -161,7 +151,8 @@ public class MqttV5ClientManager implements SmartLifecycle { } } else if (topic.equals("/server/login")) { var om = new ObjectMapper(); - de.assecutor.votianlt.dto.AppLoginRequest req = om.convertValue(payload, de.assecutor.votianlt.dto.AppLoginRequest.class); + de.assecutor.votianlt.dto.AppLoginRequest req = om.convertValue(payload, + de.assecutor.votianlt.dto.AppLoginRequest.class); messageController.handleAppLogin(req); } else { log.debug("No route for topic {}", topic); @@ -192,9 +183,9 @@ public class MqttV5ClientManager implements SmartLifecycle { private MqttQos mapQos(int q) { return switch (q) { - case 0 -> MqttQos.AT_MOST_ONCE; - case 1 -> MqttQos.AT_LEAST_ONCE; - default -> MqttQos.EXACTLY_ONCE; + case 0 -> MqttQos.AT_MOST_ONCE; + case 1 -> MqttQos.AT_LEAST_ONCE; + default -> MqttQos.EXACTLY_ONCE; }; } @@ -204,12 +195,7 @@ public class MqttV5ClientManager implements SmartLifecycle { log.warn("[MQTT] Not connected, dropping publish topic={}", topic); return; } - client.publishWith() - .topic(topic) - .payload(payload) - .qos(mapQos(qos)) - .retain(retained) - .send() + client.publishWith().topic(topic).payload(payload).qos(mapQos(qos)).retain(retained).send() .whenComplete((ack, ex) -> { if (ex != null) { log.error("Failed to publish to {}: {}", topic, ex.getMessage(), ex); diff --git a/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java b/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java index 940d92c..341705d 100644 --- a/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java +++ b/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java @@ -24,6 +24,7 @@ import de.assecutor.votianlt.pages.view.EditProfileView; import de.assecutor.votianlt.security.SecurityService; import static com.vaadin.flow.theme.lumo.LumoUtility.*; + @AnonymousAllowed @Layout @@ -37,8 +38,9 @@ public final class MainLayout extends AppLayout { public MainLayout(SecurityService securityService) { this.securityService = securityService; setPrimarySection(Section.DRAWER); - - // Always build the drawer; keep references and toggle visibility on attach and after navigation + + // Always build the drawer; keep references and toggle visibility on attach and + // after navigation headerRef = createHeader(); navRef = new Scroller(createSideNav()); userMenuRef = createUserMenu(); @@ -52,9 +54,12 @@ public final class MainLayout extends AppLayout { private void updateDrawerVisibility() { boolean loggedIn = securityService.isUserLoggedIn(); - if (headerRef != null) headerRef.setVisible( loggedIn ); - if (navRef != null) navRef.setVisible( loggedIn ); - if (userMenuRef != null) userMenuRef.setVisible( loggedIn ); + if (headerRef != null) + headerRef.setVisible(loggedIn); + if (navRef != null) + navRef.setVisible(loggedIn); + if (userMenuRef != null) + userMenuRef.setVisible(loggedIn); setDrawerOpened(loggedIn); } @@ -121,7 +126,8 @@ public final class MainLayout extends AppLayout { userContent.add(profile, myInvoices, imprint); userDetails.add(userContent); - // Create a vertical layout to hold both regular menu items and collapsible sections + // Create a vertical layout to hold both regular menu items and collapsible + // sections VerticalLayout navContainer = new VerticalLayout(); navContainer.setPadding(false); navContainer.setSpacing(false); @@ -150,13 +156,12 @@ public final class MainLayout extends AppLayout { avatar.setColorIndex(5); var userNameSpan = new Span(); - + var userMenuItem = userMenu.addItem(avatar); userMenuItem.add(userNameSpan); - + // Profil anzeigen mit Navigation - userMenuItem.getSubMenu().addItem("Profil anzeigen", e -> - UI.getCurrent().navigate(EditProfileView.class)); + userMenuItem.getSubMenu().addItem("Profil anzeigen", e -> UI.getCurrent().navigate(EditProfileView.class)); userMenuItem.getSubMenu().addItem("Einstellungen"); userMenuItem.getSubMenu().addItem("Abmelden", e -> securityService.logout()); @@ -166,7 +171,7 @@ public final class MainLayout extends AppLayout { avatar.setName(currentUser); userNameSpan.setText(currentUser); }; - + // Initial und bei Attach aktualisieren updateUserInfo.run(); addAttachListener(e -> updateUserInfo.run()); diff --git a/src/main/java/de/assecutor/votianlt/pages/domain/AddCompanyRepository.java b/src/main/java/de/assecutor/votianlt/pages/domain/AddCompanyRepository.java index 69c3e4a..847e9d9 100644 --- a/src/main/java/de/assecutor/votianlt/pages/domain/AddCompanyRepository.java +++ b/src/main/java/de/assecutor/votianlt/pages/domain/AddCompanyRepository.java @@ -5,5 +5,4 @@ import org.springframework.data.mongodb.repository.MongoRepository; public interface AddCompanyRepository extends MongoRepository { - } diff --git a/src/main/java/de/assecutor/votianlt/pages/domain/AddCustomerRepository.java b/src/main/java/de/assecutor/votianlt/pages/domain/AddCustomerRepository.java index 559d067..65ba975 100644 --- a/src/main/java/de/assecutor/votianlt/pages/domain/AddCustomerRepository.java +++ b/src/main/java/de/assecutor/votianlt/pages/domain/AddCustomerRepository.java @@ -6,5 +6,4 @@ import org.springframework.data.mongodb.repository.MongoRepository; public interface AddCustomerRepository extends MongoRepository { - } diff --git a/src/main/java/de/assecutor/votianlt/pages/domain/LoginRepository.java b/src/main/java/de/assecutor/votianlt/pages/domain/LoginRepository.java index f0186dd..19f7be1 100644 --- a/src/main/java/de/assecutor/votianlt/pages/domain/LoginRepository.java +++ b/src/main/java/de/assecutor/votianlt/pages/domain/LoginRepository.java @@ -7,7 +7,5 @@ import java.util.Optional; public interface LoginRepository extends MongoRepository { - - Optional findByEmail(String email); } diff --git a/src/main/java/de/assecutor/votianlt/pages/service/AddCompanyService.java b/src/main/java/de/assecutor/votianlt/pages/service/AddCompanyService.java index 25ac151..bcbb403 100644 --- a/src/main/java/de/assecutor/votianlt/pages/service/AddCompanyService.java +++ b/src/main/java/de/assecutor/votianlt/pages/service/AddCompanyService.java @@ -15,8 +15,6 @@ public class AddCompanyService { this.addCompanyRepository = addCompanyRepository; } - - public void addCompany(Company company) { addCompanyRepository.save(company); } diff --git a/src/main/java/de/assecutor/votianlt/pages/service/AddCustomerService.java b/src/main/java/de/assecutor/votianlt/pages/service/AddCustomerService.java index 224e31a..d1d5dda 100644 --- a/src/main/java/de/assecutor/votianlt/pages/service/AddCustomerService.java +++ b/src/main/java/de/assecutor/votianlt/pages/service/AddCustomerService.java @@ -18,8 +18,6 @@ public class AddCustomerService { this.securityService = securityService; } - - public void addCustomer(Customer customer) { // Setze den aktuellen Benutzer als Ersteller - jetzt direkt aus der Session de.assecutor.votianlt.model.User currentUser = securityService.getCurrentDatabaseUser(); diff --git a/src/main/java/de/assecutor/votianlt/pages/service/AddJobService.java b/src/main/java/de/assecutor/votianlt/pages/service/AddJobService.java index 73d60f7..e143b2d 100644 --- a/src/main/java/de/assecutor/votianlt/pages/service/AddJobService.java +++ b/src/main/java/de/assecutor/votianlt/pages/service/AddJobService.java @@ -32,10 +32,14 @@ public class AddJobService { private final SecurityService securityService; private final JobHistoryService jobHistoryService; private final EmailService emailService; + /** * Speichert einen neuen Auftrag samt CargoItems und Tasks - * @param job der Auftrag - * @param transientCargo zugehörige, noch nicht gespeicherte CargoItems aus der View + * + * @param job + * der Auftrag + * @param transientCargo + * zugehörige, noch nicht gespeicherte CargoItems aus der View */ public Job addJobWithCargo(Job job, List transientCargo, List transientTasks) { try { @@ -58,10 +62,8 @@ public class AddJobService { // CargoItems separat mit Referenz auf Job speichern, IDs im Job verknüpfen if (transientCargo != null && !transientCargo.isEmpty()) { - List itemsWithJob = transientCargo.stream() - .filter(Objects::nonNull) - .filter(ci -> ci.getDescription() != null && !ci.getDescription().isBlank()) - .map(ci -> { + List itemsWithJob = transientCargo.stream().filter(Objects::nonNull) + .filter(ci -> ci.getDescription() != null && !ci.getDescription().isBlank()).map(ci -> { CargoItem copy = new CargoItem(); copy.setJobId(jobId); copy.setDescription(ci.getDescription()); @@ -75,13 +77,12 @@ public class AddJobService { cargoItemRepository.saveAll(itemsWithJob); modified = true; } - + // Tasks separat speichern und referenzieren mit korrekter Nummerierung if (transientTasks != null && !transientTasks.isEmpty()) { - var filteredTasks = transientTasks.stream() - .filter(Objects::nonNull) - .filter(task -> task.getTaskType() != null) // Filter nach TaskType statt Text - .toList(); + var filteredTasks = transientTasks.stream().filter(Objects::nonNull) + .filter(task -> task.getTaskType() != null) // Filter nach TaskType statt Text + .toList(); // Setze JobId und stelle sicher, dass taskOrder korrekt ist for (int i = 0; i < filteredTasks.size(); i++) { @@ -113,7 +114,8 @@ public class AddJobService { try { emailService.sendJobCreationNotification(savedJob.getId(), savedJob.getCreatedBy()); } catch (Exception e) { - log.warn("Failed to send job creation email notification for job {}: {}", savedJob.getIdAsString(), e.getMessage()); + log.warn("Failed to send job creation email notification for job {}: {}", savedJob.getIdAsString(), + e.getMessage()); } log.info("Auftrag erfolgreich gespeichert: {}", savedJob.getJobNumber()); @@ -135,8 +137,7 @@ public class AddJobService { // Zähle Aufträge des aktuellen Tages String todayPrefix = prefix + timestamp; long todayCount = jobRepository.findAll().stream() - .filter(job -> job.getJobNumber() != null && job.getJobNumber().startsWith(todayPrefix)) - .count(); + .filter(job -> job.getJobNumber() != null && job.getJobNumber().startsWith(todayPrefix)).count(); // Generiere neue Nummer String jobNumber; diff --git a/src/main/java/de/assecutor/votianlt/pages/service/AppDeviceService.java b/src/main/java/de/assecutor/votianlt/pages/service/AppDeviceService.java index 2865ab9..76803bb 100644 --- a/src/main/java/de/assecutor/votianlt/pages/service/AppDeviceService.java +++ b/src/main/java/de/assecutor/votianlt/pages/service/AppDeviceService.java @@ -27,12 +27,12 @@ public class AppDeviceService { // Set creation and update metadata appDevice.setErstelltAm(java.time.LocalDateTime.now()); appDevice.setAktualisiertAm(java.time.LocalDateTime.now()); - + // Set creator and updater - current user ID is required ObjectId currentUserId = securityService.getCurrentUserId(); appDevice.setErstelltVon(currentUserId); appDevice.setAktualisiertVon(currentUserId); - + return appDeviceRepository.save(appDevice); } diff --git a/src/main/java/de/assecutor/votianlt/pages/service/AppUserService.java b/src/main/java/de/assecutor/votianlt/pages/service/AppUserService.java index 19f5578..f5fee57 100644 --- a/src/main/java/de/assecutor/votianlt/pages/service/AppUserService.java +++ b/src/main/java/de/assecutor/votianlt/pages/service/AppUserService.java @@ -32,17 +32,17 @@ public class AppUserService { String hashedPassword = passwordEncoder.encode(appUser.getPassword()); appUser.setPassword(hashedPassword); } - + // Set creation and update metadata appUser.setErstelltAm(java.time.LocalDateTime.now()); appUser.setAktualisiertAm(java.time.LocalDateTime.now()); - + // Set creator and updater - current user ID is required ObjectId currentUserId = securityService.getCurrentUserId(); appUser.setErstelltVon(currentUserId); appUser.setAktualisiertVon(currentUserId); appUser.setOwner(currentUserId); - + return appUserRepository.save(appUser); } @@ -62,22 +62,26 @@ public class AppUserService { public AppUser updateAppUser(AppUser appUser) { // Hash the password if it's being updated and not empty if (appUser.getPassword() != null && !appUser.getPassword().isEmpty()) { - // Only hash if it's not already hashed (BCrypt hashes start with $2a$, $2b$, or $2y$) + // Only hash if it's not already hashed (BCrypt hashes start with $2a$, $2b$, or + // $2y$) if (!appUser.getPassword().startsWith("$2")) { String hashedPassword = passwordEncoder.encode(appUser.getPassword()); appUser.setPassword(hashedPassword); } } - + appUser.setAktualisiertAm(java.time.LocalDateTime.now()); appUser.setAktualisiertVon(securityService.getCurrentUserId()); return appUserRepository.save(appUser); } - + /** * Verify a plain text password against the stored hashed password - * @param plainPassword The plain text password to verify - * @param hashedPassword The stored BCrypt hashed password + * + * @param plainPassword + * The plain text password to verify + * @param hashedPassword + * The stored BCrypt hashed password * @return true if the password matches, false otherwise */ public boolean verifyPassword(String plainPassword, String hashedPassword) { diff --git a/src/main/java/de/assecutor/votianlt/pages/service/CustomerService.java b/src/main/java/de/assecutor/votianlt/pages/service/CustomerService.java index b751590..5962432 100644 --- a/src/main/java/de/assecutor/votianlt/pages/service/CustomerService.java +++ b/src/main/java/de/assecutor/votianlt/pages/service/CustomerService.java @@ -26,8 +26,6 @@ public class CustomerService { return todoRepository.findAllBy(pageable).toList(); } - - public List findAll() { return todoRepository.findAll(); } diff --git a/src/main/java/de/assecutor/votianlt/pages/service/PasswordResetService.java b/src/main/java/de/assecutor/votianlt/pages/service/PasswordResetService.java index 0aea8a1..e06f530 100644 --- a/src/main/java/de/assecutor/votianlt/pages/service/PasswordResetService.java +++ b/src/main/java/de/assecutor/votianlt/pages/service/PasswordResetService.java @@ -4,7 +4,7 @@ import de.assecutor.votianlt.model.AppUser; import de.assecutor.votianlt.model.User; import de.assecutor.votianlt.repository.AppUserRepository; import de.assecutor.votianlt.repository.UserRepository; -import de.assecutor.votianlt.util.MailUtil; +import de.assecutor.votianlt.service.EmailService; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -17,33 +17,35 @@ import java.util.Optional; @Service public class PasswordResetService { - public enum UserType { USERS, APP_USER } + public enum UserType { + USERS, APP_USER + } private final UserRepository userRepository; private final AppUserRepository appUserRepository; private final PasswordEncoder passwordEncoder; - private final MailUtil mailUtil; + private final EmailService emailService; private static final Duration TOKEN_VALIDITY = Duration.ofMinutes(15); - public PasswordResetService(UserRepository userRepository, - AppUserRepository appUserRepository, - PasswordEncoder passwordEncoder, - MailUtil mailUtil) { + public PasswordResetService(UserRepository userRepository, AppUserRepository appUserRepository, + PasswordEncoder passwordEncoder, EmailService emailService) { this.userRepository = userRepository; this.appUserRepository = appUserRepository; this.passwordEncoder = passwordEncoder; - this.mailUtil = mailUtil; + this.emailService = emailService; } /** - * Initiate reset without asking for user type. Looks up the email in both collections - * and only proceeds if it exists in exactly one of them. Otherwise, it silently returns - * to avoid leaking account existence. + * Initiate reset without asking for user type. Looks up the email in both + * collections and only proceeds if it exists in exactly one of them. Otherwise, + * it silently returns to avoid leaking account existence. */ public void initiateResetAuto(String email, String baseUrl) { - if (email == null) return; + if (email == null) + return; String normalized = email.trim(); - if (normalized.isEmpty()) return; + if (normalized.isEmpty()) + return; var userOpt = userRepository.findByEmail(normalized); var appUser = appUserRepository.findByEmail(normalized); boolean inUsers = userOpt.isPresent(); @@ -63,75 +65,80 @@ public class PasswordResetService { String link = baseUrl + "/forget-password?token=" + token + "&type=" + typeParam; switch (userType) { - case USERS -> { - Optional optional = userRepository.findByEmail(email); - if (optional.isEmpty()) { - // Do not leak existence; simply return - return; - } - User user = optional.get(); - user.setPasswordCode(token); - user.setPasswordTimestamp(now); - userRepository.save(user); - sendMail(email, link); + case USERS -> { + Optional optional = userRepository.findByEmail(email); + if (optional.isEmpty()) { + // Do not leak existence; simply return + return; } - case APP_USER -> { - AppUser appUser = appUserRepository.findByEmail(email); - if (appUser == null) { - return; - } - appUser.setPasswordCode(token); - appUser.setPasswordTimestamp(now); - appUserRepository.save(appUser); - sendMail(email, link); + User user = optional.get(); + user.setPasswordCode(token); + user.setPasswordTimestamp(now); + userRepository.save(user); + sendMail(email, link); + } + case APP_USER -> { + AppUser appUser = appUserRepository.findByEmail(email); + if (appUser == null) { + return; } + appUser.setPasswordCode(token); + appUser.setPasswordTimestamp(now); + appUserRepository.save(appUser); + sendMail(email, link); + } } } private void sendMail(String to, String link) { String subject = "Passwort zurücksetzen"; - String body = "Hallo,\n\n" + - "Sie haben eine Anfrage zum Zurücksetzen Ihres Passworts gestellt. " + - "Dieser Link ist 15 Minuten gültig:\n" + link + "\n\n" + - "Wenn Sie diese Anfrage nicht gestellt haben, können Sie diese E-Mail ignorieren."; + String body = "Hallo,\n\n" + "Sie haben eine Anfrage zum Zurücksetzen Ihres Passworts gestellt. " + + "Dieser Link ist 15 Minuten gültig:\n" + link + "\n\n" + + "Wenn Sie diese Anfrage nicht gestellt haben, können Sie diese E-Mail ignorieren."; try { - mailUtil.sendMail(to, subject, body); + emailService.sendSimpleEmail(to, subject, body); } catch (Exception e) { - // In this minimal implementation we swallow to avoid leaking details to attackers + // In this minimal implementation we swallow to avoid leaking details to + // attackers } } public boolean isTokenValid(String token, UserType userType) { LocalDateTime ts = switch (userType) { - case USERS -> userRepository.findByPasswordCode(token).map(User::getPasswordTimestamp).orElse(null); - case APP_USER -> Optional.ofNullable(appUserRepository.findByPasswordCode(token)).map(AppUser::getPasswordTimestamp).orElse(null); + case USERS -> userRepository.findByPasswordCode(token).map(User::getPasswordTimestamp).orElse(null); + case APP_USER -> Optional.ofNullable(appUserRepository.findByPasswordCode(token)) + .map(AppUser::getPasswordTimestamp).orElse(null); }; - if (ts == null) return false; + if (ts == null) + return false; return Duration.between(ts, LocalDateTime.now()).compareTo(TOKEN_VALIDITY) <= 0; } public boolean resetPassword(String token, UserType userType, String newPassword) { - if (!isTokenValid(token, userType)) return false; + if (!isTokenValid(token, userType)) + return false; switch (userType) { - case USERS -> { - Optional optional = userRepository.findByPasswordCode(token); - if (optional.isEmpty()) return false; - User user = optional.get(); - user.setPassword(passwordEncoder.encode(newPassword)); - user.setPasswordCode(null); - user.setPasswordTimestamp(null); - userRepository.save(user); - return true; - } - case APP_USER -> { - AppUser appUser = appUserRepository.findByPasswordCode(token); - if (appUser == null) return false; - appUser.setPassword(passwordEncoder.encode(newPassword)); - appUser.setPasswordCode(null); - appUser.setPasswordTimestamp(null); - appUserRepository.save(appUser); - return true; - } + case USERS -> { + Optional optional = userRepository.findByPasswordCode(token); + if (optional.isEmpty()) + return false; + User user = optional.get(); + user.setPassword(passwordEncoder.encode(newPassword)); + user.setPasswordCode(null); + user.setPasswordTimestamp(null); + userRepository.save(user); + return true; + } + case APP_USER -> { + AppUser appUser = appUserRepository.findByPasswordCode(token); + if (appUser == null) + return false; + appUser.setPassword(passwordEncoder.encode(newPassword)); + appUser.setPasswordCode(null); + appUser.setPasswordTimestamp(null); + appUserRepository.save(appUser); + return true; + } } return false; } diff --git a/src/main/java/de/assecutor/votianlt/pages/service/RegisterService.java b/src/main/java/de/assecutor/votianlt/pages/service/RegisterService.java index 290ebf3..b8bee21 100644 --- a/src/main/java/de/assecutor/votianlt/pages/service/RegisterService.java +++ b/src/main/java/de/assecutor/votianlt/pages/service/RegisterService.java @@ -18,4 +18,5 @@ public class RegisterService { public void registerUser(User user) { registerRepository.save(user); - }} + } +} diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddAppDeviceView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddAppDeviceView.java index 45b79e6..6fa667e 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddAppDeviceView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddAppDeviceView.java @@ -19,37 +19,37 @@ import org.springframework.beans.factory.annotation.Autowired; @PageTitle("Neues Endgerät anlegen") @Route(value = "add-app-device", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) -@RolesAllowed({"USER","ADMIN"}) +@RolesAllowed({ "USER", "ADMIN" }) public class AddAppDeviceView extends VerticalLayout { private final AppDeviceService appDeviceService; private final Binder binder; - + // Formularfelder private final TextField nameField; @Autowired public AddAppDeviceView(AppDeviceService appDeviceService) { this.appDeviceService = appDeviceService; - + // Binder initialisieren binder = new Binder<>(AppDevice.class); - + // Formularfelder erstellen nameField = new TextField("Gerätename"); nameField.setRequired(true); nameField.setPlaceholder("z.B. iPhone 15, Samsung Galaxy S24"); nameField.setWidth("100%"); - + // Layout konfigurieren setSizeFull(); setPadding(true); setSpacing(true); - + // Content zentrieren setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); - + // Hauptcontainer erstellen VerticalLayout contentContainer = new VerticalLayout(); contentContainer.setWidth("600px"); @@ -59,78 +59,79 @@ public class AddAppDeviceView extends VerticalLayout { contentContainer.getStyle().set("box-shadow", "0 2px 8px rgba(0,0,0,0.1)"); contentContainer.setPadding(true); contentContainer.setSpacing(true); - + // Titel H2 title = new H2("Neues Endgerät anlegen"); title.getStyle().set("margin", "0"); title.getStyle().set("text-align", "center"); contentContainer.add(title); - + // Formular FormLayout formLayout = new FormLayout(); formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1)); formLayout.add(nameField); contentContainer.add(formLayout); - + // Buttons HorizontalLayout buttonLayout = new HorizontalLayout(); buttonLayout.setWidthFull(); buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); buttonLayout.setSpacing(true); - + Button backButton = new Button("Zurück"); backButton.addClickListener(e -> navigateBack()); - + Button saveButton = new Button("Speichern"); saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); saveButton.addClickListener(e -> createAppDevice()); - + buttonLayout.add(backButton, saveButton); contentContainer.add(buttonLayout); - + add(contentContainer); - + // Testdaten einfügen populateTestData(); - + // Binder konfigurieren setupBinder(); } - + private void setupBinder() { - binder.forField(nameField) - .asRequired("Gerätename ist erforderlich") - .bind(AppDevice::getName, AppDevice::setName); + binder.forField(nameField).asRequired("Gerätename ist erforderlich").bind(AppDevice::getName, + AppDevice::setName); } - + private void populateTestData() { nameField.setValue("iPhone 15 Pro"); } - + private void createAppDevice() { if (binder.validate().isOk()) { try { AppDevice appDevice = new AppDevice(); binder.writeBean(appDevice); - + // Explizit als nicht zugeordnet speichern appDevice.setAppUserId(null); - + AppDevice savedDevice = appDeviceService.createAppDevice(appDevice); - - Notification.show("Endgerät erfolgreich angelegt: " + savedDevice.getName(), 3000, Notification.Position.MIDDLE); - + + Notification.show("Endgerät erfolgreich angelegt: " + savedDevice.getName(), 3000, + Notification.Position.MIDDLE); + // Zurück zur Übersicht navigateBack(); - + } catch (Exception e) { - Notification.show("Fehler beim Anlegen des Endgeräts: " + e.getMessage(), 5000, Notification.Position.MIDDLE); + Notification.show("Fehler beim Anlegen des Endgeräts: " + e.getMessage(), 5000, + Notification.Position.MIDDLE); } } else { Notification.show("Bitte füllen Sie alle erforderlichen Felder aus", 3000, Notification.Position.MIDDLE); } } - + private void navigateBack() { getUI().ifPresent(ui -> ui.navigate("app-devices")); } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddAppUserView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddAppUserView.java index c83d6b4..e925f4d 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddAppUserView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddAppUserView.java @@ -26,7 +26,7 @@ import org.springframework.beans.factory.annotation.Autowired; @PageTitle("Neuen App-Nutzer anlegen") @Route(value = "add-app-user", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) -@RolesAllowed({"USER","ADMIN"}) +@RolesAllowed({ "USER", "ADMIN" }) public class AddAppUserView extends VerticalLayout { private final AppUserService appUserService; @@ -51,7 +51,7 @@ public class AddAppUserView extends VerticalLayout { setSizeFull(); setPadding(true); setSpacing(true); - + // Center content vertically setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); @@ -70,37 +70,35 @@ public class AddAppUserView extends VerticalLayout { HorizontalLayout header = new HorizontalLayout(); header.setAlignItems(FlexComponent.Alignment.CENTER); header.setSpacing(true); - + H2 title = new H2("Neuen App-Nutzer anlegen"); title.getStyle().set("margin", "0"); - + Button backButton = new Button("Zurück", new Icon(VaadinIcon.ARROW_LEFT)); backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); backButton.addClickListener(e -> navigateBack()); - + header.add(title, backButton); header.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN); contentContainer.add(header); // Form layout FormLayout formLayout = new FormLayout(); - formLayout.setResponsiveSteps( - new FormLayout.ResponsiveStep("0", 1) - ); + formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1)); // Configure fields designationField.setPlaceholder("(HH H 000)"); designationField.setWidthFull(); - + firstnameField.setWidthFull(); lastnameField.setWidthFull(); - + // Create horizontal layout for firstname and lastname HorizontalLayout nameLayout = new HorizontalLayout(); nameLayout.setWidthFull(); nameLayout.setSpacing(true); nameLayout.add(firstnameField, lastnameField); - + phoneField.setWidthFull(); appCodeField.setWidthFull(); emailField.setWidthFull(); @@ -108,7 +106,7 @@ public class AddAppUserView extends VerticalLayout { passwordField.setRequired(true); confirmPasswordField.setWidthFull(); confirmPasswordField.setRequired(true); - + // Configure device dropdown // Geräteauswahl vorbereiten deviceComboBox.setWidthFull(); @@ -140,7 +138,7 @@ public class AddAppUserView extends VerticalLayout { // Setup binder setupBinder(); - + // Fill with test data populateTestData(); } @@ -153,25 +151,22 @@ public class AddAppUserView extends VerticalLayout { binder.forField(phoneField).bind(AppUser::getTelefon, AppUser::setTelefon); binder.forField(appCodeField).bind(AppUser::getAppCode, AppUser::setAppCode); binder.forField(emailField).bind(AppUser::getEmail, AppUser::setEmail); - binder.forField(passwordField) - .asRequired("Passwort ist erforderlich") - .bind(AppUser::getPassword, AppUser::setPassword); - + binder.forField(passwordField).asRequired("Passwort ist erforderlich").bind(AppUser::getPassword, + AppUser::setPassword); + // Confirm password field validation - binder.forField(confirmPasswordField) - .asRequired("Passwort wiederholen ist erforderlich") - .withValidator(confirmPassword -> confirmPassword.equals(passwordField.getValue()), - "Passwörter stimmen nicht überein") - .bind( - appUser -> "", // Dummy getter - this field is not stored - (appUser, value) -> {} // Dummy setter - this field is not stored - ); - binder.forField(deviceComboBox) - .asRequired("Bitte ein Gerät auswählen") - .bind( - appUser -> null, // Initialwert, wird beim Erstellen gesetzt - (appUser, appDevice) -> appUser.setAppDeviceId(appDevice != null ? appDevice.getId() : null) - ); + binder.forField(confirmPasswordField).asRequired("Passwort wiederholen ist erforderlich") + .withValidator(confirmPassword -> confirmPassword.equals(passwordField.getValue()), + "Passwörter stimmen nicht überein") + .bind(appUser -> "", // Dummy getter - this field is not stored + (appUser, value) -> { + } // Dummy setter - this field is not stored + ); + binder.forField(deviceComboBox).asRequired("Bitte ein Gerät auswählen").bind(appUser -> null, // Initialwert, + // wird beim + // Erstellen + // gesetzt + (appUser, appDevice) -> appUser.setAppDeviceId(appDevice != null ? appDevice.getId() : null)); } private void createAppUser() { @@ -191,49 +186,42 @@ public class AddAppUserView extends VerticalLayout { } // Show success message - Notification.show( - "App-Nutzer erfolgreich angelegt", - 3000, - Notification.Position.MIDDLE - ); + Notification.show("App-Nutzer erfolgreich angelegt", 3000, Notification.Position.MIDDLE); // Navigate back to app user list navigateBack(); } catch (ValidationException e) { - Notification.show( - "Bitte überprüfen Sie die Eingaben", - 3000, - Notification.Position.MIDDLE - ); + Notification.show("Bitte überprüfen Sie die Eingaben", 3000, Notification.Position.MIDDLE); } } private void populateTestData() { // Fill designation field designationField.setValue("HH H 001"); - + // Fill name fields firstnameField.setValue("Max"); lastnameField.setValue("Mustermann"); - + // Fill phone field phoneField.setValue("+49 123 456789"); - + // Fill app code field appCodeField.setValue("APP001"); - + // Fill email field emailField.setValue("max.mustermann@example.com"); - + // Fill password field passwordField.setValue("testpassword123"); - + // Fill confirm password field confirmPasswordField.setValue("testpassword123"); - + // Set device to iPhone - // deviceComboBox.setValue("iPhone"); // This line is removed as deviceComboBox is now a ComboBox + // deviceComboBox.setValue("iPhone"); // This line is removed as deviceComboBox + // is now a ComboBox } private void navigateBack() { diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddCompanyView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddCompanyView.java index 01f750b..4189a22 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddCompanyView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddCompanyView.java @@ -20,7 +20,8 @@ import java.time.Clock; @Route(value = "add_company", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) @PageTitle("Neuen Firma anlegen") -//@Menu(order = 0, icon = "vaadin:clipboard-check", title = "Neue Firma anlegen") +// @Menu(order = 0, icon = "vaadin:clipboard-check", title = "Neue Firma +// anlegen") @RolesAllowed("USER") public class AddCompanyView extends Main { private final AddCompanyService addCompanyService; @@ -45,8 +46,7 @@ public class AddCompanyView extends Main { companyName = new TextField("Firmenname"); companyName.setRequiredIndicatorVisible(true); - binder.forField(companyName) - .asRequired("Firmenname ist ein Pflichtfeld") // Pflichtfeldmeldung + binder.forField(companyName).asRequired("Firmenname ist ein Pflichtfeld") // Pflichtfeldmeldung .bind(Company::getName, Company::setName); firstName = new TextField("Vorname"); @@ -66,7 +66,10 @@ public class AddCompanyView extends Main { // Erstelle ein Div als Container (oder direkt ein Layout) VerticalLayout formLayout = new VerticalLayout(); - formLayout.add(companyName, /*firstName, lastName, telephone, fax, mail, street, houseNumber, addressAddition, zip, city,*/ submitButton); + formLayout.add(companyName, /* + * firstName, lastName, telephone, fax, mail, street, houseNumber, + * addressAddition, zip, city, + */ submitButton); // Zentriere die Inhalte vertikal und horizontal formLayout.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddCustomerView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddCustomerView.java index 780a0b6..0eb53db 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddCustomerView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddCustomerView.java @@ -22,7 +22,8 @@ import java.time.Clock; @Route(value = "add-customer", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) @PageTitle("Neuen Kunden anlegen") -//@Menu(order = 0, icon = "vaadin:clipboard-check", title = "Neuen Kunden anlegen") +// @Menu(order = 0, icon = "vaadin:clipboard-check", title = "Neuen Kunden +// anlegen") @RolesAllowed("USER") public class AddCustomerView extends Main { private final AddCustomerService addCustomerService; @@ -151,54 +152,38 @@ public class AddCustomerView extends Main { } private void configureBinder() { - binder.forField(companyName) - .asRequired("Firma ist ein Pflichtfeld") - .bind(Customer::getCompanyName, Customer::setCompanyName); + binder.forField(companyName).asRequired("Firma ist ein Pflichtfeld").bind(Customer::getCompanyName, + Customer::setCompanyName); - binder.forField(title) - .bind(Customer::getTitle, Customer::setTitle); + binder.forField(title).bind(Customer::getTitle, Customer::setTitle); - binder.forField(firstName) - .asRequired("Vorname ist ein Pflichtfeld") - .bind(Customer::getFirstname, Customer::setFirstname); + binder.forField(firstName).asRequired("Vorname ist ein Pflichtfeld").bind(Customer::getFirstname, + Customer::setFirstname); - binder.forField(lastName) - .asRequired("Nachname ist ein Pflichtfeld") - .bind(Customer::getLastName, Customer::setLastName); + binder.forField(lastName).asRequired("Nachname ist ein Pflichtfeld").bind(Customer::getLastName, + Customer::setLastName); - binder.forField(telephone) - .asRequired("Telefonnummer ist ein Pflichtfeld") - .bind(Customer::getTelephone, Customer::setTelephone); + binder.forField(telephone).asRequired("Telefonnummer ist ein Pflichtfeld").bind(Customer::getTelephone, + Customer::setTelephone); - binder.forField(fax) - .bind(Customer::getFax, Customer::setFax); + binder.forField(fax).bind(Customer::getFax, Customer::setFax); - binder.forField(mail) - .asRequired("E-Mail-Adresse ist ein Pflichtfeld") + binder.forField(mail).asRequired("E-Mail-Adresse ist ein Pflichtfeld") .withValidator(email -> email.contains("@"), "Bitte geben Sie eine gültige E-Mail-Adresse ein") .bind(Customer::getMail, Customer::setMail); - binder.forField(street) - .asRequired("Straße ist ein Pflichtfeld") - .bind(Customer::getStreet, Customer::setStreet); + binder.forField(street).asRequired("Straße ist ein Pflichtfeld").bind(Customer::getStreet, Customer::setStreet); - binder.forField(houseNumber) - .asRequired("Hausnummer ist ein Pflichtfeld") - .bind(Customer::getHouseNumber, Customer::setHouseNumber); + binder.forField(houseNumber).asRequired("Hausnummer ist ein Pflichtfeld").bind(Customer::getHouseNumber, + Customer::setHouseNumber); - binder.forField(addressAddition) - .bind(Customer::getAddressAddition, Customer::setAddressAddition); + binder.forField(addressAddition).bind(Customer::getAddressAddition, Customer::setAddressAddition); - binder.forField(zip) - .asRequired("Postleitzahl ist ein Pflichtfeld") - .bind(Customer::getZip, Customer::setZip); + binder.forField(zip).asRequired("Postleitzahl ist ein Pflichtfeld").bind(Customer::getZip, Customer::setZip); - binder.forField(city) - .asRequired("Ort ist ein Pflichtfeld") - .bind(Customer::getCity, Customer::setCity); + binder.forField(city).asRequired("Ort ist ein Pflichtfeld").bind(Customer::getCity, Customer::setCity); } - private void setTestData() { companyName.setValue("Mustermann Transport GmbH"); title.setValue("Herr"); @@ -213,6 +198,7 @@ public class AddCustomerView extends Main { zip.setValue("20095"); city.setValue("Hamburg"); } + private void submit() { try { Customer customer = new Customer(); @@ -221,20 +207,17 @@ public class AddCustomerView extends Main { addCustomerService.addCustomer(customer); // Erfolg anzeigen und zur Kundenliste navigieren - com.vaadin.flow.component.notification.Notification.show( - "Kunde erfolgreich angelegt", 3000, - com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER); + com.vaadin.flow.component.notification.Notification.show("Kunde erfolgreich angelegt", 3000, + com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER); getUI().ifPresent(ui -> ui.navigate("customers")); } catch (ValidationException e) { - com.vaadin.flow.component.notification.Notification.show( - "Bitte überprüfen Sie Ihre Eingaben", 3000, - com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER); + com.vaadin.flow.component.notification.Notification.show("Bitte überprüfen Sie Ihre Eingaben", 3000, + com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER); } catch (Exception e) { - com.vaadin.flow.component.notification.Notification.show( - "Fehler beim Speichern: " + e.getMessage(), 5000, - com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER); + com.vaadin.flow.component.notification.Notification.show("Fehler beim Speichern: " + e.getMessage(), 5000, + com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER); } } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java index 3438913..2b8a6fa 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java @@ -131,11 +131,12 @@ public class AddJobView extends Main { // Mapping für die Anzeige-Labels der Kunden zur Entität private final Map customerLabelToEntity = new LinkedHashMap<>(); - + // Available app users for the current user private List availableAppUsers; - public AddJobView(AddJobService addJobService, AddCustomerService addCustomerService, CustomerService customerService, AppUserService appUserService) { + public AddJobView(AddJobService addJobService, AddCustomerService addCustomerService, + CustomerService customerService, AppUserService appUserService) { this.addJobService = addJobService; this.addCustomerService = addCustomerService; this.customerService = customerService; @@ -157,9 +158,11 @@ public class AddJobView extends Main { customerLabelToEntity.clear(); for (Customer c : ownerCustomers) { String label = (c.getCompanyName() != null && !c.getCompanyName().isBlank()) - ? c.getCompanyName() + " | " + - ((c.getFirstname() != null ? c.getFirstname() : "") + " " + (c.getLastName() != null ? c.getLastName() : "")).trim() - : ((c.getFirstname() != null ? c.getFirstname() : "") + " " + (c.getLastName() != null ? c.getLastName() : "")).trim(); + ? c.getCompanyName() + " | " + + ((c.getFirstname() != null ? c.getFirstname() : "") + " " + + (c.getLastName() != null ? c.getLastName() : "")).trim() + : ((c.getFirstname() != null ? c.getFirstname() : "") + " " + + (c.getLastName() != null ? c.getLastName() : "")).trim(); if (label.isBlank()) { label = "Unbenannter Kunde"; } @@ -182,30 +185,68 @@ public class AddJobView extends Main { return; } Customer c = customerLabelToEntity.get(selected); - if (c == null) return; + if (c == null) + return; // Pickup-Checkbox deaktivieren, da Kunde bereits existiert savePickupAddress.setValue(false); // Firma - if (c.getCompanyName() != null) { pickupCompany.setValue(c.getCompanyName()); } else { pickupCompany.clear(); } + if (c.getCompanyName() != null) { + pickupCompany.setValue(c.getCompanyName()); + } else { + pickupCompany.clear(); + } // Anrede (nur setzen, wenn vorhanden und zulässig) - if (c.getTitle() != null && ("Herr".equalsIgnoreCase(c.getTitle()) || "Frau".equalsIgnoreCase(c.getTitle()) || "Divers".equalsIgnoreCase(c.getTitle()))) { + if (c.getTitle() != null && ("Herr".equalsIgnoreCase(c.getTitle()) || "Frau".equalsIgnoreCase(c.getTitle()) + || "Divers".equalsIgnoreCase(c.getTitle()))) { pickupSalutation.setValue(c.getTitle()); } else { pickupSalutation.clear(); } // Namen - if (c.getFirstname() != null) { pickupFirstName.setValue(c.getFirstname()); } else { pickupFirstName.clear(); } - if (c.getLastName() != null) { pickupLastName.setValue(c.getLastName()); } else { pickupLastName.clear(); } + if (c.getFirstname() != null) { + pickupFirstName.setValue(c.getFirstname()); + } else { + pickupFirstName.clear(); + } + if (c.getLastName() != null) { + pickupLastName.setValue(c.getLastName()); + } else { + pickupLastName.clear(); + } // Telefon - if (c.getTelephone() != null) { pickupPhone.setValue(c.getTelephone()); } else { pickupPhone.clear(); } + if (c.getTelephone() != null) { + pickupPhone.setValue(c.getTelephone()); + } else { + pickupPhone.clear(); + } // Adresse - if (c.getStreet() != null) { pickupStreet.setValue(c.getStreet()); } else { pickupStreet.clear(); } - if (c.getHouseNumber() != null) { pickupHouseNumber.setValue(c.getHouseNumber()); } else { pickupHouseNumber.clear(); } - if (c.getAddressAddition() != null) { pickupAddressAddition.setValue(c.getAddressAddition()); } else { pickupAddressAddition.clear(); } - if (c.getZip() != null) { pickupZip.setValue(c.getZip()); } else { pickupZip.clear(); } - if (c.getCity() != null) { pickupCity.setValue(c.getCity()); } else { pickupCity.clear(); } + if (c.getStreet() != null) { + pickupStreet.setValue(c.getStreet()); + } else { + pickupStreet.clear(); + } + if (c.getHouseNumber() != null) { + pickupHouseNumber.setValue(c.getHouseNumber()); + } else { + pickupHouseNumber.clear(); + } + if (c.getAddressAddition() != null) { + pickupAddressAddition.setValue(c.getAddressAddition()); + } else { + pickupAddressAddition.clear(); + } + if (c.getZip() != null) { + pickupZip.setValue(c.getZip()); + } else { + pickupZip.clear(); + } + if (c.getCity() != null) { + pickupCity.setValue(c.getCity()); + } else { + pickupCity.clear(); + } }); preloadAddressButton = new Button("Vorbelegte Adressfelder leeren"); @@ -282,11 +323,12 @@ public class AddJobView extends Main { digitalProcessing = new Checkbox("Digitale Abwicklung per App"); digitalProcessing.setValue(true); appUser = new ComboBox<>("App-Nutzer"); - + // Load app users for current user and set up the ComboBox availableAppUsers = appUserService.findByCurrentUser(); appUser.setItems(availableAppUsers); - appUser.setItemLabelGenerator(user -> user.getVorname() + " " + user.getNachname() + " (" + user.getEmail() + ")"); + appUser.setItemLabelGenerator( + user -> user.getVorname() + " " + user.getNachname() + " (" + user.getEmail() + ")"); appUser.setPlaceholder("App-Nutzer auswählen..."); // Price field @@ -299,7 +341,8 @@ public class AddJobView extends Main { String v = e.getValue(); if (v != null && v.contains(".")) { String replaced = v.replace('.', ','); - if (!replaced.equals(v)) price.setValue(replaced); + if (!replaced.equals(v)) + price.setValue(replaced); } }); // Date picker fields for appointments @@ -317,9 +360,8 @@ public class AddJobView extends Main { private void setupLayout() { setSizeFull(); - addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, - LumoUtility.FlexDirection.COLUMN, LumoUtility.Padding.MEDIUM, - LumoUtility.Gap.SMALL); + addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN, + LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL); add(new ViewToolbar("Neuen Auftrag anlegen")); @@ -507,8 +549,6 @@ public class AddJobView extends Main { H3 title = new H3("Abholadresse"); title.getStyle().set("margin", "0"); - - HorizontalLayout titleLayout = new HorizontalLayout(); titleLayout.setWidthFull(); titleLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.START); @@ -569,8 +609,6 @@ public class AddJobView extends Main { H3 title = new H3("Lieferadresse"); title.getStyle().set("margin", "0"); - - HorizontalLayout titleLayout = new HorizontalLayout(); titleLayout.setWidthFull(); titleLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.START); @@ -619,76 +657,89 @@ public class AddJobView extends Main { private void setupCompanyAutocomplete(ComboBox companyField, boolean isPickup) { // Get all customers for the current owner List allCustomers = customerService.findAllForCurrentOwner(); - + // Extract unique company names (filter out null/empty values) - List companyNames = allCustomers.stream() - .map(Customer::getCompanyName) - .filter(name -> name != null && !name.trim().isEmpty()) - .distinct() - .sorted() - .toList(); - + List companyNames = allCustomers.stream().map(Customer::getCompanyName) + .filter(name -> name != null && !name.trim().isEmpty()).distinct().sorted().toList(); + // Set items for autocomplete companyField.setItems(companyNames); - + // Add selection listener to auto-fill address fields when company is selected companyField.addValueChangeListener(event -> { String selectedCompany = event.getValue(); if (selectedCompany == null || selectedCompany.trim().isEmpty()) { return; } - + // Find the first customer with this company name Optional matchingCustomer = allCustomers.stream() - .filter(c -> selectedCompany.equals(c.getCompanyName())) - .findFirst(); - + .filter(c -> selectedCompany.equals(c.getCompanyName())).findFirst(); + if (matchingCustomer.isPresent()) { Customer customer = matchingCustomer.get(); - + if (isPickup) { // Fill pickup address fields - if (customer.getTitle() != null && ("Herr".equalsIgnoreCase(customer.getTitle()) || - "Frau".equalsIgnoreCase(customer.getTitle()) || "Divers".equalsIgnoreCase(customer.getTitle()))) { + if (customer.getTitle() != null && ("Herr".equalsIgnoreCase(customer.getTitle()) + || "Frau".equalsIgnoreCase(customer.getTitle()) + || "Divers".equalsIgnoreCase(customer.getTitle()))) { pickupSalutation.setValue(customer.getTitle()); } - if (customer.getFirstname() != null) pickupFirstName.setValue(customer.getFirstname()); - if (customer.getLastName() != null) pickupLastName.setValue(customer.getLastName()); - if (customer.getTelephone() != null) pickupPhone.setValue(customer.getTelephone()); - if (customer.getStreet() != null) pickupStreet.setValue(customer.getStreet()); - if (customer.getHouseNumber() != null) pickupHouseNumber.setValue(customer.getHouseNumber()); - if (customer.getAddressAddition() != null) pickupAddressAddition.setValue(customer.getAddressAddition()); - if (customer.getZip() != null) pickupZip.setValue(customer.getZip()); - if (customer.getCity() != null) pickupCity.setValue(customer.getCity()); - + if (customer.getFirstname() != null) + pickupFirstName.setValue(customer.getFirstname()); + if (customer.getLastName() != null) + pickupLastName.setValue(customer.getLastName()); + if (customer.getTelephone() != null) + pickupPhone.setValue(customer.getTelephone()); + if (customer.getStreet() != null) + pickupStreet.setValue(customer.getStreet()); + if (customer.getHouseNumber() != null) + pickupHouseNumber.setValue(customer.getHouseNumber()); + if (customer.getAddressAddition() != null) + pickupAddressAddition.setValue(customer.getAddressAddition()); + if (customer.getZip() != null) + pickupZip.setValue(customer.getZip()); + if (customer.getCity() != null) + pickupCity.setValue(customer.getCity()); + // Deactivate save checkbox since customer already exists savePickupAddress.setValue(false); } else { // Fill delivery address fields - if (customer.getTitle() != null && ("Herr".equalsIgnoreCase(customer.getTitle()) || - "Frau".equalsIgnoreCase(customer.getTitle()) || "Divers".equalsIgnoreCase(customer.getTitle()))) { + if (customer.getTitle() != null && ("Herr".equalsIgnoreCase(customer.getTitle()) + || "Frau".equalsIgnoreCase(customer.getTitle()) + || "Divers".equalsIgnoreCase(customer.getTitle()))) { deliverySalutation.setValue(customer.getTitle()); } - if (customer.getFirstname() != null) deliveryFirstName.setValue(customer.getFirstname()); - if (customer.getLastName() != null) deliveryLastName.setValue(customer.getLastName()); - if (customer.getTelephone() != null) deliveryPhone.setValue(customer.getTelephone()); - if (customer.getStreet() != null) deliveryStreet.setValue(customer.getStreet()); - if (customer.getHouseNumber() != null) deliveryHouseNumber.setValue(customer.getHouseNumber()); - if (customer.getAddressAddition() != null) deliveryAddressAddition.setValue(customer.getAddressAddition()); - if (customer.getZip() != null) deliveryZip.setValue(customer.getZip()); - if (customer.getCity() != null) deliveryCity.setValue(customer.getCity()); - + if (customer.getFirstname() != null) + deliveryFirstName.setValue(customer.getFirstname()); + if (customer.getLastName() != null) + deliveryLastName.setValue(customer.getLastName()); + if (customer.getTelephone() != null) + deliveryPhone.setValue(customer.getTelephone()); + if (customer.getStreet() != null) + deliveryStreet.setValue(customer.getStreet()); + if (customer.getHouseNumber() != null) + deliveryHouseNumber.setValue(customer.getHouseNumber()); + if (customer.getAddressAddition() != null) + deliveryAddressAddition.setValue(customer.getAddressAddition()); + if (customer.getZip() != null) + deliveryZip.setValue(customer.getZip()); + if (customer.getCity() != null) + deliveryCity.setValue(customer.getCity()); + // Deactivate save checkbox since customer already exists saveDeliveryAddress.setValue(false); } } }); - + // Handle custom values (when user types something not in the list) companyField.addCustomValueSetListener(event -> { String customValue = event.getDetail(); companyField.setValue(customValue); - + // Reactivate save checkbox for custom values if (isPickup) { savePickupAddress.setValue(true); @@ -700,86 +751,55 @@ public class AddJobView extends Main { private void setupValidation() { // Bind pickup address fields with validation - binder.forField(pickupFirstName) - .asRequired("") - .bind(Job::getPickupFirstName, Job::setPickupFirstName); + binder.forField(pickupFirstName).asRequired("").bind(Job::getPickupFirstName, Job::setPickupFirstName); - binder.forField(pickupLastName) - .asRequired("") - .bind(Job::getPickupLastName, Job::setPickupLastName); + binder.forField(pickupLastName).asRequired("").bind(Job::getPickupLastName, Job::setPickupLastName); - binder.forField(pickupStreet) - .asRequired("") - .bind(Job::getPickupStreet, Job::setPickupStreet); + binder.forField(pickupStreet).asRequired("").bind(Job::getPickupStreet, Job::setPickupStreet); - binder.forField(pickupHouseNumber) - .asRequired("") - .bind(Job::getPickupHouseNumber, Job::setPickupHouseNumber); + binder.forField(pickupHouseNumber).asRequired("").bind(Job::getPickupHouseNumber, Job::setPickupHouseNumber); - binder.forField(pickupZip) - .asRequired("") - .bind(Job::getPickupZip, Job::setPickupZip); + binder.forField(pickupZip).asRequired("").bind(Job::getPickupZip, Job::setPickupZip); - binder.forField(pickupCity) - .asRequired("") - .bind(Job::getPickupCity, Job::setPickupCity); + binder.forField(pickupCity).asRequired("").bind(Job::getPickupCity, Job::setPickupCity); // Bind delivery address fields with validation - binder.forField(deliveryFirstName) - .asRequired("") - .bind(Job::getDeliveryFirstName, Job::setDeliveryFirstName); + binder.forField(deliveryFirstName).asRequired("").bind(Job::getDeliveryFirstName, Job::setDeliveryFirstName); - binder.forField(deliveryLastName) - .asRequired("") - .bind(Job::getDeliveryLastName, Job::setDeliveryLastName); + binder.forField(deliveryLastName).asRequired("").bind(Job::getDeliveryLastName, Job::setDeliveryLastName); - binder.forField(deliveryStreet) - .asRequired("") - .bind(Job::getDeliveryStreet, Job::setDeliveryStreet); + binder.forField(deliveryStreet).asRequired("").bind(Job::getDeliveryStreet, Job::setDeliveryStreet); - binder.forField(deliveryHouseNumber) - .asRequired("") - .bind(Job::getDeliveryHouseNumber, Job::setDeliveryHouseNumber); + binder.forField(deliveryHouseNumber).asRequired("").bind(Job::getDeliveryHouseNumber, + Job::setDeliveryHouseNumber); - binder.forField(deliveryZip) - .asRequired("") - .bind(Job::getDeliveryZip, Job::setDeliveryZip); + binder.forField(deliveryZip).asRequired("").bind(Job::getDeliveryZip, Job::setDeliveryZip); - binder.forField(deliveryCity) - .asRequired("") - .bind(Job::getDeliveryCity, Job::setDeliveryCity); + binder.forField(deliveryCity).asRequired("").bind(Job::getDeliveryCity, Job::setDeliveryCity); - // Bind price field: Komma-Zahlen in Punkt-Zahlen umsetzen, dann nach BigDecimal konvertieren - binder.forField(price) - .withNullRepresentation("") - .asRequired("Preis erforderlich") - .withConverter( - (String s) -> { - if (s == null || s.trim().isEmpty()) return null; - String normalized = s.replace(" ", "").replace(".", "").replace(',', '.'); - try { return new java.math.BigDecimal(normalized); } - catch (NumberFormatException ex) { throw new NumberFormatException("Ungültiger Betrag"); } - }, - (java.math.BigDecimal bd) -> bd == null ? "" : bd.toString(), - "Ungültiger Betrag" - ) + // Bind price field: Komma-Zahlen in Punkt-Zahlen umsetzen, dann nach BigDecimal + // konvertieren + binder.forField(price).withNullRepresentation("").asRequired("Preis erforderlich").withConverter((String s) -> { + if (s == null || s.trim().isEmpty()) + return null; + String normalized = s.replace(" ", "").replace(".", "").replace(',', '.'); + try { + return new java.math.BigDecimal(normalized); + } catch (NumberFormatException ex) { + throw new NumberFormatException("Ungültiger Betrag"); + } + }, (java.math.BigDecimal bd) -> bd == null ? "" : bd.toString(), "Ungültiger Betrag") .withValidator(value -> value != null && value.compareTo(java.math.BigDecimal.ZERO) > 0, - "Der Preis muss größer als 0 sein") + "Der Preis muss größer als 0 sein") .bind(Job::getPrice, Job::setPrice); // Bind date picker fields with validation - binder.forField(pickupDate) - .asRequired("") - .bind(Job::getPickupDate, Job::setPickupDate); + binder.forField(pickupDate).asRequired("").bind(Job::getPickupDate, Job::setPickupDate); - binder.forField(deliveryDate) - .asRequired("") - .bind(Job::getDeliveryDate, Job::setDeliveryDate); + binder.forField(deliveryDate).asRequired("").bind(Job::getDeliveryDate, Job::setDeliveryDate); // Bind customerSelection field with validation - binder.forField(customerSelection) - .asRequired("") - .bind(Job::getCustomerSelection, Job::setCustomerSelection); + binder.forField(customerSelection).asRequired("").bind(Job::getCustomerSelection, Job::setCustomerSelection); // Bind optional fields without validation binder.bind(pickupCompany, Job::getPickupCompany, Job::setPickupCompany); @@ -793,30 +813,24 @@ public class AddJobView extends Main { binder.bind(deliveryAddressAddition, Job::getDeliveryAddressAddition, Job::setDeliveryAddressAddition); binder.bind(digitalProcessing, Job::isDigitalProcessing, Job::setDigitalProcessing); - + // Bind appUser with converter: AppUser object <-> String ID - binder.forField(appUser) - .withConverter( - // Convert AppUser to String (ID) - user -> user != null ? user.getId().toHexString() : null, - // Convert String (ID) back to AppUser - id -> { - if (id == null || id.trim().isEmpty()) return null; - return availableAppUsers.stream() - .filter(user -> user.getId().toHexString().equals(id)) - .findFirst() - .orElse(null); - } - ) + binder.forField(appUser).withConverter( + // Convert AppUser to String (ID) + user -> user != null ? user.getId().toHexString() : null, + // Convert String (ID) back to AppUser + id -> { + if (id == null || id.trim().isEmpty()) + return null; + return availableAppUsers.stream().filter(user -> user.getId().toHexString().equals(id)).findFirst() + .orElse(null); + }) // Require App-Nutzer when digital processing is enabled - .withValidator( - selectedUserId -> { - boolean digital = Boolean.TRUE.equals(digitalProcessing.getValue()); - boolean hasUser = selectedUserId != null && !selectedUserId.trim().isEmpty(); - return !digital || hasUser; - }, - "Bitte App-Nutzer auswählen, wenn Digitale Abwicklung aktiv ist" - ) + .withValidator(selectedUserId -> { + boolean digital = Boolean.TRUE.equals(digitalProcessing.getValue()); + boolean hasUser = selectedUserId != null && !selectedUserId.trim().isEmpty(); + return !digital || hasUser; + }, "Bitte App-Nutzer auswählen, wenn Digitale Abwicklung aktiv ist") .bind(Job::getAppUser, Job::setAppUser); // Toggle required indicator for App-Nutzer based on digitalProcessing @@ -846,16 +860,12 @@ public class AddJobView extends Main { private void setupValidationTriggers() { // List of all required fields - TextField[] requiredTextFields = { - pickupFirstName, pickupLastName, pickupStreet, pickupHouseNumber, pickupZip, pickupCity, - deliveryFirstName, deliveryLastName, deliveryStreet, deliveryHouseNumber, deliveryZip, deliveryCity, - price - }; + TextField[] requiredTextFields = { pickupFirstName, pickupLastName, pickupStreet, pickupHouseNumber, pickupZip, + pickupCity, deliveryFirstName, deliveryLastName, deliveryStreet, deliveryHouseNumber, deliveryZip, + deliveryCity, price }; // List of required date fields - DatePicker[] requiredDateFields = { - pickupDate, deliveryDate - }; + DatePicker[] requiredDateFields = { pickupDate, deliveryDate }; // Add validation listener for customerSelection ComboBox customerSelection.addValueChangeListener(event -> { @@ -863,7 +873,8 @@ public class AddJobView extends Main { updateTabLabels(); }); - // Add value change listeners to trigger validation on every change for text fields + // Add value change listeners to trigger validation on every change for text + // fields for (TextField field : requiredTextFields) { field.addValueChangeListener(event -> { triggerValidation(); @@ -949,17 +960,18 @@ public class AddJobView extends Main { private boolean hasAddressValidationErrors() { // Check customer selection - boolean customerSelectionEmpty = customerSelection.getValue() == null || customerSelection.getValue().trim().isEmpty(); - + boolean customerSelectionEmpty = customerSelection.getValue() == null + || customerSelection.getValue().trim().isEmpty(); + // Check pickup address fields - boolean pickupErrors = isFieldEmpty(pickupFirstName) || isFieldEmpty(pickupLastName) || - isFieldEmpty(pickupStreet) || isFieldEmpty(pickupHouseNumber) || - isFieldEmpty(pickupZip) || isFieldEmpty(pickupCity); + boolean pickupErrors = isFieldEmpty(pickupFirstName) || isFieldEmpty(pickupLastName) + || isFieldEmpty(pickupStreet) || isFieldEmpty(pickupHouseNumber) || isFieldEmpty(pickupZip) + || isFieldEmpty(pickupCity); // Check delivery address fields - boolean deliveryErrors = isFieldEmpty(deliveryFirstName) || isFieldEmpty(deliveryLastName) || - isFieldEmpty(deliveryStreet) || isFieldEmpty(deliveryHouseNumber) || - isFieldEmpty(deliveryZip) || isFieldEmpty(deliveryCity); + boolean deliveryErrors = isFieldEmpty(deliveryFirstName) || isFieldEmpty(deliveryLastName) + || isFieldEmpty(deliveryStreet) || isFieldEmpty(deliveryHouseNumber) || isFieldEmpty(deliveryZip) + || isFieldEmpty(deliveryCity); return customerSelectionEmpty || pickupErrors || deliveryErrors; } @@ -974,18 +986,18 @@ public class AddJobView extends Main { if (cargoItemsState.isEmpty()) { return true; // No cargo items at all - show warning } - + // Check that ALL cargo items are complete - // A cargo item is considered complete if it has: Description, Quantity, Weight, Length, Width, Height + // A cargo item is considered complete if it has: Description, Quantity, Weight, + // Length, Width, Height boolean allCargoItemsValid = cargoItemsState.stream() - .allMatch(cargoItem -> cargoItem != null && - cargoItem.getDescription() != null && !cargoItem.getDescription().trim().isEmpty() && - cargoItem.getQuantity() != null && cargoItem.getQuantity() > 0 && - cargoItem.getWeightKg() != null && cargoItem.getWeightKg() > 0 && - cargoItem.getLengthMm() != null && cargoItem.getLengthMm() > 0 && - cargoItem.getWidthMm() != null && cargoItem.getWidthMm() > 0 && - cargoItem.getHeightMm() != null && cargoItem.getHeightMm() > 0); - + .allMatch(cargoItem -> cargoItem != null && cargoItem.getDescription() != null + && !cargoItem.getDescription().trim().isEmpty() && cargoItem.getQuantity() != null + && cargoItem.getQuantity() > 0 && cargoItem.getWeightKg() != null && cargoItem.getWeightKg() > 0 + && cargoItem.getLengthMm() != null && cargoItem.getLengthMm() > 0 + && cargoItem.getWidthMm() != null && cargoItem.getWidthMm() > 0 + && cargoItem.getHeightMm() != null && cargoItem.getHeightMm() > 0); + return !allCargoItemsValid; // Return true if ANY cargo item is incomplete (show warning) } @@ -1010,28 +1022,29 @@ public class AddJobView extends Main { // Zusätzliche Felder, die nicht über den Binder gebunden sind, manuell setzen job.setPickupDate(pickupDate.getValue()); job.setDeliveryDate(deliveryDate.getValue()); - if (remarkArea != null) job.setRemark(remarkArea.getValue()); + if (remarkArea != null) + job.setRemark(remarkArea.getValue()); // Validate all required fields using the binder if (binder.writeBeanIfValid(job)) { - // Additional validation: If digital processing is enabled, app user must be selected + // Additional validation: If digital processing is enabled, app user must be + // selected if (digitalProcessing.getValue() && appUser.getValue() == null) { Notification errorNotification = Notification.show( "Wenn digitale Abwicklung per App aktiviert ist, muss ein App-Nutzer ausgewählt werden."); errorNotification.setDuration(5000); return; } - + // Ensure at least one cargo item is provided (tasks may be empty) - // Definition: Ein Cargo-Item gilt nur als gefüllt, wenn eine Beschreibung vorhanden ist - List cargoFilled = cargoItemsState.stream() - .filter(Objects::nonNull) - .filter(ci -> ci.getDescription() != null && !ci.getDescription().isBlank()) - .toList(); + // Definition: Ein Cargo-Item gilt nur als gefüllt, wenn eine Beschreibung + // vorhanden ist + List cargoFilled = cargoItemsState.stream().filter(Objects::nonNull) + .filter(ci -> ci.getDescription() != null && !ci.getDescription().isBlank()).toList(); if (cargoFilled.isEmpty()) { - Notification errorNotification = Notification.show( - "Bitte fügen Sie mindestens eine Ladungszeile hinzu."); + Notification errorNotification = Notification + .show("Bitte fügen Sie mindestens eine Ladungszeile hinzu."); errorNotification.setDuration(5000); return; } @@ -1074,28 +1087,30 @@ public class AddJobView extends Main { addCustomerService.addCustomer(deliveryCustomer); } - // All validations passed, save the job with cargo items and tasks (tasks may be empty) + // All validations passed, save the job with cargo items and tasks (tasks may be + // empty) Job savedJob = addJobService.addJobWithCargo(job, cargoFilled, tasksState); // Erfolgsmeldung und Navigation zur Zusammenfassung - Notification successNotification = Notification.show( - "Auftrag erfolgreich erstellt! Auftragsnummer: " + savedJob.getJobNumber()); + Notification successNotification = Notification + .show("Auftrag erfolgreich erstellt! Auftragsnummer: " + savedJob.getJobNumber()); successNotification.setDuration(2000); getUI().ifPresent(ui -> ui.navigate(JobSummaryView.class, savedJob.getId().toHexString())); } else { // Validation failed, show error message - Notification errorNotification = Notification.show( - "Bitte füllen Sie alle Pflichtfelder aus (markiert mit *)"); + Notification errorNotification = Notification + .show("Bitte füllen Sie alle Pflichtfelder aus (markiert mit *)"); errorNotification.setDuration(5000); } } catch (Exception e) { // Other errors - // Reset cargo error - if (cargoError != null) cargoError.setVisible(false); - if (cargoAreaContainer != null) cargoAreaContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); - Notification errorNotification = Notification.show( - "Fehler beim Erstellen des Auftrags: " + e.getMessage()); + // Reset cargo error + if (cargoError != null) + cargoError.setVisible(false); + if (cargoAreaContainer != null) + cargoAreaContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); + Notification errorNotification = Notification.show("Fehler beim Erstellen des Auftrags: " + e.getMessage()); errorNotification.setDuration(5000); } } @@ -1117,8 +1132,8 @@ public class AddJobView extends Main { loadJobIntoForm(draft); // Benutzer informieren - Notification notification = Notification.show( - "Entwurf wiederhergestellt. Sie können Ihre Arbeit fortsetzen."); + Notification notification = Notification + .show("Entwurf wiederhergestellt. Sie können Ihre Arbeit fortsetzen."); notification.setDuration(4000); } } @@ -1163,13 +1178,15 @@ public class AddJobView extends Main { cargoList.setSpacing(true); cargoAreaContainer.add(cargoError, cargoList); - java.util.function.BiConsumer> addCargoRow = (iconName, afterCreate) -> { + java.util.function.BiConsumer> addCargoRow = (iconName, + afterCreate) -> { HorizontalLayout row = new HorizontalLayout(); row.setWidthFull(); row.setAlignItems(FlexComponent.Alignment.END); ComboBox desc = new ComboBox<>("Beschreibung"); - desc.setItems("Europalette", "Einwegpalette", "Düsseldorfer-Palette", "Gitterboxpalette", "Gitterwagen", "Paket"); + desc.setItems("Europalette", "Einwegpalette", "Düsseldorfer-Palette", "Gitterboxpalette", "Gitterwagen", + "Paket"); desc.setAllowCustomValue(true); desc.setPlaceholder("Wählen Sie eine Option oder geben Sie eigenen Text ein..."); desc.setWidth("40%"); @@ -1251,17 +1268,20 @@ public class AddJobView extends Main { updateTabLabels(); // Update tab validation when cargo height changes }); - if (afterCreate != null) afterCreate.accept(row); + if (afterCreate != null) + afterCreate.accept(row); }; - addCargoRow.accept("", r -> {}); // Show only one empty row by default + addCargoRow.accept("", r -> { + }); // Show only one empty row by default // Add button to add more cargo rows Button addCargoButton = new Button("Ladung hinzufügen", new Icon(VaadinIcon.PLUS)); addCargoButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); addCargoButton.setWidthFull(); // Make button full width of container - addCargoButton.addClickListener(e -> addCargoRow.accept("", r -> {})); - + addCargoButton.addClickListener(e -> addCargoRow.accept("", r -> { + })); + cargoAreaContainer.add(addCargoButton); wrapper.add(cargoAreaContainer); return wrapper; @@ -1315,7 +1335,6 @@ public class AddJobView extends Main { return wrapper; } - /** * Hilfsmethode zum Abrufen des aktuellen Benutzernamens */ @@ -1373,9 +1392,9 @@ public class AddJobView extends Main { Notification.show("Alle Felder wurden geleert", 2000, Notification.Position.BOTTOM_CENTER); } - /** - * Konfiguriert Focus-Listener für alle Eingabefelder um Drag-and-Drop zu steuern + * Konfiguriert Focus-Listener für alle Eingabefelder um Drag-and-Drop zu + * steuern */ private void setupInputFieldFocusListeners() { // Customer selection @@ -1468,7 +1487,6 @@ public class AddJobView extends Main { taskContainer.getStyle().set("background-color", "var(--lumo-base-color)"); taskContainer.getStyle().set("position", "relative"); - // Task type selection ComboBox taskTypeCombo = new ComboBox<>("Aufgabentyp"); taskTypeCombo.setItems(TaskType.values()); @@ -1508,9 +1526,10 @@ public class AddJobView extends Main { BaseTask task = new ConfirmationTask(""); task.setTaskOrder(tasksState.size()); // Set order based on current position tasksState.add(task); - - // Use an array to hold the current task reference (allows modification in lambda) - final BaseTask[] currentTask = {task}; + + // Use an array to hold the current task reference (allows modification in + // lambda) + final BaseTask[] currentTask = { task }; taskTypeCombo.addValueChangeListener(ev -> { TaskType selectedType = ev.getValue(); @@ -1525,14 +1544,16 @@ public class AddJobView extends Main { newTask.setCompletedBy(oldTask.getCompletedBy()); // Preserve task-specific properties - if (oldTask instanceof ConfirmationTask oldConfirmationTask && newTask instanceof ConfirmationTask newConfirmationTask) { + if (oldTask instanceof ConfirmationTask oldConfirmationTask + && newTask instanceof ConfirmationTask newConfirmationTask) { newConfirmationTask.setButtonText(oldConfirmationTask.getButtonText()); } else if (oldTask instanceof TodoListTask oldTodoTask && newTask instanceof TodoListTask newTodoTask) { newTodoTask.setTodoItems(oldTodoTask.getTodoItems()); } else if (oldTask instanceof PhotoTask oldPhotoTask && newTask instanceof PhotoTask newPhotoTask) { newPhotoTask.setMinPhotoCount(oldPhotoTask.getMinPhotoCount()); newPhotoTask.setMaxPhotoCount(oldPhotoTask.getMaxPhotoCount()); - } else if (oldTask instanceof BarcodeTask oldBarcodeTask && newTask instanceof BarcodeTask newBarcodeTask) { + } else if (oldTask instanceof BarcodeTask oldBarcodeTask + && newTask instanceof BarcodeTask newBarcodeTask) { newBarcodeTask.setMinBarcodeCount(oldBarcodeTask.getMinBarcodeCount()); newBarcodeTask.setMaxBarcodeCount(oldBarcodeTask.getMaxBarcodeCount()); } @@ -1558,11 +1579,11 @@ public class AddJobView extends Main { private BaseTask createTaskByType(TaskType taskType) { return switch (taskType) { - case CONFIRMATION -> new ConfirmationTask(""); - case SIGNATURE -> new SignatureTask(); - case TODOLIST -> new TodoListTask(new ArrayList<>()); - case PHOTO -> new PhotoTask(1, 10); - case BARCODE -> new BarcodeTask(1, 10); + case CONFIRMATION -> new ConfirmationTask(""); + case SIGNATURE -> new SignatureTask(); + case TODOLIST -> new TodoListTask(new ArrayList<>()); + case PHOTO -> new PhotoTask(1, 10); + case BARCODE -> new BarcodeTask(1, 10); }; } @@ -1578,164 +1599,157 @@ public class AddJobView extends Main { private void updateTaskConfiguration(VerticalLayout configContainer, BaseTask task) { configContainer.removeAll(); - + TaskType taskType = TaskType.valueOf(task.getTaskType()); - if (taskType == null) return; + if (taskType == null) + return; switch (taskType) { - case CONFIRMATION: - TextField buttonTextField = new TextField("Button-Text"); - buttonTextField.setPlaceholder("z.B. 'Bestätigen', 'Abgeschlossen'"); - buttonTextField.setWidthFull(); - ConfirmationTask confirmationTask = (ConfirmationTask) task; - buttonTextField.setValue(confirmationTask.getButtonText() != null ? - confirmationTask.getButtonText() : ""); - buttonTextField.addValueChangeListener(ev -> { - // Find the current ConfirmationTask in tasksState and update it - for (int i = 0; i < tasksState.size(); i++) { - BaseTask stateTask = tasksState.get(i); - if (stateTask == task && stateTask instanceof ConfirmationTask) { - ((ConfirmationTask) stateTask).setButtonText(ev.getValue()); - break; - } + case CONFIRMATION: + TextField buttonTextField = new TextField("Button-Text"); + buttonTextField.setPlaceholder("z.B. 'Bestätigen', 'Abgeschlossen'"); + buttonTextField.setWidthFull(); + ConfirmationTask confirmationTask = (ConfirmationTask) task; + buttonTextField.setValue(confirmationTask.getButtonText() != null ? confirmationTask.getButtonText() : ""); + buttonTextField.addValueChangeListener(ev -> { + // Find the current ConfirmationTask in tasksState and update it + for (int i = 0; i < tasksState.size(); i++) { + BaseTask stateTask = tasksState.get(i); + if (stateTask == task && stateTask instanceof ConfirmationTask) { + ((ConfirmationTask) stateTask).setButtonText(ev.getValue()); + break; } - }); - configContainer.add(buttonTextField); - break; + } + }); + configContainer.add(buttonTextField); + break; - case SIGNATURE: - // No additional configuration needed - Span info = new Span("Keine zusätzliche Konfiguration erforderlich"); - info.getStyle().set("color", "var(--lumo-secondary-text-color)"); - info.getStyle().set("font-style", "italic"); - configContainer.add(info); - break; + case SIGNATURE: + // No additional configuration needed + Span info = new Span("Keine zusätzliche Konfiguration erforderlich"); + info.getStyle().set("color", "var(--lumo-secondary-text-color)"); + info.getStyle().set("font-style", "italic"); + configContainer.add(info); + break; - case TODOLIST: - VerticalLayout todoContainer = new VerticalLayout(); - todoContainer.setPadding(false); - todoContainer.setSpacing(true); - - H3 todoTitle = new H3("To-Do Punkte"); - todoTitle.getStyle().set("margin", "0"); - todoContainer.add(todoTitle); + case TODOLIST: + VerticalLayout todoContainer = new VerticalLayout(); + todoContainer.setPadding(false); + todoContainer.setSpacing(true); - // Dynamic todo list - VerticalLayout todoList = new VerticalLayout(); - todoList.setPadding(false); - todoList.setSpacing(true); + H3 todoTitle = new H3("To-Do Punkte"); + todoTitle.getStyle().set("margin", "0"); + todoContainer.add(todoTitle); - java.util.function.Consumer addTodoItem = (v) -> { - HorizontalLayout todoRow = new HorizontalLayout(); - todoRow.setWidthFull(); - todoRow.setAlignItems(FlexComponent.Alignment.END); + // Dynamic todo list + VerticalLayout todoList = new VerticalLayout(); + todoList.setPadding(false); + todoList.setSpacing(true); - TextField todoField = new TextField(); - todoField.setPlaceholder("To-Do Punkt"); - todoField.setWidth("100%"); + java.util.function.Consumer addTodoItem = (v) -> { + HorizontalLayout todoRow = new HorizontalLayout(); + todoRow.setWidthFull(); + todoRow.setAlignItems(FlexComponent.Alignment.END); - Button removeTodo = new Button(new Icon(VaadinIcon.CLOSE_SMALL)); - removeTodo.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY); - removeTodo.addClickListener(e -> { - todoList.remove(todoRow); - updateTodoItems(todoList, task); - }); + TextField todoField = new TextField(); + todoField.setPlaceholder("To-Do Punkt"); + todoField.setWidth("100%"); - todoRow.add(todoField, removeTodo); - todoRow.setFlexGrow(1, todoField); - todoList.add(todoRow); - - todoField.addValueChangeListener(ev -> updateTodoItems(todoList, task)); - }; - - Button addTodoBtn = new Button("To-Do Punkt hinzufügen", new Icon(VaadinIcon.PLUS)); - addTodoBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY); - addTodoBtn.addClickListener(e -> addTodoItem.accept(null)); - - // Add initial todo item - addTodoItem.accept(null); - - todoContainer.add(todoList, addTodoBtn); - configContainer.add(todoContainer); - break; - - case PHOTO: - HorizontalLayout photoLayout = new HorizontalLayout(); - photoLayout.setWidthFull(); - photoLayout.setSpacing(true); - - PhotoTask photoTask = (PhotoTask) task; - IntegerField minPhotos = new IntegerField("Min. Anzahl Fotos"); - minPhotos.setPlaceholder("1"); - minPhotos.setMin(1); - minPhotos.setValue(photoTask.getMinPhotoCount() != null ? - photoTask.getMinPhotoCount() : 1); - - IntegerField maxPhotos = new IntegerField("Max. Anzahl Fotos"); - maxPhotos.setPlaceholder("10"); - maxPhotos.setMin(1); - maxPhotos.setValue(photoTask.getMaxPhotoCount() != null ? - photoTask.getMaxPhotoCount() : 10); - - photoLayout.add(minPhotos, maxPhotos); - - minPhotos.addValueChangeListener(ev -> { - photoTask.setMinPhotoCount(ev.getValue()); + Button removeTodo = new Button(new Icon(VaadinIcon.CLOSE_SMALL)); + removeTodo.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY); + removeTodo.addClickListener(e -> { + todoList.remove(todoRow); + updateTodoItems(todoList, task); }); - maxPhotos.addValueChangeListener(ev -> { - photoTask.setMaxPhotoCount(ev.getValue()); - }); + todoRow.add(todoField, removeTodo); + todoRow.setFlexGrow(1, todoField); + todoList.add(todoRow); - configContainer.add(photoLayout); - break; + todoField.addValueChangeListener(ev -> updateTodoItems(todoList, task)); + }; - case BARCODE: - HorizontalLayout barcodeLayout = new HorizontalLayout(); - barcodeLayout.setWidthFull(); - barcodeLayout.setSpacing(true); + Button addTodoBtn = new Button("To-Do Punkt hinzufügen", new Icon(VaadinIcon.PLUS)); + addTodoBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + addTodoBtn.addClickListener(e -> addTodoItem.accept(null)); - BarcodeTask barcodeTask = (BarcodeTask) task; - IntegerField minBarcodes = new IntegerField("Min. Anzahl Barcodes"); - minBarcodes.setPlaceholder("1"); - minBarcodes.setMin(1); - minBarcodes.setValue(barcodeTask.getMinBarcodeCount() != null ? - barcodeTask.getMinBarcodeCount() : 1); + // Add initial todo item + addTodoItem.accept(null); - IntegerField maxBarcodes = new IntegerField("Max. Anzahl Barcodes"); - maxBarcodes.setPlaceholder("10"); - maxBarcodes.setMin(1); - maxBarcodes.setValue(barcodeTask.getMaxBarcodeCount() != null ? - barcodeTask.getMaxBarcodeCount() : 10); + todoContainer.add(todoList, addTodoBtn); + configContainer.add(todoContainer); + break; - barcodeLayout.add(minBarcodes, maxBarcodes); + case PHOTO: + HorizontalLayout photoLayout = new HorizontalLayout(); + photoLayout.setWidthFull(); + photoLayout.setSpacing(true); - minBarcodes.addValueChangeListener(ev -> { - barcodeTask.setMinBarcodeCount(ev.getValue()); - }); + PhotoTask photoTask = (PhotoTask) task; + IntegerField minPhotos = new IntegerField("Min. Anzahl Fotos"); + minPhotos.setPlaceholder("1"); + minPhotos.setMin(1); + minPhotos.setValue(photoTask.getMinPhotoCount() != null ? photoTask.getMinPhotoCount() : 1); - maxBarcodes.addValueChangeListener(ev -> { - barcodeTask.setMaxBarcodeCount(ev.getValue()); - }); + IntegerField maxPhotos = new IntegerField("Max. Anzahl Fotos"); + maxPhotos.setPlaceholder("10"); + maxPhotos.setMin(1); + maxPhotos.setValue(photoTask.getMaxPhotoCount() != null ? photoTask.getMaxPhotoCount() : 10); - configContainer.add(barcodeLayout); - break; + photoLayout.add(minPhotos, maxPhotos); + + minPhotos.addValueChangeListener(ev -> { + photoTask.setMinPhotoCount(ev.getValue()); + }); + + maxPhotos.addValueChangeListener(ev -> { + photoTask.setMaxPhotoCount(ev.getValue()); + }); + + configContainer.add(photoLayout); + break; + + case BARCODE: + HorizontalLayout barcodeLayout = new HorizontalLayout(); + barcodeLayout.setWidthFull(); + barcodeLayout.setSpacing(true); + + BarcodeTask barcodeTask = (BarcodeTask) task; + IntegerField minBarcodes = new IntegerField("Min. Anzahl Barcodes"); + minBarcodes.setPlaceholder("1"); + minBarcodes.setMin(1); + minBarcodes.setValue(barcodeTask.getMinBarcodeCount() != null ? barcodeTask.getMinBarcodeCount() : 1); + + IntegerField maxBarcodes = new IntegerField("Max. Anzahl Barcodes"); + maxBarcodes.setPlaceholder("10"); + maxBarcodes.setMin(1); + maxBarcodes.setValue(barcodeTask.getMaxBarcodeCount() != null ? barcodeTask.getMaxBarcodeCount() : 10); + + barcodeLayout.add(minBarcodes, maxBarcodes); + + minBarcodes.addValueChangeListener(ev -> { + barcodeTask.setMinBarcodeCount(ev.getValue()); + }); + + maxBarcodes.addValueChangeListener(ev -> { + barcodeTask.setMaxBarcodeCount(ev.getValue()); + }); + + configContainer.add(barcodeLayout); + break; } } private void updateTodoItems(VerticalLayout todoList, BaseTask task) { - List todoItems = todoList.getChildren() - .map(component -> { - if (component instanceof HorizontalLayout) { - HorizontalLayout row = (HorizontalLayout) component; - TextField field = (TextField) row.getChildren().findFirst().orElse(null); - return field != null ? field.getValue() : null; - } - return null; - }) - .filter(Objects::nonNull) - .filter(item -> !item.trim().isEmpty()) - .collect(java.util.stream.Collectors.toList()); + List todoItems = todoList.getChildren().map(component -> { + if (component instanceof HorizontalLayout) { + HorizontalLayout row = (HorizontalLayout) component; + TextField field = (TextField) row.getChildren().findFirst().orElse(null); + return field != null ? field.getValue() : null; + } + return null; + }).filter(Objects::nonNull).filter(item -> !item.trim().isEmpty()) + .collect(java.util.stream.Collectors.toList()); if (task instanceof TodoListTask) { ((TodoListTask) task).setTodoItems(todoItems); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AppDevicesView.java b/src/main/java/de/assecutor/votianlt/pages/view/AppDevicesView.java index 071499b..109925b 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AppDevicesView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AppDevicesView.java @@ -20,7 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired; @PageTitle("Endgeräte") @Route(value = "app-devices", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) -@RolesAllowed({"USER","ADMIN"}) +@RolesAllowed({ "USER", "ADMIN" }) public class AppDevicesView extends VerticalLayout { private final AppDeviceService appDeviceService; @@ -32,7 +32,7 @@ public class AppDevicesView extends VerticalLayout { public AppDevicesView(AppDeviceService appDeviceService, AppUserService appUserService) { this.appDeviceService = appDeviceService; this.appUserService = appUserService; - + setSizeFull(); setPadding(true); setSpacing(true); @@ -41,14 +41,14 @@ public class AppDevicesView extends VerticalLayout { HorizontalLayout header = new HorizontalLayout(); header.setWidthFull(); header.setAlignItems(FlexComponent.Alignment.CENTER); - + H2 title = new H2("Endgeräte"); title.getStyle().set("margin", "0"); - + Button addButton = new Button("Neues Endgerät anlegen", new Icon(VaadinIcon.PLUS)); addButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); addButton.addClickListener(e -> navigateToAddAppDevice()); - + header.add(title, addButton); header.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN); add(header); @@ -56,17 +56,16 @@ public class AppDevicesView extends VerticalLayout { // Grid für Endgeräte appDeviceGrid = new Grid<>(AppDevice.class, false); appDeviceGrid.setSizeFull(); - + // Grid-Spalten konfigurieren appDeviceGrid.addColumn(AppDevice::getName).setHeader("Gerätename").setAutoWidth(true); - + // App-Nutzer Spalte mit Name anzeigen appDeviceGrid.addColumn(appDevice -> { if (appDevice.getAppUserId() != null) { try { AppUser appUser = appUserService.findByCurrentUser().stream() - .filter(user -> user.getId().equals(appDevice.getAppUserId())) - .findFirst().orElse(null); + .filter(user -> user.getId().equals(appDevice.getAppUserId())).findFirst().orElse(null); if (appUser != null) { return appUser.getVorname() + " " + appUser.getNachname(); } @@ -76,13 +75,13 @@ public class AppDevicesView extends VerticalLayout { } return "Nicht zugeordnet"; }).setHeader("Zugeordneter App-Nutzer").setAutoWidth(true); - + appDeviceGrid.addColumn(AppDevice::getErstelltAm).setHeader("Erstellt am").setAutoWidth(true); - + // Make grid rows clickable appDeviceGrid.setSelectionMode(Grid.SelectionMode.SINGLE); appDeviceGrid.getStyle().set("cursor", "pointer"); - + // Add click listener to navigate to edit view appDeviceGrid.addItemClickListener(event -> { AppDevice appDevice = event.getItem(); @@ -90,7 +89,7 @@ public class AppDevicesView extends VerticalLayout { getUI().ifPresent(ui -> ui.navigate("edit-app-device/" + appDevice.getId().toHexString())); } }); - + add(appDeviceGrid); // Daten laden diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AppUserView.java b/src/main/java/de/assecutor/votianlt/pages/view/AppUserView.java index fc41ff3..cf31e0e 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AppUserView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AppUserView.java @@ -20,7 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired; @PageTitle("App-Nutzer") @Route(value = "app-user", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) -@RolesAllowed({"USER","ADMIN"}) +@RolesAllowed({ "USER", "ADMIN" }) public class AppUserView extends VerticalLayout { private final AppUserService appUserService; @@ -38,14 +38,14 @@ public class AppUserView extends VerticalLayout { HorizontalLayout header = new HorizontalLayout(); header.setWidthFull(); header.setAlignItems(FlexComponent.Alignment.CENTER); - + H2 title = new H2("App-Nutzer"); title.getStyle().set("margin", "0"); - + Button addButton = new Button("Neuen App-Nutzer anlegen", new Icon(VaadinIcon.PLUS)); addButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); addButton.addClickListener(e -> navigateToAddAppUser()); - + header.add(title, addButton); header.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN); add(header); @@ -53,7 +53,7 @@ public class AppUserView extends VerticalLayout { // Grid für App-Nutzer appUserGrid = new Grid<>(AppUser.class, false); appUserGrid.setSizeFull(); - + // Grid-Spalten konfigurieren appUserGrid.addColumn(AppUser::getBezeichnung).setHeader("Bezeichnung").setAutoWidth(true); appUserGrid.addColumn(AppUser::getVorname).setHeader("Vorname").setAutoWidth(true); @@ -68,11 +68,11 @@ public class AppUserView extends VerticalLayout { } return "Kein Gerät zugewiesen"; }).setHeader("Endgerät").setAutoWidth(true); - + // Make grid rows clickable appUserGrid.setSelectionMode(Grid.SelectionMode.SINGLE); appUserGrid.getStyle().set("cursor", "pointer"); - + // Add click listener to navigate to edit view appUserGrid.addItemClickListener(event -> { AppUser appUser = event.getItem(); @@ -80,7 +80,7 @@ public class AppUserView extends VerticalLayout { getUI().ifPresent(ui -> ui.navigate("edit-app-user/" + appUser.getId().toHexString())); } }); - + add(appUserGrid); // Daten laden diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java b/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java index a206c78..0b226ce 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java @@ -46,7 +46,8 @@ public class AuthenticatedStartView extends VerticalLayout { heroSection.setPadding(true); heroSection.setSpacing(true); heroSection.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); - heroSection.getStyle().set("background", "linear-gradient(135deg, var(--lumo-primary-color-10pct), var(--lumo-primary-color-50pct))"); + heroSection.getStyle().set("background", + "linear-gradient(135deg, var(--lumo-primary-color-10pct), var(--lumo-primary-color-50pct))"); heroSection.getStyle().set("min-height", "300px"); heroSection.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); @@ -58,8 +59,7 @@ public class AuthenticatedStartView extends VerticalLayout { welcomeTitle.getStyle().set("margin-bottom", "var(--lumo-space-l)"); Paragraph welcomeDescription = new Paragraph( - "Nutzen Sie die Navigation links, um neue Aufträge zu erstellen oder Ihre Verwaltung zu bearbeiten." - ); + "Nutzen Sie die Navigation links, um neue Aufträge zu erstellen oder Ihre Verwaltung zu bearbeiten."); welcomeDescription.getStyle().set("text-align", "center"); welcomeDescription.getStyle().set("max-width", "600px"); welcomeDescription.getStyle().set("font-size", "var(--lumo-font-size-l)"); @@ -82,9 +82,8 @@ public class AuthenticatedStartView extends VerticalLayout { systemTitle.getStyle().set("text-align", "center"); Paragraph systemIntro = new Paragraph( - "Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe ist von entscheidender Bedeutung, " + - "dass sie sich in erster Linie auf ihr eigentliches Geschäft konzentrieren können: Kunden gewinnen und Waren von A nach B liefern." - ); + "Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe ist von entscheidender Bedeutung, " + + "dass sie sich in erster Linie auf ihr eigentliches Geschäft konzentrieren können: Kunden gewinnen und Waren von A nach B liefern."); systemIntro.getStyle().set("text-align", "center"); systemIntro.getStyle().set("max-width", "800px"); systemIntro.getStyle().set("margin-bottom", "var(--lumo-space-xl)"); @@ -96,14 +95,12 @@ public class AuthenticatedStartView extends VerticalLayout { featuresGrid.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.START); // Feature Cards - featuresGrid.add( - createFeatureCard(VaadinIcon.COG, "Einrichtungsassistent", + featuresGrid.add(createFeatureCard(VaadinIcon.COG, "Einrichtungsassistent", "Mithilfe des Einrichtungsassistenten haben Sie die Möglichkeit, Ihr Nutzerprofil zu vervollständigen."), - createFeatureCard(VaadinIcon.USERS, "Kunden- und Auftragsverwaltung", - "Mit der Kunden- und Auftragsverwaltung haben Sie alle Kontaktdaten und Auftragsdetails stets im Blick."), - createFeatureCard(VaadinIcon.CLIPBOARD_TEXT, "Auftragserstellung", - "Stellen Sie mit wenigen Mausklicks Aufträge ins System ein und legen Sie fest, welcher Mitarbeiter welchen Transportauftrag abarbeiten soll.") - ); + createFeatureCard(VaadinIcon.USERS, "Kunden- und Auftragsverwaltung", + "Mit der Kunden- und Auftragsverwaltung haben Sie alle Kontaktdaten und Auftragsdetails stets im Blick."), + createFeatureCard(VaadinIcon.CLIPBOARD_TEXT, "Auftragserstellung", + "Stellen Sie mit wenigen Mausklicks Aufträge ins System ein und legen Sie fest, welcher Mitarbeiter welchen Transportauftrag abarbeiten soll.")); systemSection.add(systemTitle, systemIntro, featuresGrid); return systemSection; @@ -153,9 +150,8 @@ public class AuthenticatedStartView extends VerticalLayout { appTitle.getStyle().set("text-align", "center"); Paragraph appDescription = new Paragraph( - "Mit unserer mobilen App bleiben Sie auch unterwegs immer über Ihre Aufträge informiert " + - "und können wichtige Aufgaben direkt vom Smartphone aus erledigen." - ); + "Mit unserer mobilen App bleiben Sie auch unterwegs immer über Ihre Aufträge informiert " + + "und können wichtige Aufgaben direkt vom Smartphone aus erledigen."); appDescription.getStyle().set("text-align", "center"); appDescription.getStyle().set("max-width", "600px"); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/EditAppDeviceView.java b/src/main/java/de/assecutor/votianlt/pages/view/EditAppDeviceView.java index be850ed..9d96a1e 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/EditAppDeviceView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/EditAppDeviceView.java @@ -25,17 +25,17 @@ import org.springframework.beans.factory.annotation.Autowired; @PageTitle("Endgerät bearbeiten") @Route(value = "edit-app-device", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) -@RolesAllowed({"USER","ADMIN"}) +@RolesAllowed({ "USER", "ADMIN" }) public class EditAppDeviceView extends VerticalLayout implements HasUrlParameter { private final AppDeviceService appDeviceService; private final AppUserService appUserService; private final Binder binder; - + // Formularfelder private final TextField nameField; private final ComboBox appUserComboBox; - + // Aktuelles Endgerät private AppDevice currentAppDevice; @@ -43,39 +43,40 @@ public class EditAppDeviceView extends VerticalLayout implements HasUrlParameter public EditAppDeviceView(AppDeviceService appDeviceService, AppUserService appUserService) { this.appDeviceService = appDeviceService; this.appUserService = appUserService; - + // Binder initialisieren binder = new Binder<>(AppDevice.class); - + // Formularfelder erstellen nameField = new TextField("Gerätename"); nameField.setRequired(true); nameField.setPlaceholder("z.B. iPhone 15, Samsung Galaxy S24"); nameField.setWidth("100%"); - + // AppUser ComboBox appUserComboBox = new ComboBox<>("App-Nutzer zuordnen"); appUserComboBox.setPlaceholder("App-Nutzer auswählen (optional)"); appUserComboBox.setWidth("100%"); - appUserComboBox.setItemLabelGenerator(appUser -> - appUser.getVorname() + " " + appUser.getNachname() + " (" + appUser.getEmail() + ")"); - + appUserComboBox.setItemLabelGenerator( + appUser -> appUser.getVorname() + " " + appUser.getNachname() + " (" + appUser.getEmail() + ")"); + // Lade verfügbare App-Nutzer try { appUserComboBox.setItems(appUserService.findByCurrentUser()); } catch (Exception e) { - Notification.show("Fehler beim Laden der App-Nutzer: " + e.getMessage(), 3000, Notification.Position.MIDDLE); + Notification.show("Fehler beim Laden der App-Nutzer: " + e.getMessage(), 3000, + Notification.Position.MIDDLE); } - + // Layout konfigurieren setSizeFull(); setPadding(true); setSpacing(true); - + // Content zentrieren setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); - + // Hauptcontainer erstellen VerticalLayout contentContainer = new VerticalLayout(); contentContainer.setWidth("600px"); @@ -85,45 +86,45 @@ public class EditAppDeviceView extends VerticalLayout implements HasUrlParameter contentContainer.getStyle().set("box-shadow", "0 2px 8px rgba(0,0,0,0.1)"); contentContainer.setPadding(true); contentContainer.setSpacing(true); - + // Titel H2 title = new H2("Endgerät bearbeiten"); title.getStyle().set("margin", "0"); title.getStyle().set("text-align", "center"); contentContainer.add(title); - + // Formular FormLayout formLayout = new FormLayout(); formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1)); formLayout.add(nameField, appUserComboBox); contentContainer.add(formLayout); - + // Buttons HorizontalLayout buttonLayout = new HorizontalLayout(); buttonLayout.setWidthFull(); buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); buttonLayout.setSpacing(true); - + Button backButton = new Button("Zurück"); backButton.addClickListener(e -> navigateBack()); - + Button saveButton = new Button("Speichern"); saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); saveButton.addClickListener(e -> saveAppDevice()); - + Button deleteButton = new Button("Löschen"); deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR); deleteButton.addClickListener(e -> deleteAppDevice()); - + buttonLayout.add(backButton, saveButton, deleteButton); contentContainer.add(buttonLayout); - + add(contentContainer); - + // Binder konfigurieren setupBinder(); } - + @Override public void setParameter(com.vaadin.flow.router.BeforeEvent event, String parameter) { try { @@ -134,10 +135,10 @@ public class EditAppDeviceView extends VerticalLayout implements HasUrlParameter navigateBack(); } } - + private void loadAppDevice(ObjectId deviceId) { currentAppDevice = appDeviceService.findById(deviceId); - + if (currentAppDevice != null) { // Formular mit aktuellen Daten füllen binder.readBean(currentAppDevice); @@ -146,51 +147,50 @@ public class EditAppDeviceView extends VerticalLayout implements HasUrlParameter navigateBack(); } } - + private void setupBinder() { - binder.forField(nameField) - .asRequired("Gerätename ist erforderlich") - .bind(AppDevice::getName, AppDevice::setName); - - binder.forField(appUserComboBox) - .bind(appDevice -> { - if (appDevice.getAppUserId() != null) { - return appUserService.findByCurrentUser().stream() - .filter(user -> user.getId().equals(appDevice.getAppUserId())) - .findFirst().orElse(null); - } - return null; - }, (appDevice, appUser) -> { - if (appUser != null) { - appDevice.setAppUserId(appUser.getId()); - } else { - appDevice.setAppUserId(null); - } - }); + binder.forField(nameField).asRequired("Gerätename ist erforderlich").bind(AppDevice::getName, + AppDevice::setName); + + binder.forField(appUserComboBox).bind(appDevice -> { + if (appDevice.getAppUserId() != null) { + return appUserService.findByCurrentUser().stream() + .filter(user -> user.getId().equals(appDevice.getAppUserId())).findFirst().orElse(null); + } + return null; + }, (appDevice, appUser) -> { + if (appUser != null) { + appDevice.setAppUserId(appUser.getId()); + } else { + appDevice.setAppUserId(null); + } + }); } - + private void saveAppDevice() { if (binder.validate().isOk()) { try { // Aktuelle Daten in das Modell schreiben binder.writeBean(currentAppDevice); - + // Endgerät aktualisieren AppDevice updatedDevice = appDeviceService.updateAppDevice(currentAppDevice); - - Notification.show("Endgerät erfolgreich aktualisiert: " + updatedDevice.getName(), 3000, Notification.Position.MIDDLE); - + + Notification.show("Endgerät erfolgreich aktualisiert: " + updatedDevice.getName(), 3000, + Notification.Position.MIDDLE); + // Zurück zur Übersicht navigateBack(); - + } catch (Exception e) { - Notification.show("Fehler beim Aktualisieren des Endgeräts: " + e.getMessage(), 5000, Notification.Position.MIDDLE); + Notification.show("Fehler beim Aktualisieren des Endgeräts: " + e.getMessage(), 5000, + Notification.Position.MIDDLE); } } else { Notification.show("Bitte füllen Sie alle erforderlichen Felder aus", 3000, Notification.Position.MIDDLE); } } - + private void deleteAppDevice() { if (currentAppDevice != null && currentAppDevice.getId() != null) { ConfirmDialog confirmDialog = new ConfirmDialog(); @@ -199,21 +199,22 @@ public class EditAppDeviceView extends VerticalLayout implements HasUrlParameter confirmDialog.setCancelText("Abbrechen"); confirmDialog.setConfirmText("Löschen"); confirmDialog.setConfirmButtonTheme("error primary"); - + confirmDialog.addConfirmListener(event -> { try { appDeviceService.deleteById(currentAppDevice.getId()); Notification.show("Endgerät erfolgreich gelöscht", 3000, Notification.Position.MIDDLE); navigateBack(); } catch (Exception e) { - Notification.show("Fehler beim Löschen des Endgeräts: " + e.getMessage(), 5000, Notification.Position.MIDDLE); + Notification.show("Fehler beim Löschen des Endgeräts: " + e.getMessage(), 5000, + Notification.Position.MIDDLE); } }); - + confirmDialog.open(); } } - + private void navigateBack() { getUI().ifPresent(ui -> ui.navigate("app-devices")); } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/EditAppUserView.java b/src/main/java/de/assecutor/votianlt/pages/view/EditAppUserView.java index 957d62b..9e89e81 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/EditAppUserView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/EditAppUserView.java @@ -32,7 +32,7 @@ import java.util.List; @PageTitle("App-Nutzer bearbeiten") @Route(value = "edit-app-user", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) -@RolesAllowed({"USER","ADMIN"}) +@RolesAllowed({ "USER", "ADMIN" }) public class EditAppUserView extends VerticalLayout implements HasUrlParameter { private final AppUserService appUserService; @@ -60,7 +60,7 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter navigateBack()); - + header.add(title, backButton); header.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN); contentContainer.add(header); // Form layout FormLayout formLayout = new FormLayout(); - formLayout.setResponsiveSteps( - new FormLayout.ResponsiveStep("0", 1) - ); + formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1)); // Configure fields designationField.setPlaceholder("(HH H 000)"); designationField.setWidthFull(); - + firstnameField.setWidthFull(); lastnameField.setWidthFull(); - + // Create horizontal layout for firstname and lastname HorizontalLayout nameLayout = new HorizontalLayout(); nameLayout.setWidthFull(); nameLayout.setSpacing(true); nameLayout.add(firstnameField, lastnameField); - + phoneField.setWidthFull(); appCodeField.setWidthFull(); emailField.setWidthFull(); - + // Configure password fields changePasswordField.setWidthFull(); changePasswordField.setPlaceholder("Leer lassen, wenn nicht ändern"); confirmChangePasswordField.setWidthFull(); confirmChangePasswordField.setPlaceholder("Leer lassen, wenn nicht ändern"); - + // Configure device dropdown deviceComboBox.setWidthFull(); deviceComboBox.setItemLabelGenerator(device -> device.getName()); @@ -141,10 +139,10 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter saveAppUser()); saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - + Button deleteButton = new Button("Löschen", e -> deleteAppUser()); deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR); @@ -167,11 +165,8 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter getCurrentDevice(appUser), // Get current device - (appUser, appDevice) -> appUser.setAppDeviceId(appDevice != null ? appDevice.getId() : null) - ); + binder.forField(deviceComboBox).bind(appUser -> getCurrentDevice(appUser), // Get current device + (appUser, appDevice) -> appUser.setAppDeviceId(appDevice != null ? appDevice.getId() : null)); } @Override @@ -183,10 +178,10 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter availableDevices = new ArrayList<>(); - + // First, add the currently assigned device if it exists AppDevice currentDevice = getCurrentDevice(appUser); if (currentDevice != null) { availableDevices.add(currentDevice); } - + // Then add all unassigned devices List unassignedDevices = appDeviceService.findUnassignedDevices(); availableDevices.addAll(unassignedDevices); - + deviceComboBox.setItems(availableDevices); - + // Set the current device as selected if (currentDevice != null) { deviceComboBox.setValue(currentDevice); @@ -307,7 +302,7 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter { if (appUser != null && appUser.getId() != null) { @@ -318,13 +313,13 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter confirmDialog.close()); - + buttonLayout.add(confirmDeleteButton, cancelDeleteButton); buttonLayout.setSpacing(true); confirmDialog.add(buttonLayout); - + confirmDialog.open(); } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/EditCustomerView.java b/src/main/java/de/assecutor/votianlt/pages/view/EditCustomerView.java index a85bb13..e610bdd 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/EditCustomerView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/EditCustomerView.java @@ -25,7 +25,7 @@ import com.vaadin.flow.component.orderedlayout.FlexComponent; @PageTitle("Kunde bearbeiten") @Route(value = "edit-customer", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) -@RolesAllowed({"USER","ADMIN"}) +@RolesAllowed({ "USER", "ADMIN" }) public class EditCustomerView extends VerticalLayout implements HasUrlParameter { private final CustomerService customerService; @@ -52,7 +52,7 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter< setSizeFull(); setPadding(true); setSpacing(true); - + // Center content vertically setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); @@ -75,9 +75,7 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter< // Form layout FormLayout formLayout = new FormLayout(); - formLayout.setResponsiveSteps( - new FormLayout.ResponsiveStep("0", 1) - ); + formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1)); // Add fields to form - all fields in single column formLayout.add(titleField); @@ -99,10 +97,10 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter< HorizontalLayout buttonLayout = new HorizontalLayout(); buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); buttonLayout.setWidthFull(); - + Button saveButton = new Button("Speichern", e -> saveCustomer()); saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - + Button cancelButton = new Button("Abbrechen", e -> navigateBack()); Button deleteButton = new Button("Löschen", e -> deleteCustomer()); deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR); @@ -138,7 +136,7 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter< try { ObjectId customerId = new ObjectId(parameter); customer = customerService.findById(customerId); - + if (customer == null) { Notification.show("Kunde nicht gefunden", 3000, Notification.Position.MIDDLE); navigateBack(); @@ -147,7 +145,7 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter< // Load customer data into form binder.readBean(customer); - + } catch (IllegalArgumentException e) { Notification.show("Ungültige Kunden-ID", 3000, Notification.Position.MIDDLE); navigateBack(); @@ -169,7 +167,7 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter< // Show confirmation dialog Dialog confirmDialog = new Dialog(); confirmDialog.add("Möchten Sie diesen Kunden wirklich löschen?"); - + HorizontalLayout buttonLayout = new HorizontalLayout(); Button confirmDeleteButton = new Button("Ja, löschen", e -> { if (customer != null && customer.getId() != null) { @@ -179,13 +177,13 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter< } }); confirmDeleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR); - + Button cancelDeleteButton = new Button("Abbrechen", e -> confirmDialog.close()); - + buttonLayout.add(confirmDeleteButton, cancelDeleteButton); buttonLayout.setSpacing(true); confirmDialog.add(buttonLayout); - + confirmDialog.open(); } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java b/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java index 8e1d82e..a5174a4 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java @@ -32,7 +32,7 @@ import jakarta.annotation.security.RolesAllowed; @PageTitle("Profil bearbeiten") @Route(value = "edit-profile", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) -@RolesAllowed({"USER","ADMIN"}) +@RolesAllowed({ "USER", "ADMIN" }) public class EditProfileView extends HorizontalLayout { private final TextField prefixField; private final TextField ustIdField; @@ -71,7 +71,6 @@ public class EditProfileView extends HorizontalLayout { tabSheet.setSizeFull(); formColumn.setFlexGrow(1, tabSheet); - FormLayout form = new FormLayout(); form.setWidthFull(); form.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 2)); @@ -163,35 +162,22 @@ public class EditProfileView extends HorizontalLayout { zipField.setRequiredIndicatorVisible(true); cityField.setRequiredIndicatorVisible(true); - binder.forField(companyField) - .asRequired("") - .bind(user -> null, (user, v) -> {}); - binder.forField(streetField) - .asRequired("") - .bind(user -> null, (user, v) -> {}); - binder.forField(houseNumberField) - .asRequired("") - .bind(user -> null, (user, v) -> {}); - binder.forField(zipField) - .asRequired("") - .bind(user -> null, (user, v) -> {}); - binder.forField(cityField) - .asRequired("") - .bind(user -> null, (user, v) -> {}); + binder.forField(companyField).asRequired("").bind(user -> null, (user, v) -> { + }); + binder.forField(streetField).asRequired("").bind(user -> null, (user, v) -> { + }); + binder.forField(houseNumberField).asRequired("").bind(user -> null, (user, v) -> { + }); + binder.forField(zipField).asRequired("").bind(user -> null, (user, v) -> { + }); + binder.forField(cityField).asRequired("").bind(user -> null, (user, v) -> { + }); - binder.forField(firstnameField) - .asRequired("") - .bind(User::getFirstname, User::setFirstname); - binder.forField(lastnameField) - .asRequired("") - .bind(User::getName, User::setName); - binder.forField(phoneField) - .asRequired("") - .bind(User::getPhone, User::setPhone); - binder.forField(emailField) - .asRequired("") - .withValidator(new EmailValidator("Ungültige E-Mail-Adresse")) - .bind(User::getEmail, User::setEmail); + binder.forField(firstnameField).asRequired("").bind(User::getFirstname, User::setFirstname); + binder.forField(lastnameField).asRequired("").bind(User::getName, User::setName); + binder.forField(phoneField).asRequired("").bind(User::getPhone, User::setPhone); + binder.forField(emailField).asRequired("").withValidator(new EmailValidator("Ungültige E-Mail-Adresse")) + .bind(User::getEmail, User::setEmail); // Optionale Felder binder.forField(mobileField).bind(User::getPhone2, User::setPhone2); binder.forField(faxField).bind(User::getFax, User::setFax); @@ -224,8 +210,8 @@ public class EditProfileView extends HorizontalLayout { mapDiv.setWidth("100%"); mapDiv.setHeight("400px"); mapDiv.getElement().setProperty("innerHTML", - ""); + ""); VerticalLayout mapTab = new VerticalLayout(); mapTab.setPadding(false); mapTab.setSpacing(true); @@ -282,8 +268,8 @@ public class EditProfileView extends HorizontalLayout { introTextArea.addValueChangeListener(e -> refreshPdf()); termsTextArea.addValueChangeListener(e -> refreshPdf()); - billingLeft.add(partsTitle, billingEnabled, prefixField, ustIdField, taxNumberField, bankNameField, - ibanField, taxRateField, introTextArea, termsTextArea); + billingLeft.add(partsTitle, billingEnabled, prefixField, ustIdField, taxNumberField, bankNameField, ibanField, + taxRateField, introTextArea, termsTextArea); // Rechte Spalte: Vorschau VerticalLayout billingRight = new VerticalLayout(); @@ -299,10 +285,8 @@ public class EditProfileView extends HorizontalLayout { Div previewWrapper = new Div(); previewWrapper.setWidth("100%"); previewWrapper.setHeight("650px"); - previewWrapper.getStyle() - .set("overflow", "hidden") - .set("background", "var(--lumo-contrast-10pct)") - .set("padding", "0"); + previewWrapper.getStyle().set("overflow", "hidden").set("background", "var(--lumo-contrast-10pct)") + .set("padding", "0"); // Initial noch keine PDF laden (erst bei aktiver Checkbox) pdfFrame = new IFrame(); @@ -315,11 +299,9 @@ public class EditProfileView extends HorizontalLayout { billingRight.add(previewTitle, previewWrapper); - billingTab.add(billingLeft, billingRight); tabSheet.add("Rechnungsstellung", billingTab); - // Zweiter Tab: Einstellungen (Beispiel mit Schaltern) VerticalLayout switches = new VerticalLayout(); switches.setPadding(false); @@ -392,14 +374,22 @@ public class EditProfileView extends HorizontalLayout { // PDF neu rendern und iframe aktualisieren // Felder im Billing-Tab aktivieren/deaktivieren private void setBillingFieldsEnabled(boolean enabled) { - if (prefixField != null) prefixField.setEnabled(enabled); - if (ustIdField != null) ustIdField.setEnabled(enabled); - if (taxNumberField != null) taxNumberField.setEnabled(enabled); - if (bankNameField != null) bankNameField.setEnabled(enabled); - if (ibanField != null) ibanField.setEnabled(enabled); - if (taxRateField != null) taxRateField.setEnabled(enabled); - if (introTextArea != null) introTextArea.setEnabled(enabled); - if (termsTextArea != null) termsTextArea.setEnabled(enabled); + if (prefixField != null) + prefixField.setEnabled(enabled); + if (ustIdField != null) + ustIdField.setEnabled(enabled); + if (taxNumberField != null) + taxNumberField.setEnabled(enabled); + if (bankNameField != null) + bankNameField.setEnabled(enabled); + if (ibanField != null) + ibanField.setEnabled(enabled); + if (taxRateField != null) + taxRateField.setEnabled(enabled); + if (introTextArea != null) + introTextArea.setEnabled(enabled); + if (termsTextArea != null) + termsTextArea.setEnabled(enabled); } // Checkbox steuert Aktivierung und PDF @@ -424,14 +414,14 @@ public class EditProfileView extends HorizontalLayout { private void refreshPdf() { byte[] bytes = generatePreviewPdf(); String dataUrl = "data:application/pdf;base64," + Base64.getEncoder().encodeToString(bytes) - + "#toolbar=0&navpanes=0&statusbar=0&view=Fit&zoom=page-fit"; + + "#toolbar=0&navpanes=0&statusbar=0&view=Fit&zoom=page-fit"; if (pdfFrame != null) { pdfFrame.setSrc(dataUrl); } } - - // Einfache PDF-Vorschau generieren (kann später durch echte Logik ersetzt werden) + // Einfache PDF-Vorschau generieren (kann später durch echte Logik ersetzt + // werden) private byte[] generatePreviewPdf() { try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { com.lowagie.text.Document document = new com.lowagie.text.Document(); @@ -459,7 +449,12 @@ public class EditProfileView extends HorizontalLayout { } // Utility: safe getter für TextField/TextArea - private String safe(TextField f) { return f != null && f.getValue() != null ? f.getValue() : ""; } - private String safe(TextArea f) { return f != null && f.getValue() != null ? f.getValue() : ""; } + private String safe(TextField f) { + return f != null && f.getValue() != null ? f.getValue() : ""; + } + + private String safe(TextArea f) { + return f != null && f.getValue() != null ? f.getValue() : ""; + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/ForgetPasswordView.java b/src/main/java/de/assecutor/votianlt/pages/view/ForgetPasswordView.java index bb25b17..10f7816 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/ForgetPasswordView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/ForgetPasswordView.java @@ -61,11 +61,13 @@ public class ForgetPasswordView extends VerticalLayout implements BeforeEnterObs String tokenParam = params.getOrDefault("token", java.util.List.of("")).getFirst(); String typeParam = params.getOrDefault("type", java.util.List.of("")).getFirst(); this.token = tokenParam != null ? tokenParam.trim() : ""; - this.userType = "app_user".equalsIgnoreCase(typeParam) ? PasswordResetService.UserType.APP_USER : PasswordResetService.UserType.USERS; + this.userType = "app_user".equalsIgnoreCase(typeParam) ? PasswordResetService.UserType.APP_USER + : PasswordResetService.UserType.USERS; if (this.token.isEmpty() || !passwordResetService.isTokenValid(this.token, this.userType)) { // Store a flash message in the VaadinSession so it persists through reroute - com.vaadin.flow.server.VaadinSession.getCurrent().setAttribute("flashMessage", "Ungültiger oder abgelaufener Token."); + com.vaadin.flow.server.VaadinSession.getCurrent().setAttribute("flashMessage", + "Ungültiger oder abgelaufener Token."); event.rerouteTo(LoginView.class); } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/ForgotPasswordRequestView.java b/src/main/java/de/assecutor/votianlt/pages/view/ForgotPasswordRequestView.java index 16e6f64..32f875b 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/ForgotPasswordRequestView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/ForgotPasswordRequestView.java @@ -49,7 +49,8 @@ public class ForgotPasswordRequestView extends VerticalLayout { } String baseUrl = getBaseUrl(); passwordResetService.initiateResetAuto(email.trim(), baseUrl); - Notification.show("Falls die E-Mail existiert, wurde ein Link versendet.", 4000, Notification.Position.MIDDLE); + Notification.show("Falls die E-Mail existiert, wurde ein Link versendet.", 4000, + Notification.Position.MIDDLE); getUI().ifPresent(ui -> ui.navigate("login")); }); submit.addThemeVariants(ButtonVariant.LUMO_PRIMARY); @@ -69,8 +70,8 @@ public class ForgotPasswordRequestView extends VerticalLayout { String serverName = req.getServerName(); int serverPort = req.getServerPort(); String contextPath = req.getContextPath(); - String portPart = ("http".equals(scheme) && serverPort == 80) || ("https".equals(scheme) && serverPort == 443) - ? "" : ":" + serverPort; + String portPart = ("http".equals(scheme) && serverPort == 80) + || ("https".equals(scheme) && serverPort == 443) ? "" : ":" + serverPort; return scheme + "://" + serverName + portPart + contextPath; } return ""; diff --git a/src/main/java/de/assecutor/votianlt/pages/view/ImprintView.java b/src/main/java/de/assecutor/votianlt/pages/view/ImprintView.java index 813c413..ef87421 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/ImprintView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/ImprintView.java @@ -22,7 +22,8 @@ public class ImprintView extends VerticalLayout { Paragraph p1 = new Paragraph("Max Mustermann\nMusterstraße 1\n12345 Musterstadt\nDeutschland"); Paragraph p2 = new Paragraph("Telefon: +49 123 456789\nE-Mail: info@example.com"); - Paragraph p3 = new Paragraph("Umsatzsteuer-ID: DE123456789\nHandelsregister: Amtsgericht Musterstadt, HRB 12345"); + Paragraph p3 = new Paragraph( + "Umsatzsteuer-ID: DE123456789\nHandelsregister: Amtsgericht Musterstadt, HRB 12345"); Paragraph p4 = new Paragraph("Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV: Max Mustermann"); add(p1, p2, p3, p4); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/InvoicesView.java b/src/main/java/de/assecutor/votianlt/pages/view/InvoicesView.java index 50d55b5..c9b476f 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/InvoicesView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/InvoicesView.java @@ -21,7 +21,7 @@ import com.vaadin.flow.server.StreamRegistration; @PageTitle("Rechnungen") @Route(value = "invoices", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) -@RolesAllowed({"USER","ADMIN"}) +@RolesAllowed({ "USER", "ADMIN" }) public class InvoicesView extends VerticalLayout { private final Grid invoiceGrid; @@ -47,10 +47,11 @@ public class InvoicesView extends VerticalLayout { // Testdaten List testInvoices = List.of( - new Invoice("R-2024-001", "Max Mustermann", LocalDate.now().minusDays(2), 199.99, "Transport Hamburg-Berlin"), - new Invoice("R-2024-002", "Erika Musterfrau", LocalDate.now().minusDays(1), 299.49, "Express München-Köln"), - new Invoice("R-2024-003", "Hans Beispiel", LocalDate.now(), 149.00, "Standard Leipzig-Dresden") - ); + new Invoice("R-2024-001", "Max Mustermann", LocalDate.now().minusDays(2), 199.99, + "Transport Hamburg-Berlin"), + new Invoice("R-2024-002", "Erika Musterfrau", LocalDate.now().minusDays(1), 299.49, + "Express München-Köln"), + new Invoice("R-2024-003", "Hans Beispiel", LocalDate.now(), 149.00, "Standard Leipzig-Dresden")); invoiceGrid.setItems(testInvoices); invoiceGrid.addItemClickListener(event -> { @@ -67,17 +68,16 @@ public class InvoicesView extends VerticalLayout { try { // PDF generieren byte[] pdfBytes = generatePdf(invoice); - StreamResource resource = new StreamResource( - invoice.getId() + ".pdf", - () -> new ByteArrayInputStream(pdfBytes) - ); + StreamResource resource = new StreamResource(invoice.getId() + ".pdf", + () -> new ByteArrayInputStream(pdfBytes)); resource.setContentType("application/pdf"); resource.setCacheTime(0); // Direkter Download über UI - StreamRegistration registration = UI.getCurrent().getSession().getResourceRegistry().registerResource(resource); + StreamRegistration registration = UI.getCurrent().getSession().getResourceRegistry() + .registerResource(resource); UI.getCurrent().getPage().open(registration.getResourceUri().toString()); - + } catch (Exception e) { Notification.show("Fehler beim Erstellen der PDF: " + e.getMessage(), 5000, Notification.Position.MIDDLE); } @@ -101,4 +101,3 @@ public class InvoicesView extends VerticalLayout { } } } - diff --git a/src/main/java/de/assecutor/votianlt/pages/view/JobHistoryView.java b/src/main/java/de/assecutor/votianlt/pages/view/JobHistoryView.java index 780277a..6c3842e 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/JobHistoryView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/JobHistoryView.java @@ -45,7 +45,9 @@ public class JobHistoryView extends Main implements HasUrlParameter { private final SignatureRepository signatureRepository; private final VerticalLayout content; - public JobHistoryView(JobRepository jobRepository, JobHistoryService jobHistoryService, PhotoRepository photoRepository, BarcodeRepository barcodeRepository, SignatureRepository signatureRepository) { + public JobHistoryView(JobRepository jobRepository, JobHistoryService jobHistoryService, + PhotoRepository photoRepository, BarcodeRepository barcodeRepository, + SignatureRepository signatureRepository) { this.jobRepository = jobRepository; this.jobHistoryService = jobHistoryService; this.photoRepository = photoRepository; @@ -53,9 +55,8 @@ public class JobHistoryView extends Main implements HasUrlParameter { this.signatureRepository = signatureRepository; setSizeFull(); - addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, - LumoUtility.FlexDirection.COLUMN, LumoUtility.Padding.MEDIUM, - LumoUtility.Gap.SMALL); + addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN, + LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL); add(new ViewToolbar("Job Historie")); @@ -96,7 +97,8 @@ public class JobHistoryView extends Main implements HasUrlParameter { content.removeAll(); // Header mit Job-Informationen - H2 header = new H2("Job Historie - " + (job.getJobNumber() != null ? job.getJobNumber() : "Unbekannte Auftragsnummer")); + H2 header = new H2( + "Job Historie - " + (job.getJobNumber() != null ? job.getJobNumber() : "Unbekannte Auftragsnummer")); content.add(header); // Job basic info for context @@ -131,12 +133,9 @@ public class JobHistoryView extends Main implements HasUrlParameter { private Div createJobInfoBox(Job job) { Div infoBox = new Div(); - infoBox.getStyle() - .set("border", "1px solid var(--lumo-contrast-20pct)") - .set("border-radius", "var(--lumo-border-radius-m)") - .set("padding", "var(--lumo-space-m)") - .set("background-color", "var(--lumo-base-color)") - .set("margin-bottom", "var(--lumo-space-m)"); + infoBox.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)") + .set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)") + .set("background-color", "var(--lumo-base-color)").set("margin-bottom", "var(--lumo-space-m)"); VerticalLayout infoContent = new VerticalLayout(); infoContent.setPadding(false); @@ -172,15 +171,11 @@ public class JobHistoryView extends Main implements HasUrlParameter { private Div createHistoryEntryCard(JobHistory entry) { Div card = new Div(); - card.getStyle() - .set("border", "1px solid var(--lumo-contrast-10pct)") - .set("border-left", "4px solid " + getTypeColor(entry.getChangeType())) - .set("border-radius", "var(--lumo-border-radius-s)") - .set("padding", "var(--lumo-space-m)") - .set("margin-bottom", "var(--lumo-space-s)") - .set("background-color", "var(--lumo-base-color)") - .set("width", "100%") - .set("box-sizing", "border-box"); + card.getStyle().set("border", "1px solid var(--lumo-contrast-10pct)") + .set("border-left", "4px solid " + getTypeColor(entry.getChangeType())) + .set("border-radius", "var(--lumo-border-radius-s)").set("padding", "var(--lumo-space-m)") + .set("margin-bottom", "var(--lumo-space-s)").set("background-color", "var(--lumo-base-color)") + .set("width", "100%").set("box-sizing", "border-box"); // Header row with icon, reason and timestamp HorizontalLayout headerRow = new HorizontalLayout(); @@ -194,9 +189,8 @@ public class JobHistoryView extends Main implements HasUrlParameter { reason.getStyle().set("font-weight", "500"); Span timestamp = new Span(formatDateTime(entry.getTimestamp())); - timestamp.getStyle() - .set("color", "var(--lumo-secondary-text-color)") - .set("font-size", "var(--lumo-font-size-s)"); + timestamp.getStyle().set("color", "var(--lumo-secondary-text-color)").set("font-size", + "var(--lumo-font-size-s)"); HorizontalLayout leftSide = new HorizontalLayout(typeIcon, reason); leftSide.setAlignItems(HorizontalLayout.Alignment.CENTER); @@ -213,17 +207,14 @@ public class JobHistoryView extends Main implements HasUrlParameter { // Description if (entry.getDescription() != null && !entry.getDescription().isBlank()) { Span description = new Span(entry.getDescription()); - description.getStyle() - .set("color", "var(--lumo-body-text-color)") - .set("margin-top", "var(--lumo-space-xs)") - .set("display", "block"); + description.getStyle().set("color", "var(--lumo-body-text-color)").set("margin-top", "var(--lumo-space-xs)") + .set("display", "block"); cardContent.add(description); } // Photo preview for photo tasks - if (entry.getChangeType() == JobHistoryType.TASK_COMPLETED && - entry.getDetails() != null && - entry.getDetails().contains("Task-Typ: PHOTO")) { + if (entry.getChangeType() == JobHistoryType.TASK_COMPLETED && entry.getDetails() != null + && entry.getDetails().contains("Task-Typ: PHOTO")) { HorizontalLayout photoPreview = createPhotoPreview(entry); if (photoPreview != null) { @@ -232,9 +223,8 @@ public class JobHistoryView extends Main implements HasUrlParameter { } // Barcode preview for barcode tasks - if (entry.getChangeType() == JobHistoryType.TASK_COMPLETED && - entry.getDetails() != null && - entry.getDetails().contains("Task-Typ: BARCODE")) { + if (entry.getChangeType() == JobHistoryType.TASK_COMPLETED && entry.getDetails() != null + && entry.getDetails().contains("Task-Typ: BARCODE")) { VerticalLayout barcodePreview = createBarcodePreview(entry); if (barcodePreview != null) { @@ -243,9 +233,8 @@ public class JobHistoryView extends Main implements HasUrlParameter { } // Signature preview for signature tasks - if (entry.getChangeType() == JobHistoryType.TASK_COMPLETED && - entry.getDetails() != null && - entry.getDetails().contains("Task-Typ: SIGNATURE")) { + if (entry.getChangeType() == JobHistoryType.TASK_COMPLETED && entry.getDetails() != null + && entry.getDetails().contains("Task-Typ: SIGNATURE")) { Div signaturePreview = createSignaturePreview(entry); if (signaturePreview != null) { @@ -256,11 +245,9 @@ public class JobHistoryView extends Main implements HasUrlParameter { // Changed by (if available) if (entry.getChangedBy() != null && !entry.getChangedBy().isBlank()) { Span changedBy = new Span("von: " + entry.getChangedBy()); - changedBy.getStyle() - .set("color", "var(--lumo-secondary-text-color)") - .set("font-size", "var(--lumo-font-size-xs)") - .set("margin-top", "var(--lumo-space-xs)") - .set("display", "block"); + changedBy.getStyle().set("color", "var(--lumo-secondary-text-color)") + .set("font-size", "var(--lumo-font-size-xs)").set("margin-top", "var(--lumo-space-xs)") + .set("display", "block"); cardContent.add(changedBy); } @@ -270,44 +257,47 @@ public class JobHistoryView extends Main implements HasUrlParameter { } private Icon getTypeIcon(JobHistoryType type) { - if (type == null) return new Icon(VaadinIcon.INFO_CIRCLE); + if (type == null) + return new Icon(VaadinIcon.INFO_CIRCLE); return switch (type) { - case CREATE -> new Icon(VaadinIcon.PLUS_CIRCLE); - case UPDATE -> new Icon(VaadinIcon.EDIT); - case STATUS_CHANGE -> new Icon(VaadinIcon.ARROW_RIGHT); - case TASK_COMPLETED -> new Icon(VaadinIcon.CHECK); - case ASSIGNMENT -> new Icon(VaadinIcon.USER); - case EXPORT -> new Icon(VaadinIcon.DOWNLOAD); - case DELETE -> new Icon(VaadinIcon.TRASH); - case SYSTEM -> new Icon(VaadinIcon.COG); - case COMMENT -> new Icon(VaadinIcon.COMMENT); - default -> new Icon(VaadinIcon.INFO_CIRCLE); + case CREATE -> new Icon(VaadinIcon.PLUS_CIRCLE); + case UPDATE -> new Icon(VaadinIcon.EDIT); + case STATUS_CHANGE -> new Icon(VaadinIcon.ARROW_RIGHT); + case TASK_COMPLETED -> new Icon(VaadinIcon.CHECK); + case ASSIGNMENT -> new Icon(VaadinIcon.USER); + case EXPORT -> new Icon(VaadinIcon.DOWNLOAD); + case DELETE -> new Icon(VaadinIcon.TRASH); + case SYSTEM -> new Icon(VaadinIcon.COG); + case COMMENT -> new Icon(VaadinIcon.COMMENT); + default -> new Icon(VaadinIcon.INFO_CIRCLE); }; } private String getTypeColor(JobHistoryType type) { - if (type == null) return "var(--lumo-contrast-60pct)"; + if (type == null) + return "var(--lumo-contrast-60pct)"; return switch (type) { - case CREATE -> "var(--lumo-success-color)"; - case UPDATE -> "var(--lumo-primary-color)"; - case STATUS_CHANGE -> "var(--lumo-contrast-color)"; - case TASK_COMPLETED -> "var(--lumo-success-color)"; - case ASSIGNMENT -> "var(--lumo-primary-color)"; - case EXPORT -> "var(--lumo-contrast-color)"; - case DELETE -> "var(--lumo-error-color)"; - case SYSTEM -> "var(--lumo-contrast-60pct)"; - case COMMENT -> "var(--lumo-primary-color)"; - default -> "var(--lumo-contrast-60pct)"; + case CREATE -> "var(--lumo-success-color)"; + case UPDATE -> "var(--lumo-primary-color)"; + case STATUS_CHANGE -> "var(--lumo-contrast-color)"; + case TASK_COMPLETED -> "var(--lumo-success-color)"; + case ASSIGNMENT -> "var(--lumo-primary-color)"; + case EXPORT -> "var(--lumo-contrast-color)"; + case DELETE -> "var(--lumo-error-color)"; + case SYSTEM -> "var(--lumo-contrast-60pct)"; + case COMMENT -> "var(--lumo-primary-color)"; + default -> "var(--lumo-contrast-60pct)"; }; } private String formatDateTime(java.time.LocalDateTime dateTime) { - if (dateTime == null) return ""; + if (dateTime == null) + return ""; try { - java.time.format.DateTimeFormatter formatter = - java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"); + java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter + .ofPattern("dd.MM.yyyy HH:mm"); return dateTime.format(formatter); } catch (Exception e) { return dateTime.toString(); @@ -315,18 +305,19 @@ public class JobHistoryView extends Main implements HasUrlParameter { } private String formatStatus(de.assecutor.votianlt.model.JobStatus status) { - if (status == null) return "Unbekannt"; + if (status == null) + return "Unbekannt"; return switch (status) { - case CREATED -> "Erstellt"; - case IN_PROGRESS -> "In Bearbeitung"; - case PICKUP_SCHEDULED -> "Abholung geplant"; - case PICKED_UP -> "Abgeholt"; - case IN_TRANSIT -> "Unterwegs"; - case DELIVERED -> "Zugestellt"; - case COMPLETED -> "Abgeschlossen"; - case CANCELLED -> "Storniert"; - default -> status.toString(); + case CREATED -> "Erstellt"; + case IN_PROGRESS -> "In Bearbeitung"; + case PICKUP_SCHEDULED -> "Abholung geplant"; + case PICKED_UP -> "Abgeholt"; + case IN_TRANSIT -> "Unterwegs"; + case DELIVERED -> "Zugestellt"; + case COMPLETED -> "Abgeschlossen"; + case CANCELLED -> "Storniert"; + default -> status.toString(); }; } @@ -351,9 +342,7 @@ public class JobHistoryView extends Main implements HasUrlParameter { HorizontalLayout photoLayout = new HorizontalLayout(); photoLayout.setSpacing(true); - photoLayout.getStyle() - .set("margin-top", "var(--lumo-space-s)") - .set("flex-wrap", "wrap"); + photoLayout.getStyle().set("margin-top", "var(--lumo-space-s)").set("flex-wrap", "wrap"); for (Photo photo : photos) { if (photo.getPhoto() != null && !photo.getPhoto().isBlank()) { @@ -395,18 +384,13 @@ public class JobHistoryView extends Main implements HasUrlParameter { private com.vaadin.flow.component.html.Image createPhotoThumbnail(String base64Photo) { try { - String imageData = base64Photo.startsWith("data:") - ? base64Photo - : "data:image/jpeg;base64," + base64Photo; + String imageData = base64Photo.startsWith("data:") ? base64Photo : "data:image/jpeg;base64," + base64Photo; - com.vaadin.flow.component.html.Image thumbnail = new com.vaadin.flow.component.html.Image(imageData, "Foto"); - thumbnail.getStyle() - .set("width", "100px") - .set("height", "100px") - .set("object-fit", "cover") - .set("border-radius", "var(--lumo-border-radius-s)") - .set("border", "1px solid var(--lumo-contrast-20pct)") - .set("cursor", "pointer"); + com.vaadin.flow.component.html.Image thumbnail = new com.vaadin.flow.component.html.Image(imageData, + "Foto"); + thumbnail.getStyle().set("width", "100px").set("height", "100px").set("object-fit", "cover") + .set("border-radius", "var(--lumo-border-radius-s)") + .set("border", "1px solid var(--lumo-contrast-20pct)").set("cursor", "pointer"); return thumbnail; } catch (Exception e) { @@ -424,15 +408,11 @@ public class JobHistoryView extends Main implements HasUrlParameter { photoDialog.setCloseOnEsc(true); try { - String imageData = base64Photo.startsWith("data:") - ? base64Photo - : "data:image/jpeg;base64," + base64Photo; + String imageData = base64Photo.startsWith("data:") ? base64Photo : "data:image/jpeg;base64," + base64Photo; - com.vaadin.flow.component.html.Image enlargedImage = new com.vaadin.flow.component.html.Image(imageData, "Vergrößertes Foto"); - enlargedImage.getStyle() - .set("max-width", "100%") - .set("max-height", "100%") - .set("object-fit", "contain"); + com.vaadin.flow.component.html.Image enlargedImage = new com.vaadin.flow.component.html.Image(imageData, + "Vergrößertes Foto"); + enlargedImage.getStyle().set("max-width", "100%").set("max-height", "100%").set("object-fit", "contain"); VerticalLayout dialogContent = new VerticalLayout(enlargedImage); dialogContent.setAlignItems(VerticalLayout.Alignment.CENTER); @@ -470,8 +450,7 @@ public class JobHistoryView extends Main implements HasUrlParameter { VerticalLayout barcodeLayout = new VerticalLayout(); barcodeLayout.setPadding(false); barcodeLayout.setSpacing(true); - barcodeLayout.getStyle() - .set("margin-top", "var(--lumo-space-s)"); + barcodeLayout.getStyle().set("margin-top", "var(--lumo-space-s)"); for (Barcode barcode : barcodes) { if (barcode.getBarcode() != null && !barcode.getBarcode().isBlank()) { @@ -490,15 +469,11 @@ public class JobHistoryView extends Main implements HasUrlParameter { private Div createBarcodeBox(String barcodeValue) { Div barcodeBox = new Div(); - barcodeBox.getStyle() - .set("border", "1px solid var(--lumo-contrast-20pct)") - .set("border-radius", "var(--lumo-border-radius-s)") - .set("padding", "var(--lumo-space-xs)") - .set("background-color", "var(--lumo-contrast-5pct)") - .set("font-family", "monospace") - .set("font-size", "var(--lumo-font-size-s)") - .set("margin-bottom", "var(--lumo-space-xs)") - .set("word-break", "break-all"); + barcodeBox.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)") + .set("border-radius", "var(--lumo-border-radius-s)").set("padding", "var(--lumo-space-xs)") + .set("background-color", "var(--lumo-contrast-5pct)").set("font-family", "monospace") + .set("font-size", "var(--lumo-font-size-s)").set("margin-bottom", "var(--lumo-space-xs)") + .set("word-break", "break-all"); barcodeBox.add(new Span(barcodeValue)); return barcodeBox; @@ -531,22 +506,16 @@ public class JobHistoryView extends Main implements HasUrlParameter { } Div previewContainer = new Div(); - previewContainer.getStyle() - .set("margin-top", "var(--lumo-space-s)") - .set("border", "1px solid var(--lumo-contrast-20pct)") - .set("border-radius", "var(--lumo-border-radius-s)") - .set("padding", "var(--lumo-space-xs)") - .set("background-color", "var(--lumo-base-color)") - .set("cursor", "pointer") - .set("width", "200px") - .set("height", "100px") - .set("overflow", "hidden") - .set("display", "flex") - .set("align-items", "center") - .set("justify-content", "center"); + previewContainer.getStyle().set("margin-top", "var(--lumo-space-s)") + .set("border", "1px solid var(--lumo-contrast-20pct)") + .set("border-radius", "var(--lumo-border-radius-s)").set("padding", "var(--lumo-space-xs)") + .set("background-color", "var(--lumo-base-color)").set("cursor", "pointer").set("width", "200px") + .set("height", "100px").set("overflow", "hidden").set("display", "flex") + .set("align-items", "center").set("justify-content", "center"); // Create responsive SVG for preview - com.vaadin.flow.component.Html signatureSvg = createResponsiveSignatureSvg(signature.getSignatureSvg(), "100%", "100%"); + com.vaadin.flow.component.Html signatureSvg = createResponsiveSignatureSvg(signature.getSignatureSvg(), + "100%", "100%"); previewContainer.add(signatureSvg); // Add click listener for enlarged view @@ -560,27 +529,28 @@ public class JobHistoryView extends Main implements HasUrlParameter { } } - private com.vaadin.flow.component.Html createResponsiveSignatureSvg(String svgContent, String width, String height) { + private com.vaadin.flow.component.Html createResponsiveSignatureSvg(String svgContent, String width, + String height) { // Make SVG responsive by ensuring proper viewBox and dimensions String responsiveSvg = svgContent; if (!responsiveSvg.contains("viewBox")) { // Try to extract width and height from SVG and create viewBox responsiveSvg = responsiveSvg.replaceFirst("" + responsiveSvg + ""); + return new com.vaadin.flow.component.Html( + "
" + responsiveSvg + "
"); } private void showEnlargedSignature(String svgContent) { diff --git a/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java b/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java index 54e669e..5a9e40f 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java @@ -61,13 +61,9 @@ public class JobSummaryView extends Main implements HasUrlParameter { private final VerticalLayout content; private final List
taskCards = new ArrayList<>(); - public JobSummaryView(JobRepository jobRepository, - CargoItemRepository cargoItemRepository, - TaskRepository taskRepository, - SignatureRepository signatureRepository, - BarcodeRepository barcodeRepository, - PhotoRepository photoRepository, - AppUserService appUserService) { + public JobSummaryView(JobRepository jobRepository, CargoItemRepository cargoItemRepository, + TaskRepository taskRepository, SignatureRepository signatureRepository, BarcodeRepository barcodeRepository, + PhotoRepository photoRepository, AppUserService appUserService) { this.jobRepository = jobRepository; this.cargoItemRepository = cargoItemRepository; this.taskRepository = taskRepository; @@ -77,9 +73,8 @@ public class JobSummaryView extends Main implements HasUrlParameter { this.appUserService = appUserService; setSizeFull(); - addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, - LumoUtility.FlexDirection.COLUMN, LumoUtility.Padding.MEDIUM, - LumoUtility.Gap.SMALL); + addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN, + LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL); content = new VerticalLayout(); content.setSpacing(true); @@ -145,22 +140,19 @@ public class JobSummaryView extends Main implements HasUrlParameter { VerticalLayout pickupBox = borderedBox(); pickupBox.add(new H3("Abholung " + (job.getPickupDate() != null ? formatLocalDate(job.getPickupDate()) : ""))); pickupBox.add(new Span(valueOrEmpty(job.getPickupCompany()))); - pickupBox.add(new Span(valueOrEmpty(job.getPickupSalutation()) - + (job.getPickupSalutation() != null ? " " : "") - + valueOrEmpty(job.getPickupFirstName()) - + (job.getPickupFirstName() != null ? " " : "") + pickupBox.add(new Span(valueOrEmpty(job.getPickupSalutation()) + (job.getPickupSalutation() != null ? " " : "") + + valueOrEmpty(job.getPickupFirstName()) + (job.getPickupFirstName() != null ? " " : "") + valueOrEmpty(job.getPickupLastName()))); pickupBox.add(new Span(concatAddress(job.getPickupStreet(), job.getPickupHouseNumber()))); pickupBox.add(new Span(concatZipCity(job.getPickupZip(), job.getPickupCity()))); VerticalLayout deliveryBox = borderedBox(); - deliveryBox.add(new H3("Lieferung " + (job.getDeliveryDate() != null ? formatLocalDate(job.getDeliveryDate()) : ""))); + deliveryBox.add( + new H3("Lieferung " + (job.getDeliveryDate() != null ? formatLocalDate(job.getDeliveryDate()) : ""))); deliveryBox.add(new Span(valueOrEmpty(job.getDeliveryCompany()))); deliveryBox.add(new Span(valueOrEmpty(job.getDeliverySalutation()) - + (job.getDeliverySalutation() != null ? " " : "") - + valueOrEmpty(job.getDeliveryFirstName()) - + (job.getDeliveryFirstName() != null ? " " : "") - + valueOrEmpty(job.getDeliveryLastName()))); + + (job.getDeliverySalutation() != null ? " " : "") + valueOrEmpty(job.getDeliveryFirstName()) + + (job.getDeliveryFirstName() != null ? " " : "") + valueOrEmpty(job.getDeliveryLastName()))); deliveryBox.add(new Span(concatAddress(job.getDeliveryStreet(), job.getDeliveryHouseNumber()))); deliveryBox.add(new Span(concatZipCity(job.getDeliveryZip(), job.getDeliveryCity()))); @@ -208,13 +200,16 @@ public class JobSummaryView extends Main implements HasUrlParameter { cargoBox.add(new Span("Keine Frachtangaben")); } else { for (CargoItem ci : cargoItems) { - if (ci == null) continue; + if (ci == null) + continue; String desc = ci.getDescription(); Integer qty = ci.getQuantity(); String dims = dimString(ci); String weight = ci.getWeightKg() != null ? ci.getWeightKg() + " kg" : ""; - String line = (qty != null ? qty + " x " : "") + (desc != null ? desc : "") + (dims.isBlank() ? "" : " " + dims) + (weight.isBlank() ? "" : " " + weight); - if (!line.isBlank()) cargoBox.add(new Span(line)); + String line = (qty != null ? qty + " x " : "") + (desc != null ? desc : "") + + (dims.isBlank() ? "" : " " + dims) + (weight.isBlank() ? "" : " " + weight); + if (!line.isBlank()) + cargoBox.add(new Span(line)); } } @@ -252,12 +247,17 @@ public class JobSummaryView extends Main implements HasUrlParameter { private String formatLocalDate(java.time.LocalDate date) { try { - java.time.format.DateTimeFormatter fmt = java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy").withLocale(Locale.GERMANY); + java.time.format.DateTimeFormatter fmt = java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy") + .withLocale(Locale.GERMANY); return date.format(fmt); - } catch (Exception e) { return ""; } + } catch (Exception e) { + return ""; + } } - private String valueOrEmpty(String v) { return v == null ? "" : v; } + private String valueOrEmpty(String v) { + return v == null ? "" : v; + } private String concatAddress(String street, String house) { String s = valueOrEmpty(street); @@ -268,7 +268,8 @@ public class JobSummaryView extends Main implements HasUrlParameter { private String concatZipCity(String zip, String city) { String z = valueOrEmpty(zip); String c = valueOrEmpty(city); - if (!z.isBlank() && !c.isBlank()) return z + " " + c; + if (!z.isBlank() && !c.isBlank()) + return z + " " + c; return (z + " " + c).trim(); } @@ -276,8 +277,8 @@ public class JobSummaryView extends Main implements HasUrlParameter { String len = ci.getLengthMm() != null ? ci.getLengthMm().intValue() + " mm" : ""; String wid = ci.getWidthMm() != null ? ci.getWidthMm().intValue() + " mm" : ""; String hei = ci.getHeightMm() != null ? ci.getHeightMm().intValue() + " mm" : ""; - String combined = String.join(" x ", java.util.stream.Stream.of(len, wid, hei) - .filter(s -> !s.isBlank()).toList()); + String combined = String.join(" x ", + java.util.stream.Stream.of(len, wid, hei).filter(s -> !s.isBlank()).toList()); return combined.isBlank() ? "" : combined; } @@ -293,19 +294,26 @@ public class JobSummaryView extends Main implements HasUrlParameter { if (au != null) { String fn = au.getVorname(); String ln = au.getNachname(); - String name = (fn != null ? fn : "").trim() + (fn != null && ln != null ? " " : "") + (ln != null ? ln : ""); - if (!name.isBlank()) return name; - if (au.getBezeichnung() != null && !au.getBezeichnung().isBlank()) return au.getBezeichnung(); - if (au.getEmail() != null && !au.getEmail().isBlank()) return au.getEmail(); + String name = (fn != null ? fn : "").trim() + (fn != null && ln != null ? " " : "") + + (ln != null ? ln : ""); + if (!name.isBlank()) + return name; + if (au.getBezeichnung() != null && !au.getBezeichnung().isBlank()) + return au.getBezeichnung(); + if (au.getEmail() != null && !au.getEmail().isBlank()) + return au.getEmail(); } - } catch (Exception ignored) { } + } catch (Exception ignored) { + } return appUserIdString; // Fallback: show raw string if lookup fails } private void addRouteMap(Job job) { // Baue Adress-Strings - String origin = (concatAddress(job.getPickupStreet(), job.getPickupHouseNumber()) + ", " + concatZipCity(job.getPickupZip(), job.getPickupCity())).trim(); - String destination = (concatAddress(job.getDeliveryStreet(), job.getDeliveryHouseNumber()) + ", " + concatZipCity(job.getDeliveryZip(), job.getDeliveryCity())).trim(); + String origin = (concatAddress(job.getPickupStreet(), job.getPickupHouseNumber()) + ", " + + concatZipCity(job.getPickupZip(), job.getPickupCity())).trim(); + String destination = (concatAddress(job.getDeliveryStreet(), job.getDeliveryHouseNumber()) + ", " + + concatZipCity(job.getDeliveryZip(), job.getDeliveryCity())).trim(); if (origin.isBlank() || destination.isBlank()) { // Wenn nicht genug Daten vorhanden sind, Karte nicht anzeigen @@ -328,66 +336,48 @@ public class JobSummaryView extends Main implements HasUrlParameter { content.add(map, routeInfo); - String js = ( - "(function(){" + - " var host = $0; var infoEl = $1;" + - " function init(){" + - " var map = new google.maps.Map(host, {center: {lat: 51.163, lng: 10.447}, zoom: 6, mapTypeControl: false});" + - " var trafficLayer = new google.maps.TrafficLayer(); trafficLayer.setMap(map);" + - " var ds = new google.maps.DirectionsService();" + - " ds.route({" + - " origin: '" + escapeJs(origin) + "'," + - " destination: '" + escapeJs(destination) + "'," + - " travelMode: google.maps.TravelMode.DRIVING," + - " provideRouteAlternatives: true," + - " drivingOptions: { departureTime: new Date(), trafficModel: google.maps.TrafficModel.BEST_GUESS }" + - " }, function(res, status){ if(status==='OK'){ " + - " infoEl.innerHTML='';" + - " var bounds = new google.maps.LatLngBounds();" + - " var renderers = []; var polylines = [];" + - " res.routes.forEach(function(route, idx){" + - " var dr = new google.maps.DirectionsRenderer({map: map, preserveViewport: idx>0, suppressMarkers:false, suppressPolylines:true});" + - " dr.setRouteIndex(idx); dr.setDirections(res);" + - " renderers.push(dr);" + - " var path = route.overview_path || [];" + - " var poly = new google.maps.Polyline({path: path, strokeColor: idx===0?'#1976d2':'#90caf9', strokeOpacity: 0.95, strokeWeight: idx===0?6:4});" + - " poly.setMap(map); polylines.push(poly);" + - " var leg = route.legs && route.legs[0];" + - " if (leg) {" + - " var dur = leg.duration ? leg.duration.text : '';" + - " var durT = leg.duration_in_traffic ? leg.duration_in_traffic.text : '';" + - " var dist = leg.distance ? leg.distance.text : '';" + - " var alt = (idx===0?'Schnellste Route':'Alternative '+idx);" + - " var row = document.createElement('div'); row.style.margin='4px 0'; row.style.cursor='pointer';" + - " row.textContent = alt + ': ' + dist + ' • Dauer: ' + dur + (durT?(' (mit Verkehr: '+durT+')'):'');" + - " row.onmouseenter = function(){" + - " polylines.forEach(function(p,i){ p.setOptions({strokeColor: i===0?'#90caf9':'#e3f2fd', strokeOpacity: 0.6, strokeWeight: 3}); });" + - " poly.setOptions({strokeColor:'#0d47a1', strokeOpacity:1, strokeWeight:7});" + - " };" + - " row.onmouseleave = function(){" + - " polylines.forEach(function(p,i){ p.setOptions({strokeColor: i===0?'#1976d2':'#90caf9', strokeOpacity:0.95, strokeWeight: i===0?6:4}); });" + - " };" + - " infoEl.appendChild(row);" + - " if (path && path.length){ path.forEach(function(pt){ bounds.extend(pt); }); }" + - " }" + - " });" + - " if (!bounds.isEmpty()) { map.fitBounds(bounds); }" + - " }});" + - " }" + - " if (!(window.google && window.google.maps)) {" + - " var s=document.createElement('script');" + - " s.src='https://maps.googleapis.com/maps/api/js?key=AIzaSyDnbitL06iLp3elmj-WtPudCykX9xvXcVE&libraries=places';" + - " s.onload=init; document.head.appendChild(s);" + - " } else { init(); }" + - "})();" - ); + String js = ("(function(){" + " var host = $0; var infoEl = $1;" + " function init(){" + + " var map = new google.maps.Map(host, {center: {lat: 51.163, lng: 10.447}, zoom: 6, mapTypeControl: false});" + + " var trafficLayer = new google.maps.TrafficLayer(); trafficLayer.setMap(map);" + + " var ds = new google.maps.DirectionsService();" + " ds.route({" + " origin: '" + + escapeJs(origin) + "'," + " destination: '" + escapeJs(destination) + "'," + + " travelMode: google.maps.TravelMode.DRIVING," + " provideRouteAlternatives: true," + + " drivingOptions: { departureTime: new Date(), trafficModel: google.maps.TrafficModel.BEST_GUESS }" + + " }, function(res, status){ if(status==='OK'){ " + " infoEl.innerHTML='';" + + " var bounds = new google.maps.LatLngBounds();" + + " var renderers = []; var polylines = [];" + " res.routes.forEach(function(route, idx){" + + " var dr = new google.maps.DirectionsRenderer({map: map, preserveViewport: idx>0, suppressMarkers:false, suppressPolylines:true});" + + " dr.setRouteIndex(idx); dr.setDirections(res);" + " renderers.push(dr);" + + " var path = route.overview_path || [];" + + " var poly = new google.maps.Polyline({path: path, strokeColor: idx===0?'#1976d2':'#90caf9', strokeOpacity: 0.95, strokeWeight: idx===0?6:4});" + + " poly.setMap(map); polylines.push(poly);" + + " var leg = route.legs && route.legs[0];" + " if (leg) {" + + " var dur = leg.duration ? leg.duration.text : '';" + + " var durT = leg.duration_in_traffic ? leg.duration_in_traffic.text : '';" + + " var dist = leg.distance ? leg.distance.text : '';" + + " var alt = (idx===0?'Schnellste Route':'Alternative '+idx);" + + " var row = document.createElement('div'); row.style.margin='4px 0'; row.style.cursor='pointer';" + + " row.textContent = alt + ': ' + dist + ' • Dauer: ' + dur + (durT?(' (mit Verkehr: '+durT+')'):'');" + + " row.onmouseenter = function(){" + + " polylines.forEach(function(p,i){ p.setOptions({strokeColor: i===0?'#90caf9':'#e3f2fd', strokeOpacity: 0.6, strokeWeight: 3}); });" + + " poly.setOptions({strokeColor:'#0d47a1', strokeOpacity:1, strokeWeight:7});" + + " };" + " row.onmouseleave = function(){" + + " polylines.forEach(function(p,i){ p.setOptions({strokeColor: i===0?'#1976d2':'#90caf9', strokeOpacity:0.95, strokeWeight: i===0?6:4}); });" + + " };" + " infoEl.appendChild(row);" + + " if (path && path.length){ path.forEach(function(pt){ bounds.extend(pt); }); }" + + " }" + " });" + " if (!bounds.isEmpty()) { map.fitBounds(bounds); }" + + " }});" + " }" + " if (!(window.google && window.google.maps)) {" + + " var s=document.createElement('script');" + + " s.src='https://maps.googleapis.com/maps/api/js?key=AIzaSyDnbitL06iLp3elmj-WtPudCykX9xvXcVE&libraries=places';" + + " s.onload=init; document.head.appendChild(s);" + " } else { init(); }" + "})();"); map.getElement().executeJs(js, map.getElement(), routeInfo.getElement()); } // Hilfsfunktion zum einfachen Escapen von JS-Zeichen in Strings private String escapeJs(String s) { - if (s == null) return ""; + if (s == null) + return ""; return s.replace("\\", "\\\\").replace("'", "\\'").replace("\n", " ").replace("\r", " "); } @@ -426,7 +416,7 @@ public class JobSummaryView extends Main implements HasUrlParameter { // Completion details if completed if (task.isCompleted()) { - dialogContent.add(new Span("")); // Spacer + dialogContent.add(new Span("")); // Spacer if (task.getCompletedAt() != null) { dialogContent.add(new Span("Abgeschlossen am: " + formatDateTime(task.getCompletedAt()))); } @@ -468,7 +458,8 @@ public class JobSummaryView extends Main implements HasUrlParameter { if (photoTask.getMinPhotoCount() != null || photoTask.getMaxPhotoCount() != null) { String photoInfo = "Fotos: "; if (photoTask.getMinPhotoCount() != null && photoTask.getMaxPhotoCount() != null) { - photoInfo += photoTask.getMinPhotoCount() + " - " + photoTask.getMaxPhotoCount() + " Fotos erforderlich"; + photoInfo += photoTask.getMinPhotoCount() + " - " + photoTask.getMaxPhotoCount() + + " Fotos erforderlich"; } else if (photoTask.getMinPhotoCount() != null) { photoInfo += "Mindestens " + photoTask.getMinPhotoCount() + " Fotos erforderlich"; } else if (photoTask.getMaxPhotoCount() != null) { @@ -484,7 +475,7 @@ public class JobSummaryView extends Main implements HasUrlParameter { List photos = photoRepository.findByTaskId(taskId); if (!photos.isEmpty()) { - content.add(new Span("")); // Spacer + content.add(new Span("")); // Spacer // Collect all photos from all Photo entries List allPhotos = new ArrayList<>(); @@ -520,7 +511,7 @@ public class JobSummaryView extends Main implements HasUrlParameter { List signatures = signatureRepository.findByTaskId(taskId); if (!signatures.isEmpty()) { - content.add(new Span("")); // Spacer + content.add(new Span("")); // Spacer content.add(new Span("Gespeicherte Unterschrift:")); // Display the latest signature (assuming one signature per task) @@ -530,17 +521,12 @@ public class JobSummaryView extends Main implements HasUrlParameter { if (svgContent != null && !svgContent.isBlank()) { // Create a div to hold the SVG Div svgContainer = new Div(); - svgContainer.getStyle() - .set("border", "1px solid var(--lumo-contrast-20pct)") - .set("border-radius", "var(--lumo-border-radius-m)") - .set("padding", "var(--lumo-space-s)") - .set("background-color", "white") - .set("width", "100%") - .set("max-width", "450px") - .set("overflow", "hidden") - .set("display", "flex") - .set("align-items", "center") - .set("justify-content", "center"); + svgContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)") + .set("border-radius", "var(--lumo-border-radius-m)") + .set("padding", "var(--lumo-space-s)").set("background-color", "white") + .set("width", "100%").set("max-width", "450px").set("overflow", "hidden") + .set("display", "flex").set("align-items", "center") + .set("justify-content", "center"); // Process SVG to make it responsive String responsiveSvg = makeResponsiveSvg(svgContent); @@ -562,7 +548,7 @@ public class JobSummaryView extends Main implements HasUrlParameter { List barcodes = barcodeRepository.findByTaskId(taskId); if (!barcodes.isEmpty()) { - content.add(new Span("")); // Spacer + content.add(new Span("")); // Spacer content.add(new Span("Gescannte Barcodes (" + barcodes.size() + "):")); // Display all scanned barcodes @@ -573,15 +559,12 @@ public class JobSummaryView extends Main implements HasUrlParameter { if (barcodeValue != null && !barcodeValue.isBlank()) { // Create a styled container for each barcode Div barcodeContainer = new Div(); - barcodeContainer.getStyle() - .set("border", "1px solid var(--lumo-contrast-20pct)") - .set("border-radius", "var(--lumo-border-radius-s)") - .set("padding", "var(--lumo-space-s)") - .set("margin", "var(--lumo-space-xs) 0") - .set("background-color", "var(--lumo-contrast-5pct)") - .set("font-family", "monospace") - .set("font-size", "var(--lumo-font-size-s)") - .set("word-break", "break-all"); + barcodeContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)") + .set("border-radius", "var(--lumo-border-radius-s)") + .set("padding", "var(--lumo-space-s)").set("margin", "var(--lumo-space-xs) 0") + .set("background-color", "var(--lumo-contrast-5pct)") + .set("font-family", "monospace").set("font-size", "var(--lumo-font-size-s)") + .set("word-break", "break-all"); Span barcodeSpan = new Span((i + 1) + ". " + barcodeValue); barcodeContainer.add(barcodeSpan); @@ -598,7 +581,8 @@ public class JobSummaryView extends Main implements HasUrlParameter { private String formatDateTime(java.time.LocalDateTime dateTime) { try { - java.time.format.DateTimeFormatter fmt = java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm").withLocale(Locale.GERMANY); + java.time.format.DateTimeFormatter fmt = java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm") + .withLocale(Locale.GERMANY); return dateTime.format(fmt); } catch (Exception e) { return dateTime.toString(); @@ -609,39 +593,28 @@ public class JobSummaryView extends Main implements HasUrlParameter { Div taskCard = new Div(); // Card styling with fixed width - taskCard.getStyle() - .set("border", "1px solid var(--lumo-contrast-20pct)") - .set("border-radius", "var(--lumo-border-radius-m)") - .set("padding", "var(--lumo-space-m)") - .set("margin", "var(--lumo-space-xs) 0") - .set("background-color", "var(--lumo-base-color)") - .set("cursor", "pointer") - .set("transition", "all 0.2s ease") - .set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)") - .set("display", "flex") - .set("align-items", "center") - .set("gap", "var(--lumo-space-m)") - .set("width", "100%") - .set("box-sizing", "border-box"); + taskCard.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)") + .set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)") + .set("margin", "var(--lumo-space-xs) 0").set("background-color", "var(--lumo-base-color)") + .set("cursor", "pointer").set("transition", "all 0.2s ease") + .set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)").set("display", "flex").set("align-items", "center") + .set("gap", "var(--lumo-space-m)").set("width", "100%").set("box-sizing", "border-box"); // Hover effects taskCard.getElement().addEventListener("mouseenter", e -> { - taskCard.getStyle() - .set("transform", "translateY(-2px)") - .set("box-shadow", "0 4px 12px rgba(0, 0, 0, 0.15)") - .set("border-color", "var(--lumo-primary-color-50pct)"); + taskCard.getStyle().set("transform", "translateY(-2px)").set("box-shadow", "0 4px 12px rgba(0, 0, 0, 0.15)") + .set("border-color", "var(--lumo-primary-color-50pct)"); }); taskCard.getElement().addEventListener("mouseleave", e -> { - taskCard.getStyle() - .set("transform", "translateY(0)") - .set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)") - .set("border-color", "var(--lumo-contrast-20pct)"); + taskCard.getStyle().set("transform", "translateY(0)").set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)") + .set("border-color", "var(--lumo-contrast-20pct)"); }); // Task icon based on type Icon taskIcon = getTaskIcon(task); - taskIcon.getStyle().set("color", task.isCompleted() ? "var(--lumo-success-color)" : "var(--lumo-primary-color)"); + taskIcon.getStyle().set("color", + task.isCompleted() ? "var(--lumo-success-color)" : "var(--lumo-primary-color)"); // Task content VerticalLayout taskContent = new VerticalLayout(); @@ -652,27 +625,20 @@ public class JobSummaryView extends Main implements HasUrlParameter { // Task name with order number (display as 1-based instead of 0-based) String taskNameWithOrder = (task.getTaskOrder() != null ? (task.getTaskOrder() + 1) + ". " : "") + displayName; Span taskName = new Span(taskNameWithOrder); - taskName.getStyle() - .set("font-weight", "500") - .set("font-size", "var(--lumo-font-size-m)") - .set("color", task.isCompleted() ? "var(--lumo-success-text-color)" : "var(--lumo-body-text-color)"); + taskName.getStyle().set("font-weight", "500").set("font-size", "var(--lumo-font-size-m)").set("color", + task.isCompleted() ? "var(--lumo-success-text-color)" : "var(--lumo-body-text-color)"); // Task status/description Span taskDescription = new Span(getTaskDescription(task)); - taskDescription.getStyle() - .set("font-size", "var(--lumo-font-size-s)") - .set("color", "var(--lumo-secondary-text-color)") - .set("margin-top", "var(--lumo-space-xs)"); + taskDescription.getStyle().set("font-size", "var(--lumo-font-size-s)") + .set("color", "var(--lumo-secondary-text-color)").set("margin-top", "var(--lumo-space-xs)"); taskContent.add(taskName, taskDescription); // Status indicator Div statusIndicator = new Div(); - statusIndicator.getStyle() - .set("width", "8px") - .set("height", "8px") - .set("border-radius", "50%") - .set("background-color", task.isCompleted() ? "var(--lumo-success-color)" : "var(--lumo-error-color)"); + statusIndicator.getStyle().set("width", "8px").set("height", "8px").set("border-radius", "50%") + .set("background-color", task.isCompleted() ? "var(--lumo-success-color)" : "var(--lumo-error-color)"); taskCard.add(taskIcon, taskContent, statusIndicator); @@ -680,10 +646,8 @@ public class JobSummaryView extends Main implements HasUrlParameter { taskCard.addClickListener(event -> { showTaskDetailsDialog(task); // Reset hover state after dialog interaction - taskCard.getStyle() - .set("transform", "translateY(0)") - .set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)") - .set("border-color", "var(--lumo-contrast-20pct)"); + taskCard.getStyle().set("transform", "translateY(0)").set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)") + .set("border-color", "var(--lumo-contrast-20pct)"); }); return taskCard; @@ -707,7 +671,9 @@ public class JobSummaryView extends Main implements HasUrlParameter { private String getTaskDescription(BaseTask task) { if (task.isCompleted()) { - return "Abgeschlossen" + (task.getCompletedAt() != null ? " am " + formatLocalDate(task.getCompletedAt().toLocalDate()) : ""); + return "Abgeschlossen" + + (task.getCompletedAt() != null ? " am " + formatLocalDate(task.getCompletedAt().toLocalDate()) + : ""); } if (task instanceof TodoListTask todoTask) { @@ -717,9 +683,11 @@ public class JobSummaryView extends Main implements HasUrlParameter { if (photoTask.getMinPhotoCount() != null && photoTask.getMaxPhotoCount() != null) { return photoTask.getMinPhotoCount() + "-" + photoTask.getMaxPhotoCount() + " Fotos erforderlich"; } else if (photoTask.getMinPhotoCount() != null) { - return "Mind. " + photoTask.getMinPhotoCount() + " Foto" + (photoTask.getMinPhotoCount() != 1 ? "s" : ""); + return "Mind. " + photoTask.getMinPhotoCount() + " Foto" + + (photoTask.getMinPhotoCount() != 1 ? "s" : ""); } else if (photoTask.getMaxPhotoCount() != null) { - return "Max. " + photoTask.getMaxPhotoCount() + " Foto" + (photoTask.getMaxPhotoCount() != 1 ? "s" : ""); + return "Max. " + photoTask.getMaxPhotoCount() + " Foto" + + (photoTask.getMaxPhotoCount() != 1 ? "s" : ""); } else { return "Foto erforderlich"; } @@ -740,83 +708,56 @@ public class JobSummaryView extends Main implements HasUrlParameter { private Div createPhotoGallery(List photos) { Div galleryContainer = new Div(); - galleryContainer.getStyle() - .set("border", "1px solid var(--lumo-contrast-20pct)") - .set("border-radius", "var(--lumo-border-radius-m)") - .set("padding", "var(--lumo-space-m)") - .set("background-color", "white") - .set("max-width", "600px") - .set("min-height", "500px") - .set("height", "500px") - .set("position", "relative") - .set("display", "flex") - .set("align-items", "center") - .set("justify-content", "center"); + galleryContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)") + .set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)") + .set("background-color", "white").set("max-width", "600px").set("min-height", "500px") + .set("height", "500px").set("position", "relative").set("display", "flex").set("align-items", "center") + .set("justify-content", "center"); if (photos.size() == 1) { // Single photo - no navigation needed Div photoContainer = createPhotoContainer(photos.get(0)); - photoContainer.getStyle() - .set("flex", "1") - .set("display", "flex") - .set("align-items", "center") - .set("justify-content", "center"); + photoContainer.getStyle().set("flex", "1").set("display", "flex").set("align-items", "center") + .set("justify-content", "center"); galleryContainer.add(photoContainer); } else { // Multiple photos - add navigation - final int[] currentIndex = {0}; // Use array to make it effectively final + final int[] currentIndex = { 0 }; // Use array to make it effectively final // Photo counter Span photoCounter = new Span((currentIndex[0] + 1) + " / " + photos.size()); - photoCounter.getStyle() - .set("position", "absolute") - .set("top", "var(--lumo-space-s)") - .set("right", "var(--lumo-space-s)") - .set("background-color", "rgba(0, 0, 0, 0.6)") - .set("color", "white") - .set("padding", "var(--lumo-space-xs) var(--lumo-space-s)") - .set("border-radius", "var(--lumo-border-radius-s)") - .set("font-size", "var(--lumo-font-size-s)") - .set("z-index", "10"); + photoCounter.getStyle().set("position", "absolute").set("top", "var(--lumo-space-s)") + .set("right", "var(--lumo-space-s)").set("background-color", "rgba(0, 0, 0, 0.6)") + .set("color", "white").set("padding", "var(--lumo-space-xs) var(--lumo-space-s)") + .set("border-radius", "var(--lumo-border-radius-s)").set("font-size", "var(--lumo-font-size-s)") + .set("z-index", "10"); // Photo container Div photoContainer = createPhotoContainer(photos.get(0)); - photoContainer.getStyle() - .set("margin", "0 40px") // Space for buttons - .set("flex", "1") - .set("display", "flex") - .set("align-items", "center") - .set("justify-content", "center"); + photoContainer.getStyle().set("margin", "0 40px") // Space for buttons + .set("flex", "1").set("display", "flex").set("align-items", "center") + .set("justify-content", "center"); // Previous button Button prevButton = new Button(new Icon(VaadinIcon.CHEVRON_LEFT)); prevButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ICON); - prevButton.getStyle() - .set("position", "absolute") - .set("left", "var(--lumo-space-s)") - .set("top", "50%") - .set("transform", "translateY(-50%)") - .set("background-color", "rgba(255, 255, 255, 0.8)") - .set("border-radius", "50%") - .set("z-index", "10"); + prevButton.getStyle().set("position", "absolute").set("left", "var(--lumo-space-s)").set("top", "50%") + .set("transform", "translateY(-50%)").set("background-color", "rgba(255, 255, 255, 0.8)") + .set("border-radius", "50%").set("z-index", "10"); // Next button Button nextButton = new Button(new Icon(VaadinIcon.CHEVRON_RIGHT)); nextButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ICON); - nextButton.getStyle() - .set("position", "absolute") - .set("right", "var(--lumo-space-s)") - .set("top", "50%") - .set("transform", "translateY(-50%)") - .set("background-color", "rgba(255, 255, 255, 0.8)") - .set("border-radius", "50%") - .set("z-index", "10"); + nextButton.getStyle().set("position", "absolute").set("right", "var(--lumo-space-s)").set("top", "50%") + .set("transform", "translateY(-50%)").set("background-color", "rgba(255, 255, 255, 0.8)") + .set("border-radius", "50%").set("z-index", "10"); // Navigation logic prevButton.addClickListener(e -> { if (currentIndex[0] > 0) { currentIndex[0]--; - updatePhotoDisplay(photoContainer, photos.get(currentIndex[0]), photoCounter, currentIndex[0] + 1, photos.size()); + updatePhotoDisplay(photoContainer, photos.get(currentIndex[0]), photoCounter, currentIndex[0] + 1, + photos.size()); } prevButton.setEnabled(currentIndex[0] > 0); nextButton.setEnabled(currentIndex[0] < photos.size() - 1); @@ -825,7 +766,8 @@ public class JobSummaryView extends Main implements HasUrlParameter { nextButton.addClickListener(e -> { if (currentIndex[0] < photos.size() - 1) { currentIndex[0]++; - updatePhotoDisplay(photoContainer, photos.get(currentIndex[0]), photoCounter, currentIndex[0] + 1, photos.size()); + updatePhotoDisplay(photoContainer, photos.get(currentIndex[0]), photoCounter, currentIndex[0] + 1, + photos.size()); } prevButton.setEnabled(currentIndex[0] > 0); nextButton.setEnabled(currentIndex[0] < photos.size() - 1); @@ -843,19 +785,14 @@ public class JobSummaryView extends Main implements HasUrlParameter { private Div createPhotoContainer(String base64Photo) { Div photoContainer = new Div(); - photoContainer.getStyle() - .set("width", "100%") - .set("height", "100%") - .set("display", "flex") - .set("align-items", "center") - .set("justify-content", "center") - .set("overflow", "hidden"); + photoContainer.getStyle().set("width", "100%").set("height", "100%").set("display", "flex") + .set("align-items", "center").set("justify-content", "center").set("overflow", "hidden"); // Create image element String imgSrc = base64Photo.startsWith("data:") ? base64Photo : "data:image/jpeg;base64," + base64Photo; - photoContainer.getElement().setProperty("innerHTML", - ""); + photoContainer.getElement().setProperty("innerHTML", ""); return photoContainer; } @@ -863,8 +800,8 @@ public class JobSummaryView extends Main implements HasUrlParameter { private void updatePhotoDisplay(Div photoContainer, String base64Photo, Span counter, int current, int total) { String imgSrc = base64Photo.startsWith("data:") ? base64Photo : "data:image/jpeg;base64," + base64Photo; - photoContainer.getElement().setProperty("innerHTML", - ""); + photoContainer.getElement().setProperty("innerHTML", ""); counter.setText(current + " / " + total); } @@ -875,12 +812,11 @@ public class JobSummaryView extends Main implements HasUrlParameter { } // Remove any existing width and height attributes and add responsive styling - String responsiveSvg = svgContent - .replaceAll("width\\s*=\\s*[\"'][^\"']*[\"']", "") - .replaceAll("height\\s*=\\s*[\"'][^\"']*[\"']", "") - .replaceAll("style\\s*=\\s*[\"'][^\"']*[\"']", ""); + String responsiveSvg = svgContent.replaceAll("width\\s*=\\s*[\"'][^\"']*[\"']", "") + .replaceAll("height\\s*=\\s*[\"'][^\"']*[\"']", "").replaceAll("style\\s*=\\s*[\"'][^\"']*[\"']", ""); - // Add responsive styling - preserve viewBox if it exists, otherwise try to extract from width/height + // Add responsive styling - preserve viewBox if it exists, otherwise try to + // extract from width/height if (!responsiveSvg.contains("viewBox")) { // Try to extract original dimensions for viewBox String widthMatch = extractAttribute(svgContent, "width"); @@ -894,7 +830,7 @@ public class JobSummaryView extends Main implements HasUrlParameter { if (!cleanWidth.isEmpty() && !cleanHeight.isEmpty()) { responsiveSvg = responsiveSvg.replaceFirst(" { // Add responsive styling responsiveSvg = responsiveSvg.replaceFirst(" { // Reset hover state for all task cards for (Div taskCard : taskCards) { if (taskCard != null) { - taskCard.getStyle() - .set("transform", "translateY(0)") - .set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)") - .set("border-color", "var(--lumo-contrast-20pct)"); + taskCard.getStyle().set("transform", "translateY(0)").set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)") + .set("border-color", "var(--lumo-contrast-20pct)"); } } } } - - diff --git a/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java b/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java index c1acb61..0295ed7 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java @@ -48,7 +48,7 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af private Authentication pendingAuth; - public LoginView() { + public LoginView() { addClassName("login-view"); setSizeFull(); @@ -72,23 +72,18 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af H1 title = new H1("VotianLT"); title.getStyle().set("color", "var(--lumo-primary-color)"); - Button registerButton = new Button("Noch kein Konto? Registrieren", - e -> UI.getCurrent().navigate("register")); + Button registerButton = new Button("Noch kein Konto? Registrieren", e -> UI.getCurrent().navigate("register")); registerButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); // Inline flash message box (hidden by default) - flashBox.getStyle() - .set("background", "var(--lumo-error-color-10pct)") - .set("color", "var(--lumo-error-text-color)") - .set("border", "1px solid var(--lumo-error-color)") - .set("border-radius", "var(--lumo-border-radius-m)") - .set("padding", "var(--lumo-space-m)") - .set("width", "100%") - .set("display", "none"); + flashBox.getStyle().set("background", "var(--lumo-error-color-10pct)") + .set("color", "var(--lumo-error-text-color)").set("border", "1px solid var(--lumo-error-color)") + .set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)") + .set("width", "100%").set("display", "none"); VerticalLayout loginLayout = new VerticalLayout(); loginLayout.setAlignItems(FlexComponent.Alignment.CENTER); - + loginLayout.add(flashBox, title, loginForm, twoFaField, verify2faButton, registerButton); loginLayout.setMaxWidth("400px"); loginLayout.setPadding(true); @@ -101,8 +96,9 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af private void handlePasswordLogin(String username, String password) { try { // Prüfe Benutzername/Passwort - Authentication auth = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); - + Authentication auth = authenticationManager + .authenticate(new UsernamePasswordAuthenticationToken(username, password)); + if (twoFactorEnabled) { // 2FA aktiviert: Benutzer noch nicht in SecurityContext setzen this.pendingAuth = auth; @@ -129,7 +125,8 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af private void handleVerify2fa() { if (pendingAuth == null) { - Notification.show("Bitte zuerst Benutzername und Passwort eingeben.", 3000, Notification.Position.BOTTOM_CENTER); + Notification.show("Bitte zuerst Benutzername und Passwort eingeben.", 3000, + Notification.Position.BOTTOM_CENTER); return; } String username = pendingAuth.getName(); @@ -145,7 +142,8 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af } // 2FA korrekt: Benutzer nun anmelden SecurityContextHolder.getContext().setAuthentication(pendingAuth); - // Persistiere SecurityContext in der HTTP-Session, damit Vaadin/Security ihn in neuen Requests sieht + // Persistiere SecurityContext in der HTTP-Session, damit Vaadin/Security ihn in + // neuen Requests sieht var vaadinSession = VaadinSession.getCurrent(); if (vaadinSession != null) { var wrappedSession = vaadinSession.getSession(); @@ -160,10 +158,7 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af @Override public void beforeEnter(BeforeEnterEvent beforeEnterEvent) { // Zeige Fehlermeldung bei fehlgeschlagener Anmeldung - if (beforeEnterEvent.getLocation() - .getQueryParameters() - .getParameters() - .containsKey("error")) { + if (beforeEnterEvent.getLocation().getQueryParameters().getParameters().containsKey("error")) { loginForm.setError(true); } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/MyInvoicesView.java b/src/main/java/de/assecutor/votianlt/pages/view/MyInvoicesView.java index dd31a39..9812212 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/MyInvoicesView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/MyInvoicesView.java @@ -24,9 +24,9 @@ import java.util.List; /** * Meine Rechnungen – nutzerzentrierte Übersicht. * - * Layout orientiert am bereitgestellten Screenshot: - * - Zwei Karten oben (Offene Rechnungen, Bankverbindung) - * - Darunter ein Bereich „Rechnungen" mit Grid, Suche und Seitengröße + * Layout orientiert am bereitgestellten Screenshot: - Zwei Karten oben (Offene + * Rechnungen, Bankverbindung) - Darunter ein Bereich „Rechnungen" mit Grid, + * Suche und Seitengröße */ @PageTitle("Meine Rechnungen") @Route(value = "my-invoices", layout = MainLayout.class) @@ -54,14 +54,11 @@ public class MyInvoicesView extends Main { private Component createTopCards() { // Container mit zwei Spalten (responsiv) Div container = new Div(); - container.getStyle() - .set("display", "grid") - .set("grid-template-columns", "48% 2% 48%"); - //.set("gap", "10px"); + container.getStyle().set("display", "grid").set("grid-template-columns", "48% 2% 48%"); + // .set("gap", "10px"); // Spaltenabstände: 2% zwischen den beiden Spalten container.getStyle().set("column-gap", "0"); - // Karte: Offene Rechnungen Paragraph hint = new Paragraph("Momentan sind keine neuen Rechnungen für Sie im System gespeichert."); hint.getStyle().set("color", "var(--lumo-success-text-color)"); @@ -71,12 +68,9 @@ public class MyInvoicesView extends Main { VerticalLayout bankData = new VerticalLayout(); bankData.setPadding(false); bankData.setSpacing(false); - bankData.add( - labeledValue("Kreditinstitut", "Hamburger Sparkasse"), + bankData.add(labeledValue("Kreditinstitut", "Hamburger Sparkasse"), labeledValue("Begünstigter", "Assecutor Data Service GmbH"), - labeledValue("IBAN", "DE67200505501217139888"), - labeledValue("Verwendungszweck", "vlt-00000610") - ); + labeledValue("IBAN", "DE67200505501217139888"), labeledValue("Verwendungszweck", "vlt-00000610")); Div bankCard = createCard("Bankverbindung", bankData); container.add(openInvoicesCard, bankCard); @@ -144,12 +138,11 @@ public class MyInvoicesView extends Main { private void applyFilter(String filter) { String f = filter == null ? "" : filter.toLowerCase(); - grid.setItems(allRows.stream().filter(row -> - row.status.toLowerCase().contains(f) - || row.invoiceNumber.toLowerCase().contains(f) + grid.setItems(allRows.stream() + .filter(row -> row.status.toLowerCase().contains(f) || row.invoiceNumber.toLowerCase().contains(f) || row.date.toString().toLowerCase().contains(f) - || String.valueOf(row.amount).toLowerCase().contains(f) - ).toList()); + || String.valueOf(row.amount).toLowerCase().contains(f)) + .toList()); } private Div createCard(String title, Component content) { @@ -174,16 +167,13 @@ public class MyInvoicesView extends Main { } private void styleCard(Div card) { - card.getStyle() - .set("border", "1px solid var(--lumo-contrast-20pct)") - .set("border-radius", "var(--lumo-border-radius-l)") - .set("padding", "var(--lumo-space-m)") - .set("background", "var(--lumo-base-color)") - .set("box-shadow", "0 1px 1px rgba(0,0,0,0.02)"); + card.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)") + .set("border-radius", "var(--lumo-border-radius-l)").set("padding", "var(--lumo-space-m)") + .set("background", "var(--lumo-base-color)").set("box-shadow", "0 1px 1px rgba(0,0,0,0.02)"); card.setWidthFull(); } // Schlanke lokale Repräsentation für das Grid - public record MyInvoiceRow(String status, String invoiceNumber, LocalDate date, double amount) {} + public record MyInvoiceRow(String status, String invoiceNumber, LocalDate date, double amount) { + } } - diff --git a/src/main/java/de/assecutor/votianlt/pages/view/RegisterView.java b/src/main/java/de/assecutor/votianlt/pages/view/RegisterView.java index eb4e460..441a81a 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/RegisterView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/RegisterView.java @@ -16,8 +16,7 @@ import com.vaadin.flow.router.Route; import com.vaadin.flow.server.VaadinSession; import com.vaadin.flow.server.auth.AnonymousAllowed; import de.assecutor.votianlt.pages.service.UserService; -import de.assecutor.votianlt.util.MailUtil; -import jakarta.mail.MessagingException; +import de.assecutor.votianlt.service.EmailService; import java.security.SecureRandom; import java.time.Duration; @@ -28,7 +27,7 @@ import java.time.LocalDateTime; @AnonymousAllowed public class RegisterView extends VerticalLayout { private final UserService userService; - private final MailUtil mailUtil; + private final EmailService emailService; private TextField emailField; private PasswordField passwordField; @@ -54,9 +53,9 @@ public class RegisterView extends VerticalLayout { private LocalDateTime lastSentAt; private boolean awaitingVerification = false; - public RegisterView(UserService userService, MailUtil mailUtil) { + public RegisterView(UserService userService, EmailService emailService) { this.userService = userService; - this.mailUtil = mailUtil; + this.emailService = emailService; // Layout-Konfiguration für vollständige Zentrierung setSizeFull(); setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); @@ -166,17 +165,14 @@ public class RegisterView extends VerticalLayout { resendButton.setVisible(false); // Zurück-Link - Button backButton = new Button("Zurück zur Startseite", event -> - getUI().ifPresent(ui -> ui.navigate(""))); + Button backButton = new Button("Zurück zur Startseite", event -> getUI().ifPresent(ui -> ui.navigate(""))); backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); backButton.setWidthFull(); // Zweispaltiges Formular FormLayout form = new FormLayout(); form.setWidthFull(); - form.setResponsiveSteps( - new FormLayout.ResponsiveStep("0", 2) - ); + form.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 2)); // Firma zuerst, volle Breite form.add(companyField); @@ -238,7 +234,8 @@ public class RegisterView extends VerticalLayout { return; } if (userService.existsByEmail(email)) { - Notification.show("Ein Benutzer mit dieser E-Mail-Adresse existiert bereits.", 4000, Notification.Position.MIDDLE); + Notification.show("Ein Benutzer mit dieser E-Mail-Adresse existiert bereits.", 4000, + Notification.Position.MIDDLE); return; } if (password.isEmpty()) { @@ -315,7 +312,8 @@ public class RegisterView extends VerticalLayout { // Rate-Limit: 60 Sekunden zwischen Sendungen if (lastSentAt != null && Duration.between(lastSentAt, LocalDateTime.now()).getSeconds() < 60) { long wait = 60 - Duration.between(lastSentAt, LocalDateTime.now()).getSeconds(); - Notification.show("Bitte warten Sie " + wait + " Sekunden, bevor Sie den Code erneut senden.", 4000, Notification.Position.MIDDLE); + Notification.show("Bitte warten Sie " + wait + " Sekunden, bevor Sie den Code erneut senden.", 4000, + Notification.Position.MIDDLE); return; } String code = generateSixDigitCode(); @@ -324,11 +322,10 @@ public class RegisterView extends VerticalLayout { lastSentAt = LocalDateTime.now(); String subject = "Ihr VotianLT Bestätigungscode"; - String body = "Ihr Bestätigungscode lautet: " + code + "\n\n" + - "Dieser Code ist 10 Minuten gültig.\n" + - "Wenn Sie diese Registrierung nicht angefragt haben, ignorieren Sie diese E-Mail."; + String body = "Ihr Bestätigungscode lautet: " + code + "\n\n" + "Dieser Code ist 10 Minuten gültig.\n" + + "Wenn Sie diese Registrierung nicht angefragt haben, ignorieren Sie diese E-Mail."; try { - mailUtil.sendMail(email, subject, body); + emailService.sendSimpleEmail(email, subject, body); awaitingVerification = true; // UI umstellen: Code-Eingabe anzeigen codeField.clear(); @@ -352,8 +349,9 @@ public class RegisterView extends VerticalLayout { submitButton.setEnabled(false); - Notification.show("Ein Bestätigungscode wurde an " + email + " gesendet.", 4000, Notification.Position.MIDDLE); - } catch (MessagingException e) { + Notification.show("Ein Bestätigungscode wurde an " + email + " gesendet.", 4000, + Notification.Position.MIDDLE); + } catch (Exception e) { awaitingVerification = false; Notification.show("Fehler beim Senden der E-Mail: " + e.getMessage(), 5000, Notification.Position.MIDDLE); } @@ -370,7 +368,8 @@ public class RegisterView extends VerticalLayout { return; } if (codeExpiresAt == null || LocalDateTime.now().isAfter(codeExpiresAt)) { - Notification.show("Der Code ist abgelaufen. Bitte senden Sie einen neuen Code.", 4000, Notification.Position.MIDDLE); + Notification.show("Der Code ist abgelaufen. Bitte senden Sie einen neuen Code.", 4000, + Notification.Position.MIDDLE); return; } if (!entered.equals(pendingCode)) { @@ -387,7 +386,8 @@ public class RegisterView extends VerticalLayout { try { var user = userService.createUser(email, password, firstName, lastName); // Persistiere zusätzliche Profil-/Adressdaten - if (!phone.isBlank()) user.setPhone(phone); + if (!phone.isBlank()) + user.setPhone(phone); var company = companyField.getValue() != null ? companyField.getValue().trim() : null; var street = streetField.getValue() != null ? streetField.getValue().trim() : null; var houseNo = houseNumberField.getValue() != null ? houseNumberField.getValue().trim() : null; @@ -399,7 +399,8 @@ public class RegisterView extends VerticalLayout { user.setZip(zip); user.setCity(city); userService.save(user); - VaadinSession.getCurrent().setAttribute("flashMessage", "Registrierung erfolgreich. Bitte melden Sie sich an."); + VaadinSession.getCurrent().setAttribute("flashMessage", + "Registrierung erfolgreich. Bitte melden Sie sich an."); getUI().ifPresent(ui -> ui.navigate("login")); } catch (RuntimeException e) { Notification.show("Registrierung fehlgeschlagen: " + e.getMessage(), 5000, Notification.Position.MIDDLE); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/ShowCustomersView.java b/src/main/java/de/assecutor/votianlt/pages/view/ShowCustomersView.java index a283b15..047c851 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/ShowCustomersView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/ShowCustomersView.java @@ -17,7 +17,7 @@ import org.springframework.beans.factory.annotation.Autowired; @PageTitle("Kunden") @Route(value = "customers", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) -@RolesAllowed({"USER","ADMIN"}) +@RolesAllowed({ "USER", "ADMIN" }) public class ShowCustomersView extends VerticalLayout { private final CustomerService customerService; @@ -43,32 +43,33 @@ public class ShowCustomersView extends VerticalLayout { add(header); // Add hint text - var hintText = new com.vaadin.flow.component.html.Paragraph("Klicken Sie auf einen Eintrag, um ihn zu bearbeiten."); + var hintText = new com.vaadin.flow.component.html.Paragraph( + "Klicken Sie auf einen Eintrag, um ihn zu bearbeiten."); hintText.getStyle().set("color", "var(--lumo-secondary-text-color)"); hintText.getStyle().set("font-size", "var(--lumo-font-size-s)"); add(hintText); // Configure grid columns grid.addColumn(Customer::getCompanyName).setHeader("Firma").setAutoWidth(true).setFlexGrow(1).setSortable(true); - grid.addColumn(customer -> (customer.getFirstname() != null ? customer.getFirstname() : "") + " " + - (customer.getLastName() != null ? customer.getLastName() : "")) - .setHeader("Name").setAutoWidth(true).setFlexGrow(1).setSortable(true); + grid.addColumn(customer -> (customer.getFirstname() != null ? customer.getFirstname() : "") + " " + + (customer.getLastName() != null ? customer.getLastName() : "")).setHeader("Name").setAutoWidth(true) + .setFlexGrow(1).setSortable(true); grid.addColumn(Customer::getMail).setHeader("E-Mail").setAutoWidth(true).setFlexGrow(1).setSortable(true); grid.addColumn(Customer::getTelephone).setHeader("Telefon").setAutoWidth(true).setSortable(true); - grid.addColumn(customer -> (customer.getStreet() != null ? customer.getStreet() : "") + " " + - (customer.getHouseNumber() != null ? customer.getHouseNumber() : "")) - .setHeader("Straße").setAutoWidth(true).setFlexGrow(1).setSortable(true); - grid.addColumn(customer -> (customer.getZip() != null ? customer.getZip() : "") + " " + - (customer.getCity() != null ? customer.getCity() : "")) - .setHeader("Ort").setAutoWidth(true).setFlexGrow(1).setSortable(true); + grid.addColumn(customer -> (customer.getStreet() != null ? customer.getStreet() : "") + " " + + (customer.getHouseNumber() != null ? customer.getHouseNumber() : "")).setHeader("Straße") + .setAutoWidth(true).setFlexGrow(1).setSortable(true); + grid.addColumn(customer -> (customer.getZip() != null ? customer.getZip() : "") + " " + + (customer.getCity() != null ? customer.getCity() : "")).setHeader("Ort").setAutoWidth(true) + .setFlexGrow(1).setSortable(true); grid.setMultiSort(true); grid.setSizeFull(); - + // Make grid rows clickable grid.setSelectionMode(Grid.SelectionMode.SINGLE); grid.getStyle().set("cursor", "pointer"); - + // Add click listener to navigate to edit view grid.addItemClickListener(event -> { Customer customer = event.getItem(); @@ -76,13 +77,11 @@ public class ShowCustomersView extends VerticalLayout { getUI().ifPresent(ui -> ui.navigate("edit-customer/" + customer.getId().toHexString())); } }); - + add(grid); // Button action - addCustomerButton.addClickListener(e -> - getUI().ifPresent(ui -> ui.navigate("add-customer")) - ); + addCustomerButton.addClickListener(e -> getUI().ifPresent(ui -> ui.navigate("add-customer"))); loadData(); } @@ -91,8 +90,7 @@ public class ShowCustomersView extends VerticalLayout { var customers = customerService.findAll(); var currentUserId = securityService.getCurrentUserId(); var ownCustomers = customers.stream() - .filter(c -> c.getCreatedBy() != null && c.getCreatedBy().equals(currentUserId)) - .toList(); + .filter(c -> c.getCreatedBy() != null && c.getCreatedBy().equals(currentUserId)).toList(); grid.setItems(ownCustomers); } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/ShowJobsView.java b/src/main/java/de/assecutor/votianlt/pages/view/ShowJobsView.java index 3bacad7..a2f53f2 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/ShowJobsView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/ShowJobsView.java @@ -21,7 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired; @PageTitle("Aufträge") @Route(value = "jobs", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) -@RolesAllowed({"USER"}) +@RolesAllowed({ "USER" }) public class ShowJobsView extends VerticalLayout { private final DatePicker startDate = new DatePicker("Startdatum"); @@ -64,7 +64,6 @@ public class ShowJobsView extends VerticalLayout { filterBar.setAlignItems(Alignment.END); add(filterBar); - H2 title = new H2("Aufträge"); add(title); // Init default period: last 30 days @@ -80,7 +79,6 @@ public class ShowJobsView extends VerticalLayout { startDate.addValueChangeListener(e -> loadData()); endDate.addValueChangeListener(e -> loadData()); - // Configure grid columns: Kunde, Auftragsnummer, Auftragsdatum, Zielort grid.addColumn(Job::getDeliveryCompany).setHeader("Kunde").setAutoWidth(true).setFlexGrow(1).setSortable(true); grid.addColumn(Job::getJobNumber).setHeader("Auftragsnummer").setAutoWidth(true).setSortable(true); @@ -89,11 +87,11 @@ public class ShowJobsView extends VerticalLayout { grid.setMultiSort(true); grid.setSizeFull(); - + // Make grid rows clickable grid.setSelectionMode(Grid.SelectionMode.SINGLE); grid.getStyle().set("cursor", "pointer"); - + // Add click listener to navigate to job summary view grid.addItemClickListener(event -> { Job job = event.getItem(); @@ -101,7 +99,7 @@ public class ShowJobsView extends VerticalLayout { getUI().ifPresent(ui -> ui.navigate("job_summary/" + job.getId().toHexString())); } }); - + add(grid); loadData(); @@ -110,8 +108,10 @@ public class ShowJobsView extends VerticalLayout { private void loadData() { var start = startDate.getValue(); var end = endDate.getValue(); - java.time.LocalDateTime startDt = start != null ? start.atStartOfDay() : java.time.LocalDate.now().minusDays(30).atStartOfDay(); - java.time.LocalDateTime endDt = end != null ? end.atTime(23,59,59) : java.time.LocalDate.now().atTime(23,59,59); + java.time.LocalDateTime startDt = start != null ? start.atStartOfDay() + : java.time.LocalDate.now().minusDays(30).atStartOfDay(); + java.time.LocalDateTime endDt = end != null ? end.atTime(23, 59, 59) + : java.time.LocalDate.now().atTime(23, 59, 59); // Aktuellen Benutzer (ObjectId Hex) ermitteln String currentUserIdHex = securityService.getCurrentUserId().toHexString(); @@ -123,29 +123,31 @@ public class ShowJobsView extends VerticalLayout { if ("Erledigt".equals(selectedStatus)) { statusList = java.util.List.of(JobStatus.DELIVERED, JobStatus.COMPLETED, JobStatus.CANCELLED); } else if ("Offen".equals(selectedStatus)) { - statusList = java.util.List.of(JobStatus.CREATED, JobStatus.IN_PROGRESS, - JobStatus.PICKUP_SCHEDULED, JobStatus.PICKED_UP, - JobStatus.IN_TRANSIT); + statusList = java.util.List.of(JobStatus.CREATED, JobStatus.IN_PROGRESS, JobStatus.PICKUP_SCHEDULED, + JobStatus.PICKED_UP, JobStatus.IN_TRANSIT); } else { // "Alle" statusList = java.util.Arrays.asList(JobStatus.values()); } // Suchtext für Auftragsnummer String searchText = searchField.getValue(); - String jobNumberPattern = searchText != null && !searchText.trim().isEmpty() - ? searchText.trim() - : ".*"; // Regex für alle wenn leer + String jobNumberPattern = searchText != null && !searchText.trim().isEmpty() ? searchText.trim() : ".*"; // Regex + // für + // alle + // wenn + // leer // Verwende die erweiterte Suchmethode - var filteredJobs = jobRepository.findWithFilters(startDt, endDt, currentUserIdHex, - jobNumberPattern, statusList); + var filteredJobs = jobRepository.findWithFilters(startDt, endDt, currentUserIdHex, jobNumberPattern, + statusList); grid.setItems(filteredJobs); } private void exportToCsv() { var items = grid.getListDataView().getItems().toList(); - StreamResource resource = new StreamResource("jobs.csv", () -> new java.io.ByteArrayInputStream(generateCsv(items).getBytes(java.nio.charset.StandardCharsets.UTF_8))); + StreamResource resource = new StreamResource("jobs.csv", () -> new java.io.ByteArrayInputStream( + generateCsv(items).getBytes(java.nio.charset.StandardCharsets.UTF_8))); resource.setContentType("text/csv"); resource.setCacheTime(0); @@ -157,9 +159,8 @@ public class ShowJobsView extends VerticalLayout { // Add to UI and trigger download via JavaScript add(downloadAnchor); getUI().ifPresent(ui -> ui.getPage().executeJs( - "const link = arguments[0]; link.click(); setTimeout(() => link.remove(), 100);", - downloadAnchor.getElement() - )); + "const link = arguments[0]; link.click(); setTimeout(() => link.remove(), 100);", + downloadAnchor.getElement())); } private String generateCsv(java.util.List jobs) { @@ -179,11 +180,11 @@ public class ShowJobsView extends VerticalLayout { } private String escapeCsv(String value) { - if (value == null) return ""; + if (value == null) + return ""; if (value.contains(",") || value.contains("\"") || value.contains("\n")) { return "\"" + value.replace("\"", "\"\"") + "\""; } return value; } } - diff --git a/src/main/java/de/assecutor/votianlt/pages/view/StartView.java b/src/main/java/de/assecutor/votianlt/pages/view/StartView.java index b19fae3..6a04186 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/StartView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/StartView.java @@ -73,9 +73,8 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver { logo.getStyle().set("font-weight", "bold"); // Navigation - abhängig vom Anmeldestatus - Component navigation = securityService.isUserLoggedIn() - ? createAuthenticatedNavigation() - : createAnonymousNavigation(); + Component navigation = securityService.isUserLoggedIn() ? createAuthenticatedNavigation() + : createAnonymousNavigation(); header.add(logo, navigation); @@ -103,8 +102,7 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver { navLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER); // Auftragserstellung Button - Button createOrderBtn = new Button("Auftragserstellung", event -> - UI.getCurrent().navigate("add_job")); + Button createOrderBtn = new Button("Auftragserstellung", event -> UI.getCurrent().navigate("add_job")); createOrderBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY); // Verwaltung ComboBox @@ -115,15 +113,15 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver { String value = event.getValue(); if (value != null) { switch (value) { - case "Kunden": - UI.getCurrent().navigate("customer"); - break; - case "Aufträge": - UI.getCurrent().navigate("orders"); - break; - case "Firmen": - UI.getCurrent().navigate("add_company"); - break; + case "Kunden": + UI.getCurrent().navigate("customer"); + break; + case "Aufträge": + UI.getCurrent().navigate("orders"); + break; + case "Firmen": + UI.getCurrent().navigate("add_company"); + break; } managementCombo.clear(); // Reset selection } @@ -138,13 +136,13 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver { String value = event.getValue(); if (value != null) { switch (value) { - case "Profil anzeigen": - break; - case "Einstellungen": - break; - case "Abmelden": - securityService.logout(); - break; + case "Profil anzeigen": + break; + case "Einstellungen": + break; + case "Abmelden": + securityService.logout(); + break; } userCombo.clear(); // Reset selection } @@ -157,7 +155,7 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver { notificationBtn.setTooltipText("Benachrichtigungen"); notificationBtn.addClickListener(event -> { com.vaadin.flow.component.notification.Notification.show("Keine neuen Benachrichtigungen", 3000, - com.vaadin.flow.component.notification.Notification.Position.TOP_END); + com.vaadin.flow.component.notification.Notification.Position.TOP_END); }); navLayout.add(createOrderBtn, managementCombo, userCombo, notificationBtn); @@ -170,7 +168,8 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver { heroSection.setPadding(true); heroSection.setSpacing(true); heroSection.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); - heroSection.getStyle().set("background", "linear-gradient(135deg, var(--lumo-primary-color-10pct), var(--lumo-primary-color-50pct))"); + heroSection.getStyle().set("background", + "linear-gradient(135deg, var(--lumo-primary-color-10pct), var(--lumo-primary-color-50pct))"); heroSection.getStyle().set("min-height", "400px"); heroSection.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); @@ -184,11 +183,9 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver { heroTitle.getStyle().set("color", "var(--lumo-primary-text-color)"); heroTitle.getStyle().set("margin-bottom", "var(--lumo-space-l)"); - Paragraph heroDescription = new Paragraph( - "Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe - " + - "volldigital und aus einem Guss. Konzentrieren Sie sich auf Ihr Geschäft, " + - "wir kümmern uns um die Büroarbeit." - ); + Paragraph heroDescription = new Paragraph("Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe - " + + "volldigital und aus einem Guss. Konzentrieren Sie sich auf Ihr Geschäft, " + + "wir kümmern uns um die Büroarbeit."); heroDescription.getStyle().set("text-align", "center"); heroDescription.getStyle().set("max-width", "600px"); heroDescription.getStyle().set("font-size", "var(--lumo-font-size-l)"); @@ -214,9 +211,8 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver { systemTitle.getStyle().set("text-align", "center"); Paragraph systemIntro = new Paragraph( - "Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe ist von entscheidender Bedeutung, " + - "dass sie sich in erster Linie auf ihr eigentliches Geschäft konzentrieren können: Kunden gewinnen und Waren von A nach B liefern." - ); + "Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe ist von entscheidender Bedeutung, " + + "dass sie sich in erster Linie auf ihr eigentliches Geschäft konzentrieren können: Kunden gewinnen und Waren von A nach B liefern."); systemIntro.getStyle().set("text-align", "center"); systemIntro.getStyle().set("max-width", "800px"); systemIntro.getStyle().set("margin-bottom", "var(--lumo-space-xl)"); @@ -228,14 +224,12 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver { featuresGrid.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.START); // Feature Cards - featuresGrid.add( - createFeatureCard(VaadinIcon.COG, "Einrichtungsassistent", + featuresGrid.add(createFeatureCard(VaadinIcon.COG, "Einrichtungsassistent", "Mithilfe des Einrichtungsassistenten haben Sie die Möglichkeit, Ihr Nutzerprofil zu vervollständigen."), - createFeatureCard(VaadinIcon.USERS, "Kunden- und Auftragsverwaltung", - "Mit der Kunden- und Auftragsverwaltung haben Sie alle Kontaktdaten und Auftragsdetails stets im Blick."), - createFeatureCard(VaadinIcon.CLIPBOARD_TEXT, "Auftragserstellung", - "Stellen Sie mit wenigen Mausklicks Aufträge ins System ein und legen Sie fest, welcher Mitarbeiter welchen Transportauftrag abarbeiten soll.") - ); + createFeatureCard(VaadinIcon.USERS, "Kunden- und Auftragsverwaltung", + "Mit der Kunden- und Auftragsverwaltung haben Sie alle Kontaktdaten und Auftragsdetails stets im Blick."), + createFeatureCard(VaadinIcon.CLIPBOARD_TEXT, "Auftragserstellung", + "Stellen Sie mit wenigen Mausklicks Aufträge ins System ein und legen Sie fest, welcher Mitarbeiter welchen Transportauftrag abarbeiten soll.")); systemSection.add(systemTitle, systemIntro, featuresGrid); return systemSection; @@ -281,9 +275,8 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver { appTitle.getStyle().set("text-align", "center"); Paragraph appDescription = new Paragraph( - "Jeder Auftrag kann optional über die votianLT-App abgearbeitet werden – ganz ohne \"Zettelwirtschaft\". " + - "So gelangen alle relevanten Auftragsinformationen direkt auf das Smartphone des Fahrers." - ); + "Jeder Auftrag kann optional über die votianLT-App abgearbeitet werden – ganz ohne \"Zettelwirtschaft\". " + + "So gelangen alle relevanten Auftragsinformationen direkt auf das Smartphone des Fahrers."); appDescription.getStyle().set("text-align", "center"); appDescription.getStyle().set("max-width", "800px"); @@ -314,18 +307,12 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver { companyInfo.setPadding(false); companyInfo.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); - companyInfo.add( - new Paragraph("Assecutor Data Service GmbH"), - new Paragraph("Ottensener Str. 8, 22525 Hamburg"), - new Paragraph("Telefon: +49 40 18 123 771 0"), - new Paragraph("E-Mail: ahoi@assecutor.de") - ); + companyInfo.add(new Paragraph("Assecutor Data Service GmbH"), new Paragraph("Ottensener Str. 8, 22525 Hamburg"), + new Paragraph("Telefon: +49 40 18 123 771 0"), new Paragraph("E-Mail: ahoi@assecutor.de")); // Call to Action - Paragraph ctaText = new Paragraph( - "Registrieren Sie sich noch heute und nutzen den kostenfreien Probemonat, " + - "um das System auf Herz und Nieren zu testen." - ); + Paragraph ctaText = new Paragraph("Registrieren Sie sich noch heute und nutzen den kostenfreien Probemonat, " + + "um das System auf Herz und Nieren zu testen."); ctaText.getStyle().set("text-align", "center"); ctaText.getStyle().set("font-weight", "bold"); ctaText.getStyle().set("color", "var(--lumo-primary-color)"); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/StatisticsView.java b/src/main/java/de/assecutor/votianlt/pages/view/StatisticsView.java index 1e0de59..20f926f 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/StatisticsView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/StatisticsView.java @@ -15,7 +15,7 @@ import jakarta.annotation.security.RolesAllowed; @PageTitle("Statistiken") @Route(value = "statistics", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) -@RolesAllowed({"USER","ADMIN"}) +@RolesAllowed({ "USER", "ADMIN" }) @JavaScript("https://cdn.jsdelivr.net/npm/chart.js") public class StatisticsView extends VerticalLayout { @@ -56,11 +56,11 @@ public class StatisticsView extends VerticalLayout { revenueContainer.setWidthFull(); revenueContainer.setHeight("400px"); revenueContainer.setPadding(false); - + Div revenueChart = createRevenueByCustomerChart(); revenueChart.setSizeFull(); revenueContainer.add(revenueChart); - + add(revenueContainer); } @@ -72,13 +72,13 @@ public class StatisticsView extends VerticalLayout { // Gesamtaufträge Div totalOrdersCard = createKpiCard("Gesamtaufträge", "247", "success"); - + // Offene Aufträge Div openOrdersCard = createKpiCard("Offene Aufträge", "34", "warning"); - + // Umsatz diesen Monat Div revenueCard = createKpiCard("Umsatz (Monat)", "€ 24.500", "primary"); - + // Neue Kunden Div newCustomersCard = createKpiCard("Neue Kunden", "12", "success"); @@ -89,23 +89,17 @@ public class StatisticsView extends VerticalLayout { private Div createKpiCard(String title, String value, String theme) { Div card = new Div(); card.addClassName("kpi-card"); - card.getStyle() - .set("background", "var(--lumo-base-color)") - .set("border", "1px solid var(--lumo-contrast-10pct)") - .set("border-radius", "var(--lumo-border-radius-m)") - .set("padding", "var(--lumo-space-m)") - .set("text-align", "center") - .set("box-shadow", "var(--lumo-box-shadow-xs)") - .set("min-width", "150px"); + card.getStyle().set("background", "var(--lumo-base-color)") + .set("border", "1px solid var(--lumo-contrast-10pct)") + .set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)") + .set("text-align", "center").set("box-shadow", "var(--lumo-box-shadow-xs)").set("min-width", "150px"); H3 titleElement = new H3(title); titleElement.getStyle().set("margin", "0 0 var(--lumo-space-s) 0").set("font-size", "var(--lumo-font-size-s)"); Span valueElement = new Span(value); - valueElement.getStyle() - .set("font-size", "var(--lumo-font-size-xl)") - .set("font-weight", "bold") - .set("color", getThemeColor(theme)); + valueElement.getStyle().set("font-size", "var(--lumo-font-size-xl)").set("font-weight", "bold").set("color", + getThemeColor(theme)); card.add(titleElement, valueElement); return card; @@ -113,197 +107,197 @@ public class StatisticsView extends VerticalLayout { private String getThemeColor(String theme) { return switch (theme) { - case "success" -> "var(--lumo-success-color)"; - case "warning" -> "var(--lumo-warning-color)"; - case "error" -> "var(--lumo-error-color)"; - default -> "var(--lumo-primary-color)"; + case "success" -> "var(--lumo-success-color)"; + case "warning" -> "var(--lumo-warning-color)"; + case "error" -> "var(--lumo-error-color)"; + default -> "var(--lumo-primary-color)"; }; } private Div createMonthlyOrdersChart() { Div chartContainer = new Div(); chartContainer.setId("monthlyOrdersChart"); - + String canvasHtml = ""; Html canvas = new Html(canvasHtml); chartContainer.add(canvas); - + String script = """ - - """; - + }, 100); + + """; + Html scriptElement = new Html(script); chartContainer.add(scriptElement); - + return chartContainer; } private Div createStatusPieChart() { Div chartContainer = new Div(); chartContainer.setId("statusPieChart"); - + String canvasHtml = ""; Html canvas = new Html(canvasHtml); chartContainer.add(canvas); - + String script = """ - - """; - + }, 100); + + """; + Html scriptElement = new Html(script); chartContainer.add(scriptElement); - + return chartContainer; } private Div createRevenueByCustomerChart() { Div chartContainer = new Div(); chartContainer.setId("revenueByCustomerChart"); - + String canvasHtml = ""; Html canvas = new Html(canvasHtml); chartContainer.add(canvas); - + String script = """ - - """; - + }, 100); + + """; + Html scriptElement = new Html(script); chartContainer.add(scriptElement); - + return chartContainer; } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/VerwaltungView.java b/src/main/java/de/assecutor/votianlt/pages/view/VerwaltungView.java index 211c9bf..39cddfc 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/VerwaltungView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/VerwaltungView.java @@ -27,18 +27,18 @@ public class VerwaltungView extends Main { // Content VerticalLayout content = new VerticalLayout(); - + H1 title = new H1("Verwaltung"); title.getStyle().set("color", "var(--lumo-primary-color)"); - + Paragraph description = new Paragraph("Willkommen im Verwaltungsbereich. Wählen Sie eine Option aus dem Menü."); description.getStyle().set("color", "var(--lumo-secondary-text-color)"); - + content.add(title, description); content.setDefaultHorizontalComponentAlignment(VerticalLayout.Alignment.CENTER); content.setJustifyContentMode(VerticalLayout.JustifyContentMode.CENTER); content.setSizeFull(); - + add(content); } } \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/repository/AppDeviceRepository.java b/src/main/java/de/assecutor/votianlt/repository/AppDeviceRepository.java index 557c061..9786ccd 100644 --- a/src/main/java/de/assecutor/votianlt/repository/AppDeviceRepository.java +++ b/src/main/java/de/assecutor/votianlt/repository/AppDeviceRepository.java @@ -9,7 +9,7 @@ import java.util.List; @Repository public interface AppDeviceRepository extends MongoRepository { - + // Find all devices created by a specific user List findByErstelltVon(ObjectId erstelltVon); diff --git a/src/main/java/de/assecutor/votianlt/repository/AppUserRepository.java b/src/main/java/de/assecutor/votianlt/repository/AppUserRepository.java index 99ea269..270486e 100644 --- a/src/main/java/de/assecutor/votianlt/repository/AppUserRepository.java +++ b/src/main/java/de/assecutor/votianlt/repository/AppUserRepository.java @@ -9,10 +9,10 @@ import java.util.List; @Repository public interface AppUserRepository extends MongoRepository { - + // Find all AppUsers created by a specific user List findByErstelltVon(ObjectId erstelltVon); - + // Find AppUser by email for login AppUser findByEmail(String email); diff --git a/src/main/java/de/assecutor/votianlt/repository/CargoItemRepository.java b/src/main/java/de/assecutor/votianlt/repository/CargoItemRepository.java index 7e715c8..9f3cd5b 100644 --- a/src/main/java/de/assecutor/votianlt/repository/CargoItemRepository.java +++ b/src/main/java/de/assecutor/votianlt/repository/CargoItemRepository.java @@ -9,4 +9,3 @@ import java.util.List; public interface CargoItemRepository extends MongoRepository { List findByJobId(ObjectId jobId); } - diff --git a/src/main/java/de/assecutor/votianlt/repository/JobHistoryRepository.java b/src/main/java/de/assecutor/votianlt/repository/JobHistoryRepository.java index 61e9889..18557c0 100644 --- a/src/main/java/de/assecutor/votianlt/repository/JobHistoryRepository.java +++ b/src/main/java/de/assecutor/votianlt/repository/JobHistoryRepository.java @@ -14,26 +14,27 @@ import java.util.List; public interface JobHistoryRepository extends MongoRepository { /** - * Find all history entries for a specific job, ordered by timestamp descending (newest first) + * Find all history entries for a specific job, ordered by timestamp descending + * (newest first) */ List findByJobIdOrderByTimestampDesc(ObjectId jobId); /** - * Find all history entries for a specific job, ordered by timestamp ascending (oldest first) + * Find all history entries for a specific job, ordered by timestamp ascending + * (oldest first) */ List findByJobIdOrderByTimestampAsc(ObjectId jobId); /** * Find history entries for a job within a specific time range */ - List findByJobIdAndTimestampBetweenOrderByTimestampDesc( - ObjectId jobId, LocalDateTime start, LocalDateTime end); + List findByJobIdAndTimestampBetweenOrderByTimestampDesc(ObjectId jobId, LocalDateTime start, + LocalDateTime end); /** * Find history entries by change type for a specific job */ - List findByJobIdAndChangeTypeOrderByTimestampDesc( - ObjectId jobId, JobHistoryType changeType); + List findByJobIdAndChangeTypeOrderByTimestampDesc(ObjectId jobId, JobHistoryType changeType); /** * Find history entries made by a specific user diff --git a/src/main/java/de/assecutor/votianlt/repository/JobRepository.java b/src/main/java/de/assecutor/votianlt/repository/JobRepository.java index 2d7c739..3cb4cfe 100644 --- a/src/main/java/de/assecutor/votianlt/repository/JobRepository.java +++ b/src/main/java/de/assecutor/votianlt/repository/JobRepository.java @@ -99,10 +99,8 @@ public interface JobRepository extends MongoRepository { /** * Erweiterte Suche: Zeitraum, Auftragsnummer und Status kombiniert */ - @Query("{'createdAt': {'$gte': ?0, '$lte': ?1}, 'createdBy': ?2, " + - "'jobNumber': {'$regex': ?3, '$options': 'i'}, " + - "'status': {'$in': ?4}}") - List findWithFilters(LocalDateTime startDate, LocalDateTime endDate, - String createdBy, String jobNumberPattern, - List statusList); + @Query("{'createdAt': {'$gte': ?0, '$lte': ?1}, 'createdBy': ?2, " + + "'jobNumber': {'$regex': ?3, '$options': 'i'}, " + "'status': {'$in': ?4}}") + List findWithFilters(LocalDateTime startDate, LocalDateTime endDate, String createdBy, String jobNumberPattern, + List statusList); } diff --git a/src/main/java/de/assecutor/votianlt/repository/PhotoRepository.java b/src/main/java/de/assecutor/votianlt/repository/PhotoRepository.java index c4f14fa..db62227 100644 --- a/src/main/java/de/assecutor/votianlt/repository/PhotoRepository.java +++ b/src/main/java/de/assecutor/votianlt/repository/PhotoRepository.java @@ -8,21 +8,25 @@ import org.springframework.stereotype.Repository; import java.util.List; /** - * Repository interface for Photo entities. - * Provides database operations for the photos collection. + * Repository interface for Photo entities. Provides database operations for the + * photos collection. */ @Repository public interface PhotoRepository extends MongoRepository { /** * Find all photos associated with a specific task ID. - * @param taskId The ObjectId of the task + * + * @param taskId + * The ObjectId of the task * @return List of photos for the task */ List findByTaskId(ObjectId taskId); /** * Find photos by task ID as string. - * @param taskId The task ID as string + * + * @param taskId + * The task ID as string * @return List of photos for the task */ default List findByTaskId(String taskId) { diff --git a/src/main/java/de/assecutor/votianlt/repository/TaskRepository.java b/src/main/java/de/assecutor/votianlt/repository/TaskRepository.java index 339e8f0..c6a8b09 100644 --- a/src/main/java/de/assecutor/votianlt/repository/TaskRepository.java +++ b/src/main/java/de/assecutor/votianlt/repository/TaskRepository.java @@ -15,4 +15,3 @@ public interface TaskRepository extends MongoRepository { return findByJobIdOrderByTaskOrderAsc(jobId); } } - diff --git a/src/main/java/de/assecutor/votianlt/repository/UserRepository.java b/src/main/java/de/assecutor/votianlt/repository/UserRepository.java index b8a0324..cbb7f95 100644 --- a/src/main/java/de/assecutor/votianlt/repository/UserRepository.java +++ b/src/main/java/de/assecutor/votianlt/repository/UserRepository.java @@ -9,11 +9,11 @@ import java.util.Optional; @Repository public interface UserRepository extends MongoRepository { - + Optional findByEmail(String email); - + boolean existsByEmail(String email); - + void deleteByEmail(String email); Optional findByPasswordCode(String passwordCode); diff --git a/src/main/java/de/assecutor/votianlt/security/CustomUserPrincipal.java b/src/main/java/de/assecutor/votianlt/security/CustomUserPrincipal.java index 14eb29d..3c28c47 100644 --- a/src/main/java/de/assecutor/votianlt/security/CustomUserPrincipal.java +++ b/src/main/java/de/assecutor/votianlt/security/CustomUserPrincipal.java @@ -11,61 +11,60 @@ import java.util.Set; import java.util.stream.Collectors; /** - * Custom UserDetails implementation that holds a reference to the MongoDB User entity. - * This allows access to the complete User object from the session without additional database queries. + * Custom UserDetails implementation that holds a reference to the MongoDB User + * entity. This allows access to the complete User object from the session + * without additional database queries. */ public class CustomUserPrincipal implements UserDetails { - + private final User user; // MongoDB User entity - + public CustomUserPrincipal(User user) { this.user = user; } - + /** * Get the complete MongoDB User entity */ public User getUser() { return user; } - + @Override public String getUsername() { return user.getEmail(); } - + @Override public String getPassword() { return user.getPassword(); } - + @Override public boolean isEnabled() { return user.getIsActivated() == 1; } - + @Override public Collection getAuthorities() { Set roles = user.getRoles(); if (roles != null && !roles.isEmpty()) { - return roles.stream() - .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) - .collect(Collectors.toList()); + return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect(Collectors.toList()); } // Default role if no roles are set return Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")); } - + @Override public boolean isAccountNonExpired() { return true; } - + @Override public boolean isAccountNonLocked() { return true; } - + @Override public boolean isCredentialsNonExpired() { return true; diff --git a/src/main/java/de/assecutor/votianlt/security/SecurityConfig.java b/src/main/java/de/assecutor/votianlt/security/SecurityConfig.java index b1e2914..cb3b7a4 100644 --- a/src/main/java/de/assecutor/votianlt/security/SecurityConfig.java +++ b/src/main/java/de/assecutor/votianlt/security/SecurityConfig.java @@ -24,33 +24,20 @@ public class SecurityConfig extends VaadinWebSecurity { protected void configure(HttpSecurity http) throws Exception { // Konfiguriere zusätzliche öffentliche Endpunkte vor der Basis-Konfiguration http.authorizeHttpRequests(auth -> auth - // Öffentliche Endpunkte - .requestMatchers( - new AntPathRequestMatcher("/"), - new AntPathRequestMatcher("/register"), - new AntPathRequestMatcher("/login"), - new AntPathRequestMatcher("/forget-password"), - new AntPathRequestMatcher("/forgot-password-request"), - new AntPathRequestMatcher("/images/**"), - new AntPathRequestMatcher("/icons/**"), - new AntPathRequestMatcher("/favicon.ico"), - new AntPathRequestMatcher("/robots.txt"), - new AntPathRequestMatcher("/manifest.webmanifest"), - new AntPathRequestMatcher("/sw.js"), - new AntPathRequestMatcher("/offline.html"), - new AntPathRequestMatcher("/frontend/**"), - new AntPathRequestMatcher("/webjars/**"), - new AntPathRequestMatcher("/h2-console/**"), - new AntPathRequestMatcher("/frontend-es5/**", "/frontend-es6/**") - ).permitAll() - ); + // Öffentliche Endpunkte + .requestMatchers(new AntPathRequestMatcher("/"), new AntPathRequestMatcher("/register"), + new AntPathRequestMatcher("/login"), new AntPathRequestMatcher("/forget-password"), + new AntPathRequestMatcher("/forgot-password-request"), new AntPathRequestMatcher("/images/**"), + new AntPathRequestMatcher("/icons/**"), new AntPathRequestMatcher("/favicon.ico"), + new AntPathRequestMatcher("/robots.txt"), new AntPathRequestMatcher("/manifest.webmanifest"), + new AntPathRequestMatcher("/sw.js"), new AntPathRequestMatcher("/offline.html"), + new AntPathRequestMatcher("/frontend/**"), new AntPathRequestMatcher("/webjars/**"), + new AntPathRequestMatcher("/h2-console/**"), + new AntPathRequestMatcher("/frontend-es5/**", "/frontend-es6/**")) + .permitAll()); // Standard-CSRF-Konfiguration - http.csrf(csrf -> csrf - .ignoringRequestMatchers( - new AntPathRequestMatcher("/h2-console/**") - ) - ); + http.csrf(csrf -> csrf.ignoringRequestMatchers(new AntPathRequestMatcher("/h2-console/**"))); // Delegiere die Basis-Konfiguration an VaadinWebSecurity // Dies fügt automatisch .anyRequest().authenticated() hinzu @@ -60,12 +47,8 @@ public class SecurityConfig extends VaadinWebSecurity { setLoginView(http, "/login"); // Logout-Konfiguration - http.logout(logout -> logout - .logoutUrl("/logout") - .logoutSuccessUrl("/") - .invalidateHttpSession(true) - .deleteCookies("JSESSIONID") - ); + http.logout(logout -> logout.logoutUrl("/logout").logoutSuccessUrl("/").invalidateHttpSession(true) + .deleteCookies("JSESSIONID")); } @Bean diff --git a/src/main/java/de/assecutor/votianlt/security/SecurityService.java b/src/main/java/de/assecutor/votianlt/security/SecurityService.java index 52d2baf..54dc19f 100644 --- a/src/main/java/de/assecutor/votianlt/security/SecurityService.java +++ b/src/main/java/de/assecutor/votianlt/security/SecurityService.java @@ -25,7 +25,8 @@ public class SecurityService { } public boolean isUserLoggedIn() { - if (authenticationContext.isAuthenticated()) return true; + if (authenticationContext.isAuthenticated()) + return true; Authentication auth = SecurityContextHolder.getContext().getAuthentication(); return auth != null && auth.isAuthenticated() && !(auth instanceof AnonymousAuthenticationToken); } @@ -39,8 +40,10 @@ public class SecurityService { de.assecutor.votianlt.model.User u = cup.getUser(); if (u != null) { String namePart = (nullToEmpty(u.getFirstname()) + " " + nullToEmpty(u.getName())).trim(); - if (!namePart.isBlank()) return namePart; - if (u.getEmail() != null && !u.getEmail().isBlank()) return u.getEmail(); + if (!namePart.isBlank()) + return namePart; + if (u.getEmail() != null && !u.getEmail().isBlank()) + return u.getEmail(); } return cup.getUsername(); } @@ -53,19 +56,18 @@ public class SecurityService { } // 2) Fallback: Vaadin AuthenticationContext - return getAuthenticatedUser() - .map(UserDetails::getUsername) - .orElse("Anonymous"); + return getAuthenticatedUser().map(UserDetails::getUsername).orElse("Anonymous"); } - private String nullToEmpty(String s) { return s == null ? "" : s; } + private String nullToEmpty(String s) { + return s == null ? "" : s; + } /** * Get the complete MongoDB User entity from the session */ public de.assecutor.votianlt.model.User getCurrentDatabaseUser() { - return getAuthenticatedUser() - .filter(userDetails -> userDetails instanceof CustomUserPrincipal) + return getAuthenticatedUser().filter(userDetails -> userDetails instanceof CustomUserPrincipal) .map(userDetails -> ((CustomUserPrincipal) userDetails).getUser()) .orElseThrow(() -> new RuntimeException("No user logged in")); } @@ -101,9 +103,7 @@ public class SecurityService { } public boolean hasRole(String role) { - return getAuthenticatedUser() - .map(user -> user.getAuthorities().stream() - .anyMatch(authority -> authority.getAuthority().equals("ROLE_" + role))) - .orElse(false); + return getAuthenticatedUser().map(user -> user.getAuthorities().stream() + .anyMatch(authority -> authority.getAuthority().equals("ROLE_" + role))).orElse(false); } } diff --git a/src/main/java/de/assecutor/votianlt/security/UserDetailsServiceImpl.java b/src/main/java/de/assecutor/votianlt/security/UserDetailsServiceImpl.java index 33098bc..802b3cc 100644 --- a/src/main/java/de/assecutor/votianlt/security/UserDetailsServiceImpl.java +++ b/src/main/java/de/assecutor/votianlt/security/UserDetailsServiceImpl.java @@ -25,5 +25,4 @@ public class UserDetailsServiceImpl implements UserDetailsService { return new CustomUserPrincipal(user); } - } diff --git a/src/main/java/de/assecutor/votianlt/security/totp/TwoFactorService.java b/src/main/java/de/assecutor/votianlt/security/totp/TwoFactorService.java index 8782fb1..2bdc6bf 100644 --- a/src/main/java/de/assecutor/votianlt/security/totp/TwoFactorService.java +++ b/src/main/java/de/assecutor/votianlt/security/totp/TwoFactorService.java @@ -2,7 +2,7 @@ package de.assecutor.votianlt.security.totp; import de.assecutor.votianlt.model.User; import de.assecutor.votianlt.repository.UserRepository; -import de.assecutor.votianlt.util.MailUtil; +import de.assecutor.votianlt.service.EmailService; import org.springframework.stereotype.Service; import java.security.SecureRandom; @@ -13,35 +13,42 @@ import java.util.Optional; public class TwoFactorService { private final UserRepository userRepository; - private final MailUtil mailUtil; + private final EmailService emailService; private final SecureRandom random = new SecureRandom(); - public TwoFactorService(UserRepository userRepository, MailUtil mailUtil) { + public TwoFactorService(UserRepository userRepository, EmailService emailService) { this.userRepository = userRepository; - this.mailUtil = mailUtil; + this.emailService = emailService; } public void initiateTwoFactorFor(String email) { Optional userOpt = userRepository.findByEmail(email); - if (userOpt.isEmpty()) return; + if (userOpt.isEmpty()) + return; User user = userOpt.get(); String code = generateSixDigitCode(); user.setPasswordCode(code); user.setPasswordTimestamp(LocalDateTime.now()); userRepository.save(user); try { - mailUtil.sendMail(email, "Ihr Anmeldecode (2FA)", "Ihr 2FA-Code lautet: " + code + "\nGültig für 10 Minuten."); - } catch (Exception ignored) { } + emailService.sendSimpleEmail(email, "Ihr Anmeldecode (2FA)", + "Ihr 2FA-Code lautet: " + code + "\nGültig für 10 Minuten."); + } catch (Exception ignored) { + } } public boolean verifyTwoFactorCode(String email, String code) { Optional userOpt = userRepository.findByEmail(email); - if (userOpt.isEmpty()) return false; + if (userOpt.isEmpty()) + return false; User user = userOpt.get(); - if (user.getPasswordCode() == null || !user.getPasswordCode().equals(code)) return false; - if (user.getPasswordTimestamp() == null) return false; + if (user.getPasswordCode() == null || !user.getPasswordCode().equals(code)) + return false; + if (user.getPasswordTimestamp() == null) + return false; // Gültigkeit 10 Minuten - if (user.getPasswordTimestamp().isBefore(LocalDateTime.now().minusMinutes(10))) return false; + if (user.getPasswordTimestamp().isBefore(LocalDateTime.now().minusMinutes(10))) + return false; // Code verbrauchen user.setPasswordCode(null); user.setPasswordTimestamp(null); @@ -54,5 +61,3 @@ public class TwoFactorService { return String.format("%06d", n); } } - - diff --git a/src/main/java/de/assecutor/votianlt/service/EmailService.java b/src/main/java/de/assecutor/votianlt/service/EmailService.java index 309c97c..157c8f3 100644 --- a/src/main/java/de/assecutor/votianlt/service/EmailService.java +++ b/src/main/java/de/assecutor/votianlt/service/EmailService.java @@ -71,10 +71,12 @@ public class EmailService { // Send email sendEmail(user, job, taskType, taskId, appUser); - log.info("Task completion notification sent to {} for job {} task {}", user.getEmail(), job.getJobNumber(), taskId); + log.info("Task completion notification sent to {} for job {} task {}", user.getEmail(), job.getJobNumber(), + taskId); } catch (Exception e) { - log.error("Failed to send task completion notification for job {} task {}: {}", jobId, taskId, e.getMessage(), e); + log.error("Failed to send task completion notification for job {} task {}: {}", jobId, taskId, + e.getMessage(), e); } } @@ -82,7 +84,8 @@ public class EmailService { SimpleMailMessage message = new SimpleMailMessage(); message.setFrom(smtpUsername); message.setTo(user.getEmail()); - message.setSubject("Aufgabe abgeschlossen - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId())); + message.setSubject( + "Aufgabe abgeschlossen - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId())); String fullName = buildFullName(user); String appUserName = buildAppUserName(appUser); @@ -150,18 +153,37 @@ public class EmailService { } private String getTaskTypeDisplayName(String taskType) { - if (taskType == null) return "Unbekannte Aufgabe"; + if (taskType == null) + return "Unbekannte Aufgabe"; return switch (taskType.toUpperCase()) { - case "PHOTO" -> "Foto-Aufgabe"; - case "SIGNATURE" -> "Unterschrift"; - case "BARCODE" -> "Barcode scannen"; - case "CONFIRMATION" -> "Bestätigung"; - case "TODO_LIST" -> "Checkliste"; - default -> taskType; + case "PHOTO" -> "Foto-Aufgabe"; + case "SIGNATURE" -> "Unterschrift"; + case "BARCODE" -> "Barcode scannen"; + case "CONFIRMATION" -> "Bestätigung"; + case "TODO_LIST" -> "Checkliste"; + default -> taskType; }; } + private String buildRouteString(Job job) { + if (job.getPickupCity() == null && job.getDeliveryCity() == null) { + return null; + } + + StringBuilder route = new StringBuilder(); + if (job.getPickupCity() != null) { + route.append(job.getPickupCity()); + } + if (job.getPickupCity() != null && job.getDeliveryCity() != null) { + route.append(" → "); + } + if (job.getDeliveryCity() != null) { + route.append(job.getDeliveryCity()); + } + return route.toString(); + } + public void checkAndSendJobCompletionNotification(ObjectId jobId, String completedBy) { try { // Check if all tasks for this job are completed @@ -174,7 +196,8 @@ public class EmailService { boolean allCompleted = allTasks.stream().allMatch(task -> task.isCompleted()); if (allCompleted) { - log.info("All tasks completed for job {}, updating job status and sending completion notification", jobId); + log.info("All tasks completed for job {}, updating job status and sending completion notification", + jobId); // Update job status to COMPLETED updateJobStatusToCompleted(jobId); @@ -240,7 +263,8 @@ public class EmailService { SimpleMailMessage message = new SimpleMailMessage(); message.setFrom(smtpUsername); message.setTo(user.getEmail()); - message.setSubject("Job abgeschlossen - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId())); + message.setSubject( + "Job abgeschlossen - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId())); String fullName = buildFullName(user); String appUserName = buildAppUserName(appUser); @@ -299,8 +323,8 @@ public class EmailService { job.setUpdatedAt(java.time.LocalDateTime.now()); jobRepository.save(job); - log.info("Job status updated from {} to COMPLETED for job {}", - oldStatus != null ? oldStatus : "null", job.getJobNumber()); + log.info("Job status updated from {} to COMPLETED for job {}", oldStatus != null ? oldStatus : "null", + job.getJobNumber()); } else { log.debug("Job {} already has COMPLETED status", job.getJobNumber()); } @@ -354,7 +378,8 @@ public class EmailService { SimpleMailMessage message = new SimpleMailMessage(); message.setFrom(smtpUsername); message.setTo(user.getEmail()); - message.setSubject("Neuer Job erstellt - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId())); + message.setSubject( + "Neuer Job erstellt - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId())); String fullName = buildFullName(user); @@ -389,14 +414,18 @@ public class EmailService { body.append("Anzahl Aufgaben: ").append(taskCount).append("\n"); } - body.append("Status: ").append(job.getStatus() != null ? job.getStatus().getDisplayName() : "Unbekannt").append("\n"); + body.append("Status: ").append(job.getStatus() != null ? job.getStatus().getDisplayName() : "Unbekannt") + .append("\n"); if (job.getRemark() != null && !job.getRemark().isBlank()) { body.append("Bemerkung: ").append(job.getRemark()).append("\n"); } - body.append("Erstellt am: ").append(job.getCreatedAt() != null ? - job.getCreatedAt().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")) : "Unbekannt").append("\n\n"); + body.append("Erstellt am: ") + .append(job.getCreatedAt() != null + ? job.getCreatedAt().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")) + : "Unbekannt") + .append("\n\n"); body.append("Der Job ist nun im System verfügbar und kann bearbeitet werden.\n\n"); body.append("Mit freundlichen Grüßen,\n"); @@ -405,4 +434,24 @@ public class EmailService { message.setText(body.toString()); mailSender.send(message); } + + /** + * Send a simple text email + */ + public void sendSimpleEmail(String to, String subject, String body) { + try { + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom(smtpUsername); + message.setTo(to); + message.setSubject(subject); + message.setText(body); + + mailSender.send(message); + log.debug("Simple email sent to {} with subject: {}", to, subject); + + } catch (Exception e) { + log.error("Failed to send simple email to {} with subject '{}': {}", to, subject, e.getMessage(), e); + throw new RuntimeException("Failed to send email: " + e.getMessage(), e); + } + } } \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/service/JobHistoryService.java b/src/main/java/de/assecutor/votianlt/service/JobHistoryService.java index 6ce74f0..5352a62 100644 --- a/src/main/java/de/assecutor/votianlt/service/JobHistoryService.java +++ b/src/main/java/de/assecutor/votianlt/service/JobHistoryService.java @@ -29,15 +29,9 @@ public class JobHistoryService { */ public void logJobCreation(Job job, String createdBy) { try { - JobHistory history = new JobHistory( - job.getId(), - "Job erstellt", - "Neuer Job wurde erstellt: " + (job.getJobNumber() != null ? job.getJobNumber() : "Ohne Nummer"), - createdBy, - JobHistoryType.CREATE, - null, - "Job erstellt" - ); + JobHistory history = new JobHistory(job.getId(), "Job erstellt", + "Neuer Job wurde erstellt: " + (job.getJobNumber() != null ? job.getJobNumber() : "Ohne Nummer"), + createdBy, JobHistoryType.CREATE, null, "Job erstellt"); if (job.getDeliveryCompany() != null) { history.setDetails("Kunde: " + job.getDeliveryCompany()); @@ -55,19 +49,12 @@ public class JobHistoryService { */ public void logStatusChange(Job job, JobStatus oldStatus, JobStatus newStatus, String changedBy) { try { - String description = String.format("Status geändert von %s zu %s", - formatStatus(oldStatus), - formatStatus(newStatus)); + String description = String.format("Status geändert von %s zu %s", formatStatus(oldStatus), + formatStatus(newStatus)); - JobHistory history = new JobHistory( - job.getId(), - "Status-Änderung", - description, - changedBy, - JobHistoryType.STATUS_CHANGE, - oldStatus != null ? oldStatus.toString() : null, - newStatus != null ? newStatus.toString() : null - ); + JobHistory history = new JobHistory(job.getId(), "Status-Änderung", description, changedBy, + JobHistoryType.STATUS_CHANGE, oldStatus != null ? oldStatus.toString() : null, + newStatus != null ? newStatus.toString() : null); jobHistoryRepository.save(history); log.debug("Status change logged for job {}: {} -> {}", job.getIdAsString(), oldStatus, newStatus); @@ -83,15 +70,9 @@ public class JobHistoryService { try { String description = generateUpdateDescription(oldJob, newJob); - JobHistory history = new JobHistory( - newJob.getId(), - reason != null ? reason : "Job aktualisiert", - description, - changedBy, - JobHistoryType.UPDATE, - serializeJobForComparison(oldJob), - serializeJobForComparison(newJob) - ); + JobHistory history = new JobHistory(newJob.getId(), reason != null ? reason : "Job aktualisiert", + description, changedBy, JobHistoryType.UPDATE, serializeJobForComparison(oldJob), + serializeJobForComparison(newJob)); jobHistoryRepository.save(history); log.debug("Job update logged for job {}", newJob.getIdAsString()); @@ -111,7 +92,7 @@ public class JobHistoryService { * Log task completion with detailed information and extraData */ public void logTaskCompletion(ObjectId jobId, String taskType, String taskId, String completedBy, - String taskDisplayName, String extraDataSummary) { + String taskDisplayName, String extraDataSummary) { try { String taskName = taskDisplayName != null ? taskDisplayName : taskType; String description = String.format("Aufgabe abgeschlossen: %s", taskName); @@ -120,15 +101,8 @@ public class JobHistoryService { description += " - " + extraDataSummary; } - JobHistory history = new JobHistory( - jobId, - "Aufgabe abgeschlossen", - description, - completedBy, - JobHistoryType.TASK_COMPLETED, - "In Bearbeitung", - "Abgeschlossen" - ); + JobHistory history = new JobHistory(jobId, "Aufgabe abgeschlossen", description, completedBy, + JobHistoryType.TASK_COMPLETED, "In Bearbeitung", "Abgeschlossen"); // Detaillierte Informationen in details speichern StringBuilder details = new StringBuilder(); @@ -165,15 +139,8 @@ public class JobHistoryService { description = String.format("Job-Zuweisung geändert von %s zu %s", oldAssignee, newAssignee); } - JobHistory history = new JobHistory( - job.getId(), - "Zuweisung geändert", - description, - changedBy, - JobHistoryType.ASSIGNMENT, - oldAssignee, - newAssignee - ); + JobHistory history = new JobHistory(job.getId(), "Zuweisung geändert", description, changedBy, + JobHistoryType.ASSIGNMENT, oldAssignee, newAssignee); jobHistoryRepository.save(history); log.debug("Job assignment logged for job {}", job.getIdAsString()); @@ -186,7 +153,7 @@ public class JobHistoryService { * Log custom event */ public void logCustomEvent(ObjectId jobId, String reason, String description, String changedBy, - JobHistoryType type) { + JobHistoryType type) { try { JobHistory history = new JobHistory(jobId, reason, description, changedBy, type, null, null); jobHistoryRepository.save(history); @@ -213,18 +180,19 @@ public class JobHistoryService { // Helper methods private String formatStatus(JobStatus status) { - if (status == null) return "Unbekannt"; + if (status == null) + return "Unbekannt"; return switch (status) { - case CREATED -> "Erstellt"; - case IN_PROGRESS -> "In Bearbeitung"; - case PICKUP_SCHEDULED -> "Abholung geplant"; - case PICKED_UP -> "Abgeholt"; - case IN_TRANSIT -> "Unterwegs"; - case DELIVERED -> "Zugestellt"; - case COMPLETED -> "Abgeschlossen"; - case CANCELLED -> "Storniert"; - default -> status.toString(); + case CREATED -> "Erstellt"; + case IN_PROGRESS -> "In Bearbeitung"; + case PICKUP_SCHEDULED -> "Abholung geplant"; + case PICKED_UP -> "Abgeholt"; + case IN_TRANSIT -> "Unterwegs"; + case DELIVERED -> "Zugestellt"; + case COMPLETED -> "Abgeschlossen"; + case CANCELLED -> "Storniert"; + default -> status.toString(); }; } @@ -243,15 +211,17 @@ public class JobHistoryService { hasChanges = true; } - if (!equals(oldJob.getPickupCity(), newJob.getPickupCity()) || - !equals(oldJob.getDeliveryCity(), newJob.getDeliveryCity())) { - if (hasChanges) description.append(","); + if (!equals(oldJob.getPickupCity(), newJob.getPickupCity()) + || !equals(oldJob.getDeliveryCity(), newJob.getDeliveryCity())) { + if (hasChanges) + description.append(","); description.append(" - Orte"); hasChanges = true; } if (!equals(oldJob.getRemark(), newJob.getRemark())) { - if (hasChanges) description.append(","); + if (hasChanges) + description.append(","); description.append(" - Bemerkung"); hasChanges = true; } diff --git a/src/main/java/de/assecutor/votianlt/util/MailUtil.java b/src/main/java/de/assecutor/votianlt/util/MailUtil.java deleted file mode 100644 index 419c629..0000000 --- a/src/main/java/de/assecutor/votianlt/util/MailUtil.java +++ /dev/null @@ -1,54 +0,0 @@ -package de.assecutor.votianlt.util; - -import de.assecutor.votianlt.config.MailConfig; -import jakarta.mail.Message; -import jakarta.mail.MessagingException; -import jakarta.mail.PasswordAuthentication; -import jakarta.mail.Session; -import jakarta.mail.Transport; -import jakarta.mail.internet.InternetAddress; -import jakarta.mail.internet.MimeMessage; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import java.util.Properties; - -@Component -public class MailUtil { - - private final MailConfig mailConfig; - - @Autowired - public MailUtil(MailConfig mailConfig) { - this.mailConfig = mailConfig; - } - - public void sendMail(String to, String subject, String body) throws MessagingException { - // SMTP-Konfiguration aus externalisierter Konfiguration - final String username = mailConfig.getUsername(); - final String password = mailConfig.getPassword(); - final String host = mailConfig.getHost(); - final int port = mailConfig.getPort(); - - Properties props = new Properties(); - props.put("mail.smtp.auth", "true"); - props.put("mail.smtp.starttls.enable", "true"); - props.put("mail.smtp.starttls.required", "true"); - props.put("mail.smtp.host", host); - props.put("mail.smtp.port", String.valueOf(port)); - - Session session = Session.getInstance(props, new jakarta.mail.Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - return new PasswordAuthentication(username, password); - } - }); - - Message message = new MimeMessage(session); - message.setFrom(new InternetAddress(username)); - message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to)); - message.setSubject(subject); - message.setText(body); - - Transport.send(message); - } -} diff --git a/src/main/java/de/assecutor/votianlt/util/Util.java b/src/main/java/de/assecutor/votianlt/util/Util.java index 661e4c4..1b6a8a6 100644 --- a/src/main/java/de/assecutor/votianlt/util/Util.java +++ b/src/main/java/de/assecutor/votianlt/util/Util.java @@ -5,9 +5,8 @@ import com.vaadin.flow.component.applayout.AppLayout; public class Util { public static void changeDrawerState(boolean drawerState) { - AppLayout appLayout = (AppLayout) UI.getCurrent().getChildren() - .filter(AppLayout.class::isInstance) - .findFirst().orElse(null); + AppLayout appLayout = (AppLayout) UI.getCurrent().getChildren().filter(AppLayout.class::isInstance).findFirst() + .orElse(null); if (appLayout != null) { appLayout.setDrawerOpened(drawerState); diff --git a/src/main/java/de/assecutor/votianlt/zeroconf/ZeroconfPublisher.java b/src/main/java/de/assecutor/votianlt/zeroconf/ZeroconfPublisher.java index 4936387..b7d031b 100644 --- a/src/main/java/de/assecutor/votianlt/zeroconf/ZeroconfPublisher.java +++ b/src/main/java/de/assecutor/votianlt/zeroconf/ZeroconfPublisher.java @@ -1,2 +1,3 @@ // Zeroconf removed from project. -// This file intentionally left without any classes to eliminate any zeroconf-related beans or code. +// This file intentionally left without any classes to eliminate any +// zeroconf-related beans or code.