fix: Stationsabschluss absichern und Kundenformulare anpassen
Sperrt nachträgliche Aufgabenabschlüsse in App und Backend und zeigt erledigte Aufträge trotz offener optionaler Aufgaben grün an. Ergänzt Regressionstests, die Anredeauswahl und optionale Steuerangaben für Kunden. Enthält App-Version 0.9.25, Backend-Version 0.9.26 und das aktualisierte Frontend-Bundle.
This commit is contained in:
1 parent
8758c53e5b
commit
5e6044e4b1
16 files changed
+638
-69
No files matched your search
+1
-1
@@ -11,7 +11,7 @@
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<revision>0.9.25</revision>
|
||||
<revision>0.9.26</revision>
|
||||
<java.version>21</java.version>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
|
||||
Binary file not shown.
@@ -191,6 +191,17 @@ public class MessageController {
|
||||
}
|
||||
|
||||
public void handleTaskCompleted(Map<String, Object> payload, String taskType, String appUserId) {
|
||||
Object taskId = payload != null ? payload.get("taskId") : null;
|
||||
if (taskId == null || !ObjectId.isValid(taskId.toString())) {
|
||||
return;
|
||||
}
|
||||
Optional<BaseTask> task = taskRepository.findById(new ObjectId(taskId.toString()));
|
||||
if (task.isEmpty() || taskAssignmentService.findJobForTask(task.get())
|
||||
.filter(job -> canCompleteTask(task.get(), job)).isEmpty()) {
|
||||
// Check before dispatch: photo/signature/barcode/comment handlers also
|
||||
// persist attachments before completing the task itself.
|
||||
return;
|
||||
}
|
||||
String key = taskType == null ? "" : taskType.trim().toUpperCase();
|
||||
|
||||
switch (key) {
|
||||
@@ -378,15 +389,14 @@ public class MessageController {
|
||||
return;
|
||||
}
|
||||
BaseTask task = opt.get();
|
||||
Optional<Job> jobOpt = taskAssignmentService.findJobForTask(task);
|
||||
if (jobOpt.isEmpty() || !canCompleteTask(task, jobOpt.get())) {
|
||||
return;
|
||||
}
|
||||
task.setCompletedBy(resolveTaskActor(task, appUserId));
|
||||
task.setCompleted(true);
|
||||
task.setCompletedAt(LocalDateTime.now());
|
||||
taskRepository.save(task);
|
||||
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
|
||||
@@ -421,6 +431,19 @@ public class MessageController {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean canCompleteTask(BaseTask task, Job job) {
|
||||
if (task.isCompleted() || job.getStatus() == JobStatus.COMPLETED || job.getStatus() == JobStatus.CANCELLED) {
|
||||
return false;
|
||||
}
|
||||
int stationOrder = task.getStationOrder() != null ? task.getStationOrder() : 0;
|
||||
if (task.getStationId() != null && job.getDeliveryStations() != null) {
|
||||
stationOrder = job.getDeliveryStations().stream().filter(Objects::nonNull)
|
||||
.filter(station -> task.getStationId().equals(station.getStationId()))
|
||||
.mapToInt(DeliveryStation::getStationOrder).findFirst().orElse(stationOrder);
|
||||
}
|
||||
return job.getCompletedStationOrders() == null || !job.getCompletedStationOrders().contains(stationOrder);
|
||||
}
|
||||
|
||||
private String resolveTaskActor(BaseTask task, String appUserId) {
|
||||
if (appUserId != null && !appUserId.isBlank()) {
|
||||
return appUserId;
|
||||
|
||||
@@ -52,15 +52,12 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
|
||||
companyName = new TextField(getTranslation("profile.company"));
|
||||
companyName.setWidthFull();
|
||||
|
||||
// Steuernummer (Pflichtfeld)
|
||||
// Steuernummer (optional)
|
||||
taxNumber = new TextField(getTranslation("profile.billing.taxnumber"));
|
||||
taxNumber.setRequiredIndicatorVisible(true);
|
||||
taxNumber.setWidthFull();
|
||||
taxNumber.addBlurListener(e -> validateField(taxNumber));
|
||||
|
||||
// USt-IdNr. (Pflichtfeld)
|
||||
// USt-IdNr. (optional)
|
||||
ustId = new TextField(getTranslation("profile.billing.ustid"));
|
||||
ustId.setRequiredIndicatorVisible(true);
|
||||
ustId.setPlaceholder("DE123456789");
|
||||
ustId.setWidthFull();
|
||||
ustId.addBlurListener(e -> validateUstId());
|
||||
@@ -181,11 +178,11 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
|
||||
private void configureBinder() {
|
||||
binder.forField(companyName).bind(Customer::getCompanyName, Customer::setCompanyName);
|
||||
|
||||
binder.forField(taxNumber).asRequired(getTranslation("profile.validation.taxnumber.required"))
|
||||
.bind(Customer::getTaxNumber, Customer::setTaxNumber);
|
||||
binder.forField(taxNumber).bind(Customer::getTaxNumber, Customer::setTaxNumber);
|
||||
|
||||
binder.forField(ustId).asRequired(getTranslation("profile.validation.ustid.required"))
|
||||
.withValidator(de.assecutor.votianlt.util.VatIdUtil::isValid,
|
||||
binder.forField(ustId)
|
||||
.withValidator(value -> value == null || value.isBlank()
|
||||
|| de.assecutor.votianlt.util.VatIdUtil.isValid(value),
|
||||
getTranslation("profile.validation.ustid.invalid"))
|
||||
.bind(Customer::getUstId, Customer::setUstId);
|
||||
|
||||
@@ -265,15 +262,12 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft die USt-IdNr. auf Pflichtangabe und gültiges Format
|
||||
* Prüft eine angegebene USt-IdNr. auf gültiges Format
|
||||
* (Länderkennzeichen + länderspezifischer Aufbau, z. B. DE123456789).
|
||||
*/
|
||||
private void validateUstId() {
|
||||
String value = ustId.getValue();
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
ustId.setInvalid(true);
|
||||
ustId.setErrorMessage(getTranslation("profile.validation.ustid.required"));
|
||||
} else if (!de.assecutor.votianlt.util.VatIdUtil.isValid(value)) {
|
||||
if (value != null && !value.isBlank() && !de.assecutor.votianlt.util.VatIdUtil.isValid(value)) {
|
||||
ustId.setInvalid(true);
|
||||
ustId.setErrorMessage(getTranslation("profile.validation.ustid.invalid"));
|
||||
} else {
|
||||
@@ -305,7 +299,6 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
|
||||
}
|
||||
|
||||
private boolean validateAllFields() {
|
||||
validateField(taxNumber);
|
||||
validateUstId();
|
||||
validateField(firstName);
|
||||
validateField(lastName);
|
||||
@@ -316,7 +309,7 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
|
||||
validateField(city);
|
||||
validateEmail();
|
||||
|
||||
return !taxNumber.isInvalid() && !ustId.isInvalid() && !firstName.isInvalid() && !lastName.isInvalid()
|
||||
return !ustId.isInvalid() && !firstName.isInvalid() && !lastName.isInvalid()
|
||||
&& !telephone.isInvalid() && !mail.isInvalid() && !street.isInvalid() && !houseNumber.isInvalid()
|
||||
&& !zip.isInvalid() && !city.isInvalid();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.assecutor.votianlt.pages.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.dialog.Dialog;
|
||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
||||
import com.vaadin.flow.component.html.H2;
|
||||
@@ -34,7 +35,7 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
|
||||
private final Binder<Customer> binder = new Binder<>(Customer.class);
|
||||
|
||||
// Form fields - labels set in constructor via setLabel()
|
||||
private final TextField titleField = new TextField();
|
||||
private final ComboBox<String> titleField = new ComboBox<>();
|
||||
private final TextField companyNameField = new TextField();
|
||||
private final TextField taxNumberField = new TextField();
|
||||
private final TextField ustIdField = new TextField();
|
||||
@@ -62,11 +63,12 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
|
||||
|
||||
// Set field labels via i18n
|
||||
titleField.setLabel(getTranslation("addjob.address.salutation"));
|
||||
titleField.setItems(getTranslation("addjob.salutation.mr"), getTranslation("addjob.salutation.ms"),
|
||||
getTranslation("addjob.salutation.other"));
|
||||
titleField.setPlaceholder(getTranslation("addjob.address.salutation.placeholder"));
|
||||
companyNameField.setLabel(getTranslation("profile.company"));
|
||||
taxNumberField.setLabel(getTranslation("profile.billing.taxnumber"));
|
||||
taxNumberField.setRequiredIndicatorVisible(true);
|
||||
ustIdField.setLabel(getTranslation("profile.billing.ustid"));
|
||||
ustIdField.setRequiredIndicatorVisible(true);
|
||||
ustIdField.setPlaceholder("DE123456789");
|
||||
firstnameField.setLabel(getTranslation("profile.firstname"));
|
||||
lastNameField.setLabel(getTranslation("profile.lastname"));
|
||||
@@ -143,10 +145,10 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
|
||||
private void setupBinder() {
|
||||
binder.forField(titleField).bind(Customer::getTitle, Customer::setTitle);
|
||||
binder.forField(companyNameField).bind(Customer::getCompanyName, Customer::setCompanyName);
|
||||
binder.forField(taxNumberField).asRequired(getTranslation("profile.validation.taxnumber.required"))
|
||||
.bind(Customer::getTaxNumber, Customer::setTaxNumber);
|
||||
binder.forField(ustIdField).asRequired(getTranslation("profile.validation.ustid.required"))
|
||||
.withValidator(de.assecutor.votianlt.util.VatIdUtil::isValid,
|
||||
binder.forField(taxNumberField).bind(Customer::getTaxNumber, Customer::setTaxNumber);
|
||||
binder.forField(ustIdField)
|
||||
.withValidator(value -> value == null || value.isBlank()
|
||||
|| de.assecutor.votianlt.util.VatIdUtil.isValid(value),
|
||||
getTranslation("profile.validation.ustid.invalid"))
|
||||
.bind(Customer::getUstId, Customer::setUstId);
|
||||
binder.forField(firstnameField).bind(Customer::getFirstname, Customer::setFirstname);
|
||||
|
||||
@@ -5,9 +5,14 @@ import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import de.assecutor.votianlt.model.Job;
|
||||
import de.assecutor.votianlt.model.JobStatus;
|
||||
import de.assecutor.votianlt.model.DeliveryStation;
|
||||
import de.assecutor.votianlt.model.task.BaseTask;
|
||||
import de.assecutor.votianlt.model.task.BarcodeTask;
|
||||
import de.assecutor.votianlt.model.task.CommentTask;
|
||||
@@ -73,6 +78,7 @@ class TaskCompletionHistoryTest {
|
||||
task.setCompletedBy("Unknown");
|
||||
Job job = new Job();
|
||||
job.setId(new ObjectId());
|
||||
job.setCompletedStationOrders(List.of(1));
|
||||
when(taskRepository.findById(task.getId())).thenReturn(Optional.of(task));
|
||||
when(taskAssignmentService.findJobForTask(task)).thenReturn(Optional.of(job));
|
||||
Map<String, Object> extra = Map.of("photos", List.of("image-data"), "barcodes", List.of("CODE-1"),
|
||||
@@ -100,4 +106,60 @@ class TaskCompletionHistoryTest {
|
||||
verify(commentRepository).save(argThat(comment -> actor.equals(comment.getCompletedBy())));
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("tasks")
|
||||
void optionalTasksOfClosedStationCannotPersistDataOrSendEmail(BaseTask task) {
|
||||
task.setId(new ObjectId());
|
||||
task.setOptional(true);
|
||||
task.setStationOrder(0);
|
||||
Job job = new Job();
|
||||
job.setCompletedStationOrders(List.of(0));
|
||||
rejectCompletion(task, job);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("tasks")
|
||||
void completedJobRejectsTasksEvenWithoutStationConfirmation(BaseTask task) {
|
||||
task.setId(new ObjectId());
|
||||
task.setOptional(true);
|
||||
Job job = new Job();
|
||||
job.setStatus(JobStatus.COMPLETED);
|
||||
rejectCompletion(task, job);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("tasks")
|
||||
void repeatedCompletionCannotAddAttachmentsOrSendAnotherEmail(BaseTask task) {
|
||||
task.setId(new ObjectId());
|
||||
task.setCompleted(true);
|
||||
rejectCompletion(task, new Job());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("tasks")
|
||||
void closedStationIsResolvedByIdEvenWhenTaskOrderIsStale(BaseTask task) {
|
||||
task.setId(new ObjectId());
|
||||
task.setStationId(new ObjectId());
|
||||
task.setStationOrder(7);
|
||||
DeliveryStation station = new DeliveryStation();
|
||||
station.setStationId(task.getStationId());
|
||||
station.setStationOrder(0);
|
||||
Job job = new Job();
|
||||
job.setDeliveryStations(List.of(station));
|
||||
job.setCompletedStationOrders(List.of(0));
|
||||
rejectCompletion(task, job);
|
||||
}
|
||||
|
||||
private void rejectCompletion(BaseTask task, Job job) {
|
||||
when(taskRepository.findById(task.getId())).thenReturn(Optional.of(task));
|
||||
when(taskAssignmentService.findJobForTask(task)).thenReturn(Optional.of(job));
|
||||
Map<String, Object> extra = Map.of("photos", List.of("image-data"), "barcodes", List.of("CODE-1"),
|
||||
"signatureSvg", "<svg/>", "commentText", "Late comment");
|
||||
controller.handleTaskCompleted(Map.of("taskId", task.getIdAsString(), "extraData", extra),
|
||||
task.getTaskType(), "driver-1");
|
||||
verify(taskRepository, never()).save(any());
|
||||
verifyNoInteractions(photoRepository, signatureRepository, barcodeRepository, commentRepository,
|
||||
emailService, jobHistoryService, jobUpdateBroadcaster);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user