feat: refresh job summary after task updates

- broadcast job updates after mobile task completions are persisted\n- rerender job_summary live for the affected job via UI access\n- show pickup and delivery tile detail lines like on add_job\n- highlight delivery tiles in light green when all station tasks are completed
This commit is contained in:
2026-03-10 09:25:12 +01:00
parent 9f7e0af6e0
commit c39b4f8b52
3 changed files with 274 additions and 22 deletions

View File

@@ -22,6 +22,7 @@ import de.assecutor.votianlt.model.Barcode;
import de.assecutor.votianlt.model.Signature; import de.assecutor.votianlt.model.Signature;
import de.assecutor.votianlt.model.Comment; import de.assecutor.votianlt.model.Comment;
import de.assecutor.votianlt.service.JobHistoryService; import de.assecutor.votianlt.service.JobHistoryService;
import de.assecutor.votianlt.service.JobUpdateBroadcaster;
import de.assecutor.votianlt.service.EmailService; import de.assecutor.votianlt.service.EmailService;
import de.assecutor.votianlt.service.MessageService; import de.assecutor.votianlt.service.MessageService;
import de.assecutor.votianlt.model.JobStatus; import de.assecutor.votianlt.model.JobStatus;
@@ -59,6 +60,7 @@ public class MessageController {
private final SignatureRepository signatureRepository; private final SignatureRepository signatureRepository;
private final CommentRepository commentRepository; private final CommentRepository commentRepository;
private final JobHistoryService jobHistoryService; private final JobHistoryService jobHistoryService;
private final JobUpdateBroadcaster jobUpdateBroadcaster;
private final EmailService emailService; private final EmailService emailService;
private final MessageService messageService; private final MessageService messageService;
@@ -66,7 +68,8 @@ public class MessageController {
AppUserService appUserService, JobRepository jobRepository, CargoItemRepository cargoItemRepository, AppUserService appUserService, JobRepository jobRepository, CargoItemRepository cargoItemRepository,
TaskRepository taskRepository, PhotoRepository photoRepository, BarcodeRepository barcodeRepository, TaskRepository taskRepository, PhotoRepository photoRepository, BarcodeRepository barcodeRepository,
SignatureRepository signatureRepository, CommentRepository commentRepository, SignatureRepository signatureRepository, CommentRepository commentRepository,
JobHistoryService jobHistoryService, EmailService emailService, MessageService messageService) { JobHistoryService jobHistoryService, JobUpdateBroadcaster jobUpdateBroadcaster, EmailService emailService,
MessageService messageService) {
this.messagingPublisher = messagingPublisher; this.messagingPublisher = messagingPublisher;
this.appUserRepository = appUserRepository; this.appUserRepository = appUserRepository;
this.appUserService = appUserService; this.appUserService = appUserService;
@@ -78,6 +81,7 @@ public class MessageController {
this.signatureRepository = signatureRepository; this.signatureRepository = signatureRepository;
this.commentRepository = commentRepository; this.commentRepository = commentRepository;
this.jobHistoryService = jobHistoryService; this.jobHistoryService = jobHistoryService;
this.jobUpdateBroadcaster = jobUpdateBroadcaster;
this.emailService = emailService; this.emailService = emailService;
this.messageService = messageService; this.messageService = messageService;
} }
@@ -345,10 +349,10 @@ public class MessageController {
task.setCompleted(true); task.setCompleted(true);
task.setCompletedAt(LocalDateTime.now()); task.setCompletedAt(LocalDateTime.now());
taskRepository.save(task); taskRepository.save(task);
ObjectId jobId = new ObjectId(task.getJobIdAsString());
// Log detailed task completion in job history // Log detailed task completion in job history
try { try {
ObjectId jobId = new ObjectId(task.getJobIdAsString());
String taskType = task.getTaskType() != null ? task.getTaskType().toString() : "Unknown"; String taskType = task.getTaskType() != null ? task.getTaskType().toString() : "Unknown";
String taskDisplayName = task.getDisplayName() != null ? task.getDisplayName() : taskType; String taskDisplayName = task.getDisplayName() != null ? task.getDisplayName() : taskType;
String completedBy = task.getCompletedBy() != null ? task.getCompletedBy() : "Unknown"; String completedBy = task.getCompletedBy() != null ? task.getCompletedBy() : "Unknown";
@@ -360,7 +364,6 @@ public class MessageController {
// Send email notification for task completion // Send email notification for task completion
try { try {
ObjectId jobId = new ObjectId(task.getJobIdAsString());
String taskType = task.getTaskType() != null ? task.getTaskType().toString() : "Unknown"; String taskType = task.getTaskType() != null ? task.getTaskType().toString() : "Unknown";
String completedBy = task.getCompletedBy() != null ? task.getCompletedBy() : "Unknown"; String completedBy = task.getCompletedBy() != null ? task.getCompletedBy() : "Unknown";
emailService.sendTaskCompletionNotification(jobId, taskType, taskIdStr, completedBy); emailService.sendTaskCompletionNotification(jobId, taskType, taskIdStr, completedBy);
@@ -368,6 +371,8 @@ public class MessageController {
} catch (Exception e) { } catch (Exception e) {
// Ignore email notification errors // Ignore email notification errors
} }
jobUpdateBroadcaster.broadcast(jobId);
} catch (Exception ex) { } catch (Exception ex) {
log.error("[TASK] Completion error: {}", ex.getMessage()); log.error("[TASK] Completion error: {}", ex.getMessage());
} }

View File

@@ -14,10 +14,14 @@ import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant; import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.DetachEvent;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.router.BeforeEvent; import com.vaadin.flow.router.BeforeEvent;
import com.vaadin.flow.router.HasUrlParameter; import com.vaadin.flow.router.HasUrlParameter;
import com.vaadin.flow.router.HasDynamicTitle; import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route; import com.vaadin.flow.router.Route;
import com.vaadin.flow.shared.Registration;
import com.vaadin.flow.theme.lumo.LumoUtility; import com.vaadin.flow.theme.lumo.LumoUtility;
import de.assecutor.votianlt.model.CargoItem; import de.assecutor.votianlt.model.CargoItem;
import de.assecutor.votianlt.model.DeliveryStation; import de.assecutor.votianlt.model.DeliveryStation;
@@ -51,6 +55,7 @@ import de.assecutor.votianlt.model.Comment;
import de.assecutor.votianlt.model.JobStatus; import de.assecutor.votianlt.model.JobStatus;
import de.assecutor.votianlt.pages.service.AppUserService; import de.assecutor.votianlt.pages.service.AppUserService;
import de.assecutor.votianlt.service.JobHistoryService; import de.assecutor.votianlt.service.JobHistoryService;
import de.assecutor.votianlt.service.JobUpdateBroadcaster;
import de.assecutor.votianlt.service.LocationService; import de.assecutor.votianlt.service.LocationService;
import de.assecutor.votianlt.service.MessageService; import de.assecutor.votianlt.service.MessageService;
import de.assecutor.votianlt.util.DateTimeFormatUtil; import de.assecutor.votianlt.util.DateTimeFormatUtil;
@@ -80,6 +85,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
private final CommentRepository commentRepository; private final CommentRepository commentRepository;
private final AppUserService appUserService; private final AppUserService appUserService;
private final JobHistoryService jobHistoryService; private final JobHistoryService jobHistoryService;
private final JobUpdateBroadcaster jobUpdateBroadcaster;
private final LocationService locationService; private final LocationService locationService;
private final ServiceRepository serviceRepository; private final ServiceRepository serviceRepository;
@@ -88,11 +94,14 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
private final VerticalLayout content; private final VerticalLayout content;
private final List<Div> taskCards = new ArrayList<>(); private final List<Div> taskCards = new ArrayList<>();
private Registration jobUpdateRegistration;
private ObjectId currentJobId;
public JobSummaryView(JobRepository jobRepository, CargoItemRepository cargoItemRepository, public JobSummaryView(JobRepository jobRepository, CargoItemRepository cargoItemRepository,
TaskRepository taskRepository, SignatureRepository signatureRepository, BarcodeRepository barcodeRepository, TaskRepository taskRepository, SignatureRepository signatureRepository, BarcodeRepository barcodeRepository,
PhotoRepository photoRepository, CommentRepository commentRepository, AppUserService appUserService, PhotoRepository photoRepository, CommentRepository commentRepository, AppUserService appUserService,
MessageService messageService, JobHistoryService jobHistoryService, LocationService locationService, MessageService messageService, JobHistoryService jobHistoryService,
JobUpdateBroadcaster jobUpdateBroadcaster, LocationService locationService,
ServiceRepository serviceRepository) { ServiceRepository serviceRepository) {
this.jobRepository = jobRepository; this.jobRepository = jobRepository;
this.cargoItemRepository = cargoItemRepository; this.cargoItemRepository = cargoItemRepository;
@@ -103,6 +112,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
this.commentRepository = commentRepository; this.commentRepository = commentRepository;
this.appUserService = appUserService; this.appUserService = appUserService;
this.jobHistoryService = jobHistoryService; this.jobHistoryService = jobHistoryService;
this.jobUpdateBroadcaster = jobUpdateBroadcaster;
this.locationService = locationService; this.locationService = locationService;
this.serviceRepository = serviceRepository; this.serviceRepository = serviceRepository;
@@ -118,30 +128,62 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
@Override @Override
public void setParameter(BeforeEvent event, String parameter) { public void setParameter(BeforeEvent event, String parameter) {
content.removeAll();
removeAll(); // Remove existing toolbar
if (parameter == null || parameter.isBlank()) { if (parameter == null || parameter.isBlank()) {
content.removeAll();
removeAll();
add(new ViewToolbar("Zusammenfassung")); add(new ViewToolbar("Zusammenfassung"));
content.add(new Span("Fehler: Keine Job-ID angegeben")); content.add(new Span("Fehler: Keine Job-ID angegeben"));
add(content); add(content);
return; return;
} }
ObjectId jobId;
try { try {
jobId = new ObjectId(parameter); currentJobId = new ObjectId(parameter);
} catch (Exception e) { } catch (Exception e) {
content.removeAll();
removeAll();
add(new ViewToolbar("Zusammenfassung")); add(new ViewToolbar("Zusammenfassung"));
content.add(new Span("Fehler: Ungültige Job-ID Format: " + parameter)); content.add(new Span("Fehler: Ungültige Job-ID Format: " + parameter));
add(content); add(content);
return; return;
} }
Job job = jobRepository.findById(jobId).orElse(null); refreshCurrentJobSummary();
}
@Override
protected void onAttach(AttachEvent attachEvent) {
super.onAttach(attachEvent);
UI ui = attachEvent.getUI();
jobUpdateRegistration = jobUpdateBroadcaster.register(jobId -> {
if (currentJobId == null || jobId == null || !currentJobId.equals(jobId)) {
return;
}
ui.access(this::refreshCurrentJobSummary);
});
}
@Override
protected void onDetach(DetachEvent detachEvent) {
if (jobUpdateRegistration != null) {
jobUpdateRegistration.remove();
jobUpdateRegistration = null;
}
super.onDetach(detachEvent);
}
private void refreshCurrentJobSummary() {
if (currentJobId == null) {
return;
}
content.removeAll();
removeAll();
Job job = jobRepository.findById(currentJobId).orElse(null);
if (job == null) { if (job == null) {
add(new ViewToolbar("Zusammenfassung")); add(new ViewToolbar("Zusammenfassung"));
content.add(new Span("Fehler: Job mit ID " + parameter + " nicht gefunden")); content.add(new Span("Fehler: Job mit ID " + currentJobId.toHexString() + " nicht gefunden"));
add(content); add(content);
return; return;
} }
@@ -177,8 +219,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
// Add toolbar with both buttons in top right (Send Message button on the left) // Add toolbar with both buttons in top right (Send Message button on the left)
add(new ViewToolbar("Zusammenfassung", sendMessageButton, jobHistoryButton)); add(new ViewToolbar("Zusammenfassung", sendMessageButton, jobHistoryButton));
List<CargoItem> cargo = cargoItemRepository.findByJobId(jobId); List<CargoItem> cargo = cargoItemRepository.findByJobId(currentJobId);
List<BaseTask> tasks = taskRepository.findByJobIdOrderByTaskOrderAsc(jobId); List<BaseTask> tasks = taskRepository.findByJobIdOrderByTaskOrderAsc(currentJobId);
render(job, cargo, tasks); render(job, cargo, tasks);
add(content); add(content);
@@ -187,7 +229,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
private void render(Job job, List<CargoItem> cargoItems, List<BaseTask> tasks) { private void render(Job job, List<CargoItem> cargoItems, List<BaseTask> tasks) {
content.removeAll(); content.removeAll();
content.add(createStationTilesSection(job, tasks)); content.add(createStationTilesSection(job, cargoItems, tasks));
// Fracht und weitere Infos // Fracht und weitere Infos
HorizontalLayout midRow = new HorizontalLayout(); HorizontalLayout midRow = new HorizontalLayout();
@@ -305,14 +347,14 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
return box; return box;
} }
private Div createStationTilesSection(Job job, List<BaseTask> tasks) { private Div createStationTilesSection(Job job, List<CargoItem> cargoItems, List<BaseTask> tasks) {
Div stationGrid = new Div(); Div stationGrid = new Div();
stationGrid.getStyle().set("display", "grid"); stationGrid.getStyle().set("display", "grid");
stationGrid.getStyle().set("grid-template-columns", "repeat(auto-fit, minmax(220px, 1fr))"); stationGrid.getStyle().set("grid-template-columns", "repeat(auto-fit, minmax(220px, 1fr))");
stationGrid.getStyle().set("gap", "var(--lumo-space-m)"); stationGrid.getStyle().set("gap", "var(--lumo-space-m)");
stationGrid.setWidthFull(); stationGrid.setWidthFull();
stationGrid.add(createPickupSummaryTile(job)); stationGrid.add(createPickupSummaryTile(job, cargoItems));
List<DeliveryStation> stations = job.getDeliveryStations(); List<DeliveryStation> stations = job.getDeliveryStations();
if (stations != null && !stations.isEmpty()) { if (stations != null && !stations.isEmpty()) {
@@ -326,10 +368,10 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
return stationGrid; return stationGrid;
} }
private StationTile createPickupSummaryTile(Job job) { private StationTile createPickupSummaryTile(Job job, List<CargoItem> cargoItems) {
String title = getTranslation("jobsummary.section.pickup") + " " String title = getTranslation("jobsummary.section.pickup") + " "
+ formatDateWithTime(job.getPickupDate(), job.getPickupTime()); + formatDateWithTime(job.getPickupDate(), job.getPickupTime());
List<String> additionalLines = new ArrayList<>(); List<String> additionalLines = buildPickupSummaryDetails(job, cargoItems);
if (job.getPickupPhone() != null && !job.getPickupPhone().isBlank()) { if (job.getPickupPhone() != null && !job.getPickupPhone().isBlank()) {
additionalLines.add(getTranslation("jobsummary.station.phone") + ": " + job.getPickupPhone()); additionalLines.add(getTranslation("jobsummary.station.phone") + ": " + job.getPickupPhone());
} }
@@ -345,7 +387,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
String title = getTranslation("jobsummary.section.delivery") + " " String title = getTranslation("jobsummary.section.delivery") + " "
+ (stationCount > 1 ? (index + 1) + " " : "") + (stationCount > 1 ? (index + 1) + " " : "")
+ formatDateWithTime(station.getDeliveryDate(), station.getDeliveryTime()); + formatDateWithTime(station.getDeliveryDate(), station.getDeliveryTime());
List<String> additionalLines = new ArrayList<>(); List<BaseTask> stationTasks = getTasksForStation(station, tasks, false);
List<String> additionalLines = buildDeliverySummaryDetails(stationTasks);
if (station.getPhone() != null && !station.getPhone().isBlank()) { if (station.getPhone() != null && !station.getPhone().isBlank()) {
additionalLines.add(getTranslation("jobsummary.station.phone") + ": " + station.getPhone()); additionalLines.add(getTranslation("jobsummary.station.phone") + ": " + station.getPhone());
} }
@@ -353,7 +396,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
StationTile tile = createSummaryTile(StationTile.StationType.DELIVERY, index + 1, title, station.getCompany(), StationTile tile = createSummaryTile(StationTile.StationType.DELIVERY, index + 1, title, station.getCompany(),
buildDisplayName(station.getSalutation(), station.getFirstName(), station.getLastName()), buildDisplayName(station.getSalutation(), station.getFirstName(), station.getLastName()),
station.getStreet(), station.getHouseNumber(), station.getZip(), station.getCity(), additionalLines); station.getStreet(), station.getHouseNumber(), station.getZip(), station.getCity(), additionalLines);
List<BaseTask> stationTasks = getTasksForStation(station, tasks, false); tile.setAddressValidated(areAllTasksCompleted(stationTasks));
tile.setInteractive(true); tile.setInteractive(true);
tile.setClickListener(clickedTile -> showStationTasksDialog(title, stationTasks)); tile.setClickListener(clickedTile -> showStationTasksDialog(title, stationTasks));
return tile; return tile;
@@ -362,7 +405,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
private StationTile createLegacyDeliverySummaryTile(Job job, List<BaseTask> tasks) { private StationTile createLegacyDeliverySummaryTile(Job job, List<BaseTask> tasks) {
String title = getTranslation("jobsummary.section.delivery") + " " String title = getTranslation("jobsummary.section.delivery") + " "
+ formatDateWithTime(job.getDeliveryDate(), job.getDeliveryTime()); + formatDateWithTime(job.getDeliveryDate(), job.getDeliveryTime());
List<String> additionalLines = new ArrayList<>(); List<BaseTask> stationTasks = getTasksForStation(null, tasks, true);
List<String> additionalLines = buildDeliverySummaryDetails(stationTasks);
if (job.getDeliveryPhone() != null && !job.getDeliveryPhone().isBlank()) { if (job.getDeliveryPhone() != null && !job.getDeliveryPhone().isBlank()) {
additionalLines.add(getTranslation("jobsummary.station.phone") + ": " + job.getDeliveryPhone()); additionalLines.add(getTranslation("jobsummary.station.phone") + ": " + job.getDeliveryPhone());
} }
@@ -371,7 +415,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
buildDisplayName(job.getDeliverySalutation(), job.getDeliveryFirstName(), job.getDeliveryLastName()), buildDisplayName(job.getDeliverySalutation(), job.getDeliveryFirstName(), job.getDeliveryLastName()),
job.getDeliveryStreet(), job.getDeliveryHouseNumber(), job.getDeliveryZip(), job.getDeliveryCity(), job.getDeliveryStreet(), job.getDeliveryHouseNumber(), job.getDeliveryZip(), job.getDeliveryCity(),
additionalLines); additionalLines);
List<BaseTask> stationTasks = getTasksForStation(null, tasks, true); tile.setAddressValidated(areAllTasksCompleted(stationTasks));
tile.setInteractive(true); tile.setInteractive(true);
tile.setClickListener(clickedTile -> showStationTasksDialog(title, stationTasks)); tile.setClickListener(clickedTile -> showStationTasksDialog(title, stationTasks));
return tile; return tile;
@@ -386,6 +430,161 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
return tile; return tile;
} }
private List<String> buildPickupSummaryDetails(Job job, List<CargoItem> cargoItems) {
List<String> additionalLines = new ArrayList<>();
additionalLines.add(getTranslation("addjob.tab.cargo") + ": " + summarizeCargoItems(cargoItems));
String pickupAppointment = formatPickupAppointment(job.getPickupDate(), job.getPickupTime());
if (pickupAppointment != null) {
additionalLines.add(getTranslation("addjob.appointment.pickup") + ": " + pickupAppointment);
}
additionalLines.add(buildDigitalProcessingPreview(job.isDigitalProcessing(), job.getAppUser()));
return additionalLines;
}
private String summarizeCargoItems(List<CargoItem> cargoItems) {
if (cargoItems == null || cargoItems.isEmpty()) {
return getTranslation("jobsummary.cargo.none");
}
List<String> summaries = new ArrayList<>();
for (CargoItem cargoItem : cargoItems) {
if (cargoItem == null) {
continue;
}
String description = cargoItem.getDescription() != null ? cargoItem.getDescription().trim() : "";
Integer quantity = cargoItem.getQuantity();
if (description.isEmpty() && quantity == null) {
continue;
}
StringBuilder summary = new StringBuilder();
if (quantity != null) {
summary.append(quantity).append("x ");
}
summary.append(description.isEmpty() ? getTranslation("addjob.tab.cargo") : description);
summaries.add(summary.toString().trim());
}
if (summaries.isEmpty()) {
return getTranslation("jobsummary.cargo.none");
}
if (summaries.size() <= 2) {
return String.join(", ", summaries);
}
return String.join(", ", summaries.subList(0, 2)) + " +" + (summaries.size() - 2);
}
private String formatPickupAppointment(java.time.LocalDate appointmentDate, java.time.LocalTime appointmentTime) {
if (appointmentDate == null) {
return null;
}
String formattedDate = formatLocalDate(appointmentDate);
if (appointmentTime == null) {
return formattedDate;
}
return formattedDate + " " + formatLocalTime(appointmentTime);
}
private String buildDigitalProcessingPreview(boolean digitalProcessingEnabled, String appUserId) {
StringBuilder preview = new StringBuilder();
preview.append(getTranslation("profile.settings.digitalprocess")).append(": ")
.append(getTranslation(digitalProcessingEnabled ? "common.yes" : "common.no"));
if (digitalProcessingEnabled && appUserId != null && !appUserId.isBlank()) {
preview.append(" (").append(resolveAppUserName(appUserId)).append(")");
}
return preview.toString();
}
private List<String> buildDeliverySummaryDetails(List<BaseTask> tasks) {
if (tasks == null || tasks.isEmpty()) {
return List.of(getTranslation("addjob.tab.tasks") + ": " + getTranslation("jobsummary.tasks.none"));
}
List<String> summaries = new ArrayList<>();
for (BaseTask task : tasks) {
if (task != null) {
summaries.add(summarizeDeliveryTask(task));
}
}
if (summaries.isEmpty()) {
return List.of(getTranslation("addjob.tab.tasks") + ": " + getTranslation("jobsummary.tasks.none"));
}
return summaries;
}
private String summarizeDeliveryTask(BaseTask task) {
if (task instanceof ConfirmationTask confirmationTask) {
String buttonText = trimToNull(confirmationTask.getButtonText());
if (buttonText != null) {
return confirmationTask.getDisplayName() + " \"" + buttonText + "\"";
}
String description = trimToNull(confirmationTask.getDescription());
return description != null ? confirmationTask.getDisplayName() + " \"" + description + "\""
: confirmationTask.getDisplayName();
}
if (task instanceof TodoListTask todoListTask) {
long itemCount = todoListTask.getTodoItems() == null ? 0
: todoListTask.getTodoItems().stream().filter(item -> item != null && !item.trim().isEmpty())
.count();
return itemCount > 0 ? task.getDisplayName() + " (" + itemCount + ")" : task.getDisplayName();
}
if (task instanceof PhotoTask photoTask) {
String range = formatMinMaxRange(photoTask.getMinPhotoCount(), photoTask.getMaxPhotoCount());
return range.isBlank() ? task.getDisplayName() : task.getDisplayName() + " " + range;
}
if (task instanceof BarcodeTask barcodeTask) {
String range = formatMinMaxRange(barcodeTask.getMinBarcodeCount(), barcodeTask.getMaxBarcodeCount());
return range.isBlank() ? task.getDisplayName() : task.getDisplayName() + " " + range;
}
if (task instanceof CommentTask commentTask) {
String commentText = trimToNull(commentTask.getCommentText());
if (commentText != null) {
return task.getDisplayName() + " \"" + commentText + "\"";
}
return commentTask.isRequired() ? task.getDisplayName() + " (" + getTranslation("common.required") + ")"
: task.getDisplayName();
}
if (task instanceof SignatureTask) {
return task.getDisplayName();
}
String description = trimToNull(task.getDescription());
return description != null ? task.getDisplayName() + " \"" + description + "\"" : task.getDisplayName();
}
private String formatMinMaxRange(Integer minValue, Integer maxValue) {
if (minValue != null && maxValue != null) {
return minValue.equals(maxValue) ? String.valueOf(minValue) : minValue + "-" + maxValue;
}
if (minValue != null) {
return String.valueOf(minValue);
}
if (maxValue != null) {
return String.valueOf(maxValue);
}
return "";
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
private String buildDisplayName(String salutation, String firstName, String lastName) { private String buildDisplayName(String salutation, String firstName, String lastName) {
List<String> parts = new ArrayList<>(); List<String> parts = new ArrayList<>();
if (salutation != null && !salutation.isBlank()) { if (salutation != null && !salutation.isBlank()) {
@@ -432,6 +631,10 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
return stationTasks; return stationTasks;
} }
private boolean areAllTasksCompleted(List<BaseTask> tasks) {
return tasks != null && !tasks.isEmpty() && tasks.stream().allMatch(task -> task != null && task.isCompleted());
}
private void showStationTasksDialog(String stationTitle, List<BaseTask> tasks) { private void showStationTasksDialog(String stationTitle, List<BaseTask> tasks) {
Dialog dialog = new Dialog(); Dialog dialog = new Dialog();
dialog.setWidth("720px"); dialog.setWidth("720px");

View File

@@ -0,0 +1,44 @@
package de.assecutor.votianlt.service;
import com.vaadin.flow.shared.Registration;
import lombok.extern.slf4j.Slf4j;
import org.bson.types.ObjectId;
import org.springframework.stereotype.Service;
import java.util.LinkedHashSet;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
@Service
@Slf4j
public class JobUpdateBroadcaster {
private final Executor executor = Executors.newSingleThreadExecutor();
private final LinkedHashSet<Consumer<ObjectId>> listeners = new LinkedHashSet<>();
public synchronized Registration register(Consumer<ObjectId> listener) {
listeners.add(listener);
return () -> {
synchronized (JobUpdateBroadcaster.this) {
listeners.remove(listener);
}
};
}
public synchronized void broadcast(ObjectId jobId) {
if (jobId == null) {
return;
}
for (Consumer<ObjectId> listener : listeners) {
executor.execute(() -> {
try {
listener.accept(jobId);
} catch (Exception e) {
log.error("Error broadcasting job update for {}", jobId.toHexString(), e);
}
});
}
}
}