From ba99bb29c6f5aed7d8a093b58cde882e0d0a9585 Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Tue, 10 Mar 2026 10:54:28 +0100 Subject: [PATCH] refactor: assign tasks to delivery stations --- .../votianlt/config/MongoConfig.java | 20 ++- .../controller/MessageController.java | 16 +- .../votianlt/model/DeliveryStation.java | 19 +++ .../assecutor/votianlt/model/TaskEntry.java | 16 +- .../votianlt/model/task/BaseTask.java | 15 +- .../pages/base/ui/view/MainLayout.java | 4 +- .../votianlt/pages/service/AddJobService.java | 72 +++++++-- .../votianlt/pages/view/InvoicesView.java | 140 ++++++++++-------- .../votianlt/pages/view/JobSummaryView.java | 93 ++++++++---- .../votianlt/repository/JobRepository.java | 3 + .../votianlt/repository/TaskRepository.java | 4 + .../votianlt/service/EmailService.java | 7 +- .../service/TaskAssignmentService.java | 119 +++++++++++++++ src/main/resources/messages.properties | 9 ++ src/main/resources/messages_en.properties | 9 ++ 15 files changed, 422 insertions(+), 124 deletions(-) create mode 100644 src/main/java/de/assecutor/votianlt/service/TaskAssignmentService.java diff --git a/src/main/java/de/assecutor/votianlt/config/MongoConfig.java b/src/main/java/de/assecutor/votianlt/config/MongoConfig.java index 90f0940..c623988 100644 --- a/src/main/java/de/assecutor/votianlt/config/MongoConfig.java +++ b/src/main/java/de/assecutor/votianlt/config/MongoConfig.java @@ -14,6 +14,7 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; +import org.bson.types.ObjectId; @Configuration public class MongoConfig { @@ -118,8 +119,10 @@ public class MongoConfig { if (source.containsKey("_id")) { task.setId(source.getObjectId("_id")); } - if (source.containsKey("job_id")) { - task.setJobId(source.getObjectId("job_id")); + task.setStationId(readObjectId(source, "station_id")); + task.setJobId(readObjectId(source, "job_id")); + if (source.containsKey("station_order")) { + task.setStationOrder(source.getInteger("station_order")); } if (source.containsKey("task_order")) { task.setTaskOrder(source.getInteger("task_order", 0)); @@ -150,6 +153,17 @@ public class MongoConfig { return task; } + private ObjectId readObjectId(Document source, String key) { + Object value = source.get(key); + if (value instanceof ObjectId objectId) { + return objectId; + } + if (value instanceof String stringValue && ObjectId.isValid(stringValue)) { + return new ObjectId(stringValue); + } + return null; + } + private String mapTaskTypeToClassName(String taskType) { if (taskType == null) { return "de.assecutor.votianlt.model.task.ConfirmationTask"; @@ -172,4 +186,4 @@ public class MongoConfig { } } } -} \ No newline at end of file +} diff --git a/src/main/java/de/assecutor/votianlt/controller/MessageController.java b/src/main/java/de/assecutor/votianlt/controller/MessageController.java index 512cf0b..ba30bf1 100644 --- a/src/main/java/de/assecutor/votianlt/controller/MessageController.java +++ b/src/main/java/de/assecutor/votianlt/controller/MessageController.java @@ -25,6 +25,7 @@ import de.assecutor.votianlt.service.JobHistoryService; import de.assecutor.votianlt.service.JobUpdateBroadcaster; import de.assecutor.votianlt.service.EmailService; import de.assecutor.votianlt.service.MessageService; +import de.assecutor.votianlt.service.TaskAssignmentService; import de.assecutor.votianlt.model.JobStatus; import lombok.extern.slf4j.Slf4j; import de.assecutor.votianlt.messaging.MessagingPublisher; @@ -63,13 +64,14 @@ public class MessageController { private final JobUpdateBroadcaster jobUpdateBroadcaster; private final EmailService emailService; private final MessageService messageService; + private final TaskAssignmentService taskAssignmentService; public MessageController(MessagingPublisher messagingPublisher, AppUserRepository appUserRepository, AppUserService appUserService, JobRepository jobRepository, CargoItemRepository cargoItemRepository, TaskRepository taskRepository, PhotoRepository photoRepository, BarcodeRepository barcodeRepository, SignatureRepository signatureRepository, CommentRepository commentRepository, JobHistoryService jobHistoryService, JobUpdateBroadcaster jobUpdateBroadcaster, EmailService emailService, - MessageService messageService) { + MessageService messageService, TaskAssignmentService taskAssignmentService) { this.messagingPublisher = messagingPublisher; this.appUserRepository = appUserRepository; this.appUserService = appUserService; @@ -84,6 +86,7 @@ public class MessageController { this.jobUpdateBroadcaster = jobUpdateBroadcaster; this.emailService = emailService; this.messageService = messageService; + this.taskAssignmentService = taskAssignmentService; } /** @@ -129,7 +132,7 @@ public class MessageController { List jobsWithRelatedData = assignedJobs.stream().map(job -> { List cargoItems = cargoItemRepository.findByJobId(job.getId()); - List tasks = taskRepository.findByJobIdOrderByTaskOrderAsc(job.getId()); + List tasks = taskAssignmentService.findTasksForJob(job); return new JobWithRelatedDataDTO(job, cargoItems, tasks); }).toList(); @@ -349,7 +352,12 @@ public class MessageController { task.setCompleted(true); task.setCompletedAt(LocalDateTime.now()); taskRepository.save(task); - ObjectId jobId = new ObjectId(task.getJobIdAsString()); + Optional jobOpt = taskAssignmentService.findJobForTask(task); + if (jobOpt.isEmpty()) { + log.warn("[TASK] Could not resolve job for task {}", taskIdStr); + return; + } + ObjectId jobId = jobOpt.get().getId(); // Log detailed task completion in job history try { @@ -380,7 +388,7 @@ public class MessageController { private void checkAndHandleJobCompletion(ObjectId jobId, String completedBy) { try { - var allTasks = taskRepository.findByJobIdOrderByTaskOrderAsc(jobId); + var allTasks = taskAssignmentService.findTasksForJob(jobId); if (allTasks.isEmpty()) { return; } diff --git a/src/main/java/de/assecutor/votianlt/model/DeliveryStation.java b/src/main/java/de/assecutor/votianlt/model/DeliveryStation.java index 0ee6cb5..a9641cb 100644 --- a/src/main/java/de/assecutor/votianlt/model/DeliveryStation.java +++ b/src/main/java/de/assecutor/votianlt/model/DeliveryStation.java @@ -1,9 +1,12 @@ package de.assecutor.votianlt.model; +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; import de.assecutor.votianlt.model.task.BaseTask; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.bson.types.ObjectId; import org.springframework.data.mongodb.core.mapping.Field; import java.time.LocalDate; @@ -21,6 +24,10 @@ import java.util.List; @AllArgsConstructor public class DeliveryStation { + @Field("station_id") + @JsonIgnore + private ObjectId stationId; + @Field("station_order") private int stationOrder; @@ -62,4 +69,16 @@ public class DeliveryStation { @Field("tasks") private List tasks = new ArrayList<>(); + + @JsonGetter("stationId") + public String getStationIdAsString() { + return stationId != null ? stationId.toHexString() : null; + } + + public ObjectId ensureStationId() { + if (stationId == null) { + stationId = new ObjectId(); + } + return stationId; + } } diff --git a/src/main/java/de/assecutor/votianlt/model/TaskEntry.java b/src/main/java/de/assecutor/votianlt/model/TaskEntry.java index f67ddf4..fb1a4bd 100644 --- a/src/main/java/de/assecutor/votianlt/model/TaskEntry.java +++ b/src/main/java/de/assecutor/votianlt/model/TaskEntry.java @@ -23,6 +23,10 @@ public class TaskEntry { @JsonIgnore private ObjectId id; + @Field("station_id") + @JsonIgnore + private ObjectId stationId; + @Field("job_id") @JsonIgnore private ObjectId jobId; @@ -54,10 +58,16 @@ public class TaskEntry { } /** - * Returns the job ObjectId as string for JSON serialization. This ensures that - * the job id is returned as a string instead of ObjectId object. + * Returns the station ObjectId as string for JSON serialization. + */ + @JsonGetter("stationId") + public String getStationIdAsString() { + return stationId != null ? stationId.toHexString() : null; + } + + /** + * Returns the legacy job ObjectId as string for internal fallback handling. */ - @JsonGetter("jobId") public String getJobIdAsString() { return jobId != null ? jobId.toString() : null; } diff --git a/src/main/java/de/assecutor/votianlt/model/task/BaseTask.java b/src/main/java/de/assecutor/votianlt/model/task/BaseTask.java index 4123fd9..bb217e4 100644 --- a/src/main/java/de/assecutor/votianlt/model/task/BaseTask.java +++ b/src/main/java/de/assecutor/votianlt/model/task/BaseTask.java @@ -28,6 +28,10 @@ public abstract class BaseTask { @JsonIgnore private ObjectId id; + @Field("station_id") + @JsonIgnore + private ObjectId stationId; + @Field("job_id") @JsonIgnore private ObjectId jobId; @@ -62,9 +66,16 @@ public abstract class BaseTask { } /** - * Returns the job ObjectId as string for JSON serialization. + * Returns the station ObjectId as string for JSON serialization. + */ + @JsonGetter("stationId") + public String getStationIdAsString() { + return stationId != null ? stationId.toHexString() : null; + } + + /** + * Returns the legacy job ObjectId as string for internal fallback handling. */ - @JsonGetter("jobId") public String getJobIdAsString() { return jobId != null ? jobId.toString() : null; } diff --git a/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java b/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java index ee4d14b..a9b5d75 100644 --- a/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java +++ b/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java @@ -129,6 +129,8 @@ public final class MainLayout extends AppLayout { // Add children to "Verwaltung" treeData.addItem(verwaltungItem, new MenuTreeItem(getTranslation("nav.jobs"), "jobs", VaadinIcon.CLIPBOARD_TEXT)); + treeData.addItem(verwaltungItem, + new MenuTreeItem(getTranslation("nav.invoices"), "invoices", VaadinIcon.FILE_TEXT)); treeData.addItem(verwaltungItem, new MenuTreeItem(getTranslation("nav.customers"), "customers", VaadinIcon.USERS)); treeData.addItem(verwaltungItem, @@ -139,8 +141,6 @@ public final class MainLayout extends AppLayout { // Add children to "Benutzer" treeData.addItem(benutzerItem, new MenuTreeItem(getTranslation("nav.profile"), "edit-profile", VaadinIcon.USER)); - treeData.addItem(benutzerItem, - new MenuTreeItem(getTranslation("nav.myinvoices"), "my-invoices", VaadinIcon.FILE_TEXT)); treeData.addItem(benutzerItem, new MenuTreeItem(getTranslation("nav.imprint"), "impressum", VaadinIcon.INFO_CIRCLE)); diff --git a/src/main/java/de/assecutor/votianlt/pages/service/AddJobService.java b/src/main/java/de/assecutor/votianlt/pages/service/AddJobService.java index 8af1f3b..3f43c36 100644 --- a/src/main/java/de/assecutor/votianlt/pages/service/AddJobService.java +++ b/src/main/java/de/assecutor/votianlt/pages/service/AddJobService.java @@ -14,6 +14,7 @@ import de.assecutor.votianlt.repository.CargoItemRepository; import de.assecutor.votianlt.service.ClientConnectionService; import de.assecutor.votianlt.service.JobHistoryService; import de.assecutor.votianlt.service.EmailService; +import de.assecutor.votianlt.service.TaskAssignmentService; import java.util.Objects; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -42,6 +43,7 @@ public class AddJobService { private final EmailService emailService; private final ClientConnectionService clientConnectionService; private final MessagingPublisher messagingPublisher; + private final TaskAssignmentService taskAssignmentService; /** * Speichert einen neuen Auftrag samt CargoItems und Tasks @@ -65,6 +67,8 @@ public class AddJobService { job.setJobNumber(generateJobNumber()); } + ensureDeliveryStationIds(job); + // Auftrag speichern Job savedJob = jobRepository.save(job); final ObjectId jobId = savedJob.getId(); @@ -91,15 +95,24 @@ public class AddJobService { // Tasks separat speichern und referenzieren mit korrekter Nummerierung if (transientTasks != null && !transientTasks.isEmpty()) { var filteredTasks = transientTasks.stream().filter(Objects::nonNull) - .filter(task -> task.getTaskType() != null) // Filter nach TaskType statt Text - .toList(); + .filter(task -> task.getTaskType() != null).toList(); Map taskOrderByStation = new HashMap<>(); + Map stationIdByOrder = buildStationIdByOrder(savedJob); + List tasksToPersist = new ArrayList<>(); - // Setze JobId und stelle sicher, dass taskOrder je Lieferstation korrekt ist + // Setze stationId und stelle sicher, dass taskOrder je Lieferstation korrekt ist for (BaseTask task : filteredTasks) { - task.setJobId(jobId); int stationOrder = task.getStationOrder() != null ? task.getStationOrder() : 0; + ObjectId stationId = task.getStationId() != null ? task.getStationId() : stationIdByOrder.get(stationOrder); + if (stationId == null) { + log.warn("Skipping task without resolvable stationId for job {} and stationOrder {}", jobId, + stationOrder); + continue; + } + + task.setStationId(stationId); + task.setJobId(null); if (task.getTaskOrder() == null) { int nextTaskOrder = taskOrderByStation.getOrDefault(stationOrder, 0); task.setTaskOrder(nextTaskOrder); @@ -109,12 +122,13 @@ public class AddJobService { task.getTaskOrder() + 1); taskOrderByStation.put(stationOrder, nextTaskOrder); } + tasksToPersist.add(task); } - taskRepository.saveAll(filteredTasks); - attachTasksToDeliveryStations(savedJob, filteredTasks); + taskRepository.saveAll(tasksToPersist); + attachTasksToDeliveryStations(savedJob, tasksToPersist); savedJob = jobRepository.save(savedJob); - log.info("Saved {} tasks for job {} with ordering", filteredTasks.size(), jobId); + log.info("Saved {} tasks for job {} with station-based assignment", tasksToPersist.size(), jobId); } else if (savedJob.getDeliveryStations() != null && !savedJob.getDeliveryStations().isEmpty()) { attachTasksToDeliveryStations(savedJob, List.of()); savedJob = jobRepository.save(savedJob); @@ -220,7 +234,7 @@ public class AddJobService { try { // Lade CargoItems und Tasks für den Job List cargoItems = cargoItemRepository.findByJobId(job.getId()); - List tasks = taskRepository.findByJobIdOrderByTaskOrderAsc(job.getId()); + List tasks = taskAssignmentService.findTasksForJob(job); // Erstelle DTO mit allen Daten JobWithRelatedDataDTO jobData = new JobWithRelatedDataDTO(job, cargoItems, tasks); @@ -238,20 +252,54 @@ public class AddJobService { return; } - Map> tasksByStation = new HashMap<>(); + Map> tasksByStationId = new HashMap<>(); + Map> legacyTasksByStationOrder = new HashMap<>(); for (BaseTask task : tasks) { if (task == null) { continue; } + if (task.getStationId() != null) { + tasksByStationId.computeIfAbsent(task.getStationId().toHexString(), ignored -> new ArrayList<>()).add(task); + continue; + } + int stationOrder = task.getStationOrder() != null ? task.getStationOrder() : 0; - tasksByStation.computeIfAbsent(stationOrder, ignored -> new ArrayList<>()).add(task); + legacyTasksByStationOrder.computeIfAbsent(stationOrder, ignored -> new ArrayList<>()).add(task); } for (DeliveryStation station : job.getDeliveryStations()) { - int stationOrder = station.getStationOrder(); - List stationTasks = new ArrayList<>(tasksByStation.getOrDefault(stationOrder, List.of())); + String stationKey = station.getStationId() != null ? station.getStationId().toHexString() : null; + List stationTasks = stationKey != null + ? new ArrayList<>(tasksByStationId.getOrDefault(stationKey, List.of())) + : new ArrayList<>(legacyTasksByStationOrder.getOrDefault(station.getStationOrder(), List.of())); stationTasks.sort(Comparator.comparing(task -> task.getTaskOrder() != null ? task.getTaskOrder() : 0)); station.setTasks(stationTasks); } } + + private void ensureDeliveryStationIds(Job job) { + if (job.getDeliveryStations() == null) { + return; + } + + for (DeliveryStation station : job.getDeliveryStations()) { + if (station != null) { + station.ensureStationId(); + } + } + } + + private Map buildStationIdByOrder(Job job) { + Map stationIdByOrder = new HashMap<>(); + if (job.getDeliveryStations() == null) { + return stationIdByOrder; + } + + for (DeliveryStation station : job.getDeliveryStations()) { + if (station != null && station.getStationId() != null) { + stationIdByOrder.put(station.getStationOrder(), station.getStationId()); + } + } + return stationIdByOrder; + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/InvoicesView.java b/src/main/java/de/assecutor/votianlt/pages/view/InvoicesView.java index 91afc99..405bdf1 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/InvoicesView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/InvoicesView.java @@ -2,25 +2,22 @@ package de.assecutor.votianlt.pages.view; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.orderedlayout.FlexComponent; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.router.HasDynamicTitle; import com.vaadin.flow.router.Route; import com.vaadin.flow.component.UI; -import de.assecutor.votianlt.model.invoices.SystemInvoice; -import de.assecutor.votianlt.model.invoices.SystemInvoiceData; -import de.assecutor.votianlt.model.invoices.SystemInvoiceItem; -import de.assecutor.votianlt.service.SystemInvoiceService; -import de.assecutor.votianlt.util.DateTimeFormatUtil; +import de.assecutor.votianlt.model.invoices.CustomerInvoice; +import de.assecutor.votianlt.repository.CustomerInvoiceRepository; +import de.assecutor.votianlt.security.SecurityService; import jakarta.annotation.security.RolesAllowed; import java.io.ByteArrayInputStream; -import java.text.NumberFormat; -import java.time.LocalDate; -import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.Optional; import com.vaadin.flow.server.StreamResource; import com.vaadin.flow.server.StreamRegistration; @@ -29,12 +26,13 @@ import com.vaadin.flow.server.StreamRegistration; @RolesAllowed({ "USER", "ADMIN" }) public class InvoicesView extends VerticalLayout implements HasDynamicTitle { - private final Grid invoiceGrid; + private final Grid invoiceGrid; + private final CustomerInvoiceRepository customerInvoiceRepository; + private final SecurityService securityService; - private final SystemInvoiceService systemInvoiceService; - - public InvoicesView(SystemInvoiceService systemInvoiceService) { - this.systemInvoiceService = systemInvoiceService; + public InvoicesView(CustomerInvoiceRepository customerInvoiceRepository, SecurityService securityService) { + this.customerInvoiceRepository = customerInvoiceRepository; + this.securityService = securityService; setSizeFull(); setPadding(true); @@ -45,49 +43,73 @@ public class InvoicesView extends VerticalLayout implements HasDynamicTitle { H2 title = new H2(getTranslation("invoices.title")); add(title); - invoiceGrid = new Grid<>(SystemInvoice.class, false); - invoiceGrid.addColumn(SystemInvoice::getId).setHeader(getTranslation("invoices.column.number")) + invoiceGrid = new Grid<>(CustomerInvoice.class, false); + invoiceGrid.setWidthFull(); + invoiceGrid.addColumn(invoice -> firstNonBlank(invoice.getInvoiceNumber(), invoice.getId())) + .setHeader(getTranslation("invoices.column.number")) .setAutoWidth(true); - invoiceGrid.addColumn(SystemInvoice::getKunde).setHeader(getTranslation("invoices.column.customer")) + invoiceGrid.addColumn(this::getRecipientLabel).setHeader(getTranslation("invoices.column.customer")) .setAutoWidth(true); - invoiceGrid.addColumn(SystemInvoice::getDatum).setHeader(getTranslation("invoices.column.date")) + invoiceGrid.addColumn(invoice -> Optional.ofNullable(invoice.getInvoiceDate()).map(Object::toString).orElse("")) + .setHeader(getTranslation("invoices.column.date")) .setAutoWidth(true); - invoiceGrid.addColumn(SystemInvoice::getBetrag).setHeader(getTranslation("invoices.column.amount")) + invoiceGrid.addColumn(this::formatAmount).setHeader(getTranslation("invoices.column.amount")) .setAutoWidth(true); - invoiceGrid.addColumn(SystemInvoice::getBeschreibung).setHeader(getTranslation("invoices.column.description")) + invoiceGrid.addColumn(invoice -> firstNonBlank(invoice.getDescription(), "")) + .setHeader(getTranslation("invoices.column.description")) .setAutoWidth(true); invoiceGrid.setSelectionMode(Grid.SelectionMode.SINGLE); invoiceGrid.getStyle().set("cursor", "pointer"); - // Testdaten - List testSystemInvoices = List.of( - new SystemInvoice("R-2024-001", "Max Mustermann", LocalDate.now().minusDays(2), 199.99, - "Transport Hamburg-Berlin"), - new SystemInvoice("R-2024-002", "Erika Musterfrau", LocalDate.now().minusDays(1), 299.49, - "Express München-Köln"), - new SystemInvoice("R-2024-003", "Hans Beispiel", LocalDate.now(), 149.00, "Standard Leipzig-Dresden")); - invoiceGrid.setItems(testSystemInvoices); - invoiceGrid.addItemClickListener(event -> { - SystemInvoice systemInvoice = event.getItem(); - if (systemInvoice != null) { - downloadInvoicePdf(systemInvoice); + CustomerInvoice invoice = event.getItem(); + if (invoice != null) { + downloadInvoicePdf(invoice); } }); + loadInvoices(); add(invoiceGrid); } - private void downloadInvoicePdf(SystemInvoice systemInvoice) { + private void loadInvoices() { + String currentUserId = securityService.getCurrentUserId().toHexString(); + List invoices = customerInvoiceRepository.findByUserId(currentUserId).stream() + .sorted((left, right) -> { + if (left.getInvoiceDate() == null && right.getInvoiceDate() == null) { + return 0; + } + if (left.getInvoiceDate() == null) { + return 1; + } + if (right.getInvoiceDate() == null) { + return -1; + } + return right.getInvoiceDate().compareTo(left.getInvoiceDate()); + }) + .toList(); + invoiceGrid.setItems(invoices); + + if (invoices.isEmpty()) { + Span emptyState = new Span(getTranslation("invoices.empty")); + emptyState.getStyle().set("color", "var(--lumo-secondary-text-color)"); + add(emptyState); + } + } + + private void downloadInvoicePdf(CustomerInvoice invoice) { try { - // PDF generieren mit SystemInvoice (HTML Template) - byte[] pdfBytes = generateSystemInvoicePdf(systemInvoice); - StreamResource resource = new StreamResource(systemInvoice.getId() + ".pdf", - () -> new ByteArrayInputStream(pdfBytes)); + if (invoice.getPdfData() == null || invoice.getPdfData().length == 0) { + Notification.show(getTranslation("invoices.notification.pdf.missing"), 4000, + Notification.Position.MIDDLE); + return; + } + + StreamResource resource = new StreamResource(firstNonBlank(invoice.getInvoiceNumber(), invoice.getId()) + ".pdf", + () -> new ByteArrayInputStream(invoice.getPdfData())); resource.setContentType("application/pdf"); resource.setCacheTime(0); - // Direkter Download über UI StreamRegistration registration = UI.getCurrent().getSession().getResourceRegistry() .registerResource(resource); UI.getCurrent().getPage().open(registration.getResourceUri().toString()); @@ -98,35 +120,25 @@ public class InvoicesView extends VerticalLayout implements HasDynamicTitle { } } - private byte[] generateSystemInvoicePdf(SystemInvoice systemInvoice) throws Exception { - NumberFormat CURRENCY_FMT = NumberFormat.getCurrencyInstance(Locale.GERMANY); + private String getRecipientLabel(CustomerInvoice invoice) { + return firstNonBlank(invoice.getRecipientCompany(), invoice.getRecipientName(), ""); + } - SystemInvoiceData data = new SystemInvoiceData(); - data.setInvoiceNumber(systemInvoice.getId()); - data.setInvoiceDate(DateTimeFormatUtil.formatDate(systemInvoice.getDatum())); - data.setInvoiceText(systemInvoice.getBeschreibung()); + private String formatAmount(CustomerInvoice invoice) { + var amount = invoice.getTotalAmount() != null ? invoice.getTotalAmount() : invoice.getNetAmount(); + if (amount == null) { + return ""; + } + return java.text.NumberFormat.getCurrencyInstance(Locale.GERMANY).format(amount); + } - // Empfänger aus der Zeile (nur Name in den Testdaten vorhanden) - data.setRecipientName(systemInvoice.getKunde()); - data.setRecipientDepartment(""); - data.setRecipientStreet(""); - data.setRecipientCity(""); - - // Eine Position mit dem Betrag/Beschreibung - List items = new ArrayList<>(); - String netStr = CURRENCY_FMT.format(systemInvoice.getBetrag()); - items.add(new SystemInvoiceItem("1", systemInvoice.getBeschreibung(), netStr, netStr)); - data.setInvoiceItems(items); - - // Summen berechnen (Betrag als Nettobetrag interpretieren) - double net = systemInvoice.getBetrag(); - double vat = Math.round(net * 0.19 * 100.0) / 100.0; - double total = net + vat; - data.setNetAmount(CURRENCY_FMT.format(net)); - data.setVatAmount(CURRENCY_FMT.format(vat)); - data.setTotalAmount(CURRENCY_FMT.format(total)); - - return systemInvoiceService.generateInvoicePdfFromHtml(data); + private String firstNonBlank(String... values) { + for (String value : values) { + if (value != null && !value.isBlank()) { + return value; + } + } + return ""; } @Override diff --git a/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java b/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java index 9b177a8..a96bb5c 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java @@ -41,7 +41,6 @@ import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar; import lombok.extern.slf4j.Slf4j; import de.assecutor.votianlt.repository.CargoItemRepository; import de.assecutor.votianlt.repository.JobRepository; -import de.assecutor.votianlt.repository.TaskRepository; import de.assecutor.votianlt.repository.SignatureRepository; import de.assecutor.votianlt.repository.BarcodeRepository; import de.assecutor.votianlt.repository.PhotoRepository; @@ -58,6 +57,7 @@ import de.assecutor.votianlt.service.JobHistoryService; import de.assecutor.votianlt.service.JobUpdateBroadcaster; import de.assecutor.votianlt.service.LocationService; import de.assecutor.votianlt.service.MessageService; +import de.assecutor.votianlt.service.TaskAssignmentService; import de.assecutor.votianlt.util.DateTimeFormatUtil; import com.vaadin.flow.component.confirmdialog.ConfirmDialog; import jakarta.annotation.security.RolesAllowed; @@ -78,7 +78,6 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has private final JobRepository jobRepository; private final CargoItemRepository cargoItemRepository; - private final TaskRepository taskRepository; private final SignatureRepository signatureRepository; private final BarcodeRepository barcodeRepository; private final PhotoRepository photoRepository; @@ -88,6 +87,7 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has private final JobUpdateBroadcaster jobUpdateBroadcaster; private final LocationService locationService; private final ServiceRepository serviceRepository; + private final TaskAssignmentService taskAssignmentService; @Value("${app.google.maps.api-key}") private String googleMapsApiKey; @@ -96,16 +96,16 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has private final List
taskCards = new ArrayList<>(); private Registration jobUpdateRegistration; private ObjectId currentJobId; + private Div stationTilesSection; public JobSummaryView(JobRepository jobRepository, CargoItemRepository cargoItemRepository, - TaskRepository taskRepository, SignatureRepository signatureRepository, BarcodeRepository barcodeRepository, + SignatureRepository signatureRepository, BarcodeRepository barcodeRepository, PhotoRepository photoRepository, CommentRepository commentRepository, AppUserService appUserService, MessageService messageService, JobHistoryService jobHistoryService, JobUpdateBroadcaster jobUpdateBroadcaster, LocationService locationService, - ServiceRepository serviceRepository) { + ServiceRepository serviceRepository, TaskAssignmentService taskAssignmentService) { this.jobRepository = jobRepository; this.cargoItemRepository = cargoItemRepository; - this.taskRepository = taskRepository; this.signatureRepository = signatureRepository; this.barcodeRepository = barcodeRepository; this.photoRepository = photoRepository; @@ -115,6 +115,7 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has this.jobUpdateBroadcaster = jobUpdateBroadcaster; this.locationService = locationService; this.serviceRepository = serviceRepository; + this.taskAssignmentService = taskAssignmentService; setSizeFull(); addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN, @@ -159,7 +160,7 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has if (currentJobId == null || jobId == null || !currentJobId.equals(jobId)) { return; } - ui.access(this::refreshCurrentJobSummary); + ui.access(this::refreshStationTilesOnly); }); } @@ -220,16 +221,35 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has add(new ViewToolbar("Zusammenfassung", sendMessageButton, jobHistoryButton)); List cargo = cargoItemRepository.findByJobId(currentJobId); - List tasks = taskRepository.findByJobIdOrderByTaskOrderAsc(currentJobId); + List tasks = taskAssignmentService.findTasksForJob(job); render(job, cargo, tasks); add(content); } + private void refreshStationTilesOnly() { + if (currentJobId == null || stationTilesSection == null) { + return; + } + + Job job = jobRepository.findById(currentJobId).orElse(null); + if (job == null) { + return; + } + + List cargo = cargoItemRepository.findByJobId(currentJobId); + List tasks = taskAssignmentService.findTasksForJob(job); + + Div updatedSection = createStationTilesSection(job, cargo, tasks); + content.replace(stationTilesSection, updatedSection); + stationTilesSection = updatedSection; + } + private void render(Job job, List cargoItems, List tasks) { content.removeAll(); - content.add(createStationTilesSection(job, cargoItems, tasks)); + stationTilesSection = createStationTilesSection(job, cargoItems, tasks); + content.add(stationTilesSection); // Fracht und weitere Infos HorizontalLayout midRow = new HorizontalLayout(); @@ -600,35 +620,46 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has } private List getTasksForStation(DeliveryStation station, List tasks, boolean legacyMode) { - if (!legacyMode && station != null && station.getTasks() != null && !station.getTasks().isEmpty()) { - return station.getTasks().stream() - .filter(task -> task != null && task.getDisplayName() != null && !task.getDisplayName().isBlank()) - .sorted((left, right) -> Integer.compare(left.getTaskOrder() != null ? left.getTaskOrder() : 0, - right.getTaskOrder() != null ? right.getTaskOrder() : 0)) - .toList(); - } - - if (tasks == null || tasks.isEmpty()) { - return List.of(); - } - - List stationTasks = new ArrayList<>(); int stationOrder = station != null ? station.getStationOrder() : 0; - for (BaseTask task : tasks) { - if (task == null) { - continue; - } + String stationId = station != null ? station.getStationIdAsString() : null; - Integer taskStationOrder = task.getStationOrder(); - if (legacyMode) { - if (taskStationOrder == null || taskStationOrder == stationOrder) { + if (tasks != null && !tasks.isEmpty()) { + List stationTasks = new ArrayList<>(); + for (BaseTask task : tasks) { + if (task == null) { + continue; + } + + String taskStationId = task.getStationIdAsString(); + Integer taskStationOrder = task.getStationOrder(); + if (stationId != null && stationId.equals(taskStationId)) { + stationTasks.add(task); + } else if (legacyMode) { + if (taskStationOrder == null || taskStationOrder == stationOrder) { + stationTasks.add(task); + } + } else if (taskStationOrder != null && taskStationOrder == stationOrder) { stationTasks.add(task); } - } else if (taskStationOrder != null && taskStationOrder == stationOrder) { - stationTasks.add(task); + } + if (!stationTasks.isEmpty()) { + return sortVisibleTasks(stationTasks); } } - return stationTasks; + + if (!legacyMode && station != null && station.getTasks() != null && !station.getTasks().isEmpty()) { + return sortVisibleTasks(station.getTasks()); + } + + return List.of(); + } + + private List sortVisibleTasks(List tasks) { + return tasks.stream() + .filter(task -> task != null && task.getDisplayName() != null && !task.getDisplayName().isBlank()) + .sorted((left, right) -> Integer.compare(left.getTaskOrder() != null ? left.getTaskOrder() : 0, + right.getTaskOrder() != null ? right.getTaskOrder() : 0)) + .toList(); } private boolean areAllTasksCompleted(List tasks) { diff --git a/src/main/java/de/assecutor/votianlt/repository/JobRepository.java b/src/main/java/de/assecutor/votianlt/repository/JobRepository.java index 38dca41..a25dfdf 100644 --- a/src/main/java/de/assecutor/votianlt/repository/JobRepository.java +++ b/src/main/java/de/assecutor/votianlt/repository/JobRepository.java @@ -100,6 +100,9 @@ public interface JobRepository extends MongoRepository { @Query("{'app_user': ?0, 'status': {'$nin': ['COMPLETED', 'CANCELLED']}}") List findByAppUser(String appUser); + @Query("{'delivery_stations.station_id': ?0}") + Optional findByDeliveryStationsStationId(ObjectId stationId); + /** * Findet Aufträge anhand einer partiellen Auftragsnummer (case-insensitive) */ diff --git a/src/main/java/de/assecutor/votianlt/repository/TaskRepository.java b/src/main/java/de/assecutor/votianlt/repository/TaskRepository.java index 5745f25..bcc42fd 100644 --- a/src/main/java/de/assecutor/votianlt/repository/TaskRepository.java +++ b/src/main/java/de/assecutor/votianlt/repository/TaskRepository.java @@ -7,6 +7,10 @@ import org.springframework.data.mongodb.repository.MongoRepository; import java.util.List; public interface TaskRepository extends MongoRepository { + List findByStationIdOrderByTaskOrderAsc(ObjectId stationId); + + List findByStationIdIn(List stationIds); + List findByJobIdOrderByTaskOrderAsc(ObjectId jobId); List findByJobIdAndStationOrderOrderByTaskOrderAsc(ObjectId jobId, int stationOrder); diff --git a/src/main/java/de/assecutor/votianlt/service/EmailService.java b/src/main/java/de/assecutor/votianlt/service/EmailService.java index 475afdd..0f8c89b 100644 --- a/src/main/java/de/assecutor/votianlt/service/EmailService.java +++ b/src/main/java/de/assecutor/votianlt/service/EmailService.java @@ -23,6 +23,7 @@ public class EmailService { private final UserRepository userRepository; private final JobRepository jobRepository; private final TaskRepository taskRepository; + private final TaskAssignmentService taskAssignmentService; private final JavaMailSender mailSender; @Value("${spring.mail.username}") @@ -194,7 +195,7 @@ public class EmailService { String appUserName = buildAppUserName(user); // Count completed tasks - var allTasks = taskRepository.findByJobIdOrderByTaskOrderAsc(job.getId()); + var allTasks = taskAssignmentService.findTasksForJob(job); int taskCount = allTasks.size(); StringBuilder body = new StringBuilder(); @@ -283,7 +284,7 @@ public class EmailService { String fullName = buildFullName(user); // Count tasks for this job - var allTasks = taskRepository.findByJobIdOrderByTaskOrderAsc(job.getId()); + var allTasks = taskAssignmentService.findTasksForJob(job); int taskCount = allTasks.size(); StringBuilder body = new StringBuilder(); @@ -354,4 +355,4 @@ public class EmailService { log.error("Failed to send simple email to {} with subject '{}': {}", to, subject, e.getMessage(), e); } } -} \ No newline at end of file +} diff --git a/src/main/java/de/assecutor/votianlt/service/TaskAssignmentService.java b/src/main/java/de/assecutor/votianlt/service/TaskAssignmentService.java new file mode 100644 index 0000000..ed4fbee --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/service/TaskAssignmentService.java @@ -0,0 +1,119 @@ +package de.assecutor.votianlt.service; + +import de.assecutor.votianlt.model.DeliveryStation; +import de.assecutor.votianlt.model.Job; +import de.assecutor.votianlt.model.task.BaseTask; +import de.assecutor.votianlt.repository.JobRepository; +import de.assecutor.votianlt.repository.TaskRepository; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.bson.types.ObjectId; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class TaskAssignmentService { + + private final TaskRepository taskRepository; + private final JobRepository jobRepository; + + public List findTasksForJob(Job job) { + if (job == null || job.getId() == null) { + return List.of(); + } + + Map uniqueTasks = new LinkedHashMap<>(); + List stationIds = extractStationIds(job); + + if (!stationIds.isEmpty()) { + for (BaseTask task : taskRepository.findByStationIdIn(stationIds)) { + putIfAbsent(uniqueTasks, task); + } + } + + for (BaseTask task : taskRepository.findByJobIdOrderByTaskOrderAsc(job.getId())) { + putIfAbsent(uniqueTasks, task); + } + + return sortTasksForJob(job, new ArrayList<>(uniqueTasks.values())); + } + + public List findTasksForJob(ObjectId jobId) { + if (jobId == null) { + return List.of(); + } + + return jobRepository.findById(jobId).map(this::findTasksForJob) + .orElseGet(() -> sortTasksForJob(null, taskRepository.findByJobIdOrderByTaskOrderAsc(jobId))); + } + + public Optional findJobForTask(BaseTask task) { + if (task == null) { + return Optional.empty(); + } + + if (task.getStationId() != null) { + Optional jobByStation = jobRepository.findByDeliveryStationsStationId(task.getStationId()); + if (jobByStation.isPresent()) { + return jobByStation; + } + } + + return Optional.ofNullable(task.getJobId()).flatMap(jobRepository::findById); + } + + private List extractStationIds(Job job) { + if (job.getDeliveryStations() == null || job.getDeliveryStations().isEmpty()) { + return List.of(); + } + + return job.getDeliveryStations().stream().filter(Objects::nonNull).map(DeliveryStation::getStationId) + .filter(Objects::nonNull).toList(); + } + + private List sortTasksForJob(Job job, List tasks) { + Map stationOrderById = new LinkedHashMap<>(); + if (job != null && job.getDeliveryStations() != null) { + for (DeliveryStation station : job.getDeliveryStations()) { + if (station != null && station.getStationId() != null) { + stationOrderById.put(station.getStationId().toHexString(), station.getStationOrder()); + } + } + } + + return tasks.stream().filter(Objects::nonNull) + .sorted(Comparator.comparingInt(task -> resolveStationOrder(task, stationOrderById)) + .thenComparingInt(task -> task.getTaskOrder() != null ? task.getTaskOrder() : 0)) + .toList(); + } + + private int resolveStationOrder(BaseTask task, Map stationOrderById) { + if (task.getStationId() != null) { + Integer stationOrder = stationOrderById.get(task.getStationId().toHexString()); + if (stationOrder != null) { + return stationOrder; + } + } + return task.getStationOrder() != null ? task.getStationOrder() : Integer.MAX_VALUE; + } + + private void putIfAbsent(Map uniqueTasks, BaseTask task) { + if (task == null) { + return; + } + + String key = task.getId() != null ? task.getId().toHexString() + : String.join(":", Optional.ofNullable(task.getStationIdAsString()).orElse(""), + Optional.ofNullable(task.getJobIdAsString()).orElse(""), + String.valueOf(task.getTaskOrder() != null ? task.getTaskOrder() : 0), + Optional.ofNullable(task.getDescription()).orElse(""), + Optional.ofNullable(task.getTaskType()).orElse("")); + uniqueTasks.putIfAbsent(key, task); + } +} diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index a891a90..7862a92 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -682,6 +682,8 @@ invoices.column.customer=Kunde invoices.column.date=Datum invoices.column.amount=Betrag invoices.column.description=Beschreibung +invoices.empty=Es wurden noch keine Rechnungen erstellt. +invoices.notification.pdf.missing=Für diese Rechnung ist kein PDF gespeichert. # My Invoices myinvoices.title=Rechnungen @@ -906,6 +908,13 @@ jobhistory.status.pickedup=Abgeholt jobhistory.status.intransit=Unterwegs jobhistory.status.delivered=Zugestellt jobhistory.image.alt=Vergrößertes Foto +jobhistory.title=Jobhistorie +jobhistory.header=Jobhistorie für {0} +jobhistory.info.customer=Kunde: {0} +jobhistory.info.createdat=Erstellt am: {0} +jobhistory.info.status=Status: {0} +jobhistory.count={0} Einträge in der Historie +jobhistory.changedby=Geändert von: {0} # Version version.label=Version diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties index 6240a21..33ab4a4 100644 --- a/src/main/resources/messages_en.properties +++ b/src/main/resources/messages_en.properties @@ -682,6 +682,8 @@ invoices.column.customer=Customer invoices.column.date=Date invoices.column.amount=Amount invoices.column.description=Description +invoices.empty=No invoices have been created yet. +invoices.notification.pdf.missing=No PDF is stored for this invoice. # My Invoices myinvoices.title=My Invoices @@ -905,6 +907,13 @@ jobhistory.status.pickedup=Picked Up jobhistory.status.intransit=In Transit jobhistory.status.delivered=Delivered jobhistory.image.alt=Enlarged Photo +jobhistory.title=Job History +jobhistory.header=Job history for {0} +jobhistory.info.customer=Customer: {0} +jobhistory.info.createdat=Created at: {0} +jobhistory.info.status=Status: {0} +jobhistory.count={0} history entries +jobhistory.changedby=Changed by: {0} # Version version.label=Version