Stationen-Dialoge: Validierung mit Tab-Fehlerindikatoren, Template-Laden aktualisiert Validierungsstatus

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 19:08:28 +01:00
parent cd8b82cd71
commit 07f9748674
6 changed files with 311 additions and 50 deletions

View File

@@ -14,6 +14,7 @@ import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.FlexComponent; 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.tabs.Tab;
import com.vaadin.flow.component.tabs.TabSheet; import com.vaadin.flow.component.tabs.TabSheet;
import com.vaadin.flow.component.textfield.IntegerField; import com.vaadin.flow.component.textfield.IntegerField;
import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.textfield.TextField;
@@ -171,6 +172,9 @@ public class DeliveryStationDialog extends Dialog {
private final List<BaseTask> tasksState = new ArrayList<>(); private final List<BaseTask> tasksState = new ArrayList<>();
private VerticalLayout tasksList; private VerticalLayout tasksList;
private Span addressTabError;
private Span tasksTabError;
private final DeliveryStationTile.TranslationHelper translationHelper; private final DeliveryStationTile.TranslationHelper translationHelper;
public DeliveryStationDialog(String dialogTitle, List<Customer> customers, public DeliveryStationDialog(String dialogTitle, List<Customer> customers,
@@ -181,7 +185,7 @@ public class DeliveryStationDialog extends Dialog {
setHeaderTitle(dialogTitle); setHeaderTitle(dialogTitle);
setCloseOnOutsideClick(false); setCloseOnOutsideClick(false);
setWidth("800px"); setWidth("960px");
setHeight("80vh"); setHeight("80vh");
// Address form // Address form
@@ -274,19 +278,37 @@ public class DeliveryStationDialog extends Dialog {
saveAddress.setWidthFull(); saveAddress.setWidthFull();
formLayout.add(saveAddress); 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 with address and tasks tabs
TabSheet tabSheet = new TabSheet(); TabSheet tabSheet = new TabSheet();
tabSheet.setWidthFull(); tabSheet.setWidthFull();
tabSheet.setSizeFull(); tabSheet.setSizeFull();
tabSheet.add(translationHelper.getTranslation("addjob.tab.addresses"), formLayout); addressTabError = createTabErrorIndicator();
tabSheet.add(translationHelper.getTranslation("addjob.tab.tasks"), 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)); createTasksTab(templates, templateSaveCallback));
tasksTab.add(tasksTabError);
add(tabSheet); add(tabSheet);
// Footer buttons // Footer buttons
Button saveButton = new Button(translationHelper.getTranslation("dialog.confirm"), e -> { 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(); DeliveryData data = collectData();
if (saveListener != null) { if (saveListener != null) {
saveListener.onSave(data); saveListener.onSave(data);
@@ -298,6 +320,12 @@ public class DeliveryStationDialog extends Dialog {
Button cancelButton = new Button(translationHelper.getTranslation("dialog.cancel"), e -> close()); Button cancelButton = new Button(translationHelper.getTranslation("dialog.cancel"), e -> close());
getFooter().add(cancelButton, saveButton); getFooter().add(cancelButton, saveButton);
addOpenedChangeListener(event -> {
if (event.isOpened()) {
validateRequiredFields();
}
});
} }
/** /**
@@ -363,6 +391,69 @@ public class DeliveryStationDialog extends Dialog {
return data; 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<String> companyField, List<Customer> customers) { private void setupCompanyAutocomplete(ComboBox<String> companyField, List<Customer> customers) {
List<String> companyNames = customers.stream().map(Customer::getCompanyName) List<String> companyNames = customers.stream().map(Customer::getCompanyName)
.filter(name -> name != null && !name.trim().isEmpty()).distinct().sorted().toList(); .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.setValue(task.getDescription() != null ? task.getDescription() : "");
descriptionField.addValueChangeListener(ev -> { descriptionField.addValueChangeListener(ev -> {
task.setDescription(ev.getValue()); task.setDescription(ev.getValue());
boolean isEmpty = ev.getValue() == null || ev.getValue().trim().isEmpty(); applyErrorStyling(descriptionField, ev.getValue() == null || ev.getValue().trim().isEmpty());
if (isEmpty) { validateRequiredFields();
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");
}
}); });
if (task.getDescription() == null || task.getDescription().trim().isEmpty()) { if (task.getDescription() == null || task.getDescription().trim().isEmpty()) {
descriptionField.getStyle().set("--vaadin-input-field-background", "rgba(255, 0, 0, 0.1)"); 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.setValue(confirmationTask.getButtonText() != null ? confirmationTask.getButtonText() : "");
buttonTextField.addValueChangeListener(ev -> { buttonTextField.addValueChangeListener(ev -> {
confirmationTask.setButtonText(ev.getValue()); confirmationTask.setButtonText(ev.getValue());
boolean isEmpty = ev.getValue() == null || ev.getValue().trim().isEmpty(); applyErrorStyling(buttonTextField, ev.getValue() == null || ev.getValue().trim().isEmpty());
if (isEmpty) { validateRequiredFields();
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");
}
}); });
if (confirmationTask.getButtonText() == null || confirmationTask.getButtonText().trim().isEmpty()) { if (confirmationTask.getButtonText() == null || confirmationTask.getButtonText().trim().isEmpty()) {
buttonTextField.getStyle().set("--vaadin-input-field-background", "rgba(255, 0, 0, 0.1)"); 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 -> { todoField.addValueChangeListener(ev -> {
updateTodoFieldStyling.accept(todoField); updateTodoFieldStyling.accept(todoField);
updateTodoItems(todoList, task); updateTodoItems(todoList, task);
validateRequiredFields();
}); });
}; };
@@ -1080,6 +1160,7 @@ public class DeliveryStationDialog extends Dialog {
} }
templateComboBox.clear(); templateComboBox.clear();
validateRequiredFields();
Notification.show( Notification.show(
translationHelper.getTranslation("addjob.tasks.template.loaded", template.getTemplateName()), 3000, translationHelper.getTranslation("addjob.tasks.template.loaded", template.getTemplateName()), 3000,

View File

@@ -13,11 +13,13 @@ import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.FlexComponent; 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.tabs.Tab;
import com.vaadin.flow.component.tabs.TabSheet; import com.vaadin.flow.component.tabs.TabSheet;
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.textfield.TextField; import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.component.timepicker.TimePicker; 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.AppUser;
import de.assecutor.votianlt.model.CargoItem; import de.assecutor.votianlt.model.CargoItem;
import de.assecutor.votianlt.model.Customer; import de.assecutor.votianlt.model.Customer;
@@ -219,6 +221,10 @@ public class PickupStationDialog extends Dialog {
private final List<CargoItem> cargoItemsState = new ArrayList<>(); private final List<CargoItem> cargoItemsState = new ArrayList<>();
private VerticalLayout cargoList; private VerticalLayout cargoList;
private Span addressTabError;
private Span appointmentsTabError;
private Span cargoTabError;
private final DeliveryStationTile.TranslationHelper translationHelper; private final DeliveryStationTile.TranslationHelper translationHelper;
public PickupStationDialog(String dialogTitle, List<Customer> customers, public PickupStationDialog(String dialogTitle, List<Customer> customers,
@@ -229,7 +235,7 @@ public class PickupStationDialog extends Dialog {
setHeaderTitle(dialogTitle); setHeaderTitle(dialogTitle);
setCloseOnOutsideClick(false); setCloseOnOutsideClick(false);
setWidth("800px"); setWidth("960px");
setHeight("80vh"); setHeight("80vh");
// Address form // Address form
@@ -339,6 +345,14 @@ public class PickupStationDialog extends Dialog {
saveAddress.setValue(true); saveAddress.setValue(true);
saveAddress.setWidthFull(); 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 // Customer selection fills address fields
customerComboBox.addValueChangeListener(ev -> { customerComboBox.addValueChangeListener(ev -> {
String selected = ev.getValue(); String selected = ev.getValue();
@@ -399,15 +413,27 @@ public class PickupStationDialog extends Dialog {
tabSheet.setWidthFull(); tabSheet.setWidthFull();
tabSheet.setSizeFull(); tabSheet.setSizeFull();
tabSheet.add(translationHelper.getTranslation("addjob.tab.addresses"), formLayout); addressTabError = createTabErrorIndicator();
tabSheet.add(translationHelper.getTranslation("addjob.tab.appointments"), 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)); 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); add(tabSheet);
// Footer buttons // Footer buttons
Button saveButton = new Button(translationHelper.getTranslation("dialog.confirm"), e -> { 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(); PickupData data = collectData();
if (saveListener != null) { if (saveListener != null) {
saveListener.onSave(data); saveListener.onSave(data);
@@ -419,6 +445,12 @@ public class PickupStationDialog extends Dialog {
Button cancelButton = new Button(translationHelper.getTranslation("dialog.cancel"), e -> close()); Button cancelButton = new Button(translationHelper.getTranslation("dialog.cancel"), e -> close());
getFooter().add(cancelButton, saveButton); getFooter().add(cancelButton, saveButton);
addOpenedChangeListener(event -> {
if (event.isOpened()) {
validateRequiredFields();
}
});
} }
/** /**
@@ -501,6 +533,92 @@ public class PickupStationDialog extends Dialog {
return data; 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<String> companyField, List<Customer> customers) { private void setupCompanyAutocomplete(ComboBox<String> companyField, List<Customer> customers) {
List<String> companyNames = customers.stream().map(Customer::getCompanyName) List<String> companyNames = customers.stream().map(Customer::getCompanyName)
.filter(name -> name != null && !name.trim().isEmpty()).distinct().sorted().toList(); .filter(name -> name != null && !name.trim().isEmpty()).distinct().sorted().toList();
@@ -581,6 +699,7 @@ public class PickupStationDialog extends Dialog {
appUserComboBox.setItemLabelGenerator( appUserComboBox.setItemLabelGenerator(
user -> user.getVorname() + " " + user.getNachname() + " (" + user.getEmail() + ")"); user -> user.getVorname() + " " + user.getNachname() + " (" + user.getEmail() + ")");
appUserComboBox.setPlaceholder(translationHelper.getTranslation("addjob.appuser.placeholder")); appUserComboBox.setPlaceholder(translationHelper.getTranslation("addjob.appuser.placeholder"));
appUserComboBox.addValueChangeListener(ev -> validateRequiredFields());
content.add(digitalRow, appUserComboBox); content.add(digitalRow, appUserComboBox);
@@ -592,6 +711,7 @@ public class PickupStationDialog extends Dialog {
if (!required) { if (!required) {
appUserComboBox.clear(); appUserComboBox.clear();
} }
validateRequiredFields();
}); });
boolean digitalInitial = Boolean.TRUE.equals(digitalProcessingCheckbox.getValue()); boolean digitalInitial = Boolean.TRUE.equals(digitalProcessingCheckbox.getValue());
appUserComboBox.setRequiredIndicatorVisible(digitalInitial); appUserComboBox.setRequiredIndicatorVisible(digitalInitial);
@@ -615,6 +735,8 @@ public class PickupStationDialog extends Dialog {
appointmentTimePicker = new TimePicker(translationHelper.getTranslation("addjob.appointment.time")); appointmentTimePicker = new TimePicker(translationHelper.getTranslation("addjob.appointment.time"));
appointmentTimePicker.setLocale(java.util.Locale.GERMANY); appointmentTimePicker.setLocale(java.util.Locale.GERMANY);
appointmentDatePicker.addValueChangeListener(ev -> validateRequiredFields());
HorizontalLayout pickupApptRow = new HorizontalLayout(appointmentDatePicker, appointmentTimePicker); HorizontalLayout pickupApptRow = new HorizontalLayout(appointmentDatePicker, appointmentTimePicker);
pickupApptRow.setWidthFull(); pickupApptRow.setWidthFull();
pickupApptRow.setSpacing(true); pickupApptRow.setSpacing(true);
@@ -768,11 +890,35 @@ public class PickupStationDialog extends Dialog {
cargoItemsState.add(item); cargoItemsState.add(item);
// Bind change listeners // Bind change listeners
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())); applyErrorStyling(desc, ev.getValue() == null || ev.getValue().trim().isEmpty());
len.addValueChangeListener(ev -> item.setLengthMm(ev.getValue())); validateRequiredFields();
wid.addValueChangeListener(ev -> item.setWidthMm(ev.getValue())); });
hei.addValueChangeListener(ev -> item.setHeightMm(ev.getValue())); 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();
});
} }
} }

View File

@@ -55,14 +55,20 @@ public class StationTile extends VerticalLayout {
HorizontalLayout titleLayout = new HorizontalLayout(); HorizontalLayout titleLayout = new HorizontalLayout();
titleLayout.setWidthFull(); titleLayout.setWidthFull();
titleLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER); titleLayout.setPadding(false);
titleLayout.setSpacing(false);
titleLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.START);
titleLayout.add(title); titleLayout.add(title);
if (removable) { if (removable) {
Button deleteButton = new Button(new Icon(VaadinIcon.CLOSE_SMALL)); 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 -> { deleteButton.addClickListener(e -> {
e.getSource().getElement().executeJs("arguments[0].stopPropagation()", e.getSource().getElement());
if (deleteListener != null) { if (deleteListener != null) {
deleteListener.onDelete(this); deleteListener.onDelete(this);
} }
@@ -77,6 +83,7 @@ public class StationTile extends VerticalLayout {
previewContent.setPadding(false); previewContent.setPadding(false);
previewContent.setSpacing(false); previewContent.setSpacing(false);
previewContent.getStyle().set("gap", "var(--lumo-space-xs)"); previewContent.getStyle().set("gap", "var(--lumo-space-xs)");
previewContent.getStyle().set("flex-grow", "1");
add(previewContent); add(previewContent);
// Show placeholder when no data // 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, public void updatePreview(String company, String firstName, String lastName, String street, String houseNumber,
String zip, String city) { String zip, String city) {
previewContent.removeAll(); previewContent.removeAll();
previewContent.setJustifyContentMode(FlexComponent.JustifyContentMode.START);
previewContent.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.START);
boolean hasData = false; boolean hasData = false;
@@ -126,8 +135,10 @@ public class StationTile extends VerticalLayout {
private void updateEmptyPreview() { private void updateEmptyPreview() {
previewContent.removeAll(); previewContent.removeAll();
Span placeholder = new Span("..."); previewContent.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
placeholder.getStyle().set("color", "var(--lumo-contrast-40pct)").set("font-size", "var(--lumo-font-size-s)"); 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); previewContent.add(placeholder);
} }

View File

@@ -82,8 +82,6 @@ public class AddJobView extends Main implements HasDynamicTitle {
private final TaskTemplateService taskTemplateService; private final TaskTemplateService taskTemplateService;
private final SecurityService securityService; private final SecurityService securityService;
private final ServiceRepository serviceRepository; private final ServiceRepository serviceRepository;
private final AddressValidationService addressValidationService;
// Customer selection // Customer selection
private ComboBox<String> customerSelection; private ComboBox<String> customerSelection;
@@ -139,6 +137,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
// Submit button // Submit button
private Button submitButton; private Button submitButton;
private HorizontalLayout submitButtonLayout;
// Backing list for cargo items to mirror UI rows // Backing list for cargo items to mirror UI rows
private final List<CargoItem> cargoItemsState = new ArrayList<>(); private final List<CargoItem> cargoItemsState = new ArrayList<>();
@@ -168,7 +167,6 @@ public class AddJobView extends Main implements HasDynamicTitle {
this.taskTemplateService = taskTemplateService; this.taskTemplateService = taskTemplateService;
this.securityService = securityService; this.securityService = securityService;
this.serviceRepository = serviceRepository; this.serviceRepository = serviceRepository;
this.addressValidationService = addressValidationService;
initializeComponents(); initializeComponents();
setupLayout(); setupLayout();
setupValidation(); setupValidation();
@@ -298,13 +296,14 @@ public class AddJobView extends Main implements HasDynamicTitle {
add(createCustomerAndAddressesTab()); add(createCustomerAndAddressesTab());
// Add submit button horizontally centered below the content // Add submit button horizontally centered below the content
HorizontalLayout buttonLayout = new HorizontalLayout(); submitButtonLayout = new HorizontalLayout();
buttonLayout.setWidthFull(); submitButtonLayout.setWidthFull();
buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); submitButtonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
buttonLayout.setPadding(true); submitButtonLayout.setPadding(true);
buttonLayout.add(submitButton); submitButtonLayout.add(submitButton);
submitButtonLayout.setVisible(false);
add(buttonLayout); add(submitButtonLayout);
} }
private Component createCustomerAndAddressesTab() { private Component createCustomerAndAddressesTab() {
@@ -337,6 +336,24 @@ public class AddJobView extends Main implements HasDynamicTitle {
tabContent.add(stationsGridContainer); 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 // Route Info Box
routeInfoBox = new VerticalLayout(); routeInfoBox = new VerticalLayout();
routeInfoBox.setPadding(true); routeInfoBox.setPadding(true);
@@ -378,7 +395,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
durationRow.add(durationLabel, routeDurationLabel); durationRow.add(durationLabel, routeDurationLabel);
routeInfoBox.add(routeTitle, routeRow, durationRow); routeInfoBox.add(routeTitle, routeRow, durationRow);
tabContent.add(routeInfoBox); priceAndDetailsSection.add(routeInfoBox);
// Manuelle Streckeneingabe (wenn keine Route berechnet wurde) // Manuelle Streckeneingabe (wenn keine Route berechnet wurde)
manualRouteInputBox = new VerticalLayout(); manualRouteInputBox = new VerticalLayout();
@@ -430,12 +447,12 @@ public class AddJobView extends Main implements HasDynamicTitle {
manualRouteHint.getStyle().set("font-style", "italic"); manualRouteHint.getStyle().set("font-style", "italic");
manualRouteInputBox.add(manualRouteTitle, manualInputRow, manualRouteHint); manualRouteInputBox.add(manualRouteTitle, manualInputRow, manualRouteHint);
tabContent.add(manualRouteInputBox); priceAndDetailsSection.add(manualRouteInputBox);
// Leistungen // Leistungen
H3 servicesTitle = new H3(getTranslation("addjob.services.title")); H3 servicesTitle = new H3(getTranslation("addjob.services.title"));
servicesTitle.getStyle().set("margin", "0"); servicesTitle.getStyle().set("margin", "0");
tabContent.add(servicesTitle); priceAndDetailsSection.add(servicesTitle);
// Services Grid // Services Grid
servicesGrid = new Grid<>(); servicesGrid = new Grid<>();
@@ -493,13 +510,13 @@ public class AddJobView extends Main implements HasDynamicTitle {
return removeButton; return removeButton;
}).setHeader(getTranslation("common.actions")).setAutoWidth(true).setFlexGrow(0); }).setHeader(getTranslation("common.actions")).setAutoWidth(true).setFlexGrow(0);
tabContent.add(servicesGrid); priceAndDetailsSection.add(servicesGrid);
// Add Service Button // Add Service Button
Button addServiceButton = new Button(getTranslation("addjob.services.add"), new Icon(VaadinIcon.PLUS)); Button addServiceButton = new Button(getTranslation("addjob.services.add"), new Icon(VaadinIcon.PLUS));
addServiceButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); addServiceButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
addServiceButton.addClickListener(e -> openAddServiceDialog()); addServiceButton.addClickListener(e -> openAddServiceDialog());
tabContent.add(addServiceButton); priceAndDetailsSection.add(addServiceButton);
// Price Summary // Price Summary
VerticalLayout summaryLayout = new VerticalLayout(); VerticalLayout summaryLayout = new VerticalLayout();
@@ -552,7 +569,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
summaryLayout.add(priceTable); summaryLayout.add(priceTable);
tabContent.add(summaryLayout); priceAndDetailsSection.add(summaryLayout);
// Bemerkung // Bemerkung
H3 remarksTitle = new H3(getTranslation("addjob.tasks.remark")); 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.setPlaceholder(getTranslation("addjob.tasks.remark.placeholder"));
remarkArea.setWidthFull(); remarkArea.setWidthFull();
remarkArea.setMinHeight("180px"); remarkArea.setMinHeight("180px");
tabContent.add(remarksTitle, remarkArea); priceAndDetailsSection.add(remarksTitle, remarkArea);
tabContent.add(priceAndDetailsSection);
return tabContent; return tabContent;
} }

View File

@@ -450,10 +450,12 @@ addjob.address.delivery.addition.placeholder=Adresszusatz (Lieferung)
addjob.address.save=Adresse speichern addjob.address.save=Adresse speichern
addjob.section.pickup=Abholung addjob.section.pickup=Abholung
addjob.section.delivery=Lieferung addjob.section.delivery=Lieferung
addjob.stations.apply=Stationen \u00fcbernehmen
addjob.station.delivery=Lieferstation {0} addjob.station.delivery=Lieferstation {0}
addjob.station.add=Lieferstation hinzuf\u00fcgen addjob.station.add=Lieferstation hinzuf\u00fcgen
addjob.station.remove.confirm=Lieferstation {0} wirklich entfernen? addjob.station.remove.confirm=Lieferstation {0} wirklich entfernen?
addjob.station.max.reached=Maximale Anzahl von 25 Lieferstationen erreicht 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.appointment.delivery.info=Liefertermine werden direkt in den Lieferstationen festgelegt.
addjob.tab.addresses=Auftraggeber & Adressen addjob.tab.addresses=Auftraggeber & Adressen
addjob.tab.appointments=Termine & Verarbeitung addjob.tab.appointments=Termine & Verarbeitung

View File

@@ -450,10 +450,12 @@ addjob.address.delivery.addition.placeholder=Address addition (Delivery)
addjob.address.save=Save Address addjob.address.save=Save Address
addjob.section.pickup=Pickup addjob.section.pickup=Pickup
addjob.section.delivery=Delivery addjob.section.delivery=Delivery
addjob.stations.apply=Apply Stations
addjob.station.delivery=Delivery Station {0} addjob.station.delivery=Delivery Station {0}
addjob.station.add=Add delivery station addjob.station.add=Add delivery station
addjob.station.remove.confirm=Really remove delivery station {0}? addjob.station.remove.confirm=Really remove delivery station {0}?
addjob.station.max.reached=Maximum of 25 delivery stations reached 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.appointment.delivery.info=Delivery dates are set directly in the delivery stations.
addjob.tab.addresses=Customer & Addresses addjob.tab.addresses=Customer & Addresses
addjob.tab.appointments=Appointments & Processing addjob.tab.appointments=Appointments & Processing