Erweiterungen

This commit is contained in:
2025-08-28 09:26:31 +02:00
parent fe684b8f3a
commit bb8150d7f4

View File

@@ -17,10 +17,8 @@ import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.component.ClientCallable;
import com.vaadin.flow.component.textfield.IntegerField; import com.vaadin.flow.component.textfield.IntegerField;
import com.vaadin.flow.component.textfield.NumberField; import com.vaadin.flow.component.textfield.NumberField;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.data.binder.Binder; import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.router.Menu; import com.vaadin.flow.router.Menu;
import com.vaadin.flow.component.Component; import com.vaadin.flow.component.Component;
@@ -60,7 +58,7 @@ public class AddJobView extends Main {
private Button preloadAddressButton; private Button preloadAddressButton;
// Pickup address fields // Pickup address fields
private TextField pickupCompany; private ComboBox<String> pickupCompany;
private ComboBox<String> pickupSalutation; private ComboBox<String> pickupSalutation;
private TextField pickupFirstName; private TextField pickupFirstName;
private TextField pickupLastName; private TextField pickupLastName;
@@ -73,7 +71,7 @@ public class AddJobView extends Main {
private Checkbox savePickupAddress; private Checkbox savePickupAddress;
// Delivery address fields // Delivery address fields
private TextField deliveryCompany; private ComboBox<String> deliveryCompany;
private ComboBox<String> deliverySalutation; private ComboBox<String> deliverySalutation;
private TextField deliveryFirstName; private TextField deliveryFirstName;
private TextField deliveryLastName; private TextField deliveryLastName;
@@ -122,7 +120,7 @@ public class AddJobView extends Main {
private final Binder<Job> binder = new Binder<>(Job.class); private final Binder<Job> binder = new Binder<>(Job.class);
// Mapping für die Anzeige-Labels der Kunden zur Entität // Mapping für die Anzeige-Labels der Kunden zur Entität
private Map<String, Customer> customerLabelToEntity = new LinkedHashMap<>(); private final Map<String, Customer> customerLabelToEntity = new LinkedHashMap<>();
public AddJobView(AddJobService addJobService, AddCustomerService addCustomerService, CustomerService customerService) { public AddJobView(AddJobService addJobService, AddCustomerService addCustomerService, CustomerService customerService) {
this.addJobService = addJobService; this.addJobService = addJobService;
@@ -164,10 +162,17 @@ public class AddJobView extends Main {
// Bei Auswahl eines Kunden Abholfelder befüllen // Bei Auswahl eines Kunden Abholfelder befüllen
customerSelection.addValueChangeListener(ev -> { customerSelection.addValueChangeListener(ev -> {
String selected = ev.getValue(); String selected = ev.getValue();
if (selected == null) return; if (selected == null) {
// Wenn kein Kunde ausgewählt ist, Checkbox wieder aktivieren
savePickupAddress.setValue(true);
return;
}
Customer c = customerLabelToEntity.get(selected); Customer c = customerLabelToEntity.get(selected);
if (c == null) return; if (c == null) return;
// Pickup-Checkbox deaktivieren, da Kunde bereits existiert
savePickupAddress.setValue(false);
// Firma // Firma
if (c.getCompanyName() != null) { pickupCompany.setValue(c.getCompanyName()); } else { pickupCompany.clear(); } if (c.getCompanyName() != null) { pickupCompany.setValue(c.getCompanyName()); } else { pickupCompany.clear(); }
// Anrede (nur setzen, wenn vorhanden und zulässig) // Anrede (nur setzen, wenn vorhanden und zulässig)
@@ -194,9 +199,10 @@ public class AddJobView extends Main {
preloadAddressButton.addClickListener(event -> clearAllFields()); preloadAddressButton.addClickListener(event -> clearAllFields());
// Pickup address // Pickup address
pickupCompany = new TextField("Firma"); pickupCompany = new ComboBox<>("Firma");
pickupCompany.setPlaceholder("Firmenname"); pickupCompany.setPlaceholder("Firmenname");
addGooglePlacesAutocomplete(pickupCompany, 0); // Stage 0 für Pickup pickupCompany.setAllowCustomValue(true);
setupCompanyAutocomplete(pickupCompany, true); // true für Pickup
pickupSalutation = new ComboBox<>("Anrede"); pickupSalutation = new ComboBox<>("Anrede");
pickupSalutation.setItems("Herr", "Frau", "Divers"); pickupSalutation.setItems("Herr", "Frau", "Divers");
pickupSalutation.setPlaceholder("Anrede wählen..."); pickupSalutation.setPlaceholder("Anrede wählen...");
@@ -223,11 +229,13 @@ public class AddJobView extends Main {
pickupCity.setPlaceholder("Hamburg"); pickupCity.setPlaceholder("Hamburg");
pickupCity.setRequiredIndicatorVisible(true); pickupCity.setRequiredIndicatorVisible(true);
savePickupAddress = new Checkbox("Die Adresse für zukünftige Aufträge speichern."); savePickupAddress = new Checkbox("Die Adresse für zukünftige Aufträge speichern.");
savePickupAddress.setValue(true);
// Delivery address // Delivery address
deliveryCompany = new TextField("Firma"); deliveryCompany = new ComboBox<>("Firma");
deliveryCompany.setPlaceholder("Firmenname"); deliveryCompany.setPlaceholder("Firmenname");
addGooglePlacesAutocomplete(deliveryCompany, 1); // Stage 1 für Delivery deliveryCompany.setAllowCustomValue(true);
setupCompanyAutocomplete(deliveryCompany, false); // false für Delivery
deliverySalutation = new ComboBox<>("Anrede"); deliverySalutation = new ComboBox<>("Anrede");
deliverySalutation.setItems("Herr", "Frau", "Divers"); deliverySalutation.setItems("Herr", "Frau", "Divers");
deliverySalutation.setPlaceholder("Anrede wählen..."); deliverySalutation.setPlaceholder("Anrede wählen...");
@@ -254,6 +262,7 @@ public class AddJobView extends Main {
deliveryCity.setPlaceholder("Berlin"); deliveryCity.setPlaceholder("Berlin");
deliveryCity.setRequiredIndicatorVisible(true); deliveryCity.setRequiredIndicatorVisible(true);
saveDeliveryAddress = new Checkbox("Die Adresse für zukünftige Aufträge speichern."); saveDeliveryAddress = new Checkbox("Die Adresse für zukünftige Aufträge speichern.");
saveDeliveryAddress.setValue(true);
// Digital processing // Digital processing
digitalProcessing = new Checkbox("Digitale Abwicklung per App"); digitalProcessing = new Checkbox("Digitale Abwicklung per App");
@@ -576,6 +585,88 @@ public class AddJobView extends Main {
return section; return section;
} }
private void setupCompanyAutocomplete(ComboBox<String> companyField, boolean isPickup) {
// Get all customers for the current owner
List<Customer> allCustomers = customerService.findAllForCurrentOwner();
// Extract unique company names (filter out null/empty values)
List<String> companyNames = allCustomers.stream()
.map(Customer::getCompanyName)
.filter(name -> name != null && !name.trim().isEmpty())
.distinct()
.sorted()
.toList();
// Set items for autocomplete
companyField.setItems(companyNames);
// Add selection listener to auto-fill address fields when company is selected
companyField.addValueChangeListener(event -> {
String selectedCompany = event.getValue();
if (selectedCompany == null || selectedCompany.trim().isEmpty()) {
return;
}
// Find the first customer with this company name
Optional<Customer> matchingCustomer = allCustomers.stream()
.filter(c -> selectedCompany.equals(c.getCompanyName()))
.findFirst();
if (matchingCustomer.isPresent()) {
Customer customer = matchingCustomer.get();
if (isPickup) {
// Fill pickup address fields
if (customer.getTitle() != null && ("Herr".equalsIgnoreCase(customer.getTitle()) ||
"Frau".equalsIgnoreCase(customer.getTitle()) || "Divers".equalsIgnoreCase(customer.getTitle()))) {
pickupSalutation.setValue(customer.getTitle());
}
if (customer.getFirstname() != null) pickupFirstName.setValue(customer.getFirstname());
if (customer.getLastName() != null) pickupLastName.setValue(customer.getLastName());
if (customer.getTelephone() != null) pickupPhone.setValue(customer.getTelephone());
if (customer.getStreet() != null) pickupStreet.setValue(customer.getStreet());
if (customer.getHouseNumber() != null) pickupHouseNumber.setValue(customer.getHouseNumber());
if (customer.getAddressAddition() != null) pickupAddressAddition.setValue(customer.getAddressAddition());
if (customer.getZip() != null) pickupZip.setValue(customer.getZip());
if (customer.getCity() != null) pickupCity.setValue(customer.getCity());
// Deactivate save checkbox since customer already exists
savePickupAddress.setValue(false);
} else {
// Fill delivery address fields
if (customer.getTitle() != null && ("Herr".equalsIgnoreCase(customer.getTitle()) ||
"Frau".equalsIgnoreCase(customer.getTitle()) || "Divers".equalsIgnoreCase(customer.getTitle()))) {
deliverySalutation.setValue(customer.getTitle());
}
if (customer.getFirstname() != null) deliveryFirstName.setValue(customer.getFirstname());
if (customer.getLastName() != null) deliveryLastName.setValue(customer.getLastName());
if (customer.getTelephone() != null) deliveryPhone.setValue(customer.getTelephone());
if (customer.getStreet() != null) deliveryStreet.setValue(customer.getStreet());
if (customer.getHouseNumber() != null) deliveryHouseNumber.setValue(customer.getHouseNumber());
if (customer.getAddressAddition() != null) deliveryAddressAddition.setValue(customer.getAddressAddition());
if (customer.getZip() != null) deliveryZip.setValue(customer.getZip());
if (customer.getCity() != null) deliveryCity.setValue(customer.getCity());
// Deactivate save checkbox since customer already exists
saveDeliveryAddress.setValue(false);
}
}
});
// Handle custom values (when user types something not in the list)
companyField.addCustomValueSetListener(event -> {
String customValue = event.getDetail();
companyField.setValue(customValue);
// Reactivate save checkbox for custom values
if (isPickup) {
savePickupAddress.setValue(true);
} else {
saveDeliveryAddress.setValue(true);
}
});
}
private void setupValidation() { private void setupValidation() {
// Bind pickup address fields with validation // Bind pickup address fields with validation
binder.forField(pickupFirstName) binder.forField(pickupFirstName)
@@ -773,7 +864,7 @@ public class AddJobView extends Main {
// Check validation state for each tab and update labels with exclamation marks // Check validation state for each tab and update labels with exclamation marks
updateTabLabel(addressesTab, "Auftraggeber & Adressen", hasAddressValidationErrors()); updateTabLabel(addressesTab, "Auftraggeber & Adressen", hasAddressValidationErrors());
updateTabLabel(appointmentsTab, "Termine & Verarbeitung", hasAppointmentValidationErrors()); updateTabLabel(appointmentsTab, "Termine & Verarbeitung", hasAppointmentValidationErrors());
updateTabLabel(cargoTab, "Ladung & Aufgaben", false); // No required fields in cargo tab updateTabLabel(cargoTab, "Ladung & Aufgaben", hasCargoValidationErrors());
updateTabLabel(priceTab, "Preis & Abschluss", hasPriceValidationErrors()); updateTabLabel(priceTab, "Preis & Abschluss", hasPriceValidationErrors());
} }
@@ -806,6 +897,27 @@ public class AddJobView extends Main {
return pickupDate.getValue() == null || deliveryDate.getValue() == null; return pickupDate.getValue() == null || deliveryDate.getValue() == null;
} }
private boolean hasCargoValidationErrors() {
// Check if ALL cargo items have all required fields filled
// When multiple cargo rows exist, ALL must be complete
if (cargoItemsState.isEmpty()) {
return true; // No cargo items at all - show warning
}
// Check that ALL cargo items are complete
// A cargo item is considered complete if it has: Description, Quantity, Weight, Length, Width, Height
boolean allCargoItemsValid = cargoItemsState.stream()
.allMatch(cargoItem -> cargoItem != null &&
cargoItem.getDescription() != null && !cargoItem.getDescription().trim().isEmpty() &&
cargoItem.getQuantity() != null && cargoItem.getQuantity() > 0 &&
cargoItem.getWeightKg() != null && cargoItem.getWeightKg() > 0 &&
cargoItem.getLengthMm() != null && cargoItem.getLengthMm() > 0 &&
cargoItem.getWidthMm() != null && cargoItem.getWidthMm() > 0 &&
cargoItem.getHeightMm() != null && cargoItem.getHeightMm() > 0);
return !allCargoItemsValid; // Return true if ANY cargo item is incomplete (show warning)
}
private boolean hasPriceValidationErrors() { private boolean hasPriceValidationErrors() {
return isFieldEmpty(price); return isFieldEmpty(price);
} }
@@ -840,7 +952,7 @@ public class AddJobView extends Main {
return; return;
} }
// toggle cargo error highlight // toggle cargo error highlight
boolean hasCargo = !cargoFilled.isEmpty(); boolean hasCargo = true;
cargoError.setVisible(!hasCargo); cargoError.setVisible(!hasCargo);
if (!hasCargo) { if (!hasCargo) {
cargoAreaContainer.getStyle().set("border", "1px solid var(--lumo-error-color-50pct)"); cargoAreaContainer.getStyle().set("border", "1px solid var(--lumo-error-color-50pct)");
@@ -1054,6 +1166,7 @@ public class AddJobView extends Main {
cargoItemsState.remove(idx); cargoItemsState.remove(idx);
} }
cargoList.remove(row); cargoList.remove(row);
updateTabLabels(); // Update tab validation when cargo item is removed
}); });
row.add(desc, qty, weight, len, wid, hei, remove); row.add(desc, qty, weight, len, wid, hei, remove);
@@ -1078,20 +1191,43 @@ public class AddJobView extends Main {
item.setWidthMm(wid.getValue()); item.setWidthMm(wid.getValue());
item.setHeightMm(hei.getValue()); item.setHeightMm(hei.getValue());
desc.addValueChangeListener(ev -> item.setDescription(ev.getValue())); desc.addValueChangeListener(ev -> {
qty.addValueChangeListener(ev -> item.setQuantity(ev.getValue())); item.setDescription(ev.getValue());
weight.addValueChangeListener(ev -> item.setWeightKg(ev.getValue())); updateTabLabels(); // Update tab validation when cargo description changes
len.addValueChangeListener(ev -> item.setLengthMm(ev.getValue())); });
wid.addValueChangeListener(ev -> item.setWidthMm(ev.getValue())); qty.addValueChangeListener(ev -> {
hei.addValueChangeListener(ev -> item.setHeightMm(ev.getValue())); item.setQuantity(ev.getValue());
updateTabLabels(); // Update tab validation when cargo quantity changes
});
weight.addValueChangeListener(ev -> {
item.setWeightKg(ev.getValue());
updateTabLabels(); // Update tab validation when cargo weight changes
});
len.addValueChangeListener(ev -> {
item.setLengthMm(ev.getValue());
updateTabLabels(); // Update tab validation when cargo length changes
});
wid.addValueChangeListener(ev -> {
item.setWidthMm(ev.getValue());
updateTabLabels(); // Update tab validation when cargo width changes
});
hei.addValueChangeListener(ev -> {
item.setHeightMm(ev.getValue());
updateTabLabels(); // Update tab validation when cargo height changes
});
if (afterCreate != null) afterCreate.accept(row); if (afterCreate != null) afterCreate.accept(row);
}; };
addCargoRow.accept("gitterbox", r -> {}); addCargoRow.accept("", r -> {}); // Show only one empty row by default
addCargoRow.accept("paket", r -> {});
addCargoRow.accept("", r -> {});
// Add button to add more cargo rows
Button addCargoButton = new Button("Ladung hinzufügen", new Icon(VaadinIcon.PLUS));
addCargoButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
addCargoButton.setWidthFull(); // Make button full width of container
addCargoButton.addClickListener(e -> addCargoRow.accept("", r -> {}));
cargoAreaContainer.add(addCargoButton);
wrapper.add(cargoAreaContainer); wrapper.add(cargoAreaContainer);
return wrapper; return wrapper;
} }
@@ -1226,80 +1362,6 @@ public class AddJobView extends Main {
Notification.show("Alle Felder wurden geleert", 2000, Notification.Position.BOTTOM_CENTER); Notification.show("Alle Felder wurden geleert", 2000, Notification.Position.BOTTOM_CENTER);
} }
/**
* Fügt Google Places Autocomplete zu einem TextField hinzu
*/
private void addGooglePlacesAutocomplete(TextField textField, int stageIndex) {
// Initialisierung als Runnable kapseln
Runnable initAutocomplete = () -> {
// Google Places API Script laden (falls noch nicht geladen)
UI.getCurrent().getPage().addJavaScript("https://maps.googleapis.com/maps/api/js?key=AIzaSyDnbitL06iLp3elmj-WtPudCykX9xvXcVE&libraries=places");
// JavaScript für Google Places Autocomplete - nutzt direkt das Input-Element
String script = """
setTimeout(function() {
var host = $0; // Vaadin TextField element
var input = host && host.inputElement ? host.inputElement : (host && host.shadowRoot ? host.shadowRoot.querySelector('input') : null);
if (input && window.google && window.google.maps && window.google.maps.places) {
var autocomplete = new google.maps.places.Autocomplete(input, {
types: ['establishment'],
componentRestrictions: {country: 'de'}
});
autocomplete.addListener('place_changed', function() {
var place = autocomplete.getPlace();
if (place && place.address_components) {
var streetNumber = '';
var route = '';
var locality = '';
var postalCode = '';
var country = '';
var companyName = place.name || '';
for (var i = 0; i < place.address_components.length; i++) {
var component = place.address_components[i];
var types = component.types || [];
if (types.indexOf('street_number') !== -1) {
streetNumber = component.long_name;
} else if (types.indexOf('route') !== -1) {
route = component.long_name;
} else if (types.indexOf('locality') !== -1) {
locality = component.long_name;
} else if (types.indexOf('postal_code') !== -1) {
postalCode = component.long_name;
} else if (types.indexOf('country') !== -1) {
country = component.long_name;
}
}
// Setze den Firmennamen in das Feld (sichtbar und Vaadin-Wert)
input.value = companyName || '';
host.value = companyName || '';
// Sende Daten an Server mit Stage-Index
$1.$server.handleGooglePlaceSelected(%d, companyName, route, streetNumber, postalCode, locality, country);
}
});
} else {
setTimeout(arguments.callee, 500);
}
}, 500);
""".formatted(stageIndex);
// executeJs mit Referenzen: $0 => input element (shadowRoot), $1 => Server (this)
textField.getElement().executeJs(script, textField.getElement(), getElement());
log.debug("Google Places Autocomplete initialisiert (Stage {})", stageIndex);
};
// Wenn UI bereits existiert, direkt initialisieren; sonst beim Attach initialisieren
textField.getUI().ifPresentOrElse(
ui -> ui.access((com.vaadin.flow.server.Command) initAutocomplete::run),
() -> textField.addAttachListener(event -> event.getUI().access((com.vaadin.flow.server.Command) initAutocomplete::run))
);
}
/** /**
* Konfiguriert Focus-Listener für alle Eingabefelder um Drag-and-Drop zu steuern * Konfiguriert Focus-Listener für alle Eingabefelder um Drag-and-Drop zu steuern
@@ -1386,53 +1448,4 @@ public class AddJobView extends Main {
} }
} }
/**
* Handler für Google Places Auswahl - wird vom JavaScript aufgerufen
*/
@ClientCallable
public void handleGooglePlaceSelected(int stageIndex, String companyName, String route, String streetNumber, String postalCode, String locality, String country) {
log.debug("Google Place ausgewählt - Stage: {}, Company: {}, Route: {}, StreetNumber: {}, PostalCode: {}, Locality: {}, Country: {}",
stageIndex, companyName, route, streetNumber, postalCode, locality, country);
if (stageIndex == 0) {
// Pickup address
// Firmenname wird bereits durch das TextField selbst gesetzt
if (route != null && !route.isEmpty()) {
pickupStreet.setValue(route);
}
if (streetNumber != null && !streetNumber.isEmpty()) {
pickupHouseNumber.setValue(streetNumber);
}
if (postalCode != null && !postalCode.isEmpty()) {
pickupZip.setValue(postalCode);
}
if (locality != null && !locality.isEmpty()) {
pickupCity.setValue(locality);
}
// Notification für Benutzer
Notification.show("Abholadresse automatisch ausgefüllt: " + (companyName != null ? companyName : ""),
3000, Notification.Position.BOTTOM_CENTER);
} else if (stageIndex == 1) {
// Delivery address
// Firmenname wird bereits durch das TextField selbst gesetzt
if (route != null && !route.isEmpty()) {
deliveryStreet.setValue(route);
}
if (streetNumber != null && !streetNumber.isEmpty()) {
deliveryHouseNumber.setValue(streetNumber);
}
if (postalCode != null && !postalCode.isEmpty()) {
deliveryZip.setValue(postalCode);
}
if (locality != null && !locality.isEmpty()) {
deliveryCity.setValue(locality);
}
// Notification für Benutzer
Notification.show("Lieferadresse automatisch ausgefüllt: " + (companyName != null ? companyName : ""),
3000, Notification.Position.BOTTOM_CENTER);
}
}
} }