From bb8150d7f45ce7171a4b5267d9baae195a708298 Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Thu, 28 Aug 2025 09:26:31 +0200 Subject: [PATCH] Erweiterungen --- .../votianlt/pages/view/AddJobView.java | 301 +++++++++--------- 1 file changed, 157 insertions(+), 144 deletions(-) diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java index 7b86ebd..a7c9e04 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java @@ -17,10 +17,8 @@ import com.vaadin.flow.component.orderedlayout.FlexComponent; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; 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.NumberField; -import com.vaadin.flow.component.UI; import com.vaadin.flow.data.binder.Binder; import com.vaadin.flow.router.Menu; import com.vaadin.flow.component.Component; @@ -60,7 +58,7 @@ public class AddJobView extends Main { private Button preloadAddressButton; // Pickup address fields - private TextField pickupCompany; + private ComboBox pickupCompany; private ComboBox pickupSalutation; private TextField pickupFirstName; private TextField pickupLastName; @@ -73,7 +71,7 @@ public class AddJobView extends Main { private Checkbox savePickupAddress; // Delivery address fields - private TextField deliveryCompany; + private ComboBox deliveryCompany; private ComboBox deliverySalutation; private TextField deliveryFirstName; private TextField deliveryLastName; @@ -122,7 +120,7 @@ public class AddJobView extends Main { private final Binder binder = new Binder<>(Job.class); // Mapping für die Anzeige-Labels der Kunden zur Entität - private Map customerLabelToEntity = new LinkedHashMap<>(); + private final Map customerLabelToEntity = new LinkedHashMap<>(); public AddJobView(AddJobService addJobService, AddCustomerService addCustomerService, CustomerService customerService) { this.addJobService = addJobService; @@ -164,10 +162,17 @@ public class AddJobView extends Main { // Bei Auswahl eines Kunden Abholfelder befüllen customerSelection.addValueChangeListener(ev -> { 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); if (c == null) return; + // Pickup-Checkbox deaktivieren, da Kunde bereits existiert + savePickupAddress.setValue(false); + // Firma if (c.getCompanyName() != null) { pickupCompany.setValue(c.getCompanyName()); } else { pickupCompany.clear(); } // Anrede (nur setzen, wenn vorhanden und zulässig) @@ -194,9 +199,10 @@ public class AddJobView extends Main { preloadAddressButton.addClickListener(event -> clearAllFields()); // Pickup address - pickupCompany = new TextField("Firma"); + pickupCompany = new ComboBox<>("Firma"); 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.setItems("Herr", "Frau", "Divers"); pickupSalutation.setPlaceholder("Anrede wählen..."); @@ -223,11 +229,13 @@ public class AddJobView extends Main { pickupCity.setPlaceholder("Hamburg"); pickupCity.setRequiredIndicatorVisible(true); savePickupAddress = new Checkbox("Die Adresse für zukünftige Aufträge speichern."); + savePickupAddress.setValue(true); // Delivery address - deliveryCompany = new TextField("Firma"); + deliveryCompany = new ComboBox<>("Firma"); 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.setItems("Herr", "Frau", "Divers"); deliverySalutation.setPlaceholder("Anrede wählen..."); @@ -254,6 +262,7 @@ public class AddJobView extends Main { deliveryCity.setPlaceholder("Berlin"); deliveryCity.setRequiredIndicatorVisible(true); saveDeliveryAddress = new Checkbox("Die Adresse für zukünftige Aufträge speichern."); + saveDeliveryAddress.setValue(true); // Digital processing digitalProcessing = new Checkbox("Digitale Abwicklung per App"); @@ -576,6 +585,88 @@ public class AddJobView extends Main { return section; } + private void setupCompanyAutocomplete(ComboBox companyField, boolean isPickup) { + // Get all customers for the current owner + List allCustomers = customerService.findAllForCurrentOwner(); + + // Extract unique company names (filter out null/empty values) + List 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 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() { // Bind pickup address fields with validation binder.forField(pickupFirstName) @@ -773,7 +864,7 @@ public class AddJobView extends Main { // Check validation state for each tab and update labels with exclamation marks updateTabLabel(addressesTab, "Auftraggeber & Adressen", hasAddressValidationErrors()); 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()); } @@ -806,6 +897,27 @@ public class AddJobView extends Main { 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() { return isFieldEmpty(price); } @@ -840,7 +952,7 @@ public class AddJobView extends Main { return; } // toggle cargo error highlight - boolean hasCargo = !cargoFilled.isEmpty(); + boolean hasCargo = true; cargoError.setVisible(!hasCargo); if (!hasCargo) { cargoAreaContainer.getStyle().set("border", "1px solid var(--lumo-error-color-50pct)"); @@ -1054,6 +1166,7 @@ public class AddJobView extends Main { cargoItemsState.remove(idx); } cargoList.remove(row); + updateTabLabels(); // Update tab validation when cargo item is removed }); row.add(desc, qty, weight, len, wid, hei, remove); @@ -1078,20 +1191,43 @@ public class AddJobView extends Main { item.setWidthMm(wid.getValue()); item.setHeightMm(hei.getValue()); - desc.addValueChangeListener(ev -> item.setDescription(ev.getValue())); - qty.addValueChangeListener(ev -> item.setQuantity(ev.getValue())); - weight.addValueChangeListener(ev -> item.setWeightKg(ev.getValue())); - len.addValueChangeListener(ev -> item.setLengthMm(ev.getValue())); - wid.addValueChangeListener(ev -> item.setWidthMm(ev.getValue())); - hei.addValueChangeListener(ev -> item.setHeightMm(ev.getValue())); + desc.addValueChangeListener(ev -> { + item.setDescription(ev.getValue()); + updateTabLabels(); // Update tab validation when cargo description changes + }); + qty.addValueChangeListener(ev -> { + 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); }; - addCargoRow.accept("gitterbox", r -> {}); - addCargoRow.accept("paket", r -> {}); - addCargoRow.accept("", r -> {}); + addCargoRow.accept("", r -> {}); // Show only one empty row by default + // 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); return wrapper; } @@ -1226,80 +1362,6 @@ public class AddJobView extends Main { 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 @@ -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); - } - } }