refactor: assign tasks to delivery stations
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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<JobWithRelatedDataDTO> jobsWithRelatedData = assignedJobs.stream().map(job -> {
|
||||
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);
|
||||
}).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<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
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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<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) {
|
||||
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<CargoItem> cargoItems = cargoItemRepository.findByJobId(job.getId());
|
||||
List<BaseTask> tasks = taskRepository.findByJobIdOrderByTaskOrderAsc(job.getId());
|
||||
List<BaseTask> 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<Integer, List<BaseTask>> tasksByStation = new HashMap<>();
|
||||
Map<String, List<BaseTask>> tasksByStationId = new HashMap<>();
|
||||
Map<Integer, List<BaseTask>> 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<BaseTask> stationTasks = new ArrayList<>(tasksByStation.getOrDefault(stationOrder, List.of()));
|
||||
String stationKey = station.getStationId() != null ? station.getStationId().toHexString() : null;
|
||||
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));
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SystemInvoice> invoiceGrid;
|
||||
private final Grid<CustomerInvoice> 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<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 -> {
|
||||
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<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 {
|
||||
// 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<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);
|
||||
private String firstNonBlank(String... values) {
|
||||
for (String value : values) {
|
||||
if (value != null && !value.isBlank()) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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<String>, 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<String>, 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<String>, Has
|
||||
private final List<Div> 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<String>, 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<String>, 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<String>, Has
|
||||
add(new ViewToolbar("Zusammenfassung", sendMessageButton, jobHistoryButton));
|
||||
|
||||
List<CargoItem> cargo = cargoItemRepository.findByJobId(currentJobId);
|
||||
List<BaseTask> tasks = taskRepository.findByJobIdOrderByTaskOrderAsc(currentJobId);
|
||||
List<BaseTask> 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<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) {
|
||||
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,27 +620,21 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
}
|
||||
|
||||
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;
|
||||
String stationId = station != null ? station.getStationIdAsString() : null;
|
||||
|
||||
if (tasks != null && !tasks.isEmpty()) {
|
||||
List<BaseTask> stationTasks = new ArrayList<>();
|
||||
for (BaseTask task : tasks) {
|
||||
if (task == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String taskStationId = task.getStationIdAsString();
|
||||
Integer taskStationOrder = task.getStationOrder();
|
||||
if (legacyMode) {
|
||||
if (stationId != null && stationId.equals(taskStationId)) {
|
||||
stationTasks.add(task);
|
||||
} else if (legacyMode) {
|
||||
if (taskStationOrder == null || taskStationOrder == stationOrder) {
|
||||
stationTasks.add(task);
|
||||
}
|
||||
@@ -628,7 +642,24 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
stationTasks.add(task);
|
||||
}
|
||||
}
|
||||
return stationTasks;
|
||||
if (!stationTasks.isEmpty()) {
|
||||
return sortVisibleTasks(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) {
|
||||
|
||||
@@ -100,6 +100,9 @@ public interface JobRepository extends MongoRepository<Job, ObjectId> {
|
||||
@Query("{'app_user': ?0, 'status': {'$nin': ['COMPLETED', 'CANCELLED']}}")
|
||||
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)
|
||||
*/
|
||||
|
||||
@@ -7,6 +7,10 @@ import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
import java.util.List;
|
||||
|
||||
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> findByJobIdAndStationOrderOrderByTaskOrderAsc(ObjectId jobId, int stationOrder);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user