Erweiterungen

This commit is contained in:
2025-09-15 18:02:18 +02:00
parent ba9b47be89
commit 751836e8a4
79 changed files with 1855 additions and 2019 deletions

View File

@@ -0,0 +1,12 @@
{
"permissions": {
"allow": [
"Bash(./mvnw clean compile -q)",
"mcp__ide__getDiagnostics",
"Bash(find:*)",
"Bash(./mvnw:*)"
],
"deny": [],
"ask": []
}
}

View File

@@ -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;
}
}

View File

@@ -50,55 +50,55 @@ public class MongoConfig {
BaseTask task; BaseTask task;
switch (className) { switch (className) {
case "de.assecutor.votianlt.model.task.ConfirmationTask": case "de.assecutor.votianlt.model.task.ConfirmationTask":
case "ConfirmationTask": case "ConfirmationTask":
log.debug("Creating ConfirmationTask"); log.debug("Creating ConfirmationTask");
task = new ConfirmationTask(); task = new ConfirmationTask();
if (source.containsKey("button_text")) { if (source.containsKey("button_text")) {
((ConfirmationTask) task).setButtonText(source.getString("button_text")); ((ConfirmationTask) task).setButtonText(source.getString("button_text"));
} }
break; break;
case "de.assecutor.votianlt.model.task.SignatureTask": case "de.assecutor.votianlt.model.task.SignatureTask":
case "SignatureTask": case "SignatureTask":
log.debug("Creating SignatureTask"); log.debug("Creating SignatureTask");
task = new SignatureTask(); task = new SignatureTask();
break; break;
case "de.assecutor.votianlt.model.task.PhotoTask": case "de.assecutor.votianlt.model.task.PhotoTask":
case "PhotoTask": case "PhotoTask":
log.debug("Creating PhotoTask"); log.debug("Creating PhotoTask");
task = new PhotoTask(); task = new PhotoTask();
if (source.containsKey("min_photo_count")) { if (source.containsKey("min_photo_count")) {
((PhotoTask) task).setMinPhotoCount(source.getInteger("min_photo_count")); ((PhotoTask) task).setMinPhotoCount(source.getInteger("min_photo_count"));
} }
if (source.containsKey("max_photo_count")) { if (source.containsKey("max_photo_count")) {
((PhotoTask) task).setMaxPhotoCount(source.getInteger("max_photo_count")); ((PhotoTask) task).setMaxPhotoCount(source.getInteger("max_photo_count"));
} }
break; break;
case "de.assecutor.votianlt.model.task.TodoListTask": case "de.assecutor.votianlt.model.task.TodoListTask":
case "TodoListTask": case "TodoListTask":
log.debug("Creating TodoListTask"); log.debug("Creating TodoListTask");
task = new TodoListTask(); task = new TodoListTask();
if (source.containsKey("todo_items")) { if (source.containsKey("todo_items")) {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
List<String> todoItems = (List<String>) source.get("todo_items"); List<String> todoItems = (List<String>) source.get("todo_items");
((TodoListTask) task).setTodoItems(todoItems); ((TodoListTask) task).setTodoItems(todoItems);
} }
break; break;
case "de.assecutor.votianlt.model.task.BarcodeTask": case "de.assecutor.votianlt.model.task.BarcodeTask":
case "BarcodeTask": case "BarcodeTask":
log.debug("Creating BarcodeTask"); log.debug("Creating BarcodeTask");
task = new BarcodeTask(); task = new BarcodeTask();
if (source.containsKey("min_barcode_count")) { if (source.containsKey("min_barcode_count")) {
((BarcodeTask) task).setMinBarcodeCount(source.getInteger("min_barcode_count")); ((BarcodeTask) task).setMinBarcodeCount(source.getInteger("min_barcode_count"));
} }
if (source.containsKey("max_barcode_count")) { if (source.containsKey("max_barcode_count")) {
((BarcodeTask) task).setMaxBarcodeCount(source.getInteger("max_barcode_count")); ((BarcodeTask) task).setMaxBarcodeCount(source.getInteger("max_barcode_count"));
} }
break; break;
default: default:
log.warn("Unknown className '{}', falling back to ConfirmationTask", className); log.warn("Unknown className '{}', falling back to ConfirmationTask", className);
task = new ConfirmationTask(); // fallback task = new ConfirmationTask(); // fallback
break; break;
} }
// Set common fields // Set common fields
@@ -120,9 +120,11 @@ public class MongoConfig {
if (source.containsKey("completed_at") && source.get("completed_at") != null) { if (source.containsKey("completed_at") && source.get("completed_at") != null) {
Object completedAtObj = source.get("completed_at"); Object completedAtObj = source.get("completed_at");
if (completedAtObj instanceof String) { 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) { } 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")) { if (source.containsKey("completed_by")) {
@@ -137,18 +139,18 @@ public class MongoConfig {
return "de.assecutor.votianlt.model.task.ConfirmationTask"; return "de.assecutor.votianlt.model.task.ConfirmationTask";
} }
switch (taskType) { switch (taskType) {
case "CONFIRMATION": case "CONFIRMATION":
return "de.assecutor.votianlt.model.task.ConfirmationTask"; return "de.assecutor.votianlt.model.task.ConfirmationTask";
case "SIGNATURE": case "SIGNATURE":
return "de.assecutor.votianlt.model.task.SignatureTask"; return "de.assecutor.votianlt.model.task.SignatureTask";
case "PHOTO": case "PHOTO":
return "de.assecutor.votianlt.model.task.PhotoTask"; return "de.assecutor.votianlt.model.task.PhotoTask";
case "TODOLIST": case "TODOLIST":
return "de.assecutor.votianlt.model.task.TodoListTask"; return "de.assecutor.votianlt.model.task.TodoListTask";
case "BARCODE": case "BARCODE":
return "de.assecutor.votianlt.model.task.BarcodeTask"; return "de.assecutor.votianlt.model.task.BarcodeTask";
default: default:
return "de.assecutor.votianlt.model.task.ConfirmationTask"; return "de.assecutor.votianlt.model.task.ConfirmationTask";
} }
} }
} }

View File

@@ -31,28 +31,99 @@ public class MqttProperties {
/** Default retained flag for publishing */ /** Default retained flag for publishing */
private boolean defaultRetained = false; private boolean defaultRetained = false;
public boolean isEnabled() { return enabled; } public boolean isEnabled() {
public void setEnabled(boolean enabled) { this.enabled = enabled; } return enabled;
public String getBrokerUri() { return brokerUri; } }
public void setBrokerUri(String brokerUri) { this.brokerUri = brokerUri; }
public String getClientId() { return clientId; } public void setEnabled(boolean enabled) {
public void setClientId(String clientId) { this.clientId = clientId; } this.enabled = enabled;
public String getUsername() { return username; } }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; } public String getBrokerUri() {
public void setPassword(String password) { this.password = password; } return brokerUri;
public boolean isCleanStart() { return cleanStart; } }
public void setCleanStart(boolean cleanStart) { this.cleanStart = cleanStart; }
public long getSessionExpiryInterval() { return sessionExpiryInterval; } public void setBrokerUri(String brokerUri) {
public void setSessionExpiryInterval(long sessionExpiryInterval) { this.sessionExpiryInterval = sessionExpiryInterval; } this.brokerUri = brokerUri;
public int getKeepAlive() { return keepAlive; } }
public void setKeepAlive(int keepAlive) { this.keepAlive = keepAlive; }
public int getMaxInflight() { return maxInflight; } public String getClientId() {
public void setMaxInflight(int maxInflight) { this.maxInflight = maxInflight; } return clientId;
public boolean isAutomaticReconnect() { return automaticReconnect; } }
public void setAutomaticReconnect(boolean automaticReconnect) { this.automaticReconnect = automaticReconnect; }
public int getDefaultQos() { return defaultQos; } public void setClientId(String clientId) {
public void setDefaultQos(int defaultQos) { this.defaultQos = defaultQos; } this.clientId = clientId;
public boolean isDefaultRetained() { return defaultRetained; } }
public void setDefaultRetained(boolean defaultRetained) { this.defaultRetained = defaultRetained; }
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;
}
} }

View File

@@ -60,7 +60,10 @@ public class MessageController {
private final JobHistoryService jobHistoryService; private final JobHistoryService jobHistoryService;
private final EmailService emailService; 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.mqttPublisher = mqttPublisher;
this.appUserRepository = appUserRepository; this.appUserRepository = appUserRepository;
this.appUserService = appUserService; this.appUserService = appUserService;
@@ -75,20 +78,21 @@ public class MessageController {
} }
/** /**
* Authentication endpoint for mobile app users via MQTT. * Authentication endpoint for mobile app users via MQTT. Client sends to
* Client sends to /server/login with payload { email, password, clientId }. * /server/login with payload { email, password, clientId }. The response is
* The response is sent back to the requesting client on /client/{clientId}/auth * sent back to the requesting client on /client/{clientId}/auth
*/ */
public void handleAppLogin(AppLoginRequest request) { public void handleAppLogin(AppLoginRequest request) {
log.info("MQTT Endpoint '/server/login' called with email: {}, clientId: {}", log.info("MQTT Endpoint '/server/login' called with email: {}, clientId: {}",
request != null ? request.getEmail() : "null", request != null ? request.getEmail() : "null", request != null ? request.getClientId() : "null");
request != null ? request.getClientId() : "null");
AppLoginResponse response; AppLoginResponse response;
if (request == null || request.getEmail() == null || request.getPassword() == null || request.getClientId() == null if (request == null || request.getEmail() == null || request.getPassword() == null
|| request.getEmail().isBlank() || request.getPassword().isBlank() || request.getClientId().isBlank()) { || request.getClientId() == null || request.getEmail().isBlank() || request.getPassword().isBlank()
response = new AppLoginResponse(false, "E-Mail, Passwort und Client-ID sind erforderlich", null, null, null); || request.getClientId().isBlank()) {
response = new AppLoginResponse(false, "E-Mail, Passwort und Client-ID sind erforderlich", null, null,
null);
} else { } else {
AppUser user = appUserRepository.findByEmail(request.getEmail()); AppUser user = appUserRepository.findByEmail(request.getEmail());
if (user == null) { if (user == null) {
@@ -108,20 +112,21 @@ public class MessageController {
// Send response via MQTT to specific client // Send response via MQTT to specific client
if (request != null && request.getClientId() != null && !request.getClientId().isBlank()) { if (request != null && request.getClientId() != null && !request.getClientId().isBlank()) {
mqttPublisher.publishAsJson("/client/" + request.getClientId() + "/auth", response, false); mqttPublisher.publishAsJson("/client/" + request.getClientId() + "/auth", response, false);
log.info("MQTT Response sent to '/client/{}/auth': success={}, message='{}'", log.info("MQTT Response sent to '/client/{}/auth': success={}, message='{}'", request.getClientId(),
request.getClientId(), response.isSuccess(), response.getMessage()); response.isSuccess(), response.getMessage());
} }
} }
/** /**
* Endpoint to retrieve jobs assigned to a specific app user with related cargo items and tasks. * Endpoint to retrieve jobs assigned to a specific app user with related cargo
* Client sends to /server/{clientId}/jobs/assigned with payload { appUserId }. * items and tasks. Client sends to /server/{clientId}/jobs/assigned with
* The response is sent back to the requesting client on /client/{clientId}/jobs * payload { appUserId }. The response is sent back to the requesting client on
* /client/{clientId}/jobs
*/ */
public void handleGetAssignedJobs(Map<String, Object> request) { public void handleGetAssignedJobs(Map<String, Object> request) {
log.info("MQTT Endpoint '/server/{clientId}/jobs/assigned' called with data: {}", request); log.info("MQTT Endpoint '/server/{clientId}/jobs/assigned' called with data: {}", request);
log.debug("Starting to process jobs request for MQTT endpoint"); log.debug("Starting to process jobs request for MQTT endpoint");
if (request == null || !request.containsKey("appUserId")) { if (request == null || !request.containsKey("appUserId")) {
log.info("Assigned jobs request missing appUserId; returning empty list"); log.info("Assigned jobs request missing appUserId; returning empty list");
return; // Return empty list if no appUserId provided return; // Return empty list if no appUserId provided
@@ -133,12 +138,15 @@ public class MessageController {
return; // Return empty list if appUserId is blank 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; String clientId = null;
try { try {
Object cid = request.get("clientId"); Object cid = request.get("clientId");
if (cid != null) clientId = cid.toString(); if (cid != null)
} catch (Exception ignored) {} clientId = cid.toString();
} catch (Exception ignored) {
}
if (clientId == null || clientId.isBlank()) { if (clientId == null || clientId.isBlank()) {
clientId = getClientIdForUserId(appUserId); clientId = getClientIdForUserId(appUserId);
} }
@@ -148,26 +156,26 @@ public class MessageController {
log.debug("Found {} jobs for appUserId: {}", assignedJobs.size(), appUserId); log.debug("Found {} jobs for appUserId: {}", assignedJobs.size(), appUserId);
// For each job, fetch related cargo items and tasks (ordered by task order) // For each job, fetch related cargo items and tasks (ordered by task order)
List<JobWithRelatedDataDTO> jobsWithRelatedData = assignedJobs.stream() List<JobWithRelatedDataDTO> jobsWithRelatedData = assignedJobs.stream().map(job -> {
.map(job -> { List<CargoItem> cargoItems = cargoItemRepository.findByJobId(job.getId());
List<CargoItem> cargoItems = cargoItemRepository.findByJobId(job.getId()); List<BaseTask> tasks = taskRepository.findByJobIdOrderByTaskOrderAsc(job.getId());
List<BaseTask> tasks = taskRepository.findByJobIdOrderByTaskOrderAsc(job.getId());
// Log task details for debugging // Log task details for debugging
tasks.forEach(task -> log.info("Task details for job {}: type={}, order={}", tasks.forEach(task -> log.info("Task details for job {}: type={}, order={}", job.getId(),
job.getId(), task.getTaskType(), task.getTaskOrder())); 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 // Publish to the requesting client's topic if clientId is known
if (clientId != null && !clientId.isBlank()) { if (clientId != null && !clientId.isBlank()) {
String topic = "/client/" + clientId + "/jobs"; String topic = "/client/" + clientId + "/jobs";
mqttPublisher.publishAsJson(topic, jobsWithRelatedData, false); 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 { } 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 // Log complete JSON for debugging
@@ -189,10 +197,10 @@ public class MessageController {
} }
/** /**
* Report generic task completion from apps. * Report generic task completion from apps. Client sends to /app/task/completed
* Client sends to /app/task/completed with payload { taskId, completedBy?, note? }. * with payload { taskId, completedBy?, note? }. Broadcasts to
* Broadcasts to /topic/task-updates and /topic/tasks/{taskId}. * /topic/task-updates and /topic/tasks/{taskId}. This endpoint accepts any task
* This endpoint accepts any task type (fallback for GENERIC or unknown types). * type (fallback for GENERIC or unknown types).
*/ */
public void handleTaskCompleted(Map<String, Object> payload) { public void handleTaskCompleted(Map<String, Object> payload) {
// Backward-compatible entry point: extract taskType from payload (if present) // Backward-compatible entry point: extract taskType from payload (if present)
@@ -200,14 +208,17 @@ public class MessageController {
String taskType = null; String taskType = null;
try { try {
Object tt = payload != null ? payload.get("taskType") : null; Object tt = payload != null ? payload.get("taskType") : null;
if (tt != null) taskType = tt.toString(); if (tt != null)
} catch (Exception ignored) {} taskType = tt.toString();
} catch (Exception ignored) {
}
handleTaskCompleted(payload, taskType); handleTaskCompleted(payload, taskType);
} }
/** /**
* Central dispatcher for task_completed messages. Decides handling based on taskType. * Central dispatcher for task_completed messages. Decides handling based on
* PHOTO and CONFIRMATION are routed to specialized handlers; others go to generic processing. * taskType. PHOTO and CONFIRMATION are routed to specialized handlers; others
* go to generic processing.
*/ */
public void handleTaskCompleted(Map<String, Object> payload, String taskType) { public void handleTaskCompleted(Map<String, Object> payload, String taskType) {
String key = taskType == null ? "" : taskType.trim().toUpperCase(); String key = taskType == null ? "" : taskType.trim().toUpperCase();
@@ -215,24 +226,24 @@ public class MessageController {
log.info("handleTaskCompleted called with taskType={}, data: {}", taskType, payload); log.info("handleTaskCompleted called with taskType={}, data: {}", taskType, payload);
switch (key) { switch (key) {
case "PHOTO" -> { case "PHOTO" -> {
processPhotoTaskCompletion(payload); processPhotoTaskCompletion(payload);
} }
case "CONFIRMATION" -> { case "CONFIRMATION" -> {
processConfirmationTaskCompletion(payload); processConfirmationTaskCompletion(payload);
} }
case "SIGNATURE" -> { case "SIGNATURE" -> {
processSignatureTaskCompletion(payload); processSignatureTaskCompletion(payload);
} }
case "TODOLIST" -> { case "TODOLIST" -> {
processTodoListTaskCompletion(payload); processTodoListTaskCompletion(payload);
} }
case "BARCODE" -> { case "BARCODE" -> {
processBarcodeTaskCompletion(payload); processBarcodeTaskCompletion(payload);
} }
default -> { default -> {
log.info("ERROR: handleTaskCompleted called with taskType={}, data: {}", taskType, payload); log.info("ERROR: handleTaskCompleted called with taskType={}, data: {}", taskType, payload);
} }
} }
} }
@@ -266,16 +277,15 @@ public class MessageController {
if (!barcodes.isEmpty()) { if (!barcodes.isEmpty()) {
for (String barcodeString : barcodes) { for (String barcodeString : barcodes) {
Barcode barcodeEntry = new Barcode( Barcode barcodeEntry = new Barcode(new ObjectId(taskId.toString()), barcodeString,
new ObjectId(taskId.toString()), task.getCompletedBy());
barcodeString,
task.getCompletedBy()
);
barcodeRepository.save(barcodeEntry); 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); log.info("Saved {} barcodes for taskId={}", barcodes.size(), taskId);
} else { } else {
extraDataSummary = "Keine Barcodes gescannt"; extraDataSummary = "Keine Barcodes gescannt";
@@ -315,11 +325,8 @@ public class MessageController {
Object signatureSvgObj = extraData.get("signatureSvg"); Object signatureSvgObj = extraData.get("signatureSvg");
if (signatureSvgObj instanceof String signatureSvg) { if (signatureSvgObj instanceof String signatureSvg) {
if (!signatureSvg.isBlank()) { if (!signatureSvg.isBlank()) {
Signature signatureEntry = new Signature( Signature signatureEntry = new Signature(new ObjectId(taskId.toString()), signatureSvg,
new ObjectId(taskId.toString()), task.getCompletedBy());
signatureSvg,
task.getCompletedBy()
);
signatureRepository.save(signatureEntry); signatureRepository.save(signatureEntry);
extraDataSummary = "Unterschrift erfasst (SVG, " + signatureSvg.length() + " Zeichen)"; extraDataSummary = "Unterschrift erfasst (SVG, " + signatureSvg.length() + " Zeichen)";
@@ -366,12 +373,9 @@ public class MessageController {
List<String> photos = (List<String>) photosList; List<String> photos = (List<String>) photosList;
if (!photos.isEmpty()) { if (!photos.isEmpty()) {
for (String photoString: photos) { for (String photoString : photos) {
Photo photoEntry = new Photo( Photo photoEntry = new Photo(new ObjectId(taskId.toString()), photoString,
new ObjectId(taskId.toString()), task.getCompletedBy());
photoString,
task.getCompletedBy()
);
photoRepository.save(photoEntry); photoRepository.save(photoEntry);
} }
@@ -424,8 +428,8 @@ public class MessageController {
String taskType = task.getTaskType() != null ? task.getTaskType().toString() : "Unknown"; String taskType = task.getTaskType() != null ? task.getTaskType().toString() : "Unknown";
String taskDisplayName = task.getDisplayName() != null ? task.getDisplayName() : taskType; String taskDisplayName = task.getDisplayName() != null ? task.getDisplayName() : taskType;
jobHistoryService.logTaskCompletion(jobId, taskType, taskIdStr, task.getCompletedBy(), jobHistoryService.logTaskCompletion(jobId, taskType, taskIdStr, task.getCompletedBy(), taskDisplayName,
taskDisplayName, extraDataSummary); extraDataSummary);
} catch (Exception e) { } catch (Exception e) {
log.warn("Failed to log task completion history for task {}: {}", taskIdStr, e.getMessage()); 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 // Check if this was the last task and send job completion notification
emailService.checkAndSendJobCompletionNotification(jobId, completedBy); emailService.checkAndSendJobCompletionNotification(jobId, completedBy);
} catch (Exception e) { } 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={}", log.info("Task marked completed. taskId={}, completedBy={}, extraData={}", taskIdStr, task.getCompletedBy(),
taskIdStr, task.getCompletedBy(), extraDataSummary); extraDataSummary);
} catch (IllegalArgumentException ex) { } catch (IllegalArgumentException ex) {
log.error("Invalid taskId format for completion: {}", taskIdStr); log.error("Invalid taskId format for completion: {}", taskIdStr);
} catch (Exception ex) { } catch (Exception ex) {

View File

@@ -10,8 +10,8 @@ import lombok.NoArgsConstructor;
import java.util.List; import java.util.List;
/** /**
* DTO for returning job data with related cargo items and tasks. * DTO for returning job data with related cargo items and tasks. This combines
* This combines Job entity with its associated CargoItems and TaskEntries. * Job entity with its associated CargoItems and TaskEntries.
*/ */
@Data @Data
@NoArgsConstructor @NoArgsConstructor

View File

@@ -74,8 +74,8 @@ public class AppUser {
} }
/** /**
* Returns the ObjectId as string for JSON serialization. * Returns the ObjectId as string for JSON serialization. This ensures that the
* This ensures that the app user id is returned as a string when users are retrieved via API. * app user id is returned as a string when users are retrieved via API.
*/ */
@JsonGetter("id") @JsonGetter("id")
public String getIdAsString() { public String getIdAsString() {

View File

@@ -8,8 +8,8 @@ import org.bson.types.ObjectId;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/** /**
* Barcode entity for storing barcode data from task completions. * Barcode entity for storing barcode data from task completions. References the
* References the task ObjectId and stores barcode strings. * task ObjectId and stores barcode strings.
*/ */
@Data @Data
@Document(collection = "barcodes") @Document(collection = "barcodes")

View File

@@ -41,12 +41,11 @@ public class CargoItem {
private Double heightMm; private Double heightMm;
/** /**
* Returns the ObjectId as string for JSON serialization. * Returns the ObjectId as string for JSON serialization. This ensures that the
* This ensures that the cargo item id is returned as a string when items are retrieved via API. * cargo item id is returned as a string when items are retrieved via API.
*/ */
@JsonGetter("id") @JsonGetter("id")
public String getIdAsString() { public String getIdAsString() {
return id != null ? id.toString() : null; return id != null ? id.toString() : null;
} }
} }

View File

@@ -4,8 +4,7 @@ import lombok.Data;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
@Data @Data
public class Company public class Company {
{
private ObjectId id; private ObjectId id;
private String name; private String name;

View File

@@ -8,8 +8,7 @@ import org.springframework.data.mongodb.core.mapping.Field;
@Data @Data
@Document(collection = "customers") @Document(collection = "customers")
public class Customer public class Customer {
{
@Id @Id
private ObjectId id; private ObjectId id;

View File

@@ -16,4 +16,3 @@ public class Invoice {
private double betrag; private double betrag;
private String beschreibung; private String beschreibung;
} }

View File

@@ -127,8 +127,8 @@ public class Job {
private BigDecimal price; private BigDecimal price;
/** /**
* Returns the ObjectId as string for JSON serialization. * Returns the ObjectId as string for JSON serialization. This ensures that the
* This ensures that the job id is returned as a string when jobs are retrieved via API. * job id is returned as a string when jobs are retrieved via API.
*/ */
@JsonGetter("id") @JsonGetter("id")
public String getIdAsString() { public String getIdAsString() {

View File

@@ -8,8 +8,8 @@ import org.bson.types.ObjectId;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/** /**
* Job History entity for tracking all changes made to a job. * Job History entity for tracking all changes made to a job. Each entry
* Each entry represents a single change or action performed on a job. * represents a single change or action performed on a job.
*/ */
@Data @Data
@Document(collection = "job_history") @Document(collection = "job_history")
@@ -34,7 +34,8 @@ public class JobHistory {
private String reason; 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; private String description;
@@ -78,8 +79,8 @@ public class JobHistory {
} }
// Constructor for detailed history entry // Constructor for detailed history entry
public JobHistory(ObjectId jobId, String reason, String description, String changedBy, public JobHistory(ObjectId jobId, String reason, String description, String changedBy, JobHistoryType changeType,
JobHistoryType changeType, String oldValue, String newValue) { String oldValue, String newValue) {
this(jobId, reason, description, changedBy); this(jobId, reason, description, changedBy);
this.changeType = changeType; this.changeType = changeType;
this.oldValue = oldValue; this.oldValue = oldValue;

View File

@@ -9,8 +9,8 @@ import java.time.LocalDateTime;
import java.util.List; import java.util.List;
/** /**
* Photo entity for storing photo data from task completions. * Photo entity for storing photo data from task completions. References the job
* References the job ObjectId and stores base64 encoded photos. * ObjectId and stores base64 encoded photos.
*/ */
@Data @Data
@Document(collection = "photos") @Document(collection = "photos")

View File

@@ -45,8 +45,8 @@ public class TaskEntry {
private String completedBy; private String completedBy;
/** /**
* Returns the ObjectId as string for JSON serialization. * Returns the ObjectId as string for JSON serialization. This ensures that the
* This ensures that the task id is returned as a string when jobs are retrieved via API. * task id is returned as a string when jobs are retrieved via API.
*/ */
@JsonGetter("id") @JsonGetter("id")
public String getIdAsString() { public String getIdAsString() {
@@ -54,8 +54,8 @@ public class TaskEntry {
} }
/** /**
* Returns the job ObjectId as string for JSON serialization. * Returns the job ObjectId as string for JSON serialization. This ensures that
* This ensures that the job id is returned as a string instead of ObjectId object. * the job id is returned as a string instead of ObjectId object.
*/ */
@JsonGetter("jobId") @JsonGetter("jobId")
public String getJobIdAsString() { public String getJobIdAsString() {
@@ -66,7 +66,8 @@ public class TaskEntry {
* Enum for different task types * Enum for different task types
*/ */
public enum TaskType { public enum TaskType {
CONFIRMATION("Bestätigung"), CONFIRMATION(
"Bestätigung"),
SIGNATURE("Unterschrift"), SIGNATURE("Unterschrift"),
TODOLIST("To-Do Liste"), TODOLIST("To-Do Liste"),
PHOTO("Foto"), PHOTO("Foto"),
@@ -108,4 +109,3 @@ public class TaskEntry {
private Map<String, Object> additionalConfig; private Map<String, Object> additionalConfig;
} }
} }

View File

@@ -18,16 +18,16 @@ public class User {
private int usrId; private int usrId;
private String title; private String title;
private String name; // Nachname private String name; // Nachname
private String firstname; // Vorname private String firstname; // Vorname
// Firmen-/Adressdaten // Firmen-/Adressdaten
private String company; // Firma private String company; // Firma
private String street; // Straße private String street; // Straße
private String houseNumber; // Hausnr private String houseNumber; // Hausnr
private String addressAddition; // Adresszusatz (optional) private String addressAddition; // Adresszusatz (optional)
private String zip; // Postleitzahl private String zip; // Postleitzahl
private String city; // Stadt private String city; // Stadt
@Indexed(unique = true) @Indexed(unique = true)
private String email; private String email;

View File

@@ -17,13 +17,11 @@ import java.time.LocalDateTime;
@NoArgsConstructor @NoArgsConstructor
@Document(collection = "tasks") @Document(collection = "tasks")
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "taskType") @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "taskType")
@JsonSubTypes({ @JsonSubTypes({ @JsonSubTypes.Type(value = ConfirmationTask.class, name = "CONFIRMATION"),
@JsonSubTypes.Type(value = ConfirmationTask.class, name = "CONFIRMATION"), @JsonSubTypes.Type(value = SignatureTask.class, name = "SIGNATURE"),
@JsonSubTypes.Type(value = SignatureTask.class, name = "SIGNATURE"), @JsonSubTypes.Type(value = TodoListTask.class, name = "TODOLIST"),
@JsonSubTypes.Type(value = TodoListTask.class, name = "TODOLIST"), @JsonSubTypes.Type(value = PhotoTask.class, name = "PHOTO"),
@JsonSubTypes.Type(value = PhotoTask.class, name = "PHOTO"), @JsonSubTypes.Type(value = BarcodeTask.class, name = "BARCODE") })
@JsonSubTypes.Type(value = BarcodeTask.class, name = "BARCODE")
})
public abstract class BaseTask { public abstract class BaseTask {
@Id @Id
@JsonIgnore @JsonIgnore

View File

@@ -9,7 +9,6 @@ import lombok.NoArgsConstructor;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class SignatureTask extends BaseTask { public class SignatureTask extends BaseTask {
@Override @Override
public String getTaskType() { public String getTaskType() {
return "SIGNATURE"; return "SIGNATURE";

View File

@@ -1,11 +1,7 @@
package de.assecutor.votianlt.model.task; package de.assecutor.votianlt.model.task;
public enum TaskType { public enum TaskType {
CONFIRMATION("Bestätigung"), CONFIRMATION("Bestätigung"), SIGNATURE("Unterschrift"), TODOLIST("To-Do Liste"), PHOTO("Foto"), BARCODE("Barcode");
SIGNATURE("Unterschrift"),
TODOLIST("To-Do Liste"),
PHOTO("Foto"),
BARCODE("Barcode");
private final String displayName; private final String displayName;

View File

@@ -6,8 +6,8 @@ import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
* Kept for compatibility: The actual MQTT v5 lifecycle is managed by MqttV5ClientManager. * Kept for compatibility: The actual MQTT v5 lifecycle is managed by
* This runner only logs application readiness. * MqttV5ClientManager. This runner only logs application readiness.
*/ */
@Component @Component
@Slf4j @Slf4j

View File

@@ -9,11 +9,13 @@ import org.springframework.context.annotation.Lazy;
/** /**
* Simple MQTT publishing helper to send JSON payloads. * Simple MQTT publishing helper to send JSON payloads.
* *
* Note: In environments where Spring Integration MQTT is unavailable (e.g., offline CI), * Note: In environments where Spring Integration MQTT is unavailable (e.g.,
* this implementation degrades to a no-op publisher that logs the intended message. * offline CI), this implementation degrades to a no-op publisher that logs the
* intended message.
*/ */
public interface MqttPublisher { public interface MqttPublisher {
void publishAsJson(String topic, Object payload); void publishAsJson(String topic, Object payload);
void publishAsJson(String topic, Object payload, boolean retained); void publishAsJson(String topic, Object payload, boolean retained);
} }

View File

@@ -20,7 +20,8 @@ import java.util.Map;
import java.util.UUID; 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 @Service
@Slf4j @Slf4j
@@ -50,23 +51,15 @@ public class MqttV5ClientManager implements SmartLifecycle {
String host = uri.getHost(); String host = uri.getHost();
int port = 42099; int port = 42099;
var builder = Mqtt5Client.builder() var builder = Mqtt5Client.builder().identifier(clientId).serverHost(host).serverPort(port);
.identifier(clientId)
.serverHost(host)
.serverPort(port);
if (props.isAutomaticReconnect()) { if (props.isAutomaticReconnect()) {
builder = builder.automaticReconnectWithDefaultConfig(); builder = builder.automaticReconnectWithDefaultConfig();
} }
client = builder.buildAsync(); client = builder.buildAsync();
var connect = client.connectWith() var connect = client.connectWith().cleanStart(props.isCleanStart()).keepAlive(props.getKeepAlive())
.cleanStart(props.isCleanStart()) .sessionExpiryInterval(props.getSessionExpiryInterval()).simpleAuth().username("app")
.keepAlive(props.getKeepAlive()) .password("apppwd".getBytes(StandardCharsets.UTF_8)).applySimpleAuth();
.sessionExpiryInterval(props.getSessionExpiryInterval())
.simpleAuth()
.username("app")
.password("apppwd".getBytes(StandardCharsets.UTF_8))
.applySimpleAuth();
log.info("[MQTT] Connecting to {} with clientId={} ...", props.getBrokerUri(), clientId); log.info("[MQTT] Connecting to {} with clientId={} ...", props.getBrokerUri(), clientId);
connect.send().join(); connect.send().join();
@@ -86,15 +79,9 @@ public class MqttV5ClientManager implements SmartLifecycle {
}); });
// Subscribe to topics with QoS // Subscribe to topics with QoS
String[] topics = new String[]{ String[] topics = new String[] { "/server/+/task/photo/completed", "/server/+/task/confirm",
"/server/+/task/photo/completed", "/server/+/task/completed", "/server/+/task_completed", "/server/+/job/status",
"/server/+/task/confirm", "/server/+/jobs/assigned", "/server/login" };
"/server/+/task/completed",
"/server/+/task_completed",
"/server/+/job/status",
"/server/+/jobs/assigned",
"/server/login"
};
MqttQos qos = mapQos(props.getDefaultQos()); MqttQos qos = mapQos(props.getDefaultQos());
for (String topic : topics) { for (String topic : topics) {
client.subscribeWith().topicFilter(topic).qos(qos).send().join(); client.subscribeWith().topicFilter(topic).qos(qos).send().join();
@@ -123,7 +110,8 @@ public class MqttV5ClientManager implements SmartLifecycle {
private void handleInbound(String topic, byte[] payload) { private void handleInbound(String topic, byte[] payload) {
String json = new String(payload, StandardCharsets.UTF_8); String json = new String(payload, StandardCharsets.UTF_8);
try { try {
Map<String, Object> map = objectMapper.readValue(json, new TypeReference<Map<String, Object>>(){}); Map<String, Object> map = objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {
});
routeInbound(topic, map); routeInbound(topic, map);
} catch (Exception ex) { } catch (Exception ex) {
log.error("Failed to parse inbound MQTT JSON on {}: {}", topic, ex.getMessage()); log.error("Failed to parse inbound MQTT JSON on {}: {}", topic, ex.getMessage());
@@ -134,8 +122,10 @@ public class MqttV5ClientManager implements SmartLifecycle {
try { try {
// The consolidated topic /server/{clientId}/task_completed is used by apps to // The consolidated topic /server/{clientId}/task_completed is used by apps to
// report completion of any task type. Only PHOTO and CONFIRMATION require // 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 // specialized processing on the server side. All other task types are handled
// generic handler handleTaskCompleted(). This keeps routing simple while allowing // by the
// generic handler handleTaskCompleted(). This keeps routing simple while
// allowing
// special logic (e.g., photo persistence) where necessary. // special logic (e.g., photo persistence) where necessary.
if (topic.matches("/server/.+/task_completed")) { if (topic.matches("/server/.+/task_completed")) {
try { try {
@@ -161,7 +151,8 @@ public class MqttV5ClientManager implements SmartLifecycle {
} }
} else if (topic.equals("/server/login")) { } else if (topic.equals("/server/login")) {
var om = new ObjectMapper(); 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); messageController.handleAppLogin(req);
} else { } else {
log.debug("No route for topic {}", topic); log.debug("No route for topic {}", topic);
@@ -192,9 +183,9 @@ public class MqttV5ClientManager implements SmartLifecycle {
private MqttQos mapQos(int q) { private MqttQos mapQos(int q) {
return switch (q) { return switch (q) {
case 0 -> MqttQos.AT_MOST_ONCE; case 0 -> MqttQos.AT_MOST_ONCE;
case 1 -> MqttQos.AT_LEAST_ONCE; case 1 -> MqttQos.AT_LEAST_ONCE;
default -> MqttQos.EXACTLY_ONCE; default -> MqttQos.EXACTLY_ONCE;
}; };
} }
@@ -204,12 +195,7 @@ public class MqttV5ClientManager implements SmartLifecycle {
log.warn("[MQTT] Not connected, dropping publish topic={}", topic); log.warn("[MQTT] Not connected, dropping publish topic={}", topic);
return; return;
} }
client.publishWith() client.publishWith().topic(topic).payload(payload).qos(mapQos(qos)).retain(retained).send()
.topic(topic)
.payload(payload)
.qos(mapQos(qos))
.retain(retained)
.send()
.whenComplete((ack, ex) -> { .whenComplete((ack, ex) -> {
if (ex != null) { if (ex != null) {
log.error("Failed to publish to {}: {}", topic, ex.getMessage(), ex); log.error("Failed to publish to {}: {}", topic, ex.getMessage(), ex);

View File

@@ -24,6 +24,7 @@ import de.assecutor.votianlt.pages.view.EditProfileView;
import de.assecutor.votianlt.security.SecurityService; import de.assecutor.votianlt.security.SecurityService;
import static com.vaadin.flow.theme.lumo.LumoUtility.*; import static com.vaadin.flow.theme.lumo.LumoUtility.*;
@AnonymousAllowed @AnonymousAllowed
@Layout @Layout
@@ -37,8 +38,9 @@ public final class MainLayout extends AppLayout {
public MainLayout(SecurityService securityService) { public MainLayout(SecurityService securityService) {
this.securityService = securityService; this.securityService = securityService;
setPrimarySection(Section.DRAWER); 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(); headerRef = createHeader();
navRef = new Scroller(createSideNav()); navRef = new Scroller(createSideNav());
userMenuRef = createUserMenu(); userMenuRef = createUserMenu();
@@ -52,9 +54,12 @@ public final class MainLayout extends AppLayout {
private void updateDrawerVisibility() { private void updateDrawerVisibility() {
boolean loggedIn = securityService.isUserLoggedIn(); boolean loggedIn = securityService.isUserLoggedIn();
if (headerRef != null) headerRef.setVisible( loggedIn ); if (headerRef != null)
if (navRef != null) navRef.setVisible( loggedIn ); headerRef.setVisible(loggedIn);
if (userMenuRef != null) userMenuRef.setVisible( loggedIn ); if (navRef != null)
navRef.setVisible(loggedIn);
if (userMenuRef != null)
userMenuRef.setVisible(loggedIn);
setDrawerOpened(loggedIn); setDrawerOpened(loggedIn);
} }
@@ -121,7 +126,8 @@ public final class MainLayout extends AppLayout {
userContent.add(profile, myInvoices, imprint); userContent.add(profile, myInvoices, imprint);
userDetails.add(userContent); 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(); VerticalLayout navContainer = new VerticalLayout();
navContainer.setPadding(false); navContainer.setPadding(false);
navContainer.setSpacing(false); navContainer.setSpacing(false);
@@ -150,13 +156,12 @@ public final class MainLayout extends AppLayout {
avatar.setColorIndex(5); avatar.setColorIndex(5);
var userNameSpan = new Span(); var userNameSpan = new Span();
var userMenuItem = userMenu.addItem(avatar); var userMenuItem = userMenu.addItem(avatar);
userMenuItem.add(userNameSpan); userMenuItem.add(userNameSpan);
// Profil anzeigen mit Navigation // Profil anzeigen mit Navigation
userMenuItem.getSubMenu().addItem("Profil anzeigen", e -> userMenuItem.getSubMenu().addItem("Profil anzeigen", e -> UI.getCurrent().navigate(EditProfileView.class));
UI.getCurrent().navigate(EditProfileView.class));
userMenuItem.getSubMenu().addItem("Einstellungen"); userMenuItem.getSubMenu().addItem("Einstellungen");
userMenuItem.getSubMenu().addItem("Abmelden", e -> securityService.logout()); userMenuItem.getSubMenu().addItem("Abmelden", e -> securityService.logout());
@@ -166,7 +171,7 @@ public final class MainLayout extends AppLayout {
avatar.setName(currentUser); avatar.setName(currentUser);
userNameSpan.setText(currentUser); userNameSpan.setText(currentUser);
}; };
// Initial und bei Attach aktualisieren // Initial und bei Attach aktualisieren
updateUserInfo.run(); updateUserInfo.run();
addAttachListener(e -> updateUserInfo.run()); addAttachListener(e -> updateUserInfo.run());

View File

@@ -5,5 +5,4 @@ import org.springframework.data.mongodb.repository.MongoRepository;
public interface AddCompanyRepository extends MongoRepository<Company, String> { public interface AddCompanyRepository extends MongoRepository<Company, String> {
} }

View File

@@ -6,5 +6,4 @@ import org.springframework.data.mongodb.repository.MongoRepository;
public interface AddCustomerRepository extends MongoRepository<Customer, ObjectId> { public interface AddCustomerRepository extends MongoRepository<Customer, ObjectId> {
} }

View File

@@ -7,7 +7,5 @@ import java.util.Optional;
public interface LoginRepository extends MongoRepository<User, String> { public interface LoginRepository extends MongoRepository<User, String> {
Optional<User> findByEmail(String email); Optional<User> findByEmail(String email);
} }

View File

@@ -15,8 +15,6 @@ public class AddCompanyService {
this.addCompanyRepository = addCompanyRepository; this.addCompanyRepository = addCompanyRepository;
} }
public void addCompany(Company company) { public void addCompany(Company company) {
addCompanyRepository.save(company); addCompanyRepository.save(company);
} }

View File

@@ -18,8 +18,6 @@ public class AddCustomerService {
this.securityService = securityService; this.securityService = securityService;
} }
public void addCustomer(Customer customer) { public void addCustomer(Customer customer) {
// Setze den aktuellen Benutzer als Ersteller - jetzt direkt aus der Session // Setze den aktuellen Benutzer als Ersteller - jetzt direkt aus der Session
de.assecutor.votianlt.model.User currentUser = securityService.getCurrentDatabaseUser(); de.assecutor.votianlt.model.User currentUser = securityService.getCurrentDatabaseUser();

View File

@@ -32,10 +32,14 @@ public class AddJobService {
private final SecurityService securityService; private final SecurityService securityService;
private final JobHistoryService jobHistoryService; private final JobHistoryService jobHistoryService;
private final EmailService emailService; private final EmailService emailService;
/** /**
* Speichert einen neuen Auftrag samt CargoItems und Tasks * 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<CargoItem> transientCargo, List<BaseTask> transientTasks) { public Job addJobWithCargo(Job job, List<CargoItem> transientCargo, List<BaseTask> transientTasks) {
try { try {
@@ -58,10 +62,8 @@ public class AddJobService {
// CargoItems separat mit Referenz auf Job speichern, IDs im Job verknüpfen // CargoItems separat mit Referenz auf Job speichern, IDs im Job verknüpfen
if (transientCargo != null && !transientCargo.isEmpty()) { if (transientCargo != null && !transientCargo.isEmpty()) {
List<CargoItem> itemsWithJob = transientCargo.stream() List<CargoItem> itemsWithJob = transientCargo.stream().filter(Objects::nonNull)
.filter(Objects::nonNull) .filter(ci -> ci.getDescription() != null && !ci.getDescription().isBlank()).map(ci -> {
.filter(ci -> ci.getDescription() != null && !ci.getDescription().isBlank())
.map(ci -> {
CargoItem copy = new CargoItem(); CargoItem copy = new CargoItem();
copy.setJobId(jobId); copy.setJobId(jobId);
copy.setDescription(ci.getDescription()); copy.setDescription(ci.getDescription());
@@ -75,13 +77,12 @@ public class AddJobService {
cargoItemRepository.saveAll(itemsWithJob); cargoItemRepository.saveAll(itemsWithJob);
modified = true; modified = true;
} }
// Tasks separat speichern und referenzieren mit korrekter Nummerierung // Tasks separat speichern und referenzieren mit korrekter Nummerierung
if (transientTasks != null && !transientTasks.isEmpty()) { if (transientTasks != null && !transientTasks.isEmpty()) {
var filteredTasks = transientTasks.stream() var filteredTasks = transientTasks.stream().filter(Objects::nonNull)
.filter(Objects::nonNull) .filter(task -> task.getTaskType() != null) // Filter nach TaskType statt Text
.filter(task -> task.getTaskType() != null) // Filter nach TaskType statt Text .toList();
.toList();
// Setze JobId und stelle sicher, dass taskOrder korrekt ist // Setze JobId und stelle sicher, dass taskOrder korrekt ist
for (int i = 0; i < filteredTasks.size(); i++) { for (int i = 0; i < filteredTasks.size(); i++) {
@@ -113,7 +114,8 @@ public class AddJobService {
try { try {
emailService.sendJobCreationNotification(savedJob.getId(), savedJob.getCreatedBy()); emailService.sendJobCreationNotification(savedJob.getId(), savedJob.getCreatedBy());
} catch (Exception e) { } 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()); log.info("Auftrag erfolgreich gespeichert: {}", savedJob.getJobNumber());
@@ -135,8 +137,7 @@ public class AddJobService {
// Zähle Aufträge des aktuellen Tages // Zähle Aufträge des aktuellen Tages
String todayPrefix = prefix + timestamp; String todayPrefix = prefix + timestamp;
long todayCount = jobRepository.findAll().stream() long todayCount = jobRepository.findAll().stream()
.filter(job -> job.getJobNumber() != null && job.getJobNumber().startsWith(todayPrefix)) .filter(job -> job.getJobNumber() != null && job.getJobNumber().startsWith(todayPrefix)).count();
.count();
// Generiere neue Nummer // Generiere neue Nummer
String jobNumber; String jobNumber;

View File

@@ -27,12 +27,12 @@ public class AppDeviceService {
// Set creation and update metadata // Set creation and update metadata
appDevice.setErstelltAm(java.time.LocalDateTime.now()); appDevice.setErstelltAm(java.time.LocalDateTime.now());
appDevice.setAktualisiertAm(java.time.LocalDateTime.now()); appDevice.setAktualisiertAm(java.time.LocalDateTime.now());
// Set creator and updater - current user ID is required // Set creator and updater - current user ID is required
ObjectId currentUserId = securityService.getCurrentUserId(); ObjectId currentUserId = securityService.getCurrentUserId();
appDevice.setErstelltVon(currentUserId); appDevice.setErstelltVon(currentUserId);
appDevice.setAktualisiertVon(currentUserId); appDevice.setAktualisiertVon(currentUserId);
return appDeviceRepository.save(appDevice); return appDeviceRepository.save(appDevice);
} }

View File

@@ -32,17 +32,17 @@ public class AppUserService {
String hashedPassword = passwordEncoder.encode(appUser.getPassword()); String hashedPassword = passwordEncoder.encode(appUser.getPassword());
appUser.setPassword(hashedPassword); appUser.setPassword(hashedPassword);
} }
// Set creation and update metadata // Set creation and update metadata
appUser.setErstelltAm(java.time.LocalDateTime.now()); appUser.setErstelltAm(java.time.LocalDateTime.now());
appUser.setAktualisiertAm(java.time.LocalDateTime.now()); appUser.setAktualisiertAm(java.time.LocalDateTime.now());
// Set creator and updater - current user ID is required // Set creator and updater - current user ID is required
ObjectId currentUserId = securityService.getCurrentUserId(); ObjectId currentUserId = securityService.getCurrentUserId();
appUser.setErstelltVon(currentUserId); appUser.setErstelltVon(currentUserId);
appUser.setAktualisiertVon(currentUserId); appUser.setAktualisiertVon(currentUserId);
appUser.setOwner(currentUserId); appUser.setOwner(currentUserId);
return appUserRepository.save(appUser); return appUserRepository.save(appUser);
} }
@@ -62,22 +62,26 @@ public class AppUserService {
public AppUser updateAppUser(AppUser appUser) { public AppUser updateAppUser(AppUser appUser) {
// Hash the password if it's being updated and not empty // Hash the password if it's being updated and not empty
if (appUser.getPassword() != null && !appUser.getPassword().isEmpty()) { 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")) { if (!appUser.getPassword().startsWith("$2")) {
String hashedPassword = passwordEncoder.encode(appUser.getPassword()); String hashedPassword = passwordEncoder.encode(appUser.getPassword());
appUser.setPassword(hashedPassword); appUser.setPassword(hashedPassword);
} }
} }
appUser.setAktualisiertAm(java.time.LocalDateTime.now()); appUser.setAktualisiertAm(java.time.LocalDateTime.now());
appUser.setAktualisiertVon(securityService.getCurrentUserId()); appUser.setAktualisiertVon(securityService.getCurrentUserId());
return appUserRepository.save(appUser); return appUserRepository.save(appUser);
} }
/** /**
* Verify a plain text password against the stored hashed password * 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 * @return true if the password matches, false otherwise
*/ */
public boolean verifyPassword(String plainPassword, String hashedPassword) { public boolean verifyPassword(String plainPassword, String hashedPassword) {

View File

@@ -26,8 +26,6 @@ public class CustomerService {
return todoRepository.findAllBy(pageable).toList(); return todoRepository.findAllBy(pageable).toList();
} }
public List<Customer> findAll() { public List<Customer> findAll() {
return todoRepository.findAll(); return todoRepository.findAll();
} }

View File

@@ -4,7 +4,7 @@ import de.assecutor.votianlt.model.AppUser;
import de.assecutor.votianlt.model.User; import de.assecutor.votianlt.model.User;
import de.assecutor.votianlt.repository.AppUserRepository; import de.assecutor.votianlt.repository.AppUserRepository;
import de.assecutor.votianlt.repository.UserRepository; 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.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -17,33 +17,35 @@ import java.util.Optional;
@Service @Service
public class PasswordResetService { public class PasswordResetService {
public enum UserType { USERS, APP_USER } public enum UserType {
USERS, APP_USER
}
private final UserRepository userRepository; private final UserRepository userRepository;
private final AppUserRepository appUserRepository; private final AppUserRepository appUserRepository;
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
private final MailUtil mailUtil; private final EmailService emailService;
private static final Duration TOKEN_VALIDITY = Duration.ofMinutes(15); private static final Duration TOKEN_VALIDITY = Duration.ofMinutes(15);
public PasswordResetService(UserRepository userRepository, public PasswordResetService(UserRepository userRepository, AppUserRepository appUserRepository,
AppUserRepository appUserRepository, PasswordEncoder passwordEncoder, EmailService emailService) {
PasswordEncoder passwordEncoder,
MailUtil mailUtil) {
this.userRepository = userRepository; this.userRepository = userRepository;
this.appUserRepository = appUserRepository; this.appUserRepository = appUserRepository;
this.passwordEncoder = passwordEncoder; this.passwordEncoder = passwordEncoder;
this.mailUtil = mailUtil; this.emailService = emailService;
} }
/** /**
* Initiate reset without asking for user type. Looks up the email in both collections * Initiate reset without asking for user type. Looks up the email in both
* and only proceeds if it exists in exactly one of them. Otherwise, it silently returns * collections and only proceeds if it exists in exactly one of them. Otherwise,
* to avoid leaking account existence. * it silently returns to avoid leaking account existence.
*/ */
public void initiateResetAuto(String email, String baseUrl) { public void initiateResetAuto(String email, String baseUrl) {
if (email == null) return; if (email == null)
return;
String normalized = email.trim(); String normalized = email.trim();
if (normalized.isEmpty()) return; if (normalized.isEmpty())
return;
var userOpt = userRepository.findByEmail(normalized); var userOpt = userRepository.findByEmail(normalized);
var appUser = appUserRepository.findByEmail(normalized); var appUser = appUserRepository.findByEmail(normalized);
boolean inUsers = userOpt.isPresent(); boolean inUsers = userOpt.isPresent();
@@ -63,75 +65,80 @@ public class PasswordResetService {
String link = baseUrl + "/forget-password?token=" + token + "&type=" + typeParam; String link = baseUrl + "/forget-password?token=" + token + "&type=" + typeParam;
switch (userType) { switch (userType) {
case USERS -> { case USERS -> {
Optional<User> optional = userRepository.findByEmail(email); Optional<User> optional = userRepository.findByEmail(email);
if (optional.isEmpty()) { if (optional.isEmpty()) {
// Do not leak existence; simply return // Do not leak existence; simply return
return; return;
}
User user = optional.get();
user.setPasswordCode(token);
user.setPasswordTimestamp(now);
userRepository.save(user);
sendMail(email, link);
} }
case APP_USER -> { User user = optional.get();
AppUser appUser = appUserRepository.findByEmail(email); user.setPasswordCode(token);
if (appUser == null) { user.setPasswordTimestamp(now);
return; userRepository.save(user);
} sendMail(email, link);
appUser.setPasswordCode(token); }
appUser.setPasswordTimestamp(now); case APP_USER -> {
appUserRepository.save(appUser); AppUser appUser = appUserRepository.findByEmail(email);
sendMail(email, link); if (appUser == null) {
return;
} }
appUser.setPasswordCode(token);
appUser.setPasswordTimestamp(now);
appUserRepository.save(appUser);
sendMail(email, link);
}
} }
} }
private void sendMail(String to, String link) { private void sendMail(String to, String link) {
String subject = "Passwort zurücksetzen"; String subject = "Passwort zurücksetzen";
String body = "Hallo,\n\n" + String body = "Hallo,\n\n" + "Sie haben eine Anfrage zum Zurücksetzen Ihres Passworts gestellt. "
"Sie haben eine Anfrage zum Zurücksetzen Ihres Passworts gestellt. " + + "Dieser Link ist 15 Minuten gültig:\n" + link + "\n\n"
"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.";
"Wenn Sie diese Anfrage nicht gestellt haben, können Sie diese E-Mail ignorieren.";
try { try {
mailUtil.sendMail(to, subject, body); emailService.sendSimpleEmail(to, subject, body);
} catch (Exception e) { } 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) { public boolean isTokenValid(String token, UserType userType) {
LocalDateTime ts = switch (userType) { LocalDateTime ts = switch (userType) {
case USERS -> userRepository.findByPasswordCode(token).map(User::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); 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; return Duration.between(ts, LocalDateTime.now()).compareTo(TOKEN_VALIDITY) <= 0;
} }
public boolean resetPassword(String token, UserType userType, String newPassword) { public boolean resetPassword(String token, UserType userType, String newPassword) {
if (!isTokenValid(token, userType)) return false; if (!isTokenValid(token, userType))
return false;
switch (userType) { switch (userType) {
case USERS -> { case USERS -> {
Optional<User> optional = userRepository.findByPasswordCode(token); Optional<User> optional = userRepository.findByPasswordCode(token);
if (optional.isEmpty()) return false; if (optional.isEmpty())
User user = optional.get(); return false;
user.setPassword(passwordEncoder.encode(newPassword)); User user = optional.get();
user.setPasswordCode(null); user.setPassword(passwordEncoder.encode(newPassword));
user.setPasswordTimestamp(null); user.setPasswordCode(null);
userRepository.save(user); user.setPasswordTimestamp(null);
return true; userRepository.save(user);
} return true;
case APP_USER -> { }
AppUser appUser = appUserRepository.findByPasswordCode(token); case APP_USER -> {
if (appUser == null) return false; AppUser appUser = appUserRepository.findByPasswordCode(token);
appUser.setPassword(passwordEncoder.encode(newPassword)); if (appUser == null)
appUser.setPasswordCode(null); return false;
appUser.setPasswordTimestamp(null); appUser.setPassword(passwordEncoder.encode(newPassword));
appUserRepository.save(appUser); appUser.setPasswordCode(null);
return true; appUser.setPasswordTimestamp(null);
} appUserRepository.save(appUser);
return true;
}
} }
return false; return false;
} }

View File

@@ -18,4 +18,5 @@ public class RegisterService {
public void registerUser(User user) { public void registerUser(User user) {
registerRepository.save(user); registerRepository.save(user);
}} }
}

View File

@@ -19,37 +19,37 @@ import org.springframework.beans.factory.annotation.Autowired;
@PageTitle("Neues Endgerät anlegen") @PageTitle("Neues Endgerät anlegen")
@Route(value = "add-app-device", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) @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 { public class AddAppDeviceView extends VerticalLayout {
private final AppDeviceService appDeviceService; private final AppDeviceService appDeviceService;
private final Binder<AppDevice> binder; private final Binder<AppDevice> binder;
// Formularfelder // Formularfelder
private final TextField nameField; private final TextField nameField;
@Autowired @Autowired
public AddAppDeviceView(AppDeviceService appDeviceService) { public AddAppDeviceView(AppDeviceService appDeviceService) {
this.appDeviceService = appDeviceService; this.appDeviceService = appDeviceService;
// Binder initialisieren // Binder initialisieren
binder = new Binder<>(AppDevice.class); binder = new Binder<>(AppDevice.class);
// Formularfelder erstellen // Formularfelder erstellen
nameField = new TextField("Gerätename"); nameField = new TextField("Gerätename");
nameField.setRequired(true); nameField.setRequired(true);
nameField.setPlaceholder("z.B. iPhone 15, Samsung Galaxy S24"); nameField.setPlaceholder("z.B. iPhone 15, Samsung Galaxy S24");
nameField.setWidth("100%"); nameField.setWidth("100%");
// Layout konfigurieren // Layout konfigurieren
setSizeFull(); setSizeFull();
setPadding(true); setPadding(true);
setSpacing(true); setSpacing(true);
// Content zentrieren // Content zentrieren
setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
// Hauptcontainer erstellen // Hauptcontainer erstellen
VerticalLayout contentContainer = new VerticalLayout(); VerticalLayout contentContainer = new VerticalLayout();
contentContainer.setWidth("600px"); 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.getStyle().set("box-shadow", "0 2px 8px rgba(0,0,0,0.1)");
contentContainer.setPadding(true); contentContainer.setPadding(true);
contentContainer.setSpacing(true); contentContainer.setSpacing(true);
// Titel // Titel
H2 title = new H2("Neues Endgerät anlegen"); H2 title = new H2("Neues Endgerät anlegen");
title.getStyle().set("margin", "0"); title.getStyle().set("margin", "0");
title.getStyle().set("text-align", "center"); title.getStyle().set("text-align", "center");
contentContainer.add(title); contentContainer.add(title);
// Formular // Formular
FormLayout formLayout = new FormLayout(); FormLayout formLayout = new FormLayout();
formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1)); formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1));
formLayout.add(nameField); formLayout.add(nameField);
contentContainer.add(formLayout); contentContainer.add(formLayout);
// Buttons // Buttons
HorizontalLayout buttonLayout = new HorizontalLayout(); HorizontalLayout buttonLayout = new HorizontalLayout();
buttonLayout.setWidthFull(); buttonLayout.setWidthFull();
buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
buttonLayout.setSpacing(true); buttonLayout.setSpacing(true);
Button backButton = new Button("Zurück"); Button backButton = new Button("Zurück");
backButton.addClickListener(e -> navigateBack()); backButton.addClickListener(e -> navigateBack());
Button saveButton = new Button("Speichern"); Button saveButton = new Button("Speichern");
saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
saveButton.addClickListener(e -> createAppDevice()); saveButton.addClickListener(e -> createAppDevice());
buttonLayout.add(backButton, saveButton); buttonLayout.add(backButton, saveButton);
contentContainer.add(buttonLayout); contentContainer.add(buttonLayout);
add(contentContainer); add(contentContainer);
// Testdaten einfügen // Testdaten einfügen
populateTestData(); populateTestData();
// Binder konfigurieren // Binder konfigurieren
setupBinder(); setupBinder();
} }
private void setupBinder() { private void setupBinder() {
binder.forField(nameField) binder.forField(nameField).asRequired("Gerätename ist erforderlich").bind(AppDevice::getName,
.asRequired("Gerätename ist erforderlich") AppDevice::setName);
.bind(AppDevice::getName, AppDevice::setName);
} }
private void populateTestData() { private void populateTestData() {
nameField.setValue("iPhone 15 Pro"); nameField.setValue("iPhone 15 Pro");
} }
private void createAppDevice() { private void createAppDevice() {
if (binder.validate().isOk()) { if (binder.validate().isOk()) {
try { try {
AppDevice appDevice = new AppDevice(); AppDevice appDevice = new AppDevice();
binder.writeBean(appDevice); binder.writeBean(appDevice);
// Explizit als nicht zugeordnet speichern // Explizit als nicht zugeordnet speichern
appDevice.setAppUserId(null); appDevice.setAppUserId(null);
AppDevice savedDevice = appDeviceService.createAppDevice(appDevice); 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 // Zurück zur Übersicht
navigateBack(); navigateBack();
} catch (Exception e) { } 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 { } else {
Notification.show("Bitte füllen Sie alle erforderlichen Felder aus", 3000, Notification.Position.MIDDLE); Notification.show("Bitte füllen Sie alle erforderlichen Felder aus", 3000, Notification.Position.MIDDLE);
} }
} }
private void navigateBack() { private void navigateBack() {
getUI().ifPresent(ui -> ui.navigate("app-devices")); getUI().ifPresent(ui -> ui.navigate("app-devices"));
} }

View File

@@ -26,7 +26,7 @@ import org.springframework.beans.factory.annotation.Autowired;
@PageTitle("Neuen App-Nutzer anlegen") @PageTitle("Neuen App-Nutzer anlegen")
@Route(value = "add-app-user", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) @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 { public class AddAppUserView extends VerticalLayout {
private final AppUserService appUserService; private final AppUserService appUserService;
@@ -51,7 +51,7 @@ public class AddAppUserView extends VerticalLayout {
setSizeFull(); setSizeFull();
setPadding(true); setPadding(true);
setSpacing(true); setSpacing(true);
// Center content vertically // Center content vertically
setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
@@ -70,37 +70,35 @@ public class AddAppUserView extends VerticalLayout {
HorizontalLayout header = new HorizontalLayout(); HorizontalLayout header = new HorizontalLayout();
header.setAlignItems(FlexComponent.Alignment.CENTER); header.setAlignItems(FlexComponent.Alignment.CENTER);
header.setSpacing(true); header.setSpacing(true);
H2 title = new H2("Neuen App-Nutzer anlegen"); H2 title = new H2("Neuen App-Nutzer anlegen");
title.getStyle().set("margin", "0"); title.getStyle().set("margin", "0");
Button backButton = new Button("Zurück", new Icon(VaadinIcon.ARROW_LEFT)); Button backButton = new Button("Zurück", new Icon(VaadinIcon.ARROW_LEFT));
backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
backButton.addClickListener(e -> navigateBack()); backButton.addClickListener(e -> navigateBack());
header.add(title, backButton); header.add(title, backButton);
header.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN); header.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
contentContainer.add(header); contentContainer.add(header);
// Form layout // Form layout
FormLayout formLayout = new FormLayout(); FormLayout formLayout = new FormLayout();
formLayout.setResponsiveSteps( formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1));
new FormLayout.ResponsiveStep("0", 1)
);
// Configure fields // Configure fields
designationField.setPlaceholder("(HH H 000)"); designationField.setPlaceholder("(HH H 000)");
designationField.setWidthFull(); designationField.setWidthFull();
firstnameField.setWidthFull(); firstnameField.setWidthFull();
lastnameField.setWidthFull(); lastnameField.setWidthFull();
// Create horizontal layout for firstname and lastname // Create horizontal layout for firstname and lastname
HorizontalLayout nameLayout = new HorizontalLayout(); HorizontalLayout nameLayout = new HorizontalLayout();
nameLayout.setWidthFull(); nameLayout.setWidthFull();
nameLayout.setSpacing(true); nameLayout.setSpacing(true);
nameLayout.add(firstnameField, lastnameField); nameLayout.add(firstnameField, lastnameField);
phoneField.setWidthFull(); phoneField.setWidthFull();
appCodeField.setWidthFull(); appCodeField.setWidthFull();
emailField.setWidthFull(); emailField.setWidthFull();
@@ -108,7 +106,7 @@ public class AddAppUserView extends VerticalLayout {
passwordField.setRequired(true); passwordField.setRequired(true);
confirmPasswordField.setWidthFull(); confirmPasswordField.setWidthFull();
confirmPasswordField.setRequired(true); confirmPasswordField.setRequired(true);
// Configure device dropdown // Configure device dropdown
// Geräteauswahl vorbereiten // Geräteauswahl vorbereiten
deviceComboBox.setWidthFull(); deviceComboBox.setWidthFull();
@@ -140,7 +138,7 @@ public class AddAppUserView extends VerticalLayout {
// Setup binder // Setup binder
setupBinder(); setupBinder();
// Fill with test data // Fill with test data
populateTestData(); populateTestData();
} }
@@ -153,25 +151,22 @@ public class AddAppUserView extends VerticalLayout {
binder.forField(phoneField).bind(AppUser::getTelefon, AppUser::setTelefon); binder.forField(phoneField).bind(AppUser::getTelefon, AppUser::setTelefon);
binder.forField(appCodeField).bind(AppUser::getAppCode, AppUser::setAppCode); binder.forField(appCodeField).bind(AppUser::getAppCode, AppUser::setAppCode);
binder.forField(emailField).bind(AppUser::getEmail, AppUser::setEmail); binder.forField(emailField).bind(AppUser::getEmail, AppUser::setEmail);
binder.forField(passwordField) binder.forField(passwordField).asRequired("Passwort ist erforderlich").bind(AppUser::getPassword,
.asRequired("Passwort ist erforderlich") AppUser::setPassword);
.bind(AppUser::getPassword, AppUser::setPassword);
// Confirm password field validation // Confirm password field validation
binder.forField(confirmPasswordField) binder.forField(confirmPasswordField).asRequired("Passwort wiederholen ist erforderlich")
.asRequired("Passwort wiederholen ist erforderlich") .withValidator(confirmPassword -> confirmPassword.equals(passwordField.getValue()),
.withValidator(confirmPassword -> confirmPassword.equals(passwordField.getValue()), "Passwörter stimmen nicht überein")
"Passwörter stimmen nicht überein") .bind(appUser -> "", // Dummy getter - this field is not stored
.bind( (appUser, value) -> {
appUser -> "", // Dummy getter - this field is not stored } // Dummy setter - 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,
binder.forField(deviceComboBox) // wird beim
.asRequired("Bitte ein Gerät auswählen") // Erstellen
.bind( // gesetzt
appUser -> null, // Initialwert, wird beim Erstellen gesetzt (appUser, appDevice) -> appUser.setAppDeviceId(appDevice != null ? appDevice.getId() : null));
(appUser, appDevice) -> appUser.setAppDeviceId(appDevice != null ? appDevice.getId() : null)
);
} }
private void createAppUser() { private void createAppUser() {
@@ -191,49 +186,42 @@ public class AddAppUserView extends VerticalLayout {
} }
// Show success message // Show success message
Notification.show( Notification.show("App-Nutzer erfolgreich angelegt", 3000, Notification.Position.MIDDLE);
"App-Nutzer erfolgreich angelegt",
3000,
Notification.Position.MIDDLE
);
// Navigate back to app user list // Navigate back to app user list
navigateBack(); navigateBack();
} catch (ValidationException e) { } catch (ValidationException e) {
Notification.show( Notification.show("Bitte überprüfen Sie die Eingaben", 3000, Notification.Position.MIDDLE);
"Bitte überprüfen Sie die Eingaben",
3000,
Notification.Position.MIDDLE
);
} }
} }
private void populateTestData() { private void populateTestData() {
// Fill designation field // Fill designation field
designationField.setValue("HH H 001"); designationField.setValue("HH H 001");
// Fill name fields // Fill name fields
firstnameField.setValue("Max"); firstnameField.setValue("Max");
lastnameField.setValue("Mustermann"); lastnameField.setValue("Mustermann");
// Fill phone field // Fill phone field
phoneField.setValue("+49 123 456789"); phoneField.setValue("+49 123 456789");
// Fill app code field // Fill app code field
appCodeField.setValue("APP001"); appCodeField.setValue("APP001");
// Fill email field // Fill email field
emailField.setValue("max.mustermann@example.com"); emailField.setValue("max.mustermann@example.com");
// Fill password field // Fill password field
passwordField.setValue("testpassword123"); passwordField.setValue("testpassword123");
// Fill confirm password field // Fill confirm password field
confirmPasswordField.setValue("testpassword123"); confirmPasswordField.setValue("testpassword123");
// Set device to iPhone // Set device to iPhone
// deviceComboBox.setValue("iPhone"); // This line is removed as deviceComboBox is now a ComboBox<AppDevice> // deviceComboBox.setValue("iPhone"); // This line is removed as deviceComboBox
// is now a ComboBox<AppDevice>
} }
private void navigateBack() { private void navigateBack() {

View File

@@ -20,7 +20,8 @@ import java.time.Clock;
@Route(value = "add_company", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) @Route(value = "add_company", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@PageTitle("Neuen Firma anlegen") @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") @RolesAllowed("USER")
public class AddCompanyView extends Main { public class AddCompanyView extends Main {
private final AddCompanyService addCompanyService; private final AddCompanyService addCompanyService;
@@ -45,8 +46,7 @@ public class AddCompanyView extends Main {
companyName = new TextField("Firmenname"); companyName = new TextField("Firmenname");
companyName.setRequiredIndicatorVisible(true); companyName.setRequiredIndicatorVisible(true);
binder.forField(companyName) binder.forField(companyName).asRequired("Firmenname ist ein Pflichtfeld") // Pflichtfeldmeldung
.asRequired("Firmenname ist ein Pflichtfeld") // Pflichtfeldmeldung
.bind(Company::getName, Company::setName); .bind(Company::getName, Company::setName);
firstName = new TextField("Vorname"); firstName = new TextField("Vorname");
@@ -66,7 +66,10 @@ public class AddCompanyView extends Main {
// Erstelle ein Div als Container (oder direkt ein Layout) // Erstelle ein Div als Container (oder direkt ein Layout)
VerticalLayout formLayout = new VerticalLayout(); 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 // Zentriere die Inhalte vertikal und horizontal
formLayout.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); formLayout.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);

View File

@@ -22,7 +22,8 @@ import java.time.Clock;
@Route(value = "add-customer", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) @Route(value = "add-customer", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@PageTitle("Neuen Kunden anlegen") @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") @RolesAllowed("USER")
public class AddCustomerView extends Main { public class AddCustomerView extends Main {
private final AddCustomerService addCustomerService; private final AddCustomerService addCustomerService;
@@ -151,54 +152,38 @@ public class AddCustomerView extends Main {
} }
private void configureBinder() { private void configureBinder() {
binder.forField(companyName) binder.forField(companyName).asRequired("Firma ist ein Pflichtfeld").bind(Customer::getCompanyName,
.asRequired("Firma ist ein Pflichtfeld") Customer::setCompanyName);
.bind(Customer::getCompanyName, Customer::setCompanyName);
binder.forField(title) binder.forField(title).bind(Customer::getTitle, Customer::setTitle);
.bind(Customer::getTitle, Customer::setTitle);
binder.forField(firstName) binder.forField(firstName).asRequired("Vorname ist ein Pflichtfeld").bind(Customer::getFirstname,
.asRequired("Vorname ist ein Pflichtfeld") Customer::setFirstname);
.bind(Customer::getFirstname, Customer::setFirstname);
binder.forField(lastName) binder.forField(lastName).asRequired("Nachname ist ein Pflichtfeld").bind(Customer::getLastName,
.asRequired("Nachname ist ein Pflichtfeld") Customer::setLastName);
.bind(Customer::getLastName, Customer::setLastName);
binder.forField(telephone) binder.forField(telephone).asRequired("Telefonnummer ist ein Pflichtfeld").bind(Customer::getTelephone,
.asRequired("Telefonnummer ist ein Pflichtfeld") Customer::setTelephone);
.bind(Customer::getTelephone, Customer::setTelephone);
binder.forField(fax) binder.forField(fax).bind(Customer::getFax, Customer::setFax);
.bind(Customer::getFax, Customer::setFax);
binder.forField(mail) binder.forField(mail).asRequired("E-Mail-Adresse ist ein Pflichtfeld")
.asRequired("E-Mail-Adresse ist ein Pflichtfeld")
.withValidator(email -> email.contains("@"), "Bitte geben Sie eine gültige E-Mail-Adresse ein") .withValidator(email -> email.contains("@"), "Bitte geben Sie eine gültige E-Mail-Adresse ein")
.bind(Customer::getMail, Customer::setMail); .bind(Customer::getMail, Customer::setMail);
binder.forField(street) binder.forField(street).asRequired("Straße ist ein Pflichtfeld").bind(Customer::getStreet, Customer::setStreet);
.asRequired("Straße ist ein Pflichtfeld")
.bind(Customer::getStreet, Customer::setStreet);
binder.forField(houseNumber) binder.forField(houseNumber).asRequired("Hausnummer ist ein Pflichtfeld").bind(Customer::getHouseNumber,
.asRequired("Hausnummer ist ein Pflichtfeld") Customer::setHouseNumber);
.bind(Customer::getHouseNumber, Customer::setHouseNumber);
binder.forField(addressAddition) binder.forField(addressAddition).bind(Customer::getAddressAddition, Customer::setAddressAddition);
.bind(Customer::getAddressAddition, Customer::setAddressAddition);
binder.forField(zip) binder.forField(zip).asRequired("Postleitzahl ist ein Pflichtfeld").bind(Customer::getZip, Customer::setZip);
.asRequired("Postleitzahl ist ein Pflichtfeld")
.bind(Customer::getZip, Customer::setZip);
binder.forField(city) binder.forField(city).asRequired("Ort ist ein Pflichtfeld").bind(Customer::getCity, Customer::setCity);
.asRequired("Ort ist ein Pflichtfeld")
.bind(Customer::getCity, Customer::setCity);
} }
private void setTestData() { private void setTestData() {
companyName.setValue("Mustermann Transport GmbH"); companyName.setValue("Mustermann Transport GmbH");
title.setValue("Herr"); title.setValue("Herr");
@@ -213,6 +198,7 @@ public class AddCustomerView extends Main {
zip.setValue("20095"); zip.setValue("20095");
city.setValue("Hamburg"); city.setValue("Hamburg");
} }
private void submit() { private void submit() {
try { try {
Customer customer = new Customer(); Customer customer = new Customer();
@@ -221,20 +207,17 @@ public class AddCustomerView extends Main {
addCustomerService.addCustomer(customer); addCustomerService.addCustomer(customer);
// Erfolg anzeigen und zur Kundenliste navigieren // Erfolg anzeigen und zur Kundenliste navigieren
com.vaadin.flow.component.notification.Notification.show( com.vaadin.flow.component.notification.Notification.show("Kunde erfolgreich angelegt", 3000,
"Kunde erfolgreich angelegt", 3000, com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER);
com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER);
getUI().ifPresent(ui -> ui.navigate("customers")); getUI().ifPresent(ui -> ui.navigate("customers"));
} catch (ValidationException e) { } catch (ValidationException e) {
com.vaadin.flow.component.notification.Notification.show( com.vaadin.flow.component.notification.Notification.show("Bitte überprüfen Sie Ihre Eingaben", 3000,
"Bitte überprüfen Sie Ihre Eingaben", 3000, com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER);
com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER);
} catch (Exception e) { } catch (Exception e) {
com.vaadin.flow.component.notification.Notification.show( com.vaadin.flow.component.notification.Notification.show("Fehler beim Speichern: " + e.getMessage(), 5000,
"Fehler beim Speichern: " + e.getMessage(), 5000, com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER);
com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER);
} }
} }
} }

View File

@@ -20,7 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired;
@PageTitle("Endgeräte") @PageTitle("Endgeräte")
@Route(value = "app-devices", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) @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 { public class AppDevicesView extends VerticalLayout {
private final AppDeviceService appDeviceService; private final AppDeviceService appDeviceService;
@@ -32,7 +32,7 @@ public class AppDevicesView extends VerticalLayout {
public AppDevicesView(AppDeviceService appDeviceService, AppUserService appUserService) { public AppDevicesView(AppDeviceService appDeviceService, AppUserService appUserService) {
this.appDeviceService = appDeviceService; this.appDeviceService = appDeviceService;
this.appUserService = appUserService; this.appUserService = appUserService;
setSizeFull(); setSizeFull();
setPadding(true); setPadding(true);
setSpacing(true); setSpacing(true);
@@ -41,14 +41,14 @@ public class AppDevicesView extends VerticalLayout {
HorizontalLayout header = new HorizontalLayout(); HorizontalLayout header = new HorizontalLayout();
header.setWidthFull(); header.setWidthFull();
header.setAlignItems(FlexComponent.Alignment.CENTER); header.setAlignItems(FlexComponent.Alignment.CENTER);
H2 title = new H2("Endgeräte"); H2 title = new H2("Endgeräte");
title.getStyle().set("margin", "0"); title.getStyle().set("margin", "0");
Button addButton = new Button("Neues Endgerät anlegen", new Icon(VaadinIcon.PLUS)); Button addButton = new Button("Neues Endgerät anlegen", new Icon(VaadinIcon.PLUS));
addButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); addButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
addButton.addClickListener(e -> navigateToAddAppDevice()); addButton.addClickListener(e -> navigateToAddAppDevice());
header.add(title, addButton); header.add(title, addButton);
header.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN); header.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
add(header); add(header);
@@ -56,17 +56,16 @@ public class AppDevicesView extends VerticalLayout {
// Grid für Endgeräte // Grid für Endgeräte
appDeviceGrid = new Grid<>(AppDevice.class, false); appDeviceGrid = new Grid<>(AppDevice.class, false);
appDeviceGrid.setSizeFull(); appDeviceGrid.setSizeFull();
// Grid-Spalten konfigurieren // Grid-Spalten konfigurieren
appDeviceGrid.addColumn(AppDevice::getName).setHeader("Gerätename").setAutoWidth(true); appDeviceGrid.addColumn(AppDevice::getName).setHeader("Gerätename").setAutoWidth(true);
// App-Nutzer Spalte mit Name anzeigen // App-Nutzer Spalte mit Name anzeigen
appDeviceGrid.addColumn(appDevice -> { appDeviceGrid.addColumn(appDevice -> {
if (appDevice.getAppUserId() != null) { if (appDevice.getAppUserId() != null) {
try { try {
AppUser appUser = appUserService.findByCurrentUser().stream() AppUser appUser = appUserService.findByCurrentUser().stream()
.filter(user -> user.getId().equals(appDevice.getAppUserId())) .filter(user -> user.getId().equals(appDevice.getAppUserId())).findFirst().orElse(null);
.findFirst().orElse(null);
if (appUser != null) { if (appUser != null) {
return appUser.getVorname() + " " + appUser.getNachname(); return appUser.getVorname() + " " + appUser.getNachname();
} }
@@ -76,13 +75,13 @@ public class AppDevicesView extends VerticalLayout {
} }
return "Nicht zugeordnet"; return "Nicht zugeordnet";
}).setHeader("Zugeordneter App-Nutzer").setAutoWidth(true); }).setHeader("Zugeordneter App-Nutzer").setAutoWidth(true);
appDeviceGrid.addColumn(AppDevice::getErstelltAm).setHeader("Erstellt am").setAutoWidth(true); appDeviceGrid.addColumn(AppDevice::getErstelltAm).setHeader("Erstellt am").setAutoWidth(true);
// Make grid rows clickable // Make grid rows clickable
appDeviceGrid.setSelectionMode(Grid.SelectionMode.SINGLE); appDeviceGrid.setSelectionMode(Grid.SelectionMode.SINGLE);
appDeviceGrid.getStyle().set("cursor", "pointer"); appDeviceGrid.getStyle().set("cursor", "pointer");
// Add click listener to navigate to edit view // Add click listener to navigate to edit view
appDeviceGrid.addItemClickListener(event -> { appDeviceGrid.addItemClickListener(event -> {
AppDevice appDevice = event.getItem(); AppDevice appDevice = event.getItem();
@@ -90,7 +89,7 @@ public class AppDevicesView extends VerticalLayout {
getUI().ifPresent(ui -> ui.navigate("edit-app-device/" + appDevice.getId().toHexString())); getUI().ifPresent(ui -> ui.navigate("edit-app-device/" + appDevice.getId().toHexString()));
} }
}); });
add(appDeviceGrid); add(appDeviceGrid);
// Daten laden // Daten laden

View File

@@ -20,7 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired;
@PageTitle("App-Nutzer") @PageTitle("App-Nutzer")
@Route(value = "app-user", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) @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 { public class AppUserView extends VerticalLayout {
private final AppUserService appUserService; private final AppUserService appUserService;
@@ -38,14 +38,14 @@ public class AppUserView extends VerticalLayout {
HorizontalLayout header = new HorizontalLayout(); HorizontalLayout header = new HorizontalLayout();
header.setWidthFull(); header.setWidthFull();
header.setAlignItems(FlexComponent.Alignment.CENTER); header.setAlignItems(FlexComponent.Alignment.CENTER);
H2 title = new H2("App-Nutzer"); H2 title = new H2("App-Nutzer");
title.getStyle().set("margin", "0"); title.getStyle().set("margin", "0");
Button addButton = new Button("Neuen App-Nutzer anlegen", new Icon(VaadinIcon.PLUS)); Button addButton = new Button("Neuen App-Nutzer anlegen", new Icon(VaadinIcon.PLUS));
addButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); addButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
addButton.addClickListener(e -> navigateToAddAppUser()); addButton.addClickListener(e -> navigateToAddAppUser());
header.add(title, addButton); header.add(title, addButton);
header.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN); header.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
add(header); add(header);
@@ -53,7 +53,7 @@ public class AppUserView extends VerticalLayout {
// Grid für App-Nutzer // Grid für App-Nutzer
appUserGrid = new Grid<>(AppUser.class, false); appUserGrid = new Grid<>(AppUser.class, false);
appUserGrid.setSizeFull(); appUserGrid.setSizeFull();
// Grid-Spalten konfigurieren // Grid-Spalten konfigurieren
appUserGrid.addColumn(AppUser::getBezeichnung).setHeader("Bezeichnung").setAutoWidth(true); appUserGrid.addColumn(AppUser::getBezeichnung).setHeader("Bezeichnung").setAutoWidth(true);
appUserGrid.addColumn(AppUser::getVorname).setHeader("Vorname").setAutoWidth(true); appUserGrid.addColumn(AppUser::getVorname).setHeader("Vorname").setAutoWidth(true);
@@ -68,11 +68,11 @@ public class AppUserView extends VerticalLayout {
} }
return "Kein Gerät zugewiesen"; return "Kein Gerät zugewiesen";
}).setHeader("Endgerät").setAutoWidth(true); }).setHeader("Endgerät").setAutoWidth(true);
// Make grid rows clickable // Make grid rows clickable
appUserGrid.setSelectionMode(Grid.SelectionMode.SINGLE); appUserGrid.setSelectionMode(Grid.SelectionMode.SINGLE);
appUserGrid.getStyle().set("cursor", "pointer"); appUserGrid.getStyle().set("cursor", "pointer");
// Add click listener to navigate to edit view // Add click listener to navigate to edit view
appUserGrid.addItemClickListener(event -> { appUserGrid.addItemClickListener(event -> {
AppUser appUser = event.getItem(); AppUser appUser = event.getItem();
@@ -80,7 +80,7 @@ public class AppUserView extends VerticalLayout {
getUI().ifPresent(ui -> ui.navigate("edit-app-user/" + appUser.getId().toHexString())); getUI().ifPresent(ui -> ui.navigate("edit-app-user/" + appUser.getId().toHexString()));
} }
}); });
add(appUserGrid); add(appUserGrid);
// Daten laden // Daten laden

View File

@@ -46,7 +46,8 @@ public class AuthenticatedStartView extends VerticalLayout {
heroSection.setPadding(true); heroSection.setPadding(true);
heroSection.setSpacing(true); heroSection.setSpacing(true);
heroSection.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); 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.getStyle().set("min-height", "300px");
heroSection.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); heroSection.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
@@ -58,8 +59,7 @@ public class AuthenticatedStartView extends VerticalLayout {
welcomeTitle.getStyle().set("margin-bottom", "var(--lumo-space-l)"); welcomeTitle.getStyle().set("margin-bottom", "var(--lumo-space-l)");
Paragraph welcomeDescription = new Paragraph( 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("text-align", "center");
welcomeDescription.getStyle().set("max-width", "600px"); welcomeDescription.getStyle().set("max-width", "600px");
welcomeDescription.getStyle().set("font-size", "var(--lumo-font-size-l)"); 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"); systemTitle.getStyle().set("text-align", "center");
Paragraph systemIntro = new Paragraph( Paragraph systemIntro = new Paragraph(
"Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe ist von entscheidender Bedeutung, " + "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." + "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("text-align", "center");
systemIntro.getStyle().set("max-width", "800px"); systemIntro.getStyle().set("max-width", "800px");
systemIntro.getStyle().set("margin-bottom", "var(--lumo-space-xl)"); systemIntro.getStyle().set("margin-bottom", "var(--lumo-space-xl)");
@@ -96,14 +95,12 @@ public class AuthenticatedStartView extends VerticalLayout {
featuresGrid.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.START); featuresGrid.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.START);
// Feature Cards // Feature Cards
featuresGrid.add( featuresGrid.add(createFeatureCard(VaadinIcon.COG, "Einrichtungsassistent",
createFeatureCard(VaadinIcon.COG, "Einrichtungsassistent",
"Mithilfe des Einrichtungsassistenten haben Sie die Möglichkeit, Ihr Nutzerprofil zu vervollständigen."), "Mithilfe des Einrichtungsassistenten haben Sie die Möglichkeit, Ihr Nutzerprofil zu vervollständigen."),
createFeatureCard(VaadinIcon.USERS, "Kunden- und Auftragsverwaltung", createFeatureCard(VaadinIcon.USERS, "Kunden- und Auftragsverwaltung",
"Mit der Kunden- und Auftragsverwaltung haben Sie alle Kontaktdaten und Auftragsdetails stets im Blick."), "Mit der Kunden- und Auftragsverwaltung haben Sie alle Kontaktdaten und Auftragsdetails stets im Blick."),
createFeatureCard(VaadinIcon.CLIPBOARD_TEXT, "Auftragserstellung", 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.") "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); systemSection.add(systemTitle, systemIntro, featuresGrid);
return systemSection; return systemSection;
@@ -153,9 +150,8 @@ public class AuthenticatedStartView extends VerticalLayout {
appTitle.getStyle().set("text-align", "center"); appTitle.getStyle().set("text-align", "center");
Paragraph appDescription = new Paragraph( Paragraph appDescription = new Paragraph(
"Mit unserer mobilen App bleiben Sie auch unterwegs immer über Ihre Aufträge informiert " + "Mit unserer mobilen App bleiben Sie auch unterwegs immer über Ihre Aufträge informiert "
"und können wichtige Aufgaben direkt vom Smartphone aus erledigen." + "und können wichtige Aufgaben direkt vom Smartphone aus erledigen.");
);
appDescription.getStyle().set("text-align", "center"); appDescription.getStyle().set("text-align", "center");
appDescription.getStyle().set("max-width", "600px"); appDescription.getStyle().set("max-width", "600px");

View File

@@ -25,17 +25,17 @@ import org.springframework.beans.factory.annotation.Autowired;
@PageTitle("Endgerät bearbeiten") @PageTitle("Endgerät bearbeiten")
@Route(value = "edit-app-device", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) @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<String> { public class EditAppDeviceView extends VerticalLayout implements HasUrlParameter<String> {
private final AppDeviceService appDeviceService; private final AppDeviceService appDeviceService;
private final AppUserService appUserService; private final AppUserService appUserService;
private final Binder<AppDevice> binder; private final Binder<AppDevice> binder;
// Formularfelder // Formularfelder
private final TextField nameField; private final TextField nameField;
private final ComboBox<AppUser> appUserComboBox; private final ComboBox<AppUser> appUserComboBox;
// Aktuelles Endgerät // Aktuelles Endgerät
private AppDevice currentAppDevice; private AppDevice currentAppDevice;
@@ -43,39 +43,40 @@ public class EditAppDeviceView extends VerticalLayout implements HasUrlParameter
public EditAppDeviceView(AppDeviceService appDeviceService, AppUserService appUserService) { public EditAppDeviceView(AppDeviceService appDeviceService, AppUserService appUserService) {
this.appDeviceService = appDeviceService; this.appDeviceService = appDeviceService;
this.appUserService = appUserService; this.appUserService = appUserService;
// Binder initialisieren // Binder initialisieren
binder = new Binder<>(AppDevice.class); binder = new Binder<>(AppDevice.class);
// Formularfelder erstellen // Formularfelder erstellen
nameField = new TextField("Gerätename"); nameField = new TextField("Gerätename");
nameField.setRequired(true); nameField.setRequired(true);
nameField.setPlaceholder("z.B. iPhone 15, Samsung Galaxy S24"); nameField.setPlaceholder("z.B. iPhone 15, Samsung Galaxy S24");
nameField.setWidth("100%"); nameField.setWidth("100%");
// AppUser ComboBox // AppUser ComboBox
appUserComboBox = new ComboBox<>("App-Nutzer zuordnen"); appUserComboBox = new ComboBox<>("App-Nutzer zuordnen");
appUserComboBox.setPlaceholder("App-Nutzer auswählen (optional)"); appUserComboBox.setPlaceholder("App-Nutzer auswählen (optional)");
appUserComboBox.setWidth("100%"); appUserComboBox.setWidth("100%");
appUserComboBox.setItemLabelGenerator(appUser -> appUserComboBox.setItemLabelGenerator(
appUser.getVorname() + " " + appUser.getNachname() + " (" + appUser.getEmail() + ")"); appUser -> appUser.getVorname() + " " + appUser.getNachname() + " (" + appUser.getEmail() + ")");
// Lade verfügbare App-Nutzer // Lade verfügbare App-Nutzer
try { try {
appUserComboBox.setItems(appUserService.findByCurrentUser()); appUserComboBox.setItems(appUserService.findByCurrentUser());
} catch (Exception e) { } 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 // Layout konfigurieren
setSizeFull(); setSizeFull();
setPadding(true); setPadding(true);
setSpacing(true); setSpacing(true);
// Content zentrieren // Content zentrieren
setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
// Hauptcontainer erstellen // Hauptcontainer erstellen
VerticalLayout contentContainer = new VerticalLayout(); VerticalLayout contentContainer = new VerticalLayout();
contentContainer.setWidth("600px"); 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.getStyle().set("box-shadow", "0 2px 8px rgba(0,0,0,0.1)");
contentContainer.setPadding(true); contentContainer.setPadding(true);
contentContainer.setSpacing(true); contentContainer.setSpacing(true);
// Titel // Titel
H2 title = new H2("Endgerät bearbeiten"); H2 title = new H2("Endgerät bearbeiten");
title.getStyle().set("margin", "0"); title.getStyle().set("margin", "0");
title.getStyle().set("text-align", "center"); title.getStyle().set("text-align", "center");
contentContainer.add(title); contentContainer.add(title);
// Formular // Formular
FormLayout formLayout = new FormLayout(); FormLayout formLayout = new FormLayout();
formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1)); formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1));
formLayout.add(nameField, appUserComboBox); formLayout.add(nameField, appUserComboBox);
contentContainer.add(formLayout); contentContainer.add(formLayout);
// Buttons // Buttons
HorizontalLayout buttonLayout = new HorizontalLayout(); HorizontalLayout buttonLayout = new HorizontalLayout();
buttonLayout.setWidthFull(); buttonLayout.setWidthFull();
buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
buttonLayout.setSpacing(true); buttonLayout.setSpacing(true);
Button backButton = new Button("Zurück"); Button backButton = new Button("Zurück");
backButton.addClickListener(e -> navigateBack()); backButton.addClickListener(e -> navigateBack());
Button saveButton = new Button("Speichern"); Button saveButton = new Button("Speichern");
saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
saveButton.addClickListener(e -> saveAppDevice()); saveButton.addClickListener(e -> saveAppDevice());
Button deleteButton = new Button("Löschen"); Button deleteButton = new Button("Löschen");
deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR); deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR);
deleteButton.addClickListener(e -> deleteAppDevice()); deleteButton.addClickListener(e -> deleteAppDevice());
buttonLayout.add(backButton, saveButton, deleteButton); buttonLayout.add(backButton, saveButton, deleteButton);
contentContainer.add(buttonLayout); contentContainer.add(buttonLayout);
add(contentContainer); add(contentContainer);
// Binder konfigurieren // Binder konfigurieren
setupBinder(); setupBinder();
} }
@Override @Override
public void setParameter(com.vaadin.flow.router.BeforeEvent event, String parameter) { public void setParameter(com.vaadin.flow.router.BeforeEvent event, String parameter) {
try { try {
@@ -134,10 +135,10 @@ public class EditAppDeviceView extends VerticalLayout implements HasUrlParameter
navigateBack(); navigateBack();
} }
} }
private void loadAppDevice(ObjectId deviceId) { private void loadAppDevice(ObjectId deviceId) {
currentAppDevice = appDeviceService.findById(deviceId); currentAppDevice = appDeviceService.findById(deviceId);
if (currentAppDevice != null) { if (currentAppDevice != null) {
// Formular mit aktuellen Daten füllen // Formular mit aktuellen Daten füllen
binder.readBean(currentAppDevice); binder.readBean(currentAppDevice);
@@ -146,51 +147,50 @@ public class EditAppDeviceView extends VerticalLayout implements HasUrlParameter
navigateBack(); navigateBack();
} }
} }
private void setupBinder() { private void setupBinder() {
binder.forField(nameField) binder.forField(nameField).asRequired("Gerätename ist erforderlich").bind(AppDevice::getName,
.asRequired("Gerätename ist erforderlich") AppDevice::setName);
.bind(AppDevice::getName, AppDevice::setName);
binder.forField(appUserComboBox).bind(appDevice -> {
binder.forField(appUserComboBox) if (appDevice.getAppUserId() != null) {
.bind(appDevice -> { return appUserService.findByCurrentUser().stream()
if (appDevice.getAppUserId() != null) { .filter(user -> user.getId().equals(appDevice.getAppUserId())).findFirst().orElse(null);
return appUserService.findByCurrentUser().stream() }
.filter(user -> user.getId().equals(appDevice.getAppUserId())) return null;
.findFirst().orElse(null); }, (appDevice, appUser) -> {
} if (appUser != null) {
return null; appDevice.setAppUserId(appUser.getId());
}, (appDevice, appUser) -> { } else {
if (appUser != null) { appDevice.setAppUserId(null);
appDevice.setAppUserId(appUser.getId()); }
} else { });
appDevice.setAppUserId(null);
}
});
} }
private void saveAppDevice() { private void saveAppDevice() {
if (binder.validate().isOk()) { if (binder.validate().isOk()) {
try { try {
// Aktuelle Daten in das Modell schreiben // Aktuelle Daten in das Modell schreiben
binder.writeBean(currentAppDevice); binder.writeBean(currentAppDevice);
// Endgerät aktualisieren // Endgerät aktualisieren
AppDevice updatedDevice = appDeviceService.updateAppDevice(currentAppDevice); 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 // Zurück zur Übersicht
navigateBack(); navigateBack();
} catch (Exception e) { } 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 { } else {
Notification.show("Bitte füllen Sie alle erforderlichen Felder aus", 3000, Notification.Position.MIDDLE); Notification.show("Bitte füllen Sie alle erforderlichen Felder aus", 3000, Notification.Position.MIDDLE);
} }
} }
private void deleteAppDevice() { private void deleteAppDevice() {
if (currentAppDevice != null && currentAppDevice.getId() != null) { if (currentAppDevice != null && currentAppDevice.getId() != null) {
ConfirmDialog confirmDialog = new ConfirmDialog(); ConfirmDialog confirmDialog = new ConfirmDialog();
@@ -199,21 +199,22 @@ public class EditAppDeviceView extends VerticalLayout implements HasUrlParameter
confirmDialog.setCancelText("Abbrechen"); confirmDialog.setCancelText("Abbrechen");
confirmDialog.setConfirmText("Löschen"); confirmDialog.setConfirmText("Löschen");
confirmDialog.setConfirmButtonTheme("error primary"); confirmDialog.setConfirmButtonTheme("error primary");
confirmDialog.addConfirmListener(event -> { confirmDialog.addConfirmListener(event -> {
try { try {
appDeviceService.deleteById(currentAppDevice.getId()); appDeviceService.deleteById(currentAppDevice.getId());
Notification.show("Endgerät erfolgreich gelöscht", 3000, Notification.Position.MIDDLE); Notification.show("Endgerät erfolgreich gelöscht", 3000, Notification.Position.MIDDLE);
navigateBack(); navigateBack();
} catch (Exception e) { } 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(); confirmDialog.open();
} }
} }
private void navigateBack() { private void navigateBack() {
getUI().ifPresent(ui -> ui.navigate("app-devices")); getUI().ifPresent(ui -> ui.navigate("app-devices"));
} }

View File

@@ -32,7 +32,7 @@ import java.util.List;
@PageTitle("App-Nutzer bearbeiten") @PageTitle("App-Nutzer bearbeiten")
@Route(value = "edit-app-user", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) @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<String> { public class EditAppUserView extends VerticalLayout implements HasUrlParameter<String> {
private final AppUserService appUserService; private final AppUserService appUserService;
@@ -60,7 +60,7 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
setSizeFull(); setSizeFull();
setPadding(true); setPadding(true);
setSpacing(true); setSpacing(true);
// Center content vertically // Center content vertically
setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
@@ -79,47 +79,45 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
HorizontalLayout header = new HorizontalLayout(); HorizontalLayout header = new HorizontalLayout();
header.setAlignItems(FlexComponent.Alignment.CENTER); header.setAlignItems(FlexComponent.Alignment.CENTER);
header.setSpacing(true); header.setSpacing(true);
H2 title = new H2("App-Nutzer bearbeiten"); H2 title = new H2("App-Nutzer bearbeiten");
title.getStyle().set("margin", "0"); title.getStyle().set("margin", "0");
Button backButton = new Button("Zurück", new Icon(VaadinIcon.ARROW_LEFT)); Button backButton = new Button("Zurück", new Icon(VaadinIcon.ARROW_LEFT));
backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
backButton.addClickListener(e -> navigateBack()); backButton.addClickListener(e -> navigateBack());
header.add(title, backButton); header.add(title, backButton);
header.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN); header.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
contentContainer.add(header); contentContainer.add(header);
// Form layout // Form layout
FormLayout formLayout = new FormLayout(); FormLayout formLayout = new FormLayout();
formLayout.setResponsiveSteps( formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1));
new FormLayout.ResponsiveStep("0", 1)
);
// Configure fields // Configure fields
designationField.setPlaceholder("(HH H 000)"); designationField.setPlaceholder("(HH H 000)");
designationField.setWidthFull(); designationField.setWidthFull();
firstnameField.setWidthFull(); firstnameField.setWidthFull();
lastnameField.setWidthFull(); lastnameField.setWidthFull();
// Create horizontal layout for firstname and lastname // Create horizontal layout for firstname and lastname
HorizontalLayout nameLayout = new HorizontalLayout(); HorizontalLayout nameLayout = new HorizontalLayout();
nameLayout.setWidthFull(); nameLayout.setWidthFull();
nameLayout.setSpacing(true); nameLayout.setSpacing(true);
nameLayout.add(firstnameField, lastnameField); nameLayout.add(firstnameField, lastnameField);
phoneField.setWidthFull(); phoneField.setWidthFull();
appCodeField.setWidthFull(); appCodeField.setWidthFull();
emailField.setWidthFull(); emailField.setWidthFull();
// Configure password fields // Configure password fields
changePasswordField.setWidthFull(); changePasswordField.setWidthFull();
changePasswordField.setPlaceholder("Leer lassen, wenn nicht ändern"); changePasswordField.setPlaceholder("Leer lassen, wenn nicht ändern");
confirmChangePasswordField.setWidthFull(); confirmChangePasswordField.setWidthFull();
confirmChangePasswordField.setPlaceholder("Leer lassen, wenn nicht ändern"); confirmChangePasswordField.setPlaceholder("Leer lassen, wenn nicht ändern");
// Configure device dropdown // Configure device dropdown
deviceComboBox.setWidthFull(); deviceComboBox.setWidthFull();
deviceComboBox.setItemLabelGenerator(device -> device.getName()); deviceComboBox.setItemLabelGenerator(device -> device.getName());
@@ -141,10 +139,10 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
HorizontalLayout buttonLayout = new HorizontalLayout(); HorizontalLayout buttonLayout = new HorizontalLayout();
buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
buttonLayout.setWidthFull(); buttonLayout.setWidthFull();
Button saveButton = new Button("Speichern", e -> saveAppUser()); Button saveButton = new Button("Speichern", e -> saveAppUser());
saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
Button deleteButton = new Button("Löschen", e -> deleteAppUser()); Button deleteButton = new Button("Löschen", e -> deleteAppUser());
deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR); deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR);
@@ -167,11 +165,8 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
binder.forField(phoneField).bind(AppUser::getTelefon, AppUser::setTelefon); binder.forField(phoneField).bind(AppUser::getTelefon, AppUser::setTelefon);
binder.forField(appCodeField).bind(AppUser::getAppCode, AppUser::setAppCode); binder.forField(appCodeField).bind(AppUser::getAppCode, AppUser::setAppCode);
binder.forField(emailField).bind(AppUser::getEmail, AppUser::setEmail); binder.forField(emailField).bind(AppUser::getEmail, AppUser::setEmail);
binder.forField(deviceComboBox) binder.forField(deviceComboBox).bind(appUser -> getCurrentDevice(appUser), // Get current device
.bind( (appUser, appDevice) -> appUser.setAppDeviceId(appDevice != null ? appDevice.getId() : null));
appUser -> getCurrentDevice(appUser), // Get current device
(appUser, appDevice) -> appUser.setAppDeviceId(appDevice != null ? appDevice.getId() : null)
);
} }
@Override @Override
@@ -183,10 +178,10 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
// Setup device ComboBox with available devices // Setup device ComboBox with available devices
setupDeviceComboBox(); setupDeviceComboBox();
// Load app user data into form // Load app user data into form
binder.readBean(appUser); binder.readBean(appUser);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
Notification.show("Ungültige App-Nutzer-ID", 3000, Notification.Position.MIDDLE); Notification.show("Ungültige App-Nutzer-ID", 3000, Notification.Position.MIDDLE);
navigateBack(); navigateBack();
@@ -199,16 +194,16 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
if (!validatePasswordFields()) { if (!validatePasswordFields()) {
return; return;
} }
// Save current password to restore if not changing // Save current password to restore if not changing
String originalPassword = appUser.getPassword(); String originalPassword = appUser.getPassword();
binder.writeBean(appUser); binder.writeBean(appUser);
// Handle password change logic // Handle password change logic
String newPassword = changePasswordField.getValue(); String newPassword = changePasswordField.getValue();
String confirmPassword = confirmChangePasswordField.getValue(); String confirmPassword = confirmChangePasswordField.getValue();
if (newPassword != null && !newPassword.trim().isEmpty()) { if (newPassword != null && !newPassword.trim().isEmpty()) {
// User wants to change password // User wants to change password
if (confirmPassword != null && newPassword.equals(confirmPassword)) { if (confirmPassword != null && newPassword.equals(confirmPassword)) {
@@ -222,7 +217,7 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
// No password change requested, restore original password // No password change requested, restore original password
appUser.setPassword(originalPassword); appUser.setPassword(originalPassword);
} }
// Handle device assignment changes // Handle device assignment changes
AppDevice selectedDevice = deviceComboBox.getValue(); AppDevice selectedDevice = deviceComboBox.getValue();
@@ -239,7 +234,7 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
appDeviceService.updateAppDevice(newDevice); appDeviceService.updateAppDevice(newDevice);
} }
} }
appUserService.updateAppUser(appUser); appUserService.updateAppUser(appUser);
Notification.show("App-Nutzer erfolgreich gespeichert", 3000, Notification.Position.MIDDLE); Notification.show("App-Nutzer erfolgreich gespeichert", 3000, Notification.Position.MIDDLE);
navigateBack(); navigateBack();
@@ -247,56 +242,56 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
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 boolean validatePasswordFields() { private boolean validatePasswordFields() {
String newPassword = changePasswordField.getValue(); String newPassword = changePasswordField.getValue();
String confirmPassword = confirmChangePasswordField.getValue(); String confirmPassword = confirmChangePasswordField.getValue();
// If one field is filled, both must be filled // If one field is filled, both must be filled
boolean newPasswordFilled = newPassword != null && !newPassword.trim().isEmpty(); boolean newPasswordFilled = newPassword != null && !newPassword.trim().isEmpty();
boolean confirmPasswordFilled = confirmPassword != null && !confirmPassword.trim().isEmpty(); boolean confirmPasswordFilled = confirmPassword != null && !confirmPassword.trim().isEmpty();
if (newPasswordFilled && !confirmPasswordFilled) { if (newPasswordFilled && !confirmPasswordFilled) {
Notification.show("Bitte bestätigen Sie das neue Passwort", 3000, Notification.Position.MIDDLE); Notification.show("Bitte bestätigen Sie das neue Passwort", 3000, Notification.Position.MIDDLE);
return false; return false;
} }
if (!newPasswordFilled && confirmPasswordFilled) { if (!newPasswordFilled && confirmPasswordFilled) {
Notification.show("Bitte geben Sie das neue Passwort ein", 3000, Notification.Position.MIDDLE); Notification.show("Bitte geben Sie das neue Passwort ein", 3000, Notification.Position.MIDDLE);
return false; return false;
} }
// If both are filled, they must match // If both are filled, they must match
if (newPasswordFilled && confirmPasswordFilled && newPassword != null && !newPassword.equals(confirmPassword)) { if (newPasswordFilled && confirmPasswordFilled && newPassword != null && !newPassword.equals(confirmPassword)) {
Notification.show("Passwörter stimmen nicht überein", 3000, Notification.Position.MIDDLE); Notification.show("Passwörter stimmen nicht überein", 3000, Notification.Position.MIDDLE);
return false; return false;
} }
return true; return true;
} }
private AppDevice getCurrentDevice(AppUser appUser) { private AppDevice getCurrentDevice(AppUser appUser) {
if (appUser != null && appUser.getAppDeviceId() != null) { if (appUser != null && appUser.getAppDeviceId() != null) {
return appDeviceService.findById(appUser.getAppDeviceId()); return appDeviceService.findById(appUser.getAppDeviceId());
} }
return null; return null;
} }
private void setupDeviceComboBox() { private void setupDeviceComboBox() {
List<AppDevice> availableDevices = new ArrayList<>(); List<AppDevice> availableDevices = new ArrayList<>();
// First, add the currently assigned device if it exists // First, add the currently assigned device if it exists
AppDevice currentDevice = getCurrentDevice(appUser); AppDevice currentDevice = getCurrentDevice(appUser);
if (currentDevice != null) { if (currentDevice != null) {
availableDevices.add(currentDevice); availableDevices.add(currentDevice);
} }
// Then add all unassigned devices // Then add all unassigned devices
List<AppDevice> unassignedDevices = appDeviceService.findUnassignedDevices(); List<AppDevice> unassignedDevices = appDeviceService.findUnassignedDevices();
availableDevices.addAll(unassignedDevices); availableDevices.addAll(unassignedDevices);
deviceComboBox.setItems(availableDevices); deviceComboBox.setItems(availableDevices);
// Set the current device as selected // Set the current device as selected
if (currentDevice != null) { if (currentDevice != null) {
deviceComboBox.setValue(currentDevice); deviceComboBox.setValue(currentDevice);
@@ -307,7 +302,7 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
// Show confirmation dialog // Show confirmation dialog
com.vaadin.flow.component.dialog.Dialog confirmDialog = new com.vaadin.flow.component.dialog.Dialog(); com.vaadin.flow.component.dialog.Dialog confirmDialog = new com.vaadin.flow.component.dialog.Dialog();
confirmDialog.add("Möchten Sie diesen App-Nutzer wirklich löschen?"); confirmDialog.add("Möchten Sie diesen App-Nutzer wirklich löschen?");
HorizontalLayout buttonLayout = new HorizontalLayout(); HorizontalLayout buttonLayout = new HorizontalLayout();
Button confirmDeleteButton = new Button("Ja, löschen", e -> { Button confirmDeleteButton = new Button("Ja, löschen", e -> {
if (appUser != null && appUser.getId() != null) { if (appUser != null && appUser.getId() != null) {
@@ -318,13 +313,13 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
} }
}); });
confirmDeleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR); confirmDeleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR);
Button cancelDeleteButton = new Button("Abbrechen", e -> confirmDialog.close()); Button cancelDeleteButton = new Button("Abbrechen", e -> confirmDialog.close());
buttonLayout.add(confirmDeleteButton, cancelDeleteButton); buttonLayout.add(confirmDeleteButton, cancelDeleteButton);
buttonLayout.setSpacing(true); buttonLayout.setSpacing(true);
confirmDialog.add(buttonLayout); confirmDialog.add(buttonLayout);
confirmDialog.open(); confirmDialog.open();
} }

View File

@@ -25,7 +25,7 @@ import com.vaadin.flow.component.orderedlayout.FlexComponent;
@PageTitle("Kunde bearbeiten") @PageTitle("Kunde bearbeiten")
@Route(value = "edit-customer", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) @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<String> { public class EditCustomerView extends VerticalLayout implements HasUrlParameter<String> {
private final CustomerService customerService; private final CustomerService customerService;
@@ -52,7 +52,7 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
setSizeFull(); setSizeFull();
setPadding(true); setPadding(true);
setSpacing(true); setSpacing(true);
// Center content vertically // Center content vertically
setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
@@ -75,9 +75,7 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
// Form layout // Form layout
FormLayout formLayout = new FormLayout(); FormLayout formLayout = new FormLayout();
formLayout.setResponsiveSteps( formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1));
new FormLayout.ResponsiveStep("0", 1)
);
// Add fields to form - all fields in single column // Add fields to form - all fields in single column
formLayout.add(titleField); formLayout.add(titleField);
@@ -99,10 +97,10 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
HorizontalLayout buttonLayout = new HorizontalLayout(); HorizontalLayout buttonLayout = new HorizontalLayout();
buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
buttonLayout.setWidthFull(); buttonLayout.setWidthFull();
Button saveButton = new Button("Speichern", e -> saveCustomer()); Button saveButton = new Button("Speichern", e -> saveCustomer());
saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
Button cancelButton = new Button("Abbrechen", e -> navigateBack()); Button cancelButton = new Button("Abbrechen", e -> navigateBack());
Button deleteButton = new Button("Löschen", e -> deleteCustomer()); Button deleteButton = new Button("Löschen", e -> deleteCustomer());
deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR); deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR);
@@ -138,7 +136,7 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
try { try {
ObjectId customerId = new ObjectId(parameter); ObjectId customerId = new ObjectId(parameter);
customer = customerService.findById(customerId); customer = customerService.findById(customerId);
if (customer == null) { if (customer == null) {
Notification.show("Kunde nicht gefunden", 3000, Notification.Position.MIDDLE); Notification.show("Kunde nicht gefunden", 3000, Notification.Position.MIDDLE);
navigateBack(); navigateBack();
@@ -147,7 +145,7 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
// Load customer data into form // Load customer data into form
binder.readBean(customer); binder.readBean(customer);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
Notification.show("Ungültige Kunden-ID", 3000, Notification.Position.MIDDLE); Notification.show("Ungültige Kunden-ID", 3000, Notification.Position.MIDDLE);
navigateBack(); navigateBack();
@@ -169,7 +167,7 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
// Show confirmation dialog // Show confirmation dialog
Dialog confirmDialog = new Dialog(); Dialog confirmDialog = new Dialog();
confirmDialog.add("Möchten Sie diesen Kunden wirklich löschen?"); confirmDialog.add("Möchten Sie diesen Kunden wirklich löschen?");
HorizontalLayout buttonLayout = new HorizontalLayout(); HorizontalLayout buttonLayout = new HorizontalLayout();
Button confirmDeleteButton = new Button("Ja, löschen", e -> { Button confirmDeleteButton = new Button("Ja, löschen", e -> {
if (customer != null && customer.getId() != null) { if (customer != null && customer.getId() != null) {
@@ -179,13 +177,13 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
} }
}); });
confirmDeleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR); confirmDeleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR);
Button cancelDeleteButton = new Button("Abbrechen", e -> confirmDialog.close()); Button cancelDeleteButton = new Button("Abbrechen", e -> confirmDialog.close());
buttonLayout.add(confirmDeleteButton, cancelDeleteButton); buttonLayout.add(confirmDeleteButton, cancelDeleteButton);
buttonLayout.setSpacing(true); buttonLayout.setSpacing(true);
confirmDialog.add(buttonLayout); confirmDialog.add(buttonLayout);
confirmDialog.open(); confirmDialog.open();
} }

View File

@@ -32,7 +32,7 @@ import jakarta.annotation.security.RolesAllowed;
@PageTitle("Profil bearbeiten") @PageTitle("Profil bearbeiten")
@Route(value = "edit-profile", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) @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 { public class EditProfileView extends HorizontalLayout {
private final TextField prefixField; private final TextField prefixField;
private final TextField ustIdField; private final TextField ustIdField;
@@ -71,7 +71,6 @@ public class EditProfileView extends HorizontalLayout {
tabSheet.setSizeFull(); tabSheet.setSizeFull();
formColumn.setFlexGrow(1, tabSheet); formColumn.setFlexGrow(1, tabSheet);
FormLayout form = new FormLayout(); FormLayout form = new FormLayout();
form.setWidthFull(); form.setWidthFull();
form.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 2)); form.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 2));
@@ -163,35 +162,22 @@ public class EditProfileView extends HorizontalLayout {
zipField.setRequiredIndicatorVisible(true); zipField.setRequiredIndicatorVisible(true);
cityField.setRequiredIndicatorVisible(true); cityField.setRequiredIndicatorVisible(true);
binder.forField(companyField) binder.forField(companyField).asRequired("").bind(user -> null, (user, v) -> {
.asRequired("") });
.bind(user -> null, (user, v) -> {}); binder.forField(streetField).asRequired("").bind(user -> null, (user, v) -> {
binder.forField(streetField) });
.asRequired("") binder.forField(houseNumberField).asRequired("").bind(user -> null, (user, v) -> {
.bind(user -> null, (user, v) -> {}); });
binder.forField(houseNumberField) binder.forField(zipField).asRequired("").bind(user -> null, (user, v) -> {
.asRequired("") });
.bind(user -> null, (user, v) -> {}); binder.forField(cityField).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) binder.forField(firstnameField).asRequired("").bind(User::getFirstname, User::setFirstname);
.asRequired("") binder.forField(lastnameField).asRequired("").bind(User::getName, User::setName);
.bind(User::getFirstname, User::setFirstname); binder.forField(phoneField).asRequired("").bind(User::getPhone, User::setPhone);
binder.forField(lastnameField) binder.forField(emailField).asRequired("").withValidator(new EmailValidator("Ungültige E-Mail-Adresse"))
.asRequired("") .bind(User::getEmail, User::setEmail);
.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 // Optionale Felder
binder.forField(mobileField).bind(User::getPhone2, User::setPhone2); binder.forField(mobileField).bind(User::getPhone2, User::setPhone2);
binder.forField(faxField).bind(User::getFax, User::setFax); binder.forField(faxField).bind(User::getFax, User::setFax);
@@ -224,8 +210,8 @@ public class EditProfileView extends HorizontalLayout {
mapDiv.setWidth("100%"); mapDiv.setWidth("100%");
mapDiv.setHeight("400px"); mapDiv.setHeight("400px");
mapDiv.getElement().setProperty("innerHTML", mapDiv.getElement().setProperty("innerHTML",
"<iframe width='100%' height='100%' frameborder='0' style='border:0' " + "<iframe width='100%' height='100%' frameborder='0' style='border:0' "
"src='https://www.google.com/maps/embed/v1/place?key=AIzaSyDnbitL06iLp3elmj-WtPudCykX9xvXcVE&q=53.6070,10.1125' allowfullscreen></iframe>"); + "src='https://www.google.com/maps/embed/v1/place?key=AIzaSyDnbitL06iLp3elmj-WtPudCykX9xvXcVE&q=53.6070,10.1125' allowfullscreen></iframe>");
VerticalLayout mapTab = new VerticalLayout(); VerticalLayout mapTab = new VerticalLayout();
mapTab.setPadding(false); mapTab.setPadding(false);
mapTab.setSpacing(true); mapTab.setSpacing(true);
@@ -282,8 +268,8 @@ public class EditProfileView extends HorizontalLayout {
introTextArea.addValueChangeListener(e -> refreshPdf()); introTextArea.addValueChangeListener(e -> refreshPdf());
termsTextArea.addValueChangeListener(e -> refreshPdf()); termsTextArea.addValueChangeListener(e -> refreshPdf());
billingLeft.add(partsTitle, billingEnabled, prefixField, ustIdField, taxNumberField, bankNameField, billingLeft.add(partsTitle, billingEnabled, prefixField, ustIdField, taxNumberField, bankNameField, ibanField,
ibanField, taxRateField, introTextArea, termsTextArea); taxRateField, introTextArea, termsTextArea);
// Rechte Spalte: Vorschau // Rechte Spalte: Vorschau
VerticalLayout billingRight = new VerticalLayout(); VerticalLayout billingRight = new VerticalLayout();
@@ -299,10 +285,8 @@ public class EditProfileView extends HorizontalLayout {
Div previewWrapper = new Div(); Div previewWrapper = new Div();
previewWrapper.setWidth("100%"); previewWrapper.setWidth("100%");
previewWrapper.setHeight("650px"); previewWrapper.setHeight("650px");
previewWrapper.getStyle() previewWrapper.getStyle().set("overflow", "hidden").set("background", "var(--lumo-contrast-10pct)")
.set("overflow", "hidden") .set("padding", "0");
.set("background", "var(--lumo-contrast-10pct)")
.set("padding", "0");
// Initial noch keine PDF laden (erst bei aktiver Checkbox) // Initial noch keine PDF laden (erst bei aktiver Checkbox)
pdfFrame = new IFrame(); pdfFrame = new IFrame();
@@ -315,11 +299,9 @@ public class EditProfileView extends HorizontalLayout {
billingRight.add(previewTitle, previewWrapper); billingRight.add(previewTitle, previewWrapper);
billingTab.add(billingLeft, billingRight); billingTab.add(billingLeft, billingRight);
tabSheet.add("Rechnungsstellung", billingTab); tabSheet.add("Rechnungsstellung", billingTab);
// Zweiter Tab: Einstellungen (Beispiel mit Schaltern) // Zweiter Tab: Einstellungen (Beispiel mit Schaltern)
VerticalLayout switches = new VerticalLayout(); VerticalLayout switches = new VerticalLayout();
switches.setPadding(false); switches.setPadding(false);
@@ -392,14 +374,22 @@ public class EditProfileView extends HorizontalLayout {
// PDF neu rendern und iframe aktualisieren // PDF neu rendern und iframe aktualisieren
// Felder im Billing-Tab aktivieren/deaktivieren // Felder im Billing-Tab aktivieren/deaktivieren
private void setBillingFieldsEnabled(boolean enabled) { private void setBillingFieldsEnabled(boolean enabled) {
if (prefixField != null) prefixField.setEnabled(enabled); if (prefixField != null)
if (ustIdField != null) ustIdField.setEnabled(enabled); prefixField.setEnabled(enabled);
if (taxNumberField != null) taxNumberField.setEnabled(enabled); if (ustIdField != null)
if (bankNameField != null) bankNameField.setEnabled(enabled); ustIdField.setEnabled(enabled);
if (ibanField != null) ibanField.setEnabled(enabled); if (taxNumberField != null)
if (taxRateField != null) taxRateField.setEnabled(enabled); taxNumberField.setEnabled(enabled);
if (introTextArea != null) introTextArea.setEnabled(enabled); if (bankNameField != null)
if (termsTextArea != null) termsTextArea.setEnabled(enabled); 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 // Checkbox steuert Aktivierung und PDF
@@ -424,14 +414,14 @@ public class EditProfileView extends HorizontalLayout {
private void refreshPdf() { private void refreshPdf() {
byte[] bytes = generatePreviewPdf(); byte[] bytes = generatePreviewPdf();
String dataUrl = "data:application/pdf;base64," + Base64.getEncoder().encodeToString(bytes) 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) { if (pdfFrame != null) {
pdfFrame.setSrc(dataUrl); pdfFrame.setSrc(dataUrl);
} }
} }
// Einfache PDF-Vorschau generieren (kann später durch echte Logik ersetzt
// Einfache PDF-Vorschau generieren (kann später durch echte Logik ersetzt werden) // werden)
private byte[] generatePreviewPdf() { private byte[] generatePreviewPdf() {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
com.lowagie.text.Document document = new com.lowagie.text.Document(); 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 // Utility: safe getter für TextField/TextArea
private String safe(TextField f) { return f != null && f.getValue() != null ? f.getValue() : ""; } private String safe(TextField f) {
private String safe(TextArea f) { return f != null && f.getValue() != null ? f.getValue() : ""; } return f != null && f.getValue() != null ? f.getValue() : "";
}
private String safe(TextArea f) {
return f != null && f.getValue() != null ? f.getValue() : "";
}
} }

View File

@@ -61,11 +61,13 @@ public class ForgetPasswordView extends VerticalLayout implements BeforeEnterObs
String tokenParam = params.getOrDefault("token", java.util.List.of("")).getFirst(); String tokenParam = params.getOrDefault("token", java.util.List.of("")).getFirst();
String typeParam = params.getOrDefault("type", java.util.List.of("")).getFirst(); String typeParam = params.getOrDefault("type", java.util.List.of("")).getFirst();
this.token = tokenParam != null ? tokenParam.trim() : ""; 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)) { if (this.token.isEmpty() || !passwordResetService.isTokenValid(this.token, this.userType)) {
// Store a flash message in the VaadinSession so it persists through reroute // 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); event.rerouteTo(LoginView.class);
} }
} }

View File

@@ -49,7 +49,8 @@ public class ForgotPasswordRequestView extends VerticalLayout {
} }
String baseUrl = getBaseUrl(); String baseUrl = getBaseUrl();
passwordResetService.initiateResetAuto(email.trim(), baseUrl); 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")); getUI().ifPresent(ui -> ui.navigate("login"));
}); });
submit.addThemeVariants(ButtonVariant.LUMO_PRIMARY); submit.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
@@ -69,8 +70,8 @@ public class ForgotPasswordRequestView extends VerticalLayout {
String serverName = req.getServerName(); String serverName = req.getServerName();
int serverPort = req.getServerPort(); int serverPort = req.getServerPort();
String contextPath = req.getContextPath(); String contextPath = req.getContextPath();
String portPart = ("http".equals(scheme) && serverPort == 80) || ("https".equals(scheme) && serverPort == 443) String portPart = ("http".equals(scheme) && serverPort == 80)
? "" : ":" + serverPort; || ("https".equals(scheme) && serverPort == 443) ? "" : ":" + serverPort;
return scheme + "://" + serverName + portPart + contextPath; return scheme + "://" + serverName + portPart + contextPath;
} }
return ""; return "";

View File

@@ -22,7 +22,8 @@ public class ImprintView extends VerticalLayout {
Paragraph p1 = new Paragraph("Max Mustermann\nMusterstraße 1\n12345 Musterstadt\nDeutschland"); 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 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"); Paragraph p4 = new Paragraph("Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV: Max Mustermann");
add(p1, p2, p3, p4); add(p1, p2, p3, p4);

View File

@@ -21,7 +21,7 @@ import com.vaadin.flow.server.StreamRegistration;
@PageTitle("Rechnungen") @PageTitle("Rechnungen")
@Route(value = "invoices", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) @Route(value = "invoices", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({"USER","ADMIN"}) @RolesAllowed({ "USER", "ADMIN" })
public class InvoicesView extends VerticalLayout { public class InvoicesView extends VerticalLayout {
private final Grid<Invoice> invoiceGrid; private final Grid<Invoice> invoiceGrid;
@@ -47,10 +47,11 @@ public class InvoicesView extends VerticalLayout {
// Testdaten // Testdaten
List<Invoice> testInvoices = List.of( List<Invoice> testInvoices = List.of(
new Invoice("R-2024-001", "Max Mustermann", LocalDate.now().minusDays(2), 199.99, "Transport Hamburg-Berlin"), new Invoice("R-2024-001", "Max Mustermann", LocalDate.now().minusDays(2), 199.99,
new Invoice("R-2024-002", "Erika Musterfrau", LocalDate.now().minusDays(1), 299.49, "Express München-Köln"), "Transport Hamburg-Berlin"),
new Invoice("R-2024-003", "Hans Beispiel", LocalDate.now(), 149.00, "Standard Leipzig-Dresden") 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.setItems(testInvoices);
invoiceGrid.addItemClickListener(event -> { invoiceGrid.addItemClickListener(event -> {
@@ -67,17 +68,16 @@ public class InvoicesView extends VerticalLayout {
try { try {
// PDF generieren // PDF generieren
byte[] pdfBytes = generatePdf(invoice); byte[] pdfBytes = generatePdf(invoice);
StreamResource resource = new StreamResource( StreamResource resource = new StreamResource(invoice.getId() + ".pdf",
invoice.getId() + ".pdf", () -> new ByteArrayInputStream(pdfBytes));
() -> new ByteArrayInputStream(pdfBytes)
);
resource.setContentType("application/pdf"); resource.setContentType("application/pdf");
resource.setCacheTime(0); resource.setCacheTime(0);
// Direkter Download über UI // 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()); UI.getCurrent().getPage().open(registration.getResourceUri().toString());
} catch (Exception e) { } catch (Exception e) {
Notification.show("Fehler beim Erstellen der PDF: " + e.getMessage(), 5000, Notification.Position.MIDDLE); Notification.show("Fehler beim Erstellen der PDF: " + e.getMessage(), 5000, Notification.Position.MIDDLE);
} }
@@ -101,4 +101,3 @@ public class InvoicesView extends VerticalLayout {
} }
} }
} }

View File

@@ -45,7 +45,9 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
private final SignatureRepository signatureRepository; private final SignatureRepository signatureRepository;
private final VerticalLayout content; 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.jobRepository = jobRepository;
this.jobHistoryService = jobHistoryService; this.jobHistoryService = jobHistoryService;
this.photoRepository = photoRepository; this.photoRepository = photoRepository;
@@ -53,9 +55,8 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
this.signatureRepository = signatureRepository; this.signatureRepository = signatureRepository;
setSizeFull(); setSizeFull();
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
LumoUtility.FlexDirection.COLUMN, LumoUtility.Padding.MEDIUM, LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL);
LumoUtility.Gap.SMALL);
add(new ViewToolbar("Job Historie")); add(new ViewToolbar("Job Historie"));
@@ -96,7 +97,8 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
content.removeAll(); content.removeAll();
// Header mit Job-Informationen // 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); content.add(header);
// Job basic info for context // Job basic info for context
@@ -131,12 +133,9 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
private Div createJobInfoBox(Job job) { private Div createJobInfoBox(Job job) {
Div infoBox = new Div(); Div infoBox = new Div();
infoBox.getStyle() infoBox.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border", "1px solid var(--lumo-contrast-20pct)") .set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)")
.set("border-radius", "var(--lumo-border-radius-m)") .set("background-color", "var(--lumo-base-color)").set("margin-bottom", "var(--lumo-space-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(); VerticalLayout infoContent = new VerticalLayout();
infoContent.setPadding(false); infoContent.setPadding(false);
@@ -172,15 +171,11 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
private Div createHistoryEntryCard(JobHistory entry) { private Div createHistoryEntryCard(JobHistory entry) {
Div card = new Div(); Div card = new Div();
card.getStyle() card.getStyle().set("border", "1px solid var(--lumo-contrast-10pct)")
.set("border", "1px solid var(--lumo-contrast-10pct)") .set("border-left", "4px solid " + getTypeColor(entry.getChangeType()))
.set("border-left", "4px solid " + getTypeColor(entry.getChangeType())) .set("border-radius", "var(--lumo-border-radius-s)").set("padding", "var(--lumo-space-m)")
.set("border-radius", "var(--lumo-border-radius-s)") .set("margin-bottom", "var(--lumo-space-s)").set("background-color", "var(--lumo-base-color)")
.set("padding", "var(--lumo-space-m)") .set("width", "100%").set("box-sizing", "border-box");
.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 // Header row with icon, reason and timestamp
HorizontalLayout headerRow = new HorizontalLayout(); HorizontalLayout headerRow = new HorizontalLayout();
@@ -194,9 +189,8 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
reason.getStyle().set("font-weight", "500"); reason.getStyle().set("font-weight", "500");
Span timestamp = new Span(formatDateTime(entry.getTimestamp())); Span timestamp = new Span(formatDateTime(entry.getTimestamp()));
timestamp.getStyle() timestamp.getStyle().set("color", "var(--lumo-secondary-text-color)").set("font-size",
.set("color", "var(--lumo-secondary-text-color)") "var(--lumo-font-size-s)");
.set("font-size", "var(--lumo-font-size-s)");
HorizontalLayout leftSide = new HorizontalLayout(typeIcon, reason); HorizontalLayout leftSide = new HorizontalLayout(typeIcon, reason);
leftSide.setAlignItems(HorizontalLayout.Alignment.CENTER); leftSide.setAlignItems(HorizontalLayout.Alignment.CENTER);
@@ -213,17 +207,14 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
// Description // Description
if (entry.getDescription() != null && !entry.getDescription().isBlank()) { if (entry.getDescription() != null && !entry.getDescription().isBlank()) {
Span description = new Span(entry.getDescription()); Span description = new Span(entry.getDescription());
description.getStyle() description.getStyle().set("color", "var(--lumo-body-text-color)").set("margin-top", "var(--lumo-space-xs)")
.set("color", "var(--lumo-body-text-color)") .set("display", "block");
.set("margin-top", "var(--lumo-space-xs)")
.set("display", "block");
cardContent.add(description); cardContent.add(description);
} }
// Photo preview for photo tasks // Photo preview for photo tasks
if (entry.getChangeType() == JobHistoryType.TASK_COMPLETED && if (entry.getChangeType() == JobHistoryType.TASK_COMPLETED && entry.getDetails() != null
entry.getDetails() != null && && entry.getDetails().contains("Task-Typ: PHOTO")) {
entry.getDetails().contains("Task-Typ: PHOTO")) {
HorizontalLayout photoPreview = createPhotoPreview(entry); HorizontalLayout photoPreview = createPhotoPreview(entry);
if (photoPreview != null) { if (photoPreview != null) {
@@ -232,9 +223,8 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
} }
// Barcode preview for barcode tasks // Barcode preview for barcode tasks
if (entry.getChangeType() == JobHistoryType.TASK_COMPLETED && if (entry.getChangeType() == JobHistoryType.TASK_COMPLETED && entry.getDetails() != null
entry.getDetails() != null && && entry.getDetails().contains("Task-Typ: BARCODE")) {
entry.getDetails().contains("Task-Typ: BARCODE")) {
VerticalLayout barcodePreview = createBarcodePreview(entry); VerticalLayout barcodePreview = createBarcodePreview(entry);
if (barcodePreview != null) { if (barcodePreview != null) {
@@ -243,9 +233,8 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
} }
// Signature preview for signature tasks // Signature preview for signature tasks
if (entry.getChangeType() == JobHistoryType.TASK_COMPLETED && if (entry.getChangeType() == JobHistoryType.TASK_COMPLETED && entry.getDetails() != null
entry.getDetails() != null && && entry.getDetails().contains("Task-Typ: SIGNATURE")) {
entry.getDetails().contains("Task-Typ: SIGNATURE")) {
Div signaturePreview = createSignaturePreview(entry); Div signaturePreview = createSignaturePreview(entry);
if (signaturePreview != null) { if (signaturePreview != null) {
@@ -256,11 +245,9 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
// Changed by (if available) // Changed by (if available)
if (entry.getChangedBy() != null && !entry.getChangedBy().isBlank()) { if (entry.getChangedBy() != null && !entry.getChangedBy().isBlank()) {
Span changedBy = new Span("von: " + entry.getChangedBy()); Span changedBy = new Span("von: " + entry.getChangedBy());
changedBy.getStyle() changedBy.getStyle().set("color", "var(--lumo-secondary-text-color)")
.set("color", "var(--lumo-secondary-text-color)") .set("font-size", "var(--lumo-font-size-xs)").set("margin-top", "var(--lumo-space-xs)")
.set("font-size", "var(--lumo-font-size-xs)") .set("display", "block");
.set("margin-top", "var(--lumo-space-xs)")
.set("display", "block");
cardContent.add(changedBy); cardContent.add(changedBy);
} }
@@ -270,44 +257,47 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
} }
private Icon getTypeIcon(JobHistoryType type) { 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) { return switch (type) {
case CREATE -> new Icon(VaadinIcon.PLUS_CIRCLE); case CREATE -> new Icon(VaadinIcon.PLUS_CIRCLE);
case UPDATE -> new Icon(VaadinIcon.EDIT); case UPDATE -> new Icon(VaadinIcon.EDIT);
case STATUS_CHANGE -> new Icon(VaadinIcon.ARROW_RIGHT); case STATUS_CHANGE -> new Icon(VaadinIcon.ARROW_RIGHT);
case TASK_COMPLETED -> new Icon(VaadinIcon.CHECK); case TASK_COMPLETED -> new Icon(VaadinIcon.CHECK);
case ASSIGNMENT -> new Icon(VaadinIcon.USER); case ASSIGNMENT -> new Icon(VaadinIcon.USER);
case EXPORT -> new Icon(VaadinIcon.DOWNLOAD); case EXPORT -> new Icon(VaadinIcon.DOWNLOAD);
case DELETE -> new Icon(VaadinIcon.TRASH); case DELETE -> new Icon(VaadinIcon.TRASH);
case SYSTEM -> new Icon(VaadinIcon.COG); case SYSTEM -> new Icon(VaadinIcon.COG);
case COMMENT -> new Icon(VaadinIcon.COMMENT); case COMMENT -> new Icon(VaadinIcon.COMMENT);
default -> new Icon(VaadinIcon.INFO_CIRCLE); default -> new Icon(VaadinIcon.INFO_CIRCLE);
}; };
} }
private String getTypeColor(JobHistoryType type) { private String getTypeColor(JobHistoryType type) {
if (type == null) return "var(--lumo-contrast-60pct)"; if (type == null)
return "var(--lumo-contrast-60pct)";
return switch (type) { return switch (type) {
case CREATE -> "var(--lumo-success-color)"; case CREATE -> "var(--lumo-success-color)";
case UPDATE -> "var(--lumo-primary-color)"; case UPDATE -> "var(--lumo-primary-color)";
case STATUS_CHANGE -> "var(--lumo-contrast-color)"; case STATUS_CHANGE -> "var(--lumo-contrast-color)";
case TASK_COMPLETED -> "var(--lumo-success-color)"; case TASK_COMPLETED -> "var(--lumo-success-color)";
case ASSIGNMENT -> "var(--lumo-primary-color)"; case ASSIGNMENT -> "var(--lumo-primary-color)";
case EXPORT -> "var(--lumo-contrast-color)"; case EXPORT -> "var(--lumo-contrast-color)";
case DELETE -> "var(--lumo-error-color)"; case DELETE -> "var(--lumo-error-color)";
case SYSTEM -> "var(--lumo-contrast-60pct)"; case SYSTEM -> "var(--lumo-contrast-60pct)";
case COMMENT -> "var(--lumo-primary-color)"; case COMMENT -> "var(--lumo-primary-color)";
default -> "var(--lumo-contrast-60pct)"; default -> "var(--lumo-contrast-60pct)";
}; };
} }
private String formatDateTime(java.time.LocalDateTime dateTime) { private String formatDateTime(java.time.LocalDateTime dateTime) {
if (dateTime == null) return ""; if (dateTime == null)
return "";
try { try {
java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter
java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"); .ofPattern("dd.MM.yyyy HH:mm");
return dateTime.format(formatter); return dateTime.format(formatter);
} catch (Exception e) { } catch (Exception e) {
return dateTime.toString(); return dateTime.toString();
@@ -315,18 +305,19 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
} }
private String formatStatus(de.assecutor.votianlt.model.JobStatus status) { private String formatStatus(de.assecutor.votianlt.model.JobStatus status) {
if (status == null) return "Unbekannt"; if (status == null)
return "Unbekannt";
return switch (status) { return switch (status) {
case CREATED -> "Erstellt"; case CREATED -> "Erstellt";
case IN_PROGRESS -> "In Bearbeitung"; case IN_PROGRESS -> "In Bearbeitung";
case PICKUP_SCHEDULED -> "Abholung geplant"; case PICKUP_SCHEDULED -> "Abholung geplant";
case PICKED_UP -> "Abgeholt"; case PICKED_UP -> "Abgeholt";
case IN_TRANSIT -> "Unterwegs"; case IN_TRANSIT -> "Unterwegs";
case DELIVERED -> "Zugestellt"; case DELIVERED -> "Zugestellt";
case COMPLETED -> "Abgeschlossen"; case COMPLETED -> "Abgeschlossen";
case CANCELLED -> "Storniert"; case CANCELLED -> "Storniert";
default -> status.toString(); default -> status.toString();
}; };
} }
@@ -351,9 +342,7 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
HorizontalLayout photoLayout = new HorizontalLayout(); HorizontalLayout photoLayout = new HorizontalLayout();
photoLayout.setSpacing(true); photoLayout.setSpacing(true);
photoLayout.getStyle() photoLayout.getStyle().set("margin-top", "var(--lumo-space-s)").set("flex-wrap", "wrap");
.set("margin-top", "var(--lumo-space-s)")
.set("flex-wrap", "wrap");
for (Photo photo : photos) { for (Photo photo : photos) {
if (photo.getPhoto() != null && !photo.getPhoto().isBlank()) { if (photo.getPhoto() != null && !photo.getPhoto().isBlank()) {
@@ -395,18 +384,13 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
private com.vaadin.flow.component.html.Image createPhotoThumbnail(String base64Photo) { private com.vaadin.flow.component.html.Image createPhotoThumbnail(String base64Photo) {
try { try {
String imageData = base64Photo.startsWith("data:") String imageData = base64Photo.startsWith("data:") ? base64Photo : "data:image/jpeg;base64," + base64Photo;
? base64Photo
: "data:image/jpeg;base64," + base64Photo;
com.vaadin.flow.component.html.Image thumbnail = new com.vaadin.flow.component.html.Image(imageData, "Foto"); com.vaadin.flow.component.html.Image thumbnail = new com.vaadin.flow.component.html.Image(imageData,
thumbnail.getStyle() "Foto");
.set("width", "100px") thumbnail.getStyle().set("width", "100px").set("height", "100px").set("object-fit", "cover")
.set("height", "100px") .set("border-radius", "var(--lumo-border-radius-s)")
.set("object-fit", "cover") .set("border", "1px solid var(--lumo-contrast-20pct)").set("cursor", "pointer");
.set("border-radius", "var(--lumo-border-radius-s)")
.set("border", "1px solid var(--lumo-contrast-20pct)")
.set("cursor", "pointer");
return thumbnail; return thumbnail;
} catch (Exception e) { } catch (Exception e) {
@@ -424,15 +408,11 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
photoDialog.setCloseOnEsc(true); photoDialog.setCloseOnEsc(true);
try { try {
String imageData = base64Photo.startsWith("data:") String imageData = base64Photo.startsWith("data:") ? base64Photo : "data:image/jpeg;base64," + base64Photo;
? base64Photo
: "data:image/jpeg;base64," + base64Photo;
com.vaadin.flow.component.html.Image enlargedImage = new com.vaadin.flow.component.html.Image(imageData, "Vergrößertes Foto"); com.vaadin.flow.component.html.Image enlargedImage = new com.vaadin.flow.component.html.Image(imageData,
enlargedImage.getStyle() "Vergrößertes Foto");
.set("max-width", "100%") enlargedImage.getStyle().set("max-width", "100%").set("max-height", "100%").set("object-fit", "contain");
.set("max-height", "100%")
.set("object-fit", "contain");
VerticalLayout dialogContent = new VerticalLayout(enlargedImage); VerticalLayout dialogContent = new VerticalLayout(enlargedImage);
dialogContent.setAlignItems(VerticalLayout.Alignment.CENTER); dialogContent.setAlignItems(VerticalLayout.Alignment.CENTER);
@@ -470,8 +450,7 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
VerticalLayout barcodeLayout = new VerticalLayout(); VerticalLayout barcodeLayout = new VerticalLayout();
barcodeLayout.setPadding(false); barcodeLayout.setPadding(false);
barcodeLayout.setSpacing(true); barcodeLayout.setSpacing(true);
barcodeLayout.getStyle() barcodeLayout.getStyle().set("margin-top", "var(--lumo-space-s)");
.set("margin-top", "var(--lumo-space-s)");
for (Barcode barcode : barcodes) { for (Barcode barcode : barcodes) {
if (barcode.getBarcode() != null && !barcode.getBarcode().isBlank()) { if (barcode.getBarcode() != null && !barcode.getBarcode().isBlank()) {
@@ -490,15 +469,11 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
private Div createBarcodeBox(String barcodeValue) { private Div createBarcodeBox(String barcodeValue) {
Div barcodeBox = new Div(); Div barcodeBox = new Div();
barcodeBox.getStyle() barcodeBox.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border", "1px solid var(--lumo-contrast-20pct)") .set("border-radius", "var(--lumo-border-radius-s)").set("padding", "var(--lumo-space-xs)")
.set("border-radius", "var(--lumo-border-radius-s)") .set("background-color", "var(--lumo-contrast-5pct)").set("font-family", "monospace")
.set("padding", "var(--lumo-space-xs)") .set("font-size", "var(--lumo-font-size-s)").set("margin-bottom", "var(--lumo-space-xs)")
.set("background-color", "var(--lumo-contrast-5pct)") .set("word-break", "break-all");
.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)); barcodeBox.add(new Span(barcodeValue));
return barcodeBox; return barcodeBox;
@@ -531,22 +506,16 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
} }
Div previewContainer = new Div(); Div previewContainer = new Div();
previewContainer.getStyle() previewContainer.getStyle().set("margin-top", "var(--lumo-space-s)")
.set("margin-top", "var(--lumo-space-s)") .set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border", "1px solid var(--lumo-contrast-20pct)") .set("border-radius", "var(--lumo-border-radius-s)").set("padding", "var(--lumo-space-xs)")
.set("border-radius", "var(--lumo-border-radius-s)") .set("background-color", "var(--lumo-base-color)").set("cursor", "pointer").set("width", "200px")
.set("padding", "var(--lumo-space-xs)") .set("height", "100px").set("overflow", "hidden").set("display", "flex")
.set("background-color", "var(--lumo-base-color)") .set("align-items", "center").set("justify-content", "center");
.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 // 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); previewContainer.add(signatureSvg);
// Add click listener for enlarged view // Add click listener for enlarged view
@@ -560,27 +529,28 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
} }
} }
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 // Make SVG responsive by ensuring proper viewBox and dimensions
String responsiveSvg = svgContent; String responsiveSvg = svgContent;
if (!responsiveSvg.contains("viewBox")) { if (!responsiveSvg.contains("viewBox")) {
// Try to extract width and height from SVG and create viewBox // Try to extract width and height from SVG and create viewBox
responsiveSvg = responsiveSvg.replaceFirst("<svg", responsiveSvg = responsiveSvg.replaceFirst("<svg",
"<svg viewBox=\"0 0 300 150\" preserveAspectRatio=\"xMidYMid meet\""); "<svg viewBox=\"0 0 300 150\" preserveAspectRatio=\"xMidYMid meet\"");
} }
// Ensure the SVG has proper responsive attributes // Ensure the SVG has proper responsive attributes
if (!responsiveSvg.contains("preserveAspectRatio")) { if (!responsiveSvg.contains("preserveAspectRatio")) {
responsiveSvg = responsiveSvg.replaceFirst("<svg", responsiveSvg = responsiveSvg.replaceFirst("<svg", "<svg preserveAspectRatio=\"xMidYMid meet\"");
"<svg preserveAspectRatio=\"xMidYMid meet\"");
} }
// Set responsive dimensions // Set responsive dimensions
responsiveSvg = responsiveSvg.replaceFirst("width=\"[^\"]*\"", "width=\"" + width + "\""); responsiveSvg = responsiveSvg.replaceFirst("width=\"[^\"]*\"", "width=\"" + width + "\"");
responsiveSvg = responsiveSvg.replaceFirst("height=\"[^\"]*\"", "height=\"" + height + "\""); responsiveSvg = responsiveSvg.replaceFirst("height=\"[^\"]*\"", "height=\"" + height + "\"");
return new com.vaadin.flow.component.Html("<div style=\"width: " + width + "; height: " + height + ";\">" + responsiveSvg + "</div>"); return new com.vaadin.flow.component.Html(
"<div style=\"width: " + width + "; height: " + height + ";\">" + responsiveSvg + "</div>");
} }
private void showEnlargedSignature(String svgContent) { private void showEnlargedSignature(String svgContent) {

View File

@@ -61,13 +61,9 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
private final VerticalLayout content; private final VerticalLayout content;
private final List<Div> taskCards = new ArrayList<>(); private final List<Div> taskCards = new ArrayList<>();
public JobSummaryView(JobRepository jobRepository, public JobSummaryView(JobRepository jobRepository, CargoItemRepository cargoItemRepository,
CargoItemRepository cargoItemRepository, TaskRepository taskRepository, SignatureRepository signatureRepository, BarcodeRepository barcodeRepository,
TaskRepository taskRepository, PhotoRepository photoRepository, AppUserService appUserService) {
SignatureRepository signatureRepository,
BarcodeRepository barcodeRepository,
PhotoRepository photoRepository,
AppUserService appUserService) {
this.jobRepository = jobRepository; this.jobRepository = jobRepository;
this.cargoItemRepository = cargoItemRepository; this.cargoItemRepository = cargoItemRepository;
this.taskRepository = taskRepository; this.taskRepository = taskRepository;
@@ -77,9 +73,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
this.appUserService = appUserService; this.appUserService = appUserService;
setSizeFull(); setSizeFull();
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
LumoUtility.FlexDirection.COLUMN, LumoUtility.Padding.MEDIUM, LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL);
LumoUtility.Gap.SMALL);
content = new VerticalLayout(); content = new VerticalLayout();
content.setSpacing(true); content.setSpacing(true);
@@ -145,22 +140,19 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
VerticalLayout pickupBox = borderedBox(); VerticalLayout pickupBox = borderedBox();
pickupBox.add(new H3("Abholung " + (job.getPickupDate() != null ? formatLocalDate(job.getPickupDate()) : ""))); pickupBox.add(new H3("Abholung " + (job.getPickupDate() != null ? formatLocalDate(job.getPickupDate()) : "")));
pickupBox.add(new Span(valueOrEmpty(job.getPickupCompany()))); pickupBox.add(new Span(valueOrEmpty(job.getPickupCompany())));
pickupBox.add(new Span(valueOrEmpty(job.getPickupSalutation()) pickupBox.add(new Span(valueOrEmpty(job.getPickupSalutation()) + (job.getPickupSalutation() != null ? " " : "")
+ (job.getPickupSalutation() != null ? " " : "") + valueOrEmpty(job.getPickupFirstName()) + (job.getPickupFirstName() != null ? " " : "")
+ valueOrEmpty(job.getPickupFirstName())
+ (job.getPickupFirstName() != null ? " " : "")
+ valueOrEmpty(job.getPickupLastName()))); + valueOrEmpty(job.getPickupLastName())));
pickupBox.add(new Span(concatAddress(job.getPickupStreet(), job.getPickupHouseNumber()))); pickupBox.add(new Span(concatAddress(job.getPickupStreet(), job.getPickupHouseNumber())));
pickupBox.add(new Span(concatZipCity(job.getPickupZip(), job.getPickupCity()))); pickupBox.add(new Span(concatZipCity(job.getPickupZip(), job.getPickupCity())));
VerticalLayout deliveryBox = borderedBox(); 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.getDeliveryCompany())));
deliveryBox.add(new Span(valueOrEmpty(job.getDeliverySalutation()) deliveryBox.add(new Span(valueOrEmpty(job.getDeliverySalutation())
+ (job.getDeliverySalutation() != null ? " " : "") + (job.getDeliverySalutation() != null ? " " : "") + valueOrEmpty(job.getDeliveryFirstName())
+ valueOrEmpty(job.getDeliveryFirstName()) + (job.getDeliveryFirstName() != null ? " " : "") + valueOrEmpty(job.getDeliveryLastName())));
+ (job.getDeliveryFirstName() != null ? " " : "")
+ valueOrEmpty(job.getDeliveryLastName())));
deliveryBox.add(new Span(concatAddress(job.getDeliveryStreet(), job.getDeliveryHouseNumber()))); deliveryBox.add(new Span(concatAddress(job.getDeliveryStreet(), job.getDeliveryHouseNumber())));
deliveryBox.add(new Span(concatZipCity(job.getDeliveryZip(), job.getDeliveryCity()))); deliveryBox.add(new Span(concatZipCity(job.getDeliveryZip(), job.getDeliveryCity())));
@@ -208,13 +200,16 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
cargoBox.add(new Span("Keine Frachtangaben")); cargoBox.add(new Span("Keine Frachtangaben"));
} else { } else {
for (CargoItem ci : cargoItems) { for (CargoItem ci : cargoItems) {
if (ci == null) continue; if (ci == null)
continue;
String desc = ci.getDescription(); String desc = ci.getDescription();
Integer qty = ci.getQuantity(); Integer qty = ci.getQuantity();
String dims = dimString(ci); String dims = dimString(ci);
String weight = ci.getWeightKg() != null ? ci.getWeightKg() + " kg" : ""; String weight = ci.getWeightKg() != null ? ci.getWeightKg() + " kg" : "";
String line = (qty != null ? qty + " x " : "") + (desc != null ? desc : "") + (dims.isBlank() ? "" : " " + dims) + (weight.isBlank() ? "" : " " + weight); String line = (qty != null ? qty + " x " : "") + (desc != null ? desc : "")
if (!line.isBlank()) cargoBox.add(new Span(line)); + (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<String> {
private String formatLocalDate(java.time.LocalDate date) { private String formatLocalDate(java.time.LocalDate date) {
try { 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); 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) { private String concatAddress(String street, String house) {
String s = valueOrEmpty(street); String s = valueOrEmpty(street);
@@ -268,7 +268,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
private String concatZipCity(String zip, String city) { private String concatZipCity(String zip, String city) {
String z = valueOrEmpty(zip); String z = valueOrEmpty(zip);
String c = valueOrEmpty(city); String c = valueOrEmpty(city);
if (!z.isBlank() && !c.isBlank()) return z + " " + c; if (!z.isBlank() && !c.isBlank())
return z + " " + c;
return (z + " " + c).trim(); return (z + " " + c).trim();
} }
@@ -276,8 +277,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
String len = ci.getLengthMm() != null ? ci.getLengthMm().intValue() + " mm" : ""; String len = ci.getLengthMm() != null ? ci.getLengthMm().intValue() + " mm" : "";
String wid = ci.getWidthMm() != null ? ci.getWidthMm().intValue() + " mm" : ""; String wid = ci.getWidthMm() != null ? ci.getWidthMm().intValue() + " mm" : "";
String hei = ci.getHeightMm() != null ? ci.getHeightMm().intValue() + " mm" : ""; String hei = ci.getHeightMm() != null ? ci.getHeightMm().intValue() + " mm" : "";
String combined = String.join(" x ", java.util.stream.Stream.of(len, wid, hei) String combined = String.join(" x ",
.filter(s -> !s.isBlank()).toList()); java.util.stream.Stream.of(len, wid, hei).filter(s -> !s.isBlank()).toList());
return combined.isBlank() ? "" : combined; return combined.isBlank() ? "" : combined;
} }
@@ -293,19 +294,26 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
if (au != null) { if (au != null) {
String fn = au.getVorname(); String fn = au.getVorname();
String ln = au.getNachname(); String ln = au.getNachname();
String name = (fn != null ? fn : "").trim() + (fn != null && ln != null ? " " : "") + (ln != null ? ln : ""); String name = (fn != null ? fn : "").trim() + (fn != null && ln != null ? " " : "")
if (!name.isBlank()) return name; + (ln != null ? ln : "");
if (au.getBezeichnung() != null && !au.getBezeichnung().isBlank()) return au.getBezeichnung(); if (!name.isBlank())
if (au.getEmail() != null && !au.getEmail().isBlank()) return au.getEmail(); 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 return appUserIdString; // Fallback: show raw string if lookup fails
} }
private void addRouteMap(Job job) { private void addRouteMap(Job job) {
// Baue Adress-Strings // Baue Adress-Strings
String origin = (concatAddress(job.getPickupStreet(), job.getPickupHouseNumber()) + ", " + concatZipCity(job.getPickupZip(), job.getPickupCity())).trim(); String origin = (concatAddress(job.getPickupStreet(), job.getPickupHouseNumber()) + ", "
String destination = (concatAddress(job.getDeliveryStreet(), job.getDeliveryHouseNumber()) + ", " + concatZipCity(job.getDeliveryZip(), job.getDeliveryCity())).trim(); + concatZipCity(job.getPickupZip(), job.getPickupCity())).trim();
String destination = (concatAddress(job.getDeliveryStreet(), job.getDeliveryHouseNumber()) + ", "
+ concatZipCity(job.getDeliveryZip(), job.getDeliveryCity())).trim();
if (origin.isBlank() || destination.isBlank()) { if (origin.isBlank() || destination.isBlank()) {
// Wenn nicht genug Daten vorhanden sind, Karte nicht anzeigen // Wenn nicht genug Daten vorhanden sind, Karte nicht anzeigen
@@ -328,66 +336,48 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
content.add(map, routeInfo); content.add(map, routeInfo);
String js = ( String js = ("(function(){" + " var host = $0; var infoEl = $1;" + " function init(){"
"(function(){" + + " var map = new google.maps.Map(host, {center: {lat: 51.163, lng: 10.447}, zoom: 6, mapTypeControl: false});"
" var host = $0; var infoEl = $1;" + + " var trafficLayer = new google.maps.TrafficLayer(); trafficLayer.setMap(map);"
" function init(){" + + " var ds = new google.maps.DirectionsService();" + " ds.route({" + " origin: '"
" var map = new google.maps.Map(host, {center: {lat: 51.163, lng: 10.447}, zoom: 6, mapTypeControl: false});" + + escapeJs(origin) + "'," + " destination: '" + escapeJs(destination) + "',"
" var trafficLayer = new google.maps.TrafficLayer(); trafficLayer.setMap(map);" + + " travelMode: google.maps.TravelMode.DRIVING," + " provideRouteAlternatives: true,"
" var ds = new google.maps.DirectionsService();" + + " drivingOptions: { departureTime: new Date(), trafficModel: google.maps.TrafficModel.BEST_GUESS }"
" ds.route({" + + " }, function(res, status){ if(status==='OK'){ " + " infoEl.innerHTML='';"
" origin: '" + escapeJs(origin) + "'," + + " var bounds = new google.maps.LatLngBounds();"
" destination: '" + escapeJs(destination) + "'," + + " var renderers = []; var polylines = [];" + " res.routes.forEach(function(route, idx){"
" travelMode: google.maps.TravelMode.DRIVING," + + " var dr = new google.maps.DirectionsRenderer({map: map, preserveViewport: idx>0, suppressMarkers:false, suppressPolylines:true});"
" provideRouteAlternatives: true," + + " dr.setRouteIndex(idx); dr.setDirections(res);" + " renderers.push(dr);"
" drivingOptions: { departureTime: new Date(), trafficModel: google.maps.TrafficModel.BEST_GUESS }" + + " var path = route.overview_path || [];"
" }, function(res, status){ if(status==='OK'){ " + + " var poly = new google.maps.Polyline({path: path, strokeColor: idx===0?'#1976d2':'#90caf9', strokeOpacity: 0.95, strokeWeight: idx===0?6:4});"
" infoEl.innerHTML='';" + + " poly.setMap(map); polylines.push(poly);"
" var bounds = new google.maps.LatLngBounds();" + + " var leg = route.legs && route.legs[0];" + " if (leg) {"
" var renderers = []; var polylines = [];" + + " var dur = leg.duration ? leg.duration.text : '';"
" res.routes.forEach(function(route, idx){" + + " var durT = leg.duration_in_traffic ? leg.duration_in_traffic.text : '';"
" var dr = new google.maps.DirectionsRenderer({map: map, preserveViewport: idx>0, suppressMarkers:false, suppressPolylines:true});" + + " var dist = leg.distance ? leg.distance.text : '';"
" dr.setRouteIndex(idx); dr.setDirections(res);" + + " var alt = (idx===0?'Schnellste Route':'Alternative '+idx);"
" renderers.push(dr);" + + " var row = document.createElement('div'); row.style.margin='4px 0'; row.style.cursor='pointer';"
" var path = route.overview_path || [];" + + " row.textContent = alt + ': ' + dist + ' • Dauer: ' + dur + (durT?(' (mit Verkehr: '+durT+')'):'');"
" var poly = new google.maps.Polyline({path: path, strokeColor: idx===0?'#1976d2':'#90caf9', strokeOpacity: 0.95, strokeWeight: idx===0?6:4});" + + " row.onmouseenter = function(){"
" poly.setMap(map); polylines.push(poly);" + + " polylines.forEach(function(p,i){ p.setOptions({strokeColor: i===0?'#90caf9':'#e3f2fd', strokeOpacity: 0.6, strokeWeight: 3}); });"
" var leg = route.legs && route.legs[0];" + + " poly.setOptions({strokeColor:'#0d47a1', strokeOpacity:1, strokeWeight:7});"
" if (leg) {" + + " };" + " row.onmouseleave = function(){"
" var dur = leg.duration ? leg.duration.text : '';" + + " polylines.forEach(function(p,i){ p.setOptions({strokeColor: i===0?'#1976d2':'#90caf9', strokeOpacity:0.95, strokeWeight: i===0?6:4}); });"
" var durT = leg.duration_in_traffic ? leg.duration_in_traffic.text : '';" + + " };" + " infoEl.appendChild(row);"
" var dist = leg.distance ? leg.distance.text : '';" + + " if (path && path.length){ path.forEach(function(pt){ bounds.extend(pt); }); }"
" var alt = (idx===0?'Schnellste Route':'Alternative '+idx);" + + " }" + " });" + " if (!bounds.isEmpty()) { map.fitBounds(bounds); }"
" var row = document.createElement('div'); row.style.margin='4px 0'; row.style.cursor='pointer';" + + " }});" + " }" + " if (!(window.google && window.google.maps)) {"
" row.textContent = alt + ': ' + dist + ' • Dauer: ' + dur + (durT?(' (mit Verkehr: '+durT+')'):'');" + + " var s=document.createElement('script');"
" row.onmouseenter = function(){" + + " s.src='https://maps.googleapis.com/maps/api/js?key=AIzaSyDnbitL06iLp3elmj-WtPudCykX9xvXcVE&libraries=places';"
" polylines.forEach(function(p,i){ p.setOptions({strokeColor: i===0?'#90caf9':'#e3f2fd', strokeOpacity: 0.6, strokeWeight: 3}); });" + + " s.onload=init; document.head.appendChild(s);" + " } else { init(); }" + "})();");
" 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()); map.getElement().executeJs(js, map.getElement(), routeInfo.getElement());
} }
// Hilfsfunktion zum einfachen Escapen von JS-Zeichen in Strings // Hilfsfunktion zum einfachen Escapen von JS-Zeichen in Strings
private String escapeJs(String s) { private String escapeJs(String s) {
if (s == null) return ""; if (s == null)
return "";
return s.replace("\\", "\\\\").replace("'", "\\'").replace("\n", " ").replace("\r", " "); return s.replace("\\", "\\\\").replace("'", "\\'").replace("\n", " ").replace("\r", " ");
} }
@@ -426,7 +416,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
// Completion details if completed // Completion details if completed
if (task.isCompleted()) { if (task.isCompleted()) {
dialogContent.add(new Span("")); // Spacer dialogContent.add(new Span("")); // Spacer
if (task.getCompletedAt() != null) { if (task.getCompletedAt() != null) {
dialogContent.add(new Span("Abgeschlossen am: " + formatDateTime(task.getCompletedAt()))); dialogContent.add(new Span("Abgeschlossen am: " + formatDateTime(task.getCompletedAt())));
} }
@@ -468,7 +458,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
if (photoTask.getMinPhotoCount() != null || photoTask.getMaxPhotoCount() != null) { if (photoTask.getMinPhotoCount() != null || photoTask.getMaxPhotoCount() != null) {
String photoInfo = "Fotos: "; String photoInfo = "Fotos: ";
if (photoTask.getMinPhotoCount() != null && photoTask.getMaxPhotoCount() != null) { 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) { } else if (photoTask.getMinPhotoCount() != null) {
photoInfo += "Mindestens " + photoTask.getMinPhotoCount() + " Fotos erforderlich"; photoInfo += "Mindestens " + photoTask.getMinPhotoCount() + " Fotos erforderlich";
} else if (photoTask.getMaxPhotoCount() != null) { } else if (photoTask.getMaxPhotoCount() != null) {
@@ -484,7 +475,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
List<Photo> photos = photoRepository.findByTaskId(taskId); List<Photo> photos = photoRepository.findByTaskId(taskId);
if (!photos.isEmpty()) { if (!photos.isEmpty()) {
content.add(new Span("")); // Spacer content.add(new Span("")); // Spacer
// Collect all photos from all Photo entries // Collect all photos from all Photo entries
List<String> allPhotos = new ArrayList<>(); List<String> allPhotos = new ArrayList<>();
@@ -520,7 +511,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
List<Signature> signatures = signatureRepository.findByTaskId(taskId); List<Signature> signatures = signatureRepository.findByTaskId(taskId);
if (!signatures.isEmpty()) { if (!signatures.isEmpty()) {
content.add(new Span("")); // Spacer content.add(new Span("")); // Spacer
content.add(new Span("Gespeicherte Unterschrift:")); content.add(new Span("Gespeicherte Unterschrift:"));
// Display the latest signature (assuming one signature per task) // Display the latest signature (assuming one signature per task)
@@ -530,17 +521,12 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
if (svgContent != null && !svgContent.isBlank()) { if (svgContent != null && !svgContent.isBlank()) {
// Create a div to hold the SVG // Create a div to hold the SVG
Div svgContainer = new Div(); Div svgContainer = new Div();
svgContainer.getStyle() svgContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border", "1px solid var(--lumo-contrast-20pct)") .set("border-radius", "var(--lumo-border-radius-m)")
.set("border-radius", "var(--lumo-border-radius-m)") .set("padding", "var(--lumo-space-s)").set("background-color", "white")
.set("padding", "var(--lumo-space-s)") .set("width", "100%").set("max-width", "450px").set("overflow", "hidden")
.set("background-color", "white") .set("display", "flex").set("align-items", "center")
.set("width", "100%") .set("justify-content", "center");
.set("max-width", "450px")
.set("overflow", "hidden")
.set("display", "flex")
.set("align-items", "center")
.set("justify-content", "center");
// Process SVG to make it responsive // Process SVG to make it responsive
String responsiveSvg = makeResponsiveSvg(svgContent); String responsiveSvg = makeResponsiveSvg(svgContent);
@@ -562,7 +548,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
List<Barcode> barcodes = barcodeRepository.findByTaskId(taskId); List<Barcode> barcodes = barcodeRepository.findByTaskId(taskId);
if (!barcodes.isEmpty()) { if (!barcodes.isEmpty()) {
content.add(new Span("")); // Spacer content.add(new Span("")); // Spacer
content.add(new Span("Gescannte Barcodes (" + barcodes.size() + "):")); content.add(new Span("Gescannte Barcodes (" + barcodes.size() + "):"));
// Display all scanned barcodes // Display all scanned barcodes
@@ -573,15 +559,12 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
if (barcodeValue != null && !barcodeValue.isBlank()) { if (barcodeValue != null && !barcodeValue.isBlank()) {
// Create a styled container for each barcode // Create a styled container for each barcode
Div barcodeContainer = new Div(); Div barcodeContainer = new Div();
barcodeContainer.getStyle() barcodeContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border", "1px solid var(--lumo-contrast-20pct)") .set("border-radius", "var(--lumo-border-radius-s)")
.set("border-radius", "var(--lumo-border-radius-s)") .set("padding", "var(--lumo-space-s)").set("margin", "var(--lumo-space-xs) 0")
.set("padding", "var(--lumo-space-s)") .set("background-color", "var(--lumo-contrast-5pct)")
.set("margin", "var(--lumo-space-xs) 0") .set("font-family", "monospace").set("font-size", "var(--lumo-font-size-s)")
.set("background-color", "var(--lumo-contrast-5pct)") .set("word-break", "break-all");
.set("font-family", "monospace")
.set("font-size", "var(--lumo-font-size-s)")
.set("word-break", "break-all");
Span barcodeSpan = new Span((i + 1) + ". " + barcodeValue); Span barcodeSpan = new Span((i + 1) + ". " + barcodeValue);
barcodeContainer.add(barcodeSpan); barcodeContainer.add(barcodeSpan);
@@ -598,7 +581,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
private String formatDateTime(java.time.LocalDateTime dateTime) { private String formatDateTime(java.time.LocalDateTime dateTime) {
try { 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); return dateTime.format(fmt);
} catch (Exception e) { } catch (Exception e) {
return dateTime.toString(); return dateTime.toString();
@@ -609,39 +593,28 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
Div taskCard = new Div(); Div taskCard = new Div();
// Card styling with fixed width // Card styling with fixed width
taskCard.getStyle() taskCard.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border", "1px solid var(--lumo-contrast-20pct)") .set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)")
.set("border-radius", "var(--lumo-border-radius-m)") .set("margin", "var(--lumo-space-xs) 0").set("background-color", "var(--lumo-base-color)")
.set("padding", "var(--lumo-space-m)") .set("cursor", "pointer").set("transition", "all 0.2s ease")
.set("margin", "var(--lumo-space-xs) 0") .set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)").set("display", "flex").set("align-items", "center")
.set("background-color", "var(--lumo-base-color)") .set("gap", "var(--lumo-space-m)").set("width", "100%").set("box-sizing", "border-box");
.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 // Hover effects
taskCard.getElement().addEventListener("mouseenter", e -> { taskCard.getElement().addEventListener("mouseenter", e -> {
taskCard.getStyle() taskCard.getStyle().set("transform", "translateY(-2px)").set("box-shadow", "0 4px 12px rgba(0, 0, 0, 0.15)")
.set("transform", "translateY(-2px)") .set("border-color", "var(--lumo-primary-color-50pct)");
.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.getElement().addEventListener("mouseleave", e -> {
taskCard.getStyle() taskCard.getStyle().set("transform", "translateY(0)").set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)")
.set("transform", "translateY(0)") .set("border-color", "var(--lumo-contrast-20pct)");
.set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)")
.set("border-color", "var(--lumo-contrast-20pct)");
}); });
// Task icon based on type // Task icon based on type
Icon taskIcon = getTaskIcon(task); 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 // Task content
VerticalLayout taskContent = new VerticalLayout(); VerticalLayout taskContent = new VerticalLayout();
@@ -652,27 +625,20 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
// Task name with order number (display as 1-based instead of 0-based) // Task name with order number (display as 1-based instead of 0-based)
String taskNameWithOrder = (task.getTaskOrder() != null ? (task.getTaskOrder() + 1) + ". " : "") + displayName; String taskNameWithOrder = (task.getTaskOrder() != null ? (task.getTaskOrder() + 1) + ". " : "") + displayName;
Span taskName = new Span(taskNameWithOrder); Span taskName = new Span(taskNameWithOrder);
taskName.getStyle() taskName.getStyle().set("font-weight", "500").set("font-size", "var(--lumo-font-size-m)").set("color",
.set("font-weight", "500") task.isCompleted() ? "var(--lumo-success-text-color)" : "var(--lumo-body-text-color)");
.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 // Task status/description
Span taskDescription = new Span(getTaskDescription(task)); Span taskDescription = new Span(getTaskDescription(task));
taskDescription.getStyle() taskDescription.getStyle().set("font-size", "var(--lumo-font-size-s)")
.set("font-size", "var(--lumo-font-size-s)") .set("color", "var(--lumo-secondary-text-color)").set("margin-top", "var(--lumo-space-xs)");
.set("color", "var(--lumo-secondary-text-color)")
.set("margin-top", "var(--lumo-space-xs)");
taskContent.add(taskName, taskDescription); taskContent.add(taskName, taskDescription);
// Status indicator // Status indicator
Div statusIndicator = new Div(); Div statusIndicator = new Div();
statusIndicator.getStyle() statusIndicator.getStyle().set("width", "8px").set("height", "8px").set("border-radius", "50%")
.set("width", "8px") .set("background-color", task.isCompleted() ? "var(--lumo-success-color)" : "var(--lumo-error-color)");
.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); taskCard.add(taskIcon, taskContent, statusIndicator);
@@ -680,10 +646,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
taskCard.addClickListener(event -> { taskCard.addClickListener(event -> {
showTaskDetailsDialog(task); showTaskDetailsDialog(task);
// Reset hover state after dialog interaction // Reset hover state after dialog interaction
taskCard.getStyle() taskCard.getStyle().set("transform", "translateY(0)").set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)")
.set("transform", "translateY(0)") .set("border-color", "var(--lumo-contrast-20pct)");
.set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)")
.set("border-color", "var(--lumo-contrast-20pct)");
}); });
return taskCard; return taskCard;
@@ -707,7 +671,9 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
private String getTaskDescription(BaseTask task) { private String getTaskDescription(BaseTask task) {
if (task.isCompleted()) { 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) { if (task instanceof TodoListTask todoTask) {
@@ -717,9 +683,11 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
if (photoTask.getMinPhotoCount() != null && photoTask.getMaxPhotoCount() != null) { if (photoTask.getMinPhotoCount() != null && photoTask.getMaxPhotoCount() != null) {
return photoTask.getMinPhotoCount() + "-" + photoTask.getMaxPhotoCount() + " Fotos erforderlich"; return photoTask.getMinPhotoCount() + "-" + photoTask.getMaxPhotoCount() + " Fotos erforderlich";
} else if (photoTask.getMinPhotoCount() != null) { } 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) { } else if (photoTask.getMaxPhotoCount() != null) {
return "Max. " + photoTask.getMaxPhotoCount() + " Foto" + (photoTask.getMaxPhotoCount() != 1 ? "s" : ""); return "Max. " + photoTask.getMaxPhotoCount() + " Foto"
+ (photoTask.getMaxPhotoCount() != 1 ? "s" : "");
} else { } else {
return "Foto erforderlich"; return "Foto erforderlich";
} }
@@ -740,83 +708,56 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
private Div createPhotoGallery(List<String> photos) { private Div createPhotoGallery(List<String> photos) {
Div galleryContainer = new Div(); Div galleryContainer = new Div();
galleryContainer.getStyle() galleryContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border", "1px solid var(--lumo-contrast-20pct)") .set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)")
.set("border-radius", "var(--lumo-border-radius-m)") .set("background-color", "white").set("max-width", "600px").set("min-height", "500px")
.set("padding", "var(--lumo-space-m)") .set("height", "500px").set("position", "relative").set("display", "flex").set("align-items", "center")
.set("background-color", "white") .set("justify-content", "center");
.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) { if (photos.size() == 1) {
// Single photo - no navigation needed // Single photo - no navigation needed
Div photoContainer = createPhotoContainer(photos.get(0)); Div photoContainer = createPhotoContainer(photos.get(0));
photoContainer.getStyle() photoContainer.getStyle().set("flex", "1").set("display", "flex").set("align-items", "center")
.set("flex", "1") .set("justify-content", "center");
.set("display", "flex")
.set("align-items", "center")
.set("justify-content", "center");
galleryContainer.add(photoContainer); galleryContainer.add(photoContainer);
} else { } else {
// Multiple photos - add navigation // 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 // Photo counter
Span photoCounter = new Span((currentIndex[0] + 1) + " / " + photos.size()); Span photoCounter = new Span((currentIndex[0] + 1) + " / " + photos.size());
photoCounter.getStyle() photoCounter.getStyle().set("position", "absolute").set("top", "var(--lumo-space-s)")
.set("position", "absolute") .set("right", "var(--lumo-space-s)").set("background-color", "rgba(0, 0, 0, 0.6)")
.set("top", "var(--lumo-space-s)") .set("color", "white").set("padding", "var(--lumo-space-xs) var(--lumo-space-s)")
.set("right", "var(--lumo-space-s)") .set("border-radius", "var(--lumo-border-radius-s)").set("font-size", "var(--lumo-font-size-s)")
.set("background-color", "rgba(0, 0, 0, 0.6)") .set("z-index", "10");
.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 // Photo container
Div photoContainer = createPhotoContainer(photos.get(0)); Div photoContainer = createPhotoContainer(photos.get(0));
photoContainer.getStyle() photoContainer.getStyle().set("margin", "0 40px") // Space for buttons
.set("margin", "0 40px") // Space for buttons .set("flex", "1").set("display", "flex").set("align-items", "center")
.set("flex", "1") .set("justify-content", "center");
.set("display", "flex")
.set("align-items", "center")
.set("justify-content", "center");
// Previous button // Previous button
Button prevButton = new Button(new Icon(VaadinIcon.CHEVRON_LEFT)); Button prevButton = new Button(new Icon(VaadinIcon.CHEVRON_LEFT));
prevButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ICON); prevButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ICON);
prevButton.getStyle() prevButton.getStyle().set("position", "absolute").set("left", "var(--lumo-space-s)").set("top", "50%")
.set("position", "absolute") .set("transform", "translateY(-50%)").set("background-color", "rgba(255, 255, 255, 0.8)")
.set("left", "var(--lumo-space-s)") .set("border-radius", "50%").set("z-index", "10");
.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 // Next button
Button nextButton = new Button(new Icon(VaadinIcon.CHEVRON_RIGHT)); Button nextButton = new Button(new Icon(VaadinIcon.CHEVRON_RIGHT));
nextButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ICON); nextButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ICON);
nextButton.getStyle() nextButton.getStyle().set("position", "absolute").set("right", "var(--lumo-space-s)").set("top", "50%")
.set("position", "absolute") .set("transform", "translateY(-50%)").set("background-color", "rgba(255, 255, 255, 0.8)")
.set("right", "var(--lumo-space-s)") .set("border-radius", "50%").set("z-index", "10");
.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 // Navigation logic
prevButton.addClickListener(e -> { prevButton.addClickListener(e -> {
if (currentIndex[0] > 0) { if (currentIndex[0] > 0) {
currentIndex[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); prevButton.setEnabled(currentIndex[0] > 0);
nextButton.setEnabled(currentIndex[0] < photos.size() - 1); nextButton.setEnabled(currentIndex[0] < photos.size() - 1);
@@ -825,7 +766,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
nextButton.addClickListener(e -> { nextButton.addClickListener(e -> {
if (currentIndex[0] < photos.size() - 1) { if (currentIndex[0] < photos.size() - 1) {
currentIndex[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); prevButton.setEnabled(currentIndex[0] > 0);
nextButton.setEnabled(currentIndex[0] < photos.size() - 1); nextButton.setEnabled(currentIndex[0] < photos.size() - 1);
@@ -843,19 +785,14 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
private Div createPhotoContainer(String base64Photo) { private Div createPhotoContainer(String base64Photo) {
Div photoContainer = new Div(); Div photoContainer = new Div();
photoContainer.getStyle() photoContainer.getStyle().set("width", "100%").set("height", "100%").set("display", "flex")
.set("width", "100%") .set("align-items", "center").set("justify-content", "center").set("overflow", "hidden");
.set("height", "100%")
.set("display", "flex")
.set("align-items", "center")
.set("justify-content", "center")
.set("overflow", "hidden");
// Create image element // Create image element
String imgSrc = base64Photo.startsWith("data:") ? base64Photo : "data:image/jpeg;base64," + base64Photo; String imgSrc = base64Photo.startsWith("data:") ? base64Photo : "data:image/jpeg;base64," + base64Photo;
photoContainer.getElement().setProperty("innerHTML", photoContainer.getElement().setProperty("innerHTML", "<img src='" + imgSrc
"<img src='" + imgSrc + "' style='max-width: 100%; max-height: 450px; object-fit: contain; border-radius: var(--lumo-border-radius-s); box-shadow: 0 2px 8px rgba(0,0,0,0.1);' />"); + "' style='max-width: 100%; max-height: 450px; object-fit: contain; border-radius: var(--lumo-border-radius-s); box-shadow: 0 2px 8px rgba(0,0,0,0.1);' />");
return photoContainer; return photoContainer;
} }
@@ -863,8 +800,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
private void updatePhotoDisplay(Div photoContainer, String base64Photo, Span counter, int current, int total) { private void updatePhotoDisplay(Div photoContainer, String base64Photo, Span counter, int current, int total) {
String imgSrc = base64Photo.startsWith("data:") ? base64Photo : "data:image/jpeg;base64," + base64Photo; String imgSrc = base64Photo.startsWith("data:") ? base64Photo : "data:image/jpeg;base64," + base64Photo;
photoContainer.getElement().setProperty("innerHTML", photoContainer.getElement().setProperty("innerHTML", "<img src='" + imgSrc
"<img src='" + imgSrc + "' style='max-width: 100%; max-height: 450px; object-fit: contain; border-radius: var(--lumo-border-radius-s); box-shadow: 0 2px 8px rgba(0,0,0,0.1);' />"); + "' style='max-width: 100%; max-height: 450px; object-fit: contain; border-radius: var(--lumo-border-radius-s); box-shadow: 0 2px 8px rgba(0,0,0,0.1);' />");
counter.setText(current + " / " + total); counter.setText(current + " / " + total);
} }
@@ -875,12 +812,11 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
} }
// Remove any existing width and height attributes and add responsive styling // Remove any existing width and height attributes and add responsive styling
String responsiveSvg = svgContent String responsiveSvg = svgContent.replaceAll("width\\s*=\\s*[\"'][^\"']*[\"']", "")
.replaceAll("width\\s*=\\s*[\"'][^\"']*[\"']", "") .replaceAll("height\\s*=\\s*[\"'][^\"']*[\"']", "").replaceAll("style\\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")) { if (!responsiveSvg.contains("viewBox")) {
// Try to extract original dimensions for viewBox // Try to extract original dimensions for viewBox
String widthMatch = extractAttribute(svgContent, "width"); String widthMatch = extractAttribute(svgContent, "width");
@@ -894,7 +830,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
if (!cleanWidth.isEmpty() && !cleanHeight.isEmpty()) { if (!cleanWidth.isEmpty() && !cleanHeight.isEmpty()) {
responsiveSvg = responsiveSvg.replaceFirst("<svg", responsiveSvg = responsiveSvg.replaceFirst("<svg",
"<svg viewBox=\"0 0 " + cleanWidth + " " + cleanHeight + "\""); "<svg viewBox=\"0 0 " + cleanWidth + " " + cleanHeight + "\"");
} }
} catch (Exception e) { } catch (Exception e) {
// Ignore extraction errors // Ignore extraction errors
@@ -904,7 +840,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
// Add responsive styling // Add responsive styling
responsiveSvg = responsiveSvg.replaceFirst("<svg", responsiveSvg = responsiveSvg.replaceFirst("<svg",
"<svg style=\"max-width: 100%; max-height: 200px; width: auto; height: auto;\""); "<svg style=\"max-width: 100%; max-height: 200px; width: auto; height: auto;\"");
return responsiveSvg; return responsiveSvg;
} }
@@ -923,13 +859,9 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
// Reset hover state for all task cards // Reset hover state for all task cards
for (Div taskCard : taskCards) { for (Div taskCard : taskCards) {
if (taskCard != null) { if (taskCard != null) {
taskCard.getStyle() taskCard.getStyle().set("transform", "translateY(0)").set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)")
.set("transform", "translateY(0)") .set("border-color", "var(--lumo-contrast-20pct)");
.set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)")
.set("border-color", "var(--lumo-contrast-20pct)");
} }
} }
} }
} }

View File

@@ -48,7 +48,7 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
private Authentication pendingAuth; private Authentication pendingAuth;
public LoginView() { public LoginView() {
addClassName("login-view"); addClassName("login-view");
setSizeFull(); setSizeFull();
@@ -72,23 +72,18 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
H1 title = new H1("VotianLT"); H1 title = new H1("VotianLT");
title.getStyle().set("color", "var(--lumo-primary-color)"); title.getStyle().set("color", "var(--lumo-primary-color)");
Button registerButton = new Button("Noch kein Konto? Registrieren", Button registerButton = new Button("Noch kein Konto? Registrieren", e -> UI.getCurrent().navigate("register"));
e -> UI.getCurrent().navigate("register"));
registerButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); registerButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
// Inline flash message box (hidden by default) // Inline flash message box (hidden by default)
flashBox.getStyle() flashBox.getStyle().set("background", "var(--lumo-error-color-10pct)")
.set("background", "var(--lumo-error-color-10pct)") .set("color", "var(--lumo-error-text-color)").set("border", "1px solid var(--lumo-error-color)")
.set("color", "var(--lumo-error-text-color)") .set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)")
.set("border", "1px solid var(--lumo-error-color)") .set("width", "100%").set("display", "none");
.set("border-radius", "var(--lumo-border-radius-m)")
.set("padding", "var(--lumo-space-m)")
.set("width", "100%")
.set("display", "none");
VerticalLayout loginLayout = new VerticalLayout(); VerticalLayout loginLayout = new VerticalLayout();
loginLayout.setAlignItems(FlexComponent.Alignment.CENTER); loginLayout.setAlignItems(FlexComponent.Alignment.CENTER);
loginLayout.add(flashBox, title, loginForm, twoFaField, verify2faButton, registerButton); loginLayout.add(flashBox, title, loginForm, twoFaField, verify2faButton, registerButton);
loginLayout.setMaxWidth("400px"); loginLayout.setMaxWidth("400px");
loginLayout.setPadding(true); loginLayout.setPadding(true);
@@ -101,8 +96,9 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
private void handlePasswordLogin(String username, String password) { private void handlePasswordLogin(String username, String password) {
try { try {
// Prüfe Benutzername/Passwort // Prüfe Benutzername/Passwort
Authentication auth = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); Authentication auth = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password));
if (twoFactorEnabled) { if (twoFactorEnabled) {
// 2FA aktiviert: Benutzer noch nicht in SecurityContext setzen // 2FA aktiviert: Benutzer noch nicht in SecurityContext setzen
this.pendingAuth = auth; this.pendingAuth = auth;
@@ -129,7 +125,8 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
private void handleVerify2fa() { private void handleVerify2fa() {
if (pendingAuth == null) { 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; return;
} }
String username = pendingAuth.getName(); String username = pendingAuth.getName();
@@ -145,7 +142,8 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
} }
// 2FA korrekt: Benutzer nun anmelden // 2FA korrekt: Benutzer nun anmelden
SecurityContextHolder.getContext().setAuthentication(pendingAuth); 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(); var vaadinSession = VaadinSession.getCurrent();
if (vaadinSession != null) { if (vaadinSession != null) {
var wrappedSession = vaadinSession.getSession(); var wrappedSession = vaadinSession.getSession();
@@ -160,10 +158,7 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
@Override @Override
public void beforeEnter(BeforeEnterEvent beforeEnterEvent) { public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
// Zeige Fehlermeldung bei fehlgeschlagener Anmeldung // Zeige Fehlermeldung bei fehlgeschlagener Anmeldung
if (beforeEnterEvent.getLocation() if (beforeEnterEvent.getLocation().getQueryParameters().getParameters().containsKey("error")) {
.getQueryParameters()
.getParameters()
.containsKey("error")) {
loginForm.setError(true); loginForm.setError(true);
} }
} }

View File

@@ -24,9 +24,9 @@ import java.util.List;
/** /**
* Meine Rechnungen nutzerzentrierte Übersicht. * Meine Rechnungen nutzerzentrierte Übersicht.
* *
* Layout orientiert am bereitgestellten Screenshot: * Layout orientiert am bereitgestellten Screenshot: - Zwei Karten oben (Offene
* - Zwei Karten oben (Offene Rechnungen, Bankverbindung) * Rechnungen, Bankverbindung) - Darunter ein Bereich „Rechnungen" mit Grid,
* - Darunter ein Bereich „Rechnungen" mit Grid, Suche und Seitengröße * Suche und Seitengröße
*/ */
@PageTitle("Meine Rechnungen") @PageTitle("Meine Rechnungen")
@Route(value = "my-invoices", layout = MainLayout.class) @Route(value = "my-invoices", layout = MainLayout.class)
@@ -54,14 +54,11 @@ public class MyInvoicesView extends Main {
private Component createTopCards() { private Component createTopCards() {
// Container mit zwei Spalten (responsiv) // Container mit zwei Spalten (responsiv)
Div container = new Div(); Div container = new Div();
container.getStyle() container.getStyle().set("display", "grid").set("grid-template-columns", "48% 2% 48%");
.set("display", "grid") // .set("gap", "10px");
.set("grid-template-columns", "48% 2% 48%");
//.set("gap", "10px");
// Spaltenabstände: 2% zwischen den beiden Spalten // Spaltenabstände: 2% zwischen den beiden Spalten
container.getStyle().set("column-gap", "0"); container.getStyle().set("column-gap", "0");
// Karte: Offene Rechnungen // Karte: Offene Rechnungen
Paragraph hint = new Paragraph("Momentan sind keine neuen Rechnungen für Sie im System gespeichert."); Paragraph hint = new Paragraph("Momentan sind keine neuen Rechnungen für Sie im System gespeichert.");
hint.getStyle().set("color", "var(--lumo-success-text-color)"); hint.getStyle().set("color", "var(--lumo-success-text-color)");
@@ -71,12 +68,9 @@ public class MyInvoicesView extends Main {
VerticalLayout bankData = new VerticalLayout(); VerticalLayout bankData = new VerticalLayout();
bankData.setPadding(false); bankData.setPadding(false);
bankData.setSpacing(false); bankData.setSpacing(false);
bankData.add( bankData.add(labeledValue("Kreditinstitut", "Hamburger Sparkasse"),
labeledValue("Kreditinstitut", "Hamburger Sparkasse"),
labeledValue("Begünstigter", "Assecutor Data Service GmbH"), labeledValue("Begünstigter", "Assecutor Data Service GmbH"),
labeledValue("IBAN", "DE67200505501217139888"), labeledValue("IBAN", "DE67200505501217139888"), labeledValue("Verwendungszweck", "vlt-00000610"));
labeledValue("Verwendungszweck", "vlt-00000610")
);
Div bankCard = createCard("Bankverbindung", bankData); Div bankCard = createCard("Bankverbindung", bankData);
container.add(openInvoicesCard, bankCard); container.add(openInvoicesCard, bankCard);
@@ -144,12 +138,11 @@ public class MyInvoicesView extends Main {
private void applyFilter(String filter) { private void applyFilter(String filter) {
String f = filter == null ? "" : filter.toLowerCase(); String f = filter == null ? "" : filter.toLowerCase();
grid.setItems(allRows.stream().filter(row -> grid.setItems(allRows.stream()
row.status.toLowerCase().contains(f) .filter(row -> row.status.toLowerCase().contains(f) || row.invoiceNumber.toLowerCase().contains(f)
|| row.invoiceNumber.toLowerCase().contains(f)
|| row.date.toString().toLowerCase().contains(f) || row.date.toString().toLowerCase().contains(f)
|| String.valueOf(row.amount).toLowerCase().contains(f) || String.valueOf(row.amount).toLowerCase().contains(f))
).toList()); .toList());
} }
private Div createCard(String title, Component content) { private Div createCard(String title, Component content) {
@@ -174,16 +167,13 @@ public class MyInvoicesView extends Main {
} }
private void styleCard(Div card) { private void styleCard(Div card) {
card.getStyle() card.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border", "1px solid var(--lumo-contrast-20pct)") .set("border-radius", "var(--lumo-border-radius-l)").set("padding", "var(--lumo-space-m)")
.set("border-radius", "var(--lumo-border-radius-l)") .set("background", "var(--lumo-base-color)").set("box-shadow", "0 1px 1px rgba(0,0,0,0.02)");
.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(); card.setWidthFull();
} }
// Schlanke lokale Repräsentation für das Grid // 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) {
}
} }

View File

@@ -16,8 +16,7 @@ import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.VaadinSession; import com.vaadin.flow.server.VaadinSession;
import com.vaadin.flow.server.auth.AnonymousAllowed; import com.vaadin.flow.server.auth.AnonymousAllowed;
import de.assecutor.votianlt.pages.service.UserService; import de.assecutor.votianlt.pages.service.UserService;
import de.assecutor.votianlt.util.MailUtil; import de.assecutor.votianlt.service.EmailService;
import jakarta.mail.MessagingException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.time.Duration; import java.time.Duration;
@@ -28,7 +27,7 @@ import java.time.LocalDateTime;
@AnonymousAllowed @AnonymousAllowed
public class RegisterView extends VerticalLayout { public class RegisterView extends VerticalLayout {
private final UserService userService; private final UserService userService;
private final MailUtil mailUtil; private final EmailService emailService;
private TextField emailField; private TextField emailField;
private PasswordField passwordField; private PasswordField passwordField;
@@ -54,9 +53,9 @@ public class RegisterView extends VerticalLayout {
private LocalDateTime lastSentAt; private LocalDateTime lastSentAt;
private boolean awaitingVerification = false; private boolean awaitingVerification = false;
public RegisterView(UserService userService, MailUtil mailUtil) { public RegisterView(UserService userService, EmailService emailService) {
this.userService = userService; this.userService = userService;
this.mailUtil = mailUtil; this.emailService = emailService;
// Layout-Konfiguration für vollständige Zentrierung // Layout-Konfiguration für vollständige Zentrierung
setSizeFull(); setSizeFull();
setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
@@ -166,17 +165,14 @@ public class RegisterView extends VerticalLayout {
resendButton.setVisible(false); resendButton.setVisible(false);
// Zurück-Link // Zurück-Link
Button backButton = new Button("Zurück zur Startseite", event -> Button backButton = new Button("Zurück zur Startseite", event -> getUI().ifPresent(ui -> ui.navigate("")));
getUI().ifPresent(ui -> ui.navigate("")));
backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
backButton.setWidthFull(); backButton.setWidthFull();
// Zweispaltiges Formular // Zweispaltiges Formular
FormLayout form = new FormLayout(); FormLayout form = new FormLayout();
form.setWidthFull(); form.setWidthFull();
form.setResponsiveSteps( form.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 2));
new FormLayout.ResponsiveStep("0", 2)
);
// Firma zuerst, volle Breite // Firma zuerst, volle Breite
form.add(companyField); form.add(companyField);
@@ -238,7 +234,8 @@ public class RegisterView extends VerticalLayout {
return; return;
} }
if (userService.existsByEmail(email)) { 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; return;
} }
if (password.isEmpty()) { if (password.isEmpty()) {
@@ -315,7 +312,8 @@ public class RegisterView extends VerticalLayout {
// Rate-Limit: 60 Sekunden zwischen Sendungen // Rate-Limit: 60 Sekunden zwischen Sendungen
if (lastSentAt != null && Duration.between(lastSentAt, LocalDateTime.now()).getSeconds() < 60) { if (lastSentAt != null && Duration.between(lastSentAt, LocalDateTime.now()).getSeconds() < 60) {
long wait = 60 - Duration.between(lastSentAt, LocalDateTime.now()).getSeconds(); 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; return;
} }
String code = generateSixDigitCode(); String code = generateSixDigitCode();
@@ -324,11 +322,10 @@ public class RegisterView extends VerticalLayout {
lastSentAt = LocalDateTime.now(); lastSentAt = LocalDateTime.now();
String subject = "Ihr VotianLT Bestätigungscode"; String subject = "Ihr VotianLT Bestätigungscode";
String body = "Ihr Bestätigungscode lautet: " + code + "\n\n" + String body = "Ihr Bestätigungscode lautet: " + code + "\n\n" + "Dieser Code ist 10 Minuten gültig.\n"
"Dieser Code ist 10 Minuten gültig.\n" + + "Wenn Sie diese Registrierung nicht angefragt haben, ignorieren Sie diese E-Mail.";
"Wenn Sie diese Registrierung nicht angefragt haben, ignorieren Sie diese E-Mail.";
try { try {
mailUtil.sendMail(email, subject, body); emailService.sendSimpleEmail(email, subject, body);
awaitingVerification = true; awaitingVerification = true;
// UI umstellen: Code-Eingabe anzeigen // UI umstellen: Code-Eingabe anzeigen
codeField.clear(); codeField.clear();
@@ -352,8 +349,9 @@ public class RegisterView extends VerticalLayout {
submitButton.setEnabled(false); submitButton.setEnabled(false);
Notification.show("Ein Bestätigungscode wurde an " + email + " gesendet.", 4000, Notification.Position.MIDDLE); Notification.show("Ein Bestätigungscode wurde an " + email + " gesendet.", 4000,
} catch (MessagingException e) { Notification.Position.MIDDLE);
} catch (Exception e) {
awaitingVerification = false; awaitingVerification = false;
Notification.show("Fehler beim Senden der E-Mail: " + e.getMessage(), 5000, Notification.Position.MIDDLE); Notification.show("Fehler beim Senden der E-Mail: " + e.getMessage(), 5000, Notification.Position.MIDDLE);
} }
@@ -370,7 +368,8 @@ public class RegisterView extends VerticalLayout {
return; return;
} }
if (codeExpiresAt == null || LocalDateTime.now().isAfter(codeExpiresAt)) { 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; return;
} }
if (!entered.equals(pendingCode)) { if (!entered.equals(pendingCode)) {
@@ -387,7 +386,8 @@ public class RegisterView extends VerticalLayout {
try { try {
var user = userService.createUser(email, password, firstName, lastName); var user = userService.createUser(email, password, firstName, lastName);
// Persistiere zusätzliche Profil-/Adressdaten // 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 company = companyField.getValue() != null ? companyField.getValue().trim() : null;
var street = streetField.getValue() != null ? streetField.getValue().trim() : null; var street = streetField.getValue() != null ? streetField.getValue().trim() : null;
var houseNo = houseNumberField.getValue() != null ? houseNumberField.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.setZip(zip);
user.setCity(city); user.setCity(city);
userService.save(user); 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")); getUI().ifPresent(ui -> ui.navigate("login"));
} catch (RuntimeException e) { } catch (RuntimeException e) {
Notification.show("Registrierung fehlgeschlagen: " + e.getMessage(), 5000, Notification.Position.MIDDLE); Notification.show("Registrierung fehlgeschlagen: " + e.getMessage(), 5000, Notification.Position.MIDDLE);

View File

@@ -17,7 +17,7 @@ import org.springframework.beans.factory.annotation.Autowired;
@PageTitle("Kunden") @PageTitle("Kunden")
@Route(value = "customers", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) @Route(value = "customers", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({"USER","ADMIN"}) @RolesAllowed({ "USER", "ADMIN" })
public class ShowCustomersView extends VerticalLayout { public class ShowCustomersView extends VerticalLayout {
private final CustomerService customerService; private final CustomerService customerService;
@@ -43,32 +43,33 @@ public class ShowCustomersView extends VerticalLayout {
add(header); add(header);
// Add hint text // 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("color", "var(--lumo-secondary-text-color)");
hintText.getStyle().set("font-size", "var(--lumo-font-size-s)"); hintText.getStyle().set("font-size", "var(--lumo-font-size-s)");
add(hintText); add(hintText);
// Configure grid columns // Configure grid columns
grid.addColumn(Customer::getCompanyName).setHeader("Firma").setAutoWidth(true).setFlexGrow(1).setSortable(true); grid.addColumn(Customer::getCompanyName).setHeader("Firma").setAutoWidth(true).setFlexGrow(1).setSortable(true);
grid.addColumn(customer -> (customer.getFirstname() != null ? customer.getFirstname() : "") + " " + grid.addColumn(customer -> (customer.getFirstname() != null ? customer.getFirstname() : "") + " "
(customer.getLastName() != null ? customer.getLastName() : "")) + (customer.getLastName() != null ? customer.getLastName() : "")).setHeader("Name").setAutoWidth(true)
.setHeader("Name").setAutoWidth(true).setFlexGrow(1).setSortable(true); .setFlexGrow(1).setSortable(true);
grid.addColumn(Customer::getMail).setHeader("E-Mail").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::getTelephone).setHeader("Telefon").setAutoWidth(true).setSortable(true);
grid.addColumn(customer -> (customer.getStreet() != null ? customer.getStreet() : "") + " " + grid.addColumn(customer -> (customer.getStreet() != null ? customer.getStreet() : "") + " "
(customer.getHouseNumber() != null ? customer.getHouseNumber() : "")) + (customer.getHouseNumber() != null ? customer.getHouseNumber() : "")).setHeader("Straße")
.setHeader("Straße").setAutoWidth(true).setFlexGrow(1).setSortable(true); .setAutoWidth(true).setFlexGrow(1).setSortable(true);
grid.addColumn(customer -> (customer.getZip() != null ? customer.getZip() : "") + " " + grid.addColumn(customer -> (customer.getZip() != null ? customer.getZip() : "") + " "
(customer.getCity() != null ? customer.getCity() : "")) + (customer.getCity() != null ? customer.getCity() : "")).setHeader("Ort").setAutoWidth(true)
.setHeader("Ort").setAutoWidth(true).setFlexGrow(1).setSortable(true); .setFlexGrow(1).setSortable(true);
grid.setMultiSort(true); grid.setMultiSort(true);
grid.setSizeFull(); grid.setSizeFull();
// Make grid rows clickable // Make grid rows clickable
grid.setSelectionMode(Grid.SelectionMode.SINGLE); grid.setSelectionMode(Grid.SelectionMode.SINGLE);
grid.getStyle().set("cursor", "pointer"); grid.getStyle().set("cursor", "pointer");
// Add click listener to navigate to edit view // Add click listener to navigate to edit view
grid.addItemClickListener(event -> { grid.addItemClickListener(event -> {
Customer customer = event.getItem(); Customer customer = event.getItem();
@@ -76,13 +77,11 @@ public class ShowCustomersView extends VerticalLayout {
getUI().ifPresent(ui -> ui.navigate("edit-customer/" + customer.getId().toHexString())); getUI().ifPresent(ui -> ui.navigate("edit-customer/" + customer.getId().toHexString()));
} }
}); });
add(grid); add(grid);
// Button action // Button action
addCustomerButton.addClickListener(e -> addCustomerButton.addClickListener(e -> getUI().ifPresent(ui -> ui.navigate("add-customer")));
getUI().ifPresent(ui -> ui.navigate("add-customer"))
);
loadData(); loadData();
} }
@@ -91,8 +90,7 @@ public class ShowCustomersView extends VerticalLayout {
var customers = customerService.findAll(); var customers = customerService.findAll();
var currentUserId = securityService.getCurrentUserId(); var currentUserId = securityService.getCurrentUserId();
var ownCustomers = customers.stream() var ownCustomers = customers.stream()
.filter(c -> c.getCreatedBy() != null && c.getCreatedBy().equals(currentUserId)) .filter(c -> c.getCreatedBy() != null && c.getCreatedBy().equals(currentUserId)).toList();
.toList();
grid.setItems(ownCustomers); grid.setItems(ownCustomers);
} }
} }

View File

@@ -21,7 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired;
@PageTitle("Aufträge") @PageTitle("Aufträge")
@Route(value = "jobs", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) @Route(value = "jobs", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({"USER"}) @RolesAllowed({ "USER" })
public class ShowJobsView extends VerticalLayout { public class ShowJobsView extends VerticalLayout {
private final DatePicker startDate = new DatePicker("Startdatum"); private final DatePicker startDate = new DatePicker("Startdatum");
@@ -64,7 +64,6 @@ public class ShowJobsView extends VerticalLayout {
filterBar.setAlignItems(Alignment.END); filterBar.setAlignItems(Alignment.END);
add(filterBar); add(filterBar);
H2 title = new H2("Aufträge"); H2 title = new H2("Aufträge");
add(title); add(title);
// Init default period: last 30 days // Init default period: last 30 days
@@ -80,7 +79,6 @@ public class ShowJobsView extends VerticalLayout {
startDate.addValueChangeListener(e -> loadData()); startDate.addValueChangeListener(e -> loadData());
endDate.addValueChangeListener(e -> loadData()); endDate.addValueChangeListener(e -> loadData());
// Configure grid columns: Kunde, Auftragsnummer, Auftragsdatum, Zielort // Configure grid columns: Kunde, Auftragsnummer, Auftragsdatum, Zielort
grid.addColumn(Job::getDeliveryCompany).setHeader("Kunde").setAutoWidth(true).setFlexGrow(1).setSortable(true); grid.addColumn(Job::getDeliveryCompany).setHeader("Kunde").setAutoWidth(true).setFlexGrow(1).setSortable(true);
grid.addColumn(Job::getJobNumber).setHeader("Auftragsnummer").setAutoWidth(true).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.setMultiSort(true);
grid.setSizeFull(); grid.setSizeFull();
// Make grid rows clickable // Make grid rows clickable
grid.setSelectionMode(Grid.SelectionMode.SINGLE); grid.setSelectionMode(Grid.SelectionMode.SINGLE);
grid.getStyle().set("cursor", "pointer"); grid.getStyle().set("cursor", "pointer");
// Add click listener to navigate to job summary view // Add click listener to navigate to job summary view
grid.addItemClickListener(event -> { grid.addItemClickListener(event -> {
Job job = event.getItem(); Job job = event.getItem();
@@ -101,7 +99,7 @@ public class ShowJobsView extends VerticalLayout {
getUI().ifPresent(ui -> ui.navigate("job_summary/" + job.getId().toHexString())); getUI().ifPresent(ui -> ui.navigate("job_summary/" + job.getId().toHexString()));
} }
}); });
add(grid); add(grid);
loadData(); loadData();
@@ -110,8 +108,10 @@ public class ShowJobsView extends VerticalLayout {
private void loadData() { private void loadData() {
var start = startDate.getValue(); var start = startDate.getValue();
var end = endDate.getValue(); var end = endDate.getValue();
java.time.LocalDateTime startDt = start != null ? start.atStartOfDay() : java.time.LocalDate.now().minusDays(30).atStartOfDay(); java.time.LocalDateTime startDt = start != null ? start.atStartOfDay()
java.time.LocalDateTime endDt = end != null ? end.atTime(23,59,59) : java.time.LocalDate.now().atTime(23,59,59); : 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 // Aktuellen Benutzer (ObjectId Hex) ermitteln
String currentUserIdHex = securityService.getCurrentUserId().toHexString(); String currentUserIdHex = securityService.getCurrentUserId().toHexString();
@@ -123,29 +123,31 @@ public class ShowJobsView extends VerticalLayout {
if ("Erledigt".equals(selectedStatus)) { if ("Erledigt".equals(selectedStatus)) {
statusList = java.util.List.of(JobStatus.DELIVERED, JobStatus.COMPLETED, JobStatus.CANCELLED); statusList = java.util.List.of(JobStatus.DELIVERED, JobStatus.COMPLETED, JobStatus.CANCELLED);
} else if ("Offen".equals(selectedStatus)) { } else if ("Offen".equals(selectedStatus)) {
statusList = java.util.List.of(JobStatus.CREATED, JobStatus.IN_PROGRESS, statusList = java.util.List.of(JobStatus.CREATED, JobStatus.IN_PROGRESS, JobStatus.PICKUP_SCHEDULED,
JobStatus.PICKUP_SCHEDULED, JobStatus.PICKED_UP, JobStatus.PICKED_UP, JobStatus.IN_TRANSIT);
JobStatus.IN_TRANSIT);
} else { // "Alle" } else { // "Alle"
statusList = java.util.Arrays.asList(JobStatus.values()); statusList = java.util.Arrays.asList(JobStatus.values());
} }
// Suchtext für Auftragsnummer // Suchtext für Auftragsnummer
String searchText = searchField.getValue(); String searchText = searchField.getValue();
String jobNumberPattern = searchText != null && !searchText.trim().isEmpty() String jobNumberPattern = searchText != null && !searchText.trim().isEmpty() ? searchText.trim() : ".*"; // Regex
? searchText.trim() // für
: ".*"; // Regex für alle wenn leer // alle
// wenn
// leer
// Verwende die erweiterte Suchmethode // Verwende die erweiterte Suchmethode
var filteredJobs = jobRepository.findWithFilters(startDt, endDt, currentUserIdHex, var filteredJobs = jobRepository.findWithFilters(startDt, endDt, currentUserIdHex, jobNumberPattern,
jobNumberPattern, statusList); statusList);
grid.setItems(filteredJobs); grid.setItems(filteredJobs);
} }
private void exportToCsv() { private void exportToCsv() {
var items = grid.getListDataView().getItems().toList(); 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.setContentType("text/csv");
resource.setCacheTime(0); resource.setCacheTime(0);
@@ -157,9 +159,8 @@ public class ShowJobsView extends VerticalLayout {
// Add to UI and trigger download via JavaScript // Add to UI and trigger download via JavaScript
add(downloadAnchor); add(downloadAnchor);
getUI().ifPresent(ui -> ui.getPage().executeJs( getUI().ifPresent(ui -> ui.getPage().executeJs(
"const link = arguments[0]; link.click(); setTimeout(() => link.remove(), 100);", "const link = arguments[0]; link.click(); setTimeout(() => link.remove(), 100);",
downloadAnchor.getElement() downloadAnchor.getElement()));
));
} }
private String generateCsv(java.util.List<Job> jobs) { private String generateCsv(java.util.List<Job> jobs) {
@@ -179,11 +180,11 @@ public class ShowJobsView extends VerticalLayout {
} }
private String escapeCsv(String value) { private String escapeCsv(String value) {
if (value == null) return ""; if (value == null)
return "";
if (value.contains(",") || value.contains("\"") || value.contains("\n")) { if (value.contains(",") || value.contains("\"") || value.contains("\n")) {
return "\"" + value.replace("\"", "\"\"") + "\""; return "\"" + value.replace("\"", "\"\"") + "\"";
} }
return value; return value;
} }
} }

View File

@@ -73,9 +73,8 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
logo.getStyle().set("font-weight", "bold"); logo.getStyle().set("font-weight", "bold");
// Navigation - abhängig vom Anmeldestatus // Navigation - abhängig vom Anmeldestatus
Component navigation = securityService.isUserLoggedIn() Component navigation = securityService.isUserLoggedIn() ? createAuthenticatedNavigation()
? createAuthenticatedNavigation() : createAnonymousNavigation();
: createAnonymousNavigation();
header.add(logo, navigation); header.add(logo, navigation);
@@ -103,8 +102,7 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
navLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER); navLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
// Auftragserstellung Button // Auftragserstellung Button
Button createOrderBtn = new Button("Auftragserstellung", event -> Button createOrderBtn = new Button("Auftragserstellung", event -> UI.getCurrent().navigate("add_job"));
UI.getCurrent().navigate("add_job"));
createOrderBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY); createOrderBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
// Verwaltung ComboBox // Verwaltung ComboBox
@@ -115,15 +113,15 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
String value = event.getValue(); String value = event.getValue();
if (value != null) { if (value != null) {
switch (value) { switch (value) {
case "Kunden": case "Kunden":
UI.getCurrent().navigate("customer"); UI.getCurrent().navigate("customer");
break; break;
case "Aufträge": case "Aufträge":
UI.getCurrent().navigate("orders"); UI.getCurrent().navigate("orders");
break; break;
case "Firmen": case "Firmen":
UI.getCurrent().navigate("add_company"); UI.getCurrent().navigate("add_company");
break; break;
} }
managementCombo.clear(); // Reset selection managementCombo.clear(); // Reset selection
} }
@@ -138,13 +136,13 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
String value = event.getValue(); String value = event.getValue();
if (value != null) { if (value != null) {
switch (value) { switch (value) {
case "Profil anzeigen": case "Profil anzeigen":
break; break;
case "Einstellungen": case "Einstellungen":
break; break;
case "Abmelden": case "Abmelden":
securityService.logout(); securityService.logout();
break; break;
} }
userCombo.clear(); // Reset selection userCombo.clear(); // Reset selection
} }
@@ -157,7 +155,7 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
notificationBtn.setTooltipText("Benachrichtigungen"); notificationBtn.setTooltipText("Benachrichtigungen");
notificationBtn.addClickListener(event -> { notificationBtn.addClickListener(event -> {
com.vaadin.flow.component.notification.Notification.show("Keine neuen Benachrichtigungen", 3000, 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); navLayout.add(createOrderBtn, managementCombo, userCombo, notificationBtn);
@@ -170,7 +168,8 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
heroSection.setPadding(true); heroSection.setPadding(true);
heroSection.setSpacing(true); heroSection.setSpacing(true);
heroSection.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); 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.getStyle().set("min-height", "400px");
heroSection.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); 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("color", "var(--lumo-primary-text-color)");
heroTitle.getStyle().set("margin-bottom", "var(--lumo-space-l)"); heroTitle.getStyle().set("margin-bottom", "var(--lumo-space-l)");
Paragraph heroDescription = new Paragraph( Paragraph heroDescription = new Paragraph("Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe - "
"Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe - " + + "volldigital und aus einem Guss. Konzentrieren Sie sich auf Ihr Geschäft, "
"volldigital und aus einem Guss. Konzentrieren Sie sich auf Ihr Geschäft, " + + "wir kümmern uns um die Büroarbeit.");
"wir kümmern uns um die Büroarbeit."
);
heroDescription.getStyle().set("text-align", "center"); heroDescription.getStyle().set("text-align", "center");
heroDescription.getStyle().set("max-width", "600px"); heroDescription.getStyle().set("max-width", "600px");
heroDescription.getStyle().set("font-size", "var(--lumo-font-size-l)"); 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"); systemTitle.getStyle().set("text-align", "center");
Paragraph systemIntro = new Paragraph( Paragraph systemIntro = new Paragraph(
"Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe ist von entscheidender Bedeutung, " + "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." + "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("text-align", "center");
systemIntro.getStyle().set("max-width", "800px"); systemIntro.getStyle().set("max-width", "800px");
systemIntro.getStyle().set("margin-bottom", "var(--lumo-space-xl)"); 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); featuresGrid.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.START);
// Feature Cards // Feature Cards
featuresGrid.add( featuresGrid.add(createFeatureCard(VaadinIcon.COG, "Einrichtungsassistent",
createFeatureCard(VaadinIcon.COG, "Einrichtungsassistent",
"Mithilfe des Einrichtungsassistenten haben Sie die Möglichkeit, Ihr Nutzerprofil zu vervollständigen."), "Mithilfe des Einrichtungsassistenten haben Sie die Möglichkeit, Ihr Nutzerprofil zu vervollständigen."),
createFeatureCard(VaadinIcon.USERS, "Kunden- und Auftragsverwaltung", createFeatureCard(VaadinIcon.USERS, "Kunden- und Auftragsverwaltung",
"Mit der Kunden- und Auftragsverwaltung haben Sie alle Kontaktdaten und Auftragsdetails stets im Blick."), "Mit der Kunden- und Auftragsverwaltung haben Sie alle Kontaktdaten und Auftragsdetails stets im Blick."),
createFeatureCard(VaadinIcon.CLIPBOARD_TEXT, "Auftragserstellung", 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.") "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); systemSection.add(systemTitle, systemIntro, featuresGrid);
return systemSection; return systemSection;
@@ -281,9 +275,8 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
appTitle.getStyle().set("text-align", "center"); appTitle.getStyle().set("text-align", "center");
Paragraph appDescription = new Paragraph( Paragraph appDescription = new Paragraph(
"Jeder Auftrag kann optional über die votianLT-App abgearbeitet werden ganz ohne \"Zettelwirtschaft\". " + "Jeder Auftrag kann optional über die votianLT-App abgearbeitet werden ganz ohne \"Zettelwirtschaft\". "
"So gelangen alle relevanten Auftragsinformationen direkt auf das Smartphone des Fahrers." + "So gelangen alle relevanten Auftragsinformationen direkt auf das Smartphone des Fahrers.");
);
appDescription.getStyle().set("text-align", "center"); appDescription.getStyle().set("text-align", "center");
appDescription.getStyle().set("max-width", "800px"); appDescription.getStyle().set("max-width", "800px");
@@ -314,18 +307,12 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
companyInfo.setPadding(false); companyInfo.setPadding(false);
companyInfo.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); companyInfo.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
companyInfo.add( companyInfo.add(new Paragraph("Assecutor Data Service GmbH"), new Paragraph("Ottensener Str. 8, 22525 Hamburg"),
new Paragraph("Assecutor Data Service GmbH"), new Paragraph("Telefon: +49 40 18 123 771 0"), new Paragraph("E-Mail: ahoi@assecutor.de"));
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 // Call to Action
Paragraph ctaText = new Paragraph( Paragraph ctaText = new Paragraph("Registrieren Sie sich noch heute und nutzen den kostenfreien Probemonat, "
"Registrieren Sie sich noch heute und nutzen den kostenfreien Probemonat, " + + "um das System auf Herz und Nieren zu testen.");
"um das System auf Herz und Nieren zu testen."
);
ctaText.getStyle().set("text-align", "center"); ctaText.getStyle().set("text-align", "center");
ctaText.getStyle().set("font-weight", "bold"); ctaText.getStyle().set("font-weight", "bold");
ctaText.getStyle().set("color", "var(--lumo-primary-color)"); ctaText.getStyle().set("color", "var(--lumo-primary-color)");

View File

@@ -15,7 +15,7 @@ import jakarta.annotation.security.RolesAllowed;
@PageTitle("Statistiken") @PageTitle("Statistiken")
@Route(value = "statistics", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) @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") @JavaScript("https://cdn.jsdelivr.net/npm/chart.js")
public class StatisticsView extends VerticalLayout { public class StatisticsView extends VerticalLayout {
@@ -56,11 +56,11 @@ public class StatisticsView extends VerticalLayout {
revenueContainer.setWidthFull(); revenueContainer.setWidthFull();
revenueContainer.setHeight("400px"); revenueContainer.setHeight("400px");
revenueContainer.setPadding(false); revenueContainer.setPadding(false);
Div revenueChart = createRevenueByCustomerChart(); Div revenueChart = createRevenueByCustomerChart();
revenueChart.setSizeFull(); revenueChart.setSizeFull();
revenueContainer.add(revenueChart); revenueContainer.add(revenueChart);
add(revenueContainer); add(revenueContainer);
} }
@@ -72,13 +72,13 @@ public class StatisticsView extends VerticalLayout {
// Gesamtaufträge // Gesamtaufträge
Div totalOrdersCard = createKpiCard("Gesamtaufträge", "247", "success"); Div totalOrdersCard = createKpiCard("Gesamtaufträge", "247", "success");
// Offene Aufträge // Offene Aufträge
Div openOrdersCard = createKpiCard("Offene Aufträge", "34", "warning"); Div openOrdersCard = createKpiCard("Offene Aufträge", "34", "warning");
// Umsatz diesen Monat // Umsatz diesen Monat
Div revenueCard = createKpiCard("Umsatz (Monat)", "€ 24.500", "primary"); Div revenueCard = createKpiCard("Umsatz (Monat)", "€ 24.500", "primary");
// Neue Kunden // Neue Kunden
Div newCustomersCard = createKpiCard("Neue Kunden", "12", "success"); 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) { private Div createKpiCard(String title, String value, String theme) {
Div card = new Div(); Div card = new Div();
card.addClassName("kpi-card"); card.addClassName("kpi-card");
card.getStyle() card.getStyle().set("background", "var(--lumo-base-color)")
.set("background", "var(--lumo-base-color)") .set("border", "1px solid var(--lumo-contrast-10pct)")
.set("border", "1px solid var(--lumo-contrast-10pct)") .set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)")
.set("border-radius", "var(--lumo-border-radius-m)") .set("text-align", "center").set("box-shadow", "var(--lumo-box-shadow-xs)").set("min-width", "150px");
.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); H3 titleElement = new H3(title);
titleElement.getStyle().set("margin", "0 0 var(--lumo-space-s) 0").set("font-size", "var(--lumo-font-size-s)"); titleElement.getStyle().set("margin", "0 0 var(--lumo-space-s) 0").set("font-size", "var(--lumo-font-size-s)");
Span valueElement = new Span(value); Span valueElement = new Span(value);
valueElement.getStyle() valueElement.getStyle().set("font-size", "var(--lumo-font-size-xl)").set("font-weight", "bold").set("color",
.set("font-size", "var(--lumo-font-size-xl)") getThemeColor(theme));
.set("font-weight", "bold")
.set("color", getThemeColor(theme));
card.add(titleElement, valueElement); card.add(titleElement, valueElement);
return card; return card;
@@ -113,197 +107,197 @@ public class StatisticsView extends VerticalLayout {
private String getThemeColor(String theme) { private String getThemeColor(String theme) {
return switch (theme) { return switch (theme) {
case "success" -> "var(--lumo-success-color)"; case "success" -> "var(--lumo-success-color)";
case "warning" -> "var(--lumo-warning-color)"; case "warning" -> "var(--lumo-warning-color)";
case "error" -> "var(--lumo-error-color)"; case "error" -> "var(--lumo-error-color)";
default -> "var(--lumo-primary-color)"; default -> "var(--lumo-primary-color)";
}; };
} }
private Div createMonthlyOrdersChart() { private Div createMonthlyOrdersChart() {
Div chartContainer = new Div(); Div chartContainer = new Div();
chartContainer.setId("monthlyOrdersChart"); chartContainer.setId("monthlyOrdersChart");
String canvasHtml = "<canvas id='monthlyOrdersCanvas' style='width: 100%; height: 100%;'></canvas>"; String canvasHtml = "<canvas id='monthlyOrdersCanvas' style='width: 100%; height: 100%;'></canvas>";
Html canvas = new Html(canvasHtml); Html canvas = new Html(canvasHtml);
chartContainer.add(canvas); chartContainer.add(canvas);
String script = """ String script = """
<script> <script>
setTimeout(function() { setTimeout(function() {
const ctx = document.getElementById('monthlyOrdersCanvas'); const ctx = document.getElementById('monthlyOrdersCanvas');
if (ctx) { if (ctx) {
new Chart(ctx, { new Chart(ctx, {
type: 'line', type: 'line',
data: { data: {
labels: ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'], labels: ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
datasets: [{ datasets: [{
label: '2024', label: '2024',
data: [15, 18, 22, 28, 32, 35, 42, 38, 41, 35, 28, 25], data: [15, 18, 22, 28, 32, 35, 42, 38, 41, 35, 28, 25],
borderColor: 'rgb(75, 192, 192)', borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.2)', backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.1 tension: 0.1
}, { }, {
label: '2023', label: '2023',
data: [12, 15, 18, 25, 28, 30, 35, 32, 36, 30, 25, 22], data: [12, 15, 18, 25, 28, 30, 35, 32, 36, 30, 25, 22],
borderColor: 'rgb(135, 206, 235)', borderColor: 'rgb(135, 206, 235)',
backgroundColor: 'rgba(135, 206, 235, 0.2)', backgroundColor: 'rgba(135, 206, 235, 0.2)',
tension: 0.1 tension: 0.1
}] }]
}, },
options: { options: {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
plugins: { plugins: {
title: { title: {
display: true, display: true,
text: 'Aufträge pro Monat' text: 'Aufträge pro Monat'
} }
}, },
scales: { scales: {
y: { y: {
beginAtZero: true, beginAtZero: true,
title: { title: {
display: true, display: true,
text: 'Anzahl Aufträge' text: 'Anzahl Aufträge'
}
}
} }
} }
} });
} }
}); }, 100);
} </script>
}, 100); """;
</script>
""";
Html scriptElement = new Html(script); Html scriptElement = new Html(script);
chartContainer.add(scriptElement); chartContainer.add(scriptElement);
return chartContainer; return chartContainer;
} }
private Div createStatusPieChart() { private Div createStatusPieChart() {
Div chartContainer = new Div(); Div chartContainer = new Div();
chartContainer.setId("statusPieChart"); chartContainer.setId("statusPieChart");
String canvasHtml = "<canvas id='statusPieCanvas' style='width: 100%; height: 100%;'></canvas>"; String canvasHtml = "<canvas id='statusPieCanvas' style='width: 100%; height: 100%;'></canvas>";
Html canvas = new Html(canvasHtml); Html canvas = new Html(canvasHtml);
chartContainer.add(canvas); chartContainer.add(canvas);
String script = """ String script = """
<script> <script>
setTimeout(function() { setTimeout(function() {
const ctx = document.getElementById('statusPieCanvas'); const ctx = document.getElementById('statusPieCanvas');
if (ctx) { if (ctx) {
new Chart(ctx, { new Chart(ctx, {
type: 'doughnut', type: 'doughnut',
data: { data: {
labels: ['Abgeschlossen', 'In Bearbeitung', 'Geplant', 'Storniert'], labels: ['Abgeschlossen', 'In Bearbeitung', 'Geplant', 'Storniert'],
datasets: [{ datasets: [{
data: [156, 34, 28, 12], data: [156, 34, 28, 12],
backgroundColor: [ backgroundColor: [
'rgba(54, 162, 235, 0.8)', 'rgba(54, 162, 235, 0.8)',
'rgba(255, 206, 86, 0.8)', 'rgba(255, 206, 86, 0.8)',
'rgba(75, 192, 192, 0.8)', 'rgba(75, 192, 192, 0.8)',
'rgba(255, 99, 132, 0.8)' 'rgba(255, 99, 132, 0.8)'
], ],
borderColor: [ borderColor: [
'rgba(54, 162, 235, 1)', 'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)', 'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)', 'rgba(75, 192, 192, 1)',
'rgba(255, 99, 132, 1)' 'rgba(255, 99, 132, 1)'
], ],
borderWidth: 1 borderWidth: 1
}] }]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Aufträge nach Status'
}, },
legend: { options: {
position: 'right' responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Aufträge nach Status'
},
legend: {
position: 'right'
}
}
} }
} });
} }
}); }, 100);
} </script>
}, 100); """;
</script>
""";
Html scriptElement = new Html(script); Html scriptElement = new Html(script);
chartContainer.add(scriptElement); chartContainer.add(scriptElement);
return chartContainer; return chartContainer;
} }
private Div createRevenueByCustomerChart() { private Div createRevenueByCustomerChart() {
Div chartContainer = new Div(); Div chartContainer = new Div();
chartContainer.setId("revenueByCustomerChart"); chartContainer.setId("revenueByCustomerChart");
String canvasHtml = "<canvas id='revenueByCustomerCanvas' style='width: 100%; height: 100%;'></canvas>"; String canvasHtml = "<canvas id='revenueByCustomerCanvas' style='width: 100%; height: 100%;'></canvas>";
Html canvas = new Html(canvasHtml); Html canvas = new Html(canvasHtml);
chartContainer.add(canvas); chartContainer.add(canvas);
String script = """ String script = """
<script> <script>
setTimeout(function() { setTimeout(function() {
const ctx = document.getElementById('revenueByCustomerCanvas'); const ctx = document.getElementById('revenueByCustomerCanvas');
if (ctx) { if (ctx) {
new Chart(ctx, { new Chart(ctx, {
type: 'bar', type: 'bar',
data: { data: {
labels: ['Firma A GmbH', 'Logistics B', 'Transport C', 'Spediteur D', 'Handel E', labels: ['Firma A GmbH', 'Logistics B', 'Transport C', 'Spediteur D', 'Handel E',
'Industrie F', 'Service G', 'Vertrieb H', 'Export I', 'Import J'], 'Industrie F', 'Service G', 'Vertrieb H', 'Export I', 'Import J'],
datasets: [{ datasets: [{
label: 'Umsatz (€)', label: 'Umsatz (€)',
data: [8500, 7200, 6800, 5900, 5400, 4800, 4200, 3900, 3500, 3100], data: [8500, 7200, 6800, 5900, 5400, 4800, 4200, 3900, 3500, 3100],
backgroundColor: 'rgba(75, 192, 192, 0.8)', backgroundColor: 'rgba(75, 192, 192, 0.8)',
borderColor: 'rgba(75, 192, 192, 1)', borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1 borderWidth: 1
}] }]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Top 10 Kunden nach Umsatz'
}, },
legend: { options: {
display: false responsive: true,
} maintainAspectRatio: false,
}, plugins: {
scales: { title: {
x: { display: true,
ticks: { text: 'Top 10 Kunden nach Umsatz'
maxRotation: 45, },
minRotation: 45 legend: {
} display: false
}, }
y: { },
beginAtZero: true, scales: {
title: { x: {
display: true, ticks: {
text: 'Umsatz (€)' maxRotation: 45,
minRotation: 45
}
},
y: {
beginAtZero: true,
title: {
display: true,
text: 'Umsatz (€)'
}
}
} }
} }
} });
} }
}); }, 100);
} </script>
}, 100); """;
</script>
""";
Html scriptElement = new Html(script); Html scriptElement = new Html(script);
chartContainer.add(scriptElement); chartContainer.add(scriptElement);
return chartContainer; return chartContainer;
} }
} }

View File

@@ -27,18 +27,18 @@ public class VerwaltungView extends Main {
// Content // Content
VerticalLayout content = new VerticalLayout(); VerticalLayout content = new VerticalLayout();
H1 title = new H1("Verwaltung"); H1 title = new H1("Verwaltung");
title.getStyle().set("color", "var(--lumo-primary-color)"); title.getStyle().set("color", "var(--lumo-primary-color)");
Paragraph description = new Paragraph("Willkommen im Verwaltungsbereich. Wählen Sie eine Option aus dem Menü."); Paragraph description = new Paragraph("Willkommen im Verwaltungsbereich. Wählen Sie eine Option aus dem Menü.");
description.getStyle().set("color", "var(--lumo-secondary-text-color)"); description.getStyle().set("color", "var(--lumo-secondary-text-color)");
content.add(title, description); content.add(title, description);
content.setDefaultHorizontalComponentAlignment(VerticalLayout.Alignment.CENTER); content.setDefaultHorizontalComponentAlignment(VerticalLayout.Alignment.CENTER);
content.setJustifyContentMode(VerticalLayout.JustifyContentMode.CENTER); content.setJustifyContentMode(VerticalLayout.JustifyContentMode.CENTER);
content.setSizeFull(); content.setSizeFull();
add(content); add(content);
} }
} }

View File

@@ -9,7 +9,7 @@ import java.util.List;
@Repository @Repository
public interface AppDeviceRepository extends MongoRepository<AppDevice, ObjectId> { public interface AppDeviceRepository extends MongoRepository<AppDevice, ObjectId> {
// Find all devices created by a specific user // Find all devices created by a specific user
List<AppDevice> findByErstelltVon(ObjectId erstelltVon); List<AppDevice> findByErstelltVon(ObjectId erstelltVon);

View File

@@ -9,10 +9,10 @@ import java.util.List;
@Repository @Repository
public interface AppUserRepository extends MongoRepository<AppUser, ObjectId> { public interface AppUserRepository extends MongoRepository<AppUser, ObjectId> {
// Find all AppUsers created by a specific user // Find all AppUsers created by a specific user
List<AppUser> findByErstelltVon(ObjectId erstelltVon); List<AppUser> findByErstelltVon(ObjectId erstelltVon);
// Find AppUser by email for login // Find AppUser by email for login
AppUser findByEmail(String email); AppUser findByEmail(String email);

View File

@@ -9,4 +9,3 @@ import java.util.List;
public interface CargoItemRepository extends MongoRepository<CargoItem, ObjectId> { public interface CargoItemRepository extends MongoRepository<CargoItem, ObjectId> {
List<CargoItem> findByJobId(ObjectId jobId); List<CargoItem> findByJobId(ObjectId jobId);
} }

View File

@@ -14,26 +14,27 @@ import java.util.List;
public interface JobHistoryRepository extends MongoRepository<JobHistory, ObjectId> { public interface JobHistoryRepository extends MongoRepository<JobHistory, ObjectId> {
/** /**
* 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<JobHistory> findByJobIdOrderByTimestampDesc(ObjectId jobId); List<JobHistory> 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<JobHistory> findByJobIdOrderByTimestampAsc(ObjectId jobId); List<JobHistory> findByJobIdOrderByTimestampAsc(ObjectId jobId);
/** /**
* Find history entries for a job within a specific time range * Find history entries for a job within a specific time range
*/ */
List<JobHistory> findByJobIdAndTimestampBetweenOrderByTimestampDesc( List<JobHistory> findByJobIdAndTimestampBetweenOrderByTimestampDesc(ObjectId jobId, LocalDateTime start,
ObjectId jobId, LocalDateTime start, LocalDateTime end); LocalDateTime end);
/** /**
* Find history entries by change type for a specific job * Find history entries by change type for a specific job
*/ */
List<JobHistory> findByJobIdAndChangeTypeOrderByTimestampDesc( List<JobHistory> findByJobIdAndChangeTypeOrderByTimestampDesc(ObjectId jobId, JobHistoryType changeType);
ObjectId jobId, JobHistoryType changeType);
/** /**
* Find history entries made by a specific user * Find history entries made by a specific user

View File

@@ -99,10 +99,8 @@ public interface JobRepository extends MongoRepository<Job, ObjectId> {
/** /**
* Erweiterte Suche: Zeitraum, Auftragsnummer und Status kombiniert * Erweiterte Suche: Zeitraum, Auftragsnummer und Status kombiniert
*/ */
@Query("{'createdAt': {'$gte': ?0, '$lte': ?1}, 'createdBy': ?2, " + @Query("{'createdAt': {'$gte': ?0, '$lte': ?1}, 'createdBy': ?2, "
"'jobNumber': {'$regex': ?3, '$options': 'i'}, " + + "'jobNumber': {'$regex': ?3, '$options': 'i'}, " + "'status': {'$in': ?4}}")
"'status': {'$in': ?4}}") List<Job> findWithFilters(LocalDateTime startDate, LocalDateTime endDate, String createdBy, String jobNumberPattern,
List<Job> findWithFilters(LocalDateTime startDate, LocalDateTime endDate, List<JobStatus> statusList);
String createdBy, String jobNumberPattern,
List<JobStatus> statusList);
} }

View File

@@ -8,21 +8,25 @@ import org.springframework.stereotype.Repository;
import java.util.List; import java.util.List;
/** /**
* Repository interface for Photo entities. * Repository interface for Photo entities. Provides database operations for the
* Provides database operations for the photos collection. * photos collection.
*/ */
@Repository @Repository
public interface PhotoRepository extends MongoRepository<Photo, ObjectId> { public interface PhotoRepository extends MongoRepository<Photo, ObjectId> {
/** /**
* Find all photos associated with a specific task ID. * 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 * @return List of photos for the task
*/ */
List<Photo> findByTaskId(ObjectId taskId); List<Photo> findByTaskId(ObjectId taskId);
/** /**
* Find photos by task ID as string. * 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 * @return List of photos for the task
*/ */
default List<Photo> findByTaskId(String taskId) { default List<Photo> findByTaskId(String taskId) {

View File

@@ -15,4 +15,3 @@ public interface TaskRepository extends MongoRepository<BaseTask, ObjectId> {
return findByJobIdOrderByTaskOrderAsc(jobId); return findByJobIdOrderByTaskOrderAsc(jobId);
} }
} }

View File

@@ -9,11 +9,11 @@ import java.util.Optional;
@Repository @Repository
public interface UserRepository extends MongoRepository<User, ObjectId> { public interface UserRepository extends MongoRepository<User, ObjectId> {
Optional<User> findByEmail(String email); Optional<User> findByEmail(String email);
boolean existsByEmail(String email); boolean existsByEmail(String email);
void deleteByEmail(String email); void deleteByEmail(String email);
Optional<User> findByPasswordCode(String passwordCode); Optional<User> findByPasswordCode(String passwordCode);

View File

@@ -11,61 +11,60 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* Custom UserDetails implementation that holds a reference to the MongoDB User entity. * Custom UserDetails implementation that holds a reference to the MongoDB User
* This allows access to the complete User object from the session without additional database queries. * entity. This allows access to the complete User object from the session
* without additional database queries.
*/ */
public class CustomUserPrincipal implements UserDetails { public class CustomUserPrincipal implements UserDetails {
private final User user; // MongoDB User entity private final User user; // MongoDB User entity
public CustomUserPrincipal(User user) { public CustomUserPrincipal(User user) {
this.user = user; this.user = user;
} }
/** /**
* Get the complete MongoDB User entity * Get the complete MongoDB User entity
*/ */
public User getUser() { public User getUser() {
return user; return user;
} }
@Override @Override
public String getUsername() { public String getUsername() {
return user.getEmail(); return user.getEmail();
} }
@Override @Override
public String getPassword() { public String getPassword() {
return user.getPassword(); return user.getPassword();
} }
@Override @Override
public boolean isEnabled() { public boolean isEnabled() {
return user.getIsActivated() == 1; return user.getIsActivated() == 1;
} }
@Override @Override
public Collection<? extends GrantedAuthority> getAuthorities() { public Collection<? extends GrantedAuthority> getAuthorities() {
Set<String> roles = user.getRoles(); Set<String> roles = user.getRoles();
if (roles != null && !roles.isEmpty()) { if (roles != null && !roles.isEmpty()) {
return roles.stream() return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect(Collectors.toList());
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toList());
} }
// Default role if no roles are set // Default role if no roles are set
return Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")); return Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"));
} }
@Override @Override
public boolean isAccountNonExpired() { public boolean isAccountNonExpired() {
return true; return true;
} }
@Override @Override
public boolean isAccountNonLocked() { public boolean isAccountNonLocked() {
return true; return true;
} }
@Override @Override
public boolean isCredentialsNonExpired() { public boolean isCredentialsNonExpired() {
return true; return true;

View File

@@ -24,33 +24,20 @@ public class SecurityConfig extends VaadinWebSecurity {
protected void configure(HttpSecurity http) throws Exception { protected void configure(HttpSecurity http) throws Exception {
// Konfiguriere zusätzliche öffentliche Endpunkte vor der Basis-Konfiguration // Konfiguriere zusätzliche öffentliche Endpunkte vor der Basis-Konfiguration
http.authorizeHttpRequests(auth -> auth http.authorizeHttpRequests(auth -> auth
// Öffentliche Endpunkte // Öffentliche Endpunkte
.requestMatchers( .requestMatchers(new AntPathRequestMatcher("/"), new AntPathRequestMatcher("/register"),
new AntPathRequestMatcher("/"), new AntPathRequestMatcher("/login"), new AntPathRequestMatcher("/forget-password"),
new AntPathRequestMatcher("/register"), new AntPathRequestMatcher("/forgot-password-request"), new AntPathRequestMatcher("/images/**"),
new AntPathRequestMatcher("/login"), new AntPathRequestMatcher("/icons/**"), new AntPathRequestMatcher("/favicon.ico"),
new AntPathRequestMatcher("/forget-password"), new AntPathRequestMatcher("/robots.txt"), new AntPathRequestMatcher("/manifest.webmanifest"),
new AntPathRequestMatcher("/forgot-password-request"), new AntPathRequestMatcher("/sw.js"), new AntPathRequestMatcher("/offline.html"),
new AntPathRequestMatcher("/images/**"), new AntPathRequestMatcher("/frontend/**"), new AntPathRequestMatcher("/webjars/**"),
new AntPathRequestMatcher("/icons/**"), new AntPathRequestMatcher("/h2-console/**"),
new AntPathRequestMatcher("/favicon.ico"), new AntPathRequestMatcher("/frontend-es5/**", "/frontend-es6/**"))
new AntPathRequestMatcher("/robots.txt"), .permitAll());
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 // Standard-CSRF-Konfiguration
http.csrf(csrf -> csrf http.csrf(csrf -> csrf.ignoringRequestMatchers(new AntPathRequestMatcher("/h2-console/**")));
.ignoringRequestMatchers(
new AntPathRequestMatcher("/h2-console/**")
)
);
// Delegiere die Basis-Konfiguration an VaadinWebSecurity // Delegiere die Basis-Konfiguration an VaadinWebSecurity
// Dies fügt automatisch .anyRequest().authenticated() hinzu // Dies fügt automatisch .anyRequest().authenticated() hinzu
@@ -60,12 +47,8 @@ public class SecurityConfig extends VaadinWebSecurity {
setLoginView(http, "/login"); setLoginView(http, "/login");
// Logout-Konfiguration // Logout-Konfiguration
http.logout(logout -> logout http.logout(logout -> logout.logoutUrl("/logout").logoutSuccessUrl("/").invalidateHttpSession(true)
.logoutUrl("/logout") .deleteCookies("JSESSIONID"));
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
);
} }
@Bean @Bean

View File

@@ -25,7 +25,8 @@ public class SecurityService {
} }
public boolean isUserLoggedIn() { public boolean isUserLoggedIn() {
if (authenticationContext.isAuthenticated()) return true; if (authenticationContext.isAuthenticated())
return true;
Authentication auth = SecurityContextHolder.getContext().getAuthentication(); Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return auth != null && auth.isAuthenticated() && !(auth instanceof AnonymousAuthenticationToken); return auth != null && auth.isAuthenticated() && !(auth instanceof AnonymousAuthenticationToken);
} }
@@ -39,8 +40,10 @@ public class SecurityService {
de.assecutor.votianlt.model.User u = cup.getUser(); de.assecutor.votianlt.model.User u = cup.getUser();
if (u != null) { if (u != null) {
String namePart = (nullToEmpty(u.getFirstname()) + " " + nullToEmpty(u.getName())).trim(); String namePart = (nullToEmpty(u.getFirstname()) + " " + nullToEmpty(u.getName())).trim();
if (!namePart.isBlank()) return namePart; if (!namePart.isBlank())
if (u.getEmail() != null && !u.getEmail().isBlank()) return u.getEmail(); return namePart;
if (u.getEmail() != null && !u.getEmail().isBlank())
return u.getEmail();
} }
return cup.getUsername(); return cup.getUsername();
} }
@@ -53,19 +56,18 @@ public class SecurityService {
} }
// 2) Fallback: Vaadin AuthenticationContext // 2) Fallback: Vaadin AuthenticationContext
return getAuthenticatedUser() return getAuthenticatedUser().map(UserDetails::getUsername).orElse("Anonymous");
.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 * Get the complete MongoDB User entity from the session
*/ */
public de.assecutor.votianlt.model.User getCurrentDatabaseUser() { public de.assecutor.votianlt.model.User getCurrentDatabaseUser() {
return getAuthenticatedUser() return getAuthenticatedUser().filter(userDetails -> userDetails instanceof CustomUserPrincipal)
.filter(userDetails -> userDetails instanceof CustomUserPrincipal)
.map(userDetails -> ((CustomUserPrincipal) userDetails).getUser()) .map(userDetails -> ((CustomUserPrincipal) userDetails).getUser())
.orElseThrow(() -> new RuntimeException("No user logged in")); .orElseThrow(() -> new RuntimeException("No user logged in"));
} }
@@ -101,9 +103,7 @@ public class SecurityService {
} }
public boolean hasRole(String role) { public boolean hasRole(String role) {
return getAuthenticatedUser() return getAuthenticatedUser().map(user -> user.getAuthorities().stream()
.map(user -> user.getAuthorities().stream() .anyMatch(authority -> authority.getAuthority().equals("ROLE_" + role))).orElse(false);
.anyMatch(authority -> authority.getAuthority().equals("ROLE_" + role)))
.orElse(false);
} }
} }

View File

@@ -25,5 +25,4 @@ public class UserDetailsServiceImpl implements UserDetailsService {
return new CustomUserPrincipal(user); return new CustomUserPrincipal(user);
} }
} }

View File

@@ -2,7 +2,7 @@ package de.assecutor.votianlt.security.totp;
import de.assecutor.votianlt.model.User; import de.assecutor.votianlt.model.User;
import de.assecutor.votianlt.repository.UserRepository; import de.assecutor.votianlt.repository.UserRepository;
import de.assecutor.votianlt.util.MailUtil; import de.assecutor.votianlt.service.EmailService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.security.SecureRandom; import java.security.SecureRandom;
@@ -13,35 +13,42 @@ import java.util.Optional;
public class TwoFactorService { public class TwoFactorService {
private final UserRepository userRepository; private final UserRepository userRepository;
private final MailUtil mailUtil; private final EmailService emailService;
private final SecureRandom random = new SecureRandom(); private final SecureRandom random = new SecureRandom();
public TwoFactorService(UserRepository userRepository, MailUtil mailUtil) { public TwoFactorService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository; this.userRepository = userRepository;
this.mailUtil = mailUtil; this.emailService = emailService;
} }
public void initiateTwoFactorFor(String email) { public void initiateTwoFactorFor(String email) {
Optional<User> userOpt = userRepository.findByEmail(email); Optional<User> userOpt = userRepository.findByEmail(email);
if (userOpt.isEmpty()) return; if (userOpt.isEmpty())
return;
User user = userOpt.get(); User user = userOpt.get();
String code = generateSixDigitCode(); String code = generateSixDigitCode();
user.setPasswordCode(code); user.setPasswordCode(code);
user.setPasswordTimestamp(LocalDateTime.now()); user.setPasswordTimestamp(LocalDateTime.now());
userRepository.save(user); userRepository.save(user);
try { try {
mailUtil.sendMail(email, "Ihr Anmeldecode (2FA)", "Ihr 2FA-Code lautet: " + code + "\nGültig für 10 Minuten."); emailService.sendSimpleEmail(email, "Ihr Anmeldecode (2FA)",
} catch (Exception ignored) { } "Ihr 2FA-Code lautet: " + code + "\nGültig für 10 Minuten.");
} catch (Exception ignored) {
}
} }
public boolean verifyTwoFactorCode(String email, String code) { public boolean verifyTwoFactorCode(String email, String code) {
Optional<User> userOpt = userRepository.findByEmail(email); Optional<User> userOpt = userRepository.findByEmail(email);
if (userOpt.isEmpty()) return false; if (userOpt.isEmpty())
return false;
User user = userOpt.get(); User user = userOpt.get();
if (user.getPasswordCode() == null || !user.getPasswordCode().equals(code)) return false; if (user.getPasswordCode() == null || !user.getPasswordCode().equals(code))
if (user.getPasswordTimestamp() == null) return false; return false;
if (user.getPasswordTimestamp() == null)
return false;
// Gültigkeit 10 Minuten // 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 // Code verbrauchen
user.setPasswordCode(null); user.setPasswordCode(null);
user.setPasswordTimestamp(null); user.setPasswordTimestamp(null);
@@ -54,5 +61,3 @@ public class TwoFactorService {
return String.format("%06d", n); return String.format("%06d", n);
} }
} }

View File

@@ -71,10 +71,12 @@ public class EmailService {
// Send email // Send email
sendEmail(user, job, taskType, taskId, appUser); 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) { } 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(); SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(smtpUsername); message.setFrom(smtpUsername);
message.setTo(user.getEmail()); 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 fullName = buildFullName(user);
String appUserName = buildAppUserName(appUser); String appUserName = buildAppUserName(appUser);
@@ -150,18 +153,37 @@ public class EmailService {
} }
private String getTaskTypeDisplayName(String taskType) { private String getTaskTypeDisplayName(String taskType) {
if (taskType == null) return "Unbekannte Aufgabe"; if (taskType == null)
return "Unbekannte Aufgabe";
return switch (taskType.toUpperCase()) { return switch (taskType.toUpperCase()) {
case "PHOTO" -> "Foto-Aufgabe"; case "PHOTO" -> "Foto-Aufgabe";
case "SIGNATURE" -> "Unterschrift"; case "SIGNATURE" -> "Unterschrift";
case "BARCODE" -> "Barcode scannen"; case "BARCODE" -> "Barcode scannen";
case "CONFIRMATION" -> "Bestätigung"; case "CONFIRMATION" -> "Bestätigung";
case "TODO_LIST" -> "Checkliste"; case "TODO_LIST" -> "Checkliste";
default -> taskType; 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) { public void checkAndSendJobCompletionNotification(ObjectId jobId, String completedBy) {
try { try {
// Check if all tasks for this job are completed // Check if all tasks for this job are completed
@@ -174,7 +196,8 @@ public class EmailService {
boolean allCompleted = allTasks.stream().allMatch(task -> task.isCompleted()); boolean allCompleted = allTasks.stream().allMatch(task -> task.isCompleted());
if (allCompleted) { 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 // Update job status to COMPLETED
updateJobStatusToCompleted(jobId); updateJobStatusToCompleted(jobId);
@@ -240,7 +263,8 @@ public class EmailService {
SimpleMailMessage message = new SimpleMailMessage(); SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(smtpUsername); message.setFrom(smtpUsername);
message.setTo(user.getEmail()); 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 fullName = buildFullName(user);
String appUserName = buildAppUserName(appUser); String appUserName = buildAppUserName(appUser);
@@ -299,8 +323,8 @@ public class EmailService {
job.setUpdatedAt(java.time.LocalDateTime.now()); job.setUpdatedAt(java.time.LocalDateTime.now());
jobRepository.save(job); jobRepository.save(job);
log.info("Job status updated from {} to COMPLETED for job {}", log.info("Job status updated from {} to COMPLETED for job {}", oldStatus != null ? oldStatus : "null",
oldStatus != null ? oldStatus : "null", job.getJobNumber()); job.getJobNumber());
} else { } else {
log.debug("Job {} already has COMPLETED status", job.getJobNumber()); log.debug("Job {} already has COMPLETED status", job.getJobNumber());
} }
@@ -354,7 +378,8 @@ public class EmailService {
SimpleMailMessage message = new SimpleMailMessage(); SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(smtpUsername); message.setFrom(smtpUsername);
message.setTo(user.getEmail()); 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); String fullName = buildFullName(user);
@@ -389,14 +414,18 @@ public class EmailService {
body.append("Anzahl Aufgaben: ").append(taskCount).append("\n"); 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()) { if (job.getRemark() != null && !job.getRemark().isBlank()) {
body.append("Bemerkung: ").append(job.getRemark()).append("\n"); body.append("Bemerkung: ").append(job.getRemark()).append("\n");
} }
body.append("Erstellt am: ").append(job.getCreatedAt() != null ? body.append("Erstellt am: ")
job.getCreatedAt().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")) : "Unbekannt").append("\n\n"); .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("Der Job ist nun im System verfügbar und kann bearbeitet werden.\n\n");
body.append("Mit freundlichen Grüßen,\n"); body.append("Mit freundlichen Grüßen,\n");
@@ -405,4 +434,24 @@ public class EmailService {
message.setText(body.toString()); message.setText(body.toString());
mailSender.send(message); 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);
}
}
} }

View File

@@ -29,15 +29,9 @@ public class JobHistoryService {
*/ */
public void logJobCreation(Job job, String createdBy) { public void logJobCreation(Job job, String createdBy) {
try { try {
JobHistory history = new JobHistory( JobHistory history = new JobHistory(job.getId(), "Job erstellt",
job.getId(), "Neuer Job wurde erstellt: " + (job.getJobNumber() != null ? job.getJobNumber() : "Ohne Nummer"),
"Job erstellt", createdBy, JobHistoryType.CREATE, null, "Job erstellt");
"Neuer Job wurde erstellt: " + (job.getJobNumber() != null ? job.getJobNumber() : "Ohne Nummer"),
createdBy,
JobHistoryType.CREATE,
null,
"Job erstellt"
);
if (job.getDeliveryCompany() != null) { if (job.getDeliveryCompany() != null) {
history.setDetails("Kunde: " + job.getDeliveryCompany()); history.setDetails("Kunde: " + job.getDeliveryCompany());
@@ -55,19 +49,12 @@ public class JobHistoryService {
*/ */
public void logStatusChange(Job job, JobStatus oldStatus, JobStatus newStatus, String changedBy) { public void logStatusChange(Job job, JobStatus oldStatus, JobStatus newStatus, String changedBy) {
try { try {
String description = String.format("Status geändert von %s zu %s", String description = String.format("Status geändert von %s zu %s", formatStatus(oldStatus),
formatStatus(oldStatus), formatStatus(newStatus));
formatStatus(newStatus));
JobHistory history = new JobHistory( JobHistory history = new JobHistory(job.getId(), "Status-Änderung", description, changedBy,
job.getId(), JobHistoryType.STATUS_CHANGE, oldStatus != null ? oldStatus.toString() : null,
"Status-Änderung", newStatus != null ? newStatus.toString() : null);
description,
changedBy,
JobHistoryType.STATUS_CHANGE,
oldStatus != null ? oldStatus.toString() : null,
newStatus != null ? newStatus.toString() : null
);
jobHistoryRepository.save(history); jobHistoryRepository.save(history);
log.debug("Status change logged for job {}: {} -> {}", job.getIdAsString(), oldStatus, newStatus); log.debug("Status change logged for job {}: {} -> {}", job.getIdAsString(), oldStatus, newStatus);
@@ -83,15 +70,9 @@ public class JobHistoryService {
try { try {
String description = generateUpdateDescription(oldJob, newJob); String description = generateUpdateDescription(oldJob, newJob);
JobHistory history = new JobHistory( JobHistory history = new JobHistory(newJob.getId(), reason != null ? reason : "Job aktualisiert",
newJob.getId(), description, changedBy, JobHistoryType.UPDATE, serializeJobForComparison(oldJob),
reason != null ? reason : "Job aktualisiert", serializeJobForComparison(newJob));
description,
changedBy,
JobHistoryType.UPDATE,
serializeJobForComparison(oldJob),
serializeJobForComparison(newJob)
);
jobHistoryRepository.save(history); jobHistoryRepository.save(history);
log.debug("Job update logged for job {}", newJob.getIdAsString()); log.debug("Job update logged for job {}", newJob.getIdAsString());
@@ -111,7 +92,7 @@ public class JobHistoryService {
* Log task completion with detailed information and extraData * Log task completion with detailed information and extraData
*/ */
public void logTaskCompletion(ObjectId jobId, String taskType, String taskId, String completedBy, public void logTaskCompletion(ObjectId jobId, String taskType, String taskId, String completedBy,
String taskDisplayName, String extraDataSummary) { String taskDisplayName, String extraDataSummary) {
try { try {
String taskName = taskDisplayName != null ? taskDisplayName : taskType; String taskName = taskDisplayName != null ? taskDisplayName : taskType;
String description = String.format("Aufgabe abgeschlossen: %s", taskName); String description = String.format("Aufgabe abgeschlossen: %s", taskName);
@@ -120,15 +101,8 @@ public class JobHistoryService {
description += " - " + extraDataSummary; description += " - " + extraDataSummary;
} }
JobHistory history = new JobHistory( JobHistory history = new JobHistory(jobId, "Aufgabe abgeschlossen", description, completedBy,
jobId, JobHistoryType.TASK_COMPLETED, "In Bearbeitung", "Abgeschlossen");
"Aufgabe abgeschlossen",
description,
completedBy,
JobHistoryType.TASK_COMPLETED,
"In Bearbeitung",
"Abgeschlossen"
);
// Detaillierte Informationen in details speichern // Detaillierte Informationen in details speichern
StringBuilder details = new StringBuilder(); StringBuilder details = new StringBuilder();
@@ -165,15 +139,8 @@ public class JobHistoryService {
description = String.format("Job-Zuweisung geändert von %s zu %s", oldAssignee, newAssignee); description = String.format("Job-Zuweisung geändert von %s zu %s", oldAssignee, newAssignee);
} }
JobHistory history = new JobHistory( JobHistory history = new JobHistory(job.getId(), "Zuweisung geändert", description, changedBy,
job.getId(), JobHistoryType.ASSIGNMENT, oldAssignee, newAssignee);
"Zuweisung geändert",
description,
changedBy,
JobHistoryType.ASSIGNMENT,
oldAssignee,
newAssignee
);
jobHistoryRepository.save(history); jobHistoryRepository.save(history);
log.debug("Job assignment logged for job {}", job.getIdAsString()); log.debug("Job assignment logged for job {}", job.getIdAsString());
@@ -186,7 +153,7 @@ public class JobHistoryService {
* Log custom event * Log custom event
*/ */
public void logCustomEvent(ObjectId jobId, String reason, String description, String changedBy, public void logCustomEvent(ObjectId jobId, String reason, String description, String changedBy,
JobHistoryType type) { JobHistoryType type) {
try { try {
JobHistory history = new JobHistory(jobId, reason, description, changedBy, type, null, null); JobHistory history = new JobHistory(jobId, reason, description, changedBy, type, null, null);
jobHistoryRepository.save(history); jobHistoryRepository.save(history);
@@ -213,18 +180,19 @@ public class JobHistoryService {
// Helper methods // Helper methods
private String formatStatus(JobStatus status) { private String formatStatus(JobStatus status) {
if (status == null) return "Unbekannt"; if (status == null)
return "Unbekannt";
return switch (status) { return switch (status) {
case CREATED -> "Erstellt"; case CREATED -> "Erstellt";
case IN_PROGRESS -> "In Bearbeitung"; case IN_PROGRESS -> "In Bearbeitung";
case PICKUP_SCHEDULED -> "Abholung geplant"; case PICKUP_SCHEDULED -> "Abholung geplant";
case PICKED_UP -> "Abgeholt"; case PICKED_UP -> "Abgeholt";
case IN_TRANSIT -> "Unterwegs"; case IN_TRANSIT -> "Unterwegs";
case DELIVERED -> "Zugestellt"; case DELIVERED -> "Zugestellt";
case COMPLETED -> "Abgeschlossen"; case COMPLETED -> "Abgeschlossen";
case CANCELLED -> "Storniert"; case CANCELLED -> "Storniert";
default -> status.toString(); default -> status.toString();
}; };
} }
@@ -243,15 +211,17 @@ public class JobHistoryService {
hasChanges = true; hasChanges = true;
} }
if (!equals(oldJob.getPickupCity(), newJob.getPickupCity()) || if (!equals(oldJob.getPickupCity(), newJob.getPickupCity())
!equals(oldJob.getDeliveryCity(), newJob.getDeliveryCity())) { || !equals(oldJob.getDeliveryCity(), newJob.getDeliveryCity())) {
if (hasChanges) description.append(","); if (hasChanges)
description.append(",");
description.append(" - Orte"); description.append(" - Orte");
hasChanges = true; hasChanges = true;
} }
if (!equals(oldJob.getRemark(), newJob.getRemark())) { if (!equals(oldJob.getRemark(), newJob.getRemark())) {
if (hasChanges) description.append(","); if (hasChanges)
description.append(",");
description.append(" - Bemerkung"); description.append(" - Bemerkung");
hasChanges = true; hasChanges = true;
} }

View File

@@ -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);
}
}

View File

@@ -5,9 +5,8 @@ import com.vaadin.flow.component.applayout.AppLayout;
public class Util { public class Util {
public static void changeDrawerState(boolean drawerState) { public static void changeDrawerState(boolean drawerState) {
AppLayout appLayout = (AppLayout) UI.getCurrent().getChildren() AppLayout appLayout = (AppLayout) UI.getCurrent().getChildren().filter(AppLayout.class::isInstance).findFirst()
.filter(AppLayout.class::isInstance) .orElse(null);
.findFirst().orElse(null);
if (appLayout != null) { if (appLayout != null) {
appLayout.setDrawerOpened(drawerState); appLayout.setDrawerOpened(drawerState);

View File

@@ -1,2 +1,3 @@
// Zeroconf removed from project. // 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.