Erweiterungen
This commit is contained in:
@@ -18,6 +18,7 @@ import de.assecutor.votianlt.repository.SignatureRepository;
|
|||||||
import de.assecutor.votianlt.model.Photo;
|
import de.assecutor.votianlt.model.Photo;
|
||||||
import de.assecutor.votianlt.model.Barcode;
|
import de.assecutor.votianlt.model.Barcode;
|
||||||
import de.assecutor.votianlt.model.Signature;
|
import de.assecutor.votianlt.model.Signature;
|
||||||
|
import de.assecutor.votianlt.service.JobHistoryService;
|
||||||
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;
|
||||||
@@ -55,8 +56,9 @@ public class MessageController {
|
|||||||
private final PhotoRepository photoRepository;
|
private final PhotoRepository photoRepository;
|
||||||
private final BarcodeRepository barcodeRepository;
|
private final BarcodeRepository barcodeRepository;
|
||||||
private final SignatureRepository signatureRepository;
|
private final SignatureRepository signatureRepository;
|
||||||
|
private final JobHistoryService jobHistoryService;
|
||||||
|
|
||||||
public MessageController(MqttPublisher mqttPublisher, AppUserRepository appUserRepository, AppUserService appUserService, JobRepository jobRepository, CargoItemRepository cargoItemRepository, TaskRepository taskRepository, PhotoRepository photoRepository, BarcodeRepository barcodeRepository, SignatureRepository signatureRepository) {
|
public MessageController(MqttPublisher mqttPublisher, AppUserRepository appUserRepository, AppUserService appUserService, JobRepository jobRepository, CargoItemRepository cargoItemRepository, TaskRepository taskRepository, PhotoRepository photoRepository, BarcodeRepository barcodeRepository, SignatureRepository signatureRepository, JobHistoryService jobHistoryService) {
|
||||||
this.mqttPublisher = mqttPublisher;
|
this.mqttPublisher = mqttPublisher;
|
||||||
this.appUserRepository = appUserRepository;
|
this.appUserRepository = appUserRepository;
|
||||||
this.appUserService = appUserService;
|
this.appUserService = appUserService;
|
||||||
@@ -66,6 +68,7 @@ public class MessageController {
|
|||||||
this.photoRepository = photoRepository;
|
this.photoRepository = photoRepository;
|
||||||
this.barcodeRepository = barcodeRepository;
|
this.barcodeRepository = barcodeRepository;
|
||||||
this.signatureRepository = signatureRepository;
|
this.signatureRepository = signatureRepository;
|
||||||
|
this.jobHistoryService = jobHistoryService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -232,14 +235,12 @@ public class MessageController {
|
|||||||
|
|
||||||
private void processConfirmationTaskCompletion(Map<String, Object> payload) {
|
private void processConfirmationTaskCompletion(Map<String, Object> payload) {
|
||||||
Object taskId = payload.get("taskId");
|
Object taskId = payload.get("taskId");
|
||||||
|
completeTaskWithHistory(taskId, "Bestätigung durchgeführt");
|
||||||
completeTask(taskId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processTodoListTaskCompletion(Map<String, Object> payload) {
|
private void processTodoListTaskCompletion(Map<String, Object> payload) {
|
||||||
Object taskId = payload.get("taskId");
|
Object taskId = payload.get("taskId");
|
||||||
|
completeTaskWithHistory(taskId, "Alle To-Do-Elemente abgehakt");
|
||||||
completeTask(taskId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processBarcodeTaskCompletion(Map<String, Object> payload) {
|
private void processBarcodeTaskCompletion(Map<String, Object> payload) {
|
||||||
@@ -252,6 +253,7 @@ public class MessageController {
|
|||||||
}
|
}
|
||||||
BaseTask task = opt.get();
|
BaseTask task = opt.get();
|
||||||
|
|
||||||
|
String extraDataSummary = null;
|
||||||
Object extra = payload.get("extraData");
|
Object extra = payload.get("extraData");
|
||||||
if (extra instanceof Map<?, ?> extraData) {
|
if (extra instanceof Map<?, ?> extraData) {
|
||||||
Object barcodesObj = extraData.get("barcodes");
|
Object barcodesObj = extraData.get("barcodes");
|
||||||
@@ -270,19 +272,23 @@ public class MessageController {
|
|||||||
barcodeRepository.save(barcodeEntry);
|
barcodeRepository.save(barcodeEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
log.info("Saved {} barcodes for taskId={}", barcodes.size(), taskId);
|
||||||
} else {
|
} else {
|
||||||
|
extraDataSummary = "Keine Barcodes gescannt";
|
||||||
log.info("No barcodes found in extraData for taskId={}", taskId);
|
log.info("No barcodes found in extraData for taskId={}", taskId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
extraDataSummary = "Barcode-Daten fehlerhaft";
|
||||||
log.warn("extraData.barcodes is not a List for taskId={}", taskId);
|
log.warn("extraData.barcodes is not a List for taskId={}", taskId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
extraDataSummary = "Keine Extra-Daten";
|
||||||
log.warn("extraData is not a Map for taskId={}", taskId);
|
log.warn("extraData is not a Map for taskId={}", taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, mark the task as completed
|
// Finally, mark the task as completed with history logging
|
||||||
completeTask(taskId);
|
completeTaskWithHistory(taskId, extraDataSummary);
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
log.error("Invalid taskId format for barcode completion: {}", taskId);
|
log.error("Invalid taskId format for barcode completion: {}", taskId);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@@ -300,6 +306,7 @@ public class MessageController {
|
|||||||
}
|
}
|
||||||
BaseTask task = opt.get();
|
BaseTask task = opt.get();
|
||||||
|
|
||||||
|
String extraDataSummary = null;
|
||||||
Object extra = payload.get("extraData");
|
Object extra = payload.get("extraData");
|
||||||
if (extra instanceof Map<?, ?> extraData) {
|
if (extra instanceof Map<?, ?> extraData) {
|
||||||
Object signatureSvgObj = extraData.get("signatureSvg");
|
Object signatureSvgObj = extraData.get("signatureSvg");
|
||||||
@@ -312,19 +319,23 @@ public class MessageController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
signatureRepository.save(signatureEntry);
|
signatureRepository.save(signatureEntry);
|
||||||
|
extraDataSummary = "Unterschrift erfasst (SVG, " + signatureSvg.length() + " Zeichen)";
|
||||||
log.info("Saved signature for taskId={}", taskId);
|
log.info("Saved signature for taskId={}", taskId);
|
||||||
} else {
|
} else {
|
||||||
|
extraDataSummary = "Leere Unterschrift";
|
||||||
log.info("Empty signature SVG found for taskId={}", taskId);
|
log.info("Empty signature SVG found for taskId={}", taskId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
extraDataSummary = "Unterschrift-Daten fehlerhaft";
|
||||||
log.warn("extraData.signatureSvg is not a String for taskId={}", taskId);
|
log.warn("extraData.signatureSvg is not a String for taskId={}", taskId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
extraDataSummary = "Keine Extra-Daten";
|
||||||
log.warn("extraData is not a Map for taskId={}", taskId);
|
log.warn("extraData is not a Map for taskId={}", taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, mark the task as completed
|
// Finally, mark the task as completed with history logging
|
||||||
completeTask(taskId);
|
completeTaskWithHistory(taskId, extraDataSummary);
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
log.error("Invalid taskId format for signature completion: {}", taskId);
|
log.error("Invalid taskId format for signature completion: {}", taskId);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@@ -343,6 +354,7 @@ public class MessageController {
|
|||||||
BaseTask task = opt.get();
|
BaseTask task = opt.get();
|
||||||
ObjectId jobId = new ObjectId(task.getJobIdAsString());
|
ObjectId jobId = new ObjectId(task.getJobIdAsString());
|
||||||
|
|
||||||
|
String extraDataSummary = null;
|
||||||
Object extra = payload.get("extraData");
|
Object extra = payload.get("extraData");
|
||||||
if (extra instanceof Map<?, ?> extraData) {
|
if (extra instanceof Map<?, ?> extraData) {
|
||||||
Object photosObj = extraData.get("photos");
|
Object photosObj = extraData.get("photos");
|
||||||
@@ -361,19 +373,23 @@ public class MessageController {
|
|||||||
photoRepository.save(photoEntry);
|
photoRepository.save(photoEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extraDataSummary = photos.size() + " Foto(s) aufgenommen";
|
||||||
log.info("Saved {} photos for taskId={}, jobId={}", photos.size(), taskId, jobId);
|
log.info("Saved {} photos for taskId={}, jobId={}", photos.size(), taskId, jobId);
|
||||||
} else {
|
} else {
|
||||||
|
extraDataSummary = "Keine Fotos aufgenommen";
|
||||||
log.info("No photos found in extraData for taskId={}", taskId);
|
log.info("No photos found in extraData for taskId={}", taskId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
extraDataSummary = "Foto-Daten fehlerhaft";
|
||||||
log.warn("extraData.photos is not a List for taskId={}", taskId);
|
log.warn("extraData.photos is not a List for taskId={}", taskId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
extraDataSummary = "Keine Extra-Daten";
|
||||||
log.warn("extraData is not a Map for taskId={}", taskId);
|
log.warn("extraData is not a Map for taskId={}", taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, mark the task as completed
|
// Finally, mark the task as completed with history logging
|
||||||
completeTask(taskId);
|
completeTaskWithHistory(taskId, extraDataSummary);
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
log.error("Invalid taskId format for photo completion: {}", taskId);
|
log.error("Invalid taskId format for photo completion: {}", taskId);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@@ -382,19 +398,37 @@ public class MessageController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void completeTask(Object tid) {
|
private void completeTask(Object tid) {
|
||||||
|
completeTaskWithHistory(tid, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void completeTaskWithHistory(Object tid, String extraDataSummary) {
|
||||||
String taskIdStr = tid.toString();
|
String taskIdStr = tid.toString();
|
||||||
try {
|
try {
|
||||||
ObjectId taskId = new ObjectId(taskIdStr);
|
ObjectId taskId = new ObjectId(taskIdStr);
|
||||||
var opt = taskRepository.findById(taskId);
|
var opt = taskRepository.findById(taskId);
|
||||||
if (opt.isEmpty()) {
|
if (opt.isEmpty()) {
|
||||||
log.warn("Task not found for confirmation completion. taskId={}", taskIdStr);
|
log.warn("Task not found for completion. taskId={}", taskIdStr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
BaseTask task = opt.get();
|
BaseTask task = opt.get();
|
||||||
task.setCompleted(true);
|
task.setCompleted(true);
|
||||||
task.setCompletedAt(LocalDateTime.now());
|
task.setCompletedAt(LocalDateTime.now());
|
||||||
taskRepository.save(task);
|
taskRepository.save(task);
|
||||||
log.info("Task marked completed. taskId={}, completedBy={}", taskIdStr, task.getCompletedBy());
|
|
||||||
|
// Log detailed task completion in job history
|
||||||
|
try {
|
||||||
|
ObjectId jobId = new ObjectId(task.getJobIdAsString());
|
||||||
|
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);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to log task completion history for task {}: {}", taskIdStr, e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Task marked completed. taskId={}, completedBy={}, extraData={}",
|
||||||
|
taskIdStr, task.getCompletedBy(), extraDataSummary);
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
log.error("Invalid taskId format for completion: {}", taskIdStr);
|
log.error("Invalid taskId format for completion: {}", taskIdStr);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
|
|||||||
98
src/main/java/de/assecutor/votianlt/model/JobHistory.java
Normal file
98
src/main/java/de/assecutor/votianlt/model/JobHistory.java
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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")
|
||||||
|
public class JobHistory {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
private ObjectId id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to the job this history entry belongs to
|
||||||
|
*/
|
||||||
|
private ObjectId jobId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp when the change occurred
|
||||||
|
*/
|
||||||
|
private LocalDateTime timestamp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reason for the change (e.g., "Status Update", "User Edit", "System Update")
|
||||||
|
*/
|
||||||
|
private String reason;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Description of what was changed (e.g., "Status changed from CREATED to IN_PROGRESS")
|
||||||
|
*/
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User who made the change (can be null for system changes)
|
||||||
|
*/
|
||||||
|
private String changedBy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional details about the change (optional)
|
||||||
|
*/
|
||||||
|
private String details;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of change (CREATE, UPDATE, STATUS_CHANGE, DELETE, etc.)
|
||||||
|
*/
|
||||||
|
private JobHistoryType changeType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Old value (for comparison, stored as JSON string if complex)
|
||||||
|
*/
|
||||||
|
private String oldValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* New value (for comparison, stored as JSON string if complex)
|
||||||
|
*/
|
||||||
|
private String newValue;
|
||||||
|
|
||||||
|
// Default constructor
|
||||||
|
public JobHistory() {
|
||||||
|
this.timestamp = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor for basic history entry
|
||||||
|
public JobHistory(ObjectId jobId, String reason, String description, String changedBy) {
|
||||||
|
this();
|
||||||
|
this.jobId = jobId;
|
||||||
|
this.reason = reason;
|
||||||
|
this.description = description;
|
||||||
|
this.changedBy = changedBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor for detailed history entry
|
||||||
|
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;
|
||||||
|
this.newValue = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getter for ID as String
|
||||||
|
public String getIdAsString() {
|
||||||
|
return id != null ? id.toHexString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getter for Job ID as String
|
||||||
|
public String getJobIdAsString() {
|
||||||
|
return jobId != null ? jobId.toHexString() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package de.assecutor.votianlt.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumeration of different types of job history changes
|
||||||
|
*/
|
||||||
|
public enum JobHistoryType {
|
||||||
|
/**
|
||||||
|
* Job was created
|
||||||
|
*/
|
||||||
|
CREATE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job data was updated
|
||||||
|
*/
|
||||||
|
UPDATE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job status was changed
|
||||||
|
*/
|
||||||
|
STATUS_CHANGE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job was assigned to a user
|
||||||
|
*/
|
||||||
|
ASSIGNMENT,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Task was completed within the job
|
||||||
|
*/
|
||||||
|
TASK_COMPLETED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job was exported or shared
|
||||||
|
*/
|
||||||
|
EXPORT,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job was deleted or archived
|
||||||
|
*/
|
||||||
|
DELETE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System-generated change
|
||||||
|
*/
|
||||||
|
SYSTEM,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comment or note was added
|
||||||
|
*/
|
||||||
|
COMMENT,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Other type of change
|
||||||
|
*/
|
||||||
|
OTHER
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import de.assecutor.votianlt.repository.JobRepository;
|
|||||||
import de.assecutor.votianlt.repository.TaskRepository;
|
import de.assecutor.votianlt.repository.TaskRepository;
|
||||||
import de.assecutor.votianlt.security.SecurityService;
|
import de.assecutor.votianlt.security.SecurityService;
|
||||||
import de.assecutor.votianlt.repository.CargoItemRepository;
|
import de.assecutor.votianlt.repository.CargoItemRepository;
|
||||||
|
import de.assecutor.votianlt.service.JobHistoryService;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -28,6 +29,7 @@ public class AddJobService {
|
|||||||
private final JobRepository jobRepository;
|
private final JobRepository jobRepository;
|
||||||
private final TaskRepository taskRepository;
|
private final TaskRepository taskRepository;
|
||||||
private final SecurityService securityService;
|
private final SecurityService securityService;
|
||||||
|
private final JobHistoryService jobHistoryService;
|
||||||
/**
|
/**
|
||||||
* Speichert einen neuen Auftrag samt CargoItems und Tasks
|
* Speichert einen neuen Auftrag samt CargoItems und Tasks
|
||||||
* @param job der Auftrag
|
* @param job der Auftrag
|
||||||
@@ -97,6 +99,14 @@ public class AddJobService {
|
|||||||
savedJob = jobRepository.save(savedJob);
|
savedJob = jobRepository.save(savedJob);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// History-Eintrag für Job-Erstellung
|
||||||
|
try {
|
||||||
|
String currentUserName = getCurrentUserName();
|
||||||
|
jobHistoryService.logJobCreation(savedJob, currentUserName);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to log job creation history for job {}: {}", savedJob.getIdAsString(), e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
log.info("Auftrag erfolgreich gespeichert: {}", savedJob.getJobNumber());
|
log.info("Auftrag erfolgreich gespeichert: {}", savedJob.getJobNumber());
|
||||||
return savedJob;
|
return savedJob;
|
||||||
|
|
||||||
@@ -136,4 +146,20 @@ public class AddJobService {
|
|||||||
List<Job> drafts = jobRepository.findByCreatedByAndIsDraftTrue(username);
|
List<Job> drafts = jobRepository.findByCreatedByAndIsDraftTrue(username);
|
||||||
return drafts.isEmpty() ? Optional.empty() : Optional.of(drafts.getFirst());
|
return drafts.isEmpty() ? Optional.empty() : Optional.of(drafts.getFirst());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilfsmethode um den aktuellen Benutzernamen zu ermitteln
|
||||||
|
*/
|
||||||
|
private String getCurrentUserName() {
|
||||||
|
try {
|
||||||
|
var authenticatedUserOpt = securityService.getAuthenticatedUser();
|
||||||
|
if (authenticatedUserOpt.isPresent()) {
|
||||||
|
var user = authenticatedUserOpt.get();
|
||||||
|
return user.getUsername() != null ? user.getUsername() : "Unknown User";
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Could not get authenticated user: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
return "System"; // Fallback wenn kein authentifizierter Benutzer gefunden wird
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,612 @@
|
|||||||
|
package de.assecutor.votianlt.pages.view;
|
||||||
|
|
||||||
|
import com.vaadin.flow.component.button.Button;
|
||||||
|
import com.vaadin.flow.component.button.ButtonVariant;
|
||||||
|
import com.vaadin.flow.component.dialog.Dialog;
|
||||||
|
import com.vaadin.flow.component.html.H2;
|
||||||
|
import com.vaadin.flow.component.html.Main;
|
||||||
|
import com.vaadin.flow.component.html.Span;
|
||||||
|
import com.vaadin.flow.component.html.Div;
|
||||||
|
import com.vaadin.flow.component.icon.Icon;
|
||||||
|
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||||
|
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||||
|
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||||
|
import com.vaadin.flow.router.BeforeEvent;
|
||||||
|
import com.vaadin.flow.router.HasUrlParameter;
|
||||||
|
import com.vaadin.flow.router.PageTitle;
|
||||||
|
import com.vaadin.flow.router.Route;
|
||||||
|
import com.vaadin.flow.theme.lumo.LumoUtility;
|
||||||
|
import de.assecutor.votianlt.model.Job;
|
||||||
|
import de.assecutor.votianlt.model.JobHistory;
|
||||||
|
import de.assecutor.votianlt.model.JobHistoryType;
|
||||||
|
import de.assecutor.votianlt.model.Barcode;
|
||||||
|
import de.assecutor.votianlt.model.Photo;
|
||||||
|
import de.assecutor.votianlt.model.Signature;
|
||||||
|
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
|
||||||
|
import de.assecutor.votianlt.repository.BarcodeRepository;
|
||||||
|
import de.assecutor.votianlt.repository.JobRepository;
|
||||||
|
import de.assecutor.votianlt.repository.PhotoRepository;
|
||||||
|
import de.assecutor.votianlt.repository.SignatureRepository;
|
||||||
|
import de.assecutor.votianlt.service.JobHistoryService;
|
||||||
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.bson.types.ObjectId;
|
||||||
|
|
||||||
|
@Route(value = "job_history", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
|
||||||
|
@PageTitle("Job History")
|
||||||
|
@RolesAllowed("USER")
|
||||||
|
@Slf4j
|
||||||
|
public class JobHistoryView extends Main implements HasUrlParameter<String> {
|
||||||
|
|
||||||
|
private final JobRepository jobRepository;
|
||||||
|
private final JobHistoryService jobHistoryService;
|
||||||
|
private final PhotoRepository photoRepository;
|
||||||
|
private final BarcodeRepository barcodeRepository;
|
||||||
|
private final SignatureRepository signatureRepository;
|
||||||
|
private final VerticalLayout content;
|
||||||
|
|
||||||
|
public JobHistoryView(JobRepository jobRepository, JobHistoryService jobHistoryService, PhotoRepository photoRepository, BarcodeRepository barcodeRepository, SignatureRepository signatureRepository) {
|
||||||
|
this.jobRepository = jobRepository;
|
||||||
|
this.jobHistoryService = jobHistoryService;
|
||||||
|
this.photoRepository = photoRepository;
|
||||||
|
this.barcodeRepository = barcodeRepository;
|
||||||
|
this.signatureRepository = signatureRepository;
|
||||||
|
|
||||||
|
setSizeFull();
|
||||||
|
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX,
|
||||||
|
LumoUtility.FlexDirection.COLUMN, LumoUtility.Padding.MEDIUM,
|
||||||
|
LumoUtility.Gap.SMALL);
|
||||||
|
|
||||||
|
add(new ViewToolbar("Job History"));
|
||||||
|
|
||||||
|
content = new VerticalLayout();
|
||||||
|
content.setSpacing(true);
|
||||||
|
content.setPadding(true);
|
||||||
|
content.setWidthFull();
|
||||||
|
add(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setParameter(BeforeEvent event, String parameter) {
|
||||||
|
content.removeAll();
|
||||||
|
|
||||||
|
if (parameter == null || parameter.isBlank()) {
|
||||||
|
content.add(new Span("Fehler: Keine Job-ID angegeben"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectId jobId;
|
||||||
|
try {
|
||||||
|
jobId = new ObjectId(parameter);
|
||||||
|
} catch (Exception e) {
|
||||||
|
content.add(new Span("Fehler: Ungültige Job-ID Format: " + parameter));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Job job = jobRepository.findById(jobId).orElse(null);
|
||||||
|
if (job == null) {
|
||||||
|
content.add(new Span("Fehler: Job mit ID " + parameter + " nicht gefunden"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void render(Job job) {
|
||||||
|
content.removeAll();
|
||||||
|
|
||||||
|
// Header mit Job-Informationen
|
||||||
|
H2 header = new H2("Job History - " + (job.getJobNumber() != null ? job.getJobNumber() : "Unbekannte Auftragsnummer"));
|
||||||
|
content.add(header);
|
||||||
|
|
||||||
|
// Job basic info for context
|
||||||
|
Div jobInfoBox = createJobInfoBox(job);
|
||||||
|
content.add(jobInfoBox);
|
||||||
|
|
||||||
|
// Load and display history entries
|
||||||
|
try {
|
||||||
|
var historyEntries = jobHistoryService.getJobHistory(job.getId());
|
||||||
|
long historyCount = jobHistoryService.getJobHistoryCount(job.getId());
|
||||||
|
|
||||||
|
if (historyEntries.isEmpty()) {
|
||||||
|
Span noHistory = new Span("Noch keine History-Einträge für diesen Job vorhanden.");
|
||||||
|
noHistory.getStyle().set("color", "var(--lumo-secondary-text-color)");
|
||||||
|
content.add(noHistory);
|
||||||
|
} else {
|
||||||
|
// History section header
|
||||||
|
H2 historyHeader = new H2("Verlauf (" + historyCount + " Einträge)");
|
||||||
|
historyHeader.getStyle().set("margin-top", "var(--lumo-space-l)");
|
||||||
|
content.add(historyHeader);
|
||||||
|
|
||||||
|
// History timeline
|
||||||
|
VerticalLayout timeline = createHistoryTimeline(historyEntries);
|
||||||
|
content.add(timeline);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Span errorMessage = new Span("Fehler beim Laden der Job History: " + e.getMessage());
|
||||||
|
errorMessage.getStyle().set("color", "var(--lumo-error-text-color)");
|
||||||
|
content.add(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)");
|
||||||
|
|
||||||
|
VerticalLayout infoContent = new VerticalLayout();
|
||||||
|
infoContent.setPadding(false);
|
||||||
|
infoContent.setSpacing(false);
|
||||||
|
|
||||||
|
if (job.getDeliveryCompany() != null) {
|
||||||
|
infoContent.add(new Span("Kunde: " + job.getDeliveryCompany()));
|
||||||
|
}
|
||||||
|
if (job.getCreatedAt() != null) {
|
||||||
|
infoContent.add(new Span("Erstellt am: " + formatDateTime(job.getCreatedAt())));
|
||||||
|
}
|
||||||
|
if (job.getStatus() != null) {
|
||||||
|
infoContent.add(new Span("Status: " + formatStatus(job.getStatus())));
|
||||||
|
}
|
||||||
|
|
||||||
|
infoBox.add(infoContent);
|
||||||
|
return infoBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VerticalLayout createHistoryTimeline(java.util.List<JobHistory> historyEntries) {
|
||||||
|
VerticalLayout timeline = new VerticalLayout();
|
||||||
|
timeline.setPadding(false);
|
||||||
|
timeline.setSpacing(false);
|
||||||
|
timeline.setWidthFull();
|
||||||
|
|
||||||
|
for (JobHistory entry : historyEntries) {
|
||||||
|
Div entryCard = createHistoryEntryCard(entry);
|
||||||
|
timeline.add(entryCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
// Header row with icon, reason and timestamp
|
||||||
|
HorizontalLayout headerRow = new HorizontalLayout();
|
||||||
|
headerRow.setAlignItems(HorizontalLayout.Alignment.CENTER);
|
||||||
|
headerRow.setWidthFull();
|
||||||
|
|
||||||
|
Icon typeIcon = getTypeIcon(entry.getChangeType());
|
||||||
|
typeIcon.getStyle().set("color", getTypeColor(entry.getChangeType()));
|
||||||
|
|
||||||
|
Span reason = new Span(entry.getReason() != null ? entry.getReason() : "Unbekannt");
|
||||||
|
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)");
|
||||||
|
|
||||||
|
HorizontalLayout leftSide = new HorizontalLayout(typeIcon, reason);
|
||||||
|
leftSide.setAlignItems(HorizontalLayout.Alignment.CENTER);
|
||||||
|
leftSide.setSpacing(true);
|
||||||
|
|
||||||
|
headerRow.add(leftSide, timestamp);
|
||||||
|
headerRow.setJustifyContentMode(HorizontalLayout.JustifyContentMode.BETWEEN);
|
||||||
|
|
||||||
|
VerticalLayout cardContent = new VerticalLayout();
|
||||||
|
cardContent.setPadding(false);
|
||||||
|
cardContent.setSpacing(false);
|
||||||
|
cardContent.add(headerRow);
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
cardContent.add(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Photo preview for photo tasks
|
||||||
|
if (entry.getChangeType() == JobHistoryType.TASK_COMPLETED &&
|
||||||
|
entry.getDetails() != null &&
|
||||||
|
entry.getDetails().contains("Task-Typ: PHOTO")) {
|
||||||
|
|
||||||
|
HorizontalLayout photoPreview = createPhotoPreview(entry);
|
||||||
|
if (photoPreview != null) {
|
||||||
|
cardContent.add(photoPreview);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Barcode preview for barcode tasks
|
||||||
|
if (entry.getChangeType() == JobHistoryType.TASK_COMPLETED &&
|
||||||
|
entry.getDetails() != null &&
|
||||||
|
entry.getDetails().contains("Task-Typ: BARCODE")) {
|
||||||
|
|
||||||
|
VerticalLayout barcodePreview = createBarcodePreview(entry);
|
||||||
|
if (barcodePreview != null) {
|
||||||
|
cardContent.add(barcodePreview);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signature preview for signature tasks
|
||||||
|
if (entry.getChangeType() == JobHistoryType.TASK_COMPLETED &&
|
||||||
|
entry.getDetails() != null &&
|
||||||
|
entry.getDetails().contains("Task-Typ: SIGNATURE")) {
|
||||||
|
|
||||||
|
Div signaturePreview = createSignaturePreview(entry);
|
||||||
|
if (signaturePreview != null) {
|
||||||
|
cardContent.add(signaturePreview);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
cardContent.add(changedBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
card.add(cardContent);
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Icon getTypeIcon(JobHistoryType type) {
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getTypeColor(JobHistoryType type) {
|
||||||
|
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)";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatDateTime(java.time.LocalDateTime dateTime) {
|
||||||
|
if (dateTime == null) return "";
|
||||||
|
try {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatStatus(de.assecutor.votianlt.model.JobStatus status) {
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private HorizontalLayout createPhotoPreview(JobHistory entry) {
|
||||||
|
try {
|
||||||
|
// Extract task ID from details
|
||||||
|
String details = entry.getDetails();
|
||||||
|
if (details == null || !details.contains("Task-ID: ")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String taskId = extractTaskIdFromDetails(details);
|
||||||
|
if (taskId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load photos for this task
|
||||||
|
var photos = photoRepository.findByTaskId(taskId);
|
||||||
|
if (photos.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalLayout photoLayout = new HorizontalLayout();
|
||||||
|
photoLayout.setSpacing(true);
|
||||||
|
photoLayout.getStyle()
|
||||||
|
.set("margin-top", "var(--lumo-space-s)")
|
||||||
|
.set("flex-wrap", "wrap");
|
||||||
|
|
||||||
|
for (Photo photo : photos) {
|
||||||
|
if (photo.getPhoto() != null && !photo.getPhoto().isBlank()) {
|
||||||
|
com.vaadin.flow.component.html.Image thumbnail = createPhotoThumbnail(photo.getPhoto());
|
||||||
|
if (thumbnail != null) {
|
||||||
|
thumbnail.addClickListener(e -> showEnlargedPhoto(photo.getPhoto()));
|
||||||
|
photoLayout.add(thumbnail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return photoLayout.getComponentCount() > 0 ? photoLayout : null;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error creating photo preview for history entry: {}", e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractTaskIdFromDetails(String details) {
|
||||||
|
try {
|
||||||
|
String prefix = "Task-ID: ";
|
||||||
|
int startIndex = details.indexOf(prefix);
|
||||||
|
if (startIndex == -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
startIndex += prefix.length();
|
||||||
|
int endIndex = details.indexOf(",", startIndex);
|
||||||
|
if (endIndex == -1) {
|
||||||
|
endIndex = details.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
return details.substring(startIndex, endIndex).trim();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private com.vaadin.flow.component.html.Image createPhotoThumbnail(String base64Photo) {
|
||||||
|
try {
|
||||||
|
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");
|
||||||
|
|
||||||
|
return thumbnail;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error creating photo thumbnail: {}", e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showEnlargedPhoto(String base64Photo) {
|
||||||
|
Dialog photoDialog = new Dialog();
|
||||||
|
photoDialog.setWidth("80vw");
|
||||||
|
photoDialog.setHeight("80vh");
|
||||||
|
photoDialog.setModal(true);
|
||||||
|
photoDialog.setCloseOnOutsideClick(true);
|
||||||
|
photoDialog.setCloseOnEsc(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
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");
|
||||||
|
|
||||||
|
VerticalLayout dialogContent = new VerticalLayout(enlargedImage);
|
||||||
|
dialogContent.setAlignItems(VerticalLayout.Alignment.CENTER);
|
||||||
|
dialogContent.setJustifyContentMode(VerticalLayout.JustifyContentMode.CENTER);
|
||||||
|
dialogContent.setSizeFull();
|
||||||
|
|
||||||
|
photoDialog.add(dialogContent);
|
||||||
|
photoDialog.open();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error showing enlarged photo: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private VerticalLayout createBarcodePreview(JobHistory entry) {
|
||||||
|
try {
|
||||||
|
// Extract task ID from details
|
||||||
|
String details = entry.getDetails();
|
||||||
|
if (details == null || !details.contains("Task-ID: ")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String taskId = extractTaskIdFromDetails(details);
|
||||||
|
if (taskId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load barcodes for this task
|
||||||
|
ObjectId taskObjectId = new ObjectId(taskId);
|
||||||
|
var barcodes = barcodeRepository.findByTaskId(taskObjectId);
|
||||||
|
if (barcodes.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
VerticalLayout barcodeLayout = new VerticalLayout();
|
||||||
|
barcodeLayout.setPadding(false);
|
||||||
|
barcodeLayout.setSpacing(true);
|
||||||
|
barcodeLayout.getStyle()
|
||||||
|
.set("margin-top", "var(--lumo-space-s)");
|
||||||
|
|
||||||
|
for (Barcode barcode : barcodes) {
|
||||||
|
if (barcode.getBarcode() != null && !barcode.getBarcode().isBlank()) {
|
||||||
|
Div barcodeBox = createBarcodeBox(barcode.getBarcode());
|
||||||
|
barcodeLayout.add(barcodeBox);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return barcodeLayout.getComponentCount() > 0 ? barcodeLayout : null;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error creating barcode preview for history entry: {}", e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.add(new Span(barcodeValue));
|
||||||
|
return barcodeBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Div createSignaturePreview(JobHistory entry) {
|
||||||
|
try {
|
||||||
|
// Extract task ID from details
|
||||||
|
String details = entry.getDetails();
|
||||||
|
if (details == null || !details.contains("Task-ID: ")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String taskId = extractTaskIdFromDetails(details);
|
||||||
|
if (taskId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load signature for this task
|
||||||
|
ObjectId taskObjectId = new ObjectId(taskId);
|
||||||
|
var signatures = signatureRepository.findByTaskId(taskObjectId);
|
||||||
|
if (signatures.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the first signature
|
||||||
|
Signature signature = signatures.get(0);
|
||||||
|
if (signature.getSignatureSvg() == null || signature.getSignatureSvg().isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
// Create responsive SVG for preview
|
||||||
|
com.vaadin.flow.component.Html signatureSvg = createResponsiveSignatureSvg(signature.getSignatureSvg(), "100%", "100%");
|
||||||
|
previewContainer.add(signatureSvg);
|
||||||
|
|
||||||
|
// Add click listener for enlarged view
|
||||||
|
previewContainer.addClickListener(e -> showEnlargedSignature(signature.getSignatureSvg()));
|
||||||
|
|
||||||
|
return previewContainer;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error creating signature preview for history entry: {}", e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the SVG has proper responsive attributes
|
||||||
|
if (!responsiveSvg.contains("preserveAspectRatio")) {
|
||||||
|
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>");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showEnlargedSignature(String svgContent) {
|
||||||
|
Dialog signatureDialog = new Dialog();
|
||||||
|
signatureDialog.setWidth("60vw");
|
||||||
|
signatureDialog.setHeight("40vh");
|
||||||
|
signatureDialog.setModal(true);
|
||||||
|
signatureDialog.setCloseOnOutsideClick(true);
|
||||||
|
signatureDialog.setCloseOnEsc(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create enlarged responsive SVG
|
||||||
|
com.vaadin.flow.component.Html enlargedSignature = createResponsiveSignatureSvg(svgContent, "100%", "100%");
|
||||||
|
|
||||||
|
VerticalLayout dialogContent = new VerticalLayout();
|
||||||
|
dialogContent.setAlignItems(VerticalLayout.Alignment.CENTER);
|
||||||
|
dialogContent.setJustifyContentMode(VerticalLayout.JustifyContentMode.CENTER);
|
||||||
|
dialogContent.setSizeFull();
|
||||||
|
dialogContent.setPadding(true);
|
||||||
|
|
||||||
|
dialogContent.add(enlargedSignature);
|
||||||
|
signatureDialog.add(dialogContent);
|
||||||
|
signatureDialog.open();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error showing enlarged signature: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -222,6 +222,18 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
|
|||||||
|
|
||||||
// Google Maps Karte mit Route
|
// Google Maps Karte mit Route
|
||||||
addRouteMap(job);
|
addRouteMap(job);
|
||||||
|
|
||||||
|
// Job History Button
|
||||||
|
Button jobHistoryButton = new Button("Job History");
|
||||||
|
jobHistoryButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
|
jobHistoryButton.getStyle().set("margin-top", "var(--lumo-space-m)");
|
||||||
|
jobHistoryButton.addClickListener(e -> {
|
||||||
|
getUI().ifPresent(ui -> ui.navigate("job_history/" + job.getId().toHexString()));
|
||||||
|
});
|
||||||
|
|
||||||
|
HorizontalLayout buttonLayout = new HorizontalLayout(jobHistoryButton);
|
||||||
|
buttonLayout.setJustifyContentMode(HorizontalLayout.JustifyContentMode.CENTER);
|
||||||
|
content.add(buttonLayout);
|
||||||
}
|
}
|
||||||
|
|
||||||
private VerticalLayout borderedBox() {
|
private VerticalLayout borderedBox() {
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ package de.assecutor.votianlt.pages.view;
|
|||||||
|
|
||||||
import com.vaadin.flow.component.datepicker.DatePicker;
|
import com.vaadin.flow.component.datepicker.DatePicker;
|
||||||
import com.vaadin.flow.component.button.Button;
|
import com.vaadin.flow.component.button.Button;
|
||||||
|
import com.vaadin.flow.component.combobox.ComboBox;
|
||||||
import com.vaadin.flow.component.grid.Grid;
|
import com.vaadin.flow.component.grid.Grid;
|
||||||
import com.vaadin.flow.component.html.Anchor;
|
import com.vaadin.flow.component.html.Anchor;
|
||||||
import com.vaadin.flow.component.html.H2;
|
import com.vaadin.flow.component.html.H2;
|
||||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||||
|
import com.vaadin.flow.component.textfield.TextField;
|
||||||
import com.vaadin.flow.server.StreamResource;
|
import com.vaadin.flow.server.StreamResource;
|
||||||
import com.vaadin.flow.router.PageTitle;
|
import com.vaadin.flow.router.PageTitle;
|
||||||
import com.vaadin.flow.router.Route;
|
import com.vaadin.flow.router.Route;
|
||||||
@@ -24,6 +26,8 @@ public class ShowJobsView extends VerticalLayout {
|
|||||||
|
|
||||||
private final DatePicker startDate = new DatePicker("Startdatum");
|
private final DatePicker startDate = new DatePicker("Startdatum");
|
||||||
private final DatePicker endDate = new DatePicker("Enddatum");
|
private final DatePicker endDate = new DatePicker("Enddatum");
|
||||||
|
private final TextField searchField = new TextField("Auftragsnummer suchen");
|
||||||
|
private final ComboBox<String> statusFilter = new ComboBox<>("Status");
|
||||||
private final JobRepository jobRepository;
|
private final JobRepository jobRepository;
|
||||||
private final SecurityService securityService;
|
private final SecurityService securityService;
|
||||||
private final Grid<Job> grid = new Grid<>(Job.class, false);
|
private final Grid<Job> grid = new Grid<>(Job.class, false);
|
||||||
@@ -35,9 +39,20 @@ public class ShowJobsView extends VerticalLayout {
|
|||||||
setSizeFull();
|
setSizeFull();
|
||||||
setPadding(true);
|
setPadding(true);
|
||||||
setSpacing(true);
|
setSpacing(true);
|
||||||
|
|
||||||
|
// Configure status filter
|
||||||
|
statusFilter.setItems("Alle", "Offen", "Erledigt");
|
||||||
|
statusFilter.setValue("Offen");
|
||||||
|
statusFilter.setWidth("150px");
|
||||||
|
|
||||||
|
// Configure search field
|
||||||
|
searchField.setPlaceholder("Auftragsnummer eingeben...");
|
||||||
|
searchField.setClearButtonVisible(true);
|
||||||
|
searchField.setWidth("200px");
|
||||||
|
|
||||||
// Filterleiste mit Export-Button am rechten Rand
|
// Filterleiste mit Export-Button am rechten Rand
|
||||||
Button applyFilter = new Button("Anwenden");
|
Button applyFilter = new Button("Anwenden");
|
||||||
HorizontalLayout leftFilters = new HorizontalLayout(startDate, endDate, applyFilter);
|
HorizontalLayout leftFilters = new HorizontalLayout(startDate, endDate, searchField, statusFilter, applyFilter);
|
||||||
leftFilters.setAlignItems(Alignment.END);
|
leftFilters.setAlignItems(Alignment.END);
|
||||||
|
|
||||||
HorizontalLayout filterBar = new HorizontalLayout();
|
HorizontalLayout filterBar = new HorizontalLayout();
|
||||||
@@ -50,7 +65,8 @@ public class ShowJobsView extends VerticalLayout {
|
|||||||
add(filterBar);
|
add(filterBar);
|
||||||
|
|
||||||
|
|
||||||
add(new H2("Offene Aufträge"));
|
H2 title = new H2("Aufträge");
|
||||||
|
add(title);
|
||||||
// Init default period: last 30 days
|
// Init default period: last 30 days
|
||||||
java.time.LocalDate today = java.time.LocalDate.now();
|
java.time.LocalDate today = java.time.LocalDate.now();
|
||||||
startDate.setValue(today.minusDays(30));
|
startDate.setValue(today.minusDays(30));
|
||||||
@@ -58,6 +74,12 @@ public class ShowJobsView extends VerticalLayout {
|
|||||||
applyFilter.addClickListener(e -> loadData());
|
applyFilter.addClickListener(e -> loadData());
|
||||||
exportButton.addClickListener(e -> exportToCsv());
|
exportButton.addClickListener(e -> exportToCsv());
|
||||||
|
|
||||||
|
// Add real-time filtering
|
||||||
|
searchField.addValueChangeListener(e -> loadData());
|
||||||
|
statusFilter.addValueChangeListener(e -> loadData());
|
||||||
|
startDate.addValueChangeListener(e -> loadData());
|
||||||
|
endDate.addValueChangeListener(e -> loadData());
|
||||||
|
|
||||||
|
|
||||||
// Configure grid columns: Kunde, Auftragsnummer, Auftragsdatum, Zielort
|
// Configure grid columns: Kunde, Auftragsnummer, Auftragsdatum, Zielort
|
||||||
grid.addColumn(Job::getDeliveryCompany).setHeader("Kunde").setAutoWidth(true).setFlexGrow(1).setSortable(true);
|
grid.addColumn(Job::getDeliveryCompany).setHeader("Kunde").setAutoWidth(true).setFlexGrow(1).setSortable(true);
|
||||||
@@ -94,17 +116,30 @@ public class ShowJobsView extends VerticalLayout {
|
|||||||
// Aktuellen Benutzer (ObjectId Hex) ermitteln
|
// Aktuellen Benutzer (ObjectId Hex) ermitteln
|
||||||
String currentUserIdHex = securityService.getCurrentUserId().toHexString();
|
String currentUserIdHex = securityService.getCurrentUserId().toHexString();
|
||||||
|
|
||||||
// Hole Aufträge im Zeitraum, filtere auf offenen Status und auf den angemeldeten Benutzer
|
// Status-Filter bestimmen
|
||||||
var inRange = jobRepository.findByCreatedAtBetween(startDt, endDt);
|
String selectedStatus = statusFilter.getValue();
|
||||||
var openAndOwn = inRange.stream()
|
java.util.List<JobStatus> statusList;
|
||||||
.filter(j -> j.getStatus() == JobStatus.CREATED
|
|
||||||
|| j.getStatus() == JobStatus.IN_PROGRESS
|
if ("Erledigt".equals(selectedStatus)) {
|
||||||
|| j.getStatus() == JobStatus.PICKUP_SCHEDULED
|
statusList = java.util.List.of(JobStatus.DELIVERED, JobStatus.COMPLETED, JobStatus.CANCELLED);
|
||||||
|| j.getStatus() == JobStatus.PICKED_UP
|
} else if ("Offen".equals(selectedStatus)) {
|
||||||
|| j.getStatus() == JobStatus.IN_TRANSIT)
|
statusList = java.util.List.of(JobStatus.CREATED, JobStatus.IN_PROGRESS,
|
||||||
.filter(j -> j.getCreatedBy() != null && j.getCreatedBy().equals(currentUserIdHex))
|
JobStatus.PICKUP_SCHEDULED, JobStatus.PICKED_UP,
|
||||||
.toList();
|
JobStatus.IN_TRANSIT);
|
||||||
grid.setItems(openAndOwn);
|
} 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
|
||||||
|
|
||||||
|
// Verwende die erweiterte Suchmethode
|
||||||
|
var filteredJobs = jobRepository.findWithFilters(startDt, endDt, currentUserIdHex,
|
||||||
|
jobNumberPattern, statusList);
|
||||||
|
grid.setItems(filteredJobs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void exportToCsv() {
|
private void exportToCsv() {
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package de.assecutor.votianlt.repository;
|
||||||
|
|
||||||
|
import de.assecutor.votianlt.model.JobHistory;
|
||||||
|
import de.assecutor.votianlt.model.JobHistoryType;
|
||||||
|
import org.bson.types.ObjectId;
|
||||||
|
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||||
|
import org.springframework.data.mongodb.repository.Query;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface JobHistoryRepository extends MongoRepository<JobHistory, ObjectId> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find history entries by change type for a specific job
|
||||||
|
*/
|
||||||
|
List<JobHistory> findByJobIdAndChangeTypeOrderByTimestampDesc(
|
||||||
|
ObjectId jobId, JobHistoryType changeType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find history entries made by a specific user
|
||||||
|
*/
|
||||||
|
List<JobHistory> findByChangedByOrderByTimestampDesc(String changedBy);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find recent history entries across all jobs (for dashboard/overview)
|
||||||
|
*/
|
||||||
|
@Query(value = "{}", sort = "{ 'timestamp' : -1 }")
|
||||||
|
List<JobHistory> findRecentHistoryEntries();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count history entries for a specific job
|
||||||
|
*/
|
||||||
|
long countByJobId(ObjectId jobId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the latest history entry for a specific job
|
||||||
|
*/
|
||||||
|
@Query(value = "{'jobId': ?0}", sort = "{ 'timestamp' : -1 }")
|
||||||
|
JobHistory findLatestByJobId(ObjectId jobId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all history entries for a specific job
|
||||||
|
*/
|
||||||
|
void deleteByJobId(ObjectId jobId);
|
||||||
|
}
|
||||||
@@ -89,4 +89,20 @@ public interface JobRepository extends MongoRepository<Job, ObjectId> {
|
|||||||
* Findet alle Aufträge, die einem bestimmten App-Nutzer zugewiesen sind
|
* Findet alle Aufträge, die einem bestimmten App-Nutzer zugewiesen sind
|
||||||
*/
|
*/
|
||||||
List<Job> findByAppUser(String appUser);
|
List<Job> findByAppUser(String appUser);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Findet Aufträge anhand einer partiellen Auftragsnummer (case-insensitive)
|
||||||
|
*/
|
||||||
|
@Query("{'jobNumber': {'$regex': ?0, '$options': 'i'}}")
|
||||||
|
List<Job> findByJobNumberContainingIgnoreCase(String jobNumber);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,274 @@
|
|||||||
|
package de.assecutor.votianlt.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import de.assecutor.votianlt.model.Job;
|
||||||
|
import de.assecutor.votianlt.model.JobHistory;
|
||||||
|
import de.assecutor.votianlt.model.JobHistoryType;
|
||||||
|
import de.assecutor.votianlt.model.JobStatus;
|
||||||
|
import de.assecutor.votianlt.repository.JobHistoryRepository;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.bson.types.ObjectId;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class JobHistoryService {
|
||||||
|
|
||||||
|
private final JobHistoryRepository jobHistoryRepository;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public JobHistoryService(JobHistoryRepository jobHistoryRepository) {
|
||||||
|
this.jobHistoryRepository = jobHistoryRepository;
|
||||||
|
this.objectMapper = new ObjectMapper();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log job creation
|
||||||
|
*/
|
||||||
|
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"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (job.getDeliveryCompany() != null) {
|
||||||
|
history.setDetails("Kunde: " + job.getDeliveryCompany());
|
||||||
|
}
|
||||||
|
|
||||||
|
jobHistoryRepository.save(history);
|
||||||
|
log.debug("Job creation logged for job {}", job.getIdAsString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to log job creation for job {}: {}", job.getIdAsString(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log job status change
|
||||||
|
*/
|
||||||
|
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));
|
||||||
|
|
||||||
|
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);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to log status change for job {}: {}", job.getIdAsString(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log general job update
|
||||||
|
*/
|
||||||
|
public void logJobUpdate(Job oldJob, Job newJob, String changedBy, String reason) {
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
|
jobHistoryRepository.save(history);
|
||||||
|
log.debug("Job update logged for job {}", newJob.getIdAsString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to log job update for job {}: {}", newJob.getIdAsString(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log task completion
|
||||||
|
*/
|
||||||
|
public void logTaskCompletion(ObjectId jobId, String taskType, String taskId, String completedBy) {
|
||||||
|
logTaskCompletion(jobId, taskType, taskId, completedBy, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log task completion with detailed information and extraData
|
||||||
|
*/
|
||||||
|
public void logTaskCompletion(ObjectId jobId, String taskType, String taskId, String completedBy,
|
||||||
|
String taskDisplayName, String extraDataSummary) {
|
||||||
|
try {
|
||||||
|
String taskName = taskDisplayName != null ? taskDisplayName : taskType;
|
||||||
|
String description = String.format("Aufgabe abgeschlossen: %s", taskName);
|
||||||
|
|
||||||
|
if (extraDataSummary != null && !extraDataSummary.isBlank()) {
|
||||||
|
description += " - " + extraDataSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
JobHistory history = new JobHistory(
|
||||||
|
jobId,
|
||||||
|
"Aufgabe abgeschlossen",
|
||||||
|
description,
|
||||||
|
completedBy,
|
||||||
|
JobHistoryType.TASK_COMPLETED,
|
||||||
|
"In Bearbeitung",
|
||||||
|
"Abgeschlossen"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Detaillierte Informationen in details speichern
|
||||||
|
StringBuilder details = new StringBuilder();
|
||||||
|
details.append("Task-ID: ").append(taskId);
|
||||||
|
details.append(", Task-Typ: ").append(taskType);
|
||||||
|
|
||||||
|
if (taskDisplayName != null && !taskDisplayName.equals(taskType)) {
|
||||||
|
details.append(", Name: ").append(taskDisplayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extraDataSummary != null && !extraDataSummary.isBlank()) {
|
||||||
|
details.append(", Zusatzdaten: ").append(extraDataSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
history.setDetails(details.toString());
|
||||||
|
jobHistoryRepository.save(history);
|
||||||
|
log.debug("Task completion logged for job {}, task {} with details", jobId.toHexString(), taskId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to log task completion for job {}: {}", jobId.toHexString(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log job assignment
|
||||||
|
*/
|
||||||
|
public void logJobAssignment(Job job, String oldAssignee, String newAssignee, String changedBy) {
|
||||||
|
try {
|
||||||
|
String description;
|
||||||
|
if (oldAssignee == null && newAssignee != null) {
|
||||||
|
description = "Job zugewiesen an: " + newAssignee;
|
||||||
|
} else if (oldAssignee != null && newAssignee == null) {
|
||||||
|
description = "Job-Zuweisung entfernt von: " + oldAssignee;
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
jobHistoryRepository.save(history);
|
||||||
|
log.debug("Job assignment logged for job {}", job.getIdAsString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to log job assignment for job {}: {}", job.getIdAsString(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log custom event
|
||||||
|
*/
|
||||||
|
public void logCustomEvent(ObjectId jobId, String reason, String description, String changedBy,
|
||||||
|
JobHistoryType type) {
|
||||||
|
try {
|
||||||
|
JobHistory history = new JobHistory(jobId, reason, description, changedBy, type, null, null);
|
||||||
|
jobHistoryRepository.save(history);
|
||||||
|
log.debug("Custom event logged for job {}: {}", jobId.toHexString(), reason);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to log custom event for job {}: {}", jobId.toHexString(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get job history
|
||||||
|
*/
|
||||||
|
public List<JobHistory> getJobHistory(ObjectId jobId) {
|
||||||
|
return jobHistoryRepository.findByJobIdOrderByTimestampDesc(jobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get job history count
|
||||||
|
*/
|
||||||
|
public long getJobHistoryCount(ObjectId jobId) {
|
||||||
|
return jobHistoryRepository.countByJobId(jobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
|
||||||
|
private String formatStatus(JobStatus status) {
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateUpdateDescription(Job oldJob, Job newJob) {
|
||||||
|
StringBuilder description = new StringBuilder("Job-Daten aktualisiert");
|
||||||
|
|
||||||
|
if (oldJob == null) {
|
||||||
|
return description.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for specific field changes
|
||||||
|
boolean hasChanges = false;
|
||||||
|
|
||||||
|
if (!equals(oldJob.getDeliveryCompany(), newJob.getDeliveryCompany())) {
|
||||||
|
description.append(" - Kunde");
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(",");
|
||||||
|
description.append(" - Bemerkung");
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return description.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean equals(Object a, Object b) {
|
||||||
|
return (a == null && b == null) || (a != null && a.equals(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String serializeJobForComparison(Job job) {
|
||||||
|
try {
|
||||||
|
// Only serialize relevant fields for comparison
|
||||||
|
return objectMapper.writeValueAsString(job);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "Serialization failed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user