Erweiterungen
This commit is contained in:
12
.claude/settings.local.json
Normal file
12
.claude/settings.local.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(./mvnw clean compile -q)",
|
||||
"mcp__ide__getDiagnostics",
|
||||
"Bash(find:*)",
|
||||
"Bash(./mvnw:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -50,55 +50,55 @@ public class MongoConfig {
|
||||
|
||||
BaseTask task;
|
||||
switch (className) {
|
||||
case "de.assecutor.votianlt.model.task.ConfirmationTask":
|
||||
case "ConfirmationTask":
|
||||
log.debug("Creating ConfirmationTask");
|
||||
task = new ConfirmationTask();
|
||||
if (source.containsKey("button_text")) {
|
||||
((ConfirmationTask) task).setButtonText(source.getString("button_text"));
|
||||
}
|
||||
break;
|
||||
case "de.assecutor.votianlt.model.task.SignatureTask":
|
||||
case "SignatureTask":
|
||||
log.debug("Creating SignatureTask");
|
||||
task = new SignatureTask();
|
||||
break;
|
||||
case "de.assecutor.votianlt.model.task.PhotoTask":
|
||||
case "PhotoTask":
|
||||
log.debug("Creating PhotoTask");
|
||||
task = new PhotoTask();
|
||||
if (source.containsKey("min_photo_count")) {
|
||||
((PhotoTask) task).setMinPhotoCount(source.getInteger("min_photo_count"));
|
||||
}
|
||||
if (source.containsKey("max_photo_count")) {
|
||||
((PhotoTask) task).setMaxPhotoCount(source.getInteger("max_photo_count"));
|
||||
}
|
||||
break;
|
||||
case "de.assecutor.votianlt.model.task.TodoListTask":
|
||||
case "TodoListTask":
|
||||
log.debug("Creating TodoListTask");
|
||||
task = new TodoListTask();
|
||||
if (source.containsKey("todo_items")) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> todoItems = (List<String>) source.get("todo_items");
|
||||
((TodoListTask) task).setTodoItems(todoItems);
|
||||
}
|
||||
break;
|
||||
case "de.assecutor.votianlt.model.task.BarcodeTask":
|
||||
case "BarcodeTask":
|
||||
log.debug("Creating BarcodeTask");
|
||||
task = new BarcodeTask();
|
||||
if (source.containsKey("min_barcode_count")) {
|
||||
((BarcodeTask) task).setMinBarcodeCount(source.getInteger("min_barcode_count"));
|
||||
}
|
||||
if (source.containsKey("max_barcode_count")) {
|
||||
((BarcodeTask) task).setMaxBarcodeCount(source.getInteger("max_barcode_count"));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
log.warn("Unknown className '{}', falling back to ConfirmationTask", className);
|
||||
task = new ConfirmationTask(); // fallback
|
||||
break;
|
||||
case "de.assecutor.votianlt.model.task.ConfirmationTask":
|
||||
case "ConfirmationTask":
|
||||
log.debug("Creating ConfirmationTask");
|
||||
task = new ConfirmationTask();
|
||||
if (source.containsKey("button_text")) {
|
||||
((ConfirmationTask) task).setButtonText(source.getString("button_text"));
|
||||
}
|
||||
break;
|
||||
case "de.assecutor.votianlt.model.task.SignatureTask":
|
||||
case "SignatureTask":
|
||||
log.debug("Creating SignatureTask");
|
||||
task = new SignatureTask();
|
||||
break;
|
||||
case "de.assecutor.votianlt.model.task.PhotoTask":
|
||||
case "PhotoTask":
|
||||
log.debug("Creating PhotoTask");
|
||||
task = new PhotoTask();
|
||||
if (source.containsKey("min_photo_count")) {
|
||||
((PhotoTask) task).setMinPhotoCount(source.getInteger("min_photo_count"));
|
||||
}
|
||||
if (source.containsKey("max_photo_count")) {
|
||||
((PhotoTask) task).setMaxPhotoCount(source.getInteger("max_photo_count"));
|
||||
}
|
||||
break;
|
||||
case "de.assecutor.votianlt.model.task.TodoListTask":
|
||||
case "TodoListTask":
|
||||
log.debug("Creating TodoListTask");
|
||||
task = new TodoListTask();
|
||||
if (source.containsKey("todo_items")) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> todoItems = (List<String>) source.get("todo_items");
|
||||
((TodoListTask) task).setTodoItems(todoItems);
|
||||
}
|
||||
break;
|
||||
case "de.assecutor.votianlt.model.task.BarcodeTask":
|
||||
case "BarcodeTask":
|
||||
log.debug("Creating BarcodeTask");
|
||||
task = new BarcodeTask();
|
||||
if (source.containsKey("min_barcode_count")) {
|
||||
((BarcodeTask) task).setMinBarcodeCount(source.getInteger("min_barcode_count"));
|
||||
}
|
||||
if (source.containsKey("max_barcode_count")) {
|
||||
((BarcodeTask) task).setMaxBarcodeCount(source.getInteger("max_barcode_count"));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
log.warn("Unknown className '{}', falling back to ConfirmationTask", className);
|
||||
task = new ConfirmationTask(); // fallback
|
||||
break;
|
||||
}
|
||||
|
||||
// Set common fields
|
||||
@@ -120,9 +120,11 @@ public class MongoConfig {
|
||||
if (source.containsKey("completed_at") && source.get("completed_at") != null) {
|
||||
Object completedAtObj = source.get("completed_at");
|
||||
if (completedAtObj instanceof String) {
|
||||
task.setCompletedAt(LocalDateTime.parse((String) completedAtObj, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
|
||||
task.setCompletedAt(
|
||||
LocalDateTime.parse((String) completedAtObj, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
|
||||
} else if (completedAtObj instanceof java.util.Date) {
|
||||
task.setCompletedAt(((java.util.Date) completedAtObj).toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime());
|
||||
task.setCompletedAt(((java.util.Date) completedAtObj).toInstant()
|
||||
.atZone(java.time.ZoneId.systemDefault()).toLocalDateTime());
|
||||
}
|
||||
}
|
||||
if (source.containsKey("completed_by")) {
|
||||
@@ -137,18 +139,18 @@ public class MongoConfig {
|
||||
return "de.assecutor.votianlt.model.task.ConfirmationTask";
|
||||
}
|
||||
switch (taskType) {
|
||||
case "CONFIRMATION":
|
||||
return "de.assecutor.votianlt.model.task.ConfirmationTask";
|
||||
case "SIGNATURE":
|
||||
return "de.assecutor.votianlt.model.task.SignatureTask";
|
||||
case "PHOTO":
|
||||
return "de.assecutor.votianlt.model.task.PhotoTask";
|
||||
case "TODOLIST":
|
||||
return "de.assecutor.votianlt.model.task.TodoListTask";
|
||||
case "BARCODE":
|
||||
return "de.assecutor.votianlt.model.task.BarcodeTask";
|
||||
default:
|
||||
return "de.assecutor.votianlt.model.task.ConfirmationTask";
|
||||
case "CONFIRMATION":
|
||||
return "de.assecutor.votianlt.model.task.ConfirmationTask";
|
||||
case "SIGNATURE":
|
||||
return "de.assecutor.votianlt.model.task.SignatureTask";
|
||||
case "PHOTO":
|
||||
return "de.assecutor.votianlt.model.task.PhotoTask";
|
||||
case "TODOLIST":
|
||||
return "de.assecutor.votianlt.model.task.TodoListTask";
|
||||
case "BARCODE":
|
||||
return "de.assecutor.votianlt.model.task.BarcodeTask";
|
||||
default:
|
||||
return "de.assecutor.votianlt.model.task.ConfirmationTask";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<CargoItem> cargoItems = cargoItemRepository.findByJobId(job.getId());
|
||||
List<BaseTask> tasks = taskRepository.findByJobIdOrderByTaskOrderAsc(job.getId());
|
||||
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()));
|
||||
// Log task details for debugging
|
||||
tasks.forEach(task -> log.info("Task details for job {}: type={}, order={}", job.getId(),
|
||||
task.getTaskType(), task.getTaskOrder()));
|
||||
|
||||
return new JobWithRelatedDataDTO(job, cargoItems, tasks);
|
||||
})
|
||||
.toList();
|
||||
return new JobWithRelatedDataDTO(job, cargoItems, tasks);
|
||||
}).toList();
|
||||
|
||||
// Publish to the requesting client's topic if clientId is known
|
||||
if (clientId != null && !clientId.isBlank()) {
|
||||
String topic = "/client/" + clientId + "/jobs";
|
||||
mqttPublisher.publishAsJson(topic, jobsWithRelatedData, false);
|
||||
log.info("Published {} assigned jobs for appUserId='{}' to topic '{}'", jobsWithRelatedData.size(), appUserId, topic);
|
||||
log.info("Published {} assigned jobs for appUserId='{}' to topic '{}'", jobsWithRelatedData.size(),
|
||||
appUserId, topic);
|
||||
} else {
|
||||
log.warn("No clientId available to publish assigned jobs for appUserId='{}'. Skipping MQTT publish.", appUserId);
|
||||
log.warn("No clientId available to publish assigned jobs for appUserId='{}'. Skipping MQTT publish.",
|
||||
appUserId);
|
||||
}
|
||||
|
||||
// Log complete JSON for debugging
|
||||
@@ -189,10 +197,10 @@ public class MessageController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Report generic task completion from apps.
|
||||
* Client sends to /app/task/completed with payload { taskId, completedBy?, note? }.
|
||||
* Broadcasts to /topic/task-updates and /topic/tasks/{taskId}.
|
||||
* This endpoint accepts any task type (fallback for GENERIC or unknown types).
|
||||
* Report generic task completion from apps. Client sends to /app/task/completed
|
||||
* with payload { taskId, completedBy?, note? }. Broadcasts to
|
||||
* /topic/task-updates and /topic/tasks/{taskId}. This endpoint accepts any task
|
||||
* type (fallback for GENERIC or unknown types).
|
||||
*/
|
||||
public void handleTaskCompleted(Map<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();
|
||||
@@ -215,24 +226,24 @@ public class MessageController {
|
||||
log.info("handleTaskCompleted called with taskType={}, data: {}", taskType, payload);
|
||||
|
||||
switch (key) {
|
||||
case "PHOTO" -> {
|
||||
processPhotoTaskCompletion(payload);
|
||||
}
|
||||
case "CONFIRMATION" -> {
|
||||
processConfirmationTaskCompletion(payload);
|
||||
}
|
||||
case "SIGNATURE" -> {
|
||||
processSignatureTaskCompletion(payload);
|
||||
}
|
||||
case "TODOLIST" -> {
|
||||
processTodoListTaskCompletion(payload);
|
||||
}
|
||||
case "BARCODE" -> {
|
||||
processBarcodeTaskCompletion(payload);
|
||||
}
|
||||
default -> {
|
||||
log.info("ERROR: handleTaskCompleted called with taskType={}, data: {}", taskType, payload);
|
||||
}
|
||||
case "PHOTO" -> {
|
||||
processPhotoTaskCompletion(payload);
|
||||
}
|
||||
case "CONFIRMATION" -> {
|
||||
processConfirmationTaskCompletion(payload);
|
||||
}
|
||||
case "SIGNATURE" -> {
|
||||
processSignatureTaskCompletion(payload);
|
||||
}
|
||||
case "TODOLIST" -> {
|
||||
processTodoListTaskCompletion(payload);
|
||||
}
|
||||
case "BARCODE" -> {
|
||||
processBarcodeTaskCompletion(payload);
|
||||
}
|
||||
default -> {
|
||||
log.info("ERROR: handleTaskCompleted called with taskType={}, data: {}", taskType, payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,16 +277,15 @@ public class MessageController {
|
||||
|
||||
if (!barcodes.isEmpty()) {
|
||||
for (String barcodeString : barcodes) {
|
||||
Barcode barcodeEntry = new Barcode(
|
||||
new ObjectId(taskId.toString()),
|
||||
barcodeString,
|
||||
task.getCompletedBy()
|
||||
);
|
||||
Barcode barcodeEntry = new Barcode(new ObjectId(taskId.toString()), barcodeString,
|
||||
task.getCompletedBy());
|
||||
|
||||
barcodeRepository.save(barcodeEntry);
|
||||
}
|
||||
|
||||
extraDataSummary = barcodes.size() + " Barcode(s) gescannt: " + String.join(", ", barcodes.subList(0, Math.min(3, barcodes.size()))) + (barcodes.size() > 3 ? "..." : "");
|
||||
extraDataSummary = barcodes.size() + " Barcode(s) gescannt: "
|
||||
+ String.join(", ", barcodes.subList(0, Math.min(3, barcodes.size())))
|
||||
+ (barcodes.size() > 3 ? "..." : "");
|
||||
log.info("Saved {} barcodes for taskId={}", barcodes.size(), taskId);
|
||||
} else {
|
||||
extraDataSummary = "Keine Barcodes gescannt";
|
||||
@@ -315,11 +325,8 @@ public class MessageController {
|
||||
Object signatureSvgObj = extraData.get("signatureSvg");
|
||||
if (signatureSvgObj instanceof String signatureSvg) {
|
||||
if (!signatureSvg.isBlank()) {
|
||||
Signature signatureEntry = new Signature(
|
||||
new ObjectId(taskId.toString()),
|
||||
signatureSvg,
|
||||
task.getCompletedBy()
|
||||
);
|
||||
Signature signatureEntry = new Signature(new ObjectId(taskId.toString()), signatureSvg,
|
||||
task.getCompletedBy());
|
||||
|
||||
signatureRepository.save(signatureEntry);
|
||||
extraDataSummary = "Unterschrift erfasst (SVG, " + signatureSvg.length() + " Zeichen)";
|
||||
@@ -366,12 +373,9 @@ public class MessageController {
|
||||
List<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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -16,4 +16,3 @@ public class Invoice {
|
||||
private double betrag;
|
||||
private String beschreibung;
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,16 +18,16 @@ public class User {
|
||||
|
||||
private int usrId;
|
||||
private String title;
|
||||
private String name; // Nachname
|
||||
private String firstname; // Vorname
|
||||
private String name; // Nachname
|
||||
private String firstname; // Vorname
|
||||
|
||||
// Firmen-/Adressdaten
|
||||
private String company; // Firma
|
||||
private String street; // Straße
|
||||
private String houseNumber; // Hausnr
|
||||
private String company; // Firma
|
||||
private String street; // Straße
|
||||
private String houseNumber; // Hausnr
|
||||
private String addressAddition; // Adresszusatz (optional)
|
||||
private String zip; // Postleitzahl
|
||||
private String city; // Stadt
|
||||
private String zip; // Postleitzahl
|
||||
private String city; // Stadt
|
||||
|
||||
@Indexed(unique = true)
|
||||
private String email;
|
||||
|
||||
@@ -17,13 +17,11 @@ import java.time.LocalDateTime;
|
||||
@NoArgsConstructor
|
||||
@Document(collection = "tasks")
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "taskType")
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = ConfirmationTask.class, name = "CONFIRMATION"),
|
||||
@JsonSubTypes.Type(value = SignatureTask.class, name = "SIGNATURE"),
|
||||
@JsonSubTypes.Type(value = TodoListTask.class, name = "TODOLIST"),
|
||||
@JsonSubTypes.Type(value = PhotoTask.class, name = "PHOTO"),
|
||||
@JsonSubTypes.Type(value = BarcodeTask.class, name = "BARCODE")
|
||||
})
|
||||
@JsonSubTypes({ @JsonSubTypes.Type(value = ConfirmationTask.class, name = "CONFIRMATION"),
|
||||
@JsonSubTypes.Type(value = SignatureTask.class, name = "SIGNATURE"),
|
||||
@JsonSubTypes.Type(value = TodoListTask.class, name = "TODOLIST"),
|
||||
@JsonSubTypes.Type(value = PhotoTask.class, name = "PHOTO"),
|
||||
@JsonSubTypes.Type(value = BarcodeTask.class, name = "BARCODE") })
|
||||
public abstract class BaseTask {
|
||||
@Id
|
||||
@JsonIgnore
|
||||
|
||||
@@ -9,7 +9,6 @@ import lombok.NoArgsConstructor;
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SignatureTask extends BaseTask {
|
||||
|
||||
|
||||
@Override
|
||||
public String getTaskType() {
|
||||
return "SIGNATURE";
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -192,9 +183,9 @@ public class MqttV5ClientManager implements SmartLifecycle {
|
||||
|
||||
private MqttQos mapQos(int q) {
|
||||
return switch (q) {
|
||||
case 0 -> MqttQos.AT_MOST_ONCE;
|
||||
case 1 -> MqttQos.AT_LEAST_ONCE;
|
||||
default -> MqttQos.EXACTLY_ONCE;
|
||||
case 0 -> MqttQos.AT_MOST_ONCE;
|
||||
case 1 -> MqttQos.AT_LEAST_ONCE;
|
||||
default -> MqttQos.EXACTLY_ONCE;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -204,12 +195,7 @@ public class MqttV5ClientManager implements SmartLifecycle {
|
||||
log.warn("[MQTT] Not connected, dropping publish topic={}", topic);
|
||||
return;
|
||||
}
|
||||
client.publishWith()
|
||||
.topic(topic)
|
||||
.payload(payload)
|
||||
.qos(mapQos(qos))
|
||||
.retain(retained)
|
||||
.send()
|
||||
client.publishWith().topic(topic).payload(payload).qos(mapQos(qos)).retain(retained).send()
|
||||
.whenComplete((ack, ex) -> {
|
||||
if (ex != null) {
|
||||
log.error("Failed to publish to {}: {}", topic, ex.getMessage(), ex);
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -5,5 +5,4 @@ import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
|
||||
public interface AddCompanyRepository extends MongoRepository<Company, String> {
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -6,5 +6,4 @@ import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
|
||||
public interface AddCustomerRepository extends MongoRepository<Customer, ObjectId> {
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -7,7 +7,5 @@ import java.util.Optional;
|
||||
|
||||
public interface LoginRepository extends MongoRepository<User, String> {
|
||||
|
||||
|
||||
|
||||
Optional<User> findByEmail(String email);
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@ public class AddCompanyService {
|
||||
this.addCompanyRepository = addCompanyRepository;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void addCompany(Company company) {
|
||||
addCompanyRepository.save(company);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,10 +80,9 @@ public class AddJobService {
|
||||
|
||||
// Tasks separat speichern und referenzieren mit korrekter Nummerierung
|
||||
if (transientTasks != null && !transientTasks.isEmpty()) {
|
||||
var filteredTasks = transientTasks.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.filter(task -> task.getTaskType() != null) // Filter nach TaskType statt Text
|
||||
.toList();
|
||||
var filteredTasks = transientTasks.stream().filter(Objects::nonNull)
|
||||
.filter(task -> task.getTaskType() != null) // Filter nach TaskType statt Text
|
||||
.toList();
|
||||
|
||||
// Setze JobId und stelle sicher, dass taskOrder korrekt ist
|
||||
for (int i = 0; i < filteredTasks.size(); i++) {
|
||||
@@ -113,7 +114,8 @@ public class AddJobService {
|
||||
try {
|
||||
emailService.sendJobCreationNotification(savedJob.getId(), savedJob.getCreatedBy());
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to send job creation email notification for job {}: {}", savedJob.getIdAsString(), e.getMessage());
|
||||
log.warn("Failed to send job creation email notification for job {}: {}", savedJob.getIdAsString(),
|
||||
e.getMessage());
|
||||
}
|
||||
|
||||
log.info("Auftrag erfolgreich gespeichert: {}", savedJob.getJobNumber());
|
||||
@@ -135,8 +137,7 @@ public class AddJobService {
|
||||
// Zähle Aufträge des aktuellen Tages
|
||||
String todayPrefix = prefix + timestamp;
|
||||
long todayCount = jobRepository.findAll().stream()
|
||||
.filter(job -> job.getJobNumber() != null && job.getJobNumber().startsWith(todayPrefix))
|
||||
.count();
|
||||
.filter(job -> job.getJobNumber() != null && job.getJobNumber().startsWith(todayPrefix)).count();
|
||||
|
||||
// Generiere neue Nummer
|
||||
String jobNumber;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -26,8 +26,6 @@ public class CustomerService {
|
||||
return todoRepository.findAllBy(pageable).toList();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public List<Customer> findAll() {
|
||||
return todoRepository.findAll();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import de.assecutor.votianlt.model.AppUser;
|
||||
import de.assecutor.votianlt.model.User;
|
||||
import de.assecutor.votianlt.repository.AppUserRepository;
|
||||
import de.assecutor.votianlt.repository.UserRepository;
|
||||
import de.assecutor.votianlt.util.MailUtil;
|
||||
import de.assecutor.votianlt.service.EmailService;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -17,33 +17,35 @@ import java.util.Optional;
|
||||
@Service
|
||||
public class PasswordResetService {
|
||||
|
||||
public enum UserType { USERS, APP_USER }
|
||||
public enum UserType {
|
||||
USERS, APP_USER
|
||||
}
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final AppUserRepository appUserRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final MailUtil mailUtil;
|
||||
private final EmailService emailService;
|
||||
private static final Duration TOKEN_VALIDITY = Duration.ofMinutes(15);
|
||||
|
||||
public PasswordResetService(UserRepository userRepository,
|
||||
AppUserRepository appUserRepository,
|
||||
PasswordEncoder passwordEncoder,
|
||||
MailUtil mailUtil) {
|
||||
public PasswordResetService(UserRepository userRepository, AppUserRepository appUserRepository,
|
||||
PasswordEncoder passwordEncoder, EmailService emailService) {
|
||||
this.userRepository = userRepository;
|
||||
this.appUserRepository = appUserRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.mailUtil = mailUtil;
|
||||
this.emailService = emailService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate reset without asking for user type. Looks up the email in both collections
|
||||
* and only proceeds if it exists in exactly one of them. Otherwise, it silently returns
|
||||
* to avoid leaking account existence.
|
||||
* Initiate reset without asking for user type. Looks up the email in both
|
||||
* collections and only proceeds if it exists in exactly one of them. Otherwise,
|
||||
* it silently returns to avoid leaking account existence.
|
||||
*/
|
||||
public void initiateResetAuto(String email, String baseUrl) {
|
||||
if (email == null) return;
|
||||
if (email == null)
|
||||
return;
|
||||
String normalized = email.trim();
|
||||
if (normalized.isEmpty()) return;
|
||||
if (normalized.isEmpty())
|
||||
return;
|
||||
var userOpt = userRepository.findByEmail(normalized);
|
||||
var appUser = appUserRepository.findByEmail(normalized);
|
||||
boolean inUsers = userOpt.isPresent();
|
||||
@@ -63,75 +65,80 @@ public class PasswordResetService {
|
||||
String link = baseUrl + "/forget-password?token=" + token + "&type=" + typeParam;
|
||||
|
||||
switch (userType) {
|
||||
case USERS -> {
|
||||
Optional<User> optional = userRepository.findByEmail(email);
|
||||
if (optional.isEmpty()) {
|
||||
// Do not leak existence; simply return
|
||||
return;
|
||||
}
|
||||
User user = optional.get();
|
||||
user.setPasswordCode(token);
|
||||
user.setPasswordTimestamp(now);
|
||||
userRepository.save(user);
|
||||
sendMail(email, link);
|
||||
case USERS -> {
|
||||
Optional<User> optional = userRepository.findByEmail(email);
|
||||
if (optional.isEmpty()) {
|
||||
// Do not leak existence; simply return
|
||||
return;
|
||||
}
|
||||
case APP_USER -> {
|
||||
AppUser appUser = appUserRepository.findByEmail(email);
|
||||
if (appUser == null) {
|
||||
return;
|
||||
}
|
||||
appUser.setPasswordCode(token);
|
||||
appUser.setPasswordTimestamp(now);
|
||||
appUserRepository.save(appUser);
|
||||
sendMail(email, link);
|
||||
User user = optional.get();
|
||||
user.setPasswordCode(token);
|
||||
user.setPasswordTimestamp(now);
|
||||
userRepository.save(user);
|
||||
sendMail(email, link);
|
||||
}
|
||||
case APP_USER -> {
|
||||
AppUser appUser = appUserRepository.findByEmail(email);
|
||||
if (appUser == null) {
|
||||
return;
|
||||
}
|
||||
appUser.setPasswordCode(token);
|
||||
appUser.setPasswordTimestamp(now);
|
||||
appUserRepository.save(appUser);
|
||||
sendMail(email, link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void sendMail(String to, String link) {
|
||||
String subject = "Passwort zurücksetzen";
|
||||
String body = "Hallo,\n\n" +
|
||||
"Sie haben eine Anfrage zum Zurücksetzen Ihres Passworts gestellt. " +
|
||||
"Dieser Link ist 15 Minuten gültig:\n" + link + "\n\n" +
|
||||
"Wenn Sie diese Anfrage nicht gestellt haben, können Sie diese E-Mail ignorieren.";
|
||||
String body = "Hallo,\n\n" + "Sie haben eine Anfrage zum Zurücksetzen Ihres Passworts gestellt. "
|
||||
+ "Dieser Link ist 15 Minuten gültig:\n" + link + "\n\n"
|
||||
+ "Wenn Sie diese Anfrage nicht gestellt haben, können Sie diese E-Mail ignorieren.";
|
||||
try {
|
||||
mailUtil.sendMail(to, subject, body);
|
||||
emailService.sendSimpleEmail(to, subject, body);
|
||||
} catch (Exception e) {
|
||||
// In this minimal implementation we swallow to avoid leaking details to attackers
|
||||
// In this minimal implementation we swallow to avoid leaking details to
|
||||
// attackers
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isTokenValid(String token, UserType userType) {
|
||||
LocalDateTime ts = switch (userType) {
|
||||
case USERS -> userRepository.findByPasswordCode(token).map(User::getPasswordTimestamp).orElse(null);
|
||||
case APP_USER -> Optional.ofNullable(appUserRepository.findByPasswordCode(token)).map(AppUser::getPasswordTimestamp).orElse(null);
|
||||
case USERS -> userRepository.findByPasswordCode(token).map(User::getPasswordTimestamp).orElse(null);
|
||||
case APP_USER -> Optional.ofNullable(appUserRepository.findByPasswordCode(token))
|
||||
.map(AppUser::getPasswordTimestamp).orElse(null);
|
||||
};
|
||||
if (ts == null) return false;
|
||||
if (ts == null)
|
||||
return false;
|
||||
return Duration.between(ts, LocalDateTime.now()).compareTo(TOKEN_VALIDITY) <= 0;
|
||||
}
|
||||
|
||||
public boolean resetPassword(String token, UserType userType, String newPassword) {
|
||||
if (!isTokenValid(token, userType)) return false;
|
||||
if (!isTokenValid(token, userType))
|
||||
return false;
|
||||
switch (userType) {
|
||||
case USERS -> {
|
||||
Optional<User> optional = userRepository.findByPasswordCode(token);
|
||||
if (optional.isEmpty()) return false;
|
||||
User user = optional.get();
|
||||
user.setPassword(passwordEncoder.encode(newPassword));
|
||||
user.setPasswordCode(null);
|
||||
user.setPasswordTimestamp(null);
|
||||
userRepository.save(user);
|
||||
return true;
|
||||
}
|
||||
case APP_USER -> {
|
||||
AppUser appUser = appUserRepository.findByPasswordCode(token);
|
||||
if (appUser == null) return false;
|
||||
appUser.setPassword(passwordEncoder.encode(newPassword));
|
||||
appUser.setPasswordCode(null);
|
||||
appUser.setPasswordTimestamp(null);
|
||||
appUserRepository.save(appUser);
|
||||
return true;
|
||||
}
|
||||
case USERS -> {
|
||||
Optional<User> optional = userRepository.findByPasswordCode(token);
|
||||
if (optional.isEmpty())
|
||||
return false;
|
||||
User user = optional.get();
|
||||
user.setPassword(passwordEncoder.encode(newPassword));
|
||||
user.setPasswordCode(null);
|
||||
user.setPasswordTimestamp(null);
|
||||
userRepository.save(user);
|
||||
return true;
|
||||
}
|
||||
case APP_USER -> {
|
||||
AppUser appUser = appUserRepository.findByPasswordCode(token);
|
||||
if (appUser == null)
|
||||
return false;
|
||||
appUser.setPassword(passwordEncoder.encode(newPassword));
|
||||
appUser.setPasswordCode(null);
|
||||
appUser.setPasswordTimestamp(null);
|
||||
appUserRepository.save(appUser);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -18,4 +18,5 @@ public class RegisterService {
|
||||
|
||||
public void registerUser(User user) {
|
||||
registerRepository.save(user);
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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")
|
||||
.withValidator(confirmPassword -> confirmPassword.equals(passwordField.getValue()),
|
||||
"Passwörter stimmen nicht überein")
|
||||
.bind(
|
||||
appUser -> "", // Dummy getter - this field is not stored
|
||||
(appUser, value) -> {} // Dummy setter - this field is not stored
|
||||
);
|
||||
binder.forField(deviceComboBox)
|
||||
.asRequired("Bitte ein Gerät auswählen")
|
||||
.bind(
|
||||
appUser -> null, // Initialwert, wird beim Erstellen gesetzt
|
||||
(appUser, appDevice) -> appUser.setAppDeviceId(appDevice != null ? appDevice.getId() : null)
|
||||
);
|
||||
binder.forField(confirmPasswordField).asRequired("Passwort wiederholen ist erforderlich")
|
||||
.withValidator(confirmPassword -> confirmPassword.equals(passwordField.getValue()),
|
||||
"Passwörter stimmen nicht überein")
|
||||
.bind(appUser -> "", // Dummy getter - this field is not stored
|
||||
(appUser, value) -> {
|
||||
} // Dummy setter - this field is not stored
|
||||
);
|
||||
binder.forField(deviceComboBox).asRequired("Bitte ein Gerät auswählen").bind(appUser -> null, // Initialwert,
|
||||
// wird beim
|
||||
// Erstellen
|
||||
// gesetzt
|
||||
(appUser, appDevice) -> appUser.setAppDeviceId(appDevice != null ? appDevice.getId() : null));
|
||||
}
|
||||
|
||||
private void createAppUser() {
|
||||
@@ -191,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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -22,7 +22,8 @@ import java.time.Clock;
|
||||
|
||||
@Route(value = "add-customer", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
|
||||
@PageTitle("Neuen Kunden anlegen")
|
||||
//@Menu(order = 0, icon = "vaadin:clipboard-check", title = "Neuen Kunden anlegen")
|
||||
// @Menu(order = 0, icon = "vaadin:clipboard-check", title = "Neuen Kunden
|
||||
// anlegen")
|
||||
@RolesAllowed("USER")
|
||||
public class AddCustomerView extends Main {
|
||||
private final AddCustomerService addCustomerService;
|
||||
@@ -151,54 +152,38 @@ public class AddCustomerView extends Main {
|
||||
}
|
||||
|
||||
private void configureBinder() {
|
||||
binder.forField(companyName)
|
||||
.asRequired("Firma ist ein Pflichtfeld")
|
||||
.bind(Customer::getCompanyName, Customer::setCompanyName);
|
||||
binder.forField(companyName).asRequired("Firma ist ein Pflichtfeld").bind(Customer::getCompanyName,
|
||||
Customer::setCompanyName);
|
||||
|
||||
binder.forField(title)
|
||||
.bind(Customer::getTitle, Customer::setTitle);
|
||||
binder.forField(title).bind(Customer::getTitle, Customer::setTitle);
|
||||
|
||||
binder.forField(firstName)
|
||||
.asRequired("Vorname ist ein Pflichtfeld")
|
||||
.bind(Customer::getFirstname, Customer::setFirstname);
|
||||
binder.forField(firstName).asRequired("Vorname ist ein Pflichtfeld").bind(Customer::getFirstname,
|
||||
Customer::setFirstname);
|
||||
|
||||
binder.forField(lastName)
|
||||
.asRequired("Nachname ist ein Pflichtfeld")
|
||||
.bind(Customer::getLastName, Customer::setLastName);
|
||||
binder.forField(lastName).asRequired("Nachname ist ein Pflichtfeld").bind(Customer::getLastName,
|
||||
Customer::setLastName);
|
||||
|
||||
binder.forField(telephone)
|
||||
.asRequired("Telefonnummer ist ein Pflichtfeld")
|
||||
.bind(Customer::getTelephone, Customer::setTelephone);
|
||||
binder.forField(telephone).asRequired("Telefonnummer ist ein Pflichtfeld").bind(Customer::getTelephone,
|
||||
Customer::setTelephone);
|
||||
|
||||
binder.forField(fax)
|
||||
.bind(Customer::getFax, Customer::setFax);
|
||||
binder.forField(fax).bind(Customer::getFax, Customer::setFax);
|
||||
|
||||
binder.forField(mail)
|
||||
.asRequired("E-Mail-Adresse ist ein Pflichtfeld")
|
||||
binder.forField(mail).asRequired("E-Mail-Adresse ist ein Pflichtfeld")
|
||||
.withValidator(email -> email.contains("@"), "Bitte geben Sie eine gültige E-Mail-Adresse ein")
|
||||
.bind(Customer::getMail, Customer::setMail);
|
||||
|
||||
binder.forField(street)
|
||||
.asRequired("Straße ist ein Pflichtfeld")
|
||||
.bind(Customer::getStreet, Customer::setStreet);
|
||||
binder.forField(street).asRequired("Straße ist ein Pflichtfeld").bind(Customer::getStreet, Customer::setStreet);
|
||||
|
||||
binder.forField(houseNumber)
|
||||
.asRequired("Hausnummer ist ein Pflichtfeld")
|
||||
.bind(Customer::getHouseNumber, Customer::setHouseNumber);
|
||||
binder.forField(houseNumber).asRequired("Hausnummer ist ein Pflichtfeld").bind(Customer::getHouseNumber,
|
||||
Customer::setHouseNumber);
|
||||
|
||||
binder.forField(addressAddition)
|
||||
.bind(Customer::getAddressAddition, Customer::setAddressAddition);
|
||||
binder.forField(addressAddition).bind(Customer::getAddressAddition, Customer::setAddressAddition);
|
||||
|
||||
binder.forField(zip)
|
||||
.asRequired("Postleitzahl ist ein Pflichtfeld")
|
||||
.bind(Customer::getZip, Customer::setZip);
|
||||
binder.forField(zip).asRequired("Postleitzahl ist ein Pflichtfeld").bind(Customer::getZip, Customer::setZip);
|
||||
|
||||
binder.forField(city)
|
||||
.asRequired("Ort ist ein Pflichtfeld")
|
||||
.bind(Customer::getCity, Customer::setCity);
|
||||
binder.forField(city).asRequired("Ort ist ein Pflichtfeld").bind(Customer::getCity, Customer::setCity);
|
||||
}
|
||||
|
||||
|
||||
private void setTestData() {
|
||||
companyName.setValue("Mustermann Transport GmbH");
|
||||
title.setValue("Herr");
|
||||
@@ -213,6 +198,7 @@ public class AddCustomerView extends Main {
|
||||
zip.setValue("20095");
|
||||
city.setValue("Hamburg");
|
||||
}
|
||||
|
||||
private void submit() {
|
||||
try {
|
||||
Customer customer = new Customer();
|
||||
@@ -221,20 +207,17 @@ public class AddCustomerView extends Main {
|
||||
addCustomerService.addCustomer(customer);
|
||||
|
||||
// Erfolg anzeigen und zur Kundenliste navigieren
|
||||
com.vaadin.flow.component.notification.Notification.show(
|
||||
"Kunde erfolgreich angelegt", 3000,
|
||||
com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER);
|
||||
com.vaadin.flow.component.notification.Notification.show("Kunde erfolgreich angelegt", 3000,
|
||||
com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER);
|
||||
|
||||
getUI().ifPresent(ui -> ui.navigate("customers"));
|
||||
|
||||
} catch (ValidationException e) {
|
||||
com.vaadin.flow.component.notification.Notification.show(
|
||||
"Bitte überprüfen Sie Ihre Eingaben", 3000,
|
||||
com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER);
|
||||
com.vaadin.flow.component.notification.Notification.show("Bitte überprüfen Sie Ihre Eingaben", 3000,
|
||||
com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER);
|
||||
} catch (Exception e) {
|
||||
com.vaadin.flow.component.notification.Notification.show(
|
||||
"Fehler beim Speichern: " + e.getMessage(), 5000,
|
||||
com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER);
|
||||
com.vaadin.flow.component.notification.Notification.show("Fehler beim Speichern: " + e.getMessage(), 5000,
|
||||
com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
String normalized = s.replace(" ", "").replace(".", "").replace(',', '.');
|
||||
try { return new java.math.BigDecimal(normalized); }
|
||||
catch (NumberFormatException ex) { throw new NumberFormatException("Ungültiger Betrag"); }
|
||||
},
|
||||
(java.math.BigDecimal bd) -> bd == null ? "" : bd.toString(),
|
||||
"Ungültiger Betrag"
|
||||
)
|
||||
// Bind price field: Komma-Zahlen in Punkt-Zahlen umsetzen, dann nach BigDecimal
|
||||
// konvertieren
|
||||
binder.forField(price).withNullRepresentation("").asRequired("Preis erforderlich").withConverter((String s) -> {
|
||||
if (s == null || s.trim().isEmpty())
|
||||
return null;
|
||||
String normalized = s.replace(" ", "").replace(".", "").replace(',', '.');
|
||||
try {
|
||||
return new java.math.BigDecimal(normalized);
|
||||
} catch (NumberFormatException ex) {
|
||||
throw new NumberFormatException("Ungültiger Betrag");
|
||||
}
|
||||
}, (java.math.BigDecimal bd) -> bd == null ? "" : bd.toString(), "Ungültiger Betrag")
|
||||
.withValidator(value -> value != null && value.compareTo(java.math.BigDecimal.ZERO) > 0,
|
||||
"Der Preis muss größer als 0 sein")
|
||||
"Der Preis muss größer als 0 sein")
|
||||
.bind(Job::getPrice, Job::setPrice);
|
||||
|
||||
// Bind date picker fields with validation
|
||||
binder.forField(pickupDate)
|
||||
.asRequired("")
|
||||
.bind(Job::getPickupDate, Job::setPickupDate);
|
||||
binder.forField(pickupDate).asRequired("").bind(Job::getPickupDate, Job::setPickupDate);
|
||||
|
||||
binder.forField(deliveryDate)
|
||||
.asRequired("")
|
||||
.bind(Job::getDeliveryDate, Job::setDeliveryDate);
|
||||
binder.forField(deliveryDate).asRequired("").bind(Job::getDeliveryDate, Job::setDeliveryDate);
|
||||
|
||||
// Bind customerSelection field with validation
|
||||
binder.forField(customerSelection)
|
||||
.asRequired("")
|
||||
.bind(Job::getCustomerSelection, Job::setCustomerSelection);
|
||||
binder.forField(customerSelection).asRequired("").bind(Job::getCustomerSelection, Job::setCustomerSelection);
|
||||
|
||||
// Bind optional fields without validation
|
||||
binder.bind(pickupCompany, Job::getPickupCompany, Job::setPickupCompany);
|
||||
@@ -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(
|
||||
// Convert AppUser to String (ID)
|
||||
user -> user != null ? user.getId().toHexString() : null,
|
||||
// Convert String (ID) back to AppUser
|
||||
id -> {
|
||||
if (id == null || id.trim().isEmpty()) return null;
|
||||
return availableAppUsers.stream()
|
||||
.filter(user -> user.getId().toHexString().equals(id))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
)
|
||||
binder.forField(appUser).withConverter(
|
||||
// Convert AppUser to String (ID)
|
||||
user -> user != null ? user.getId().toHexString() : null,
|
||||
// Convert String (ID) back to AppUser
|
||||
id -> {
|
||||
if (id == null || id.trim().isEmpty())
|
||||
return null;
|
||||
return availableAppUsers.stream().filter(user -> user.getId().toHexString().equals(id)).findFirst()
|
||||
.orElse(null);
|
||||
})
|
||||
// Require App-Nutzer when digital processing is enabled
|
||||
.withValidator(
|
||||
selectedUserId -> {
|
||||
boolean digital = Boolean.TRUE.equals(digitalProcessing.getValue());
|
||||
boolean hasUser = selectedUserId != null && !selectedUserId.trim().isEmpty();
|
||||
return !digital || hasUser;
|
||||
},
|
||||
"Bitte App-Nutzer auswählen, wenn Digitale Abwicklung aktiv ist"
|
||||
)
|
||||
.withValidator(selectedUserId -> {
|
||||
boolean digital = Boolean.TRUE.equals(digitalProcessing.getValue());
|
||||
boolean hasUser = selectedUserId != null && !selectedUserId.trim().isEmpty();
|
||||
return !digital || hasUser;
|
||||
}, "Bitte App-Nutzer auswählen, wenn Digitale Abwicklung aktiv ist")
|
||||
.bind(Job::getAppUser, Job::setAppUser);
|
||||
|
||||
// Toggle required indicator for App-Nutzer based on digitalProcessing
|
||||
@@ -846,16 +860,12 @@ public class AddJobView extends Main {
|
||||
|
||||
private void setupValidationTriggers() {
|
||||
// List of all required fields
|
||||
TextField[] requiredTextFields = {
|
||||
pickupFirstName, pickupLastName, pickupStreet, pickupHouseNumber, pickupZip, pickupCity,
|
||||
deliveryFirstName, deliveryLastName, deliveryStreet, deliveryHouseNumber, deliveryZip, deliveryCity,
|
||||
price
|
||||
};
|
||||
TextField[] requiredTextFields = { pickupFirstName, pickupLastName, pickupStreet, pickupHouseNumber, pickupZip,
|
||||
pickupCity, deliveryFirstName, deliveryLastName, deliveryStreet, deliveryHouseNumber, deliveryZip,
|
||||
deliveryCity, price };
|
||||
|
||||
// List of required date fields
|
||||
DatePicker[] requiredDateFields = {
|
||||
pickupDate, deliveryDate
|
||||
};
|
||||
DatePicker[] requiredDateFields = { pickupDate, deliveryDate };
|
||||
|
||||
// Add validation listener for customerSelection ComboBox
|
||||
customerSelection.addValueChangeListener(event -> {
|
||||
@@ -863,7 +873,8 @@ public class AddJobView extends Main {
|
||||
updateTabLabels();
|
||||
});
|
||||
|
||||
// Add value change listeners to trigger validation on every change for text fields
|
||||
// Add value change listeners to trigger validation on every change for text
|
||||
// fields
|
||||
for (TextField field : requiredTextFields) {
|
||||
field.addValueChangeListener(event -> {
|
||||
triggerValidation();
|
||||
@@ -949,17 +960,18 @@ public class AddJobView extends Main {
|
||||
|
||||
private boolean hasAddressValidationErrors() {
|
||||
// Check customer selection
|
||||
boolean customerSelectionEmpty = customerSelection.getValue() == null || customerSelection.getValue().trim().isEmpty();
|
||||
boolean customerSelectionEmpty = customerSelection.getValue() == null
|
||||
|| customerSelection.getValue().trim().isEmpty();
|
||||
|
||||
// Check pickup address fields
|
||||
boolean pickupErrors = isFieldEmpty(pickupFirstName) || isFieldEmpty(pickupLastName) ||
|
||||
isFieldEmpty(pickupStreet) || isFieldEmpty(pickupHouseNumber) ||
|
||||
isFieldEmpty(pickupZip) || isFieldEmpty(pickupCity);
|
||||
boolean pickupErrors = isFieldEmpty(pickupFirstName) || isFieldEmpty(pickupLastName)
|
||||
|| isFieldEmpty(pickupStreet) || isFieldEmpty(pickupHouseNumber) || isFieldEmpty(pickupZip)
|
||||
|| isFieldEmpty(pickupCity);
|
||||
|
||||
// Check delivery address fields
|
||||
boolean deliveryErrors = isFieldEmpty(deliveryFirstName) || isFieldEmpty(deliveryLastName) ||
|
||||
isFieldEmpty(deliveryStreet) || isFieldEmpty(deliveryHouseNumber) ||
|
||||
isFieldEmpty(deliveryZip) || isFieldEmpty(deliveryCity);
|
||||
boolean deliveryErrors = isFieldEmpty(deliveryFirstName) || isFieldEmpty(deliveryLastName)
|
||||
|| isFieldEmpty(deliveryStreet) || isFieldEmpty(deliveryHouseNumber) || isFieldEmpty(deliveryZip)
|
||||
|| isFieldEmpty(deliveryCity);
|
||||
|
||||
return customerSelectionEmpty || pickupErrors || deliveryErrors;
|
||||
}
|
||||
@@ -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());
|
||||
// Reset cargo error
|
||||
if (cargoError != null)
|
||||
cargoError.setVisible(false);
|
||||
if (cargoAreaContainer != null)
|
||||
cargoAreaContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)");
|
||||
Notification errorNotification = Notification.show("Fehler beim Erstellen des Auftrags: " + e.getMessage());
|
||||
errorNotification.setDuration(5000);
|
||||
}
|
||||
}
|
||||
@@ -1117,8 +1132,8 @@ public class AddJobView extends Main {
|
||||
loadJobIntoForm(draft);
|
||||
|
||||
// Benutzer informieren
|
||||
Notification notification = Notification.show(
|
||||
"Entwurf wiederhergestellt. Sie können Ihre Arbeit fortsetzen.");
|
||||
Notification notification = Notification
|
||||
.show("Entwurf wiederhergestellt. Sie können Ihre Arbeit fortsetzen.");
|
||||
notification.setDuration(4000);
|
||||
}
|
||||
}
|
||||
@@ -1163,13 +1178,15 @@ public class AddJobView extends Main {
|
||||
cargoList.setSpacing(true);
|
||||
cargoAreaContainer.add(cargoError, cargoList);
|
||||
|
||||
java.util.function.BiConsumer<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());
|
||||
}
|
||||
@@ -1558,11 +1579,11 @@ public class AddJobView extends Main {
|
||||
|
||||
private BaseTask createTaskByType(TaskType taskType) {
|
||||
return switch (taskType) {
|
||||
case CONFIRMATION -> new ConfirmationTask("");
|
||||
case SIGNATURE -> new SignatureTask();
|
||||
case TODOLIST -> new TodoListTask(new ArrayList<>());
|
||||
case PHOTO -> new PhotoTask(1, 10);
|
||||
case BARCODE -> new BarcodeTask(1, 10);
|
||||
case CONFIRMATION -> new ConfirmationTask("");
|
||||
case SIGNATURE -> new SignatureTask();
|
||||
case TODOLIST -> new TodoListTask(new ArrayList<>());
|
||||
case PHOTO -> new PhotoTask(1, 10);
|
||||
case BARCODE -> new BarcodeTask(1, 10);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1580,162 +1601,155 @@ 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:
|
||||
TextField buttonTextField = new TextField("Button-Text");
|
||||
buttonTextField.setPlaceholder("z.B. 'Bestätigen', 'Abgeschlossen'");
|
||||
buttonTextField.setWidthFull();
|
||||
ConfirmationTask confirmationTask = (ConfirmationTask) task;
|
||||
buttonTextField.setValue(confirmationTask.getButtonText() != null ?
|
||||
confirmationTask.getButtonText() : "");
|
||||
buttonTextField.addValueChangeListener(ev -> {
|
||||
// Find the current ConfirmationTask in tasksState and update it
|
||||
for (int i = 0; i < tasksState.size(); i++) {
|
||||
BaseTask stateTask = tasksState.get(i);
|
||||
if (stateTask == task && stateTask instanceof ConfirmationTask) {
|
||||
((ConfirmationTask) stateTask).setButtonText(ev.getValue());
|
||||
break;
|
||||
}
|
||||
case CONFIRMATION:
|
||||
TextField buttonTextField = new TextField("Button-Text");
|
||||
buttonTextField.setPlaceholder("z.B. 'Bestätigen', 'Abgeschlossen'");
|
||||
buttonTextField.setWidthFull();
|
||||
ConfirmationTask confirmationTask = (ConfirmationTask) task;
|
||||
buttonTextField.setValue(confirmationTask.getButtonText() != null ? confirmationTask.getButtonText() : "");
|
||||
buttonTextField.addValueChangeListener(ev -> {
|
||||
// Find the current ConfirmationTask in tasksState and update it
|
||||
for (int i = 0; i < tasksState.size(); i++) {
|
||||
BaseTask stateTask = tasksState.get(i);
|
||||
if (stateTask == task && stateTask instanceof ConfirmationTask) {
|
||||
((ConfirmationTask) stateTask).setButtonText(ev.getValue());
|
||||
break;
|
||||
}
|
||||
});
|
||||
configContainer.add(buttonTextField);
|
||||
break;
|
||||
}
|
||||
});
|
||||
configContainer.add(buttonTextField);
|
||||
break;
|
||||
|
||||
case SIGNATURE:
|
||||
// No additional configuration needed
|
||||
Span info = new Span("Keine zusätzliche Konfiguration erforderlich");
|
||||
info.getStyle().set("color", "var(--lumo-secondary-text-color)");
|
||||
info.getStyle().set("font-style", "italic");
|
||||
configContainer.add(info);
|
||||
break;
|
||||
case SIGNATURE:
|
||||
// No additional configuration needed
|
||||
Span info = new Span("Keine zusätzliche Konfiguration erforderlich");
|
||||
info.getStyle().set("color", "var(--lumo-secondary-text-color)");
|
||||
info.getStyle().set("font-style", "italic");
|
||||
configContainer.add(info);
|
||||
break;
|
||||
|
||||
case TODOLIST:
|
||||
VerticalLayout todoContainer = new VerticalLayout();
|
||||
todoContainer.setPadding(false);
|
||||
todoContainer.setSpacing(true);
|
||||
case TODOLIST:
|
||||
VerticalLayout todoContainer = new VerticalLayout();
|
||||
todoContainer.setPadding(false);
|
||||
todoContainer.setSpacing(true);
|
||||
|
||||
H3 todoTitle = new H3("To-Do Punkte");
|
||||
todoTitle.getStyle().set("margin", "0");
|
||||
todoContainer.add(todoTitle);
|
||||
H3 todoTitle = new H3("To-Do Punkte");
|
||||
todoTitle.getStyle().set("margin", "0");
|
||||
todoContainer.add(todoTitle);
|
||||
|
||||
// Dynamic todo list
|
||||
VerticalLayout todoList = new VerticalLayout();
|
||||
todoList.setPadding(false);
|
||||
todoList.setSpacing(true);
|
||||
// Dynamic todo list
|
||||
VerticalLayout todoList = new VerticalLayout();
|
||||
todoList.setPadding(false);
|
||||
todoList.setSpacing(true);
|
||||
|
||||
java.util.function.Consumer<Void> addTodoItem = (v) -> {
|
||||
HorizontalLayout todoRow = new HorizontalLayout();
|
||||
todoRow.setWidthFull();
|
||||
todoRow.setAlignItems(FlexComponent.Alignment.END);
|
||||
java.util.function.Consumer<Void> addTodoItem = (v) -> {
|
||||
HorizontalLayout todoRow = new HorizontalLayout();
|
||||
todoRow.setWidthFull();
|
||||
todoRow.setAlignItems(FlexComponent.Alignment.END);
|
||||
|
||||
TextField todoField = new TextField();
|
||||
todoField.setPlaceholder("To-Do Punkt");
|
||||
todoField.setWidth("100%");
|
||||
TextField todoField = new TextField();
|
||||
todoField.setPlaceholder("To-Do Punkt");
|
||||
todoField.setWidth("100%");
|
||||
|
||||
Button removeTodo = new Button(new Icon(VaadinIcon.CLOSE_SMALL));
|
||||
removeTodo.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
|
||||
removeTodo.addClickListener(e -> {
|
||||
todoList.remove(todoRow);
|
||||
updateTodoItems(todoList, task);
|
||||
});
|
||||
|
||||
todoRow.add(todoField, removeTodo);
|
||||
todoRow.setFlexGrow(1, todoField);
|
||||
todoList.add(todoRow);
|
||||
|
||||
todoField.addValueChangeListener(ev -> updateTodoItems(todoList, task));
|
||||
};
|
||||
|
||||
Button addTodoBtn = new Button("To-Do Punkt hinzufügen", new Icon(VaadinIcon.PLUS));
|
||||
addTodoBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
addTodoBtn.addClickListener(e -> addTodoItem.accept(null));
|
||||
|
||||
// Add initial todo item
|
||||
addTodoItem.accept(null);
|
||||
|
||||
todoContainer.add(todoList, addTodoBtn);
|
||||
configContainer.add(todoContainer);
|
||||
break;
|
||||
|
||||
case PHOTO:
|
||||
HorizontalLayout photoLayout = new HorizontalLayout();
|
||||
photoLayout.setWidthFull();
|
||||
photoLayout.setSpacing(true);
|
||||
|
||||
PhotoTask photoTask = (PhotoTask) task;
|
||||
IntegerField minPhotos = new IntegerField("Min. Anzahl Fotos");
|
||||
minPhotos.setPlaceholder("1");
|
||||
minPhotos.setMin(1);
|
||||
minPhotos.setValue(photoTask.getMinPhotoCount() != null ?
|
||||
photoTask.getMinPhotoCount() : 1);
|
||||
|
||||
IntegerField maxPhotos = new IntegerField("Max. Anzahl Fotos");
|
||||
maxPhotos.setPlaceholder("10");
|
||||
maxPhotos.setMin(1);
|
||||
maxPhotos.setValue(photoTask.getMaxPhotoCount() != null ?
|
||||
photoTask.getMaxPhotoCount() : 10);
|
||||
|
||||
photoLayout.add(minPhotos, maxPhotos);
|
||||
|
||||
minPhotos.addValueChangeListener(ev -> {
|
||||
photoTask.setMinPhotoCount(ev.getValue());
|
||||
Button removeTodo = new Button(new Icon(VaadinIcon.CLOSE_SMALL));
|
||||
removeTodo.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
|
||||
removeTodo.addClickListener(e -> {
|
||||
todoList.remove(todoRow);
|
||||
updateTodoItems(todoList, task);
|
||||
});
|
||||
|
||||
maxPhotos.addValueChangeListener(ev -> {
|
||||
photoTask.setMaxPhotoCount(ev.getValue());
|
||||
});
|
||||
todoRow.add(todoField, removeTodo);
|
||||
todoRow.setFlexGrow(1, todoField);
|
||||
todoList.add(todoRow);
|
||||
|
||||
configContainer.add(photoLayout);
|
||||
break;
|
||||
todoField.addValueChangeListener(ev -> updateTodoItems(todoList, task));
|
||||
};
|
||||
|
||||
case BARCODE:
|
||||
HorizontalLayout barcodeLayout = new HorizontalLayout();
|
||||
barcodeLayout.setWidthFull();
|
||||
barcodeLayout.setSpacing(true);
|
||||
Button addTodoBtn = new Button("To-Do Punkt hinzufügen", new Icon(VaadinIcon.PLUS));
|
||||
addTodoBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
addTodoBtn.addClickListener(e -> addTodoItem.accept(null));
|
||||
|
||||
BarcodeTask barcodeTask = (BarcodeTask) task;
|
||||
IntegerField minBarcodes = new IntegerField("Min. Anzahl Barcodes");
|
||||
minBarcodes.setPlaceholder("1");
|
||||
minBarcodes.setMin(1);
|
||||
minBarcodes.setValue(barcodeTask.getMinBarcodeCount() != null ?
|
||||
barcodeTask.getMinBarcodeCount() : 1);
|
||||
// Add initial todo item
|
||||
addTodoItem.accept(null);
|
||||
|
||||
IntegerField maxBarcodes = new IntegerField("Max. Anzahl Barcodes");
|
||||
maxBarcodes.setPlaceholder("10");
|
||||
maxBarcodes.setMin(1);
|
||||
maxBarcodes.setValue(barcodeTask.getMaxBarcodeCount() != null ?
|
||||
barcodeTask.getMaxBarcodeCount() : 10);
|
||||
todoContainer.add(todoList, addTodoBtn);
|
||||
configContainer.add(todoContainer);
|
||||
break;
|
||||
|
||||
barcodeLayout.add(minBarcodes, maxBarcodes);
|
||||
case PHOTO:
|
||||
HorizontalLayout photoLayout = new HorizontalLayout();
|
||||
photoLayout.setWidthFull();
|
||||
photoLayout.setSpacing(true);
|
||||
|
||||
minBarcodes.addValueChangeListener(ev -> {
|
||||
barcodeTask.setMinBarcodeCount(ev.getValue());
|
||||
});
|
||||
PhotoTask photoTask = (PhotoTask) task;
|
||||
IntegerField minPhotos = new IntegerField("Min. Anzahl Fotos");
|
||||
minPhotos.setPlaceholder("1");
|
||||
minPhotos.setMin(1);
|
||||
minPhotos.setValue(photoTask.getMinPhotoCount() != null ? photoTask.getMinPhotoCount() : 1);
|
||||
|
||||
maxBarcodes.addValueChangeListener(ev -> {
|
||||
barcodeTask.setMaxBarcodeCount(ev.getValue());
|
||||
});
|
||||
IntegerField maxPhotos = new IntegerField("Max. Anzahl Fotos");
|
||||
maxPhotos.setPlaceholder("10");
|
||||
maxPhotos.setMin(1);
|
||||
maxPhotos.setValue(photoTask.getMaxPhotoCount() != null ? photoTask.getMaxPhotoCount() : 10);
|
||||
|
||||
configContainer.add(barcodeLayout);
|
||||
break;
|
||||
photoLayout.add(minPhotos, maxPhotos);
|
||||
|
||||
minPhotos.addValueChangeListener(ev -> {
|
||||
photoTask.setMinPhotoCount(ev.getValue());
|
||||
});
|
||||
|
||||
maxPhotos.addValueChangeListener(ev -> {
|
||||
photoTask.setMaxPhotoCount(ev.getValue());
|
||||
});
|
||||
|
||||
configContainer.add(photoLayout);
|
||||
break;
|
||||
|
||||
case BARCODE:
|
||||
HorizontalLayout barcodeLayout = new HorizontalLayout();
|
||||
barcodeLayout.setWidthFull();
|
||||
barcodeLayout.setSpacing(true);
|
||||
|
||||
BarcodeTask barcodeTask = (BarcodeTask) task;
|
||||
IntegerField minBarcodes = new IntegerField("Min. Anzahl Barcodes");
|
||||
minBarcodes.setPlaceholder("1");
|
||||
minBarcodes.setMin(1);
|
||||
minBarcodes.setValue(barcodeTask.getMinBarcodeCount() != null ? barcodeTask.getMinBarcodeCount() : 1);
|
||||
|
||||
IntegerField maxBarcodes = new IntegerField("Max. Anzahl Barcodes");
|
||||
maxBarcodes.setPlaceholder("10");
|
||||
maxBarcodes.setMin(1);
|
||||
maxBarcodes.setValue(barcodeTask.getMaxBarcodeCount() != null ? barcodeTask.getMaxBarcodeCount() : 10);
|
||||
|
||||
barcodeLayout.add(minBarcodes, maxBarcodes);
|
||||
|
||||
minBarcodes.addValueChangeListener(ev -> {
|
||||
barcodeTask.setMinBarcodeCount(ev.getValue());
|
||||
});
|
||||
|
||||
maxBarcodes.addValueChangeListener(ev -> {
|
||||
barcodeTask.setMaxBarcodeCount(ev.getValue());
|
||||
});
|
||||
|
||||
configContainer.add(barcodeLayout);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTodoItems(VerticalLayout todoList, BaseTask task) {
|
||||
List<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())
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
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())
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
|
||||
if (task instanceof TodoListTask) {
|
||||
((TodoListTask) task).setTodoItems(todoItems);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -46,7 +46,8 @@ public class AuthenticatedStartView extends VerticalLayout {
|
||||
heroSection.setPadding(true);
|
||||
heroSection.setSpacing(true);
|
||||
heroSection.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||
heroSection.getStyle().set("background", "linear-gradient(135deg, var(--lumo-primary-color-10pct), var(--lumo-primary-color-50pct))");
|
||||
heroSection.getStyle().set("background",
|
||||
"linear-gradient(135deg, var(--lumo-primary-color-10pct), var(--lumo-primary-color-50pct))");
|
||||
heroSection.getStyle().set("min-height", "300px");
|
||||
heroSection.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
|
||||
|
||||
@@ -58,8 +59,7 @@ public class AuthenticatedStartView extends VerticalLayout {
|
||||
welcomeTitle.getStyle().set("margin-bottom", "var(--lumo-space-l)");
|
||||
|
||||
Paragraph welcomeDescription = new Paragraph(
|
||||
"Nutzen Sie die Navigation links, um neue Aufträge zu erstellen oder Ihre Verwaltung zu bearbeiten."
|
||||
);
|
||||
"Nutzen Sie die Navigation links, um neue Aufträge zu erstellen oder Ihre Verwaltung zu bearbeiten.");
|
||||
welcomeDescription.getStyle().set("text-align", "center");
|
||||
welcomeDescription.getStyle().set("max-width", "600px");
|
||||
welcomeDescription.getStyle().set("font-size", "var(--lumo-font-size-l)");
|
||||
@@ -82,9 +82,8 @@ public class AuthenticatedStartView extends VerticalLayout {
|
||||
systemTitle.getStyle().set("text-align", "center");
|
||||
|
||||
Paragraph systemIntro = new Paragraph(
|
||||
"Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe ist von entscheidender Bedeutung, " +
|
||||
"dass sie sich in erster Linie auf ihr eigentliches Geschäft konzentrieren können: Kunden gewinnen und Waren von A nach B liefern."
|
||||
);
|
||||
"Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe ist von entscheidender Bedeutung, "
|
||||
+ "dass sie sich in erster Linie auf ihr eigentliches Geschäft konzentrieren können: Kunden gewinnen und Waren von A nach B liefern.");
|
||||
systemIntro.getStyle().set("text-align", "center");
|
||||
systemIntro.getStyle().set("max-width", "800px");
|
||||
systemIntro.getStyle().set("margin-bottom", "var(--lumo-space-xl)");
|
||||
@@ -96,14 +95,12 @@ public class AuthenticatedStartView extends VerticalLayout {
|
||||
featuresGrid.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.START);
|
||||
|
||||
// Feature Cards
|
||||
featuresGrid.add(
|
||||
createFeatureCard(VaadinIcon.COG, "Einrichtungsassistent",
|
||||
featuresGrid.add(createFeatureCard(VaadinIcon.COG, "Einrichtungsassistent",
|
||||
"Mithilfe des Einrichtungsassistenten haben Sie die Möglichkeit, Ihr Nutzerprofil zu vervollständigen."),
|
||||
createFeatureCard(VaadinIcon.USERS, "Kunden- und Auftragsverwaltung",
|
||||
"Mit der Kunden- und Auftragsverwaltung haben Sie alle Kontaktdaten und Auftragsdetails stets im Blick."),
|
||||
createFeatureCard(VaadinIcon.CLIPBOARD_TEXT, "Auftragserstellung",
|
||||
"Stellen Sie mit wenigen Mausklicks Aufträge ins System ein und legen Sie fest, welcher Mitarbeiter welchen Transportauftrag abarbeiten soll.")
|
||||
);
|
||||
createFeatureCard(VaadinIcon.USERS, "Kunden- und Auftragsverwaltung",
|
||||
"Mit der Kunden- und Auftragsverwaltung haben Sie alle Kontaktdaten und Auftragsdetails stets im Blick."),
|
||||
createFeatureCard(VaadinIcon.CLIPBOARD_TEXT, "Auftragserstellung",
|
||||
"Stellen Sie mit wenigen Mausklicks Aufträge ins System ein und legen Sie fest, welcher Mitarbeiter welchen Transportauftrag abarbeiten soll."));
|
||||
|
||||
systemSection.add(systemTitle, systemIntro, featuresGrid);
|
||||
return systemSection;
|
||||
@@ -153,9 +150,8 @@ public class AuthenticatedStartView extends VerticalLayout {
|
||||
appTitle.getStyle().set("text-align", "center");
|
||||
|
||||
Paragraph appDescription = new Paragraph(
|
||||
"Mit unserer mobilen App bleiben Sie auch unterwegs immer über Ihre Aufträge informiert " +
|
||||
"und können wichtige Aufgaben direkt vom Smartphone aus erledigen."
|
||||
);
|
||||
"Mit unserer mobilen App bleiben Sie auch unterwegs immer über Ihre Aufträge informiert "
|
||||
+ "und können wichtige Aufgaben direkt vom Smartphone aus erledigen.");
|
||||
appDescription.getStyle().set("text-align", "center");
|
||||
appDescription.getStyle().set("max-width", "600px");
|
||||
|
||||
|
||||
@@ -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,25 +149,22 @@ 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 -> {
|
||||
if (appDevice.getAppUserId() != null) {
|
||||
return appUserService.findByCurrentUser().stream()
|
||||
.filter(user -> user.getId().equals(appDevice.getAppUserId()))
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
return null;
|
||||
}, (appDevice, appUser) -> {
|
||||
if (appUser != null) {
|
||||
appDevice.setAppUserId(appUser.getId());
|
||||
} else {
|
||||
appDevice.setAppUserId(null);
|
||||
}
|
||||
});
|
||||
binder.forField(appUserComboBox).bind(appDevice -> {
|
||||
if (appDevice.getAppUserId() != null) {
|
||||
return appUserService.findByCurrentUser().stream()
|
||||
.filter(user -> user.getId().equals(appDevice.getAppUserId())).findFirst().orElse(null);
|
||||
}
|
||||
return null;
|
||||
}, (appDevice, appUser) -> {
|
||||
if (appUser != null) {
|
||||
appDevice.setAppUserId(appUser.getId());
|
||||
} else {
|
||||
appDevice.setAppUserId(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void saveAppDevice() {
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -32,7 +32,7 @@ import jakarta.annotation.security.RolesAllowed;
|
||||
|
||||
@PageTitle("Profil bearbeiten")
|
||||
@Route(value = "edit-profile", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
|
||||
@RolesAllowed({"USER","ADMIN"})
|
||||
@RolesAllowed({ "USER", "ADMIN" })
|
||||
public class EditProfileView extends HorizontalLayout {
|
||||
private final TextField prefixField;
|
||||
private final TextField ustIdField;
|
||||
@@ -71,7 +71,6 @@ public class EditProfileView extends HorizontalLayout {
|
||||
tabSheet.setSizeFull();
|
||||
formColumn.setFlexGrow(1, tabSheet);
|
||||
|
||||
|
||||
FormLayout form = new FormLayout();
|
||||
form.setWidthFull();
|
||||
form.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 2));
|
||||
@@ -163,35 +162,22 @@ public class EditProfileView extends HorizontalLayout {
|
||||
zipField.setRequiredIndicatorVisible(true);
|
||||
cityField.setRequiredIndicatorVisible(true);
|
||||
|
||||
binder.forField(companyField)
|
||||
.asRequired("")
|
||||
.bind(user -> null, (user, v) -> {});
|
||||
binder.forField(streetField)
|
||||
.asRequired("")
|
||||
.bind(user -> null, (user, v) -> {});
|
||||
binder.forField(houseNumberField)
|
||||
.asRequired("")
|
||||
.bind(user -> null, (user, v) -> {});
|
||||
binder.forField(zipField)
|
||||
.asRequired("")
|
||||
.bind(user -> null, (user, v) -> {});
|
||||
binder.forField(cityField)
|
||||
.asRequired("")
|
||||
.bind(user -> null, (user, v) -> {});
|
||||
binder.forField(companyField).asRequired("").bind(user -> null, (user, v) -> {
|
||||
});
|
||||
binder.forField(streetField).asRequired("").bind(user -> null, (user, v) -> {
|
||||
});
|
||||
binder.forField(houseNumberField).asRequired("").bind(user -> null, (user, v) -> {
|
||||
});
|
||||
binder.forField(zipField).asRequired("").bind(user -> null, (user, v) -> {
|
||||
});
|
||||
binder.forField(cityField).asRequired("").bind(user -> null, (user, v) -> {
|
||||
});
|
||||
|
||||
binder.forField(firstnameField)
|
||||
.asRequired("")
|
||||
.bind(User::getFirstname, User::setFirstname);
|
||||
binder.forField(lastnameField)
|
||||
.asRequired("")
|
||||
.bind(User::getName, User::setName);
|
||||
binder.forField(phoneField)
|
||||
.asRequired("")
|
||||
.bind(User::getPhone, User::setPhone);
|
||||
binder.forField(emailField)
|
||||
.asRequired("")
|
||||
.withValidator(new EmailValidator("Ungültige E-Mail-Adresse"))
|
||||
.bind(User::getEmail, User::setEmail);
|
||||
binder.forField(firstnameField).asRequired("").bind(User::getFirstname, User::setFirstname);
|
||||
binder.forField(lastnameField).asRequired("").bind(User::getName, User::setName);
|
||||
binder.forField(phoneField).asRequired("").bind(User::getPhone, User::setPhone);
|
||||
binder.forField(emailField).asRequired("").withValidator(new EmailValidator("Ungültige E-Mail-Adresse"))
|
||||
.bind(User::getEmail, User::setEmail);
|
||||
// Optionale Felder
|
||||
binder.forField(mobileField).bind(User::getPhone2, User::setPhone2);
|
||||
binder.forField(faxField).bind(User::getFax, User::setFax);
|
||||
@@ -224,8 +210,8 @@ public class EditProfileView extends HorizontalLayout {
|
||||
mapDiv.setWidth("100%");
|
||||
mapDiv.setHeight("400px");
|
||||
mapDiv.getElement().setProperty("innerHTML",
|
||||
"<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,10 +285,8 @@ public class EditProfileView extends HorizontalLayout {
|
||||
Div previewWrapper = new Div();
|
||||
previewWrapper.setWidth("100%");
|
||||
previewWrapper.setHeight("650px");
|
||||
previewWrapper.getStyle()
|
||||
.set("overflow", "hidden")
|
||||
.set("background", "var(--lumo-contrast-10pct)")
|
||||
.set("padding", "0");
|
||||
previewWrapper.getStyle().set("overflow", "hidden").set("background", "var(--lumo-contrast-10pct)")
|
||||
.set("padding", "0");
|
||||
|
||||
// Initial noch keine PDF laden (erst bei aktiver Checkbox)
|
||||
pdfFrame = new IFrame();
|
||||
@@ -315,11 +299,9 @@ public class EditProfileView extends HorizontalLayout {
|
||||
|
||||
billingRight.add(previewTitle, previewWrapper);
|
||||
|
||||
|
||||
billingTab.add(billingLeft, billingRight);
|
||||
tabSheet.add("Rechnungsstellung", billingTab);
|
||||
|
||||
|
||||
// Zweiter Tab: Einstellungen (Beispiel mit Schaltern)
|
||||
VerticalLayout switches = new VerticalLayout();
|
||||
switches.setPadding(false);
|
||||
@@ -392,14 +374,22 @@ public class EditProfileView extends HorizontalLayout {
|
||||
// PDF neu rendern und iframe aktualisieren
|
||||
// Felder im Billing-Tab aktivieren/deaktivieren
|
||||
private void setBillingFieldsEnabled(boolean enabled) {
|
||||
if (prefixField != null) prefixField.setEnabled(enabled);
|
||||
if (ustIdField != null) ustIdField.setEnabled(enabled);
|
||||
if (taxNumberField != null) taxNumberField.setEnabled(enabled);
|
||||
if (bankNameField != null) bankNameField.setEnabled(enabled);
|
||||
if (ibanField != null) ibanField.setEnabled(enabled);
|
||||
if (taxRateField != null) taxRateField.setEnabled(enabled);
|
||||
if (introTextArea != null) introTextArea.setEnabled(enabled);
|
||||
if (termsTextArea != null) termsTextArea.setEnabled(enabled);
|
||||
if (prefixField != null)
|
||||
prefixField.setEnabled(enabled);
|
||||
if (ustIdField != null)
|
||||
ustIdField.setEnabled(enabled);
|
||||
if (taxNumberField != null)
|
||||
taxNumberField.setEnabled(enabled);
|
||||
if (bankNameField != null)
|
||||
bankNameField.setEnabled(enabled);
|
||||
if (ibanField != null)
|
||||
ibanField.setEnabled(enabled);
|
||||
if (taxRateField != null)
|
||||
taxRateField.setEnabled(enabled);
|
||||
if (introTextArea != null)
|
||||
introTextArea.setEnabled(enabled);
|
||||
if (termsTextArea != null)
|
||||
termsTextArea.setEnabled(enabled);
|
||||
}
|
||||
|
||||
// Checkbox steuert Aktivierung und PDF
|
||||
@@ -424,14 +414,14 @@ public class EditProfileView extends HorizontalLayout {
|
||||
private void refreshPdf() {
|
||||
byte[] bytes = generatePreviewPdf();
|
||||
String dataUrl = "data:application/pdf;base64," + Base64.getEncoder().encodeToString(bytes)
|
||||
+ "#toolbar=0&navpanes=0&statusbar=0&view=Fit&zoom=page-fit";
|
||||
+ "#toolbar=0&navpanes=0&statusbar=0&view=Fit&zoom=page-fit";
|
||||
if (pdfFrame != null) {
|
||||
pdfFrame.setSrc(dataUrl);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Einfache PDF-Vorschau generieren (kann später durch echte Logik ersetzt werden)
|
||||
// Einfache PDF-Vorschau generieren (kann später durch echte Logik ersetzt
|
||||
// werden)
|
||||
private byte[] generatePreviewPdf() {
|
||||
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
com.lowagie.text.Document document = new com.lowagie.text.Document();
|
||||
@@ -459,7 +449,12 @@ public class EditProfileView extends HorizontalLayout {
|
||||
}
|
||||
|
||||
// Utility: safe getter für TextField/TextArea
|
||||
private String safe(TextField f) { return f != null && f.getValue() != null ? f.getValue() : ""; }
|
||||
private String safe(TextArea f) { return f != null && f.getValue() != null ? f.getValue() : ""; }
|
||||
private String safe(TextField f) {
|
||||
return f != null && f.getValue() != null ? f.getValue() : "";
|
||||
}
|
||||
|
||||
private String safe(TextArea f) {
|
||||
return f != null && f.getValue() != null ? f.getValue() : "";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 "";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)")
|
||||
.set("border-left", "4px solid " + getTypeColor(entry.getChangeType()))
|
||||
.set("border-radius", "var(--lumo-border-radius-s)")
|
||||
.set("padding", "var(--lumo-space-m)")
|
||||
.set("margin-bottom", "var(--lumo-space-s)")
|
||||
.set("background-color", "var(--lumo-base-color)")
|
||||
.set("width", "100%")
|
||||
.set("box-sizing", "border-box");
|
||||
card.getStyle().set("border", "1px solid var(--lumo-contrast-10pct)")
|
||||
.set("border-left", "4px solid " + getTypeColor(entry.getChangeType()))
|
||||
.set("border-radius", "var(--lumo-border-radius-s)").set("padding", "var(--lumo-space-m)")
|
||||
.set("margin-bottom", "var(--lumo-space-s)").set("background-color", "var(--lumo-base-color)")
|
||||
.set("width", "100%").set("box-sizing", "border-box");
|
||||
|
||||
// Header row with icon, reason and timestamp
|
||||
HorizontalLayout headerRow = new HorizontalLayout();
|
||||
@@ -194,9 +189,8 @@ public class JobHistoryView extends Main implements HasUrlParameter<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)")
|
||||
.set("display", "block");
|
||||
description.getStyle().set("color", "var(--lumo-body-text-color)").set("margin-top", "var(--lumo-space-xs)")
|
||||
.set("display", "block");
|
||||
cardContent.add(description);
|
||||
}
|
||||
|
||||
// Photo preview for photo tasks
|
||||
if (entry.getChangeType() == JobHistoryType.TASK_COMPLETED &&
|
||||
entry.getDetails() != null &&
|
||||
entry.getDetails().contains("Task-Typ: PHOTO")) {
|
||||
if (entry.getChangeType() == JobHistoryType.TASK_COMPLETED && entry.getDetails() != null
|
||||
&& entry.getDetails().contains("Task-Typ: PHOTO")) {
|
||||
|
||||
HorizontalLayout photoPreview = createPhotoPreview(entry);
|
||||
if (photoPreview != null) {
|
||||
@@ -232,9 +223,8 @@ public class JobHistoryView extends Main implements HasUrlParameter<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,11 +245,9 @@ 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)")
|
||||
.set("display", "block");
|
||||
changedBy.getStyle().set("color", "var(--lumo-secondary-text-color)")
|
||||
.set("font-size", "var(--lumo-font-size-xs)").set("margin-top", "var(--lumo-space-xs)")
|
||||
.set("display", "block");
|
||||
cardContent.add(changedBy);
|
||||
}
|
||||
|
||||
@@ -270,44 +257,47 @@ public class JobHistoryView extends Main implements HasUrlParameter<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);
|
||||
case UPDATE -> new Icon(VaadinIcon.EDIT);
|
||||
case STATUS_CHANGE -> new Icon(VaadinIcon.ARROW_RIGHT);
|
||||
case TASK_COMPLETED -> new Icon(VaadinIcon.CHECK);
|
||||
case ASSIGNMENT -> new Icon(VaadinIcon.USER);
|
||||
case EXPORT -> new Icon(VaadinIcon.DOWNLOAD);
|
||||
case DELETE -> new Icon(VaadinIcon.TRASH);
|
||||
case SYSTEM -> new Icon(VaadinIcon.COG);
|
||||
case COMMENT -> new Icon(VaadinIcon.COMMENT);
|
||||
default -> new Icon(VaadinIcon.INFO_CIRCLE);
|
||||
case CREATE -> new Icon(VaadinIcon.PLUS_CIRCLE);
|
||||
case UPDATE -> new Icon(VaadinIcon.EDIT);
|
||||
case STATUS_CHANGE -> new Icon(VaadinIcon.ARROW_RIGHT);
|
||||
case TASK_COMPLETED -> new Icon(VaadinIcon.CHECK);
|
||||
case ASSIGNMENT -> new Icon(VaadinIcon.USER);
|
||||
case EXPORT -> new Icon(VaadinIcon.DOWNLOAD);
|
||||
case DELETE -> new Icon(VaadinIcon.TRASH);
|
||||
case SYSTEM -> new Icon(VaadinIcon.COG);
|
||||
case COMMENT -> new Icon(VaadinIcon.COMMENT);
|
||||
default -> new Icon(VaadinIcon.INFO_CIRCLE);
|
||||
};
|
||||
}
|
||||
|
||||
private String getTypeColor(JobHistoryType type) {
|
||||
if (type == null) return "var(--lumo-contrast-60pct)";
|
||||
if (type == null)
|
||||
return "var(--lumo-contrast-60pct)";
|
||||
|
||||
return switch (type) {
|
||||
case CREATE -> "var(--lumo-success-color)";
|
||||
case UPDATE -> "var(--lumo-primary-color)";
|
||||
case STATUS_CHANGE -> "var(--lumo-contrast-color)";
|
||||
case TASK_COMPLETED -> "var(--lumo-success-color)";
|
||||
case ASSIGNMENT -> "var(--lumo-primary-color)";
|
||||
case EXPORT -> "var(--lumo-contrast-color)";
|
||||
case DELETE -> "var(--lumo-error-color)";
|
||||
case SYSTEM -> "var(--lumo-contrast-60pct)";
|
||||
case COMMENT -> "var(--lumo-primary-color)";
|
||||
default -> "var(--lumo-contrast-60pct)";
|
||||
case CREATE -> "var(--lumo-success-color)";
|
||||
case UPDATE -> "var(--lumo-primary-color)";
|
||||
case STATUS_CHANGE -> "var(--lumo-contrast-color)";
|
||||
case TASK_COMPLETED -> "var(--lumo-success-color)";
|
||||
case ASSIGNMENT -> "var(--lumo-primary-color)";
|
||||
case EXPORT -> "var(--lumo-contrast-color)";
|
||||
case DELETE -> "var(--lumo-error-color)";
|
||||
case SYSTEM -> "var(--lumo-contrast-60pct)";
|
||||
case COMMENT -> "var(--lumo-primary-color)";
|
||||
default -> "var(--lumo-contrast-60pct)";
|
||||
};
|
||||
}
|
||||
|
||||
private String formatDateTime(java.time.LocalDateTime dateTime) {
|
||||
if (dateTime == null) return "";
|
||||
if (dateTime == null)
|
||||
return "";
|
||||
try {
|
||||
java.time.format.DateTimeFormatter formatter =
|
||||
java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
|
||||
java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter
|
||||
.ofPattern("dd.MM.yyyy HH:mm");
|
||||
return dateTime.format(formatter);
|
||||
} catch (Exception e) {
|
||||
return dateTime.toString();
|
||||
@@ -315,18 +305,19 @@ public class JobHistoryView extends Main implements HasUrlParameter<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";
|
||||
case IN_PROGRESS -> "In Bearbeitung";
|
||||
case PICKUP_SCHEDULED -> "Abholung geplant";
|
||||
case PICKED_UP -> "Abgeholt";
|
||||
case IN_TRANSIT -> "Unterwegs";
|
||||
case DELIVERED -> "Zugestellt";
|
||||
case COMPLETED -> "Abgeschlossen";
|
||||
case CANCELLED -> "Storniert";
|
||||
default -> status.toString();
|
||||
case CREATED -> "Erstellt";
|
||||
case IN_PROGRESS -> "In Bearbeitung";
|
||||
case PICKUP_SCHEDULED -> "Abholung geplant";
|
||||
case PICKED_UP -> "Abgeholt";
|
||||
case IN_TRANSIT -> "Unterwegs";
|
||||
case DELIVERED -> "Zugestellt";
|
||||
case COMPLETED -> "Abgeschlossen";
|
||||
case CANCELLED -> "Storniert";
|
||||
default -> status.toString();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -351,9 +342,7 @@ public class JobHistoryView extends Main implements HasUrlParameter<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")
|
||||
.set("border-radius", "var(--lumo-border-radius-s)")
|
||||
.set("border", "1px solid var(--lumo-contrast-20pct)")
|
||||
.set("cursor", "pointer");
|
||||
com.vaadin.flow.component.html.Image thumbnail = new com.vaadin.flow.component.html.Image(imageData,
|
||||
"Foto");
|
||||
thumbnail.getStyle().set("width", "100px").set("height", "100px").set("object-fit", "cover")
|
||||
.set("border-radius", "var(--lumo-border-radius-s)")
|
||||
.set("border", "1px solid var(--lumo-contrast-20pct)").set("cursor", "pointer");
|
||||
|
||||
return thumbnail;
|
||||
} catch (Exception e) {
|
||||
@@ -424,15 +408,11 @@ public class JobHistoryView extends Main implements HasUrlParameter<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,15 +469,11 @@ 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)")
|
||||
.set("word-break", "break-all");
|
||||
barcodeBox.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-s)").set("padding", "var(--lumo-space-xs)")
|
||||
.set("background-color", "var(--lumo-contrast-5pct)").set("font-family", "monospace")
|
||||
.set("font-size", "var(--lumo-font-size-s)").set("margin-bottom", "var(--lumo-space-xs)")
|
||||
.set("word-break", "break-all");
|
||||
|
||||
barcodeBox.add(new Span(barcodeValue));
|
||||
return barcodeBox;
|
||||
@@ -531,22 +506,16 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
|
||||
}
|
||||
|
||||
Div previewContainer = new Div();
|
||||
previewContainer.getStyle()
|
||||
.set("margin-top", "var(--lumo-space-s)")
|
||||
.set("border", "1px solid var(--lumo-contrast-20pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-s)")
|
||||
.set("padding", "var(--lumo-space-xs)")
|
||||
.set("background-color", "var(--lumo-base-color)")
|
||||
.set("cursor", "pointer")
|
||||
.set("width", "200px")
|
||||
.set("height", "100px")
|
||||
.set("overflow", "hidden")
|
||||
.set("display", "flex")
|
||||
.set("align-items", "center")
|
||||
.set("justify-content", "center");
|
||||
previewContainer.getStyle().set("margin-top", "var(--lumo-space-s)")
|
||||
.set("border", "1px solid var(--lumo-contrast-20pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-s)").set("padding", "var(--lumo-space-xs)")
|
||||
.set("background-color", "var(--lumo-base-color)").set("cursor", "pointer").set("width", "200px")
|
||||
.set("height", "100px").set("overflow", "hidden").set("display", "flex")
|
||||
.set("align-items", "center").set("justify-content", "center");
|
||||
|
||||
// Create responsive SVG for preview
|
||||
com.vaadin.flow.component.Html signatureSvg = createResponsiveSignatureSvg(signature.getSignatureSvg(), "100%", "100%");
|
||||
com.vaadin.flow.component.Html signatureSvg = createResponsiveSignatureSvg(signature.getSignatureSvg(),
|
||||
"100%", "100%");
|
||||
previewContainer.add(signatureSvg);
|
||||
|
||||
// Add click listener for enlarged view
|
||||
@@ -560,27 +529,28 @@ public class JobHistoryView extends Main implements HasUrlParameter<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;
|
||||
|
||||
if (!responsiveSvg.contains("viewBox")) {
|
||||
// Try to extract width and height from SVG and create viewBox
|
||||
responsiveSvg = responsiveSvg.replaceFirst("<svg",
|
||||
"<svg viewBox=\"0 0 300 150\" preserveAspectRatio=\"xMidYMid meet\"");
|
||||
"<svg viewBox=\"0 0 300 150\" preserveAspectRatio=\"xMidYMid meet\"");
|
||||
}
|
||||
|
||||
// Ensure the SVG has proper responsive attributes
|
||||
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) {
|
||||
|
||||
@@ -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", " ");
|
||||
}
|
||||
|
||||
@@ -426,7 +416,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
|
||||
|
||||
// Completion details if completed
|
||||
if (task.isCompleted()) {
|
||||
dialogContent.add(new Span("")); // Spacer
|
||||
dialogContent.add(new Span("")); // Spacer
|
||||
if (task.getCompletedAt() != null) {
|
||||
dialogContent.add(new Span("Abgeschlossen am: " + formatDateTime(task.getCompletedAt())));
|
||||
}
|
||||
@@ -468,7 +458,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<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) {
|
||||
@@ -484,7 +475,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
|
||||
List<Photo> photos = photoRepository.findByTaskId(taskId);
|
||||
|
||||
if (!photos.isEmpty()) {
|
||||
content.add(new Span("")); // Spacer
|
||||
content.add(new Span("")); // Spacer
|
||||
|
||||
// Collect all photos from all Photo entries
|
||||
List<String> allPhotos = new ArrayList<>();
|
||||
@@ -520,7 +511,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
|
||||
List<Signature> signatures = signatureRepository.findByTaskId(taskId);
|
||||
|
||||
if (!signatures.isEmpty()) {
|
||||
content.add(new Span("")); // Spacer
|
||||
content.add(new Span("")); // Spacer
|
||||
content.add(new Span("Gespeicherte Unterschrift:"));
|
||||
|
||||
// Display the latest signature (assuming one signature per task)
|
||||
@@ -530,17 +521,12 @@ public class JobSummaryView extends Main implements HasUrlParameter<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)")
|
||||
.set("border-radius", "var(--lumo-border-radius-m)")
|
||||
.set("padding", "var(--lumo-space-s)")
|
||||
.set("background-color", "white")
|
||||
.set("width", "100%")
|
||||
.set("max-width", "450px")
|
||||
.set("overflow", "hidden")
|
||||
.set("display", "flex")
|
||||
.set("align-items", "center")
|
||||
.set("justify-content", "center");
|
||||
svgContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-m)")
|
||||
.set("padding", "var(--lumo-space-s)").set("background-color", "white")
|
||||
.set("width", "100%").set("max-width", "450px").set("overflow", "hidden")
|
||||
.set("display", "flex").set("align-items", "center")
|
||||
.set("justify-content", "center");
|
||||
|
||||
// Process SVG to make it responsive
|
||||
String responsiveSvg = makeResponsiveSvg(svgContent);
|
||||
@@ -562,7 +548,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
|
||||
List<Barcode> barcodes = barcodeRepository.findByTaskId(taskId);
|
||||
|
||||
if (!barcodes.isEmpty()) {
|
||||
content.add(new Span("")); // Spacer
|
||||
content.add(new Span("")); // Spacer
|
||||
content.add(new Span("Gescannte Barcodes (" + barcodes.size() + "):"));
|
||||
|
||||
// Display all scanned barcodes
|
||||
@@ -573,15 +559,12 @@ public class JobSummaryView extends Main implements HasUrlParameter<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)")
|
||||
.set("border-radius", "var(--lumo-border-radius-s)")
|
||||
.set("padding", "var(--lumo-space-s)")
|
||||
.set("margin", "var(--lumo-space-xs) 0")
|
||||
.set("background-color", "var(--lumo-contrast-5pct)")
|
||||
.set("font-family", "monospace")
|
||||
.set("font-size", "var(--lumo-font-size-s)")
|
||||
.set("word-break", "break-all");
|
||||
barcodeContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-s)")
|
||||
.set("padding", "var(--lumo-space-s)").set("margin", "var(--lumo-space-xs) 0")
|
||||
.set("background-color", "var(--lumo-contrast-5pct)")
|
||||
.set("font-family", "monospace").set("font-size", "var(--lumo-font-size-s)")
|
||||
.set("word-break", "break-all");
|
||||
|
||||
Span barcodeSpan = new Span((i + 1) + ". " + barcodeValue);
|
||||
barcodeContainer.add(barcodeSpan);
|
||||
@@ -598,7 +581,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<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)")
|
||||
.set("border-color", "var(--lumo-primary-color-50pct)");
|
||||
taskCard.getStyle().set("transform", "translateY(-2px)").set("box-shadow", "0 4px 12px rgba(0, 0, 0, 0.15)")
|
||||
.set("border-color", "var(--lumo-primary-color-50pct)");
|
||||
});
|
||||
|
||||
taskCard.getElement().addEventListener("mouseleave", e -> {
|
||||
taskCard.getStyle()
|
||||
.set("transform", "translateY(0)")
|
||||
.set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)")
|
||||
.set("border-color", "var(--lumo-contrast-20pct)");
|
||||
taskCard.getStyle().set("transform", "translateY(0)").set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)")
|
||||
.set("border-color", "var(--lumo-contrast-20pct)");
|
||||
});
|
||||
|
||||
// Task icon based on type
|
||||
Icon taskIcon = getTaskIcon(task);
|
||||
taskIcon.getStyle().set("color", task.isCompleted() ? "var(--lumo-success-color)" : "var(--lumo-primary-color)");
|
||||
taskIcon.getStyle().set("color",
|
||||
task.isCompleted() ? "var(--lumo-success-color)" : "var(--lumo-primary-color)");
|
||||
|
||||
// Task content
|
||||
VerticalLayout taskContent = new VerticalLayout();
|
||||
@@ -652,27 +625,20 @@ public class JobSummaryView extends Main implements HasUrlParameter<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%")
|
||||
.set("background-color", task.isCompleted() ? "var(--lumo-success-color)" : "var(--lumo-error-color)");
|
||||
statusIndicator.getStyle().set("width", "8px").set("height", "8px").set("border-radius", "50%")
|
||||
.set("background-color", task.isCompleted() ? "var(--lumo-success-color)" : "var(--lumo-error-color)");
|
||||
|
||||
taskCard.add(taskIcon, taskContent, statusIndicator);
|
||||
|
||||
@@ -680,10 +646,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<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)")
|
||||
.set("border-color", "var(--lumo-contrast-20pct)");
|
||||
taskCard.getStyle().set("transform", "translateY(0)").set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)")
|
||||
.set("border-color", "var(--lumo-contrast-20pct)");
|
||||
});
|
||||
|
||||
return taskCard;
|
||||
@@ -707,7 +671,9 @@ public class JobSummaryView extends Main implements HasUrlParameter<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")
|
||||
.set("justify-content", "center");
|
||||
galleryContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)")
|
||||
.set("background-color", "white").set("max-width", "600px").set("min-height", "500px")
|
||||
.set("height", "500px").set("position", "relative").set("display", "flex").set("align-items", "center")
|
||||
.set("justify-content", "center");
|
||||
|
||||
if (photos.size() == 1) {
|
||||
// Single photo - no navigation needed
|
||||
Div photoContainer = createPhotoContainer(photos.get(0));
|
||||
photoContainer.getStyle()
|
||||
.set("flex", "1")
|
||||
.set("display", "flex")
|
||||
.set("align-items", "center")
|
||||
.set("justify-content", "center");
|
||||
photoContainer.getStyle().set("flex", "1").set("display", "flex").set("align-items", "center")
|
||||
.set("justify-content", "center");
|
||||
galleryContainer.add(photoContainer);
|
||||
} else {
|
||||
// Multiple photos - add navigation
|
||||
final int[] currentIndex = {0}; // Use array to make it effectively final
|
||||
final int[] currentIndex = { 0 }; // Use array to make it effectively final
|
||||
|
||||
// Photo counter
|
||||
Span photoCounter = new Span((currentIndex[0] + 1) + " / " + photos.size());
|
||||
photoCounter.getStyle()
|
||||
.set("position", "absolute")
|
||||
.set("top", "var(--lumo-space-s)")
|
||||
.set("right", "var(--lumo-space-s)")
|
||||
.set("background-color", "rgba(0, 0, 0, 0.6)")
|
||||
.set("color", "white")
|
||||
.set("padding", "var(--lumo-space-xs) var(--lumo-space-s)")
|
||||
.set("border-radius", "var(--lumo-border-radius-s)")
|
||||
.set("font-size", "var(--lumo-font-size-s)")
|
||||
.set("z-index", "10");
|
||||
photoCounter.getStyle().set("position", "absolute").set("top", "var(--lumo-space-s)")
|
||||
.set("right", "var(--lumo-space-s)").set("background-color", "rgba(0, 0, 0, 0.6)")
|
||||
.set("color", "white").set("padding", "var(--lumo-space-xs) var(--lumo-space-s)")
|
||||
.set("border-radius", "var(--lumo-border-radius-s)").set("font-size", "var(--lumo-font-size-s)")
|
||||
.set("z-index", "10");
|
||||
|
||||
// Photo container
|
||||
Div photoContainer = createPhotoContainer(photos.get(0));
|
||||
photoContainer.getStyle()
|
||||
.set("margin", "0 40px") // Space for buttons
|
||||
.set("flex", "1")
|
||||
.set("display", "flex")
|
||||
.set("align-items", "center")
|
||||
.set("justify-content", "center");
|
||||
photoContainer.getStyle().set("margin", "0 40px") // Space for buttons
|
||||
.set("flex", "1").set("display", "flex").set("align-items", "center")
|
||||
.set("justify-content", "center");
|
||||
|
||||
// Previous button
|
||||
Button prevButton = new Button(new Icon(VaadinIcon.CHEVRON_LEFT));
|
||||
prevButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ICON);
|
||||
prevButton.getStyle()
|
||||
.set("position", "absolute")
|
||||
.set("left", "var(--lumo-space-s)")
|
||||
.set("top", "50%")
|
||||
.set("transform", "translateY(-50%)")
|
||||
.set("background-color", "rgba(255, 255, 255, 0.8)")
|
||||
.set("border-radius", "50%")
|
||||
.set("z-index", "10");
|
||||
prevButton.getStyle().set("position", "absolute").set("left", "var(--lumo-space-s)").set("top", "50%")
|
||||
.set("transform", "translateY(-50%)").set("background-color", "rgba(255, 255, 255, 0.8)")
|
||||
.set("border-radius", "50%").set("z-index", "10");
|
||||
|
||||
// Next button
|
||||
Button nextButton = new Button(new Icon(VaadinIcon.CHEVRON_RIGHT));
|
||||
nextButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ICON);
|
||||
nextButton.getStyle()
|
||||
.set("position", "absolute")
|
||||
.set("right", "var(--lumo-space-s)")
|
||||
.set("top", "50%")
|
||||
.set("transform", "translateY(-50%)")
|
||||
.set("background-color", "rgba(255, 255, 255, 0.8)")
|
||||
.set("border-radius", "50%")
|
||||
.set("z-index", "10");
|
||||
nextButton.getStyle().set("position", "absolute").set("right", "var(--lumo-space-s)").set("top", "50%")
|
||||
.set("transform", "translateY(-50%)").set("background-color", "rgba(255, 255, 255, 0.8)")
|
||||
.set("border-radius", "50%").set("z-index", "10");
|
||||
|
||||
// Navigation logic
|
||||
prevButton.addClickListener(e -> {
|
||||
if (currentIndex[0] > 0) {
|
||||
currentIndex[0]--;
|
||||
updatePhotoDisplay(photoContainer, photos.get(currentIndex[0]), photoCounter, currentIndex[0] + 1, photos.size());
|
||||
updatePhotoDisplay(photoContainer, photos.get(currentIndex[0]), photoCounter, currentIndex[0] + 1,
|
||||
photos.size());
|
||||
}
|
||||
prevButton.setEnabled(currentIndex[0] > 0);
|
||||
nextButton.setEnabled(currentIndex[0] < photos.size() - 1);
|
||||
@@ -825,7 +766,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<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");
|
||||
@@ -894,7 +830,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
|
||||
|
||||
if (!cleanWidth.isEmpty() && !cleanHeight.isEmpty()) {
|
||||
responsiveSvg = responsiveSvg.replaceFirst("<svg",
|
||||
"<svg viewBox=\"0 0 " + cleanWidth + " " + cleanHeight + "\"");
|
||||
"<svg viewBox=\"0 0 " + cleanWidth + " " + cleanHeight + "\"");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Ignore extraction errors
|
||||
@@ -904,7 +840,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
|
||||
|
||||
// Add responsive styling
|
||||
responsiveSvg = responsiveSvg.replaceFirst("<svg",
|
||||
"<svg style=\"max-width: 100%; max-height: 200px; width: auto; height: auto;\"");
|
||||
"<svg style=\"max-width: 100%; max-height: 200px; width: auto; height: auto;\"");
|
||||
|
||||
return responsiveSvg;
|
||||
}
|
||||
@@ -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)")
|
||||
.set("border-color", "var(--lumo-contrast-20pct)");
|
||||
taskCard.getStyle().set("transform", "translateY(0)").set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)")
|
||||
.set("border-color", "var(--lumo-contrast-20pct)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -157,9 +159,8 @@ public class ShowJobsView extends VerticalLayout {
|
||||
// Add to UI and trigger download via JavaScript
|
||||
add(downloadAnchor);
|
||||
getUI().ifPresent(ui -> ui.getPage().executeJs(
|
||||
"const link = arguments[0]; link.click(); setTimeout(() => link.remove(), 100);",
|
||||
downloadAnchor.getElement()
|
||||
));
|
||||
"const link = arguments[0]; link.click(); setTimeout(() => link.remove(), 100);",
|
||||
downloadAnchor.getElement()));
|
||||
}
|
||||
|
||||
private String generateCsv(java.util.List<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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,9 +73,8 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
|
||||
logo.getStyle().set("font-weight", "bold");
|
||||
|
||||
// Navigation - abhängig vom Anmeldestatus
|
||||
Component navigation = securityService.isUserLoggedIn()
|
||||
? createAuthenticatedNavigation()
|
||||
: createAnonymousNavigation();
|
||||
Component navigation = securityService.isUserLoggedIn() ? createAuthenticatedNavigation()
|
||||
: createAnonymousNavigation();
|
||||
|
||||
header.add(logo, navigation);
|
||||
|
||||
@@ -103,8 +102,7 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
|
||||
navLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||
|
||||
// Auftragserstellung Button
|
||||
Button createOrderBtn = new Button("Auftragserstellung", event ->
|
||||
UI.getCurrent().navigate("add_job"));
|
||||
Button createOrderBtn = new Button("Auftragserstellung", event -> UI.getCurrent().navigate("add_job"));
|
||||
createOrderBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
|
||||
// Verwaltung ComboBox
|
||||
@@ -115,15 +113,15 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
|
||||
String value = event.getValue();
|
||||
if (value != null) {
|
||||
switch (value) {
|
||||
case "Kunden":
|
||||
UI.getCurrent().navigate("customer");
|
||||
break;
|
||||
case "Aufträge":
|
||||
UI.getCurrent().navigate("orders");
|
||||
break;
|
||||
case "Firmen":
|
||||
UI.getCurrent().navigate("add_company");
|
||||
break;
|
||||
case "Kunden":
|
||||
UI.getCurrent().navigate("customer");
|
||||
break;
|
||||
case "Aufträge":
|
||||
UI.getCurrent().navigate("orders");
|
||||
break;
|
||||
case "Firmen":
|
||||
UI.getCurrent().navigate("add_company");
|
||||
break;
|
||||
}
|
||||
managementCombo.clear(); // Reset selection
|
||||
}
|
||||
@@ -138,13 +136,13 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
|
||||
String value = event.getValue();
|
||||
if (value != null) {
|
||||
switch (value) {
|
||||
case "Profil anzeigen":
|
||||
break;
|
||||
case "Einstellungen":
|
||||
break;
|
||||
case "Abmelden":
|
||||
securityService.logout();
|
||||
break;
|
||||
case "Profil anzeigen":
|
||||
break;
|
||||
case "Einstellungen":
|
||||
break;
|
||||
case "Abmelden":
|
||||
securityService.logout();
|
||||
break;
|
||||
}
|
||||
userCombo.clear(); // Reset selection
|
||||
}
|
||||
@@ -157,7 +155,7 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
|
||||
notificationBtn.setTooltipText("Benachrichtigungen");
|
||||
notificationBtn.addClickListener(event -> {
|
||||
com.vaadin.flow.component.notification.Notification.show("Keine neuen Benachrichtigungen", 3000,
|
||||
com.vaadin.flow.component.notification.Notification.Position.TOP_END);
|
||||
com.vaadin.flow.component.notification.Notification.Position.TOP_END);
|
||||
});
|
||||
|
||||
navLayout.add(createOrderBtn, managementCombo, userCombo, notificationBtn);
|
||||
@@ -170,7 +168,8 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
|
||||
heroSection.setPadding(true);
|
||||
heroSection.setSpacing(true);
|
||||
heroSection.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||
heroSection.getStyle().set("background", "linear-gradient(135deg, var(--lumo-primary-color-10pct), var(--lumo-primary-color-50pct))");
|
||||
heroSection.getStyle().set("background",
|
||||
"linear-gradient(135deg, var(--lumo-primary-color-10pct), var(--lumo-primary-color-50pct))");
|
||||
heroSection.getStyle().set("min-height", "400px");
|
||||
heroSection.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
|
||||
|
||||
@@ -184,11 +183,9 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
|
||||
heroTitle.getStyle().set("color", "var(--lumo-primary-text-color)");
|
||||
heroTitle.getStyle().set("margin-bottom", "var(--lumo-space-l)");
|
||||
|
||||
Paragraph heroDescription = new Paragraph(
|
||||
"Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe - " +
|
||||
"volldigital und aus einem Guss. Konzentrieren Sie sich auf Ihr Geschäft, " +
|
||||
"wir kümmern uns um die Büroarbeit."
|
||||
);
|
||||
Paragraph heroDescription = new Paragraph("Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe - "
|
||||
+ "volldigital und aus einem Guss. Konzentrieren Sie sich auf Ihr Geschäft, "
|
||||
+ "wir kümmern uns um die Büroarbeit.");
|
||||
heroDescription.getStyle().set("text-align", "center");
|
||||
heroDescription.getStyle().set("max-width", "600px");
|
||||
heroDescription.getStyle().set("font-size", "var(--lumo-font-size-l)");
|
||||
@@ -214,9 +211,8 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
|
||||
systemTitle.getStyle().set("text-align", "center");
|
||||
|
||||
Paragraph systemIntro = new Paragraph(
|
||||
"Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe ist von entscheidender Bedeutung, " +
|
||||
"dass sie sich in erster Linie auf ihr eigentliches Geschäft konzentrieren können: Kunden gewinnen und Waren von A nach B liefern."
|
||||
);
|
||||
"Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe ist von entscheidender Bedeutung, "
|
||||
+ "dass sie sich in erster Linie auf ihr eigentliches Geschäft konzentrieren können: Kunden gewinnen und Waren von A nach B liefern.");
|
||||
systemIntro.getStyle().set("text-align", "center");
|
||||
systemIntro.getStyle().set("max-width", "800px");
|
||||
systemIntro.getStyle().set("margin-bottom", "var(--lumo-space-xl)");
|
||||
@@ -228,14 +224,12 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
|
||||
featuresGrid.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.START);
|
||||
|
||||
// Feature Cards
|
||||
featuresGrid.add(
|
||||
createFeatureCard(VaadinIcon.COG, "Einrichtungsassistent",
|
||||
featuresGrid.add(createFeatureCard(VaadinIcon.COG, "Einrichtungsassistent",
|
||||
"Mithilfe des Einrichtungsassistenten haben Sie die Möglichkeit, Ihr Nutzerprofil zu vervollständigen."),
|
||||
createFeatureCard(VaadinIcon.USERS, "Kunden- und Auftragsverwaltung",
|
||||
"Mit der Kunden- und Auftragsverwaltung haben Sie alle Kontaktdaten und Auftragsdetails stets im Blick."),
|
||||
createFeatureCard(VaadinIcon.CLIPBOARD_TEXT, "Auftragserstellung",
|
||||
"Stellen Sie mit wenigen Mausklicks Aufträge ins System ein und legen Sie fest, welcher Mitarbeiter welchen Transportauftrag abarbeiten soll.")
|
||||
);
|
||||
createFeatureCard(VaadinIcon.USERS, "Kunden- und Auftragsverwaltung",
|
||||
"Mit der Kunden- und Auftragsverwaltung haben Sie alle Kontaktdaten und Auftragsdetails stets im Blick."),
|
||||
createFeatureCard(VaadinIcon.CLIPBOARD_TEXT, "Auftragserstellung",
|
||||
"Stellen Sie mit wenigen Mausklicks Aufträge ins System ein und legen Sie fest, welcher Mitarbeiter welchen Transportauftrag abarbeiten soll."));
|
||||
|
||||
systemSection.add(systemTitle, systemIntro, featuresGrid);
|
||||
return systemSection;
|
||||
@@ -281,9 +275,8 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
|
||||
appTitle.getStyle().set("text-align", "center");
|
||||
|
||||
Paragraph appDescription = new Paragraph(
|
||||
"Jeder Auftrag kann optional über die votianLT-App abgearbeitet werden – ganz ohne \"Zettelwirtschaft\". " +
|
||||
"So gelangen alle relevanten Auftragsinformationen direkt auf das Smartphone des Fahrers."
|
||||
);
|
||||
"Jeder Auftrag kann optional über die votianLT-App abgearbeitet werden – ganz ohne \"Zettelwirtschaft\". "
|
||||
+ "So gelangen alle relevanten Auftragsinformationen direkt auf das Smartphone des Fahrers.");
|
||||
appDescription.getStyle().set("text-align", "center");
|
||||
appDescription.getStyle().set("max-width", "800px");
|
||||
|
||||
@@ -314,18 +307,12 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
|
||||
companyInfo.setPadding(false);
|
||||
companyInfo.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||
|
||||
companyInfo.add(
|
||||
new Paragraph("Assecutor Data Service GmbH"),
|
||||
new Paragraph("Ottensener Str. 8, 22525 Hamburg"),
|
||||
new Paragraph("Telefon: +49 40 18 123 771 0"),
|
||||
new Paragraph("E-Mail: ahoi@assecutor.de")
|
||||
);
|
||||
companyInfo.add(new Paragraph("Assecutor Data Service GmbH"), new Paragraph("Ottensener Str. 8, 22525 Hamburg"),
|
||||
new Paragraph("Telefon: +49 40 18 123 771 0"), new Paragraph("E-Mail: ahoi@assecutor.de"));
|
||||
|
||||
// Call to Action
|
||||
Paragraph ctaText = new Paragraph(
|
||||
"Registrieren Sie sich noch heute und nutzen den kostenfreien Probemonat, " +
|
||||
"um das System auf Herz und Nieren zu testen."
|
||||
);
|
||||
Paragraph ctaText = new Paragraph("Registrieren Sie sich noch heute und nutzen den kostenfreien Probemonat, "
|
||||
+ "um das System auf Herz und Nieren zu testen.");
|
||||
ctaText.getStyle().set("text-align", "center");
|
||||
ctaText.getStyle().set("font-weight", "bold");
|
||||
ctaText.getStyle().set("color", "var(--lumo-primary-color)");
|
||||
|
||||
@@ -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)")
|
||||
.set("border", "1px solid var(--lumo-contrast-10pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-m)")
|
||||
.set("padding", "var(--lumo-space-m)")
|
||||
.set("text-align", "center")
|
||||
.set("box-shadow", "var(--lumo-box-shadow-xs)")
|
||||
.set("min-width", "150px");
|
||||
card.getStyle().set("background", "var(--lumo-base-color)")
|
||||
.set("border", "1px solid var(--lumo-contrast-10pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)")
|
||||
.set("text-align", "center").set("box-shadow", "var(--lumo-box-shadow-xs)").set("min-width", "150px");
|
||||
|
||||
H3 titleElement = new H3(title);
|
||||
titleElement.getStyle().set("margin", "0 0 var(--lumo-space-s) 0").set("font-size", "var(--lumo-font-size-s)");
|
||||
|
||||
Span valueElement = new Span(value);
|
||||
valueElement.getStyle()
|
||||
.set("font-size", "var(--lumo-font-size-xl)")
|
||||
.set("font-weight", "bold")
|
||||
.set("color", getThemeColor(theme));
|
||||
valueElement.getStyle().set("font-size", "var(--lumo-font-size-xl)").set("font-weight", "bold").set("color",
|
||||
getThemeColor(theme));
|
||||
|
||||
card.add(titleElement, valueElement);
|
||||
return card;
|
||||
@@ -113,10 +107,10 @@ public class StatisticsView extends VerticalLayout {
|
||||
|
||||
private String getThemeColor(String theme) {
|
||||
return switch (theme) {
|
||||
case "success" -> "var(--lumo-success-color)";
|
||||
case "warning" -> "var(--lumo-warning-color)";
|
||||
case "error" -> "var(--lumo-error-color)";
|
||||
default -> "var(--lumo-primary-color)";
|
||||
case "success" -> "var(--lumo-success-color)";
|
||||
case "warning" -> "var(--lumo-warning-color)";
|
||||
case "error" -> "var(--lumo-error-color)";
|
||||
default -> "var(--lumo-primary-color)";
|
||||
};
|
||||
}
|
||||
|
||||
@@ -129,52 +123,52 @@ public class StatisticsView extends VerticalLayout {
|
||||
chartContainer.add(canvas);
|
||||
|
||||
String script = """
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
const ctx = document.getElementById('monthlyOrdersCanvas');
|
||||
if (ctx) {
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
|
||||
datasets: [{
|
||||
label: '2024',
|
||||
data: [15, 18, 22, 28, 32, 35, 42, 38, 41, 35, 28, 25],
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||
tension: 0.1
|
||||
}, {
|
||||
label: '2023',
|
||||
data: [12, 15, 18, 25, 28, 30, 35, 32, 36, 30, 25, 22],
|
||||
borderColor: 'rgb(135, 206, 235)',
|
||||
backgroundColor: 'rgba(135, 206, 235, 0.2)',
|
||||
tension: 0.1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Aufträge pro Monat'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Anzahl Aufträge'
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
const ctx = document.getElementById('monthlyOrdersCanvas');
|
||||
if (ctx) {
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
|
||||
datasets: [{
|
||||
label: '2024',
|
||||
data: [15, 18, 22, 28, 32, 35, 42, 38, 41, 35, 28, 25],
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||
tension: 0.1
|
||||
}, {
|
||||
label: '2023',
|
||||
data: [12, 15, 18, 25, 28, 30, 35, 32, 36, 30, 25, 22],
|
||||
borderColor: 'rgb(135, 206, 235)',
|
||||
backgroundColor: 'rgba(135, 206, 235, 0.2)',
|
||||
tension: 0.1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Aufträge pro Monat'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Anzahl Aufträge'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
</script>
|
||||
""";
|
||||
}, 100);
|
||||
</script>
|
||||
""";
|
||||
|
||||
Html scriptElement = new Html(script);
|
||||
chartContainer.add(scriptElement);
|
||||
@@ -191,49 +185,49 @@ public class StatisticsView extends VerticalLayout {
|
||||
chartContainer.add(canvas);
|
||||
|
||||
String script = """
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
const ctx = document.getElementById('statusPieCanvas');
|
||||
if (ctx) {
|
||||
new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Abgeschlossen', 'In Bearbeitung', 'Geplant', 'Storniert'],
|
||||
datasets: [{
|
||||
data: [156, 34, 28, 12],
|
||||
backgroundColor: [
|
||||
'rgba(54, 162, 235, 0.8)',
|
||||
'rgba(255, 206, 86, 0.8)',
|
||||
'rgba(75, 192, 192, 0.8)',
|
||||
'rgba(255, 99, 132, 0.8)'
|
||||
],
|
||||
borderColor: [
|
||||
'rgba(54, 162, 235, 1)',
|
||||
'rgba(255, 206, 86, 1)',
|
||||
'rgba(75, 192, 192, 1)',
|
||||
'rgba(255, 99, 132, 1)'
|
||||
],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Aufträge nach Status'
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
const ctx = document.getElementById('statusPieCanvas');
|
||||
if (ctx) {
|
||||
new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Abgeschlossen', 'In Bearbeitung', 'Geplant', 'Storniert'],
|
||||
datasets: [{
|
||||
data: [156, 34, 28, 12],
|
||||
backgroundColor: [
|
||||
'rgba(54, 162, 235, 0.8)',
|
||||
'rgba(255, 206, 86, 0.8)',
|
||||
'rgba(75, 192, 192, 0.8)',
|
||||
'rgba(255, 99, 132, 0.8)'
|
||||
],
|
||||
borderColor: [
|
||||
'rgba(54, 162, 235, 1)',
|
||||
'rgba(255, 206, 86, 1)',
|
||||
'rgba(75, 192, 192, 1)',
|
||||
'rgba(255, 99, 132, 1)'
|
||||
],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
legend: {
|
||||
position: 'right'
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Aufträge nach Status'
|
||||
},
|
||||
legend: {
|
||||
position: 'right'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
</script>
|
||||
""";
|
||||
}, 100);
|
||||
</script>
|
||||
""";
|
||||
|
||||
Html scriptElement = new Html(script);
|
||||
chartContainer.add(scriptElement);
|
||||
@@ -250,56 +244,56 @@ public class StatisticsView extends VerticalLayout {
|
||||
chartContainer.add(canvas);
|
||||
|
||||
String script = """
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
const ctx = document.getElementById('revenueByCustomerCanvas');
|
||||
if (ctx) {
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['Firma A GmbH', 'Logistics B', 'Transport C', 'Spediteur D', 'Handel E',
|
||||
'Industrie F', 'Service G', 'Vertrieb H', 'Export I', 'Import J'],
|
||||
datasets: [{
|
||||
label: 'Umsatz (€)',
|
||||
data: [8500, 7200, 6800, 5900, 5400, 4800, 4200, 3900, 3500, 3100],
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.8)',
|
||||
borderColor: 'rgba(75, 192, 192, 1)',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Top 10 Kunden nach Umsatz'
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
const ctx = document.getElementById('revenueByCustomerCanvas');
|
||||
if (ctx) {
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['Firma A GmbH', 'Logistics B', 'Transport C', 'Spediteur D', 'Handel E',
|
||||
'Industrie F', 'Service G', 'Vertrieb H', 'Export I', 'Import J'],
|
||||
datasets: [{
|
||||
label: 'Umsatz (€)',
|
||||
data: [8500, 7200, 6800, 5900, 5400, 4800, 4200, 3900, 3500, 3100],
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.8)',
|
||||
borderColor: 'rgba(75, 192, 192, 1)',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
maxRotation: 45,
|
||||
minRotation: 45
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Umsatz (€)'
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Top 10 Kunden nach Umsatz'
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
maxRotation: 45,
|
||||
minRotation: 45
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Umsatz (€)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
</script>
|
||||
""";
|
||||
}, 100);
|
||||
</script>
|
||||
""";
|
||||
|
||||
Html scriptElement = new Html(script);
|
||||
chartContainer.add(scriptElement);
|
||||
|
||||
@@ -9,4 +9,3 @@ import java.util.List;
|
||||
public interface CargoItemRepository extends MongoRepository<CargoItem, ObjectId> {
|
||||
List<CargoItem> findByJobId(ObjectId jobId);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
List<JobStatus> statusList);
|
||||
@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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -15,4 +15,3 @@ public interface TaskRepository extends MongoRepository<BaseTask, ObjectId> {
|
||||
return findByJobIdOrderByTaskOrderAsc(jobId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -24,33 +24,20 @@ public class SecurityConfig extends VaadinWebSecurity {
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
// Konfiguriere zusätzliche öffentliche Endpunkte vor der Basis-Konfiguration
|
||||
http.authorizeHttpRequests(auth -> auth
|
||||
// Öffentliche Endpunkte
|
||||
.requestMatchers(
|
||||
new AntPathRequestMatcher("/"),
|
||||
new AntPathRequestMatcher("/register"),
|
||||
new AntPathRequestMatcher("/login"),
|
||||
new AntPathRequestMatcher("/forget-password"),
|
||||
new AntPathRequestMatcher("/forgot-password-request"),
|
||||
new AntPathRequestMatcher("/images/**"),
|
||||
new AntPathRequestMatcher("/icons/**"),
|
||||
new AntPathRequestMatcher("/favicon.ico"),
|
||||
new AntPathRequestMatcher("/robots.txt"),
|
||||
new AntPathRequestMatcher("/manifest.webmanifest"),
|
||||
new AntPathRequestMatcher("/sw.js"),
|
||||
new AntPathRequestMatcher("/offline.html"),
|
||||
new AntPathRequestMatcher("/frontend/**"),
|
||||
new AntPathRequestMatcher("/webjars/**"),
|
||||
new AntPathRequestMatcher("/h2-console/**"),
|
||||
new AntPathRequestMatcher("/frontend-es5/**", "/frontend-es6/**")
|
||||
).permitAll()
|
||||
);
|
||||
// Öffentliche Endpunkte
|
||||
.requestMatchers(new AntPathRequestMatcher("/"), new AntPathRequestMatcher("/register"),
|
||||
new AntPathRequestMatcher("/login"), new AntPathRequestMatcher("/forget-password"),
|
||||
new AntPathRequestMatcher("/forgot-password-request"), new AntPathRequestMatcher("/images/**"),
|
||||
new AntPathRequestMatcher("/icons/**"), new AntPathRequestMatcher("/favicon.ico"),
|
||||
new AntPathRequestMatcher("/robots.txt"), new AntPathRequestMatcher("/manifest.webmanifest"),
|
||||
new AntPathRequestMatcher("/sw.js"), new AntPathRequestMatcher("/offline.html"),
|
||||
new AntPathRequestMatcher("/frontend/**"), new AntPathRequestMatcher("/webjars/**"),
|
||||
new AntPathRequestMatcher("/h2-console/**"),
|
||||
new AntPathRequestMatcher("/frontend-es5/**", "/frontend-es6/**"))
|
||||
.permitAll());
|
||||
|
||||
// Standard-CSRF-Konfiguration
|
||||
http.csrf(csrf -> csrf
|
||||
.ignoringRequestMatchers(
|
||||
new AntPathRequestMatcher("/h2-console/**")
|
||||
)
|
||||
);
|
||||
http.csrf(csrf -> csrf.ignoringRequestMatchers(new AntPathRequestMatcher("/h2-console/**")));
|
||||
|
||||
// Delegiere die Basis-Konfiguration an VaadinWebSecurity
|
||||
// Dies fügt automatisch .anyRequest().authenticated() hinzu
|
||||
@@ -60,12 +47,8 @@ public class SecurityConfig extends VaadinWebSecurity {
|
||||
setLoginView(http, "/login");
|
||||
|
||||
// Logout-Konfiguration
|
||||
http.logout(logout -> logout
|
||||
.logoutUrl("/logout")
|
||||
.logoutSuccessUrl("/")
|
||||
.invalidateHttpSession(true)
|
||||
.deleteCookies("JSESSIONID")
|
||||
);
|
||||
http.logout(logout -> logout.logoutUrl("/logout").logoutSuccessUrl("/").invalidateHttpSession(true)
|
||||
.deleteCookies("JSESSIONID"));
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,5 +25,4 @@ public class UserDetailsServiceImpl implements UserDetailsService {
|
||||
return new CustomUserPrincipal(user);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -71,10 +71,12 @@ public class EmailService {
|
||||
|
||||
// Send email
|
||||
sendEmail(user, job, taskType, taskId, appUser);
|
||||
log.info("Task completion notification sent to {} for job {} task {}", user.getEmail(), job.getJobNumber(), taskId);
|
||||
log.info("Task completion notification sent to {} for job {} task {}", user.getEmail(), job.getJobNumber(),
|
||||
taskId);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send task completion notification for job {} task {}: {}", jobId, taskId, e.getMessage(), e);
|
||||
log.error("Failed to send task completion notification for job {} task {}: {}", jobId, taskId,
|
||||
e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +84,8 @@ public class EmailService {
|
||||
SimpleMailMessage message = new SimpleMailMessage();
|
||||
message.setFrom(smtpUsername);
|
||||
message.setTo(user.getEmail());
|
||||
message.setSubject("Aufgabe abgeschlossen - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
|
||||
message.setSubject(
|
||||
"Aufgabe abgeschlossen - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
|
||||
|
||||
String fullName = buildFullName(user);
|
||||
String appUserName = buildAppUserName(appUser);
|
||||
@@ -150,18 +153,37 @@ public class EmailService {
|
||||
}
|
||||
|
||||
private String getTaskTypeDisplayName(String taskType) {
|
||||
if (taskType == null) return "Unbekannte Aufgabe";
|
||||
if (taskType == null)
|
||||
return "Unbekannte Aufgabe";
|
||||
|
||||
return switch (taskType.toUpperCase()) {
|
||||
case "PHOTO" -> "Foto-Aufgabe";
|
||||
case "SIGNATURE" -> "Unterschrift";
|
||||
case "BARCODE" -> "Barcode scannen";
|
||||
case "CONFIRMATION" -> "Bestätigung";
|
||||
case "TODO_LIST" -> "Checkliste";
|
||||
default -> taskType;
|
||||
case "PHOTO" -> "Foto-Aufgabe";
|
||||
case "SIGNATURE" -> "Unterschrift";
|
||||
case "BARCODE" -> "Barcode scannen";
|
||||
case "CONFIRMATION" -> "Bestätigung";
|
||||
case "TODO_LIST" -> "Checkliste";
|
||||
default -> taskType;
|
||||
};
|
||||
}
|
||||
|
||||
private String buildRouteString(Job job) {
|
||||
if (job.getPickupCity() == null && job.getDeliveryCity() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
StringBuilder route = new StringBuilder();
|
||||
if (job.getPickupCity() != null) {
|
||||
route.append(job.getPickupCity());
|
||||
}
|
||||
if (job.getPickupCity() != null && job.getDeliveryCity() != null) {
|
||||
route.append(" → ");
|
||||
}
|
||||
if (job.getDeliveryCity() != null) {
|
||||
route.append(job.getDeliveryCity());
|
||||
}
|
||||
return route.toString();
|
||||
}
|
||||
|
||||
public void checkAndSendJobCompletionNotification(ObjectId jobId, String completedBy) {
|
||||
try {
|
||||
// Check if all tasks for this job are completed
|
||||
@@ -174,7 +196,8 @@ public class EmailService {
|
||||
boolean allCompleted = allTasks.stream().allMatch(task -> task.isCompleted());
|
||||
|
||||
if (allCompleted) {
|
||||
log.info("All tasks completed for job {}, updating job status and sending completion notification", jobId);
|
||||
log.info("All tasks completed for job {}, updating job status and sending completion notification",
|
||||
jobId);
|
||||
|
||||
// Update job status to COMPLETED
|
||||
updateJobStatusToCompleted(jobId);
|
||||
@@ -240,7 +263,8 @@ public class EmailService {
|
||||
SimpleMailMessage message = new SimpleMailMessage();
|
||||
message.setFrom(smtpUsername);
|
||||
message.setTo(user.getEmail());
|
||||
message.setSubject("Job abgeschlossen - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
|
||||
message.setSubject(
|
||||
"Job abgeschlossen - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
|
||||
|
||||
String fullName = buildFullName(user);
|
||||
String appUserName = buildAppUserName(appUser);
|
||||
@@ -299,8 +323,8 @@ public class EmailService {
|
||||
job.setUpdatedAt(java.time.LocalDateTime.now());
|
||||
jobRepository.save(job);
|
||||
|
||||
log.info("Job status updated from {} to COMPLETED for job {}",
|
||||
oldStatus != null ? oldStatus : "null", job.getJobNumber());
|
||||
log.info("Job status updated from {} to COMPLETED for job {}", oldStatus != null ? oldStatus : "null",
|
||||
job.getJobNumber());
|
||||
} else {
|
||||
log.debug("Job {} already has COMPLETED status", job.getJobNumber());
|
||||
}
|
||||
@@ -354,7 +378,8 @@ public class EmailService {
|
||||
SimpleMailMessage message = new SimpleMailMessage();
|
||||
message.setFrom(smtpUsername);
|
||||
message.setTo(user.getEmail());
|
||||
message.setSubject("Neuer Job erstellt - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
|
||||
message.setSubject(
|
||||
"Neuer Job erstellt - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
|
||||
|
||||
String fullName = buildFullName(user);
|
||||
|
||||
@@ -389,14 +414,18 @@ public class EmailService {
|
||||
body.append("Anzahl Aufgaben: ").append(taskCount).append("\n");
|
||||
}
|
||||
|
||||
body.append("Status: ").append(job.getStatus() != null ? job.getStatus().getDisplayName() : "Unbekannt").append("\n");
|
||||
body.append("Status: ").append(job.getStatus() != null ? job.getStatus().getDisplayName() : "Unbekannt")
|
||||
.append("\n");
|
||||
|
||||
if (job.getRemark() != null && !job.getRemark().isBlank()) {
|
||||
body.append("Bemerkung: ").append(job.getRemark()).append("\n");
|
||||
}
|
||||
|
||||
body.append("Erstellt am: ").append(job.getCreatedAt() != null ?
|
||||
job.getCreatedAt().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")) : "Unbekannt").append("\n\n");
|
||||
body.append("Erstellt am: ")
|
||||
.append(job.getCreatedAt() != null
|
||||
? job.getCreatedAt().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))
|
||||
: "Unbekannt")
|
||||
.append("\n\n");
|
||||
|
||||
body.append("Der Job ist nun im System verfügbar und kann bearbeitet werden.\n\n");
|
||||
body.append("Mit freundlichen Grüßen,\n");
|
||||
@@ -405,4 +434,24 @@ public class EmailService {
|
||||
message.setText(body.toString());
|
||||
mailSender.send(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a simple text email
|
||||
*/
|
||||
public void sendSimpleEmail(String to, String subject, String body) {
|
||||
try {
|
||||
SimpleMailMessage message = new SimpleMailMessage();
|
||||
message.setFrom(smtpUsername);
|
||||
message.setTo(to);
|
||||
message.setSubject(subject);
|
||||
message.setText(body);
|
||||
|
||||
mailSender.send(message);
|
||||
log.debug("Simple email sent to {} with subject: {}", to, subject);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send simple email to {} with subject '{}': {}", to, subject, e.getMessage(), e);
|
||||
throw new RuntimeException("Failed to send email: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,15 +29,9 @@ public class JobHistoryService {
|
||||
*/
|
||||
public void logJobCreation(Job job, String createdBy) {
|
||||
try {
|
||||
JobHistory history = new JobHistory(
|
||||
job.getId(),
|
||||
"Job erstellt",
|
||||
"Neuer Job wurde erstellt: " + (job.getJobNumber() != null ? job.getJobNumber() : "Ohne Nummer"),
|
||||
createdBy,
|
||||
JobHistoryType.CREATE,
|
||||
null,
|
||||
"Job erstellt"
|
||||
);
|
||||
JobHistory history = new JobHistory(job.getId(), "Job erstellt",
|
||||
"Neuer Job wurde erstellt: " + (job.getJobNumber() != null ? job.getJobNumber() : "Ohne Nummer"),
|
||||
createdBy, JobHistoryType.CREATE, null, "Job erstellt");
|
||||
|
||||
if (job.getDeliveryCompany() != null) {
|
||||
history.setDetails("Kunde: " + job.getDeliveryCompany());
|
||||
@@ -55,19 +49,12 @@ public class JobHistoryService {
|
||||
*/
|
||||
public void logStatusChange(Job job, JobStatus oldStatus, JobStatus newStatus, String changedBy) {
|
||||
try {
|
||||
String description = String.format("Status geändert von %s zu %s",
|
||||
formatStatus(oldStatus),
|
||||
formatStatus(newStatus));
|
||||
String description = String.format("Status geändert von %s zu %s", formatStatus(oldStatus),
|
||||
formatStatus(newStatus));
|
||||
|
||||
JobHistory history = new JobHistory(
|
||||
job.getId(),
|
||||
"Status-Änderung",
|
||||
description,
|
||||
changedBy,
|
||||
JobHistoryType.STATUS_CHANGE,
|
||||
oldStatus != null ? oldStatus.toString() : null,
|
||||
newStatus != null ? newStatus.toString() : null
|
||||
);
|
||||
JobHistory history = new JobHistory(job.getId(), "Status-Änderung", description, changedBy,
|
||||
JobHistoryType.STATUS_CHANGE, oldStatus != null ? oldStatus.toString() : null,
|
||||
newStatus != null ? newStatus.toString() : null);
|
||||
|
||||
jobHistoryRepository.save(history);
|
||||
log.debug("Status change logged for job {}: {} -> {}", job.getIdAsString(), oldStatus, newStatus);
|
||||
@@ -83,15 +70,9 @@ public class JobHistoryService {
|
||||
try {
|
||||
String description = generateUpdateDescription(oldJob, newJob);
|
||||
|
||||
JobHistory history = new JobHistory(
|
||||
newJob.getId(),
|
||||
reason != null ? reason : "Job aktualisiert",
|
||||
description,
|
||||
changedBy,
|
||||
JobHistoryType.UPDATE,
|
||||
serializeJobForComparison(oldJob),
|
||||
serializeJobForComparison(newJob)
|
||||
);
|
||||
JobHistory history = new JobHistory(newJob.getId(), reason != null ? reason : "Job aktualisiert",
|
||||
description, changedBy, JobHistoryType.UPDATE, serializeJobForComparison(oldJob),
|
||||
serializeJobForComparison(newJob));
|
||||
|
||||
jobHistoryRepository.save(history);
|
||||
log.debug("Job update logged for job {}", newJob.getIdAsString());
|
||||
@@ -111,7 +92,7 @@ public class JobHistoryService {
|
||||
* Log task completion with detailed information and extraData
|
||||
*/
|
||||
public void logTaskCompletion(ObjectId jobId, String taskType, String taskId, String completedBy,
|
||||
String taskDisplayName, String extraDataSummary) {
|
||||
String taskDisplayName, String extraDataSummary) {
|
||||
try {
|
||||
String taskName = taskDisplayName != null ? taskDisplayName : taskType;
|
||||
String description = String.format("Aufgabe abgeschlossen: %s", taskName);
|
||||
@@ -120,15 +101,8 @@ public class JobHistoryService {
|
||||
description += " - " + extraDataSummary;
|
||||
}
|
||||
|
||||
JobHistory history = new JobHistory(
|
||||
jobId,
|
||||
"Aufgabe abgeschlossen",
|
||||
description,
|
||||
completedBy,
|
||||
JobHistoryType.TASK_COMPLETED,
|
||||
"In Bearbeitung",
|
||||
"Abgeschlossen"
|
||||
);
|
||||
JobHistory history = new JobHistory(jobId, "Aufgabe abgeschlossen", description, completedBy,
|
||||
JobHistoryType.TASK_COMPLETED, "In Bearbeitung", "Abgeschlossen");
|
||||
|
||||
// Detaillierte Informationen in details speichern
|
||||
StringBuilder details = new StringBuilder();
|
||||
@@ -165,15 +139,8 @@ public class JobHistoryService {
|
||||
description = String.format("Job-Zuweisung geändert von %s zu %s", oldAssignee, newAssignee);
|
||||
}
|
||||
|
||||
JobHistory history = new JobHistory(
|
||||
job.getId(),
|
||||
"Zuweisung geändert",
|
||||
description,
|
||||
changedBy,
|
||||
JobHistoryType.ASSIGNMENT,
|
||||
oldAssignee,
|
||||
newAssignee
|
||||
);
|
||||
JobHistory history = new JobHistory(job.getId(), "Zuweisung geändert", description, changedBy,
|
||||
JobHistoryType.ASSIGNMENT, oldAssignee, newAssignee);
|
||||
|
||||
jobHistoryRepository.save(history);
|
||||
log.debug("Job assignment logged for job {}", job.getIdAsString());
|
||||
@@ -186,7 +153,7 @@ public class JobHistoryService {
|
||||
* Log custom event
|
||||
*/
|
||||
public void logCustomEvent(ObjectId jobId, String reason, String description, String changedBy,
|
||||
JobHistoryType type) {
|
||||
JobHistoryType type) {
|
||||
try {
|
||||
JobHistory history = new JobHistory(jobId, reason, description, changedBy, type, null, null);
|
||||
jobHistoryRepository.save(history);
|
||||
@@ -213,18 +180,19 @@ public class JobHistoryService {
|
||||
// Helper methods
|
||||
|
||||
private String formatStatus(JobStatus status) {
|
||||
if (status == null) return "Unbekannt";
|
||||
if (status == null)
|
||||
return "Unbekannt";
|
||||
|
||||
return switch (status) {
|
||||
case CREATED -> "Erstellt";
|
||||
case IN_PROGRESS -> "In Bearbeitung";
|
||||
case PICKUP_SCHEDULED -> "Abholung geplant";
|
||||
case PICKED_UP -> "Abgeholt";
|
||||
case IN_TRANSIT -> "Unterwegs";
|
||||
case DELIVERED -> "Zugestellt";
|
||||
case COMPLETED -> "Abgeschlossen";
|
||||
case CANCELLED -> "Storniert";
|
||||
default -> status.toString();
|
||||
case CREATED -> "Erstellt";
|
||||
case IN_PROGRESS -> "In Bearbeitung";
|
||||
case PICKUP_SCHEDULED -> "Abholung geplant";
|
||||
case PICKED_UP -> "Abgeholt";
|
||||
case IN_TRANSIT -> "Unterwegs";
|
||||
case DELIVERED -> "Zugestellt";
|
||||
case COMPLETED -> "Abgeschlossen";
|
||||
case CANCELLED -> "Storniert";
|
||||
default -> status.toString();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -243,15 +211,17 @@ public class JobHistoryService {
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (!equals(oldJob.getPickupCity(), newJob.getPickupCity()) ||
|
||||
!equals(oldJob.getDeliveryCity(), newJob.getDeliveryCity())) {
|
||||
if (hasChanges) description.append(",");
|
||||
if (!equals(oldJob.getPickupCity(), newJob.getPickupCity())
|
||||
|| !equals(oldJob.getDeliveryCity(), newJob.getDeliveryCity())) {
|
||||
if (hasChanges)
|
||||
description.append(",");
|
||||
description.append(" - Orte");
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (!equals(oldJob.getRemark(), newJob.getRemark())) {
|
||||
if (hasChanges) description.append(",");
|
||||
if (hasChanges)
|
||||
description.append(",");
|
||||
description.append(" - Bemerkung");
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user