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

@@ -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")) {

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,15 +112,16 @@ 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);
@@ -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); return new JobWithRelatedDataDTO(job, cargoItems, tasks);
}) }).toList();
.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();
@@ -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

@@ -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);
@@ -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
@@ -38,7 +39,8 @@ public final class MainLayout extends AppLayout {
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);
@@ -155,8 +161,7 @@ public final class MainLayout extends AppLayout {
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());

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());
@@ -78,8 +80,7 @@ public class AddJobService {
// 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();
@@ -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

@@ -62,7 +62,8 @@ 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);
@@ -76,8 +77,11 @@ public class AppUserService {
/** /**
* 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();
@@ -90,32 +92,36 @@ public class PasswordResetService {
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())
return false;
User user = optional.get(); User user = optional.get();
user.setPassword(passwordEncoder.encode(newPassword)); user.setPassword(passwordEncoder.encode(newPassword));
user.setPasswordCode(null); user.setPasswordCode(null);
@@ -125,7 +131,8 @@ public class PasswordResetService {
} }
case APP_USER -> { case APP_USER -> {
AppUser appUser = appUserRepository.findByPasswordCode(token); AppUser appUser = appUserRepository.findByPasswordCode(token);
if (appUser == null) return false; if (appUser == null)
return false;
appUser.setPassword(passwordEncoder.encode(newPassword)); appUser.setPassword(passwordEncoder.encode(newPassword));
appUser.setPasswordCode(null); appUser.setPasswordCode(null);
appUser.setPasswordTimestamp(null); appUser.setPasswordTimestamp(null);

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,7 +19,7 @@ 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;
@@ -98,9 +98,8 @@ public class AddAppDeviceView extends VerticalLayout {
} }
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() {
@@ -118,13 +117,15 @@ public class AddAppDeviceView extends VerticalLayout {
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);

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;
@@ -84,9 +84,7 @@ public class AddAppUserView extends VerticalLayout {
// 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)");
@@ -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( .bind(appUser -> "", // Dummy getter - this field is not stored
appUser -> "", // Dummy getter - this field is not stored (appUser, value) -> {
(appUser, value) -> {} // Dummy setter - this field is not stored } // Dummy setter - this field is not stored
);
binder.forField(deviceComboBox)
.asRequired("Bitte ein Gerät auswählen")
.bind(
appUser -> null, // Initialwert, wird beim Erstellen gesetzt
(appUser, appDevice) -> appUser.setAppDeviceId(appDevice != null ? appDevice.getId() : null)
); );
binder.forField(deviceComboBox).asRequired("Bitte ein Gerät auswählen").bind(appUser -> null, // Initialwert,
// wird beim
// Erstellen
// gesetzt
(appUser, appDevice) -> appUser.setAppDeviceId(appDevice != null ? appDevice.getId() : null));
} }
private void createAppUser() { private void createAppUser() {
@@ -191,21 +186,13 @@ 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
);
} }
} }
@@ -233,7 +220,8 @@ public class AddAppUserView extends VerticalLayout {
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,19 +207,16 @@ 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

@@ -135,7 +135,8 @@ public class AddJobView extends Main {
// Available app users for the current user // Available app users for the current user
private List<AppUser> availableAppUsers; private List<AppUser> availableAppUsers;
public AddJobView(AddJobService addJobService, AddCustomerService addCustomerService, CustomerService customerService, AppUserService appUserService) { public AddJobView(AddJobService addJobService, AddCustomerService addCustomerService,
CustomerService customerService, AppUserService appUserService) {
this.addJobService = addJobService; this.addJobService = addJobService;
this.addCustomerService = addCustomerService; this.addCustomerService = addCustomerService;
this.customerService = customerService; this.customerService = customerService;
@@ -157,9 +158,11 @@ public class AddJobView extends Main {
customerLabelToEntity.clear(); customerLabelToEntity.clear();
for (Customer c : ownerCustomers) { for (Customer c : ownerCustomers) {
String label = (c.getCompanyName() != null && !c.getCompanyName().isBlank()) String label = (c.getCompanyName() != null && !c.getCompanyName().isBlank())
? c.getCompanyName() + " | " + ? c.getCompanyName() + " | "
((c.getFirstname() != null ? c.getFirstname() : "") + " " + (c.getLastName() != null ? c.getLastName() : "")).trim() + ((c.getFirstname() != null ? c.getFirstname() : "") + " "
: ((c.getFirstname() != null ? c.getFirstname() : "") + " " + (c.getLastName() != null ? c.getLastName() : "")).trim(); + (c.getLastName() != null ? c.getLastName() : "")).trim()
: ((c.getFirstname() != null ? c.getFirstname() : "") + " "
+ (c.getLastName() != null ? c.getLastName() : "")).trim();
if (label.isBlank()) { if (label.isBlank()) {
label = "Unbenannter Kunde"; label = "Unbenannter Kunde";
} }
@@ -182,30 +185,68 @@ public class AddJobView extends Main {
return; return;
} }
Customer c = customerLabelToEntity.get(selected); Customer c = customerLabelToEntity.get(selected);
if (c == null) return; if (c == null)
return;
// Pickup-Checkbox deaktivieren, da Kunde bereits existiert // Pickup-Checkbox deaktivieren, da Kunde bereits existiert
savePickupAddress.setValue(false); savePickupAddress.setValue(false);
// Firma // Firma
if (c.getCompanyName() != null) { pickupCompany.setValue(c.getCompanyName()); } else { pickupCompany.clear(); } if (c.getCompanyName() != null) {
pickupCompany.setValue(c.getCompanyName());
} else {
pickupCompany.clear();
}
// Anrede (nur setzen, wenn vorhanden und zulässig) // Anrede (nur setzen, wenn vorhanden und zulässig)
if (c.getTitle() != null && ("Herr".equalsIgnoreCase(c.getTitle()) || "Frau".equalsIgnoreCase(c.getTitle()) || "Divers".equalsIgnoreCase(c.getTitle()))) { if (c.getTitle() != null && ("Herr".equalsIgnoreCase(c.getTitle()) || "Frau".equalsIgnoreCase(c.getTitle())
|| "Divers".equalsIgnoreCase(c.getTitle()))) {
pickupSalutation.setValue(c.getTitle()); pickupSalutation.setValue(c.getTitle());
} else { } else {
pickupSalutation.clear(); pickupSalutation.clear();
} }
// Namen // Namen
if (c.getFirstname() != null) { pickupFirstName.setValue(c.getFirstname()); } else { pickupFirstName.clear(); } if (c.getFirstname() != null) {
if (c.getLastName() != null) { pickupLastName.setValue(c.getLastName()); } else { pickupLastName.clear(); } pickupFirstName.setValue(c.getFirstname());
} else {
pickupFirstName.clear();
}
if (c.getLastName() != null) {
pickupLastName.setValue(c.getLastName());
} else {
pickupLastName.clear();
}
// Telefon // Telefon
if (c.getTelephone() != null) { pickupPhone.setValue(c.getTelephone()); } else { pickupPhone.clear(); } if (c.getTelephone() != null) {
pickupPhone.setValue(c.getTelephone());
} else {
pickupPhone.clear();
}
// Adresse // Adresse
if (c.getStreet() != null) { pickupStreet.setValue(c.getStreet()); } else { pickupStreet.clear(); } if (c.getStreet() != null) {
if (c.getHouseNumber() != null) { pickupHouseNumber.setValue(c.getHouseNumber()); } else { pickupHouseNumber.clear(); } pickupStreet.setValue(c.getStreet());
if (c.getAddressAddition() != null) { pickupAddressAddition.setValue(c.getAddressAddition()); } else { pickupAddressAddition.clear(); } } else {
if (c.getZip() != null) { pickupZip.setValue(c.getZip()); } else { pickupZip.clear(); } pickupStreet.clear();
if (c.getCity() != null) { pickupCity.setValue(c.getCity()); } else { pickupCity.clear(); } }
if (c.getHouseNumber() != null) {
pickupHouseNumber.setValue(c.getHouseNumber());
} else {
pickupHouseNumber.clear();
}
if (c.getAddressAddition() != null) {
pickupAddressAddition.setValue(c.getAddressAddition());
} else {
pickupAddressAddition.clear();
}
if (c.getZip() != null) {
pickupZip.setValue(c.getZip());
} else {
pickupZip.clear();
}
if (c.getCity() != null) {
pickupCity.setValue(c.getCity());
} else {
pickupCity.clear();
}
}); });
preloadAddressButton = new Button("Vorbelegte Adressfelder leeren"); preloadAddressButton = new Button("Vorbelegte Adressfelder leeren");
@@ -286,7 +327,8 @@ public class AddJobView extends Main {
// Load app users for current user and set up the ComboBox // Load app users for current user and set up the ComboBox
availableAppUsers = appUserService.findByCurrentUser(); availableAppUsers = appUserService.findByCurrentUser();
appUser.setItems(availableAppUsers); appUser.setItems(availableAppUsers);
appUser.setItemLabelGenerator(user -> user.getVorname() + " " + user.getNachname() + " (" + user.getEmail() + ")"); appUser.setItemLabelGenerator(
user -> user.getVorname() + " " + user.getNachname() + " (" + user.getEmail() + ")");
appUser.setPlaceholder("App-Nutzer auswählen..."); appUser.setPlaceholder("App-Nutzer auswählen...");
// Price field // Price field
@@ -299,7 +341,8 @@ public class AddJobView extends Main {
String v = e.getValue(); String v = e.getValue();
if (v != null && v.contains(".")) { if (v != null && v.contains(".")) {
String replaced = v.replace('.', ','); String replaced = v.replace('.', ',');
if (!replaced.equals(v)) price.setValue(replaced); if (!replaced.equals(v))
price.setValue(replaced);
} }
}); });
// Date picker fields for appointments // Date picker fields for appointments
@@ -317,9 +360,8 @@ public class AddJobView extends Main {
private void setupLayout() { private void setupLayout() {
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("Neuen Auftrag anlegen")); add(new ViewToolbar("Neuen Auftrag anlegen"));
@@ -507,8 +549,6 @@ public class AddJobView extends Main {
H3 title = new H3("Abholadresse"); H3 title = new H3("Abholadresse");
title.getStyle().set("margin", "0"); title.getStyle().set("margin", "0");
HorizontalLayout titleLayout = new HorizontalLayout(); HorizontalLayout titleLayout = new HorizontalLayout();
titleLayout.setWidthFull(); titleLayout.setWidthFull();
titleLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.START); titleLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.START);
@@ -569,8 +609,6 @@ public class AddJobView extends Main {
H3 title = new H3("Lieferadresse"); H3 title = new H3("Lieferadresse");
title.getStyle().set("margin", "0"); title.getStyle().set("margin", "0");
HorizontalLayout titleLayout = new HorizontalLayout(); HorizontalLayout titleLayout = new HorizontalLayout();
titleLayout.setWidthFull(); titleLayout.setWidthFull();
titleLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.START); titleLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.START);
@@ -621,12 +659,8 @@ public class AddJobView extends Main {
List<Customer> allCustomers = customerService.findAllForCurrentOwner(); List<Customer> allCustomers = customerService.findAllForCurrentOwner();
// Extract unique company names (filter out null/empty values) // Extract unique company names (filter out null/empty values)
List<String> companyNames = allCustomers.stream() List<String> companyNames = allCustomers.stream().map(Customer::getCompanyName)
.map(Customer::getCompanyName) .filter(name -> name != null && !name.trim().isEmpty()).distinct().sorted().toList();
.filter(name -> name != null && !name.trim().isEmpty())
.distinct()
.sorted()
.toList();
// Set items for autocomplete // Set items for autocomplete
companyField.setItems(companyNames); companyField.setItems(companyNames);
@@ -640,43 +674,60 @@ public class AddJobView extends Main {
// Find the first customer with this company name // Find the first customer with this company name
Optional<Customer> matchingCustomer = allCustomers.stream() Optional<Customer> matchingCustomer = allCustomers.stream()
.filter(c -> selectedCompany.equals(c.getCompanyName())) .filter(c -> selectedCompany.equals(c.getCompanyName())).findFirst();
.findFirst();
if (matchingCustomer.isPresent()) { if (matchingCustomer.isPresent()) {
Customer customer = matchingCustomer.get(); Customer customer = matchingCustomer.get();
if (isPickup) { if (isPickup) {
// Fill pickup address fields // Fill pickup address fields
if (customer.getTitle() != null && ("Herr".equalsIgnoreCase(customer.getTitle()) || if (customer.getTitle() != null && ("Herr".equalsIgnoreCase(customer.getTitle())
"Frau".equalsIgnoreCase(customer.getTitle()) || "Divers".equalsIgnoreCase(customer.getTitle()))) { || "Frau".equalsIgnoreCase(customer.getTitle())
|| "Divers".equalsIgnoreCase(customer.getTitle()))) {
pickupSalutation.setValue(customer.getTitle()); pickupSalutation.setValue(customer.getTitle());
} }
if (customer.getFirstname() != null) pickupFirstName.setValue(customer.getFirstname()); if (customer.getFirstname() != null)
if (customer.getLastName() != null) pickupLastName.setValue(customer.getLastName()); pickupFirstName.setValue(customer.getFirstname());
if (customer.getTelephone() != null) pickupPhone.setValue(customer.getTelephone()); if (customer.getLastName() != null)
if (customer.getStreet() != null) pickupStreet.setValue(customer.getStreet()); pickupLastName.setValue(customer.getLastName());
if (customer.getHouseNumber() != null) pickupHouseNumber.setValue(customer.getHouseNumber()); if (customer.getTelephone() != null)
if (customer.getAddressAddition() != null) pickupAddressAddition.setValue(customer.getAddressAddition()); pickupPhone.setValue(customer.getTelephone());
if (customer.getZip() != null) pickupZip.setValue(customer.getZip()); if (customer.getStreet() != null)
if (customer.getCity() != null) pickupCity.setValue(customer.getCity()); pickupStreet.setValue(customer.getStreet());
if (customer.getHouseNumber() != null)
pickupHouseNumber.setValue(customer.getHouseNumber());
if (customer.getAddressAddition() != null)
pickupAddressAddition.setValue(customer.getAddressAddition());
if (customer.getZip() != null)
pickupZip.setValue(customer.getZip());
if (customer.getCity() != null)
pickupCity.setValue(customer.getCity());
// Deactivate save checkbox since customer already exists // Deactivate save checkbox since customer already exists
savePickupAddress.setValue(false); savePickupAddress.setValue(false);
} else { } else {
// Fill delivery address fields // Fill delivery address fields
if (customer.getTitle() != null && ("Herr".equalsIgnoreCase(customer.getTitle()) || if (customer.getTitle() != null && ("Herr".equalsIgnoreCase(customer.getTitle())
"Frau".equalsIgnoreCase(customer.getTitle()) || "Divers".equalsIgnoreCase(customer.getTitle()))) { || "Frau".equalsIgnoreCase(customer.getTitle())
|| "Divers".equalsIgnoreCase(customer.getTitle()))) {
deliverySalutation.setValue(customer.getTitle()); deliverySalutation.setValue(customer.getTitle());
} }
if (customer.getFirstname() != null) deliveryFirstName.setValue(customer.getFirstname()); if (customer.getFirstname() != null)
if (customer.getLastName() != null) deliveryLastName.setValue(customer.getLastName()); deliveryFirstName.setValue(customer.getFirstname());
if (customer.getTelephone() != null) deliveryPhone.setValue(customer.getTelephone()); if (customer.getLastName() != null)
if (customer.getStreet() != null) deliveryStreet.setValue(customer.getStreet()); deliveryLastName.setValue(customer.getLastName());
if (customer.getHouseNumber() != null) deliveryHouseNumber.setValue(customer.getHouseNumber()); if (customer.getTelephone() != null)
if (customer.getAddressAddition() != null) deliveryAddressAddition.setValue(customer.getAddressAddition()); deliveryPhone.setValue(customer.getTelephone());
if (customer.getZip() != null) deliveryZip.setValue(customer.getZip()); if (customer.getStreet() != null)
if (customer.getCity() != null) deliveryCity.setValue(customer.getCity()); deliveryStreet.setValue(customer.getStreet());
if (customer.getHouseNumber() != null)
deliveryHouseNumber.setValue(customer.getHouseNumber());
if (customer.getAddressAddition() != null)
deliveryAddressAddition.setValue(customer.getAddressAddition());
if (customer.getZip() != null)
deliveryZip.setValue(customer.getZip());
if (customer.getCity() != null)
deliveryCity.setValue(customer.getCity());
// Deactivate save checkbox since customer already exists // Deactivate save checkbox since customer already exists
saveDeliveryAddress.setValue(false); saveDeliveryAddress.setValue(false);
@@ -700,86 +751,55 @@ public class AddJobView extends Main {
private void setupValidation() { private void setupValidation() {
// Bind pickup address fields with validation // Bind pickup address fields with validation
binder.forField(pickupFirstName) binder.forField(pickupFirstName).asRequired("").bind(Job::getPickupFirstName, Job::setPickupFirstName);
.asRequired("")
.bind(Job::getPickupFirstName, Job::setPickupFirstName);
binder.forField(pickupLastName) binder.forField(pickupLastName).asRequired("").bind(Job::getPickupLastName, Job::setPickupLastName);
.asRequired("")
.bind(Job::getPickupLastName, Job::setPickupLastName);
binder.forField(pickupStreet) binder.forField(pickupStreet).asRequired("").bind(Job::getPickupStreet, Job::setPickupStreet);
.asRequired("")
.bind(Job::getPickupStreet, Job::setPickupStreet);
binder.forField(pickupHouseNumber) binder.forField(pickupHouseNumber).asRequired("").bind(Job::getPickupHouseNumber, Job::setPickupHouseNumber);
.asRequired("")
.bind(Job::getPickupHouseNumber, Job::setPickupHouseNumber);
binder.forField(pickupZip) binder.forField(pickupZip).asRequired("").bind(Job::getPickupZip, Job::setPickupZip);
.asRequired("")
.bind(Job::getPickupZip, Job::setPickupZip);
binder.forField(pickupCity) binder.forField(pickupCity).asRequired("").bind(Job::getPickupCity, Job::setPickupCity);
.asRequired("")
.bind(Job::getPickupCity, Job::setPickupCity);
// Bind delivery address fields with validation // Bind delivery address fields with validation
binder.forField(deliveryFirstName) binder.forField(deliveryFirstName).asRequired("").bind(Job::getDeliveryFirstName, Job::setDeliveryFirstName);
.asRequired("")
.bind(Job::getDeliveryFirstName, Job::setDeliveryFirstName);
binder.forField(deliveryLastName) binder.forField(deliveryLastName).asRequired("").bind(Job::getDeliveryLastName, Job::setDeliveryLastName);
.asRequired("")
.bind(Job::getDeliveryLastName, Job::setDeliveryLastName);
binder.forField(deliveryStreet) binder.forField(deliveryStreet).asRequired("").bind(Job::getDeliveryStreet, Job::setDeliveryStreet);
.asRequired("")
.bind(Job::getDeliveryStreet, Job::setDeliveryStreet);
binder.forField(deliveryHouseNumber) binder.forField(deliveryHouseNumber).asRequired("").bind(Job::getDeliveryHouseNumber,
.asRequired("") Job::setDeliveryHouseNumber);
.bind(Job::getDeliveryHouseNumber, Job::setDeliveryHouseNumber);
binder.forField(deliveryZip) binder.forField(deliveryZip).asRequired("").bind(Job::getDeliveryZip, Job::setDeliveryZip);
.asRequired("")
.bind(Job::getDeliveryZip, Job::setDeliveryZip);
binder.forField(deliveryCity) binder.forField(deliveryCity).asRequired("").bind(Job::getDeliveryCity, Job::setDeliveryCity);
.asRequired("")
.bind(Job::getDeliveryCity, Job::setDeliveryCity);
// Bind price field: Komma-Zahlen in Punkt-Zahlen umsetzen, dann nach BigDecimal konvertieren // Bind price field: Komma-Zahlen in Punkt-Zahlen umsetzen, dann nach BigDecimal
binder.forField(price) // konvertieren
.withNullRepresentation("") binder.forField(price).withNullRepresentation("").asRequired("Preis erforderlich").withConverter((String s) -> {
.asRequired("Preis erforderlich") if (s == null || s.trim().isEmpty())
.withConverter( return null;
(String s) -> {
if (s == null || s.trim().isEmpty()) return null;
String normalized = s.replace(" ", "").replace(".", "").replace(',', '.'); String normalized = s.replace(" ", "").replace(".", "").replace(',', '.');
try { return new java.math.BigDecimal(normalized); } try {
catch (NumberFormatException ex) { throw new NumberFormatException("Ungültiger Betrag"); } return new java.math.BigDecimal(normalized);
}, } catch (NumberFormatException ex) {
(java.math.BigDecimal bd) -> bd == null ? "" : bd.toString(), throw new NumberFormatException("Ungültiger Betrag");
"Ungültiger Betrag" }
) }, (java.math.BigDecimal bd) -> bd == null ? "" : bd.toString(), "Ungültiger Betrag")
.withValidator(value -> value != null && value.compareTo(java.math.BigDecimal.ZERO) > 0, .withValidator(value -> value != null && value.compareTo(java.math.BigDecimal.ZERO) > 0,
"Der Preis muss größer als 0 sein") "Der Preis muss größer als 0 sein")
.bind(Job::getPrice, Job::setPrice); .bind(Job::getPrice, Job::setPrice);
// Bind date picker fields with validation // Bind date picker fields with validation
binder.forField(pickupDate) binder.forField(pickupDate).asRequired("").bind(Job::getPickupDate, Job::setPickupDate);
.asRequired("")
.bind(Job::getPickupDate, Job::setPickupDate);
binder.forField(deliveryDate) binder.forField(deliveryDate).asRequired("").bind(Job::getDeliveryDate, Job::setDeliveryDate);
.asRequired("")
.bind(Job::getDeliveryDate, Job::setDeliveryDate);
// Bind customerSelection field with validation // Bind customerSelection field with validation
binder.forField(customerSelection) binder.forField(customerSelection).asRequired("").bind(Job::getCustomerSelection, Job::setCustomerSelection);
.asRequired("")
.bind(Job::getCustomerSelection, Job::setCustomerSelection);
// Bind optional fields without validation // Bind optional fields without validation
binder.bind(pickupCompany, Job::getPickupCompany, Job::setPickupCompany); binder.bind(pickupCompany, Job::getPickupCompany, Job::setPickupCompany);
@@ -795,28 +815,22 @@ public class AddJobView extends Main {
binder.bind(digitalProcessing, Job::isDigitalProcessing, Job::setDigitalProcessing); binder.bind(digitalProcessing, Job::isDigitalProcessing, Job::setDigitalProcessing);
// Bind appUser with converter: AppUser object <-> String ID // Bind appUser with converter: AppUser object <-> String ID
binder.forField(appUser) binder.forField(appUser).withConverter(
.withConverter(
// Convert AppUser to String (ID) // Convert AppUser to String (ID)
user -> user != null ? user.getId().toHexString() : null, user -> user != null ? user.getId().toHexString() : null,
// Convert String (ID) back to AppUser // Convert String (ID) back to AppUser
id -> { id -> {
if (id == null || id.trim().isEmpty()) return null; if (id == null || id.trim().isEmpty())
return availableAppUsers.stream() return null;
.filter(user -> user.getId().toHexString().equals(id)) return availableAppUsers.stream().filter(user -> user.getId().toHexString().equals(id)).findFirst()
.findFirst()
.orElse(null); .orElse(null);
} })
)
// Require App-Nutzer when digital processing is enabled // Require App-Nutzer when digital processing is enabled
.withValidator( .withValidator(selectedUserId -> {
selectedUserId -> {
boolean digital = Boolean.TRUE.equals(digitalProcessing.getValue()); boolean digital = Boolean.TRUE.equals(digitalProcessing.getValue());
boolean hasUser = selectedUserId != null && !selectedUserId.trim().isEmpty(); boolean hasUser = selectedUserId != null && !selectedUserId.trim().isEmpty();
return !digital || hasUser; return !digital || hasUser;
}, }, "Bitte App-Nutzer auswählen, wenn Digitale Abwicklung aktiv ist")
"Bitte App-Nutzer auswählen, wenn Digitale Abwicklung aktiv ist"
)
.bind(Job::getAppUser, Job::setAppUser); .bind(Job::getAppUser, Job::setAppUser);
// Toggle required indicator for App-Nutzer based on digitalProcessing // Toggle required indicator for App-Nutzer based on digitalProcessing
@@ -846,16 +860,12 @@ public class AddJobView extends Main {
private void setupValidationTriggers() { private void setupValidationTriggers() {
// List of all required fields // List of all required fields
TextField[] requiredTextFields = { TextField[] requiredTextFields = { pickupFirstName, pickupLastName, pickupStreet, pickupHouseNumber, pickupZip,
pickupFirstName, pickupLastName, pickupStreet, pickupHouseNumber, pickupZip, pickupCity, pickupCity, deliveryFirstName, deliveryLastName, deliveryStreet, deliveryHouseNumber, deliveryZip,
deliveryFirstName, deliveryLastName, deliveryStreet, deliveryHouseNumber, deliveryZip, deliveryCity, deliveryCity, price };
price
};
// List of required date fields // List of required date fields
DatePicker[] requiredDateFields = { DatePicker[] requiredDateFields = { pickupDate, deliveryDate };
pickupDate, deliveryDate
};
// Add validation listener for customerSelection ComboBox // Add validation listener for customerSelection ComboBox
customerSelection.addValueChangeListener(event -> { customerSelection.addValueChangeListener(event -> {
@@ -863,7 +873,8 @@ public class AddJobView extends Main {
updateTabLabels(); updateTabLabels();
}); });
// Add value change listeners to trigger validation on every change for text fields // Add value change listeners to trigger validation on every change for text
// fields
for (TextField field : requiredTextFields) { for (TextField field : requiredTextFields) {
field.addValueChangeListener(event -> { field.addValueChangeListener(event -> {
triggerValidation(); triggerValidation();
@@ -949,17 +960,18 @@ public class AddJobView extends Main {
private boolean hasAddressValidationErrors() { private boolean hasAddressValidationErrors() {
// Check customer selection // Check customer selection
boolean customerSelectionEmpty = customerSelection.getValue() == null || customerSelection.getValue().trim().isEmpty(); boolean customerSelectionEmpty = customerSelection.getValue() == null
|| customerSelection.getValue().trim().isEmpty();
// Check pickup address fields // Check pickup address fields
boolean pickupErrors = isFieldEmpty(pickupFirstName) || isFieldEmpty(pickupLastName) || boolean pickupErrors = isFieldEmpty(pickupFirstName) || isFieldEmpty(pickupLastName)
isFieldEmpty(pickupStreet) || isFieldEmpty(pickupHouseNumber) || || isFieldEmpty(pickupStreet) || isFieldEmpty(pickupHouseNumber) || isFieldEmpty(pickupZip)
isFieldEmpty(pickupZip) || isFieldEmpty(pickupCity); || isFieldEmpty(pickupCity);
// Check delivery address fields // Check delivery address fields
boolean deliveryErrors = isFieldEmpty(deliveryFirstName) || isFieldEmpty(deliveryLastName) || boolean deliveryErrors = isFieldEmpty(deliveryFirstName) || isFieldEmpty(deliveryLastName)
isFieldEmpty(deliveryStreet) || isFieldEmpty(deliveryHouseNumber) || || isFieldEmpty(deliveryStreet) || isFieldEmpty(deliveryHouseNumber) || isFieldEmpty(deliveryZip)
isFieldEmpty(deliveryZip) || isFieldEmpty(deliveryCity); || isFieldEmpty(deliveryCity);
return customerSelectionEmpty || pickupErrors || deliveryErrors; return customerSelectionEmpty || pickupErrors || deliveryErrors;
} }
@@ -976,15 +988,15 @@ public class AddJobView extends Main {
} }
// Check that ALL cargo items are complete // Check that ALL cargo items are complete
// A cargo item is considered complete if it has: Description, Quantity, Weight, Length, Width, Height // A cargo item is considered complete if it has: Description, Quantity, Weight,
// Length, Width, Height
boolean allCargoItemsValid = cargoItemsState.stream() boolean allCargoItemsValid = cargoItemsState.stream()
.allMatch(cargoItem -> cargoItem != null && .allMatch(cargoItem -> cargoItem != null && cargoItem.getDescription() != null
cargoItem.getDescription() != null && !cargoItem.getDescription().trim().isEmpty() && && !cargoItem.getDescription().trim().isEmpty() && cargoItem.getQuantity() != null
cargoItem.getQuantity() != null && cargoItem.getQuantity() > 0 && && cargoItem.getQuantity() > 0 && cargoItem.getWeightKg() != null && cargoItem.getWeightKg() > 0
cargoItem.getWeightKg() != null && cargoItem.getWeightKg() > 0 && && cargoItem.getLengthMm() != null && cargoItem.getLengthMm() > 0
cargoItem.getLengthMm() != null && cargoItem.getLengthMm() > 0 && && cargoItem.getWidthMm() != null && cargoItem.getWidthMm() > 0
cargoItem.getWidthMm() != null && cargoItem.getWidthMm() > 0 && && cargoItem.getHeightMm() != null && cargoItem.getHeightMm() > 0);
cargoItem.getHeightMm() != null && cargoItem.getHeightMm() > 0);
return !allCargoItemsValid; // Return true if ANY cargo item is incomplete (show warning) return !allCargoItemsValid; // Return true if ANY cargo item is incomplete (show warning)
} }
@@ -1010,11 +1022,13 @@ public class AddJobView extends Main {
// Zusätzliche Felder, die nicht über den Binder gebunden sind, manuell setzen // Zusätzliche Felder, die nicht über den Binder gebunden sind, manuell setzen
job.setPickupDate(pickupDate.getValue()); job.setPickupDate(pickupDate.getValue());
job.setDeliveryDate(deliveryDate.getValue()); job.setDeliveryDate(deliveryDate.getValue());
if (remarkArea != null) job.setRemark(remarkArea.getValue()); if (remarkArea != null)
job.setRemark(remarkArea.getValue());
// Validate all required fields using the binder // Validate all required fields using the binder
if (binder.writeBeanIfValid(job)) { if (binder.writeBeanIfValid(job)) {
// Additional validation: If digital processing is enabled, app user must be selected // Additional validation: If digital processing is enabled, app user must be
// selected
if (digitalProcessing.getValue() && appUser.getValue() == null) { if (digitalProcessing.getValue() && appUser.getValue() == null) {
Notification errorNotification = Notification.show( Notification errorNotification = Notification.show(
"Wenn digitale Abwicklung per App aktiviert ist, muss ein App-Nutzer ausgewählt werden."); "Wenn digitale Abwicklung per App aktiviert ist, muss ein App-Nutzer ausgewählt werden.");
@@ -1023,15 +1037,14 @@ public class AddJobView extends Main {
} }
// Ensure at least one cargo item is provided (tasks may be empty) // Ensure at least one cargo item is provided (tasks may be empty)
// Definition: Ein Cargo-Item gilt nur als gefüllt, wenn eine Beschreibung vorhanden ist // Definition: Ein Cargo-Item gilt nur als gefüllt, wenn eine Beschreibung
List<CargoItem> cargoFilled = cargoItemsState.stream() // vorhanden ist
.filter(Objects::nonNull) List<CargoItem> cargoFilled = cargoItemsState.stream().filter(Objects::nonNull)
.filter(ci -> ci.getDescription() != null && !ci.getDescription().isBlank()) .filter(ci -> ci.getDescription() != null && !ci.getDescription().isBlank()).toList();
.toList();
if (cargoFilled.isEmpty()) { if (cargoFilled.isEmpty()) {
Notification errorNotification = Notification.show( Notification errorNotification = Notification
"Bitte fügen Sie mindestens eine Ladungszeile hinzu."); .show("Bitte fügen Sie mindestens eine Ladungszeile hinzu.");
errorNotification.setDuration(5000); errorNotification.setDuration(5000);
return; return;
} }
@@ -1074,28 +1087,30 @@ public class AddJobView extends Main {
addCustomerService.addCustomer(deliveryCustomer); addCustomerService.addCustomer(deliveryCustomer);
} }
// All validations passed, save the job with cargo items and tasks (tasks may be empty) // All validations passed, save the job with cargo items and tasks (tasks may be
// empty)
Job savedJob = addJobService.addJobWithCargo(job, cargoFilled, tasksState); Job savedJob = addJobService.addJobWithCargo(job, cargoFilled, tasksState);
// Erfolgsmeldung und Navigation zur Zusammenfassung // Erfolgsmeldung und Navigation zur Zusammenfassung
Notification successNotification = Notification.show( Notification successNotification = Notification
"Auftrag erfolgreich erstellt! Auftragsnummer: " + savedJob.getJobNumber()); .show("Auftrag erfolgreich erstellt! Auftragsnummer: " + savedJob.getJobNumber());
successNotification.setDuration(2000); successNotification.setDuration(2000);
getUI().ifPresent(ui -> ui.navigate(JobSummaryView.class, savedJob.getId().toHexString())); getUI().ifPresent(ui -> ui.navigate(JobSummaryView.class, savedJob.getId().toHexString()));
} else { } else {
// Validation failed, show error message // Validation failed, show error message
Notification errorNotification = Notification.show( Notification errorNotification = Notification
"Bitte füllen Sie alle Pflichtfelder aus (markiert mit *)"); .show("Bitte füllen Sie alle Pflichtfelder aus (markiert mit *)");
errorNotification.setDuration(5000); errorNotification.setDuration(5000);
} }
} catch (Exception e) { } catch (Exception e) {
// Other errors // Other errors
// Reset cargo error // Reset cargo error
if (cargoError != null) cargoError.setVisible(false); if (cargoError != null)
if (cargoAreaContainer != null) cargoAreaContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); cargoError.setVisible(false);
Notification errorNotification = Notification.show( if (cargoAreaContainer != null)
"Fehler beim Erstellen des Auftrags: " + e.getMessage()); cargoAreaContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)");
Notification errorNotification = Notification.show("Fehler beim Erstellen des Auftrags: " + e.getMessage());
errorNotification.setDuration(5000); errorNotification.setDuration(5000);
} }
} }
@@ -1117,8 +1132,8 @@ public class AddJobView extends Main {
loadJobIntoForm(draft); loadJobIntoForm(draft);
// Benutzer informieren // Benutzer informieren
Notification notification = Notification.show( Notification notification = Notification
"Entwurf wiederhergestellt. Sie können Ihre Arbeit fortsetzen."); .show("Entwurf wiederhergestellt. Sie können Ihre Arbeit fortsetzen.");
notification.setDuration(4000); notification.setDuration(4000);
} }
} }
@@ -1163,13 +1178,15 @@ public class AddJobView extends Main {
cargoList.setSpacing(true); cargoList.setSpacing(true);
cargoAreaContainer.add(cargoError, cargoList); cargoAreaContainer.add(cargoError, cargoList);
java.util.function.BiConsumer<String, java.util.function.Consumer<HorizontalLayout>> addCargoRow = (iconName, afterCreate) -> { java.util.function.BiConsumer<String, java.util.function.Consumer<HorizontalLayout>> addCargoRow = (iconName,
afterCreate) -> {
HorizontalLayout row = new HorizontalLayout(); HorizontalLayout row = new HorizontalLayout();
row.setWidthFull(); row.setWidthFull();
row.setAlignItems(FlexComponent.Alignment.END); row.setAlignItems(FlexComponent.Alignment.END);
ComboBox<String> desc = new ComboBox<>("Beschreibung"); ComboBox<String> desc = new ComboBox<>("Beschreibung");
desc.setItems("Europalette", "Einwegpalette", "Düsseldorfer-Palette", "Gitterboxpalette", "Gitterwagen", "Paket"); desc.setItems("Europalette", "Einwegpalette", "Düsseldorfer-Palette", "Gitterboxpalette", "Gitterwagen",
"Paket");
desc.setAllowCustomValue(true); desc.setAllowCustomValue(true);
desc.setPlaceholder("Wählen Sie eine Option oder geben Sie eigenen Text ein..."); desc.setPlaceholder("Wählen Sie eine Option oder geben Sie eigenen Text ein...");
desc.setWidth("40%"); desc.setWidth("40%");
@@ -1251,16 +1268,19 @@ public class AddJobView extends Main {
updateTabLabels(); // Update tab validation when cargo height changes updateTabLabels(); // Update tab validation when cargo height changes
}); });
if (afterCreate != null) afterCreate.accept(row); if (afterCreate != null)
afterCreate.accept(row);
}; };
addCargoRow.accept("", r -> {}); // Show only one empty row by default addCargoRow.accept("", r -> {
}); // Show only one empty row by default
// Add button to add more cargo rows // Add button to add more cargo rows
Button addCargoButton = new Button("Ladung hinzufügen", new Icon(VaadinIcon.PLUS)); Button addCargoButton = new Button("Ladung hinzufügen", new Icon(VaadinIcon.PLUS));
addCargoButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); addCargoButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
addCargoButton.setWidthFull(); // Make button full width of container addCargoButton.setWidthFull(); // Make button full width of container
addCargoButton.addClickListener(e -> addCargoRow.accept("", r -> {})); addCargoButton.addClickListener(e -> addCargoRow.accept("", r -> {
}));
cargoAreaContainer.add(addCargoButton); cargoAreaContainer.add(addCargoButton);
wrapper.add(cargoAreaContainer); wrapper.add(cargoAreaContainer);
@@ -1315,7 +1335,6 @@ public class AddJobView extends Main {
return wrapper; return wrapper;
} }
/** /**
* Hilfsmethode zum Abrufen des aktuellen Benutzernamens * Hilfsmethode zum Abrufen des aktuellen Benutzernamens
*/ */
@@ -1373,9 +1392,9 @@ public class AddJobView extends Main {
Notification.show("Alle Felder wurden geleert", 2000, Notification.Position.BOTTOM_CENTER); Notification.show("Alle Felder wurden geleert", 2000, Notification.Position.BOTTOM_CENTER);
} }
/** /**
* Konfiguriert Focus-Listener für alle Eingabefelder um Drag-and-Drop zu steuern * Konfiguriert Focus-Listener für alle Eingabefelder um Drag-and-Drop zu
* steuern
*/ */
private void setupInputFieldFocusListeners() { private void setupInputFieldFocusListeners() {
// Customer selection // Customer selection
@@ -1468,7 +1487,6 @@ public class AddJobView extends Main {
taskContainer.getStyle().set("background-color", "var(--lumo-base-color)"); taskContainer.getStyle().set("background-color", "var(--lumo-base-color)");
taskContainer.getStyle().set("position", "relative"); taskContainer.getStyle().set("position", "relative");
// Task type selection // Task type selection
ComboBox<TaskType> taskTypeCombo = new ComboBox<>("Aufgabentyp"); ComboBox<TaskType> taskTypeCombo = new ComboBox<>("Aufgabentyp");
taskTypeCombo.setItems(TaskType.values()); taskTypeCombo.setItems(TaskType.values());
@@ -1509,8 +1527,9 @@ public class AddJobView extends Main {
task.setTaskOrder(tasksState.size()); // Set order based on current position task.setTaskOrder(tasksState.size()); // Set order based on current position
tasksState.add(task); tasksState.add(task);
// Use an array to hold the current task reference (allows modification in lambda) // Use an array to hold the current task reference (allows modification in
final BaseTask[] currentTask = {task}; // lambda)
final BaseTask[] currentTask = { task };
taskTypeCombo.addValueChangeListener(ev -> { taskTypeCombo.addValueChangeListener(ev -> {
TaskType selectedType = ev.getValue(); TaskType selectedType = ev.getValue();
@@ -1525,14 +1544,16 @@ public class AddJobView extends Main {
newTask.setCompletedBy(oldTask.getCompletedBy()); newTask.setCompletedBy(oldTask.getCompletedBy());
// Preserve task-specific properties // Preserve task-specific properties
if (oldTask instanceof ConfirmationTask oldConfirmationTask && newTask instanceof ConfirmationTask newConfirmationTask) { if (oldTask instanceof ConfirmationTask oldConfirmationTask
&& newTask instanceof ConfirmationTask newConfirmationTask) {
newConfirmationTask.setButtonText(oldConfirmationTask.getButtonText()); newConfirmationTask.setButtonText(oldConfirmationTask.getButtonText());
} else if (oldTask instanceof TodoListTask oldTodoTask && newTask instanceof TodoListTask newTodoTask) { } else if (oldTask instanceof TodoListTask oldTodoTask && newTask instanceof TodoListTask newTodoTask) {
newTodoTask.setTodoItems(oldTodoTask.getTodoItems()); newTodoTask.setTodoItems(oldTodoTask.getTodoItems());
} else if (oldTask instanceof PhotoTask oldPhotoTask && newTask instanceof PhotoTask newPhotoTask) { } else if (oldTask instanceof PhotoTask oldPhotoTask && newTask instanceof PhotoTask newPhotoTask) {
newPhotoTask.setMinPhotoCount(oldPhotoTask.getMinPhotoCount()); newPhotoTask.setMinPhotoCount(oldPhotoTask.getMinPhotoCount());
newPhotoTask.setMaxPhotoCount(oldPhotoTask.getMaxPhotoCount()); newPhotoTask.setMaxPhotoCount(oldPhotoTask.getMaxPhotoCount());
} else if (oldTask instanceof BarcodeTask oldBarcodeTask && newTask instanceof BarcodeTask newBarcodeTask) { } else if (oldTask instanceof BarcodeTask oldBarcodeTask
&& newTask instanceof BarcodeTask newBarcodeTask) {
newBarcodeTask.setMinBarcodeCount(oldBarcodeTask.getMinBarcodeCount()); newBarcodeTask.setMinBarcodeCount(oldBarcodeTask.getMinBarcodeCount());
newBarcodeTask.setMaxBarcodeCount(oldBarcodeTask.getMaxBarcodeCount()); newBarcodeTask.setMaxBarcodeCount(oldBarcodeTask.getMaxBarcodeCount());
} }
@@ -1580,7 +1601,8 @@ public class AddJobView extends Main {
configContainer.removeAll(); configContainer.removeAll();
TaskType taskType = TaskType.valueOf(task.getTaskType()); TaskType taskType = TaskType.valueOf(task.getTaskType());
if (taskType == null) return; if (taskType == null)
return;
switch (taskType) { switch (taskType) {
case CONFIRMATION: case CONFIRMATION:
@@ -1588,8 +1610,7 @@ public class AddJobView extends Main {
buttonTextField.setPlaceholder("z.B. 'Bestätigen', 'Abgeschlossen'"); buttonTextField.setPlaceholder("z.B. 'Bestätigen', 'Abgeschlossen'");
buttonTextField.setWidthFull(); buttonTextField.setWidthFull();
ConfirmationTask confirmationTask = (ConfirmationTask) task; ConfirmationTask confirmationTask = (ConfirmationTask) task;
buttonTextField.setValue(confirmationTask.getButtonText() != null ? buttonTextField.setValue(confirmationTask.getButtonText() != null ? confirmationTask.getButtonText() : "");
confirmationTask.getButtonText() : "");
buttonTextField.addValueChangeListener(ev -> { buttonTextField.addValueChangeListener(ev -> {
// Find the current ConfirmationTask in tasksState and update it // Find the current ConfirmationTask in tasksState and update it
for (int i = 0; i < tasksState.size(); i++) { for (int i = 0; i < tasksState.size(); i++) {
@@ -1668,14 +1689,12 @@ public class AddJobView extends Main {
IntegerField minPhotos = new IntegerField("Min. Anzahl Fotos"); IntegerField minPhotos = new IntegerField("Min. Anzahl Fotos");
minPhotos.setPlaceholder("1"); minPhotos.setPlaceholder("1");
minPhotos.setMin(1); minPhotos.setMin(1);
minPhotos.setValue(photoTask.getMinPhotoCount() != null ? minPhotos.setValue(photoTask.getMinPhotoCount() != null ? photoTask.getMinPhotoCount() : 1);
photoTask.getMinPhotoCount() : 1);
IntegerField maxPhotos = new IntegerField("Max. Anzahl Fotos"); IntegerField maxPhotos = new IntegerField("Max. Anzahl Fotos");
maxPhotos.setPlaceholder("10"); maxPhotos.setPlaceholder("10");
maxPhotos.setMin(1); maxPhotos.setMin(1);
maxPhotos.setValue(photoTask.getMaxPhotoCount() != null ? maxPhotos.setValue(photoTask.getMaxPhotoCount() != null ? photoTask.getMaxPhotoCount() : 10);
photoTask.getMaxPhotoCount() : 10);
photoLayout.add(minPhotos, maxPhotos); photoLayout.add(minPhotos, maxPhotos);
@@ -1699,14 +1718,12 @@ public class AddJobView extends Main {
IntegerField minBarcodes = new IntegerField("Min. Anzahl Barcodes"); IntegerField minBarcodes = new IntegerField("Min. Anzahl Barcodes");
minBarcodes.setPlaceholder("1"); minBarcodes.setPlaceholder("1");
minBarcodes.setMin(1); minBarcodes.setMin(1);
minBarcodes.setValue(barcodeTask.getMinBarcodeCount() != null ? minBarcodes.setValue(barcodeTask.getMinBarcodeCount() != null ? barcodeTask.getMinBarcodeCount() : 1);
barcodeTask.getMinBarcodeCount() : 1);
IntegerField maxBarcodes = new IntegerField("Max. Anzahl Barcodes"); IntegerField maxBarcodes = new IntegerField("Max. Anzahl Barcodes");
maxBarcodes.setPlaceholder("10"); maxBarcodes.setPlaceholder("10");
maxBarcodes.setMin(1); maxBarcodes.setMin(1);
maxBarcodes.setValue(barcodeTask.getMaxBarcodeCount() != null ? maxBarcodes.setValue(barcodeTask.getMaxBarcodeCount() != null ? barcodeTask.getMaxBarcodeCount() : 10);
barcodeTask.getMaxBarcodeCount() : 10);
barcodeLayout.add(minBarcodes, maxBarcodes); barcodeLayout.add(minBarcodes, maxBarcodes);
@@ -1724,17 +1741,14 @@ public class AddJobView extends Main {
} }
private void updateTodoItems(VerticalLayout todoList, BaseTask task) { private void updateTodoItems(VerticalLayout todoList, BaseTask task) {
List<String> todoItems = todoList.getChildren() List<String> todoItems = todoList.getChildren().map(component -> {
.map(component -> {
if (component instanceof HorizontalLayout) { if (component instanceof HorizontalLayout) {
HorizontalLayout row = (HorizontalLayout) component; HorizontalLayout row = (HorizontalLayout) component;
TextField field = (TextField) row.getChildren().findFirst().orElse(null); TextField field = (TextField) row.getChildren().findFirst().orElse(null);
return field != null ? field.getValue() : null; return field != null ? field.getValue() : null;
} }
return null; return null;
}) }).filter(Objects::nonNull).filter(item -> !item.trim().isEmpty())
.filter(Objects::nonNull)
.filter(item -> !item.trim().isEmpty())
.collect(java.util.stream.Collectors.toList()); .collect(java.util.stream.Collectors.toList());
if (task instanceof TodoListTask) { if (task instanceof TodoListTask) {

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;
@@ -65,8 +65,7 @@ public class AppDevicesView extends VerticalLayout {
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();
} }

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;

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,7 +25,7 @@ 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;
@@ -57,14 +57,15 @@ public class EditAppDeviceView extends VerticalLayout implements HasUrlParameter
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
@@ -148,16 +149,13 @@ public class EditAppDeviceView extends VerticalLayout implements HasUrlParameter
} }
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) binder.forField(appUserComboBox).bind(appDevice -> {
.bind(appDevice -> {
if (appDevice.getAppUserId() != null) { if (appDevice.getAppUserId() != null) {
return appUserService.findByCurrentUser().stream() return appUserService.findByCurrentUser().stream()
.filter(user -> user.getId().equals(appDevice.getAppUserId())) .filter(user -> user.getId().equals(appDevice.getAppUserId())).findFirst().orElse(null);
.findFirst().orElse(null);
} }
return null; return null;
}, (appDevice, appUser) -> { }, (appDevice, appUser) -> {
@@ -178,13 +176,15 @@ public class EditAppDeviceView extends VerticalLayout implements HasUrlParameter
// 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);
@@ -206,7 +206,8 @@ public class EditAppDeviceView extends VerticalLayout implements HasUrlParameter
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);
} }
}); });

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;
@@ -93,9 +93,7 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
// 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)");
@@ -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

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

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,34 +162,21 @@ 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::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); .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);
@@ -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,9 +285,7 @@ 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("background", "var(--lumo-contrast-10pct)")
.set("padding", "0"); .set("padding", "0");
// Initial noch keine PDF laden (erst bei aktiver Checkbox) // Initial noch keine PDF laden (erst bei aktiver Checkbox)
@@ -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
@@ -430,8 +420,8 @@ public class EditProfileView extends HorizontalLayout {
} }
} }
// 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,15 +68,14 @@ 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) {
@@ -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("border-radius", "var(--lumo-border-radius-s)").set("padding", "var(--lumo-space-m)")
.set("padding", "var(--lumo-space-m)") .set("margin-bottom", "var(--lumo-space-s)").set("background-color", "var(--lumo-base-color)")
.set("margin-bottom", "var(--lumo-space-s)") .set("width", "100%").set("box-sizing", "border-box");
.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("margin-top", "var(--lumo-space-xs)")
.set("display", "block"); .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,10 +245,8 @@ 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("margin-top", "var(--lumo-space-xs)")
.set("display", "block"); .set("display", "block");
cardContent.add(changedBy); cardContent.add(changedBy);
} }
@@ -270,7 +257,8 @@ 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);
@@ -287,7 +275,8 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
} }
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)";
@@ -304,10 +293,11 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
} }
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,7 +305,8 @@ 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";
@@ -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("object-fit", "cover")
.set("border-radius", "var(--lumo-border-radius-s)") .set("border-radius", "var(--lumo-border-radius-s)")
.set("border", "1px solid var(--lumo-contrast-20pct)") .set("border", "1px solid var(--lumo-contrast-20pct)").set("cursor", "pointer");
.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,14 +469,10 @@ 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("font-family", "monospace")
.set("font-size", "var(--lumo-font-size-s)")
.set("margin-bottom", "var(--lumo-space-xs)")
.set("word-break", "break-all"); .set("word-break", "break-all");
barcodeBox.add(new Span(barcodeValue)); barcodeBox.add(new Span(barcodeValue));
@@ -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("border-radius", "var(--lumo-border-radius-s)").set("padding", "var(--lumo-space-xs)")
.set("padding", "var(--lumo-space-xs)") .set("background-color", "var(--lumo-base-color)").set("cursor", "pointer").set("width", "200px")
.set("background-color", "var(--lumo-base-color)") .set("height", "100px").set("overflow", "hidden").set("display", "flex")
.set("cursor", "pointer") .set("align-items", "center").set("justify-content", "center");
.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,7 +529,8 @@ 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;
@@ -572,15 +542,15 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
// 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", " ");
} }
@@ -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) {
@@ -530,16 +521,11 @@ 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("padding", "var(--lumo-space-s)").set("background-color", "white")
.set("background-color", "white") .set("width", "100%").set("max-width", "450px").set("overflow", "hidden")
.set("width", "100%") .set("display", "flex").set("align-items", "center")
.set("max-width", "450px")
.set("overflow", "hidden")
.set("display", "flex")
.set("align-items", "center")
.set("justify-content", "center"); .set("justify-content", "center");
// Process SVG to make it responsive // Process SVG to make it responsive
@@ -573,14 +559,11 @@ 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("padding", "var(--lumo-space-s)").set("margin", "var(--lumo-space-xs) 0")
.set("margin", "var(--lumo-space-xs) 0")
.set("background-color", "var(--lumo-contrast-5pct)") .set("background-color", "var(--lumo-contrast-5pct)")
.set("font-family", "monospace") .set("font-family", "monospace").set("font-size", "var(--lumo-font-size-s)")
.set("font-size", "var(--lumo-font-size-s)")
.set("word-break", "break-all"); .set("word-break", "break-all");
Span barcodeSpan = new Span((i + 1) + ". " + barcodeValue); Span barcodeSpan = new Span((i + 1) + ". " + barcodeValue);
@@ -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("box-shadow", "0 4px 12px rgba(0, 0, 0, 0.15)")
.set("border-color", "var(--lumo-primary-color-50pct)"); .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("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)")
.set("border-color", "var(--lumo-contrast-20pct)"); .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,26 +625,19 @@ 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("height", "8px")
.set("border-radius", "50%")
.set("background-color", task.isCompleted() ? "var(--lumo-success-color)" : "var(--lumo-error-color)"); .set("background-color", task.isCompleted() ? "var(--lumo-success-color)" : "var(--lumo-error-color)");
taskCard.add(taskIcon, taskContent, statusIndicator); taskCard.add(taskIcon, taskContent, statusIndicator);
@@ -680,9 +646,7 @@ 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("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)")
.set("border-color", "var(--lumo-contrast-20pct)"); .set("border-color", "var(--lumo-contrast-20pct)");
}); });
@@ -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("max-width", "600px")
.set("min-height", "500px")
.set("height", "500px")
.set("position", "relative")
.set("display", "flex")
.set("align-items", "center")
.set("justify-content", "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("display", "flex")
.set("align-items", "center")
.set("justify-content", "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("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"); .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("display", "flex")
.set("align-items", "center")
.set("justify-content", "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");
@@ -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("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)")
.set("border-color", "var(--lumo-contrast-20pct)"); .set("border-color", "var(--lumo-contrast-20pct)");
} }
} }
} }
} }

View File

@@ -72,19 +72,14 @@ 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);
@@ -101,7 +96,8 @@ 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
@@ -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,24 +43,25 @@ 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();
@@ -80,9 +81,7 @@ public class ShowCustomersView extends VerticalLayout {
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);
@@ -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);
@@ -158,8 +160,7 @@ public class ShowJobsView extends VerticalLayout {
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,8 +73,7 @@ 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
@@ -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 {
@@ -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("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)")
.set("padding", "var(--lumo-space-m)") .set("text-align", "center").set("box-shadow", "var(--lumo-box-shadow-xs)").set("min-width", "150px");
.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;

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,
String createdBy, String jobNumberPattern,
List<JobStatus> statusList); 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

@@ -11,8 +11,9 @@ 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 {
@@ -48,9 +49,7 @@ public class CustomUserPrincipal implements UserDetails {
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"));

View File

@@ -25,32 +25,19 @@ public class SecurityConfig extends VaadinWebSecurity {
// 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("/favicon.ico"),
new AntPathRequestMatcher("/robots.txt"),
new AntPathRequestMatcher("/manifest.webmanifest"),
new AntPathRequestMatcher("/sw.js"),
new AntPathRequestMatcher("/offline.html"),
new AntPathRequestMatcher("/frontend/**"),
new AntPathRequestMatcher("/webjars/**"),
new AntPathRequestMatcher("/h2-console/**"), new AntPathRequestMatcher("/h2-console/**"),
new AntPathRequestMatcher("/frontend-es5/**", "/frontend-es6/**") new AntPathRequestMatcher("/frontend-es5/**", "/frontend-es6/**"))
).permitAll() .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,7 +153,8 @@ 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";
@@ -162,6 +166,24 @@ public class EmailService {
}; };
} }
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(),
"Job erstellt",
"Neuer Job wurde erstellt: " + (job.getJobNumber() != null ? job.getJobNumber() : "Ohne Nummer"), "Neuer Job wurde erstellt: " + (job.getJobNumber() != null ? job.getJobNumber() : "Ohne Nummer"),
createdBy, createdBy, JobHistoryType.CREATE, null, "Job erstellt");
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());
@@ -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());
@@ -213,7 +180,8 @@ 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";
@@ -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.