Erweiterungen
This commit is contained in:
@@ -128,9 +128,6 @@ public class MongoConfig {
|
|||||||
if (source.containsKey("completed_by")) {
|
if (source.containsKey("completed_by")) {
|
||||||
task.setCompletedBy(source.getString("completed_by"));
|
task.setCompletedBy(source.getString("completed_by"));
|
||||||
}
|
}
|
||||||
if (source.containsKey("completion_note")) {
|
|
||||||
task.setCompletionNote(source.getString("completion_note"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import de.assecutor.votianlt.dto.JobWithRelatedDataDTO;
|
|||||||
import de.assecutor.votianlt.model.AppUser;
|
import de.assecutor.votianlt.model.AppUser;
|
||||||
import de.assecutor.votianlt.model.CargoItem;
|
import de.assecutor.votianlt.model.CargoItem;
|
||||||
import de.assecutor.votianlt.model.Job;
|
import de.assecutor.votianlt.model.Job;
|
||||||
import de.assecutor.votianlt.model.Photo;
|
|
||||||
import de.assecutor.votianlt.model.task.BaseTask;
|
import de.assecutor.votianlt.model.task.BaseTask;
|
||||||
import de.assecutor.votianlt.pages.service.AppUserService;
|
import de.assecutor.votianlt.pages.service.AppUserService;
|
||||||
import de.assecutor.votianlt.repository.AppUserRepository;
|
import de.assecutor.votianlt.repository.AppUserRepository;
|
||||||
@@ -14,6 +13,11 @@ import de.assecutor.votianlt.repository.CargoItemRepository;
|
|||||||
import de.assecutor.votianlt.repository.JobRepository;
|
import de.assecutor.votianlt.repository.JobRepository;
|
||||||
import de.assecutor.votianlt.repository.PhotoRepository;
|
import de.assecutor.votianlt.repository.PhotoRepository;
|
||||||
import de.assecutor.votianlt.repository.TaskRepository;
|
import de.assecutor.votianlt.repository.TaskRepository;
|
||||||
|
import de.assecutor.votianlt.repository.BarcodeRepository;
|
||||||
|
import de.assecutor.votianlt.repository.SignatureRepository;
|
||||||
|
import de.assecutor.votianlt.model.Photo;
|
||||||
|
import de.assecutor.votianlt.model.Barcode;
|
||||||
|
import de.assecutor.votianlt.model.Signature;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
@@ -21,10 +25,10 @@ import de.assecutor.votianlt.mqtt.MqttPublisher;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import org.bson.types.ObjectId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MQTT message controller for handling real-time communication with apps.
|
* MQTT message controller for handling real-time communication with apps.
|
||||||
@@ -48,10 +52,11 @@ public class MessageController {
|
|||||||
private final CargoItemRepository cargoItemRepository;
|
private final CargoItemRepository cargoItemRepository;
|
||||||
|
|
||||||
private final TaskRepository taskRepository;
|
private final TaskRepository taskRepository;
|
||||||
|
|
||||||
private final PhotoRepository photoRepository;
|
private final PhotoRepository photoRepository;
|
||||||
|
private final BarcodeRepository barcodeRepository;
|
||||||
|
private final SignatureRepository signatureRepository;
|
||||||
|
|
||||||
public MessageController(MqttPublisher mqttPublisher, AppUserRepository appUserRepository, AppUserService appUserService, JobRepository jobRepository, CargoItemRepository cargoItemRepository, TaskRepository taskRepository, PhotoRepository photoRepository) {
|
public MessageController(MqttPublisher mqttPublisher, AppUserRepository appUserRepository, AppUserService appUserService, JobRepository jobRepository, CargoItemRepository cargoItemRepository, TaskRepository taskRepository, PhotoRepository photoRepository, BarcodeRepository barcodeRepository, SignatureRepository signatureRepository) {
|
||||||
this.mqttPublisher = mqttPublisher;
|
this.mqttPublisher = mqttPublisher;
|
||||||
this.appUserRepository = appUserRepository;
|
this.appUserRepository = appUserRepository;
|
||||||
this.appUserService = appUserService;
|
this.appUserService = appUserService;
|
||||||
@@ -59,6 +64,8 @@ public class MessageController {
|
|||||||
this.cargoItemRepository = cargoItemRepository;
|
this.cargoItemRepository = cargoItemRepository;
|
||||||
this.taskRepository = taskRepository;
|
this.taskRepository = taskRepository;
|
||||||
this.photoRepository = photoRepository;
|
this.photoRepository = photoRepository;
|
||||||
|
this.barcodeRepository = barcodeRepository;
|
||||||
|
this.signatureRepository = signatureRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -182,197 +189,216 @@ public class MessageController {
|
|||||||
* This endpoint accepts any task type (fallback for GENERIC or unknown types).
|
* This endpoint accepts any task type (fallback for GENERIC or unknown types).
|
||||||
*/
|
*/
|
||||||
public void handleTaskCompleted(Map<String, Object> payload) {
|
public void handleTaskCompleted(Map<String, Object> payload) {
|
||||||
log.info("MQTT Endpoint '/app/task/completed' called with data: {}", payload);
|
// Backward-compatible entry point: extract taskType from payload (if present)
|
||||||
processTaskCompletion(payload, null);
|
// and delegate to the overloaded handler with explicit type.
|
||||||
}
|
String taskType = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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}.
|
|
||||||
*/
|
|
||||||
public void handleTaskConfirmation(Map<String, Object> payload) {
|
|
||||||
log.info("MQTT Endpoint '/app/task/confirm' called with data: {}", payload);
|
|
||||||
processTaskCompletion(payload, "CONFIRMATION");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Report photo task completion from apps.
|
|
||||||
* Client sends to /app/task/photo/completed with payload { taskId, completedBy?, note?, extraData? }.
|
|
||||||
* The extraData contains: { photos: base64List, count: base64List.length }
|
|
||||||
* Broadcasts to /topic/task-updates and /topic/tasks/{taskId}.
|
|
||||||
*/
|
|
||||||
public void handlePhotoTaskCompleted(Map<String, Object> payload) {
|
|
||||||
log.info("MQTT Endpoint '/app/task/photo/completed' called");
|
|
||||||
processPhotoTaskCompletion(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specialized method to process photo task completion with extraData handling.
|
|
||||||
* Saves photo data to the photos collection and processes task completion.
|
|
||||||
*/
|
|
||||||
private void processPhotoTaskCompletion(Map<String, Object> payload) {
|
|
||||||
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("Photo task completion failed: {}", response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
org.bson.types.ObjectId taskId = new org.bson.types.ObjectId(taskIdStr);
|
Object tt = payload != null ? payload.get("taskType") : null;
|
||||||
java.util.Optional<BaseTask> opt = taskRepository.findById(taskId);
|
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.
|
||||||
|
*/
|
||||||
|
public void handleTaskCompleted(Map<String, Object> payload, String taskType) {
|
||||||
|
String key = taskType == null ? "" : taskType.trim().toUpperCase();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processConfirmationTaskCompletion(Map<String, Object> payload) {
|
||||||
|
Object taskId = payload.get("taskId");
|
||||||
|
|
||||||
|
completeTask(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processTodoListTaskCompletion(Map<String, Object> payload) {
|
||||||
|
Object taskId = payload.get("taskId");
|
||||||
|
|
||||||
|
completeTask(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processBarcodeTaskCompletion(Map<String, Object> payload) {
|
||||||
|
Object taskId = payload.get("taskId");
|
||||||
|
try {
|
||||||
|
var opt = taskRepository.findById(new ObjectId(taskId.toString()));
|
||||||
if (opt.isEmpty()) {
|
if (opt.isEmpty()) {
|
||||||
response.put("success", false);
|
log.warn("Task not found for barcode completion. taskId={}", taskId);
|
||||||
response.put("message", "Task nicht gefunden");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
BaseTask task = opt.get();
|
BaseTask task = opt.get();
|
||||||
|
|
||||||
// Validate task type is PHOTO
|
Object extra = payload.get("extraData");
|
||||||
if (!"PHOTO".equals(task.getTaskType())) {
|
if (extra instanceof Map<?, ?> extraData) {
|
||||||
response.put("success", false);
|
Object barcodesObj = extraData.get("barcodes");
|
||||||
response.put("message", "Task-Typ stimmt nicht mit dem Endpunkt überein. Erwartet: PHOTO, Gefunden: " + task.getTaskType());
|
if (barcodesObj instanceof List<?> barcodesList) {
|
||||||
log.warn("Task type mismatch for taskId={}: expected=PHOTO, actual={}", taskIdStr, task.getTaskType());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process extraData if present
|
|
||||||
if (payload.containsKey("extraData") && payload.get("extraData") != null) {
|
|
||||||
try {
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Map<String, Object> extraData = (Map<String, Object>) payload.get("extraData");
|
List<String> barcodes = (List<String>) barcodesList;
|
||||||
|
|
||||||
if (extraData.containsKey("photos") && extraData.get("photos") != null) {
|
if (!barcodes.isEmpty()) {
|
||||||
@SuppressWarnings("unchecked")
|
for (String barcodeString : barcodes) {
|
||||||
List<String> base64Photos = (List<String>) extraData.get("photos");
|
Barcode barcodeEntry = new Barcode(
|
||||||
|
new ObjectId(taskId.toString()),
|
||||||
|
barcodeString,
|
||||||
|
task.getCompletedBy()
|
||||||
|
);
|
||||||
|
|
||||||
// Create and save Photo entity
|
barcodeRepository.save(barcodeEntry);
|
||||||
Photo photo = new Photo(task.getJobId(), task.getId(), base64Photos, completedBy);
|
}
|
||||||
photoRepository.save(photo);
|
|
||||||
|
|
||||||
log.info("Saved {} photos for taskId={}, jobId={}, photoId={}",
|
log.info("Saved {} barcodes for taskId={}", barcodes.size(), taskId);
|
||||||
base64Photos.size(), taskIdStr, task.getJobIdAsString(), photo.getIdAsString());
|
} else {
|
||||||
|
log.info("No barcodes found in extraData for taskId={}", taskId);
|
||||||
response.put("photoId", photo.getIdAsString());
|
|
||||||
response.put("photosCount", base64Photos.size());
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} else {
|
||||||
log.error("Error processing photo extraData for taskId={}: {}", taskIdStr, e.getMessage(), e);
|
log.warn("extraData.barcodes is not a List for taskId={}", taskId);
|
||||||
response.put("photoError", "Fehler beim Speichern der Fotos: " + e.getMessage());
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("extraData is not a Map for taskId={}", taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Complete the task
|
// Finally, mark the task as completed
|
||||||
task.setCompleted(true);
|
completeTask(taskId);
|
||||||
task.setCompletedAt(LocalDateTime.now());
|
} catch (IllegalArgumentException ex) {
|
||||||
if (completedBy != null) task.setCompletedBy(completedBy);
|
log.error("Invalid taskId format for barcode completion: {}", taskId);
|
||||||
if (note != null) task.setCompletionNote(note);
|
} catch (Exception ex) {
|
||||||
taskRepository.save(task);
|
log.error("Error while processing barcode task completion (taskId={}): {}", taskId, ex.getMessage(), ex);
|
||||||
|
|
||||||
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());
|
|
||||||
|
|
||||||
// Task event publishing has been removed
|
|
||||||
log.info("Task completed: taskId={}, taskType={}, completed={}", task.getIdAsString(), task.getTaskType(), task.isCompleted());
|
|
||||||
|
|
||||||
response.put("success", true);
|
|
||||||
response.putAll(event);
|
|
||||||
log.info("Photo task completion processed successfully for taskId={}", taskIdStr);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
response.put("success", false);
|
|
||||||
response.put("message", "Ungültige taskId");
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error processing photo task completion", e);
|
|
||||||
response.put("success", false);
|
|
||||||
response.put("message", "Fehler bei der Verarbeitung");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void processSignatureTaskCompletion(Map<String, Object> payload) {
|
||||||
* Common method to process task completion for different task types.
|
Object taskId = payload.get("taskId");
|
||||||
* This method contains the shared logic for all task completion endpoints.
|
|
||||||
*/
|
|
||||||
private void 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
org.bson.types.ObjectId taskId = new org.bson.types.ObjectId(taskIdStr);
|
var opt = taskRepository.findById(new ObjectId(taskId.toString()));
|
||||||
java.util.Optional<BaseTask> opt = taskRepository.findById(taskId);
|
|
||||||
if (opt.isEmpty()) {
|
if (opt.isEmpty()) {
|
||||||
response.put("success", false);
|
log.warn("Task not found for signature completion. taskId={}", taskId);
|
||||||
response.put("message", "Task nicht gefunden");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
BaseTask task = opt.get();
|
BaseTask task = opt.get();
|
||||||
|
|
||||||
// Validate task type matches the endpoint
|
Object extra = payload.get("extraData");
|
||||||
if (expectedTaskType != null && !expectedTaskType.equals(task.getTaskType())) {
|
if (extra instanceof Map<?, ?> extraData) {
|
||||||
response.put("success", false);
|
Object signatureSvgObj = extraData.get("signatureSvg");
|
||||||
response.put("message", "Task-Typ stimmt nicht mit dem Endpunkt überein. Erwartet: " + expectedTaskType + ", Gefunden: " + task.getTaskType());
|
if (signatureSvgObj instanceof String signatureSvg) {
|
||||||
log.warn("Task type mismatch for taskId={}: expected={}, actual={}", taskIdStr, expectedTaskType, task.getTaskType());
|
if (!signatureSvg.isBlank()) {
|
||||||
return;
|
Signature signatureEntry = new Signature(
|
||||||
|
new ObjectId(taskId.toString()),
|
||||||
|
signatureSvg,
|
||||||
|
task.getCompletedBy()
|
||||||
|
);
|
||||||
|
|
||||||
|
signatureRepository.save(signatureEntry);
|
||||||
|
log.info("Saved signature for taskId={}", taskId);
|
||||||
|
} else {
|
||||||
|
log.info("Empty signature SVG found for taskId={}", taskId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("extraData.signatureSvg is not a String for taskId={}", taskId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("extraData is not a Map for taskId={}", taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Finally, mark the task as completed
|
||||||
|
completeTask(taskId);
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
log.error("Invalid taskId format for signature completion: {}", taskId);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("Error while processing signature task completion (taskId={}): {}", taskId, ex.getMessage(), ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processPhotoTaskCompletion(Map<String, Object> payload) {
|
||||||
|
Object taskId = payload.get("taskId");
|
||||||
|
try {
|
||||||
|
var opt = taskRepository.findById(new ObjectId(taskId.toString()));
|
||||||
|
if (opt.isEmpty()) {
|
||||||
|
log.warn("Task not found for photo completion. taskId={}", taskId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BaseTask task = opt.get();
|
||||||
|
ObjectId jobId = new ObjectId(task.getJobIdAsString());
|
||||||
|
|
||||||
|
Object extra = payload.get("extraData");
|
||||||
|
if (extra instanceof Map<?, ?> extraData) {
|
||||||
|
Object photosObj = extraData.get("photos");
|
||||||
|
if (photosObj instanceof List<?> photosList) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> photos = (List<String>) photosList;
|
||||||
|
|
||||||
|
if (!photos.isEmpty()) {
|
||||||
|
for (String photoString: photos) {
|
||||||
|
Photo photoEntry = new Photo(
|
||||||
|
new ObjectId(taskId.toString()),
|
||||||
|
photoString,
|
||||||
|
task.getCompletedBy()
|
||||||
|
);
|
||||||
|
|
||||||
|
photoRepository.save(photoEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Saved {} photos for taskId={}, jobId={}", photos.size(), taskId, jobId);
|
||||||
|
} else {
|
||||||
|
log.info("No photos found in extraData for taskId={}", taskId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("extraData.photos is not a List for taskId={}", taskId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("extraData is not a Map for taskId={}", taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, mark the task as completed
|
||||||
|
completeTask(taskId);
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
log.error("Invalid taskId format for photo completion: {}", taskId);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("Error while processing photo task completion (taskId={}): {}", taskId, ex.getMessage(), ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void completeTask(Object tid) {
|
||||||
|
String taskIdStr = tid.toString();
|
||||||
|
try {
|
||||||
|
ObjectId taskId = new ObjectId(taskIdStr);
|
||||||
|
var opt = taskRepository.findById(taskId);
|
||||||
|
if (opt.isEmpty()) {
|
||||||
|
log.warn("Task not found for confirmation completion. taskId={}", taskIdStr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BaseTask task = opt.get();
|
||||||
task.setCompleted(true);
|
task.setCompleted(true);
|
||||||
task.setCompletedAt(LocalDateTime.now());
|
task.setCompletedAt(LocalDateTime.now());
|
||||||
if (completedBy != null) task.setCompletedBy(completedBy);
|
|
||||||
if (note != null) task.setCompletionNote(note);
|
|
||||||
taskRepository.save(task);
|
taskRepository.save(task);
|
||||||
|
log.info("Task marked completed. taskId={}, completedBy={}", taskIdStr, task.getCompletedBy());
|
||||||
java.util.Map<String, Object> event = new java.util.HashMap<>();
|
} catch (IllegalArgumentException ex) {
|
||||||
event.put("taskId", task.getIdAsString());
|
log.error("Invalid taskId format for completion: {}", taskIdStr);
|
||||||
event.put("jobId", task.getJobIdAsString());
|
} catch (Exception ex) {
|
||||||
event.put("completed", task.isCompleted());
|
log.error("Error while marking task completed (taskId={}): {}", taskIdStr, ex.getMessage(), ex);
|
||||||
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());
|
|
||||||
|
|
||||||
// Task event publishing has been removed
|
|
||||||
log.info("Task completed: taskId={}, taskType={}, completed={}", task.getIdAsString(), task.getTaskType(), task.isCompleted());
|
|
||||||
|
|
||||||
response.put("success", true);
|
|
||||||
response.putAll(event);
|
|
||||||
log.info("Task completion processed successfully for taskId={}, taskType={}", taskIdStr, task.getTaskType());
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
response.put("success", false);
|
|
||||||
response.put("message", "Ungültige taskId");
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error processing task completion", e);
|
|
||||||
response.put("success", false);
|
|
||||||
response.put("message", "Fehler bei der Verarbeitung");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
38
src/main/java/de/assecutor/votianlt/model/Barcode.java
Normal file
38
src/main/java/de/assecutor/votianlt/model/Barcode.java
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package de.assecutor.votianlt.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.mongodb.core.mapping.Document;
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Document(collection = "barcodes")
|
||||||
|
public class Barcode {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
private ObjectId id;
|
||||||
|
|
||||||
|
private ObjectId taskId;
|
||||||
|
private String barcode;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String completedBy;
|
||||||
|
|
||||||
|
// Default constructor
|
||||||
|
public Barcode() {
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor with parameters
|
||||||
|
public Barcode(ObjectId taskId, String barcode, String completedBy) {
|
||||||
|
this();
|
||||||
|
this.taskId = taskId;
|
||||||
|
this.barcode = barcode;
|
||||||
|
this.completedBy = completedBy;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package de.assecutor.votianlt.model;
|
package de.assecutor.votianlt.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
import org.springframework.data.annotation.Id;
|
import org.springframework.data.annotation.Id;
|
||||||
import org.springframework.data.mongodb.core.mapping.Document;
|
import org.springframework.data.mongodb.core.mapping.Document;
|
||||||
import org.bson.types.ObjectId;
|
import org.bson.types.ObjectId;
|
||||||
@@ -11,16 +12,15 @@ import java.util.List;
|
|||||||
* Photo entity for storing photo data from task completions.
|
* Photo entity for storing photo data from task completions.
|
||||||
* References the job ObjectId and stores base64 encoded photos.
|
* References the job ObjectId and stores base64 encoded photos.
|
||||||
*/
|
*/
|
||||||
|
@Data
|
||||||
@Document(collection = "photos")
|
@Document(collection = "photos")
|
||||||
public class Photo {
|
public class Photo {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
private ObjectId id;
|
private ObjectId id;
|
||||||
|
|
||||||
private ObjectId jobId;
|
|
||||||
private ObjectId taskId;
|
private ObjectId taskId;
|
||||||
private List<String> photos; // base64 encoded photos
|
private String photo; // base64 encoded photos
|
||||||
private int count;
|
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
private String completedBy;
|
private String completedBy;
|
||||||
|
|
||||||
@@ -30,94 +30,10 @@ public class Photo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Constructor with parameters
|
// Constructor with parameters
|
||||||
public Photo(ObjectId jobId, ObjectId taskId, List<String> photos, String completedBy) {
|
public Photo(ObjectId taskId, String photo, String completedBy) {
|
||||||
this();
|
this();
|
||||||
this.jobId = jobId;
|
|
||||||
this.taskId = taskId;
|
this.taskId = taskId;
|
||||||
this.photos = photos;
|
this.photo = photo;
|
||||||
this.count = photos != null ? photos.size() : 0;
|
|
||||||
this.completedBy = completedBy;
|
this.completedBy = completedBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getters and Setters
|
|
||||||
public ObjectId getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setId(ObjectId id) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getIdAsString() {
|
|
||||||
return id != null ? id.toHexString() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObjectId getJobId() {
|
|
||||||
return jobId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setJobId(ObjectId jobId) {
|
|
||||||
this.jobId = jobId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getJobIdAsString() {
|
|
||||||
return jobId != null ? jobId.toHexString() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObjectId getTaskId() {
|
|
||||||
return taskId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTaskId(ObjectId taskId) {
|
|
||||||
this.taskId = taskId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTaskIdAsString() {
|
|
||||||
return taskId != null ? taskId.toHexString() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getPhotos() {
|
|
||||||
return photos;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPhotos(List<String> photos) {
|
|
||||||
this.photos = photos;
|
|
||||||
this.count = photos != null ? photos.size() : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCount() {
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCount(int count) {
|
|
||||||
this.count = count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalDateTime getCreatedAt() {
|
|
||||||
return createdAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCreatedAt(LocalDateTime createdAt) {
|
|
||||||
this.createdAt = createdAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCompletedBy() {
|
|
||||||
return completedBy;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCompletedBy(String completedBy) {
|
|
||||||
this.completedBy = completedBy;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "Photo{" +
|
|
||||||
"id=" + id +
|
|
||||||
", jobId=" + jobId +
|
|
||||||
", taskId=" + taskId +
|
|
||||||
", count=" + count +
|
|
||||||
", createdAt=" + createdAt +
|
|
||||||
", completedBy='" + completedBy + '\'' +
|
|
||||||
'}';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
38
src/main/java/de/assecutor/votianlt/model/Signature.java
Normal file
38
src/main/java/de/assecutor/votianlt/model/Signature.java
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package de.assecutor.votianlt.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.mongodb.core.mapping.Document;
|
||||||
|
import org.bson.types.ObjectId;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signature entity for storing signature SVG data from task completions.
|
||||||
|
* References the task ObjectId and stores SVG signature strings.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Document(collection = "signatures")
|
||||||
|
public class Signature {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
private ObjectId id;
|
||||||
|
|
||||||
|
private ObjectId taskId;
|
||||||
|
private String signatureSvg;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String completedBy;
|
||||||
|
|
||||||
|
// Default constructor
|
||||||
|
public Signature() {
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor with parameters
|
||||||
|
public Signature(ObjectId taskId, String signatureSvg, String completedBy) {
|
||||||
|
this();
|
||||||
|
this.taskId = taskId;
|
||||||
|
this.signatureSvg = signatureSvg;
|
||||||
|
this.completedBy = completedBy;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,9 +44,6 @@ public class TaskEntry {
|
|||||||
@Field("completed_by")
|
@Field("completed_by")
|
||||||
private String completedBy;
|
private String completedBy;
|
||||||
|
|
||||||
@Field("completion_note")
|
|
||||||
private String completionNote;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the ObjectId as string for JSON serialization.
|
* 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.
|
* This ensures that the task id is returned as a string when jobs are retrieved via API.
|
||||||
|
|||||||
@@ -48,9 +48,6 @@ public abstract class BaseTask {
|
|||||||
@Field("completed_by")
|
@Field("completed_by")
|
||||||
private String completedBy;
|
private String completedBy;
|
||||||
|
|
||||||
@Field("completion_note")
|
|
||||||
private String completionNote;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the ObjectId as string for JSON serialization.
|
* Returns the ObjectId as string for JSON serialization.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ public class MqttV5ClientManager implements SmartLifecycle {
|
|||||||
"/server/+/task/photo/completed",
|
"/server/+/task/photo/completed",
|
||||||
"/server/+/task/confirm",
|
"/server/+/task/confirm",
|
||||||
"/server/+/task/completed",
|
"/server/+/task/completed",
|
||||||
|
"/server/+/task_completed",
|
||||||
"/server/+/job/status",
|
"/server/+/job/status",
|
||||||
"/server/+/jobs/assigned",
|
"/server/+/jobs/assigned",
|
||||||
"/server/login"
|
"/server/login"
|
||||||
@@ -130,28 +131,33 @@ public class MqttV5ClientManager implements SmartLifecycle {
|
|||||||
|
|
||||||
private void routeInbound(String topic, Map<String, Object> payload) {
|
private void routeInbound(String topic, Map<String, Object> payload) {
|
||||||
try {
|
try {
|
||||||
if (topic.matches("/server/.+/task/photo/completed")) {
|
// The consolidated topic /server/{clientId}/task_completed is used by apps to
|
||||||
messageController.handlePhotoTaskCompleted(payload);
|
// report completion of any task type. Only PHOTO and CONFIRMATION require
|
||||||
} else if (topic.matches("/server/.+/task/confirm")) {
|
// specialized processing on the server side. All other task types are handled by the
|
||||||
messageController.handleTaskConfirmation(payload);
|
// generic handler handleTaskCompleted(). This keeps routing simple while allowing
|
||||||
} else if (topic.matches("/server/.+/task/completed")) {
|
// special logic (e.g., photo persistence) where necessary.
|
||||||
messageController.handleTaskCompleted(payload);
|
if (topic.matches("/server/.+/task_completed")) {
|
||||||
} else if (topic.matches("/server/.+/job/status")) {
|
|
||||||
messageController.handleJobStatusUpdate(payload);
|
|
||||||
} else if (topic.matches("/server/.+/jobs/assigned")) {
|
|
||||||
// Extract clientId from topic: /server/{clientId}/jobs/assigned
|
|
||||||
try {
|
try {
|
||||||
String[] parts = topic.split("/");
|
Object tt = payload.get("taskType");
|
||||||
if (parts.length >= 5 && "server".equals(parts[1])) {
|
String taskType = tt != null ? tt.toString() : null;
|
||||||
String clientId = parts[2];
|
messageController.handleTaskCompleted(payload, taskType);
|
||||||
if (clientId != null && !clientId.isBlank()) {
|
} catch (Exception e) {
|
||||||
payload.put("clientId", clientId);
|
log.error("Error routing task_completed by taskType: {}", e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
} else if (topic.matches("/server/.+/jobs/assigned")) {
|
||||||
} catch (Exception ignore) {
|
try {
|
||||||
// ignore extraction errors
|
// Extract clientId from topic: /server/{clientId}/jobs/assigned
|
||||||
|
String[] parts = topic.split("/");
|
||||||
|
String clientId = parts.length > 2 ? parts[2] : null;
|
||||||
|
if (clientId != null && !clientId.isBlank()) {
|
||||||
|
payload.put("clientId", clientId);
|
||||||
|
} else {
|
||||||
|
log.warn("Couldn't extract clientId from topic {} for jobs/assigned", topic);
|
||||||
|
}
|
||||||
|
messageController.handleGetAssignedJobs(payload);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error handling jobs/assigned on {}: {}", topic, e.getMessage(), e);
|
||||||
}
|
}
|
||||||
messageController.handleGetAssignedJobs(payload);
|
|
||||||
} else if (topic.equals("/server/login")) {
|
} else if (topic.equals("/server/login")) {
|
||||||
var om = new ObjectMapper();
|
var om = new ObjectMapper();
|
||||||
de.assecutor.votianlt.dto.AppLoginRequest req = om.convertValue(payload, de.assecutor.votianlt.dto.AppLoginRequest.class);
|
de.assecutor.votianlt.dto.AppLoginRequest req = om.convertValue(payload, de.assecutor.votianlt.dto.AppLoginRequest.class);
|
||||||
|
|||||||
@@ -1523,7 +1523,6 @@ public class AddJobView extends Main {
|
|||||||
newTask.setCompleted(oldTask.isCompleted());
|
newTask.setCompleted(oldTask.isCompleted());
|
||||||
newTask.setCompletedAt(oldTask.getCompletedAt());
|
newTask.setCompletedAt(oldTask.getCompletedAt());
|
||||||
newTask.setCompletedBy(oldTask.getCompletedBy());
|
newTask.setCompletedBy(oldTask.getCompletedBy());
|
||||||
newTask.setCompletionNote(oldTask.getCompletionNote());
|
|
||||||
|
|
||||||
// Preserve task-specific properties
|
// Preserve task-specific properties
|
||||||
if (oldTask instanceof ConfirmationTask oldConfirmationTask && newTask instanceof ConfirmationTask newConfirmationTask) {
|
if (oldTask instanceof ConfirmationTask oldConfirmationTask && newTask instanceof ConfirmationTask newConfirmationTask) {
|
||||||
|
|||||||
@@ -386,9 +386,6 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
|
|||||||
if (task.getCompletedBy() != null && !task.getCompletedBy().isBlank()) {
|
if (task.getCompletedBy() != null && !task.getCompletedBy().isBlank()) {
|
||||||
dialogContent.add(new Span("Abgeschlossen von: " + task.getCompletedBy()));
|
dialogContent.add(new Span("Abgeschlossen von: " + task.getCompletedBy()));
|
||||||
}
|
}
|
||||||
if (task.getCompletionNote() != null && !task.getCompletionNote().isBlank()) {
|
|
||||||
dialogContent.add(new Span("Notiz: " + task.getCompletionNote()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close button
|
// Close button
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package de.assecutor.votianlt.repository;
|
||||||
|
|
||||||
|
import de.assecutor.votianlt.model.Barcode;
|
||||||
|
import org.bson.types.ObjectId;
|
||||||
|
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface BarcodeRepository extends MongoRepository<Barcode, ObjectId> {
|
||||||
|
}
|
||||||
@@ -13,14 +13,6 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@Repository
|
@Repository
|
||||||
public interface PhotoRepository extends MongoRepository<Photo, ObjectId> {
|
public interface PhotoRepository extends MongoRepository<Photo, ObjectId> {
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all photos associated with a specific job ID.
|
|
||||||
* @param jobId The ObjectId of the job
|
|
||||||
* @return List of photos for the job
|
|
||||||
*/
|
|
||||||
List<Photo> findByJobId(ObjectId jobId);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find all photos associated with a specific task ID.
|
* Find all photos associated with a specific task ID.
|
||||||
* @param taskId The ObjectId of the task
|
* @param taskId The ObjectId of the task
|
||||||
@@ -28,15 +20,6 @@ public interface PhotoRepository extends MongoRepository<Photo, ObjectId> {
|
|||||||
*/
|
*/
|
||||||
List<Photo> findByTaskId(ObjectId taskId);
|
List<Photo> findByTaskId(ObjectId taskId);
|
||||||
|
|
||||||
/**
|
|
||||||
* Find photos by job ID as string.
|
|
||||||
* @param jobId The job ID as string
|
|
||||||
* @return List of photos for the job
|
|
||||||
*/
|
|
||||||
default List<Photo> findByJobId(String jobId) {
|
|
||||||
return findByJobId(new ObjectId(jobId));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find photos by task ID as string.
|
* Find photos by task ID as string.
|
||||||
* @param taskId The task ID as string
|
* @param taskId The task ID as string
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package de.assecutor.votianlt.repository;
|
||||||
|
|
||||||
|
import de.assecutor.votianlt.model.Signature;
|
||||||
|
import org.bson.types.ObjectId;
|
||||||
|
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface SignatureRepository extends MongoRepository<Signature, ObjectId> {
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user