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) {
Object completedAtObj = source.get("completed_at");
if (completedAtObj instanceof String) {
task.setCompletedAt(LocalDateTime.parse((String) completedAtObj, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
task.setCompletedAt(
LocalDateTime.parse((String) completedAtObj, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
} else if (completedAtObj instanceof java.util.Date) {
task.setCompletedAt(((java.util.Date) completedAtObj).toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime());
task.setCompletedAt(((java.util.Date) completedAtObj).toInstant()
.atZone(java.time.ZoneId.systemDefault()).toLocalDateTime());
}
}
if (source.containsKey("completed_by")) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,6 +24,7 @@ import de.assecutor.votianlt.pages.view.EditProfileView;
import de.assecutor.votianlt.security.SecurityService;
import static com.vaadin.flow.theme.lumo.LumoUtility.*;
@AnonymousAllowed
@Layout
@@ -38,7 +39,8 @@ public final class MainLayout extends AppLayout {
this.securityService = securityService;
setPrimarySection(Section.DRAWER);
// Always build the drawer; keep references and toggle visibility on attach and after navigation
// Always build the drawer; keep references and toggle visibility on attach and
// after navigation
headerRef = createHeader();
navRef = new Scroller(createSideNav());
userMenuRef = createUserMenu();
@@ -52,9 +54,12 @@ public final class MainLayout extends AppLayout {
private void updateDrawerVisibility() {
boolean loggedIn = securityService.isUserLoggedIn();
if (headerRef != null) headerRef.setVisible( loggedIn );
if (navRef != null) navRef.setVisible( loggedIn );
if (userMenuRef != null) userMenuRef.setVisible( loggedIn );
if (headerRef != null)
headerRef.setVisible(loggedIn);
if (navRef != null)
navRef.setVisible(loggedIn);
if (userMenuRef != null)
userMenuRef.setVisible(loggedIn);
setDrawerOpened(loggedIn);
}
@@ -121,7 +126,8 @@ public final class MainLayout extends AppLayout {
userContent.add(profile, myInvoices, imprint);
userDetails.add(userContent);
// Create a vertical layout to hold both regular menu items and collapsible sections
// Create a vertical layout to hold both regular menu items and collapsible
// sections
VerticalLayout navContainer = new VerticalLayout();
navContainer.setPadding(false);
navContainer.setSpacing(false);
@@ -155,8 +161,7 @@ public final class MainLayout extends AppLayout {
userMenuItem.add(userNameSpan);
// Profil anzeigen mit Navigation
userMenuItem.getSubMenu().addItem("Profil anzeigen", e ->
UI.getCurrent().navigate(EditProfileView.class));
userMenuItem.getSubMenu().addItem("Profil anzeigen", e -> UI.getCurrent().navigate(EditProfileView.class));
userMenuItem.getSubMenu().addItem("Einstellungen");
userMenuItem.getSubMenu().addItem("Abmelden", e -> securityService.logout());

View File

@@ -5,5 +5,4 @@ import org.springframework.data.mongodb.repository.MongoRepository;
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> {
}

View File

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

View File

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

View File

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

View File

@@ -32,10 +32,14 @@ public class AddJobService {
private final SecurityService securityService;
private final JobHistoryService jobHistoryService;
private final EmailService emailService;
/**
* Speichert einen neuen Auftrag samt CargoItems und Tasks
* @param job der Auftrag
* @param transientCargo zugehörige, noch nicht gespeicherte CargoItems aus der View
*
* @param job
* der Auftrag
* @param transientCargo
* zugehörige, noch nicht gespeicherte CargoItems aus der View
*/
public Job addJobWithCargo(Job job, List<CargoItem> transientCargo, List<BaseTask> transientTasks) {
try {
@@ -58,10 +62,8 @@ public class AddJobService {
// CargoItems separat mit Referenz auf Job speichern, IDs im Job verknüpfen
if (transientCargo != null && !transientCargo.isEmpty()) {
List<CargoItem> itemsWithJob = transientCargo.stream()
.filter(Objects::nonNull)
.filter(ci -> ci.getDescription() != null && !ci.getDescription().isBlank())
.map(ci -> {
List<CargoItem> itemsWithJob = transientCargo.stream().filter(Objects::nonNull)
.filter(ci -> ci.getDescription() != null && !ci.getDescription().isBlank()).map(ci -> {
CargoItem copy = new CargoItem();
copy.setJobId(jobId);
copy.setDescription(ci.getDescription());
@@ -78,8 +80,7 @@ public class AddJobService {
// Tasks separat speichern und referenzieren mit korrekter Nummerierung
if (transientTasks != null && !transientTasks.isEmpty()) {
var filteredTasks = transientTasks.stream()
.filter(Objects::nonNull)
var filteredTasks = transientTasks.stream().filter(Objects::nonNull)
.filter(task -> task.getTaskType() != null) // Filter nach TaskType statt Text
.toList();
@@ -113,7 +114,8 @@ public class AddJobService {
try {
emailService.sendJobCreationNotification(savedJob.getId(), savedJob.getCreatedBy());
} catch (Exception e) {
log.warn("Failed to send job creation email notification for job {}: {}", savedJob.getIdAsString(), e.getMessage());
log.warn("Failed to send job creation email notification for job {}: {}", savedJob.getIdAsString(),
e.getMessage());
}
log.info("Auftrag erfolgreich gespeichert: {}", savedJob.getJobNumber());
@@ -135,8 +137,7 @@ public class AddJobService {
// Zähle Aufträge des aktuellen Tages
String todayPrefix = prefix + timestamp;
long todayCount = jobRepository.findAll().stream()
.filter(job -> job.getJobNumber() != null && job.getJobNumber().startsWith(todayPrefix))
.count();
.filter(job -> job.getJobNumber() != null && job.getJobNumber().startsWith(todayPrefix)).count();
// Generiere neue Nummer
String jobNumber;

View File

@@ -62,7 +62,8 @@ public class AppUserService {
public AppUser updateAppUser(AppUser appUser) {
// Hash the password if it's being updated and not empty
if (appUser.getPassword() != null && !appUser.getPassword().isEmpty()) {
// Only hash if it's not already hashed (BCrypt hashes start with $2a$, $2b$, or $2y$)
// Only hash if it's not already hashed (BCrypt hashes start with $2a$, $2b$, or
// $2y$)
if (!appUser.getPassword().startsWith("$2")) {
String hashedPassword = passwordEncoder.encode(appUser.getPassword());
appUser.setPassword(hashedPassword);
@@ -76,8 +77,11 @@ public class AppUserService {
/**
* Verify a plain text password against the stored hashed password
* @param plainPassword The plain text password to verify
* @param hashedPassword The stored BCrypt hashed password
*
* @param plainPassword
* The plain text password to verify
* @param hashedPassword
* The stored BCrypt hashed password
* @return true if the password matches, false otherwise
*/
public boolean verifyPassword(String plainPassword, String hashedPassword) {

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ import org.springframework.beans.factory.annotation.Autowired;
@PageTitle("Neues Endgerät anlegen")
@Route(value = "add-app-device", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({"USER","ADMIN"})
@RolesAllowed({ "USER", "ADMIN" })
public class AddAppDeviceView extends VerticalLayout {
private final AppDeviceService appDeviceService;
@@ -98,9 +98,8 @@ public class AddAppDeviceView extends VerticalLayout {
}
private void setupBinder() {
binder.forField(nameField)
.asRequired("Gerätename ist erforderlich")
.bind(AppDevice::getName, AppDevice::setName);
binder.forField(nameField).asRequired("Gerätename ist erforderlich").bind(AppDevice::getName,
AppDevice::setName);
}
private void populateTestData() {
@@ -118,13 +117,15 @@ public class AddAppDeviceView extends VerticalLayout {
AppDevice savedDevice = appDeviceService.createAppDevice(appDevice);
Notification.show("Endgerät erfolgreich angelegt: " + savedDevice.getName(), 3000, Notification.Position.MIDDLE);
Notification.show("Endgerät erfolgreich angelegt: " + savedDevice.getName(), 3000,
Notification.Position.MIDDLE);
// Zurück zur Übersicht
navigateBack();
} catch (Exception e) {
Notification.show("Fehler beim Anlegen des Endgeräts: " + e.getMessage(), 5000, Notification.Position.MIDDLE);
Notification.show("Fehler beim Anlegen des Endgeräts: " + e.getMessage(), 5000,
Notification.Position.MIDDLE);
}
} else {
Notification.show("Bitte füllen Sie alle erforderlichen Felder aus", 3000, Notification.Position.MIDDLE);

View File

@@ -26,7 +26,7 @@ import org.springframework.beans.factory.annotation.Autowired;
@PageTitle("Neuen App-Nutzer anlegen")
@Route(value = "add-app-user", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({"USER","ADMIN"})
@RolesAllowed({ "USER", "ADMIN" })
public class AddAppUserView extends VerticalLayout {
private final AppUserService appUserService;
@@ -84,9 +84,7 @@ public class AddAppUserView extends VerticalLayout {
// Form layout
FormLayout formLayout = new FormLayout();
formLayout.setResponsiveSteps(
new FormLayout.ResponsiveStep("0", 1)
);
formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1));
// Configure fields
designationField.setPlaceholder("(HH H 000)");
@@ -153,25 +151,22 @@ public class AddAppUserView extends VerticalLayout {
binder.forField(phoneField).bind(AppUser::getTelefon, AppUser::setTelefon);
binder.forField(appCodeField).bind(AppUser::getAppCode, AppUser::setAppCode);
binder.forField(emailField).bind(AppUser::getEmail, AppUser::setEmail);
binder.forField(passwordField)
.asRequired("Passwort ist erforderlich")
.bind(AppUser::getPassword, AppUser::setPassword);
binder.forField(passwordField).asRequired("Passwort ist erforderlich").bind(AppUser::getPassword,
AppUser::setPassword);
// Confirm password field validation
binder.forField(confirmPasswordField)
.asRequired("Passwort wiederholen ist erforderlich")
binder.forField(confirmPasswordField).asRequired("Passwort wiederholen ist erforderlich")
.withValidator(confirmPassword -> confirmPassword.equals(passwordField.getValue()),
"Passwörter stimmen nicht überein")
.bind(
appUser -> "", // Dummy getter - this field is not stored
(appUser, value) -> {} // Dummy setter - this field is not stored
);
binder.forField(deviceComboBox)
.asRequired("Bitte ein Gerät auswählen")
.bind(
appUser -> null, // Initialwert, wird beim Erstellen gesetzt
(appUser, appDevice) -> appUser.setAppDeviceId(appDevice != null ? appDevice.getId() : null)
.bind(appUser -> "", // Dummy getter - this field is not stored
(appUser, value) -> {
} // Dummy setter - this field is not stored
);
binder.forField(deviceComboBox).asRequired("Bitte ein Gerät auswählen").bind(appUser -> null, // Initialwert,
// wird beim
// Erstellen
// gesetzt
(appUser, appDevice) -> appUser.setAppDeviceId(appDevice != null ? appDevice.getId() : null));
}
private void createAppUser() {
@@ -191,21 +186,13 @@ public class AddAppUserView extends VerticalLayout {
}
// Show success message
Notification.show(
"App-Nutzer erfolgreich angelegt",
3000,
Notification.Position.MIDDLE
);
Notification.show("App-Nutzer erfolgreich angelegt", 3000, Notification.Position.MIDDLE);
// Navigate back to app user list
navigateBack();
} catch (ValidationException e) {
Notification.show(
"Bitte überprüfen Sie die Eingaben",
3000,
Notification.Position.MIDDLE
);
Notification.show("Bitte überprüfen Sie die Eingaben", 3000, Notification.Position.MIDDLE);
}
}
@@ -233,7 +220,8 @@ public class AddAppUserView extends VerticalLayout {
confirmPasswordField.setValue("testpassword123");
// 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() {

View File

@@ -20,7 +20,8 @@ import java.time.Clock;
@Route(value = "add_company", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@PageTitle("Neuen Firma anlegen")
//@Menu(order = 0, icon = "vaadin:clipboard-check", title = "Neue Firma anlegen")
// @Menu(order = 0, icon = "vaadin:clipboard-check", title = "Neue Firma
// anlegen")
@RolesAllowed("USER")
public class AddCompanyView extends Main {
private final AddCompanyService addCompanyService;
@@ -45,8 +46,7 @@ public class AddCompanyView extends Main {
companyName = new TextField("Firmenname");
companyName.setRequiredIndicatorVisible(true);
binder.forField(companyName)
.asRequired("Firmenname ist ein Pflichtfeld") // Pflichtfeldmeldung
binder.forField(companyName).asRequired("Firmenname ist ein Pflichtfeld") // Pflichtfeldmeldung
.bind(Company::getName, Company::setName);
firstName = new TextField("Vorname");
@@ -66,7 +66,10 @@ public class AddCompanyView extends Main {
// Erstelle ein Div als Container (oder direkt ein Layout)
VerticalLayout formLayout = new VerticalLayout();
formLayout.add(companyName, /*firstName, lastName, telephone, fax, mail, street, houseNumber, addressAddition, zip, city,*/ submitButton);
formLayout.add(companyName, /*
* firstName, lastName, telephone, fax, mail, street, houseNumber,
* addressAddition, zip, city,
*/ submitButton);
// Zentriere die Inhalte vertikal und horizontal
formLayout.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);

View File

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

View File

@@ -20,7 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired;
@PageTitle("Endgeräte")
@Route(value = "app-devices", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({"USER","ADMIN"})
@RolesAllowed({ "USER", "ADMIN" })
public class AppDevicesView extends VerticalLayout {
private final AppDeviceService appDeviceService;
@@ -65,8 +65,7 @@ public class AppDevicesView extends VerticalLayout {
if (appDevice.getAppUserId() != null) {
try {
AppUser appUser = appUserService.findByCurrentUser().stream()
.filter(user -> user.getId().equals(appDevice.getAppUserId()))
.findFirst().orElse(null);
.filter(user -> user.getId().equals(appDevice.getAppUserId())).findFirst().orElse(null);
if (appUser != null) {
return appUser.getVorname() + " " + appUser.getNachname();
}

View File

@@ -20,7 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired;
@PageTitle("App-Nutzer")
@Route(value = "app-user", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({"USER","ADMIN"})
@RolesAllowed({ "USER", "ADMIN" })
public class AppUserView extends VerticalLayout {
private final AppUserService appUserService;

View File

@@ -46,7 +46,8 @@ public class AuthenticatedStartView extends VerticalLayout {
heroSection.setPadding(true);
heroSection.setSpacing(true);
heroSection.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
heroSection.getStyle().set("background", "linear-gradient(135deg, var(--lumo-primary-color-10pct), var(--lumo-primary-color-50pct))");
heroSection.getStyle().set("background",
"linear-gradient(135deg, var(--lumo-primary-color-10pct), var(--lumo-primary-color-50pct))");
heroSection.getStyle().set("min-height", "300px");
heroSection.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
@@ -58,8 +59,7 @@ public class AuthenticatedStartView extends VerticalLayout {
welcomeTitle.getStyle().set("margin-bottom", "var(--lumo-space-l)");
Paragraph welcomeDescription = new Paragraph(
"Nutzen Sie die Navigation links, um neue Aufträge zu erstellen oder Ihre Verwaltung zu bearbeiten."
);
"Nutzen Sie die Navigation links, um neue Aufträge zu erstellen oder Ihre Verwaltung zu bearbeiten.");
welcomeDescription.getStyle().set("text-align", "center");
welcomeDescription.getStyle().set("max-width", "600px");
welcomeDescription.getStyle().set("font-size", "var(--lumo-font-size-l)");
@@ -82,9 +82,8 @@ public class AuthenticatedStartView extends VerticalLayout {
systemTitle.getStyle().set("text-align", "center");
Paragraph systemIntro = new Paragraph(
"Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe ist von entscheidender Bedeutung, " +
"dass sie sich in erster Linie auf ihr eigentliches Geschäft konzentrieren können: Kunden gewinnen und Waren von A nach B liefern."
);
"Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe ist von entscheidender Bedeutung, "
+ "dass sie sich in erster Linie auf ihr eigentliches Geschäft konzentrieren können: Kunden gewinnen und Waren von A nach B liefern.");
systemIntro.getStyle().set("text-align", "center");
systemIntro.getStyle().set("max-width", "800px");
systemIntro.getStyle().set("margin-bottom", "var(--lumo-space-xl)");
@@ -96,14 +95,12 @@ public class AuthenticatedStartView extends VerticalLayout {
featuresGrid.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.START);
// Feature Cards
featuresGrid.add(
createFeatureCard(VaadinIcon.COG, "Einrichtungsassistent",
featuresGrid.add(createFeatureCard(VaadinIcon.COG, "Einrichtungsassistent",
"Mithilfe des Einrichtungsassistenten haben Sie die Möglichkeit, Ihr Nutzerprofil zu vervollständigen."),
createFeatureCard(VaadinIcon.USERS, "Kunden- und Auftragsverwaltung",
"Mit der Kunden- und Auftragsverwaltung haben Sie alle Kontaktdaten und Auftragsdetails stets im Blick."),
createFeatureCard(VaadinIcon.CLIPBOARD_TEXT, "Auftragserstellung",
"Stellen Sie mit wenigen Mausklicks Aufträge ins System ein und legen Sie fest, welcher Mitarbeiter welchen Transportauftrag abarbeiten soll.")
);
"Stellen Sie mit wenigen Mausklicks Aufträge ins System ein und legen Sie fest, welcher Mitarbeiter welchen Transportauftrag abarbeiten soll."));
systemSection.add(systemTitle, systemIntro, featuresGrid);
return systemSection;
@@ -153,9 +150,8 @@ public class AuthenticatedStartView extends VerticalLayout {
appTitle.getStyle().set("text-align", "center");
Paragraph appDescription = new Paragraph(
"Mit unserer mobilen App bleiben Sie auch unterwegs immer über Ihre Aufträge informiert " +
"und können wichtige Aufgaben direkt vom Smartphone aus erledigen."
);
"Mit unserer mobilen App bleiben Sie auch unterwegs immer über Ihre Aufträge informiert "
+ "und können wichtige Aufgaben direkt vom Smartphone aus erledigen.");
appDescription.getStyle().set("text-align", "center");
appDescription.getStyle().set("max-width", "600px");

View File

@@ -25,7 +25,7 @@ import org.springframework.beans.factory.annotation.Autowired;
@PageTitle("Endgerät bearbeiten")
@Route(value = "edit-app-device", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({"USER","ADMIN"})
@RolesAllowed({ "USER", "ADMIN" })
public class EditAppDeviceView extends VerticalLayout implements HasUrlParameter<String> {
private final AppDeviceService appDeviceService;
@@ -57,14 +57,15 @@ public class EditAppDeviceView extends VerticalLayout implements HasUrlParameter
appUserComboBox = new ComboBox<>("App-Nutzer zuordnen");
appUserComboBox.setPlaceholder("App-Nutzer auswählen (optional)");
appUserComboBox.setWidth("100%");
appUserComboBox.setItemLabelGenerator(appUser ->
appUser.getVorname() + " " + appUser.getNachname() + " (" + appUser.getEmail() + ")");
appUserComboBox.setItemLabelGenerator(
appUser -> appUser.getVorname() + " " + appUser.getNachname() + " (" + appUser.getEmail() + ")");
// Lade verfügbare App-Nutzer
try {
appUserComboBox.setItems(appUserService.findByCurrentUser());
} catch (Exception e) {
Notification.show("Fehler beim Laden der App-Nutzer: " + e.getMessage(), 3000, Notification.Position.MIDDLE);
Notification.show("Fehler beim Laden der App-Nutzer: " + e.getMessage(), 3000,
Notification.Position.MIDDLE);
}
// Layout konfigurieren
@@ -148,16 +149,13 @@ public class EditAppDeviceView extends VerticalLayout implements HasUrlParameter
}
private void setupBinder() {
binder.forField(nameField)
.asRequired("Gerätename ist erforderlich")
.bind(AppDevice::getName, AppDevice::setName);
binder.forField(nameField).asRequired("Gerätename ist erforderlich").bind(AppDevice::getName,
AppDevice::setName);
binder.forField(appUserComboBox)
.bind(appDevice -> {
binder.forField(appUserComboBox).bind(appDevice -> {
if (appDevice.getAppUserId() != null) {
return appUserService.findByCurrentUser().stream()
.filter(user -> user.getId().equals(appDevice.getAppUserId()))
.findFirst().orElse(null);
.filter(user -> user.getId().equals(appDevice.getAppUserId())).findFirst().orElse(null);
}
return null;
}, (appDevice, appUser) -> {
@@ -178,13 +176,15 @@ public class EditAppDeviceView extends VerticalLayout implements HasUrlParameter
// Endgerät aktualisieren
AppDevice updatedDevice = appDeviceService.updateAppDevice(currentAppDevice);
Notification.show("Endgerät erfolgreich aktualisiert: " + updatedDevice.getName(), 3000, Notification.Position.MIDDLE);
Notification.show("Endgerät erfolgreich aktualisiert: " + updatedDevice.getName(), 3000,
Notification.Position.MIDDLE);
// Zurück zur Übersicht
navigateBack();
} catch (Exception e) {
Notification.show("Fehler beim Aktualisieren des Endgeräts: " + e.getMessage(), 5000, Notification.Position.MIDDLE);
Notification.show("Fehler beim Aktualisieren des Endgeräts: " + e.getMessage(), 5000,
Notification.Position.MIDDLE);
}
} else {
Notification.show("Bitte füllen Sie alle erforderlichen Felder aus", 3000, Notification.Position.MIDDLE);
@@ -206,7 +206,8 @@ public class EditAppDeviceView extends VerticalLayout implements HasUrlParameter
Notification.show("Endgerät erfolgreich gelöscht", 3000, Notification.Position.MIDDLE);
navigateBack();
} catch (Exception e) {
Notification.show("Fehler beim Löschen des Endgeräts: " + e.getMessage(), 5000, Notification.Position.MIDDLE);
Notification.show("Fehler beim Löschen des Endgeräts: " + e.getMessage(), 5000,
Notification.Position.MIDDLE);
}
});

View File

@@ -32,7 +32,7 @@ import java.util.List;
@PageTitle("App-Nutzer bearbeiten")
@Route(value = "edit-app-user", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({"USER","ADMIN"})
@RolesAllowed({ "USER", "ADMIN" })
public class EditAppUserView extends VerticalLayout implements HasUrlParameter<String> {
private final AppUserService appUserService;
@@ -93,9 +93,7 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
// Form layout
FormLayout formLayout = new FormLayout();
formLayout.setResponsiveSteps(
new FormLayout.ResponsiveStep("0", 1)
);
formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1));
// Configure fields
designationField.setPlaceholder("(HH H 000)");
@@ -167,11 +165,8 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
binder.forField(phoneField).bind(AppUser::getTelefon, AppUser::setTelefon);
binder.forField(appCodeField).bind(AppUser::getAppCode, AppUser::setAppCode);
binder.forField(emailField).bind(AppUser::getEmail, AppUser::setEmail);
binder.forField(deviceComboBox)
.bind(
appUser -> getCurrentDevice(appUser), // Get current device
(appUser, appDevice) -> appUser.setAppDeviceId(appDevice != null ? appDevice.getId() : null)
);
binder.forField(deviceComboBox).bind(appUser -> getCurrentDevice(appUser), // Get current device
(appUser, appDevice) -> appUser.setAppDeviceId(appDevice != null ? appDevice.getId() : null));
}
@Override

View File

@@ -25,7 +25,7 @@ import com.vaadin.flow.component.orderedlayout.FlexComponent;
@PageTitle("Kunde bearbeiten")
@Route(value = "edit-customer", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({"USER","ADMIN"})
@RolesAllowed({ "USER", "ADMIN" })
public class EditCustomerView extends VerticalLayout implements HasUrlParameter<String> {
private final CustomerService customerService;
@@ -75,9 +75,7 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
// Form layout
FormLayout formLayout = new FormLayout();
formLayout.setResponsiveSteps(
new FormLayout.ResponsiveStep("0", 1)
);
formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1));
// Add fields to form - all fields in single column
formLayout.add(titleField);

View File

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

View File

@@ -61,11 +61,13 @@ public class ForgetPasswordView extends VerticalLayout implements BeforeEnterObs
String tokenParam = params.getOrDefault("token", java.util.List.of("")).getFirst();
String typeParam = params.getOrDefault("type", java.util.List.of("")).getFirst();
this.token = tokenParam != null ? tokenParam.trim() : "";
this.userType = "app_user".equalsIgnoreCase(typeParam) ? PasswordResetService.UserType.APP_USER : PasswordResetService.UserType.USERS;
this.userType = "app_user".equalsIgnoreCase(typeParam) ? PasswordResetService.UserType.APP_USER
: PasswordResetService.UserType.USERS;
if (this.token.isEmpty() || !passwordResetService.isTokenValid(this.token, this.userType)) {
// Store a flash message in the VaadinSession so it persists through reroute
com.vaadin.flow.server.VaadinSession.getCurrent().setAttribute("flashMessage", "Ungültiger oder abgelaufener Token.");
com.vaadin.flow.server.VaadinSession.getCurrent().setAttribute("flashMessage",
"Ungültiger oder abgelaufener Token.");
event.rerouteTo(LoginView.class);
}
}

View File

@@ -49,7 +49,8 @@ public class ForgotPasswordRequestView extends VerticalLayout {
}
String baseUrl = getBaseUrl();
passwordResetService.initiateResetAuto(email.trim(), baseUrl);
Notification.show("Falls die E-Mail existiert, wurde ein Link versendet.", 4000, Notification.Position.MIDDLE);
Notification.show("Falls die E-Mail existiert, wurde ein Link versendet.", 4000,
Notification.Position.MIDDLE);
getUI().ifPresent(ui -> ui.navigate("login"));
});
submit.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
@@ -69,8 +70,8 @@ public class ForgotPasswordRequestView extends VerticalLayout {
String serverName = req.getServerName();
int serverPort = req.getServerPort();
String contextPath = req.getContextPath();
String portPart = ("http".equals(scheme) && serverPort == 80) || ("https".equals(scheme) && serverPort == 443)
? "" : ":" + serverPort;
String portPart = ("http".equals(scheme) && serverPort == 80)
|| ("https".equals(scheme) && serverPort == 443) ? "" : ":" + serverPort;
return scheme + "://" + serverName + portPart + contextPath;
}
return "";

View File

@@ -22,7 +22,8 @@ public class ImprintView extends VerticalLayout {
Paragraph p1 = new Paragraph("Max Mustermann\nMusterstraße 1\n12345 Musterstadt\nDeutschland");
Paragraph p2 = new Paragraph("Telefon: +49 123 456789\nE-Mail: info@example.com");
Paragraph p3 = new Paragraph("Umsatzsteuer-ID: DE123456789\nHandelsregister: Amtsgericht Musterstadt, HRB 12345");
Paragraph p3 = new Paragraph(
"Umsatzsteuer-ID: DE123456789\nHandelsregister: Amtsgericht Musterstadt, HRB 12345");
Paragraph p4 = new Paragraph("Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV: Max Mustermann");
add(p1, p2, p3, p4);

View File

@@ -21,7 +21,7 @@ import com.vaadin.flow.server.StreamRegistration;
@PageTitle("Rechnungen")
@Route(value = "invoices", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({"USER","ADMIN"})
@RolesAllowed({ "USER", "ADMIN" })
public class InvoicesView extends VerticalLayout {
private final Grid<Invoice> invoiceGrid;
@@ -47,10 +47,11 @@ public class InvoicesView extends VerticalLayout {
// Testdaten
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-002", "Erika Musterfrau", LocalDate.now().minusDays(1), 299.49, "Express München-Köln"),
new Invoice("R-2024-003", "Hans Beispiel", LocalDate.now(), 149.00, "Standard Leipzig-Dresden")
);
new Invoice("R-2024-001", "Max Mustermann", LocalDate.now().minusDays(2), 199.99,
"Transport Hamburg-Berlin"),
new Invoice("R-2024-002", "Erika Musterfrau", LocalDate.now().minusDays(1), 299.49,
"Express München-Köln"),
new Invoice("R-2024-003", "Hans Beispiel", LocalDate.now(), 149.00, "Standard Leipzig-Dresden"));
invoiceGrid.setItems(testInvoices);
invoiceGrid.addItemClickListener(event -> {
@@ -67,15 +68,14 @@ public class InvoicesView extends VerticalLayout {
try {
// PDF generieren
byte[] pdfBytes = generatePdf(invoice);
StreamResource resource = new StreamResource(
invoice.getId() + ".pdf",
() -> new ByteArrayInputStream(pdfBytes)
);
StreamResource resource = new StreamResource(invoice.getId() + ".pdf",
() -> new ByteArrayInputStream(pdfBytes));
resource.setContentType("application/pdf");
resource.setCacheTime(0);
// Direkter Download über UI
StreamRegistration registration = UI.getCurrent().getSession().getResourceRegistry().registerResource(resource);
StreamRegistration registration = UI.getCurrent().getSession().getResourceRegistry()
.registerResource(resource);
UI.getCurrent().getPage().open(registration.getResourceUri().toString());
} catch (Exception e) {
@@ -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 VerticalLayout content;
public JobHistoryView(JobRepository jobRepository, JobHistoryService jobHistoryService, PhotoRepository photoRepository, BarcodeRepository barcodeRepository, SignatureRepository signatureRepository) {
public JobHistoryView(JobRepository jobRepository, JobHistoryService jobHistoryService,
PhotoRepository photoRepository, BarcodeRepository barcodeRepository,
SignatureRepository signatureRepository) {
this.jobRepository = jobRepository;
this.jobHistoryService = jobHistoryService;
this.photoRepository = photoRepository;
@@ -53,9 +55,8 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
this.signatureRepository = signatureRepository;
setSizeFull();
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX,
LumoUtility.FlexDirection.COLUMN, LumoUtility.Padding.MEDIUM,
LumoUtility.Gap.SMALL);
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL);
add(new ViewToolbar("Job Historie"));
@@ -96,7 +97,8 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
content.removeAll();
// Header mit Job-Informationen
H2 header = new H2("Job Historie - " + (job.getJobNumber() != null ? job.getJobNumber() : "Unbekannte Auftragsnummer"));
H2 header = new H2(
"Job Historie - " + (job.getJobNumber() != null ? job.getJobNumber() : "Unbekannte Auftragsnummer"));
content.add(header);
// Job basic info for context
@@ -131,12 +133,9 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
private Div createJobInfoBox(Job job) {
Div infoBox = new Div();
infoBox.getStyle()
.set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border-radius", "var(--lumo-border-radius-m)")
.set("padding", "var(--lumo-space-m)")
.set("background-color", "var(--lumo-base-color)")
.set("margin-bottom", "var(--lumo-space-m)");
infoBox.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)")
.set("background-color", "var(--lumo-base-color)").set("margin-bottom", "var(--lumo-space-m)");
VerticalLayout infoContent = new VerticalLayout();
infoContent.setPadding(false);
@@ -172,15 +171,11 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
private Div createHistoryEntryCard(JobHistory entry) {
Div card = new Div();
card.getStyle()
.set("border", "1px solid var(--lumo-contrast-10pct)")
card.getStyle().set("border", "1px solid var(--lumo-contrast-10pct)")
.set("border-left", "4px solid " + getTypeColor(entry.getChangeType()))
.set("border-radius", "var(--lumo-border-radius-s)")
.set("padding", "var(--lumo-space-m)")
.set("margin-bottom", "var(--lumo-space-s)")
.set("background-color", "var(--lumo-base-color)")
.set("width", "100%")
.set("box-sizing", "border-box");
.set("border-radius", "var(--lumo-border-radius-s)").set("padding", "var(--lumo-space-m)")
.set("margin-bottom", "var(--lumo-space-s)").set("background-color", "var(--lumo-base-color)")
.set("width", "100%").set("box-sizing", "border-box");
// Header row with icon, reason and timestamp
HorizontalLayout headerRow = new HorizontalLayout();
@@ -194,9 +189,8 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
reason.getStyle().set("font-weight", "500");
Span timestamp = new Span(formatDateTime(entry.getTimestamp()));
timestamp.getStyle()
.set("color", "var(--lumo-secondary-text-color)")
.set("font-size", "var(--lumo-font-size-s)");
timestamp.getStyle().set("color", "var(--lumo-secondary-text-color)").set("font-size",
"var(--lumo-font-size-s)");
HorizontalLayout leftSide = new HorizontalLayout(typeIcon, reason);
leftSide.setAlignItems(HorizontalLayout.Alignment.CENTER);
@@ -213,17 +207,14 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
// Description
if (entry.getDescription() != null && !entry.getDescription().isBlank()) {
Span description = new Span(entry.getDescription());
description.getStyle()
.set("color", "var(--lumo-body-text-color)")
.set("margin-top", "var(--lumo-space-xs)")
description.getStyle().set("color", "var(--lumo-body-text-color)").set("margin-top", "var(--lumo-space-xs)")
.set("display", "block");
cardContent.add(description);
}
// Photo preview for photo tasks
if (entry.getChangeType() == JobHistoryType.TASK_COMPLETED &&
entry.getDetails() != null &&
entry.getDetails().contains("Task-Typ: PHOTO")) {
if (entry.getChangeType() == JobHistoryType.TASK_COMPLETED && entry.getDetails() != null
&& entry.getDetails().contains("Task-Typ: PHOTO")) {
HorizontalLayout photoPreview = createPhotoPreview(entry);
if (photoPreview != null) {
@@ -232,9 +223,8 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
}
// Barcode preview for barcode tasks
if (entry.getChangeType() == JobHistoryType.TASK_COMPLETED &&
entry.getDetails() != null &&
entry.getDetails().contains("Task-Typ: BARCODE")) {
if (entry.getChangeType() == JobHistoryType.TASK_COMPLETED && entry.getDetails() != null
&& entry.getDetails().contains("Task-Typ: BARCODE")) {
VerticalLayout barcodePreview = createBarcodePreview(entry);
if (barcodePreview != null) {
@@ -243,9 +233,8 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
}
// Signature preview for signature tasks
if (entry.getChangeType() == JobHistoryType.TASK_COMPLETED &&
entry.getDetails() != null &&
entry.getDetails().contains("Task-Typ: SIGNATURE")) {
if (entry.getChangeType() == JobHistoryType.TASK_COMPLETED && entry.getDetails() != null
&& entry.getDetails().contains("Task-Typ: SIGNATURE")) {
Div signaturePreview = createSignaturePreview(entry);
if (signaturePreview != null) {
@@ -256,10 +245,8 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
// Changed by (if available)
if (entry.getChangedBy() != null && !entry.getChangedBy().isBlank()) {
Span changedBy = new Span("von: " + entry.getChangedBy());
changedBy.getStyle()
.set("color", "var(--lumo-secondary-text-color)")
.set("font-size", "var(--lumo-font-size-xs)")
.set("margin-top", "var(--lumo-space-xs)")
changedBy.getStyle().set("color", "var(--lumo-secondary-text-color)")
.set("font-size", "var(--lumo-font-size-xs)").set("margin-top", "var(--lumo-space-xs)")
.set("display", "block");
cardContent.add(changedBy);
}
@@ -270,7 +257,8 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
}
private Icon getTypeIcon(JobHistoryType type) {
if (type == null) return new Icon(VaadinIcon.INFO_CIRCLE);
if (type == null)
return new Icon(VaadinIcon.INFO_CIRCLE);
return switch (type) {
case CREATE -> new Icon(VaadinIcon.PLUS_CIRCLE);
@@ -287,7 +275,8 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
}
private String getTypeColor(JobHistoryType type) {
if (type == null) return "var(--lumo-contrast-60pct)";
if (type == null)
return "var(--lumo-contrast-60pct)";
return switch (type) {
case CREATE -> "var(--lumo-success-color)";
@@ -304,10 +293,11 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
}
private String formatDateTime(java.time.LocalDateTime dateTime) {
if (dateTime == null) return "";
if (dateTime == null)
return "";
try {
java.time.format.DateTimeFormatter formatter =
java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter
.ofPattern("dd.MM.yyyy HH:mm");
return dateTime.format(formatter);
} catch (Exception e) {
return dateTime.toString();
@@ -315,7 +305,8 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
}
private String formatStatus(de.assecutor.votianlt.model.JobStatus status) {
if (status == null) return "Unbekannt";
if (status == null)
return "Unbekannt";
return switch (status) {
case CREATED -> "Erstellt";
@@ -351,9 +342,7 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
HorizontalLayout photoLayout = new HorizontalLayout();
photoLayout.setSpacing(true);
photoLayout.getStyle()
.set("margin-top", "var(--lumo-space-s)")
.set("flex-wrap", "wrap");
photoLayout.getStyle().set("margin-top", "var(--lumo-space-s)").set("flex-wrap", "wrap");
for (Photo photo : photos) {
if (photo.getPhoto() != null && !photo.getPhoto().isBlank()) {
@@ -395,18 +384,13 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
private com.vaadin.flow.component.html.Image createPhotoThumbnail(String base64Photo) {
try {
String imageData = base64Photo.startsWith("data:")
? base64Photo
: "data:image/jpeg;base64," + base64Photo;
String imageData = base64Photo.startsWith("data:") ? base64Photo : "data:image/jpeg;base64," + base64Photo;
com.vaadin.flow.component.html.Image thumbnail = new com.vaadin.flow.component.html.Image(imageData, "Foto");
thumbnail.getStyle()
.set("width", "100px")
.set("height", "100px")
.set("object-fit", "cover")
com.vaadin.flow.component.html.Image thumbnail = new com.vaadin.flow.component.html.Image(imageData,
"Foto");
thumbnail.getStyle().set("width", "100px").set("height", "100px").set("object-fit", "cover")
.set("border-radius", "var(--lumo-border-radius-s)")
.set("border", "1px solid var(--lumo-contrast-20pct)")
.set("cursor", "pointer");
.set("border", "1px solid var(--lumo-contrast-20pct)").set("cursor", "pointer");
return thumbnail;
} catch (Exception e) {
@@ -424,15 +408,11 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
photoDialog.setCloseOnEsc(true);
try {
String imageData = base64Photo.startsWith("data:")
? base64Photo
: "data:image/jpeg;base64," + base64Photo;
String imageData = base64Photo.startsWith("data:") ? base64Photo : "data:image/jpeg;base64," + base64Photo;
com.vaadin.flow.component.html.Image enlargedImage = new com.vaadin.flow.component.html.Image(imageData, "Vergrößertes Foto");
enlargedImage.getStyle()
.set("max-width", "100%")
.set("max-height", "100%")
.set("object-fit", "contain");
com.vaadin.flow.component.html.Image enlargedImage = new com.vaadin.flow.component.html.Image(imageData,
"Vergrößertes Foto");
enlargedImage.getStyle().set("max-width", "100%").set("max-height", "100%").set("object-fit", "contain");
VerticalLayout dialogContent = new VerticalLayout(enlargedImage);
dialogContent.setAlignItems(VerticalLayout.Alignment.CENTER);
@@ -470,8 +450,7 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
VerticalLayout barcodeLayout = new VerticalLayout();
barcodeLayout.setPadding(false);
barcodeLayout.setSpacing(true);
barcodeLayout.getStyle()
.set("margin-top", "var(--lumo-space-s)");
barcodeLayout.getStyle().set("margin-top", "var(--lumo-space-s)");
for (Barcode barcode : barcodes) {
if (barcode.getBarcode() != null && !barcode.getBarcode().isBlank()) {
@@ -490,14 +469,10 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
private Div createBarcodeBox(String barcodeValue) {
Div barcodeBox = new Div();
barcodeBox.getStyle()
.set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border-radius", "var(--lumo-border-radius-s)")
.set("padding", "var(--lumo-space-xs)")
.set("background-color", "var(--lumo-contrast-5pct)")
.set("font-family", "monospace")
.set("font-size", "var(--lumo-font-size-s)")
.set("margin-bottom", "var(--lumo-space-xs)")
barcodeBox.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border-radius", "var(--lumo-border-radius-s)").set("padding", "var(--lumo-space-xs)")
.set("background-color", "var(--lumo-contrast-5pct)").set("font-family", "monospace")
.set("font-size", "var(--lumo-font-size-s)").set("margin-bottom", "var(--lumo-space-xs)")
.set("word-break", "break-all");
barcodeBox.add(new Span(barcodeValue));
@@ -531,22 +506,16 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
}
Div previewContainer = new Div();
previewContainer.getStyle()
.set("margin-top", "var(--lumo-space-s)")
previewContainer.getStyle().set("margin-top", "var(--lumo-space-s)")
.set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border-radius", "var(--lumo-border-radius-s)")
.set("padding", "var(--lumo-space-xs)")
.set("background-color", "var(--lumo-base-color)")
.set("cursor", "pointer")
.set("width", "200px")
.set("height", "100px")
.set("overflow", "hidden")
.set("display", "flex")
.set("align-items", "center")
.set("justify-content", "center");
.set("border-radius", "var(--lumo-border-radius-s)").set("padding", "var(--lumo-space-xs)")
.set("background-color", "var(--lumo-base-color)").set("cursor", "pointer").set("width", "200px")
.set("height", "100px").set("overflow", "hidden").set("display", "flex")
.set("align-items", "center").set("justify-content", "center");
// Create responsive SVG for preview
com.vaadin.flow.component.Html signatureSvg = createResponsiveSignatureSvg(signature.getSignatureSvg(), "100%", "100%");
com.vaadin.flow.component.Html signatureSvg = createResponsiveSignatureSvg(signature.getSignatureSvg(),
"100%", "100%");
previewContainer.add(signatureSvg);
// Add click listener for enlarged view
@@ -560,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
String responsiveSvg = svgContent;
@@ -572,15 +542,15 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
// Ensure the SVG has proper responsive attributes
if (!responsiveSvg.contains("preserveAspectRatio")) {
responsiveSvg = responsiveSvg.replaceFirst("<svg",
"<svg preserveAspectRatio=\"xMidYMid meet\"");
responsiveSvg = responsiveSvg.replaceFirst("<svg", "<svg preserveAspectRatio=\"xMidYMid meet\"");
}
// Set responsive dimensions
responsiveSvg = responsiveSvg.replaceFirst("width=\"[^\"]*\"", "width=\"" + width + "\"");
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) {

View File

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

View File

@@ -72,19 +72,14 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
H1 title = new H1("VotianLT");
title.getStyle().set("color", "var(--lumo-primary-color)");
Button registerButton = new Button("Noch kein Konto? Registrieren",
e -> UI.getCurrent().navigate("register"));
Button registerButton = new Button("Noch kein Konto? Registrieren", e -> UI.getCurrent().navigate("register"));
registerButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
// Inline flash message box (hidden by default)
flashBox.getStyle()
.set("background", "var(--lumo-error-color-10pct)")
.set("color", "var(--lumo-error-text-color)")
.set("border", "1px solid var(--lumo-error-color)")
.set("border-radius", "var(--lumo-border-radius-m)")
.set("padding", "var(--lumo-space-m)")
.set("width", "100%")
.set("display", "none");
flashBox.getStyle().set("background", "var(--lumo-error-color-10pct)")
.set("color", "var(--lumo-error-text-color)").set("border", "1px solid var(--lumo-error-color)")
.set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)")
.set("width", "100%").set("display", "none");
VerticalLayout loginLayout = new VerticalLayout();
loginLayout.setAlignItems(FlexComponent.Alignment.CENTER);
@@ -101,7 +96,8 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
private void handlePasswordLogin(String username, String password) {
try {
// Prüfe Benutzername/Passwort
Authentication auth = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
Authentication auth = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password));
if (twoFactorEnabled) {
// 2FA aktiviert: Benutzer noch nicht in SecurityContext setzen
@@ -129,7 +125,8 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
private void handleVerify2fa() {
if (pendingAuth == null) {
Notification.show("Bitte zuerst Benutzername und Passwort eingeben.", 3000, Notification.Position.BOTTOM_CENTER);
Notification.show("Bitte zuerst Benutzername und Passwort eingeben.", 3000,
Notification.Position.BOTTOM_CENTER);
return;
}
String username = pendingAuth.getName();
@@ -145,7 +142,8 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
}
// 2FA korrekt: Benutzer nun anmelden
SecurityContextHolder.getContext().setAuthentication(pendingAuth);
// Persistiere SecurityContext in der HTTP-Session, damit Vaadin/Security ihn in neuen Requests sieht
// Persistiere SecurityContext in der HTTP-Session, damit Vaadin/Security ihn in
// neuen Requests sieht
var vaadinSession = VaadinSession.getCurrent();
if (vaadinSession != null) {
var wrappedSession = vaadinSession.getSession();
@@ -160,10 +158,7 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
@Override
public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
// Zeige Fehlermeldung bei fehlgeschlagener Anmeldung
if (beforeEnterEvent.getLocation()
.getQueryParameters()
.getParameters()
.containsKey("error")) {
if (beforeEnterEvent.getLocation().getQueryParameters().getParameters().containsKey("error")) {
loginForm.setError(true);
}
}

View File

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

View File

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

View File

@@ -17,7 +17,7 @@ import org.springframework.beans.factory.annotation.Autowired;
@PageTitle("Kunden")
@Route(value = "customers", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({"USER","ADMIN"})
@RolesAllowed({ "USER", "ADMIN" })
public class ShowCustomersView extends VerticalLayout {
private final CustomerService customerService;
@@ -43,24 +43,25 @@ public class ShowCustomersView extends VerticalLayout {
add(header);
// Add hint text
var hintText = new com.vaadin.flow.component.html.Paragraph("Klicken Sie auf einen Eintrag, um ihn zu bearbeiten.");
var hintText = new com.vaadin.flow.component.html.Paragraph(
"Klicken Sie auf einen Eintrag, um ihn zu bearbeiten.");
hintText.getStyle().set("color", "var(--lumo-secondary-text-color)");
hintText.getStyle().set("font-size", "var(--lumo-font-size-s)");
add(hintText);
// Configure grid columns
grid.addColumn(Customer::getCompanyName).setHeader("Firma").setAutoWidth(true).setFlexGrow(1).setSortable(true);
grid.addColumn(customer -> (customer.getFirstname() != null ? customer.getFirstname() : "") + " " +
(customer.getLastName() != null ? customer.getLastName() : ""))
.setHeader("Name").setAutoWidth(true).setFlexGrow(1).setSortable(true);
grid.addColumn(customer -> (customer.getFirstname() != null ? customer.getFirstname() : "") + " "
+ (customer.getLastName() != null ? customer.getLastName() : "")).setHeader("Name").setAutoWidth(true)
.setFlexGrow(1).setSortable(true);
grid.addColumn(Customer::getMail).setHeader("E-Mail").setAutoWidth(true).setFlexGrow(1).setSortable(true);
grid.addColumn(Customer::getTelephone).setHeader("Telefon").setAutoWidth(true).setSortable(true);
grid.addColumn(customer -> (customer.getStreet() != null ? customer.getStreet() : "") + " " +
(customer.getHouseNumber() != null ? customer.getHouseNumber() : ""))
.setHeader("Straße").setAutoWidth(true).setFlexGrow(1).setSortable(true);
grid.addColumn(customer -> (customer.getZip() != null ? customer.getZip() : "") + " " +
(customer.getCity() != null ? customer.getCity() : ""))
.setHeader("Ort").setAutoWidth(true).setFlexGrow(1).setSortable(true);
grid.addColumn(customer -> (customer.getStreet() != null ? customer.getStreet() : "") + " "
+ (customer.getHouseNumber() != null ? customer.getHouseNumber() : "")).setHeader("Straße")
.setAutoWidth(true).setFlexGrow(1).setSortable(true);
grid.addColumn(customer -> (customer.getZip() != null ? customer.getZip() : "") + " "
+ (customer.getCity() != null ? customer.getCity() : "")).setHeader("Ort").setAutoWidth(true)
.setFlexGrow(1).setSortable(true);
grid.setMultiSort(true);
grid.setSizeFull();
@@ -80,9 +81,7 @@ public class ShowCustomersView extends VerticalLayout {
add(grid);
// Button action
addCustomerButton.addClickListener(e ->
getUI().ifPresent(ui -> ui.navigate("add-customer"))
);
addCustomerButton.addClickListener(e -> getUI().ifPresent(ui -> ui.navigate("add-customer")));
loadData();
}
@@ -91,8 +90,7 @@ public class ShowCustomersView extends VerticalLayout {
var customers = customerService.findAll();
var currentUserId = securityService.getCurrentUserId();
var ownCustomers = customers.stream()
.filter(c -> c.getCreatedBy() != null && c.getCreatedBy().equals(currentUserId))
.toList();
.filter(c -> c.getCreatedBy() != null && c.getCreatedBy().equals(currentUserId)).toList();
grid.setItems(ownCustomers);
}
}

View File

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

View File

@@ -73,8 +73,7 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
logo.getStyle().set("font-weight", "bold");
// Navigation - abhängig vom Anmeldestatus
Component navigation = securityService.isUserLoggedIn()
? createAuthenticatedNavigation()
Component navigation = securityService.isUserLoggedIn() ? createAuthenticatedNavigation()
: createAnonymousNavigation();
header.add(logo, navigation);
@@ -103,8 +102,7 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
navLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
// Auftragserstellung Button
Button createOrderBtn = new Button("Auftragserstellung", event ->
UI.getCurrent().navigate("add_job"));
Button createOrderBtn = new Button("Auftragserstellung", event -> UI.getCurrent().navigate("add_job"));
createOrderBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
// Verwaltung ComboBox
@@ -170,7 +168,8 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
heroSection.setPadding(true);
heroSection.setSpacing(true);
heroSection.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
heroSection.getStyle().set("background", "linear-gradient(135deg, var(--lumo-primary-color-10pct), var(--lumo-primary-color-50pct))");
heroSection.getStyle().set("background",
"linear-gradient(135deg, var(--lumo-primary-color-10pct), var(--lumo-primary-color-50pct))");
heroSection.getStyle().set("min-height", "400px");
heroSection.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
@@ -184,11 +183,9 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
heroTitle.getStyle().set("color", "var(--lumo-primary-text-color)");
heroTitle.getStyle().set("margin-bottom", "var(--lumo-space-l)");
Paragraph heroDescription = new Paragraph(
"Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe - " +
"volldigital und aus einem Guss. Konzentrieren Sie sich auf Ihr Geschäft, " +
"wir kümmern uns um die Büroarbeit."
);
Paragraph heroDescription = new Paragraph("Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe - "
+ "volldigital und aus einem Guss. Konzentrieren Sie sich auf Ihr Geschäft, "
+ "wir kümmern uns um die Büroarbeit.");
heroDescription.getStyle().set("text-align", "center");
heroDescription.getStyle().set("max-width", "600px");
heroDescription.getStyle().set("font-size", "var(--lumo-font-size-l)");
@@ -214,9 +211,8 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
systemTitle.getStyle().set("text-align", "center");
Paragraph systemIntro = new Paragraph(
"Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe ist von entscheidender Bedeutung, " +
"dass sie sich in erster Linie auf ihr eigentliches Geschäft konzentrieren können: Kunden gewinnen und Waren von A nach B liefern."
);
"Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe ist von entscheidender Bedeutung, "
+ "dass sie sich in erster Linie auf ihr eigentliches Geschäft konzentrieren können: Kunden gewinnen und Waren von A nach B liefern.");
systemIntro.getStyle().set("text-align", "center");
systemIntro.getStyle().set("max-width", "800px");
systemIntro.getStyle().set("margin-bottom", "var(--lumo-space-xl)");
@@ -228,14 +224,12 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
featuresGrid.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.START);
// Feature Cards
featuresGrid.add(
createFeatureCard(VaadinIcon.COG, "Einrichtungsassistent",
featuresGrid.add(createFeatureCard(VaadinIcon.COG, "Einrichtungsassistent",
"Mithilfe des Einrichtungsassistenten haben Sie die Möglichkeit, Ihr Nutzerprofil zu vervollständigen."),
createFeatureCard(VaadinIcon.USERS, "Kunden- und Auftragsverwaltung",
"Mit der Kunden- und Auftragsverwaltung haben Sie alle Kontaktdaten und Auftragsdetails stets im Blick."),
createFeatureCard(VaadinIcon.CLIPBOARD_TEXT, "Auftragserstellung",
"Stellen Sie mit wenigen Mausklicks Aufträge ins System ein und legen Sie fest, welcher Mitarbeiter welchen Transportauftrag abarbeiten soll.")
);
"Stellen Sie mit wenigen Mausklicks Aufträge ins System ein und legen Sie fest, welcher Mitarbeiter welchen Transportauftrag abarbeiten soll."));
systemSection.add(systemTitle, systemIntro, featuresGrid);
return systemSection;
@@ -281,9 +275,8 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
appTitle.getStyle().set("text-align", "center");
Paragraph appDescription = new Paragraph(
"Jeder Auftrag kann optional über die votianLT-App abgearbeitet werden ganz ohne \"Zettelwirtschaft\". " +
"So gelangen alle relevanten Auftragsinformationen direkt auf das Smartphone des Fahrers."
);
"Jeder Auftrag kann optional über die votianLT-App abgearbeitet werden ganz ohne \"Zettelwirtschaft\". "
+ "So gelangen alle relevanten Auftragsinformationen direkt auf das Smartphone des Fahrers.");
appDescription.getStyle().set("text-align", "center");
appDescription.getStyle().set("max-width", "800px");
@@ -314,18 +307,12 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
companyInfo.setPadding(false);
companyInfo.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
companyInfo.add(
new Paragraph("Assecutor Data Service GmbH"),
new Paragraph("Ottensener Str. 8, 22525 Hamburg"),
new Paragraph("Telefon: +49 40 18 123 771 0"),
new Paragraph("E-Mail: ahoi@assecutor.de")
);
companyInfo.add(new Paragraph("Assecutor Data Service GmbH"), new Paragraph("Ottensener Str. 8, 22525 Hamburg"),
new Paragraph("Telefon: +49 40 18 123 771 0"), new Paragraph("E-Mail: ahoi@assecutor.de"));
// Call to Action
Paragraph ctaText = new Paragraph(
"Registrieren Sie sich noch heute und nutzen den kostenfreien Probemonat, " +
"um das System auf Herz und Nieren zu testen."
);
Paragraph ctaText = new Paragraph("Registrieren Sie sich noch heute und nutzen den kostenfreien Probemonat, "
+ "um das System auf Herz und Nieren zu testen.");
ctaText.getStyle().set("text-align", "center");
ctaText.getStyle().set("font-weight", "bold");
ctaText.getStyle().set("color", "var(--lumo-primary-color)");

View File

@@ -15,7 +15,7 @@ import jakarta.annotation.security.RolesAllowed;
@PageTitle("Statistiken")
@Route(value = "statistics", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({"USER","ADMIN"})
@RolesAllowed({ "USER", "ADMIN" })
@JavaScript("https://cdn.jsdelivr.net/npm/chart.js")
public class StatisticsView extends VerticalLayout {
@@ -89,23 +89,17 @@ public class StatisticsView extends VerticalLayout {
private Div createKpiCard(String title, String value, String theme) {
Div card = new Div();
card.addClassName("kpi-card");
card.getStyle()
.set("background", "var(--lumo-base-color)")
card.getStyle().set("background", "var(--lumo-base-color)")
.set("border", "1px solid var(--lumo-contrast-10pct)")
.set("border-radius", "var(--lumo-border-radius-m)")
.set("padding", "var(--lumo-space-m)")
.set("text-align", "center")
.set("box-shadow", "var(--lumo-box-shadow-xs)")
.set("min-width", "150px");
.set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)")
.set("text-align", "center").set("box-shadow", "var(--lumo-box-shadow-xs)").set("min-width", "150px");
H3 titleElement = new H3(title);
titleElement.getStyle().set("margin", "0 0 var(--lumo-space-s) 0").set("font-size", "var(--lumo-font-size-s)");
Span valueElement = new Span(value);
valueElement.getStyle()
.set("font-size", "var(--lumo-font-size-xl)")
.set("font-weight", "bold")
.set("color", getThemeColor(theme));
valueElement.getStyle().set("font-size", "var(--lumo-font-size-xl)").set("font-weight", "bold").set("color",
getThemeColor(theme));
card.add(titleElement, valueElement);
return card;

View File

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

View File

@@ -14,26 +14,27 @@ import java.util.List;
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);
/**
* 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);
/**
* Find history entries for a job within a specific time range
*/
List<JobHistory> findByJobIdAndTimestampBetweenOrderByTimestampDesc(
ObjectId jobId, LocalDateTime start, LocalDateTime end);
List<JobHistory> findByJobIdAndTimestampBetweenOrderByTimestampDesc(ObjectId jobId, LocalDateTime start,
LocalDateTime end);
/**
* Find history entries by change type for a specific job
*/
List<JobHistory> findByJobIdAndChangeTypeOrderByTimestampDesc(
ObjectId jobId, JobHistoryType changeType);
List<JobHistory> findByJobIdAndChangeTypeOrderByTimestampDesc(ObjectId jobId, JobHistoryType changeType);
/**
* 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
*/
@Query("{'createdAt': {'$gte': ?0, '$lte': ?1}, 'createdBy': ?2, " +
"'jobNumber': {'$regex': ?3, '$options': 'i'}, " +
"'status': {'$in': ?4}}")
List<Job> findWithFilters(LocalDateTime startDate, LocalDateTime endDate,
String createdBy, String jobNumberPattern,
@Query("{'createdAt': {'$gte': ?0, '$lte': ?1}, 'createdBy': ?2, "
+ "'jobNumber': {'$regex': ?3, '$options': 'i'}, " + "'status': {'$in': ?4}}")
List<Job> findWithFilters(LocalDateTime startDate, LocalDateTime endDate, String createdBy, String jobNumberPattern,
List<JobStatus> statusList);
}

View File

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

View File

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

View File

@@ -11,8 +11,9 @@ import java.util.Set;
import java.util.stream.Collectors;
/**
* Custom UserDetails implementation that holds a reference to the MongoDB User entity.
* This allows access to the complete User object from the session without additional database queries.
* Custom UserDetails implementation that holds a reference to the MongoDB User
* entity. This allows access to the complete User object from the session
* without additional database queries.
*/
public class CustomUserPrincipal implements UserDetails {
@@ -48,9 +49,7 @@ public class CustomUserPrincipal implements UserDetails {
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<String> roles = user.getRoles();
if (roles != null && !roles.isEmpty()) {
return roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toList());
return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect(Collectors.toList());
}
// Default role if no roles are set
return Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"));

View File

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

View File

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

View File

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

View File

@@ -71,10 +71,12 @@ public class EmailService {
// Send email
sendEmail(user, job, taskType, taskId, appUser);
log.info("Task completion notification sent to {} for job {} task {}", user.getEmail(), job.getJobNumber(), taskId);
log.info("Task completion notification sent to {} for job {} task {}", user.getEmail(), job.getJobNumber(),
taskId);
} catch (Exception e) {
log.error("Failed to send task completion notification for job {} task {}: {}", jobId, taskId, e.getMessage(), e);
log.error("Failed to send task completion notification for job {} task {}: {}", jobId, taskId,
e.getMessage(), e);
}
}
@@ -82,7 +84,8 @@ public class EmailService {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(smtpUsername);
message.setTo(user.getEmail());
message.setSubject("Aufgabe abgeschlossen - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
message.setSubject(
"Aufgabe abgeschlossen - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
String fullName = buildFullName(user);
String appUserName = buildAppUserName(appUser);
@@ -150,7 +153,8 @@ public class EmailService {
}
private String getTaskTypeDisplayName(String taskType) {
if (taskType == null) return "Unbekannte Aufgabe";
if (taskType == null)
return "Unbekannte Aufgabe";
return switch (taskType.toUpperCase()) {
case "PHOTO" -> "Foto-Aufgabe";
@@ -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) {
try {
// Check if all tasks for this job are completed
@@ -174,7 +196,8 @@ public class EmailService {
boolean allCompleted = allTasks.stream().allMatch(task -> task.isCompleted());
if (allCompleted) {
log.info("All tasks completed for job {}, updating job status and sending completion notification", jobId);
log.info("All tasks completed for job {}, updating job status and sending completion notification",
jobId);
// Update job status to COMPLETED
updateJobStatusToCompleted(jobId);
@@ -240,7 +263,8 @@ public class EmailService {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(smtpUsername);
message.setTo(user.getEmail());
message.setSubject("Job abgeschlossen - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
message.setSubject(
"Job abgeschlossen - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
String fullName = buildFullName(user);
String appUserName = buildAppUserName(appUser);
@@ -299,8 +323,8 @@ public class EmailService {
job.setUpdatedAt(java.time.LocalDateTime.now());
jobRepository.save(job);
log.info("Job status updated from {} to COMPLETED for job {}",
oldStatus != null ? oldStatus : "null", job.getJobNumber());
log.info("Job status updated from {} to COMPLETED for job {}", oldStatus != null ? oldStatus : "null",
job.getJobNumber());
} else {
log.debug("Job {} already has COMPLETED status", job.getJobNumber());
}
@@ -354,7 +378,8 @@ public class EmailService {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(smtpUsername);
message.setTo(user.getEmail());
message.setSubject("Neuer Job erstellt - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
message.setSubject(
"Neuer Job erstellt - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
String fullName = buildFullName(user);
@@ -389,14 +414,18 @@ public class EmailService {
body.append("Anzahl Aufgaben: ").append(taskCount).append("\n");
}
body.append("Status: ").append(job.getStatus() != null ? job.getStatus().getDisplayName() : "Unbekannt").append("\n");
body.append("Status: ").append(job.getStatus() != null ? job.getStatus().getDisplayName() : "Unbekannt")
.append("\n");
if (job.getRemark() != null && !job.getRemark().isBlank()) {
body.append("Bemerkung: ").append(job.getRemark()).append("\n");
}
body.append("Erstellt am: ").append(job.getCreatedAt() != null ?
job.getCreatedAt().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")) : "Unbekannt").append("\n\n");
body.append("Erstellt am: ")
.append(job.getCreatedAt() != null
? job.getCreatedAt().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))
: "Unbekannt")
.append("\n\n");
body.append("Der Job ist nun im System verfügbar und kann bearbeitet werden.\n\n");
body.append("Mit freundlichen Grüßen,\n");
@@ -405,4 +434,24 @@ public class EmailService {
message.setText(body.toString());
mailSender.send(message);
}
/**
* Send a simple text email
*/
public void sendSimpleEmail(String to, String subject, String body) {
try {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(smtpUsername);
message.setTo(to);
message.setSubject(subject);
message.setText(body);
mailSender.send(message);
log.debug("Simple email sent to {} with subject: {}", to, subject);
} catch (Exception e) {
log.error("Failed to send simple email to {} with subject '{}': {}", to, subject, e.getMessage(), e);
throw new RuntimeException("Failed to send email: " + e.getMessage(), e);
}
}
}

View File

@@ -29,15 +29,9 @@ public class JobHistoryService {
*/
public void logJobCreation(Job job, String createdBy) {
try {
JobHistory history = new JobHistory(
job.getId(),
"Job erstellt",
JobHistory history = new JobHistory(job.getId(), "Job erstellt",
"Neuer Job wurde erstellt: " + (job.getJobNumber() != null ? job.getJobNumber() : "Ohne Nummer"),
createdBy,
JobHistoryType.CREATE,
null,
"Job erstellt"
);
createdBy, JobHistoryType.CREATE, null, "Job erstellt");
if (job.getDeliveryCompany() != null) {
history.setDetails("Kunde: " + job.getDeliveryCompany());
@@ -55,19 +49,12 @@ public class JobHistoryService {
*/
public void logStatusChange(Job job, JobStatus oldStatus, JobStatus newStatus, String changedBy) {
try {
String description = String.format("Status geändert von %s zu %s",
formatStatus(oldStatus),
String description = String.format("Status geändert von %s zu %s", formatStatus(oldStatus),
formatStatus(newStatus));
JobHistory history = new JobHistory(
job.getId(),
"Status-Änderung",
description,
changedBy,
JobHistoryType.STATUS_CHANGE,
oldStatus != null ? oldStatus.toString() : null,
newStatus != null ? newStatus.toString() : null
);
JobHistory history = new JobHistory(job.getId(), "Status-Änderung", description, changedBy,
JobHistoryType.STATUS_CHANGE, oldStatus != null ? oldStatus.toString() : null,
newStatus != null ? newStatus.toString() : null);
jobHistoryRepository.save(history);
log.debug("Status change logged for job {}: {} -> {}", job.getIdAsString(), oldStatus, newStatus);
@@ -83,15 +70,9 @@ public class JobHistoryService {
try {
String description = generateUpdateDescription(oldJob, newJob);
JobHistory history = new JobHistory(
newJob.getId(),
reason != null ? reason : "Job aktualisiert",
description,
changedBy,
JobHistoryType.UPDATE,
serializeJobForComparison(oldJob),
serializeJobForComparison(newJob)
);
JobHistory history = new JobHistory(newJob.getId(), reason != null ? reason : "Job aktualisiert",
description, changedBy, JobHistoryType.UPDATE, serializeJobForComparison(oldJob),
serializeJobForComparison(newJob));
jobHistoryRepository.save(history);
log.debug("Job update logged for job {}", newJob.getIdAsString());
@@ -120,15 +101,8 @@ public class JobHistoryService {
description += " - " + extraDataSummary;
}
JobHistory history = new JobHistory(
jobId,
"Aufgabe abgeschlossen",
description,
completedBy,
JobHistoryType.TASK_COMPLETED,
"In Bearbeitung",
"Abgeschlossen"
);
JobHistory history = new JobHistory(jobId, "Aufgabe abgeschlossen", description, completedBy,
JobHistoryType.TASK_COMPLETED, "In Bearbeitung", "Abgeschlossen");
// Detaillierte Informationen in details speichern
StringBuilder details = new StringBuilder();
@@ -165,15 +139,8 @@ public class JobHistoryService {
description = String.format("Job-Zuweisung geändert von %s zu %s", oldAssignee, newAssignee);
}
JobHistory history = new JobHistory(
job.getId(),
"Zuweisung geändert",
description,
changedBy,
JobHistoryType.ASSIGNMENT,
oldAssignee,
newAssignee
);
JobHistory history = new JobHistory(job.getId(), "Zuweisung geändert", description, changedBy,
JobHistoryType.ASSIGNMENT, oldAssignee, newAssignee);
jobHistoryRepository.save(history);
log.debug("Job assignment logged for job {}", job.getIdAsString());
@@ -213,7 +180,8 @@ public class JobHistoryService {
// Helper methods
private String formatStatus(JobStatus status) {
if (status == null) return "Unbekannt";
if (status == null)
return "Unbekannt";
return switch (status) {
case CREATED -> "Erstellt";
@@ -243,15 +211,17 @@ public class JobHistoryService {
hasChanges = true;
}
if (!equals(oldJob.getPickupCity(), newJob.getPickupCity()) ||
!equals(oldJob.getDeliveryCity(), newJob.getDeliveryCity())) {
if (hasChanges) description.append(",");
if (!equals(oldJob.getPickupCity(), newJob.getPickupCity())
|| !equals(oldJob.getDeliveryCity(), newJob.getDeliveryCity())) {
if (hasChanges)
description.append(",");
description.append(" - Orte");
hasChanges = true;
}
if (!equals(oldJob.getRemark(), newJob.getRemark())) {
if (hasChanges) description.append(",");
if (hasChanges)
description.append(",");
description.append(" - Bemerkung");
hasChanges = true;
}

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 static void changeDrawerState(boolean drawerState) {
AppLayout appLayout = (AppLayout) UI.getCurrent().getChildren()
.filter(AppLayout.class::isInstance)
.findFirst().orElse(null);
AppLayout appLayout = (AppLayout) UI.getCurrent().getChildren().filter(AppLayout.class::isInstance).findFirst()
.orElse(null);
if (appLayout != null) {
appLayout.setDrawerOpened(drawerState);

View File

@@ -1,2 +1,3 @@
// Zeroconf removed from project.
// This file intentionally left without any classes to eliminate any zeroconf-related beans or code.
// This file intentionally left without any classes to eliminate any
// zeroconf-related beans or code.