Erweiterungen

This commit is contained in:
2025-09-13 18:45:57 +02:00
parent 079f10d047
commit f4656cd193
11 changed files with 385 additions and 12 deletions

View File

@@ -0,0 +1,158 @@
package de.assecutor.votianlt.config;
import de.assecutor.votianlt.model.task.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.bson.Document;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class MongoConfig {
@Bean
public MongoCustomConversions customConversions() {
List<Converter<?, ?>> converters = new ArrayList<>();
converters.add(new DocumentToBaseTaskConverter());
return new MongoCustomConversions(converters);
}
@ReadingConverter
@Slf4j
public static class DocumentToBaseTaskConverter implements Converter<Document, BaseTask> {
@Override
public BaseTask convert(Document source) {
// Debug logging to see what's in the document
log.debug("Converting MongoDB document to BaseTask. Document keys: {}", source.keySet());
log.debug("Full document content: {}", source.toJson());
// Use _class field for type discrimination (MongoDB standard)
String className = source.getString("_class");
if (className == null) {
// Fallback to taskType field if _class is not present
String taskType = source.getString("taskType");
if (taskType == null) {
taskType = source.getString("task_type");
}
// Map taskType to class name
className = mapTaskTypeToClassName(taskType);
}
log.debug("Extracted className: '{}' from document", className);
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;
}
// Set common fields
if (source.containsKey("_id")) {
task.setId(source.getObjectId("_id"));
}
if (source.containsKey("job_id")) {
task.setJobId(source.getObjectId("job_id"));
}
if (source.containsKey("text")) {
task.setText(source.getString("text"));
}
if (source.containsKey("task_order")) {
task.setTaskOrder(source.getInteger("task_order", 0));
}
if (source.containsKey("completed")) {
task.setCompleted(source.getBoolean("completed", false));
}
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));
} else if (completedAtObj instanceof java.util.Date) {
task.setCompletedAt(((java.util.Date) completedAtObj).toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime());
}
}
if (source.containsKey("completed_by")) {
task.setCompletedBy(source.getString("completed_by"));
}
if (source.containsKey("completion_note")) {
task.setCompletionNote(source.getString("completion_note"));
}
return task;
}
private String mapTaskTypeToClassName(String taskType) {
if (taskType == null) {
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";
}
}
}
}

View File

@@ -13,6 +13,8 @@ import de.assecutor.votianlt.repository.CargoItemRepository;
import de.assecutor.votianlt.repository.JobRepository;
import de.assecutor.votianlt.repository.TaskRepository;
import lombok.extern.slf4j.Slf4j;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
@@ -177,6 +179,7 @@ public class MessageController {
@SendToUser("/queue/jobs")
public List<JobWithRelatedDataDTO> handleGetAssignedJobs(Map<String, Object> request) {
log.info("STOMP Endpoint '/app/jobs/assigned' called with data: {}", request);
log.debug("Starting to process jobs request for STOMP endpoint");
if (request == null || !request.containsKey("appUserId")) {
log.info("STOMP Response for '/app/jobs/assigned' sent to '/user/queue/jobs': empty list (no appUserId provided)");
@@ -191,6 +194,7 @@ public class MessageController {
// Find jobs assigned to this app user
List<Job> assignedJobs = jobRepository.findByAppUser(appUserId);
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()
@@ -211,6 +215,21 @@ public class MessageController {
log.info("STOMP Response for '/app/jobs/assigned' sent to '/user/queue/jobs': {} jobs with related data found for appUserId='{}'",
jobsWithRelatedData.size(), appUserId);
// Log complete JSON for debugging
log.debug("About to serialize {} jobs to JSON for logging", jobsWithRelatedData.size());
try {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
String jsonOutput = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jobsWithRelatedData);
log.info("=== COMPLETE JSON RESPONSE FOR STOMP CLIENT ===");
log.info("AppUserId: {}", appUserId);
log.info("Number of jobs: {}", jobsWithRelatedData.size());
log.info("JSON Data:\n{}", jsonOutput);
log.info("=== END JSON RESPONSE ===");
} catch (Exception e) {
log.error("Failed to serialize jobs to JSON for logging: {}", e.getMessage(), e);
}
return jobsWithRelatedData;
}
@@ -280,4 +299,138 @@ public class MessageController {
return response;
}
}
/**
* Report task confirmation completion from apps.
* Client sends to /app/task/confirm with payload { taskId, completedBy?, note? }.
* Broadcasts to /topic/task-updates and /topic/tasks/{taskId}.
*/
@MessageMapping("/task/confirm")
@SendTo("/topic/task-updates")
public Map<String, Object> handleTaskConfirmation(Map<String, Object> payload) {
log.info("STOMP Endpoint '/app/task/confirm' called with data: {}", payload);
return processTaskCompletion(payload, "CONFIRMATION");
}
/**
* Report photo task completion from apps.
* Client sends to /app/task/photo/completed with payload { taskId, completedBy?, note? }.
* Broadcasts to /topic/task-updates and /topic/tasks/{taskId}.
*/
@MessageMapping("/task/photo/completed")
@SendTo("/topic/task-updates")
public Map<String, Object> handlePhotoTaskCompleted(Map<String, Object> payload) {
log.info("STOMP Endpoint '/app/task/photo/completed' called with data: {}", payload);
return processTaskCompletion(payload, "PHOTO");
}
/**
* Report signature task completion from apps.
* Client sends to /app/task/signature/completed with payload { taskId, completedBy?, note? }.
* Broadcasts to /topic/task-updates and /topic/tasks/{taskId}.
*/
@MessageMapping("/task/signature/completed")
@SendTo("/topic/task-updates")
public Map<String, Object> handleSignatureTaskCompleted(Map<String, Object> payload) {
log.info("STOMP Endpoint '/app/task/signature/completed' called with data: {}", payload);
return processTaskCompletion(payload, "SIGNATURE");
}
/**
* Report barcode task completion from apps.
* Client sends to /app/task/barcode/completed with payload { taskId, completedBy?, note? }.
* Broadcasts to /topic/task-updates and /topic/tasks/{taskId}.
*/
@MessageMapping("/task/barcode/completed")
@SendTo("/topic/task-updates")
public Map<String, Object> handleBarcodeTaskCompleted(Map<String, Object> payload) {
log.info("STOMP Endpoint '/app/task/barcode/completed' called with data: {}", payload);
return processTaskCompletion(payload, "BARCODE");
}
/**
* Report todolist task completion from apps.
* Client sends to /app/task/todolist/completed with payload { taskId, completedBy?, note? }.
* Broadcasts to /topic/task-updates and /topic/tasks/{taskId}.
*/
@MessageMapping("/task/todolist/completed")
@SendTo("/topic/task-updates")
public Map<String, Object> handleTodolistTaskCompleted(Map<String, Object> payload) {
log.info("STOMP Endpoint '/app/task/todolist/completed' called with data: {}", payload);
return processTaskCompletion(payload, "TODOLIST");
}
/**
* Common method to process task completion for different task types.
* This method contains the shared logic for all task completion endpoints.
*/
private Map<String, Object> processTaskCompletion(Map<String, Object> payload, String expectedTaskType) {
Map<String, Object> response = new java.util.HashMap<>();
response.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
response.put("type", "taskCompletedAck");
if (payload == null || !payload.containsKey("taskId") || payload.get("taskId") == null || payload.get("taskId").toString().isBlank()) {
response.put("success", false);
response.put("message", "taskId ist erforderlich");
log.info("Task completion failed: {}", response);
return response;
}
String taskIdStr = payload.get("taskId").toString();
String completedBy = payload.get("completedBy") != null ? payload.get("completedBy").toString() : null;
String note = payload.get("note") != null ? payload.get("note").toString() : null;
try {
org.bson.types.ObjectId taskId = new org.bson.types.ObjectId(taskIdStr);
java.util.Optional<BaseTask> opt = taskRepository.findById(taskId);
if (opt.isEmpty()) {
response.put("success", false);
response.put("message", "Task nicht gefunden");
return response;
}
BaseTask task = opt.get();
// Validate task type matches the endpoint
if (expectedTaskType != null && !expectedTaskType.equals(task.getTaskType())) {
response.put("success", false);
response.put("message", "Task-Typ stimmt nicht mit dem Endpunkt überein. Erwartet: " + expectedTaskType + ", Gefunden: " + task.getTaskType());
log.warn("Task type mismatch for taskId={}: expected={}, actual={}", taskIdStr, expectedTaskType, task.getTaskType());
return response;
}
task.setCompleted(true);
task.setCompletedAt(LocalDateTime.now());
if (completedBy != null) task.setCompletedBy(completedBy);
if (note != null) task.setCompletionNote(note);
taskRepository.save(task);
java.util.Map<String, Object> event = new java.util.HashMap<>();
event.put("taskId", task.getIdAsString());
event.put("jobId", task.getJobIdAsString());
event.put("completed", task.isCompleted());
event.put("completedAt", task.getCompletedAt() != null ? task.getCompletedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) : null);
event.put("completedBy", task.getCompletedBy());
event.put("note", task.getCompletionNote());
event.put("event", "taskCompleted");
event.put("taskType", task.getTaskType());
// Send specific task topic
messagingTemplate.convertAndSend("/topic/tasks/" + task.getIdAsString(), event);
response.put("success", true);
response.putAll(event);
log.info("Task completion processed successfully for taskId={}, taskType={}", taskIdStr, task.getTaskType());
return response;
} catch (IllegalArgumentException e) {
response.put("success", false);
response.put("message", "Ungültige taskId");
return response;
} catch (Exception e) {
log.error("Error processing task completion", e);
response.put("success", false);
response.put("message", "Fehler bei der Verarbeitung");
return response;
}
}
}

View File

@@ -27,9 +27,6 @@ public class TaskEntry {
@JsonIgnore
private ObjectId jobId;
@Field("text")
private String text;
@Field("task_type")
private TaskType taskType = TaskType.CONFIRMATION;

View File

@@ -30,4 +30,15 @@ public class BarcodeTask extends BaseTask {
public String getDisplayName() {
return "Barcode";
}
@Override
public Object getTaskSpecificData() {
return new TaskSpecificData();
}
public class TaskSpecificData {
public String taskType = getTaskType();
public Integer minBarcodeCount = BarcodeTask.this.minBarcodeCount;
public Integer maxBarcodeCount = BarcodeTask.this.maxBarcodeCount;
}
}

View File

@@ -77,4 +77,10 @@ public abstract class BaseTask {
* Returns the display name for this task type.
*/
public abstract String getDisplayName();
/**
* Returns task-specific data for JSON serialization.
*/
@JsonGetter("taskSpecificData")
public abstract Object getTaskSpecificData();
}

View File

@@ -26,4 +26,14 @@ public class ConfirmationTask extends BaseTask {
public String getDisplayName() {
return "Bestätigung";
}
@Override
public Object getTaskSpecificData() {
return new TaskSpecificData();
}
public class TaskSpecificData {
public String taskType = getTaskType();
public String buttonText = ConfirmationTask.this.buttonText;
}
}

View File

@@ -30,4 +30,15 @@ public class PhotoTask extends BaseTask {
public String getDisplayName() {
return "Foto";
}
@Override
public Object getTaskSpecificData() {
return new TaskSpecificData();
}
public class TaskSpecificData {
public String taskType = getTaskType();
public Integer minPhotoCount = PhotoTask.this.minPhotoCount;
public Integer maxPhotoCount = PhotoTask.this.maxPhotoCount;
}
}

View File

@@ -19,4 +19,14 @@ public class SignatureTask extends BaseTask {
public String getDisplayName() {
return "Unterschrift";
}
@Override
public Object getTaskSpecificData() {
return new TaskSpecificData();
}
public class TaskSpecificData {
public String taskType = getTaskType();
// No specific data for signature task
}
}

View File

@@ -28,4 +28,14 @@ public class TodoListTask extends BaseTask {
public String getDisplayName() {
return "To-Do Liste";
}
@Override
public Object getTaskSpecificData() {
return new TaskSpecificData();
}
public class TaskSpecificData {
public String taskType = getTaskType();
public List<String> todoItems = TodoListTask.this.todoItems;
}
}

View File

@@ -1504,7 +1504,7 @@ public class AddJobView extends Main {
taskContainer.add(taskTypeCombo, configContainer);
taskContainer.add(deleteXButton);
// Create TaskEntry and add to state with correct order
// Create Task and add to state with correct order
BaseTask task = new ConfirmationTask("");
task.setTaskOrder(tasksState.size()); // Set order based on current position
tasksState.add(task);

View File

@@ -1,6 +1,9 @@
server.port=${PORT:8080}
server.address=0.0.0.0
logging.level.org.atmosphere=warn
logging.level.de.assecutor.votianlt=INFO
logging.level.de.assecutor.votianlt.controller.MessageController=DEBUG
logging.level.de.assecutor.votianlt.config.MongoConfig=DEBUG
spring.mustache.check-template-location=false
# Launch the default browser when starting the application in development mode
@@ -14,6 +17,10 @@ spring.jpa.open-in-view=false
# MongoDB
spring.data.mongodb.uri=mongodb://192.168.180.25:27017/votianlt
spring.data.mongodb.auto-index-creation=true
spring.data.mongodb.socket-timeout=30000
spring.data.mongodb.connect-timeout=10000
spring.data.mongodb.server-selection-timeout=5000
# Mail Configuration
mail.smtp.username=no-reply@appcreation.de