diff --git a/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationDialog.java b/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationDialog.java index 580c53c..780c117 100644 --- a/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationDialog.java +++ b/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationDialog.java @@ -14,6 +14,7 @@ import com.vaadin.flow.component.notification.Notification; 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.tabs.Tab; import com.vaadin.flow.component.tabs.TabSheet; import com.vaadin.flow.component.textfield.IntegerField; import com.vaadin.flow.component.textfield.TextField; @@ -171,6 +172,9 @@ public class DeliveryStationDialog extends Dialog { private final List tasksState = new ArrayList<>(); private VerticalLayout tasksList; + private Span addressTabError; + private Span tasksTabError; + private final DeliveryStationTile.TranslationHelper translationHelper; public DeliveryStationDialog(String dialogTitle, List customers, @@ -181,7 +185,7 @@ public class DeliveryStationDialog extends Dialog { setHeaderTitle(dialogTitle); setCloseOnOutsideClick(false); - setWidth("800px"); + setWidth("960px"); setHeight("80vh"); // Address form @@ -274,19 +278,37 @@ public class DeliveryStationDialog extends Dialog { saveAddress.setWidthFull(); formLayout.add(saveAddress); + // Clear error styling on value change for required fields and update tab indicators + firstName.addValueChangeListener(ev -> validateRequiredFields()); + lastName.addValueChangeListener(ev -> validateRequiredFields()); + street.addValueChangeListener(ev -> validateRequiredFields()); + houseNumber.addValueChangeListener(ev -> validateRequiredFields()); + zip.addValueChangeListener(ev -> validateRequiredFields()); + city.addValueChangeListener(ev -> validateRequiredFields()); + // TabSheet with address and tasks tabs TabSheet tabSheet = new TabSheet(); tabSheet.setWidthFull(); tabSheet.setSizeFull(); - tabSheet.add(translationHelper.getTranslation("addjob.tab.addresses"), formLayout); - tabSheet.add(translationHelper.getTranslation("addjob.tab.tasks"), + addressTabError = createTabErrorIndicator(); + tasksTabError = createTabErrorIndicator(); + + Tab addressTab = tabSheet.add(translationHelper.getTranslation("addjob.tab.addresses"), formLayout); + addressTab.add(addressTabError); + Tab tasksTab = tabSheet.add(translationHelper.getTranslation("addjob.tab.tasks"), createTasksTab(templates, templateSaveCallback)); + tasksTab.add(tasksTabError); add(tabSheet); // Footer buttons Button saveButton = new Button(translationHelper.getTranslation("dialog.confirm"), e -> { + if (!validateRequiredFields()) { + Notification.show(translationHelper.getTranslation("addjob.validation.required.fields"), 3000, + Notification.Position.BOTTOM_END); + return; + } DeliveryData data = collectData(); if (saveListener != null) { saveListener.onSave(data); @@ -298,6 +320,12 @@ public class DeliveryStationDialog extends Dialog { Button cancelButton = new Button(translationHelper.getTranslation("dialog.cancel"), e -> close()); getFooter().add(cancelButton, saveButton); + + addOpenedChangeListener(event -> { + if (event.isOpened()) { + validateRequiredFields(); + } + }); } /** @@ -363,6 +391,69 @@ public class DeliveryStationDialog extends Dialog { return data; } + private boolean validateRequiredFields() { + // Address tab validation + boolean addressValid = true; + addressValid &= validateTextField(firstName); + addressValid &= validateTextField(lastName); + addressValid &= validateTextField(street); + addressValid &= validateTextField(houseNumber); + addressValid &= validateTextField(zip); + addressValid &= validateTextField(city); + addressTabError.setVisible(!addressValid); + + // Tasks tab validation + boolean tasksValid = validateTasks(); + tasksTabError.setVisible(!tasksValid); + + return addressValid && tasksValid; + } + + private boolean validateTasks() { + boolean valid = true; + for (BaseTask task : tasksState) { + if (task instanceof ConfirmationTask ct) { + if (ct.getDescription() == null || ct.getDescription().trim().isEmpty()) { + valid = false; + } + if (ct.getButtonText() == null || ct.getButtonText().trim().isEmpty()) { + valid = false; + } + } else if (task instanceof TodoListTask tt) { + if (tt.getTodoItems() == null || tt.getTodoItems().isEmpty() + || tt.getTodoItems().stream().allMatch(item -> item == null || item.trim().isEmpty())) { + valid = false; + } + } + } + return valid; + } + + private boolean validateTextField(TextField field) { + boolean empty = field.getValue() == null || field.getValue().trim().isEmpty(); + applyErrorStyling(field, empty); + return !empty; + } + + private void applyErrorStyling(com.vaadin.flow.component.Component field, boolean error) { + if (error) { + field.getElement().getStyle().set("--vaadin-input-field-background", "rgba(255, 0, 0, 0.1)"); + field.getElement().getStyle().set("--vaadin-input-field-border-color", "rgba(255, 0, 0, 0.3)"); + } else { + field.getElement().getStyle().remove("--vaadin-input-field-background"); + field.getElement().getStyle().remove("--vaadin-input-field-border-color"); + } + } + + private Span createTabErrorIndicator() { + Span indicator = new Span(" !"); + indicator.getStyle().set("color", "var(--lumo-error-color)"); + indicator.getStyle().set("font-weight", "bold"); + indicator.getStyle().set("margin-left", "4px"); + indicator.setVisible(false); + return indicator; + } + private void setupCompanyAutocomplete(ComboBox companyField, List customers) { List companyNames = customers.stream().map(Customer::getCompanyName) .filter(name -> name != null && !name.trim().isEmpty()).distinct().sorted().toList(); @@ -713,14 +804,8 @@ public class DeliveryStationDialog extends Dialog { descriptionField.setValue(task.getDescription() != null ? task.getDescription() : ""); descriptionField.addValueChangeListener(ev -> { task.setDescription(ev.getValue()); - boolean isEmpty = ev.getValue() == null || ev.getValue().trim().isEmpty(); - if (isEmpty) { - descriptionField.getStyle().set("--vaadin-input-field-background", "rgba(255, 0, 0, 0.1)"); - descriptionField.getStyle().set("--vaadin-input-field-border-color", "rgba(255, 0, 0, 0.3)"); - } else { - descriptionField.getStyle().remove("--vaadin-input-field-background"); - descriptionField.getStyle().remove("--vaadin-input-field-border-color"); - } + applyErrorStyling(descriptionField, ev.getValue() == null || ev.getValue().trim().isEmpty()); + validateRequiredFields(); }); if (task.getDescription() == null || task.getDescription().trim().isEmpty()) { descriptionField.getStyle().set("--vaadin-input-field-background", "rgba(255, 0, 0, 0.1)"); @@ -735,14 +820,8 @@ public class DeliveryStationDialog extends Dialog { buttonTextField.setValue(confirmationTask.getButtonText() != null ? confirmationTask.getButtonText() : ""); buttonTextField.addValueChangeListener(ev -> { confirmationTask.setButtonText(ev.getValue()); - boolean isEmpty = ev.getValue() == null || ev.getValue().trim().isEmpty(); - if (isEmpty) { - buttonTextField.getStyle().set("--vaadin-input-field-background", "rgba(255, 0, 0, 0.1)"); - buttonTextField.getStyle().set("--vaadin-input-field-border-color", "rgba(255, 0, 0, 0.3)"); - } else { - buttonTextField.getStyle().remove("--vaadin-input-field-background"); - buttonTextField.getStyle().remove("--vaadin-input-field-border-color"); - } + applyErrorStyling(buttonTextField, ev.getValue() == null || ev.getValue().trim().isEmpty()); + validateRequiredFields(); }); if (confirmationTask.getButtonText() == null || confirmationTask.getButtonText().trim().isEmpty()) { buttonTextField.getStyle().set("--vaadin-input-field-background", "rgba(255, 0, 0, 0.1)"); @@ -807,6 +886,7 @@ public class DeliveryStationDialog extends Dialog { todoField.addValueChangeListener(ev -> { updateTodoFieldStyling.accept(todoField); updateTodoItems(todoList, task); + validateRequiredFields(); }); }; @@ -1080,6 +1160,7 @@ public class DeliveryStationDialog extends Dialog { } templateComboBox.clear(); + validateRequiredFields(); Notification.show( translationHelper.getTranslation("addjob.tasks.template.loaded", template.getTemplateName()), 3000, diff --git a/src/main/java/de/assecutor/votianlt/pages/base/ui/component/PickupStationDialog.java b/src/main/java/de/assecutor/votianlt/pages/base/ui/component/PickupStationDialog.java index 4ac128e..c5ebfa0 100644 --- a/src/main/java/de/assecutor/votianlt/pages/base/ui/component/PickupStationDialog.java +++ b/src/main/java/de/assecutor/votianlt/pages/base/ui/component/PickupStationDialog.java @@ -13,11 +13,13 @@ import com.vaadin.flow.component.icon.VaadinIcon; 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.tabs.Tab; import com.vaadin.flow.component.tabs.TabSheet; import com.vaadin.flow.component.textfield.IntegerField; import com.vaadin.flow.component.textfield.NumberField; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.timepicker.TimePicker; +import com.vaadin.flow.component.notification.Notification; import de.assecutor.votianlt.model.AppUser; import de.assecutor.votianlt.model.CargoItem; import de.assecutor.votianlt.model.Customer; @@ -219,6 +221,10 @@ public class PickupStationDialog extends Dialog { private final List cargoItemsState = new ArrayList<>(); private VerticalLayout cargoList; + private Span addressTabError; + private Span appointmentsTabError; + private Span cargoTabError; + private final DeliveryStationTile.TranslationHelper translationHelper; public PickupStationDialog(String dialogTitle, List customers, @@ -229,7 +235,7 @@ public class PickupStationDialog extends Dialog { setHeaderTitle(dialogTitle); setCloseOnOutsideClick(false); - setWidth("800px"); + setWidth("960px"); setHeight("80vh"); // Address form @@ -339,6 +345,14 @@ public class PickupStationDialog extends Dialog { saveAddress.setValue(true); saveAddress.setWidthFull(); + // Clear error styling on value change for required fields and update tab indicators + firstName.addValueChangeListener(ev -> validateRequiredFields()); + lastName.addValueChangeListener(ev -> validateRequiredFields()); + street.addValueChangeListener(ev -> validateRequiredFields()); + houseNumber.addValueChangeListener(ev -> validateRequiredFields()); + zip.addValueChangeListener(ev -> validateRequiredFields()); + city.addValueChangeListener(ev -> validateRequiredFields()); + // Customer selection fills address fields customerComboBox.addValueChangeListener(ev -> { String selected = ev.getValue(); @@ -399,15 +413,27 @@ public class PickupStationDialog extends Dialog { tabSheet.setWidthFull(); tabSheet.setSizeFull(); - tabSheet.add(translationHelper.getTranslation("addjob.tab.addresses"), formLayout); - tabSheet.add(translationHelper.getTranslation("addjob.tab.appointments"), + addressTabError = createTabErrorIndicator(); + appointmentsTabError = createTabErrorIndicator(); + cargoTabError = createTabErrorIndicator(); + + Tab addressTab = tabSheet.add(translationHelper.getTranslation("addjob.tab.addresses"), formLayout); + addressTab.add(addressTabError); + Tab appointmentsTab = tabSheet.add(translationHelper.getTranslation("addjob.tab.appointments"), createAppointmentsTab(availableAppUsers)); - tabSheet.add(translationHelper.getTranslation("addjob.tab.cargo"), createCargoTab()); + appointmentsTab.add(appointmentsTabError); + Tab cargoTab = tabSheet.add(translationHelper.getTranslation("addjob.tab.cargo"), createCargoTab()); + cargoTab.add(cargoTabError); add(tabSheet); // Footer buttons Button saveButton = new Button(translationHelper.getTranslation("dialog.confirm"), e -> { + if (!validateRequiredFields()) { + Notification.show(translationHelper.getTranslation("addjob.validation.required.fields"), 3000, + Notification.Position.BOTTOM_END); + return; + } PickupData data = collectData(); if (saveListener != null) { saveListener.onSave(data); @@ -419,6 +445,12 @@ public class PickupStationDialog extends Dialog { Button cancelButton = new Button(translationHelper.getTranslation("dialog.cancel"), e -> close()); getFooter().add(cancelButton, saveButton); + + addOpenedChangeListener(event -> { + if (event.isOpened()) { + validateRequiredFields(); + } + }); } /** @@ -501,6 +533,92 @@ public class PickupStationDialog extends Dialog { return data; } + private boolean validateRequiredFields() { + // Address tab validation + boolean addressValid = true; + addressValid &= validateTextField(firstName); + addressValid &= validateTextField(lastName); + addressValid &= validateTextField(street); + addressValid &= validateTextField(houseNumber); + addressValid &= validateTextField(zip); + addressValid &= validateTextField(city); + addressTabError.setVisible(!addressValid); + + // Appointments tab validation + boolean appointmentsValid = true; + if (appointmentDatePicker != null) { + boolean dateEmpty = appointmentDatePicker.getValue() == null; + applyErrorStyling(appointmentDatePicker, dateEmpty); + appointmentsValid &= !dateEmpty; + } + if (Boolean.TRUE.equals(digitalProcessingCheckbox.getValue()) && appUserComboBox != null) { + boolean appUserEmpty = appUserComboBox.getValue() == null; + applyErrorStyling(appUserComboBox, appUserEmpty); + appointmentsValid &= !appUserEmpty; + } + appointmentsTabError.setVisible(!appointmentsValid); + + // Cargo tab validation + boolean cargoValid = validateCargoItems(); + cargoTabError.setVisible(!cargoValid); + + return addressValid && appointmentsValid && cargoValid; + } + + private boolean validateTextField(TextField field) { + boolean empty = field.getValue() == null || field.getValue().trim().isEmpty(); + applyErrorStyling(field, empty); + return !empty; + } + + private boolean validateCargoItems() { + boolean valid = true; + if (cargoList == null) + return true; + for (com.vaadin.flow.component.Component rowComp : cargoList.getChildren().toList()) { + if (rowComp instanceof HorizontalLayout row) { + for (com.vaadin.flow.component.Component field : row.getChildren().toList()) { + if (field instanceof ComboBox combo && combo.isRequiredIndicatorVisible()) { + boolean empty = combo.getValue() == null || combo.getValue().toString().trim().isEmpty(); + applyErrorStyling(combo, empty); + if (empty) + valid = false; + } else if (field instanceof IntegerField intField && intField.isRequiredIndicatorVisible()) { + boolean empty = intField.getValue() == null; + applyErrorStyling(intField, empty); + if (empty) + valid = false; + } else if (field instanceof NumberField numField && numField.isRequiredIndicatorVisible()) { + boolean empty = numField.getValue() == null; + applyErrorStyling(numField, empty); + if (empty) + valid = false; + } + } + } + } + return valid; + } + + private void applyErrorStyling(com.vaadin.flow.component.Component field, boolean error) { + if (error) { + field.getElement().getStyle().set("--vaadin-input-field-background", "rgba(255, 0, 0, 0.1)"); + field.getElement().getStyle().set("--vaadin-input-field-border-color", "rgba(255, 0, 0, 0.3)"); + } else { + field.getElement().getStyle().remove("--vaadin-input-field-background"); + field.getElement().getStyle().remove("--vaadin-input-field-border-color"); + } + } + + private Span createTabErrorIndicator() { + Span indicator = new Span(" !"); + indicator.getStyle().set("color", "var(--lumo-error-color)"); + indicator.getStyle().set("font-weight", "bold"); + indicator.getStyle().set("margin-left", "4px"); + indicator.setVisible(false); + return indicator; + } + private void setupCompanyAutocomplete(ComboBox companyField, List customers) { List companyNames = customers.stream().map(Customer::getCompanyName) .filter(name -> name != null && !name.trim().isEmpty()).distinct().sorted().toList(); @@ -581,6 +699,7 @@ public class PickupStationDialog extends Dialog { appUserComboBox.setItemLabelGenerator( user -> user.getVorname() + " " + user.getNachname() + " (" + user.getEmail() + ")"); appUserComboBox.setPlaceholder(translationHelper.getTranslation("addjob.appuser.placeholder")); + appUserComboBox.addValueChangeListener(ev -> validateRequiredFields()); content.add(digitalRow, appUserComboBox); @@ -592,6 +711,7 @@ public class PickupStationDialog extends Dialog { if (!required) { appUserComboBox.clear(); } + validateRequiredFields(); }); boolean digitalInitial = Boolean.TRUE.equals(digitalProcessingCheckbox.getValue()); appUserComboBox.setRequiredIndicatorVisible(digitalInitial); @@ -615,6 +735,8 @@ public class PickupStationDialog extends Dialog { appointmentTimePicker = new TimePicker(translationHelper.getTranslation("addjob.appointment.time")); appointmentTimePicker.setLocale(java.util.Locale.GERMANY); + appointmentDatePicker.addValueChangeListener(ev -> validateRequiredFields()); + HorizontalLayout pickupApptRow = new HorizontalLayout(appointmentDatePicker, appointmentTimePicker); pickupApptRow.setWidthFull(); pickupApptRow.setSpacing(true); @@ -768,11 +890,35 @@ public class PickupStationDialog extends Dialog { cargoItemsState.add(item); // Bind change listeners - 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()); + applyErrorStyling(desc, ev.getValue() == null || ev.getValue().trim().isEmpty()); + validateRequiredFields(); + }); + qty.addValueChangeListener(ev -> { + item.setQuantity(ev.getValue()); + applyErrorStyling(qty, ev.getValue() == null); + validateRequiredFields(); + }); + weight.addValueChangeListener(ev -> { + item.setWeightKg(ev.getValue()); + applyErrorStyling(weight, ev.getValue() == null); + validateRequiredFields(); + }); + len.addValueChangeListener(ev -> { + item.setLengthMm(ev.getValue()); + applyErrorStyling(len, ev.getValue() == null); + validateRequiredFields(); + }); + wid.addValueChangeListener(ev -> { + item.setWidthMm(ev.getValue()); + applyErrorStyling(wid, ev.getValue() == null); + validateRequiredFields(); + }); + hei.addValueChangeListener(ev -> { + item.setHeightMm(ev.getValue()); + applyErrorStyling(hei, ev.getValue() == null); + validateRequiredFields(); + }); } } diff --git a/src/main/java/de/assecutor/votianlt/pages/base/ui/component/StationTile.java b/src/main/java/de/assecutor/votianlt/pages/base/ui/component/StationTile.java index 74bc38f..772763c 100644 --- a/src/main/java/de/assecutor/votianlt/pages/base/ui/component/StationTile.java +++ b/src/main/java/de/assecutor/votianlt/pages/base/ui/component/StationTile.java @@ -55,14 +55,20 @@ public class StationTile extends VerticalLayout { HorizontalLayout titleLayout = new HorizontalLayout(); titleLayout.setWidthFull(); - titleLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER); + titleLayout.setPadding(false); + titleLayout.setSpacing(false); + titleLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.START); titleLayout.add(title); if (removable) { Button deleteButton = new Button(new Icon(VaadinIcon.CLOSE_SMALL)); - deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY); + deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY, + ButtonVariant.LUMO_ICON); + deleteButton.getStyle().set("min-width", "0").set("min-height", "0").set("padding", "0").set("margin", "0") + .set("height", "var(--lumo-font-size-m)").set("line-height", "var(--lumo-font-size-m)"); + // Stop propagation on the client side to prevent the tile click from firing + deleteButton.getElement().setAttribute("onclick", "event.stopPropagation()"); deleteButton.addClickListener(e -> { - e.getSource().getElement().executeJs("arguments[0].stopPropagation()", e.getSource().getElement()); if (deleteListener != null) { deleteListener.onDelete(this); } @@ -77,6 +83,7 @@ public class StationTile extends VerticalLayout { previewContent.setPadding(false); previewContent.setSpacing(false); previewContent.getStyle().set("gap", "var(--lumo-space-xs)"); + previewContent.getStyle().set("flex-grow", "1"); add(previewContent); // Show placeholder when no data @@ -93,6 +100,8 @@ public class StationTile extends VerticalLayout { public void updatePreview(String company, String firstName, String lastName, String street, String houseNumber, String zip, String city) { previewContent.removeAll(); + previewContent.setJustifyContentMode(FlexComponent.JustifyContentMode.START); + previewContent.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.START); boolean hasData = false; @@ -126,8 +135,10 @@ public class StationTile extends VerticalLayout { private void updateEmptyPreview() { previewContent.removeAll(); - Span placeholder = new Span("..."); - placeholder.getStyle().set("color", "var(--lumo-contrast-40pct)").set("font-size", "var(--lumo-font-size-s)"); + previewContent.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); + previewContent.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); + Span placeholder = new Span(getTranslation("addjob.station.unused")); + placeholder.getStyle().set("color", "var(--lumo-contrast-30pct)").set("font-size", "var(--lumo-font-size-xl)"); previewContent.add(placeholder); } 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 091960c..689f746 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java @@ -82,8 +82,6 @@ public class AddJobView extends Main implements HasDynamicTitle { private final TaskTemplateService taskTemplateService; private final SecurityService securityService; private final ServiceRepository serviceRepository; - private final AddressValidationService addressValidationService; - // Customer selection private ComboBox customerSelection; @@ -139,6 +137,7 @@ public class AddJobView extends Main implements HasDynamicTitle { // Submit button private Button submitButton; + private HorizontalLayout submitButtonLayout; // Backing list for cargo items to mirror UI rows private final List cargoItemsState = new ArrayList<>(); @@ -168,7 +167,6 @@ public class AddJobView extends Main implements HasDynamicTitle { this.taskTemplateService = taskTemplateService; this.securityService = securityService; this.serviceRepository = serviceRepository; - this.addressValidationService = addressValidationService; initializeComponents(); setupLayout(); setupValidation(); @@ -298,13 +296,14 @@ public class AddJobView extends Main implements HasDynamicTitle { add(createCustomerAndAddressesTab()); // Add submit button horizontally centered below the content - HorizontalLayout buttonLayout = new HorizontalLayout(); - buttonLayout.setWidthFull(); - buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); - buttonLayout.setPadding(true); - buttonLayout.add(submitButton); + submitButtonLayout = new HorizontalLayout(); + submitButtonLayout.setWidthFull(); + submitButtonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); + submitButtonLayout.setPadding(true); + submitButtonLayout.add(submitButton); + submitButtonLayout.setVisible(false); - add(buttonLayout); + add(submitButtonLayout); } private Component createCustomerAndAddressesTab() { @@ -337,6 +336,24 @@ public class AddJobView extends Main implements HasDynamicTitle { tabContent.add(stationsGridContainer); + // Wrapper für alle Elemente nach Stationen (initial versteckt) + VerticalLayout priceAndDetailsSection = new VerticalLayout(); + priceAndDetailsSection.setWidthFull(); + priceAndDetailsSection.setPadding(false); + priceAndDetailsSection.setSpacing(true); + priceAndDetailsSection.setVisible(false); + + // "Stationen übernehmen" Button + Button applyStationsButton = new Button(getTranslation("addjob.stations.apply")); + applyStationsButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + applyStationsButton.setWidthFull(); + applyStationsButton.addClickListener(e -> { + applyStationsButton.setVisible(false); + priceAndDetailsSection.setVisible(true); + submitButtonLayout.setVisible(true); + }); + tabContent.add(applyStationsButton); + // Route Info Box routeInfoBox = new VerticalLayout(); routeInfoBox.setPadding(true); @@ -378,7 +395,7 @@ public class AddJobView extends Main implements HasDynamicTitle { durationRow.add(durationLabel, routeDurationLabel); routeInfoBox.add(routeTitle, routeRow, durationRow); - tabContent.add(routeInfoBox); + priceAndDetailsSection.add(routeInfoBox); // Manuelle Streckeneingabe (wenn keine Route berechnet wurde) manualRouteInputBox = new VerticalLayout(); @@ -430,12 +447,12 @@ public class AddJobView extends Main implements HasDynamicTitle { manualRouteHint.getStyle().set("font-style", "italic"); manualRouteInputBox.add(manualRouteTitle, manualInputRow, manualRouteHint); - tabContent.add(manualRouteInputBox); + priceAndDetailsSection.add(manualRouteInputBox); // Leistungen H3 servicesTitle = new H3(getTranslation("addjob.services.title")); servicesTitle.getStyle().set("margin", "0"); - tabContent.add(servicesTitle); + priceAndDetailsSection.add(servicesTitle); // Services Grid servicesGrid = new Grid<>(); @@ -493,13 +510,13 @@ public class AddJobView extends Main implements HasDynamicTitle { return removeButton; }).setHeader(getTranslation("common.actions")).setAutoWidth(true).setFlexGrow(0); - tabContent.add(servicesGrid); + priceAndDetailsSection.add(servicesGrid); // Add Service Button Button addServiceButton = new Button(getTranslation("addjob.services.add"), new Icon(VaadinIcon.PLUS)); addServiceButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); addServiceButton.addClickListener(e -> openAddServiceDialog()); - tabContent.add(addServiceButton); + priceAndDetailsSection.add(addServiceButton); // Price Summary VerticalLayout summaryLayout = new VerticalLayout(); @@ -552,7 +569,7 @@ public class AddJobView extends Main implements HasDynamicTitle { summaryLayout.add(priceTable); - tabContent.add(summaryLayout); + priceAndDetailsSection.add(summaryLayout); // Bemerkung H3 remarksTitle = new H3(getTranslation("addjob.tasks.remark")); @@ -561,7 +578,9 @@ public class AddJobView extends Main implements HasDynamicTitle { remarkArea.setPlaceholder(getTranslation("addjob.tasks.remark.placeholder")); remarkArea.setWidthFull(); remarkArea.setMinHeight("180px"); - tabContent.add(remarksTitle, remarkArea); + priceAndDetailsSection.add(remarksTitle, remarkArea); + + tabContent.add(priceAndDetailsSection); return tabContent; } diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 64dfa5a..d0e31bb 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -450,10 +450,12 @@ addjob.address.delivery.addition.placeholder=Adresszusatz (Lieferung) addjob.address.save=Adresse speichern addjob.section.pickup=Abholung addjob.section.delivery=Lieferung +addjob.stations.apply=Stationen \u00fcbernehmen addjob.station.delivery=Lieferstation {0} addjob.station.add=Lieferstation hinzuf\u00fcgen addjob.station.remove.confirm=Lieferstation {0} wirklich entfernen? addjob.station.max.reached=Maximale Anzahl von 25 Lieferstationen erreicht +addjob.station.unused=Nicht genutzt addjob.appointment.delivery.info=Liefertermine werden direkt in den Lieferstationen festgelegt. addjob.tab.addresses=Auftraggeber & Adressen addjob.tab.appointments=Termine & Verarbeitung diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties index 58d4c9c..609c0f1 100644 --- a/src/main/resources/messages_en.properties +++ b/src/main/resources/messages_en.properties @@ -450,10 +450,12 @@ addjob.address.delivery.addition.placeholder=Address addition (Delivery) addjob.address.save=Save Address addjob.section.pickup=Pickup addjob.section.delivery=Delivery +addjob.stations.apply=Apply Stations addjob.station.delivery=Delivery Station {0} addjob.station.add=Add delivery station addjob.station.remove.confirm=Really remove delivery station {0}? addjob.station.max.reached=Maximum of 25 delivery stations reached +addjob.station.unused=Not used addjob.appointment.delivery.info=Delivery dates are set directly in the delivery stations. addjob.tab.addresses=Customer & Addresses addjob.tab.appointments=Appointments & Processing