Kachel-Header: Titel, Einklapp- und Schließen-Icon auf eine Zeile gestellt, Plus-Kachel klappt mit ein wenn alle Kacheln eingeklappt sind

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 10:44:41 +01:00
parent 8f40fd017f
commit 7ba2880148
2 changed files with 177 additions and 15 deletions

View File

@@ -1,10 +1,12 @@
package de.assecutor.votianlt.pages.base.ui.component; package de.assecutor.votianlt.pages.base.ui.component;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.checkbox.Checkbox; import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.combobox.ComboBox; import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.html.H3; import com.vaadin.flow.component.html.H3;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.FlexComponent; import com.vaadin.flow.component.orderedlayout.FlexComponent;
@@ -31,6 +33,10 @@ public class DeliveryStationTile extends VerticalLayout {
void onDelete(DeliveryStationTile tile); void onDelete(DeliveryStationTile tile);
} }
public interface CollapseListener {
void onCollapseChanged(boolean collapsed);
}
private final int stationNumber; private final int stationNumber;
private final ComboBox<String> company; private final ComboBox<String> company;
@@ -48,6 +54,12 @@ public class DeliveryStationTile extends VerticalLayout {
private ChangeListener changeListener; private ChangeListener changeListener;
private DeleteListener deleteListener; private DeleteListener deleteListener;
private CollapseListener collapseListener;
private boolean collapsed = false;
private Button collapseButton;
private VerticalLayout collapsedContent;
private List<Component> expandedOnlyComponents;
public DeliveryStationTile(int stationNumber, boolean removable, List<Customer> customers, public DeliveryStationTile(int stationNumber, boolean removable, List<Customer> customers,
TranslationHelper translationHelper) { TranslationHelper translationHelper) {
@@ -62,15 +74,14 @@ public class DeliveryStationTile extends VerticalLayout {
getStyle().set("border-radius", "var(--lumo-border-radius-m)"); getStyle().set("border-radius", "var(--lumo-border-radius-m)");
getStyle().set("background-color", "var(--lumo-base-color)"); getStyle().set("background-color", "var(--lumo-base-color)");
// Header // Header with title, collapse button and delete button on one line
title = new H3(translationHelper.getTranslation("addjob.station.delivery", stationNumber)); title = new H3(translationHelper.getTranslation("addjob.station.delivery", stationNumber));
title.getStyle().set("margin", "0"); title.getStyle().set("margin", "0").set("flex-grow", "1");
HorizontalLayout titleLayout = new HorizontalLayout(); collapseButton = new Button(new Icon(VaadinIcon.ANGLE_DOUBLE_LEFT));
titleLayout.setWidthFull(); collapseButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE);
titleLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN); collapseButton.getStyle().set("cursor", "pointer");
titleLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER); collapseButton.addClickListener(e -> toggleCollapse());
titleLayout.add(title);
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);
@@ -83,7 +94,11 @@ public class DeliveryStationTile extends VerticalLayout {
} else { } else {
deleteButton.getStyle().set("visibility", "hidden"); deleteButton.getStyle().set("visibility", "hidden");
} }
titleLayout.add(deleteButton);
HorizontalLayout titleLayout = new HorizontalLayout();
titleLayout.setWidthFull();
titleLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
titleLayout.add(title, collapseButton, deleteButton);
add(titleLayout); add(titleLayout);
@@ -172,6 +187,19 @@ public class DeliveryStationTile extends VerticalLayout {
// Register change listeners on all fields // Register change listeners on all fields
setupChangeListeners(); setupChangeListeners();
// Store references to expanded-mode components (excluding titleLayout which stays visible)
expandedOnlyComponents = getChildren().filter(c -> c != titleLayout).toList();
getStyle().set("transition", "width 0.3s ease, min-width 0.3s ease");
// Collapsed content (initially hidden)
collapsedContent = new VerticalLayout();
collapsedContent.setPadding(false);
collapsedContent.setSpacing(false);
collapsedContent.getStyle().set("gap", "var(--lumo-space-xs)");
collapsedContent.setVisible(false);
add(collapsedContent);
} }
private void setupChangeListeners() { private void setupChangeListeners() {
@@ -372,6 +400,14 @@ public class DeliveryStationTile extends VerticalLayout {
this.deleteListener = listener; this.deleteListener = listener;
} }
public void setCollapseListener(CollapseListener listener) {
this.collapseListener = listener;
}
public boolean isCollapsed() {
return collapsed;
}
/** /**
* Returns whether the user wants to save this address as a customer. * Returns whether the user wants to save this address as a customer.
*/ */
@@ -403,6 +439,51 @@ public class DeliveryStationTile extends VerticalLayout {
} }
} }
private void toggleCollapse() {
collapsed = !collapsed;
if (collapsed) {
updateCollapsedContent();
expandedOnlyComponents.forEach(c -> c.setVisible(false));
collapsedContent.setVisible(true);
setWidth("25%");
getStyle().set("min-width", "150px");
collapseButton.setIcon(new Icon(VaadinIcon.ANGLE_DOUBLE_RIGHT));
} else {
expandedOnlyComponents.forEach(c -> c.setVisible(true));
collapsedContent.setVisible(false);
setWidth("40%");
getStyle().set("min-width", "300px");
collapseButton.setIcon(new Icon(VaadinIcon.ANGLE_DOUBLE_LEFT));
}
if (collapseListener != null) {
collapseListener.onCollapseChanged(collapsed);
}
}
private void updateCollapsedContent() {
collapsedContent.removeAll();
addCollapsedLine(company.getValue());
String name = (getValueOrEmpty(firstName) + " " + getValueOrEmpty(lastName)).trim();
addCollapsedLine(name);
String streetLine = (getValueOrEmpty(street) + " " + getValueOrEmpty(houseNumber)).trim();
addCollapsedLine(streetLine);
String zipCityLine = (getValueOrEmpty(zip) + " " + getValueOrEmpty(city)).trim();
addCollapsedLine(zipCityLine);
}
private void addCollapsedLine(String text) {
if (text != null && !text.trim().isEmpty()) {
Span span = new Span(text);
span.getStyle().set("font-size", "var(--lumo-font-size-xs)").set("word-break", "break-word")
.set("color", "var(--lumo-secondary-text-color)");
collapsedContent.add(span);
}
}
/** /**
* Functional interface for accessing translations from the parent view. * Functional interface for accessing translations from the parent view.
*/ */

View File

@@ -162,6 +162,10 @@ public class AddJobView extends Main implements HasDynamicTitle {
private ComboBox<TaskTemplate> templateComboBox; private ComboBox<TaskTemplate> templateComboBox;
private TextArea remarkArea; private TextArea remarkArea;
private VerticalLayout pickupSection; private VerticalLayout pickupSection;
private boolean pickupCollapsed = false;
private Button pickupCollapseButton;
private VerticalLayout pickupCollapsedContent;
private List<Component> pickupExpandedOnlyComponents;
private final Binder<Job> binder = new Binder<>(Job.class); private final Binder<Job> binder = new Binder<>(Job.class);
@@ -531,6 +535,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
updateTabLabels(); updateTabLabels();
}); });
tile.setDeleteListener(this::removeDeliveryStationTile); tile.setDeleteListener(this::removeDeliveryStationTile);
tile.setCollapseListener(collapsed -> updateAddStationButtonSize());
deliveryStationTiles.add(tile); deliveryStationTiles.add(tile);
@@ -571,6 +576,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
resetRouteInformation(); resetRouteInformation();
triggerValidation(); triggerValidation();
updateTabLabels(); updateTabLabels();
updateAddStationButtonSize();
}); });
dialog.open(); dialog.open();
} }
@@ -1013,19 +1019,22 @@ public class AddJobView extends Main implements HasDynamicTitle {
section.getStyle().set("background-color", "var(--lumo-base-color)"); section.getStyle().set("background-color", "var(--lumo-base-color)");
H3 title = new H3(getTranslation("addjob.section.pickup")); H3 title = new H3(getTranslation("addjob.section.pickup"));
title.getStyle().set("margin", "0"); title.getStyle().set("margin", "0").set("flex-grow", "1");
HorizontalLayout titleLayout = new HorizontalLayout(); pickupCollapseButton = new Button(new Icon(VaadinIcon.ANGLE_DOUBLE_LEFT));
titleLayout.setWidthFull(); pickupCollapseButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE);
titleLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN); pickupCollapseButton.getStyle().set("cursor", "pointer");
titleLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER); pickupCollapseButton.addClickListener(e -> togglePickupCollapse());
titleLayout.add(title);
// Invisible placeholder button to match DeliveryStationTile header height // Invisible placeholder button to match DeliveryStationTile header height
Button placeholder = new Button(new Icon(VaadinIcon.CLOSE_SMALL)); Button placeholder = new Button(new Icon(VaadinIcon.CLOSE_SMALL));
placeholder.addThemeVariants(ButtonVariant.LUMO_TERTIARY); placeholder.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
placeholder.getStyle().set("visibility", "hidden"); placeholder.getStyle().set("visibility", "hidden");
titleLayout.add(placeholder);
HorizontalLayout titleLayout = new HorizontalLayout();
titleLayout.setWidthFull();
titleLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
titleLayout.add(title, pickupCollapseButton, placeholder);
// Alle einzelnen Controls auf volle Breite setzen // Alle einzelnen Controls auf volle Breite setzen
pickupCompany.setWidthFull(); pickupCompany.setWidthFull();
@@ -1064,9 +1073,81 @@ public class AddJobView extends Main implements HasDynamicTitle {
section.add(savePickupAddress); section.add(savePickupAddress);
// Store references to expanded-mode components (excluding titleLayout which stays visible)
pickupExpandedOnlyComponents = section.getChildren().filter(c -> c != titleLayout).toList();
section.getStyle().set("transition", "width 0.3s ease, min-width 0.3s ease");
// Collapsed content (initially hidden)
pickupCollapsedContent = new VerticalLayout();
pickupCollapsedContent.setPadding(false);
pickupCollapsedContent.setSpacing(false);
pickupCollapsedContent.getStyle().set("gap", "var(--lumo-space-xs)");
pickupCollapsedContent.setVisible(false);
section.add(pickupCollapsedContent);
return section; return section;
} }
private void togglePickupCollapse() {
pickupCollapsed = !pickupCollapsed;
if (pickupCollapsed) {
updatePickupCollapsedContent();
pickupExpandedOnlyComponents.forEach(c -> c.setVisible(false));
pickupCollapsedContent.setVisible(true);
pickupSection.setWidth("25%");
pickupSection.getStyle().set("min-width", "150px");
pickupCollapseButton.setIcon(new Icon(VaadinIcon.ANGLE_DOUBLE_RIGHT));
} else {
pickupExpandedOnlyComponents.forEach(c -> c.setVisible(true));
pickupCollapsedContent.setVisible(false);
pickupSection.setWidth("40%");
pickupSection.getStyle().set("min-width", "300px");
pickupCollapseButton.setIcon(new Icon(VaadinIcon.ANGLE_DOUBLE_LEFT));
}
updateAddStationButtonSize();
}
private void updateAddStationButtonSize() {
boolean allCollapsed = pickupCollapsed
&& deliveryStationTiles.stream().allMatch(DeliveryStationTile::isCollapsed);
if (allCollapsed) {
addStationButton.getStyle().set("width", "25%");
addStationButton.getStyle().set("min-width", "150px");
} else {
addStationButton.getStyle().set("width", "40%");
addStationButton.getStyle().set("min-width", "300px");
}
}
private void updatePickupCollapsedContent() {
pickupCollapsedContent.removeAll();
addPickupCollapsedLine(pickupCompany.getValue());
String name = (safeValue(pickupFirstName) + " " + safeValue(pickupLastName)).trim();
addPickupCollapsedLine(name);
String streetLine = (safeValue(pickupStreet) + " " + safeValue(pickupHouseNumber)).trim();
addPickupCollapsedLine(streetLine);
String zipCityLine = (safeValue(pickupZip) + " " + safeValue(pickupCity)).trim();
addPickupCollapsedLine(zipCityLine);
}
private void addPickupCollapsedLine(String text) {
if (text != null && !text.trim().isEmpty()) {
Span span = new Span(text);
span.getStyle().set("font-size", "var(--lumo-font-size-xs)").set("word-break", "break-word")
.set("color", "var(--lumo-secondary-text-color)");
pickupCollapsedContent.add(span);
}
}
private String safeValue(TextField field) {
return field.getValue() != null ? field.getValue().trim() : "";
}
// createDeliverySection() removed - delivery stations are now handled by // createDeliverySection() removed - delivery stations are now handled by
// DeliveryStationTile // DeliveryStationTile