diff --git a/src/main/java/de/assecutor/votianlt/config/MongoConfig.java b/src/main/java/de/assecutor/votianlt/config/MongoConfig.java new file mode 100644 index 0000000..9710165 --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/config/MongoConfig.java @@ -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> converters = new ArrayList<>(); + converters.add(new DocumentToBaseTaskConverter()); + return new MongoCustomConversions(converters); + } + + @ReadingConverter + @Slf4j + public static class DocumentToBaseTaskConverter implements Converter { + + @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 todoItems = (List) 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"; + } + } + } +} \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/controller/MessageController.java b/src/main/java/de/assecutor/votianlt/controller/MessageController.java index 5fcb659..4240bcf 100644 --- a/src/main/java/de/assecutor/votianlt/controller/MessageController.java +++ b/src/main/java/de/assecutor/votianlt/controller/MessageController.java @@ -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 handleGetAssignedJobs(Map 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,7 +194,8 @@ public class MessageController { // Find jobs assigned to this app user List 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 jobsWithRelatedData = assignedJobs.stream() .map(job -> { @@ -210,7 +214,22 @@ 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 handleTaskConfirmation(Map 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 handlePhotoTaskCompleted(Map 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 handleSignatureTaskCompleted(Map 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 handleBarcodeTaskCompleted(Map 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 handleTodolistTaskCompleted(Map 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 processTaskCompletion(Map payload, String expectedTaskType) { + Map 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 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 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; + } + } } \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/model/TaskEntry.java b/src/main/java/de/assecutor/votianlt/model/TaskEntry.java index e0be32b..ab087e9 100644 --- a/src/main/java/de/assecutor/votianlt/model/TaskEntry.java +++ b/src/main/java/de/assecutor/votianlt/model/TaskEntry.java @@ -27,9 +27,6 @@ public class TaskEntry { @JsonIgnore private ObjectId jobId; - @Field("text") - private String text; - @Field("task_type") private TaskType taskType = TaskType.CONFIRMATION; diff --git a/src/main/java/de/assecutor/votianlt/model/task/BarcodeTask.java b/src/main/java/de/assecutor/votianlt/model/task/BarcodeTask.java index 9e3805b..cd88c8b 100644 --- a/src/main/java/de/assecutor/votianlt/model/task/BarcodeTask.java +++ b/src/main/java/de/assecutor/votianlt/model/task/BarcodeTask.java @@ -9,10 +9,10 @@ import org.springframework.data.mongodb.core.mapping.Field; @NoArgsConstructor @EqualsAndHashCode(callSuper = true) public class BarcodeTask extends BaseTask { - + @Field("min_barcode_count") private Integer minBarcodeCount; - + @Field("max_barcode_count") private Integer maxBarcodeCount; @@ -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; + } } diff --git a/src/main/java/de/assecutor/votianlt/model/task/BaseTask.java b/src/main/java/de/assecutor/votianlt/model/task/BaseTask.java index 2f27603..65fc8f2 100644 --- a/src/main/java/de/assecutor/votianlt/model/task/BaseTask.java +++ b/src/main/java/de/assecutor/votianlt/model/task/BaseTask.java @@ -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(); } diff --git a/src/main/java/de/assecutor/votianlt/model/task/ConfirmationTask.java b/src/main/java/de/assecutor/votianlt/model/task/ConfirmationTask.java index ba230b0..5fae187 100644 --- a/src/main/java/de/assecutor/votianlt/model/task/ConfirmationTask.java +++ b/src/main/java/de/assecutor/votianlt/model/task/ConfirmationTask.java @@ -9,7 +9,7 @@ import org.springframework.data.mongodb.core.mapping.Field; @NoArgsConstructor @EqualsAndHashCode(callSuper = true) public class ConfirmationTask extends BaseTask { - + @Field("button_text") private String buttonText; @@ -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; + } } diff --git a/src/main/java/de/assecutor/votianlt/model/task/PhotoTask.java b/src/main/java/de/assecutor/votianlt/model/task/PhotoTask.java index 1cae814..37f3914 100644 --- a/src/main/java/de/assecutor/votianlt/model/task/PhotoTask.java +++ b/src/main/java/de/assecutor/votianlt/model/task/PhotoTask.java @@ -9,10 +9,10 @@ import org.springframework.data.mongodb.core.mapping.Field; @NoArgsConstructor @EqualsAndHashCode(callSuper = true) public class PhotoTask extends BaseTask { - + @Field("min_photo_count") private Integer minPhotoCount; - + @Field("max_photo_count") private Integer maxPhotoCount; @@ -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; + } } diff --git a/src/main/java/de/assecutor/votianlt/model/task/SignatureTask.java b/src/main/java/de/assecutor/votianlt/model/task/SignatureTask.java index a5619de..e1dfc04 100644 --- a/src/main/java/de/assecutor/votianlt/model/task/SignatureTask.java +++ b/src/main/java/de/assecutor/votianlt/model/task/SignatureTask.java @@ -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 + } } diff --git a/src/main/java/de/assecutor/votianlt/model/task/TodoListTask.java b/src/main/java/de/assecutor/votianlt/model/task/TodoListTask.java index c7becf4..1731253 100644 --- a/src/main/java/de/assecutor/votianlt/model/task/TodoListTask.java +++ b/src/main/java/de/assecutor/votianlt/model/task/TodoListTask.java @@ -11,7 +11,7 @@ import java.util.List; @NoArgsConstructor @EqualsAndHashCode(callSuper = true) public class TodoListTask extends BaseTask { - + @Field("todo_items") private List todoItems; @@ -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 todoItems = TodoListTask.this.todoItems; + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java index 99bf2a5..ce15dff 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java @@ -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); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index fdefb93..e4a48a2 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -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