Erweiterungen

This commit is contained in:
2025-09-15 11:27:41 +02:00
parent 99355039ed
commit c694222224
10 changed files with 1253 additions and 26 deletions

View File

@@ -18,6 +18,7 @@ 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 de.assecutor.votianlt.service.JobHistoryService;
import lombok.extern.slf4j.Slf4j;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
@@ -55,8 +56,9 @@ public class MessageController {
private final PhotoRepository photoRepository;
private final BarcodeRepository barcodeRepository;
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.appUserRepository = appUserRepository;
this.appUserService = appUserService;
@@ -66,6 +68,7 @@ public class MessageController {
this.photoRepository = photoRepository;
this.barcodeRepository = barcodeRepository;
this.signatureRepository = signatureRepository;
this.jobHistoryService = jobHistoryService;
}
/**
@@ -232,14 +235,12 @@ public class MessageController {
private void processConfirmationTaskCompletion(Map<String, Object> payload) {
Object taskId = payload.get("taskId");
completeTask(taskId);
completeTaskWithHistory(taskId, "Bestätigung durchgeführt");
}
private void processTodoListTaskCompletion(Map<String, Object> payload) {
Object taskId = payload.get("taskId");
completeTask(taskId);
completeTaskWithHistory(taskId, "Alle To-Do-Elemente abgehakt");
}
private void processBarcodeTaskCompletion(Map<String, Object> payload) {
@@ -252,6 +253,7 @@ public class MessageController {
}
BaseTask task = opt.get();
String extraDataSummary = null;
Object extra = payload.get("extraData");
if (extra instanceof Map<?, ?> extraData) {
Object barcodesObj = extraData.get("barcodes");
@@ -270,19 +272,23 @@ public class MessageController {
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);
} else {
extraDataSummary = "Keine Barcodes gescannt";
log.info("No barcodes found in extraData for taskId={}", taskId);
}
} else {
extraDataSummary = "Barcode-Daten fehlerhaft";
log.warn("extraData.barcodes is not a List for taskId={}", taskId);
}
} else {
extraDataSummary = "Keine Extra-Daten";
log.warn("extraData is not a Map for taskId={}", taskId);
}
// Finally, mark the task as completed
completeTask(taskId);
// Finally, mark the task as completed with history logging
completeTaskWithHistory(taskId, extraDataSummary);
} catch (IllegalArgumentException ex) {
log.error("Invalid taskId format for barcode completion: {}", taskId);
} catch (Exception ex) {
@@ -300,6 +306,7 @@ public class MessageController {
}
BaseTask task = opt.get();
String extraDataSummary = null;
Object extra = payload.get("extraData");
if (extra instanceof Map<?, ?> extraData) {
Object signatureSvgObj = extraData.get("signatureSvg");
@@ -312,19 +319,23 @@ public class MessageController {
);
signatureRepository.save(signatureEntry);
extraDataSummary = "Unterschrift erfasst (SVG, " + signatureSvg.length() + " Zeichen)";
log.info("Saved signature for taskId={}", taskId);
} else {
extraDataSummary = "Leere Unterschrift";
log.info("Empty signature SVG found for taskId={}", taskId);
}
} else {
extraDataSummary = "Unterschrift-Daten fehlerhaft";
log.warn("extraData.signatureSvg is not a String for taskId={}", taskId);
}
} else {
extraDataSummary = "Keine Extra-Daten";
log.warn("extraData is not a Map for taskId={}", taskId);
}
// Finally, mark the task as completed
completeTask(taskId);
// Finally, mark the task as completed with history logging
completeTaskWithHistory(taskId, extraDataSummary);
} catch (IllegalArgumentException ex) {
log.error("Invalid taskId format for signature completion: {}", taskId);
} catch (Exception ex) {
@@ -343,6 +354,7 @@ public class MessageController {
BaseTask task = opt.get();
ObjectId jobId = new ObjectId(task.getJobIdAsString());
String extraDataSummary = null;
Object extra = payload.get("extraData");
if (extra instanceof Map<?, ?> extraData) {
Object photosObj = extraData.get("photos");
@@ -361,19 +373,23 @@ public class MessageController {
photoRepository.save(photoEntry);
}
extraDataSummary = photos.size() + " Foto(s) aufgenommen";
log.info("Saved {} photos for taskId={}, jobId={}", photos.size(), taskId, jobId);
} else {
extraDataSummary = "Keine Fotos aufgenommen";
log.info("No photos found in extraData for taskId={}", taskId);
}
} else {
extraDataSummary = "Foto-Daten fehlerhaft";
log.warn("extraData.photos is not a List for taskId={}", taskId);
}
} else {
extraDataSummary = "Keine Extra-Daten";
log.warn("extraData is not a Map for taskId={}", taskId);
}
// Finally, mark the task as completed
completeTask(taskId);
// Finally, mark the task as completed with history logging
completeTaskWithHistory(taskId, extraDataSummary);
} catch (IllegalArgumentException ex) {
log.error("Invalid taskId format for photo completion: {}", taskId);
} catch (Exception ex) {
@@ -382,19 +398,37 @@ public class MessageController {
}
private void completeTask(Object tid) {
completeTaskWithHistory(tid, null);
}
private void completeTaskWithHistory(Object tid, String extraDataSummary) {
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);
log.warn("Task not found for completion. taskId={}", taskIdStr);
return;
}
BaseTask task = opt.get();
task.setCompleted(true);
task.setCompletedAt(LocalDateTime.now());
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) {
log.error("Invalid taskId format for completion: {}", taskIdStr);
} catch (Exception ex) {

View 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;
}
}

View File

@@ -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
}

View File

@@ -8,6 +8,7 @@ import de.assecutor.votianlt.repository.JobRepository;
import de.assecutor.votianlt.repository.TaskRepository;
import de.assecutor.votianlt.security.SecurityService;
import de.assecutor.votianlt.repository.CargoItemRepository;
import de.assecutor.votianlt.service.JobHistoryService;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -28,6 +29,7 @@ public class AddJobService {
private final JobRepository jobRepository;
private final TaskRepository taskRepository;
private final SecurityService securityService;
private final JobHistoryService jobHistoryService;
/**
* Speichert einen neuen Auftrag samt CargoItems und Tasks
* @param job der Auftrag
@@ -97,6 +99,14 @@ public class AddJobService {
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());
return savedJob;
@@ -136,4 +146,20 @@ public class AddJobService {
List<Job> drafts = jobRepository.findByCreatedByAndIsDraftTrue(username);
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
}
}

View File

@@ -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());
}
}
}

View File

@@ -222,6 +222,18 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
// Google Maps Karte mit Route
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() {

View File

@@ -2,11 +2,13 @@ package de.assecutor.votianlt.pages.view;
import com.vaadin.flow.component.datepicker.DatePicker;
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.html.Anchor;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.server.StreamResource;
import com.vaadin.flow.router.PageTitle;
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 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 SecurityService securityService;
private final Grid<Job> grid = new Grid<>(Job.class, false);
@@ -35,9 +39,20 @@ public class ShowJobsView extends VerticalLayout {
setSizeFull();
setPadding(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
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);
HorizontalLayout filterBar = new HorizontalLayout();
@@ -50,7 +65,8 @@ public class ShowJobsView extends VerticalLayout {
add(filterBar);
add(new H2("Offene Aufträge"));
H2 title = new H2("Aufträge");
add(title);
// Init default period: last 30 days
java.time.LocalDate today = java.time.LocalDate.now();
startDate.setValue(today.minusDays(30));
@@ -58,6 +74,12 @@ public class ShowJobsView extends VerticalLayout {
applyFilter.addClickListener(e -> loadData());
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
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
String currentUserIdHex = securityService.getCurrentUserId().toHexString();
// Hole Aufträge im Zeitraum, filtere auf offenen Status und auf den angemeldeten Benutzer
var inRange = jobRepository.findByCreatedAtBetween(startDt, endDt);
var openAndOwn = inRange.stream()
.filter(j -> j.getStatus() == JobStatus.CREATED
|| j.getStatus() == JobStatus.IN_PROGRESS
|| j.getStatus() == JobStatus.PICKUP_SCHEDULED
|| j.getStatus() == JobStatus.PICKED_UP
|| j.getStatus() == JobStatus.IN_TRANSIT)
.filter(j -> j.getCreatedBy() != null && j.getCreatedBy().equals(currentUserIdHex))
.toList();
grid.setItems(openAndOwn);
// Status-Filter bestimmen
String selectedStatus = statusFilter.getValue();
java.util.List<JobStatus> statusList;
if ("Erledigt".equals(selectedStatus)) {
statusList = java.util.List.of(JobStatus.DELIVERED, JobStatus.COMPLETED, JobStatus.CANCELLED);
} else if ("Offen".equals(selectedStatus)) {
statusList = java.util.List.of(JobStatus.CREATED, JobStatus.IN_PROGRESS,
JobStatus.PICKUP_SCHEDULED, JobStatus.PICKED_UP,
JobStatus.IN_TRANSIT);
} 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() {

View File

@@ -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);
}

View File

@@ -89,4 +89,20 @@ public interface JobRepository extends MongoRepository<Job, ObjectId> {
* Findet alle Aufträge, die einem bestimmten App-Nutzer zugewiesen sind
*/
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);
}

View File

@@ -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";
}
}
}