refactor: assign tasks to delivery stations

This commit is contained in:
2026-03-10 10:54:28 +01:00
parent c39b4f8b52
commit ba99bb29c6
15 changed files with 422 additions and 124 deletions

View File

@@ -14,6 +14,7 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.bson.types.ObjectId;
@Configuration @Configuration
public class MongoConfig { public class MongoConfig {
@@ -118,8 +119,10 @@ public class MongoConfig {
if (source.containsKey("_id")) { if (source.containsKey("_id")) {
task.setId(source.getObjectId("_id")); task.setId(source.getObjectId("_id"));
} }
if (source.containsKey("job_id")) { task.setStationId(readObjectId(source, "station_id"));
task.setJobId(source.getObjectId("job_id")); task.setJobId(readObjectId(source, "job_id"));
if (source.containsKey("station_order")) {
task.setStationOrder(source.getInteger("station_order"));
} }
if (source.containsKey("task_order")) { if (source.containsKey("task_order")) {
task.setTaskOrder(source.getInteger("task_order", 0)); task.setTaskOrder(source.getInteger("task_order", 0));
@@ -150,6 +153,17 @@ public class MongoConfig {
return task; 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) { private String mapTaskTypeToClassName(String taskType) {
if (taskType == null) { if (taskType == null) {
return "de.assecutor.votianlt.model.task.ConfirmationTask"; return "de.assecutor.votianlt.model.task.ConfirmationTask";
@@ -172,4 +186,4 @@ public class MongoConfig {
} }
} }
} }
} }

View File

@@ -25,6 +25,7 @@ import de.assecutor.votianlt.service.JobHistoryService;
import de.assecutor.votianlt.service.JobUpdateBroadcaster; 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.service.TaskAssignmentService;
import de.assecutor.votianlt.model.JobStatus; import de.assecutor.votianlt.model.JobStatus;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import de.assecutor.votianlt.messaging.MessagingPublisher; import de.assecutor.votianlt.messaging.MessagingPublisher;
@@ -63,13 +64,14 @@ public class MessageController {
private final JobUpdateBroadcaster jobUpdateBroadcaster; private final JobUpdateBroadcaster jobUpdateBroadcaster;
private final EmailService emailService; private final EmailService emailService;
private final MessageService messageService; private final MessageService messageService;
private final TaskAssignmentService taskAssignmentService;
public MessageController(MessagingPublisher messagingPublisher, AppUserRepository appUserRepository, public MessageController(MessagingPublisher messagingPublisher, AppUserRepository appUserRepository,
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, JobUpdateBroadcaster jobUpdateBroadcaster, EmailService emailService, JobHistoryService jobHistoryService, JobUpdateBroadcaster jobUpdateBroadcaster, EmailService emailService,
MessageService messageService) { MessageService messageService, TaskAssignmentService taskAssignmentService) {
this.messagingPublisher = messagingPublisher; this.messagingPublisher = messagingPublisher;
this.appUserRepository = appUserRepository; this.appUserRepository = appUserRepository;
this.appUserService = appUserService; this.appUserService = appUserService;
@@ -84,6 +86,7 @@ public class MessageController {
this.jobUpdateBroadcaster = jobUpdateBroadcaster; this.jobUpdateBroadcaster = jobUpdateBroadcaster;
this.emailService = emailService; this.emailService = emailService;
this.messageService = messageService; this.messageService = messageService;
this.taskAssignmentService = taskAssignmentService;
} }
/** /**
@@ -129,7 +132,7 @@ public class MessageController {
List<JobWithRelatedDataDTO> jobsWithRelatedData = assignedJobs.stream().map(job -> { List<JobWithRelatedDataDTO> jobsWithRelatedData = assignedJobs.stream().map(job -> {
List<CargoItem> cargoItems = cargoItemRepository.findByJobId(job.getId()); List<CargoItem> cargoItems = cargoItemRepository.findByJobId(job.getId());
List<BaseTask> tasks = taskRepository.findByJobIdOrderByTaskOrderAsc(job.getId()); List<BaseTask> tasks = taskAssignmentService.findTasksForJob(job);
return new JobWithRelatedDataDTO(job, cargoItems, tasks); return new JobWithRelatedDataDTO(job, cargoItems, tasks);
}).toList(); }).toList();
@@ -349,7 +352,12 @@ 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()); Optional<Job> 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 // Log detailed task completion in job history
try { try {
@@ -380,7 +388,7 @@ public class MessageController {
private void checkAndHandleJobCompletion(ObjectId jobId, String completedBy) { private void checkAndHandleJobCompletion(ObjectId jobId, String completedBy) {
try { try {
var allTasks = taskRepository.findByJobIdOrderByTaskOrderAsc(jobId); var allTasks = taskAssignmentService.findTasksForJob(jobId);
if (allTasks.isEmpty()) { if (allTasks.isEmpty()) {
return; return;
} }

View File

@@ -1,9 +1,12 @@
package de.assecutor.votianlt.model; 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 de.assecutor.votianlt.model.task.BaseTask;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.Field;
import java.time.LocalDate; import java.time.LocalDate;
@@ -21,6 +24,10 @@ import java.util.List;
@AllArgsConstructor @AllArgsConstructor
public class DeliveryStation { public class DeliveryStation {
@Field("station_id")
@JsonIgnore
private ObjectId stationId;
@Field("station_order") @Field("station_order")
private int stationOrder; private int stationOrder;
@@ -62,4 +69,16 @@ public class DeliveryStation {
@Field("tasks") @Field("tasks")
private List<BaseTask> tasks = new ArrayList<>(); private List<BaseTask> 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;
}
} }

View File

@@ -23,6 +23,10 @@ public class TaskEntry {
@JsonIgnore @JsonIgnore
private ObjectId id; private ObjectId id;
@Field("station_id")
@JsonIgnore
private ObjectId stationId;
@Field("job_id") @Field("job_id")
@JsonIgnore @JsonIgnore
private ObjectId jobId; private ObjectId jobId;
@@ -54,10 +58,16 @@ public class TaskEntry {
} }
/** /**
* Returns the job ObjectId as string for JSON serialization. This ensures that * Returns the station ObjectId as string for JSON serialization.
* the job id is returned as a string instead of ObjectId object. */
@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() { public String getJobIdAsString() {
return jobId != null ? jobId.toString() : null; return jobId != null ? jobId.toString() : null;
} }

View File

@@ -28,6 +28,10 @@ public abstract class BaseTask {
@JsonIgnore @JsonIgnore
private ObjectId id; private ObjectId id;
@Field("station_id")
@JsonIgnore
private ObjectId stationId;
@Field("job_id") @Field("job_id")
@JsonIgnore @JsonIgnore
private ObjectId jobId; 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() { public String getJobIdAsString() {
return jobId != null ? jobId.toString() : null; return jobId != null ? jobId.toString() : null;
} }

View File

@@ -129,6 +129,8 @@ public final class MainLayout extends AppLayout {
// Add children to "Verwaltung" // Add children to "Verwaltung"
treeData.addItem(verwaltungItem, treeData.addItem(verwaltungItem,
new MenuTreeItem(getTranslation("nav.jobs"), "jobs", VaadinIcon.CLIPBOARD_TEXT)); new MenuTreeItem(getTranslation("nav.jobs"), "jobs", VaadinIcon.CLIPBOARD_TEXT));
treeData.addItem(verwaltungItem,
new MenuTreeItem(getTranslation("nav.invoices"), "invoices", VaadinIcon.FILE_TEXT));
treeData.addItem(verwaltungItem, treeData.addItem(verwaltungItem,
new MenuTreeItem(getTranslation("nav.customers"), "customers", VaadinIcon.USERS)); new MenuTreeItem(getTranslation("nav.customers"), "customers", VaadinIcon.USERS));
treeData.addItem(verwaltungItem, treeData.addItem(verwaltungItem,
@@ -139,8 +141,6 @@ public final class MainLayout extends AppLayout {
// Add children to "Benutzer" // Add children to "Benutzer"
treeData.addItem(benutzerItem, treeData.addItem(benutzerItem,
new MenuTreeItem(getTranslation("nav.profile"), "edit-profile", VaadinIcon.USER)); 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, treeData.addItem(benutzerItem,
new MenuTreeItem(getTranslation("nav.imprint"), "impressum", VaadinIcon.INFO_CIRCLE)); new MenuTreeItem(getTranslation("nav.imprint"), "impressum", VaadinIcon.INFO_CIRCLE));

View File

@@ -14,6 +14,7 @@ import de.assecutor.votianlt.repository.CargoItemRepository;
import de.assecutor.votianlt.service.ClientConnectionService; import de.assecutor.votianlt.service.ClientConnectionService;
import de.assecutor.votianlt.service.JobHistoryService; import de.assecutor.votianlt.service.JobHistoryService;
import de.assecutor.votianlt.service.EmailService; import de.assecutor.votianlt.service.EmailService;
import de.assecutor.votianlt.service.TaskAssignmentService;
import java.util.Objects; import java.util.Objects;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -42,6 +43,7 @@ public class AddJobService {
private final EmailService emailService; private final EmailService emailService;
private final ClientConnectionService clientConnectionService; private final ClientConnectionService clientConnectionService;
private final MessagingPublisher messagingPublisher; private final MessagingPublisher messagingPublisher;
private final TaskAssignmentService taskAssignmentService;
/** /**
* Speichert einen neuen Auftrag samt CargoItems und Tasks * Speichert einen neuen Auftrag samt CargoItems und Tasks
@@ -65,6 +67,8 @@ public class AddJobService {
job.setJobNumber(generateJobNumber()); job.setJobNumber(generateJobNumber());
} }
ensureDeliveryStationIds(job);
// Auftrag speichern // Auftrag speichern
Job savedJob = jobRepository.save(job); Job savedJob = jobRepository.save(job);
final ObjectId jobId = savedJob.getId(); final ObjectId jobId = savedJob.getId();
@@ -91,15 +95,24 @@ public class AddJobService {
// Tasks separat speichern und referenzieren mit korrekter Nummerierung // Tasks separat speichern und referenzieren mit korrekter Nummerierung
if (transientTasks != null && !transientTasks.isEmpty()) { if (transientTasks != null && !transientTasks.isEmpty()) {
var filteredTasks = transientTasks.stream().filter(Objects::nonNull) var filteredTasks = transientTasks.stream().filter(Objects::nonNull)
.filter(task -> task.getTaskType() != null) // Filter nach TaskType statt Text .filter(task -> task.getTaskType() != null).toList();
.toList();
Map<Integer, Integer> taskOrderByStation = new HashMap<>(); Map<Integer, Integer> taskOrderByStation = new HashMap<>();
Map<Integer, ObjectId> stationIdByOrder = buildStationIdByOrder(savedJob);
List<BaseTask> 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) { for (BaseTask task : filteredTasks) {
task.setJobId(jobId);
int stationOrder = task.getStationOrder() != null ? task.getStationOrder() : 0; 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) { if (task.getTaskOrder() == null) {
int nextTaskOrder = taskOrderByStation.getOrDefault(stationOrder, 0); int nextTaskOrder = taskOrderByStation.getOrDefault(stationOrder, 0);
task.setTaskOrder(nextTaskOrder); task.setTaskOrder(nextTaskOrder);
@@ -109,12 +122,13 @@ public class AddJobService {
task.getTaskOrder() + 1); task.getTaskOrder() + 1);
taskOrderByStation.put(stationOrder, nextTaskOrder); taskOrderByStation.put(stationOrder, nextTaskOrder);
} }
tasksToPersist.add(task);
} }
taskRepository.saveAll(filteredTasks); taskRepository.saveAll(tasksToPersist);
attachTasksToDeliveryStations(savedJob, filteredTasks); attachTasksToDeliveryStations(savedJob, tasksToPersist);
savedJob = jobRepository.save(savedJob); 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()) { } else if (savedJob.getDeliveryStations() != null && !savedJob.getDeliveryStations().isEmpty()) {
attachTasksToDeliveryStations(savedJob, List.of()); attachTasksToDeliveryStations(savedJob, List.of());
savedJob = jobRepository.save(savedJob); savedJob = jobRepository.save(savedJob);
@@ -220,7 +234,7 @@ public class AddJobService {
try { try {
// Lade CargoItems und Tasks für den Job // Lade CargoItems und Tasks für den Job
List<CargoItem> cargoItems = cargoItemRepository.findByJobId(job.getId()); List<CargoItem> cargoItems = cargoItemRepository.findByJobId(job.getId());
List<BaseTask> tasks = taskRepository.findByJobIdOrderByTaskOrderAsc(job.getId()); List<BaseTask> tasks = taskAssignmentService.findTasksForJob(job);
// Erstelle DTO mit allen Daten // Erstelle DTO mit allen Daten
JobWithRelatedDataDTO jobData = new JobWithRelatedDataDTO(job, cargoItems, tasks); JobWithRelatedDataDTO jobData = new JobWithRelatedDataDTO(job, cargoItems, tasks);
@@ -238,20 +252,54 @@ public class AddJobService {
return; return;
} }
Map<Integer, List<BaseTask>> tasksByStation = new HashMap<>(); Map<String, List<BaseTask>> tasksByStationId = new HashMap<>();
Map<Integer, List<BaseTask>> legacyTasksByStationOrder = new HashMap<>();
for (BaseTask task : tasks) { for (BaseTask task : tasks) {
if (task == null) { if (task == null) {
continue; continue;
} }
if (task.getStationId() != null) {
tasksByStationId.computeIfAbsent(task.getStationId().toHexString(), ignored -> new ArrayList<>()).add(task);
continue;
}
int stationOrder = task.getStationOrder() != null ? task.getStationOrder() : 0; 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()) { for (DeliveryStation station : job.getDeliveryStations()) {
int stationOrder = station.getStationOrder(); String stationKey = station.getStationId() != null ? station.getStationId().toHexString() : null;
List<BaseTask> stationTasks = new ArrayList<>(tasksByStation.getOrDefault(stationOrder, List.of())); List<BaseTask> 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)); stationTasks.sort(Comparator.comparing(task -> task.getTaskOrder() != null ? task.getTaskOrder() : 0));
station.setTasks(stationTasks); 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<Integer, ObjectId> buildStationIdByOrder(Job job) {
Map<Integer, ObjectId> 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;
}
} }

View File

@@ -2,25 +2,22 @@ package de.assecutor.votianlt.pages.view;
import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.H2; 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.notification.Notification;
import com.vaadin.flow.component.orderedlayout.FlexComponent; import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout;
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.component.UI; import com.vaadin.flow.component.UI;
import de.assecutor.votianlt.model.invoices.SystemInvoice; import de.assecutor.votianlt.model.invoices.CustomerInvoice;
import de.assecutor.votianlt.model.invoices.SystemInvoiceData; import de.assecutor.votianlt.repository.CustomerInvoiceRepository;
import de.assecutor.votianlt.model.invoices.SystemInvoiceItem; import de.assecutor.votianlt.security.SecurityService;
import de.assecutor.votianlt.service.SystemInvoiceService;
import de.assecutor.votianlt.util.DateTimeFormatUtil;
import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RolesAllowed;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.text.NumberFormat;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Optional;
import com.vaadin.flow.server.StreamResource; import com.vaadin.flow.server.StreamResource;
import com.vaadin.flow.server.StreamRegistration; import com.vaadin.flow.server.StreamRegistration;
@@ -29,12 +26,13 @@ import com.vaadin.flow.server.StreamRegistration;
@RolesAllowed({ "USER", "ADMIN" }) @RolesAllowed({ "USER", "ADMIN" })
public class InvoicesView extends VerticalLayout implements HasDynamicTitle { public class InvoicesView extends VerticalLayout implements HasDynamicTitle {
private final Grid<SystemInvoice> invoiceGrid; private final Grid<CustomerInvoice> invoiceGrid;
private final CustomerInvoiceRepository customerInvoiceRepository;
private final SecurityService securityService;
private final SystemInvoiceService systemInvoiceService; public InvoicesView(CustomerInvoiceRepository customerInvoiceRepository, SecurityService securityService) {
this.customerInvoiceRepository = customerInvoiceRepository;
public InvoicesView(SystemInvoiceService systemInvoiceService) { this.securityService = securityService;
this.systemInvoiceService = systemInvoiceService;
setSizeFull(); setSizeFull();
setPadding(true); setPadding(true);
@@ -45,49 +43,73 @@ public class InvoicesView extends VerticalLayout implements HasDynamicTitle {
H2 title = new H2(getTranslation("invoices.title")); H2 title = new H2(getTranslation("invoices.title"));
add(title); add(title);
invoiceGrid = new Grid<>(SystemInvoice.class, false); invoiceGrid = new Grid<>(CustomerInvoice.class, false);
invoiceGrid.addColumn(SystemInvoice::getId).setHeader(getTranslation("invoices.column.number")) invoiceGrid.setWidthFull();
invoiceGrid.addColumn(invoice -> firstNonBlank(invoice.getInvoiceNumber(), invoice.getId()))
.setHeader(getTranslation("invoices.column.number"))
.setAutoWidth(true); .setAutoWidth(true);
invoiceGrid.addColumn(SystemInvoice::getKunde).setHeader(getTranslation("invoices.column.customer")) invoiceGrid.addColumn(this::getRecipientLabel).setHeader(getTranslation("invoices.column.customer"))
.setAutoWidth(true); .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); .setAutoWidth(true);
invoiceGrid.addColumn(SystemInvoice::getBetrag).setHeader(getTranslation("invoices.column.amount")) invoiceGrid.addColumn(this::formatAmount).setHeader(getTranslation("invoices.column.amount"))
.setAutoWidth(true); .setAutoWidth(true);
invoiceGrid.addColumn(SystemInvoice::getBeschreibung).setHeader(getTranslation("invoices.column.description")) invoiceGrid.addColumn(invoice -> firstNonBlank(invoice.getDescription(), ""))
.setHeader(getTranslation("invoices.column.description"))
.setAutoWidth(true); .setAutoWidth(true);
invoiceGrid.setSelectionMode(Grid.SelectionMode.SINGLE); invoiceGrid.setSelectionMode(Grid.SelectionMode.SINGLE);
invoiceGrid.getStyle().set("cursor", "pointer"); invoiceGrid.getStyle().set("cursor", "pointer");
// Testdaten
List<SystemInvoice> 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 -> { invoiceGrid.addItemClickListener(event -> {
SystemInvoice systemInvoice = event.getItem(); CustomerInvoice invoice = event.getItem();
if (systemInvoice != null) { if (invoice != null) {
downloadInvoicePdf(systemInvoice); downloadInvoicePdf(invoice);
} }
}); });
loadInvoices();
add(invoiceGrid); add(invoiceGrid);
} }
private void downloadInvoicePdf(SystemInvoice systemInvoice) { private void loadInvoices() {
String currentUserId = securityService.getCurrentUserId().toHexString();
List<CustomerInvoice> 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 { try {
// PDF generieren mit SystemInvoice (HTML Template) if (invoice.getPdfData() == null || invoice.getPdfData().length == 0) {
byte[] pdfBytes = generateSystemInvoicePdf(systemInvoice); Notification.show(getTranslation("invoices.notification.pdf.missing"), 4000,
StreamResource resource = new StreamResource(systemInvoice.getId() + ".pdf", Notification.Position.MIDDLE);
() -> new ByteArrayInputStream(pdfBytes)); return;
}
StreamResource resource = new StreamResource(firstNonBlank(invoice.getInvoiceNumber(), invoice.getId()) + ".pdf",
() -> new ByteArrayInputStream(invoice.getPdfData()));
resource.setContentType("application/pdf"); resource.setContentType("application/pdf");
resource.setCacheTime(0); resource.setCacheTime(0);
// Direkter Download über UI
StreamRegistration registration = UI.getCurrent().getSession().getResourceRegistry() StreamRegistration registration = UI.getCurrent().getSession().getResourceRegistry()
.registerResource(resource); .registerResource(resource);
UI.getCurrent().getPage().open(registration.getResourceUri().toString()); 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 { private String getRecipientLabel(CustomerInvoice invoice) {
NumberFormat CURRENCY_FMT = NumberFormat.getCurrencyInstance(Locale.GERMANY); return firstNonBlank(invoice.getRecipientCompany(), invoice.getRecipientName(), "");
}
SystemInvoiceData data = new SystemInvoiceData(); private String formatAmount(CustomerInvoice invoice) {
data.setInvoiceNumber(systemInvoice.getId()); var amount = invoice.getTotalAmount() != null ? invoice.getTotalAmount() : invoice.getNetAmount();
data.setInvoiceDate(DateTimeFormatUtil.formatDate(systemInvoice.getDatum())); if (amount == null) {
data.setInvoiceText(systemInvoice.getBeschreibung()); return "";
}
return java.text.NumberFormat.getCurrencyInstance(Locale.GERMANY).format(amount);
}
// Empfänger aus der Zeile (nur Name in den Testdaten vorhanden) private String firstNonBlank(String... values) {
data.setRecipientName(systemInvoice.getKunde()); for (String value : values) {
data.setRecipientDepartment(""); if (value != null && !value.isBlank()) {
data.setRecipientStreet(""); return value;
data.setRecipientCity(""); }
}
// Eine Position mit dem Betrag/Beschreibung return "";
List<SystemInvoiceItem> 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);
} }
@Override @Override

View File

@@ -41,7 +41,6 @@ import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import de.assecutor.votianlt.repository.CargoItemRepository; import de.assecutor.votianlt.repository.CargoItemRepository;
import de.assecutor.votianlt.repository.JobRepository; import de.assecutor.votianlt.repository.JobRepository;
import de.assecutor.votianlt.repository.TaskRepository;
import de.assecutor.votianlt.repository.SignatureRepository; import de.assecutor.votianlt.repository.SignatureRepository;
import de.assecutor.votianlt.repository.BarcodeRepository; import de.assecutor.votianlt.repository.BarcodeRepository;
import de.assecutor.votianlt.repository.PhotoRepository; 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.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.service.TaskAssignmentService;
import de.assecutor.votianlt.util.DateTimeFormatUtil; import de.assecutor.votianlt.util.DateTimeFormatUtil;
import com.vaadin.flow.component.confirmdialog.ConfirmDialog; import com.vaadin.flow.component.confirmdialog.ConfirmDialog;
import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RolesAllowed;
@@ -78,7 +78,6 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
private final JobRepository jobRepository; private final JobRepository jobRepository;
private final CargoItemRepository cargoItemRepository; private final CargoItemRepository cargoItemRepository;
private final TaskRepository taskRepository;
private final SignatureRepository signatureRepository; private final SignatureRepository signatureRepository;
private final BarcodeRepository barcodeRepository; private final BarcodeRepository barcodeRepository;
private final PhotoRepository photoRepository; private final PhotoRepository photoRepository;
@@ -88,6 +87,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
private final JobUpdateBroadcaster jobUpdateBroadcaster; private final JobUpdateBroadcaster jobUpdateBroadcaster;
private final LocationService locationService; private final LocationService locationService;
private final ServiceRepository serviceRepository; private final ServiceRepository serviceRepository;
private final TaskAssignmentService taskAssignmentService;
@Value("${app.google.maps.api-key}") @Value("${app.google.maps.api-key}")
private String googleMapsApiKey; private String googleMapsApiKey;
@@ -96,16 +96,16 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
private final List<Div> taskCards = new ArrayList<>(); private final List<Div> taskCards = new ArrayList<>();
private Registration jobUpdateRegistration; private Registration jobUpdateRegistration;
private ObjectId currentJobId; private ObjectId currentJobId;
private Div stationTilesSection;
public JobSummaryView(JobRepository jobRepository, CargoItemRepository cargoItemRepository, public JobSummaryView(JobRepository jobRepository, CargoItemRepository cargoItemRepository,
TaskRepository taskRepository, SignatureRepository signatureRepository, BarcodeRepository barcodeRepository, SignatureRepository signatureRepository, BarcodeRepository barcodeRepository,
PhotoRepository photoRepository, CommentRepository commentRepository, AppUserService appUserService, PhotoRepository photoRepository, CommentRepository commentRepository, AppUserService appUserService,
MessageService messageService, JobHistoryService jobHistoryService, MessageService messageService, JobHistoryService jobHistoryService,
JobUpdateBroadcaster jobUpdateBroadcaster, LocationService locationService, JobUpdateBroadcaster jobUpdateBroadcaster, LocationService locationService,
ServiceRepository serviceRepository) { ServiceRepository serviceRepository, TaskAssignmentService taskAssignmentService) {
this.jobRepository = jobRepository; this.jobRepository = jobRepository;
this.cargoItemRepository = cargoItemRepository; this.cargoItemRepository = cargoItemRepository;
this.taskRepository = taskRepository;
this.signatureRepository = signatureRepository; this.signatureRepository = signatureRepository;
this.barcodeRepository = barcodeRepository; this.barcodeRepository = barcodeRepository;
this.photoRepository = photoRepository; this.photoRepository = photoRepository;
@@ -115,6 +115,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
this.jobUpdateBroadcaster = jobUpdateBroadcaster; this.jobUpdateBroadcaster = jobUpdateBroadcaster;
this.locationService = locationService; this.locationService = locationService;
this.serviceRepository = serviceRepository; this.serviceRepository = serviceRepository;
this.taskAssignmentService = taskAssignmentService;
setSizeFull(); setSizeFull();
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN, addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
@@ -159,7 +160,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
if (currentJobId == null || jobId == null || !currentJobId.equals(jobId)) { if (currentJobId == null || jobId == null || !currentJobId.equals(jobId)) {
return; return;
} }
ui.access(this::refreshCurrentJobSummary); ui.access(this::refreshStationTilesOnly);
}); });
} }
@@ -220,16 +221,35 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
add(new ViewToolbar("Zusammenfassung", sendMessageButton, jobHistoryButton)); add(new ViewToolbar("Zusammenfassung", sendMessageButton, jobHistoryButton));
List<CargoItem> cargo = cargoItemRepository.findByJobId(currentJobId); List<CargoItem> cargo = cargoItemRepository.findByJobId(currentJobId);
List<BaseTask> tasks = taskRepository.findByJobIdOrderByTaskOrderAsc(currentJobId); List<BaseTask> tasks = taskAssignmentService.findTasksForJob(job);
render(job, cargo, tasks); render(job, cargo, tasks);
add(content); add(content);
} }
private void refreshStationTilesOnly() {
if (currentJobId == null || stationTilesSection == null) {
return;
}
Job job = jobRepository.findById(currentJobId).orElse(null);
if (job == null) {
return;
}
List<CargoItem> cargo = cargoItemRepository.findByJobId(currentJobId);
List<BaseTask> tasks = taskAssignmentService.findTasksForJob(job);
Div updatedSection = createStationTilesSection(job, cargo, tasks);
content.replace(stationTilesSection, updatedSection);
stationTilesSection = updatedSection;
}
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, cargoItems, tasks)); stationTilesSection = createStationTilesSection(job, cargoItems, tasks);
content.add(stationTilesSection);
// Fracht und weitere Infos // Fracht und weitere Infos
HorizontalLayout midRow = new HorizontalLayout(); HorizontalLayout midRow = new HorizontalLayout();
@@ -600,35 +620,46 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
} }
private List<BaseTask> getTasksForStation(DeliveryStation station, List<BaseTask> tasks, boolean legacyMode) { private List<BaseTask> getTasksForStation(DeliveryStation station, List<BaseTask> 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<BaseTask> stationTasks = new ArrayList<>();
int stationOrder = station != null ? station.getStationOrder() : 0; int stationOrder = station != null ? station.getStationOrder() : 0;
for (BaseTask task : tasks) { String stationId = station != null ? station.getStationIdAsString() : null;
if (task == null) {
continue;
}
Integer taskStationOrder = task.getStationOrder(); if (tasks != null && !tasks.isEmpty()) {
if (legacyMode) { List<BaseTask> stationTasks = new ArrayList<>();
if (taskStationOrder == null || taskStationOrder == stationOrder) { 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); 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<BaseTask> sortVisibleTasks(List<BaseTask> 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<BaseTask> tasks) { private boolean areAllTasksCompleted(List<BaseTask> tasks) {

View File

@@ -100,6 +100,9 @@ public interface JobRepository extends MongoRepository<Job, ObjectId> {
@Query("{'app_user': ?0, 'status': {'$nin': ['COMPLETED', 'CANCELLED']}}") @Query("{'app_user': ?0, 'status': {'$nin': ['COMPLETED', 'CANCELLED']}}")
List<Job> findByAppUser(String appUser); List<Job> findByAppUser(String appUser);
@Query("{'delivery_stations.station_id': ?0}")
Optional<Job> findByDeliveryStationsStationId(ObjectId stationId);
/** /**
* Findet Aufträge anhand einer partiellen Auftragsnummer (case-insensitive) * Findet Aufträge anhand einer partiellen Auftragsnummer (case-insensitive)
*/ */

View File

@@ -7,6 +7,10 @@ import org.springframework.data.mongodb.repository.MongoRepository;
import java.util.List; import java.util.List;
public interface TaskRepository extends MongoRepository<BaseTask, ObjectId> { public interface TaskRepository extends MongoRepository<BaseTask, ObjectId> {
List<BaseTask> findByStationIdOrderByTaskOrderAsc(ObjectId stationId);
List<BaseTask> findByStationIdIn(List<ObjectId> stationIds);
List<BaseTask> findByJobIdOrderByTaskOrderAsc(ObjectId jobId); List<BaseTask> findByJobIdOrderByTaskOrderAsc(ObjectId jobId);
List<BaseTask> findByJobIdAndStationOrderOrderByTaskOrderAsc(ObjectId jobId, int stationOrder); List<BaseTask> findByJobIdAndStationOrderOrderByTaskOrderAsc(ObjectId jobId, int stationOrder);

View File

@@ -23,6 +23,7 @@ public class EmailService {
private final UserRepository userRepository; private final UserRepository userRepository;
private final JobRepository jobRepository; private final JobRepository jobRepository;
private final TaskRepository taskRepository; private final TaskRepository taskRepository;
private final TaskAssignmentService taskAssignmentService;
private final JavaMailSender mailSender; private final JavaMailSender mailSender;
@Value("${spring.mail.username}") @Value("${spring.mail.username}")
@@ -194,7 +195,7 @@ public class EmailService {
String appUserName = buildAppUserName(user); String appUserName = buildAppUserName(user);
// Count completed tasks // Count completed tasks
var allTasks = taskRepository.findByJobIdOrderByTaskOrderAsc(job.getId()); var allTasks = taskAssignmentService.findTasksForJob(job);
int taskCount = allTasks.size(); int taskCount = allTasks.size();
StringBuilder body = new StringBuilder(); StringBuilder body = new StringBuilder();
@@ -283,7 +284,7 @@ public class EmailService {
String fullName = buildFullName(user); String fullName = buildFullName(user);
// Count tasks for this job // Count tasks for this job
var allTasks = taskRepository.findByJobIdOrderByTaskOrderAsc(job.getId()); var allTasks = taskAssignmentService.findTasksForJob(job);
int taskCount = allTasks.size(); int taskCount = allTasks.size();
StringBuilder body = new StringBuilder(); 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); log.error("Failed to send simple email to {} with subject '{}': {}", to, subject, e.getMessage(), e);
} }
} }
} }

View File

@@ -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<BaseTask> findTasksForJob(Job job) {
if (job == null || job.getId() == null) {
return List.of();
}
Map<String, BaseTask> uniqueTasks = new LinkedHashMap<>();
List<ObjectId> 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<BaseTask> findTasksForJob(ObjectId jobId) {
if (jobId == null) {
return List.of();
}
return jobRepository.findById(jobId).map(this::findTasksForJob)
.orElseGet(() -> sortTasksForJob(null, taskRepository.findByJobIdOrderByTaskOrderAsc(jobId)));
}
public Optional<Job> findJobForTask(BaseTask task) {
if (task == null) {
return Optional.empty();
}
if (task.getStationId() != null) {
Optional<Job> jobByStation = jobRepository.findByDeliveryStationsStationId(task.getStationId());
if (jobByStation.isPresent()) {
return jobByStation;
}
}
return Optional.ofNullable(task.getJobId()).flatMap(jobRepository::findById);
}
private List<ObjectId> 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<BaseTask> sortTasksForJob(Job job, List<BaseTask> tasks) {
Map<String, Integer> 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.<BaseTask>comparingInt(task -> resolveStationOrder(task, stationOrderById))
.thenComparingInt(task -> task.getTaskOrder() != null ? task.getTaskOrder() : 0))
.toList();
}
private int resolveStationOrder(BaseTask task, Map<String, Integer> 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<String, BaseTask> 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);
}
}

View File

@@ -682,6 +682,8 @@ invoices.column.customer=Kunde
invoices.column.date=Datum invoices.column.date=Datum
invoices.column.amount=Betrag invoices.column.amount=Betrag
invoices.column.description=Beschreibung 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 # My Invoices
myinvoices.title=Rechnungen myinvoices.title=Rechnungen
@@ -906,6 +908,13 @@ jobhistory.status.pickedup=Abgeholt
jobhistory.status.intransit=Unterwegs jobhistory.status.intransit=Unterwegs
jobhistory.status.delivered=Zugestellt jobhistory.status.delivered=Zugestellt
jobhistory.image.alt=Vergrößertes Foto 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
version.label=Version version.label=Version

View File

@@ -682,6 +682,8 @@ invoices.column.customer=Customer
invoices.column.date=Date invoices.column.date=Date
invoices.column.amount=Amount invoices.column.amount=Amount
invoices.column.description=Description 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 # My Invoices
myinvoices.title=My Invoices myinvoices.title=My Invoices
@@ -905,6 +907,13 @@ jobhistory.status.pickedup=Picked Up
jobhistory.status.intransit=In Transit jobhistory.status.intransit=In Transit
jobhistory.status.delivered=Delivered jobhistory.status.delivered=Delivered
jobhistory.image.alt=Enlarged Photo 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
version.label=Version version.label=Version