feat: modernisiere UI mit votian-modern Theme und überarbeite Layouts

This commit is contained in:
2026-03-19 14:51:08 +01:00
parent 6afcb60cdd
commit 199671409f
42 changed files with 2235 additions and 616 deletions

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
{
"lumoImports": [
"typography",
"color",
"spacing",
"badge",
"utility"
]
}

View File

@@ -14,7 +14,7 @@ import java.time.Clock;
@SpringBootApplication
@EnableAsync
@EnableScheduling
@Theme("default")
@Theme("votian-modern")
@Push
public class Application implements AppShellConfigurator {

View File

@@ -216,6 +216,7 @@ public class DeliveryStationDialog extends Dialog {
formLayout.setPadding(true);
formLayout.setSpacing(true);
formLayout.setWidthFull();
formLayout.addClassName("dialog-form-panel");
// Company with autocomplete
company = new ComboBox<>(translationHelper.getTranslation("profile.company"));
@@ -694,6 +695,7 @@ public class DeliveryStationDialog extends Dialog {
content.setPadding(false);
content.setSpacing(true);
content.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH);
content.addClassName("dialog-content-panel");
// Task title with template selection
H3 tasksTitle = new H3(translationHelper.getTranslation("addjob.tasks.title"));
@@ -749,10 +751,7 @@ public class DeliveryStationDialog extends Dialog {
VerticalLayout taskContainer = new VerticalLayout();
taskContainer.setPadding(true);
taskContainer.setSpacing(true);
taskContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)");
taskContainer.getStyle().set("border-radius", "var(--lumo-border-radius-m)");
taskContainer.getStyle().set("background-color", "var(--lumo-base-color)");
taskContainer.getStyle().set("position", "relative");
taskContainer.addClassName("dialog-task-card");
// Task type selection
ComboBox<TaskType> taskTypeCombo = new ComboBox<>(translationHelper.getTranslation("addjob.tasks.tasktype"));
@@ -769,13 +768,7 @@ public class DeliveryStationDialog extends Dialog {
// Red X button positioned in top-right corner
Button deleteXButton = new Button(new Icon(VaadinIcon.CLOSE_SMALL));
deleteXButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
deleteXButton.getStyle().set("position", "absolute");
deleteXButton.getStyle().set("top", "8px");
deleteXButton.getStyle().set("right", "8px");
deleteXButton.getStyle().set("z-index", "10");
deleteXButton.getStyle().set("padding", "4px");
deleteXButton.getStyle().set("min-width", "24px");
deleteXButton.getStyle().set("min-height", "24px");
deleteXButton.addClassName("dialog-floating-delete");
deleteXButton.addClickListener(e -> {
int idx = tasksList.getChildren().toList().indexOf(taskContainer);
if (idx >= 0 && idx < tasksState.size()) {
@@ -852,10 +845,7 @@ public class DeliveryStationDialog extends Dialog {
VerticalLayout taskContainer = new VerticalLayout();
taskContainer.setPadding(true);
taskContainer.setSpacing(true);
taskContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)");
taskContainer.getStyle().set("border-radius", "var(--lumo-border-radius-m)");
taskContainer.getStyle().set("background-color", "var(--lumo-base-color)");
taskContainer.getStyle().set("position", "relative");
taskContainer.addClassName("dialog-task-card");
ComboBox<TaskType> taskTypeCombo = new ComboBox<>(translationHelper.getTranslation("addjob.tasks.tasktype"));
taskTypeCombo.setItems(TaskType.values());
@@ -869,13 +859,7 @@ public class DeliveryStationDialog extends Dialog {
Button deleteXButton = new Button(new Icon(VaadinIcon.CLOSE_SMALL));
deleteXButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
deleteXButton.getStyle().set("position", "absolute");
deleteXButton.getStyle().set("top", "8px");
deleteXButton.getStyle().set("right", "8px");
deleteXButton.getStyle().set("z-index", "10");
deleteXButton.getStyle().set("padding", "4px");
deleteXButton.getStyle().set("min-width", "24px");
deleteXButton.getStyle().set("min-height", "24px");
deleteXButton.addClassName("dialog-floating-delete");
deleteXButton.addClickListener(e -> {
int idx = tasksList.getChildren().toList().indexOf(taskContainer);
if (idx >= 0 && idx < tasksState.size()) {

View File

@@ -67,12 +67,10 @@ public class DeliveryStationTile extends VerticalLayout {
setSpacing(true);
setPadding(true);
setWidth("40%");
getStyle().set("min-width", "300px");
getStyle().set("flex-shrink", "0");
getStyle().set("border", "1px solid var(--lumo-contrast-20pct)");
getStyle().set("border-radius", "var(--lumo-border-radius-m)");
getStyle().set("background-color", "var(--lumo-base-color)");
setWidthFull();
addClassName("delivery-station-card");
getStyle().set("min-width", "0");
getStyle().set("box-sizing", "border-box");
// Header with title, collapse button and delete button on one line
title = new H3(translationHelper.getTranslation("addjob.station.delivery", stationNumber));
@@ -150,8 +148,9 @@ public class DeliveryStationTile extends VerticalLayout {
HorizontalLayout streetLayout = new HorizontalLayout();
streetLayout.setWidthFull();
streetLayout.setSpacing(true);
street.setWidth("70%");
houseNumber.setWidth("30%");
streetLayout.getStyle().set("flex-wrap", "wrap");
street.getStyle().set("flex", "3 1 150px").set("min-width", "0");
houseNumber.getStyle().set("flex", "1 1 60px").set("min-width", "0");
streetLayout.add(street, houseNumber);
add(streetLayout);
@@ -174,8 +173,9 @@ public class DeliveryStationTile extends VerticalLayout {
HorizontalLayout zipCityLayout = new HorizontalLayout();
zipCityLayout.setWidthFull();
zipCityLayout.setSpacing(true);
zip.setWidth("30%");
city.setWidth("70%");
zipCityLayout.getStyle().set("flex-wrap", "wrap");
zip.getStyle().set("flex", "1 1 80px").set("min-width", "0");
city.getStyle().set("flex", "3 1 150px").set("min-width", "0");
zipCityLayout.add(zip, city);
add(zipCityLayout);
@@ -192,7 +192,7 @@ public class DeliveryStationTile extends VerticalLayout {
// stays visible)
expandedOnlyComponents = getChildren().filter(c -> c != titleLayout).toList();
getStyle().set("transition", "width 0.3s ease, min-width 0.3s ease");
getStyle().set("transition", "opacity 0.2s ease");
// Collapsed content (initially hidden)
collapsedContent = new VerticalLayout();
@@ -446,14 +446,10 @@ public class DeliveryStationTile extends VerticalLayout {
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) {

View File

@@ -267,6 +267,7 @@ public class PickupStationDialog extends Dialog {
formLayout.setPadding(true);
formLayout.setSpacing(true);
formLayout.setWidthFull();
formLayout.addClassName("dialog-form-panel");
// Customer selection
customerComboBox = new ComboBox<>(translationHelper.getTranslation("addjob.customer.label"));
@@ -760,6 +761,7 @@ public class PickupStationDialog extends Dialog {
content.setSpacing(true);
content.setWidth("720px");
content.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH);
content.addClassName("dialog-content-panel");
// Digital processing + App user
digitalProcessingCheckbox = new Checkbox(translationHelper.getTranslation("profile.settings.digitalprocess"));
@@ -827,8 +829,7 @@ public class PickupStationDialog extends Dialog {
// Info about delivery dates
Span deliveryInfoLabel = new Span(translationHelper.getTranslation("addjob.appointment.delivery.info"));
deliveryInfoLabel.getStyle().set("color", "var(--lumo-secondary-text-color)");
deliveryInfoLabel.getStyle().set("font-style", "italic");
deliveryInfoLabel.addClassName("inline-caption");
deliveryInfoLabel.getStyle().set("margin-top", "var(--lumo-space-m)");
content.add(deliveryInfoLabel);
@@ -853,10 +854,7 @@ public class PickupStationDialog extends Dialog {
VerticalLayout cargoAreaContainer = new VerticalLayout();
cargoAreaContainer.setWidthFull();
cargoAreaContainer.setSpacing(true);
cargoAreaContainer.getStyle().set("background", "var(--lumo-base-color)");
cargoAreaContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)");
cargoAreaContainer.getStyle().set("border-radius", "var(--lumo-border-radius-m)");
cargoAreaContainer.getStyle().set("padding", "var(--lumo-space-m)");
cargoAreaContainer.addClassName("dialog-cargo-card");
H3 cargoTitle = new H3(translationHelper.getTranslation("addjob.tab.cargo"));

View File

@@ -43,9 +43,7 @@ public class StationTile extends VerticalLayout {
setPadding(false);
setSpacing(false);
getStyle().set("border", "1px solid var(--lumo-contrast-20pct)");
getStyle().set("border-radius", "var(--lumo-border-radius-m)");
getStyle().set("background-color", "var(--lumo-base-color)");
addClassName("station-tile");
getStyle().set("cursor", "pointer");
getStyle().set("aspect-ratio", "1 / 1");
getStyle().set("overflow", "hidden");
@@ -196,11 +194,9 @@ public class StationTile extends VerticalLayout {
public void setAddressValidated(boolean validated) {
if (validated) {
getStyle().set("background-color", "rgba(76, 175, 80, 0.15)");
getStyle().set("border", "1px solid rgba(76, 175, 80, 0.3)");
addClassName("validated");
} else {
getStyle().set("background-color", "var(--lumo-base-color)");
getStyle().set("border", "1px solid var(--lumo-contrast-20pct)");
removeClassName("validated");
}
}
}

View File

@@ -15,6 +15,8 @@ public final class ViewToolbar extends Composite<Header> {
}
public ViewToolbar(String viewTitle, boolean showDrawerToggle, Component... components) {
addClassName("view-toolbar-host");
getContent().addClassName("view-toolbar");
addClassNames(Display.FLEX, FlexDirection.COLUMN, JustifyContent.BETWEEN, AlignItems.STRETCH, Gap.MEDIUM,
FlexDirection.Breakpoint.Medium.ROW, AlignItems.Breakpoint.Medium.CENTER);
@@ -22,21 +24,26 @@ public final class ViewToolbar extends Composite<Header> {
if (showDrawerToggle) {
var drawerToggle = new DrawerToggle();
drawerToggle.addClassNames(Margin.NONE);
drawerToggle.addClassName("view-toolbar-toggle");
var title = new H1(viewTitle);
title.addClassNames(FontSize.XLARGE, Margin.NONE, FontWeight.LIGHT);
title.addClassName("view-toolbar-title");
toggleAndTitle = new Div(drawerToggle, title);
} else {
var title = new H1(viewTitle);
title.addClassNames(FontSize.XLARGE, Margin.NONE, FontWeight.LIGHT);
title.addClassName("view-toolbar-title");
toggleAndTitle = new Div(title);
}
toggleAndTitle.addClassNames(Display.FLEX, AlignItems.CENTER);
toggleAndTitle.addClassName("view-toolbar-title-row");
getContent().add(toggleAndTitle);
if (components.length > 0) {
var actions = new Div(components);
actions.addClassNames(Display.FLEX, FlexDirection.COLUMN, JustifyContent.END, Flex.GROW, Gap.SMALL,
FlexDirection.Breakpoint.Medium.ROW);
actions.addClassName("view-toolbar-actions");
getContent().add(actions);
}
}
@@ -45,6 +52,7 @@ public final class ViewToolbar extends Composite<Header> {
var group = new Div(components);
group.addClassNames(Display.FLEX, FlexDirection.COLUMN, AlignItems.STRETCH, Gap.SMALL,
FlexDirection.Breakpoint.Medium.ROW, AlignItems.Breakpoint.Medium.CENTER);
group.addClassName("view-toolbar-group");
return group;
}
}

View File

@@ -33,12 +33,16 @@ public final class AdminLayout extends AppLayout {
public AdminLayout(SecurityService securityService) {
this.securityService = securityService;
addClassName("admin-shell");
setPrimarySection(Section.DRAWER);
getStyle().set("--vaadin-app-layout-drawer-width", "336px");
// Always build the drawer; keep references and toggle visibility on attach and
// after navigation
headerRef = createHeader();
navRef = new Scroller(createSideNav());
navRef.addClassName("app-drawer-scroll");
navRef.setScrollDirection(Scroller.ScrollDirection.VERTICAL);
userMenuRef = createUserMenu();
addToDrawer(headerRef, navRef, userMenuRef);
@@ -62,18 +66,21 @@ public final class AdminLayout extends AppLayout {
private Div createHeader() {
var appLogo = VaadinIcon.SHIELD.create();
appLogo.addClassNames(TextColor.PRIMARY, IconSize.LARGE);
appLogo.addClassName("app-drawer-logo-icon");
var appName = new Span("VotianLT Admin");
appName.addClassNames(FontWeight.SEMIBOLD, FontSize.LARGE);
appName.addClassName("app-drawer-title");
var header = new Div(appLogo, appName);
header.addClassNames(Display.FLEX, Padding.MEDIUM, Gap.MEDIUM, AlignItems.CENTER);
header.addClassName("app-drawer-header");
return header;
}
private Component createSideNav() {
var nav = new SideNav();
nav.addClassNames(Margin.Horizontal.MEDIUM);
nav.addClassName("app-side-nav");
// Only admin-specific menu items
SideNavItem dashboard = new SideNavItem("Dashboard", "admin-dashboard", new Icon(VaadinIcon.DASHBOARD));
@@ -98,6 +105,8 @@ public final class AdminLayout extends AppLayout {
VerticalLayout navContainer = new VerticalLayout();
navContainer.setPadding(false);
navContainer.setSpacing(false);
navContainer.setWidthFull();
navContainer.addClassName("app-nav-container");
navContainer.add(nav);
return navContainer;
@@ -106,15 +115,17 @@ public final class AdminLayout extends AppLayout {
private Component createUserMenu() {
var userMenu = new MenuBar();
userMenu.addThemeVariants(MenuBarVariant.LUMO_TERTIARY_INLINE);
userMenu.addClassNames(Margin.MEDIUM);
userMenu.addClassName("app-user-menu");
// Dynamically updatable components
var avatar = new Avatar();
avatar.addThemeVariants(AvatarVariant.LUMO_XSMALL);
avatar.addClassNames(Margin.Right.SMALL);
avatar.setColorIndex(1); // Different color for admin
avatar.addClassName("app-user-avatar");
var userNameSpan = new Span();
userNameSpan.addClassName("app-user-name");
var userMenuItem = userMenu.addItem(avatar);
userMenuItem.add(userNameSpan);

View File

@@ -3,10 +3,12 @@ package de.assecutor.votianlt.pages.base.ui.view;
import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.DetachEvent;
import com.vaadin.flow.component.HasElement;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.applayout.AppLayout;
import com.vaadin.flow.component.avatar.Avatar;
import com.vaadin.flow.component.avatar.AvatarVariant;
import com.vaadin.flow.component.grid.GridVariant;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.icon.Icon;
@@ -24,9 +26,9 @@ import com.vaadin.flow.router.Layout;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import com.vaadin.flow.shared.Registration;
import de.assecutor.votianlt.model.User;
import de.assecutor.votianlt.model.Language;
import de.assecutor.votianlt.pages.service.AppUserService;
import de.assecutor.votianlt.pages.view.EditProfileView;
import de.assecutor.votianlt.model.Language;
import de.assecutor.votianlt.security.SecurityService;
import de.assecutor.votianlt.service.MessageBadgeUpdateService;
import de.assecutor.votianlt.service.MessageService;
@@ -51,8 +53,9 @@ public final class MainLayout extends AppLayout {
private Scroller navRef;
private Component userMenuRef;
private TreeGrid<MenuTreeItem> tree;
private MenuTreeItem messagesTreeItem;
private long unreadMessagesCount;
private Registration badgeUpdateRegistration;
private final Div viewContainer = new Div();
public MainLayout(SecurityService securityService, MessageService messageService,
MessageBadgeUpdateService messageBadgeUpdateService, AppUserService appUserService) {
@@ -60,10 +63,10 @@ public final class MainLayout extends AppLayout {
this.messageService = messageService;
this.messageBadgeUpdateService = messageBadgeUpdateService;
this.appUserService = appUserService;
addClassName("app-shell");
setPrimarySection(Section.DRAWER);
// Drawer Styles für volle Höhe
getStyle().set("--vaadin-app-layout-drawer-width", "286px");
getStyle().set("--vaadin-app-layout-drawer-width", "336px");
// Always build the drawer; keep references and toggle visibility on attach and
// after navigation
@@ -72,10 +75,15 @@ public final class MainLayout extends AppLayout {
// Scroller für Navigation mit maximaler Höhe
Component sideNav = createSideNav();
navRef = new Scroller(sideNav);
navRef.addClassName("app-drawer-scroll");
navRef.setScrollDirection(Scroller.ScrollDirection.VERTICAL);
userMenuRef = createUserMenu();
addToDrawer(headerRef, navRef, userMenuRef);
viewContainer.addClassName("view-container");
getElement().appendChild(viewContainer.getElement());
updateDrawerVisibility();
// Re-check on attach (new UI/session) and on every navigation cycle
@@ -96,12 +104,15 @@ public final class MainLayout extends AppLayout {
private Div createHeader() {
var appLogo = VaadinIcon.CUBES.create();
appLogo.addClassNames(TextColor.PRIMARY, IconSize.LARGE);
appLogo.addClassName("app-drawer-logo-icon");
var appName = new Span("VotianLT");
appName.addClassNames(FontWeight.SEMIBOLD, FontSize.LARGE);
appName.addClassName("app-drawer-title");
var header = new Div(appLogo, appName);
header.addClassNames(Display.FLEX, Padding.MEDIUM, Gap.MEDIUM, AlignItems.CENTER);
header.addClassName("app-drawer-header");
return header;
}
@@ -117,9 +128,6 @@ public final class MainLayout extends AppLayout {
MenuTreeItem verwaltungItem = new MenuTreeItem(getTranslation("nav.management"), null, VaadinIcon.COG);
MenuTreeItem benutzerItem = new MenuTreeItem(getTranslation("nav.users"), null, VaadinIcon.USER);
// Store reference to messages item for badge updates
messagesTreeItem = nachrichtenItem;
// Add root items
treeData.addItem(null, auftragserstellungItem);
treeData.addItem(null, nachrichtenItem);
@@ -147,50 +155,55 @@ public final class MainLayout extends AppLayout {
// Create Tree
tree = new TreeGrid<>();
tree.setDataProvider(new TreeDataProvider<>(treeData));
tree.addClassNames(Margin.Horizontal.MEDIUM);
tree.setHeight("435px");
tree.addClassName("app-nav-tree");
tree.addThemeVariants(GridVariant.LUMO_NO_BORDER, GridVariant.LUMO_NO_ROW_BORDERS);
tree.setAllRowsVisible(true);
tree.setWidthFull();
// Custom item renderer to show icon and label with badge
tree.addComponentHierarchyColumn(item -> {
var navColumn = tree.addComponentHierarchyColumn(item -> {
HorizontalLayout row = new HorizontalLayout();
row.setAlignItems(FlexComponent.Alignment.CENTER);
row.setSpacing(true);
row.setPadding(false);
row.setMargin(false);
row.setWidthFull();
row.getStyle().set("max-width", "calc(100% - 4px)");
row.addClassName("app-nav-row");
// Icon
if (item.icon() != null) {
Icon icon = item.icon().create();
icon.setSize("var(--lumo-icon-size-s)");
icon.getStyle().set("min-width", "var(--lumo-icon-size-s)");
icon.addClassName("app-nav-icon");
row.add(icon);
}
// Label
Span label = new Span(item.label());
label.getStyle().set("font-size", "var(--lumo-font-size-s)");
label.addClassName("app-nav-label");
row.add(label);
row.setFlexGrow(1, label);
// Badge for messages
if (item == messagesTreeItem && item.badgeCount() > 0) {
Span badge = new Span(String.valueOf(item.badgeCount()));
if ("messages".equals(item.path()) && unreadMessagesCount > 0) {
Span badge = new Span(String.valueOf(unreadMessagesCount));
badge.getElement().getThemeList().add("badge");
badge.getStyle().set("background-color", "var(--lumo-primary-color)");
badge.getStyle().set("color", "#ffffff");
badge.getStyle().set("border-radius", "12px");
badge.getStyle().set("padding", "2px 8px");
badge.getStyle().set("font-size", "11px");
badge.getStyle().set("font-weight", "bold");
badge.getStyle().set("min-width", "18px");
badge.getStyle().set("text-align", "center");
badge.getStyle().set("margin-left", "auto");
badge.addClassName("app-nav-badge");
row.add(badge);
}
// Expand/collapse indicator for parent nodes
Icon chevron = VaadinIcon.ANGLE_RIGHT.create();
chevron.setSize("var(--lumo-icon-size-s)");
chevron.addClassName("nav-expand-icon");
row.add(chevron);
return row;
});
navColumn.setAutoWidth(false);
navColumn.setFlexGrow(1);
// Handle selection/navigation
tree.addSelectionListener(event -> {
@@ -208,6 +221,8 @@ public final class MainLayout extends AppLayout {
VerticalLayout navContainer = new VerticalLayout();
navContainer.setPadding(false);
navContainer.setSpacing(false);
navContainer.setWidthFull();
navContainer.addClassName("app-nav-container");
navContainer.add(tree);
return navContainer;
@@ -221,31 +236,14 @@ public final class MainLayout extends AppLayout {
return;
}
long unreadCount = resolveUnreadMessageCount();
// Get current data provider and update the messages item
TreeDataProvider<MenuTreeItem> dataProvider = (TreeDataProvider<MenuTreeItem>) tree.getDataProvider();
TreeData<MenuTreeItem> treeData = dataProvider.getTreeData();
// Find and update the messages item with new badge count
treeData.getChildren(null).stream().filter(item -> "messages".equals(item.path())).findFirst()
.ifPresent(oldItem -> {
MenuTreeItem newItem = new MenuTreeItem(getTranslation("nav.messages"), "messages",
VaadinIcon.ENVELOPE, unreadCount);
messagesTreeItem = newItem;
// Refresh to show updated badge
dataProvider.refreshAll();
});
unreadMessagesCount = resolveUnreadMessageCount();
tree.getDataProvider().refreshAll();
}
/**
* Record representing a menu item in the tree
*/
private record MenuTreeItem(String label, String path, VaadinIcon icon, long badgeCount) {
MenuTreeItem(String label, String path, VaadinIcon icon) {
this(label, path, icon, 0);
}
private record MenuTreeItem(String label, String path, VaadinIcon icon) {
@Override
public boolean equals(Object o) {
if (this == o)
@@ -294,15 +292,17 @@ public final class MainLayout extends AppLayout {
private Component createUserMenu() {
var userMenu = new MenuBar();
userMenu.addThemeVariants(MenuBarVariant.LUMO_TERTIARY_INLINE);
userMenu.addClassNames(Margin.MEDIUM);
userMenu.addClassName("app-user-menu");
// Dynamisch aktualisierbare Komponenten
var avatar = new Avatar();
avatar.addThemeVariants(AvatarVariant.LUMO_XSMALL);
avatar.addClassNames(Margin.Right.SMALL);
avatar.setColorIndex(5);
avatar.addClassName("app-user-avatar");
var userNameSpan = new Span();
userNameSpan.addClassName("app-user-name");
var userMenuItem = userMenu.addItem(avatar);
userMenuItem.add(userNameSpan);
@@ -335,12 +335,19 @@ public final class MainLayout extends AppLayout {
// Drawer-Layout anpassen nach kurzer Verzögerung
ui.access(() -> {
getElement().executeJs(
"setTimeout(() => {" + " const drawer = this.shadowRoot?.querySelector('[part=drawer]');"
+ " if (drawer) {" + " drawer.style.display = 'flex';"
+ " drawer.style.flexDirection = 'column';" + " drawer.style.height = '100vh';"
+ " const scroller = drawer.querySelector('vaadin-scroller');" + " if (scroller) {"
+ " scroller.style.flex = '1 1 auto';" + " scroller.style.minHeight = '0';"
+ " }" + " }" + "}, 100);");
"setTimeout(() => {"
+ " const drawer = this.shadowRoot?.querySelector('[part=drawer]');"
+ " if (drawer) {"
+ " drawer.style.display = 'flex';"
+ " drawer.style.flexDirection = 'column';"
+ " drawer.style.height = '100vh';"
+ " const scroller = drawer.querySelector('vaadin-scroller');"
+ " if (scroller) {"
+ " scroller.style.flex = '1 1 auto';"
+ " scroller.style.minHeight = '0';"
+ " }"
+ " }"
+ "}, 100);");
});
// Apply user's preferred language immediately after login
@@ -393,6 +400,14 @@ public final class MainLayout extends AppLayout {
};
}
@Override
public void showRouterLayoutContent(HasElement content) {
viewContainer.getElement().removeAllChildren();
if (content != null) {
viewContainer.getElement().appendChild(content.getElement());
}
}
@Override
protected void onDetach(DetachEvent detachEvent) {
if (badgeUpdateRegistration != null) {

View File

@@ -42,6 +42,7 @@ public class AddAppUserView extends VerticalLayout implements HasDynamicTitle {
setSizeFull();
setPadding(true);
setSpacing(true);
addClassName("form-page");
// Set field labels via i18n
designationField.setLabel(getTranslation("addappuser.designation"));
@@ -62,17 +63,17 @@ public class AddAppUserView extends VerticalLayout implements HasDynamicTitle {
contentContainer.setMaxWidth("90%");
contentContainer.setSpacing(true);
contentContainer.setPadding(true);
contentContainer.getStyle().set("background", "var(--lumo-contrast-5pct)");
contentContainer.getStyle().set("border-radius", "var(--lumo-border-radius)");
contentContainer.getStyle().set("box-shadow", "var(--lumo-box-shadow-s)");
contentContainer.addClassNames("form-shell", "form-card");
// Header with title and back button
HorizontalLayout header = new HorizontalLayout();
header.setAlignItems(FlexComponent.Alignment.CENTER);
header.setSpacing(true);
header.addClassName("form-header");
H2 title = new H2(getTranslation("addappuser.title"));
title.getStyle().set("margin", "0");
title.addClassName("form-title");
Button backButton = new Button(getTranslation("button.back"), new Icon(VaadinIcon.ARROW_LEFT));
backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);

View File

@@ -73,11 +73,14 @@ public class AddCompanyView extends Main implements HasDynamicTitle {
// Zentriere die Inhalte vertikal und horizontal
formLayout.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
formLayout.setSpacing(true);
formLayout.setSizeUndefined(); // Inhalt eng setzen
formLayout.setWidthFull();
formLayout.setMaxWidth("620px");
formLayout.addClassNames("form-shell", "form-card");
setSizeFull();
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL);
addClassName("form-page");
add(new ViewToolbar(getTranslation("addcompany.title")));
add(formLayout);

View File

@@ -150,11 +150,13 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
VerticalLayout container = new VerticalLayout();
container.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
container.setSpacing(true);
container.addClassNames("form-shell", "form-card");
container.add(formLayout);
setSizeFull();
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL);
addClassName("form-page");
add(new ViewToolbar(getTranslation("addcustomer.title")));
add(container);

View File

@@ -10,7 +10,6 @@ import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.datepicker.DatePicker;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.timepicker.TimePicker;
import com.vaadin.flow.component.html.H3;
import com.vaadin.flow.component.html.Main;
@@ -67,6 +66,7 @@ import de.assecutor.votianlt.pages.base.ui.component.DeliveryStationTile;
import de.assecutor.votianlt.pages.base.ui.component.StationTile;
import de.assecutor.votianlt.pages.base.ui.component.PickupStationDialog;
import de.assecutor.votianlt.pages.base.ui.component.DeliveryStationDialog;
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
@@ -338,9 +338,9 @@ public class AddJobView extends Main implements HasDynamicTitle {
setSizeFull();
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL);
addClassNames("form-page", "add-job-view");
H2 title = new H2(getTranslation("addjob.title"));
add(title);
add(new ViewToolbar(getTranslation("addjob.title")));
// Add content directly (no tabs)
add(createCustomerAndAddressesTab());
@@ -350,6 +350,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
submitButtonLayout.setWidthFull();
submitButtonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
submitButtonLayout.setPadding(true);
submitButtonLayout.addClassNames("surface-panel", "form-shell");
submitButtonLayout.add(submitButton);
submitButtonLayout.setVisible(false);
@@ -361,14 +362,13 @@ public class AddJobView extends Main implements HasDynamicTitle {
tabContent.setWidthFull();
tabContent.setPadding(true);
tabContent.setSpacing(true);
tabContent.addClassNames("form-shell", "form-card");
// 3x3 Grid container for station tiles
stationsGridContainer = new Div();
stationsGridContainer.getStyle().set("display", "grid");
stationsGridContainer.getStyle().set("grid-template-columns", "repeat(4, 1fr)");
stationsGridContainer.getStyle().set("gap", "var(--lumo-space-m)");
stationsGridContainer.getStyle().set("padding", "var(--lumo-space-s)");
stationsGridContainer.setWidthFull();
stationsGridContainer.addClassName("stations-grid");
// Pickup tile (always present)
pickupTile = new StationTile(StationTile.StationType.PICKUP, 0, getTranslation("addjob.section.pickup"), false);
@@ -394,6 +394,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
priceAndDetailsSection.setPadding(false);
priceAndDetailsSection.setSpacing(true);
priceAndDetailsSection.setVisible(false);
priceAndDetailsSection.addClassName("content-panel");
// "Stationen übernehmen" Button
applyStationsButton = new Button(getTranslation("addjob.stations.apply"));
@@ -413,6 +414,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
routeInfoBox.setWidthFull();
routeInfoBox.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH);
routeInfoBox.setVisible(false); // Initial versteckt
routeInfoBox.addClassName("route-card");
H3 routeTitle = new H3(getTranslation("addjob.route.title"));
routeTitle.getStyle().set("margin", "0");
@@ -456,6 +458,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
manualRouteInputBox.setWidthFull();
manualRouteInputBox.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH);
manualRouteInputBox.setVisible(true); // Initial sichtbar
manualRouteInputBox.addClassName("route-card");
H3 manualRouteTitle = new H3(getTranslation("addjob.route.manual.title"));
manualRouteTitle.getStyle().set("margin", "0");
@@ -514,6 +517,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
servicesGrid.setWidthFull();
servicesGrid.setHeight("250px");
servicesGrid.setItems(selectedServices);
servicesGrid.addClassName("data-grid");
servicesGrid.addColumn(entry -> entry.getService().getName()).setHeader(getTranslation("common.service"))
.setSortable(true);
@@ -585,6 +589,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
summaryLayout.getStyle().set("background-color", "var(--lumo-contrast-5pct)");
summaryLayout.setWidthFull();
summaryLayout.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH);
summaryLayout.addClassName("summary-card");
H3 summaryTitle = new H3(getTranslation("addjob.summary.title"));
summaryTitle.getStyle().set("margin", "0");
@@ -635,6 +640,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
private Div createAddStationButton() {
Div button = new Div();
button.addClassName("add-station-tile");
button.getStyle().set("border", "2px dashed var(--lumo-contrast-30pct)");
button.getStyle().set("border-radius", "var(--lumo-border-radius-m)");
button.getStyle().set("display", "flex");
@@ -664,12 +670,14 @@ public class AddJobView extends Main implements HasDynamicTitle {
private Div createStationSlot(Component content, Span distanceChip) {
Div slot = new Div();
slot.addClassName("station-slot");
slot.getStyle().set("display", "flex");
slot.getStyle().set("flex-direction", "column");
slot.getStyle().set("align-items", "center");
slot.getStyle().set("gap", "var(--lumo-space-s)");
slot.setWidthFull();
content.getElement().getClassList().add("station-slot-content");
content.getElement().getStyle().set("width", "100%");
slot.add(content);

View File

@@ -61,24 +61,29 @@ public class AdminDashboardView extends Main implements HasDynamicTitle {
setSizeFull();
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
LumoUtility.Padding.MEDIUM);
addClassName("dashboard-view");
// Header
H1 title = new H1(getTranslation("admindashboard.title"));
title.addClassNames(LumoUtility.Margin.Bottom.MEDIUM, LumoUtility.Margin.Top.NONE);
title.addClassName("dashboard-title");
HorizontalLayout header = new HorizontalLayout(title);
header.setAlignItems(FlexComponent.Alignment.CENTER);
header.setWidthFull();
header.addClassNames("surface-panel", "dashboard-header");
// Statistics container
statisticsContainer = new Div();
statisticsContainer.setSizeFull();
statisticsContainer.addClassName("dashboard-content");
// Content container
VerticalLayout content = new VerticalLayout(header, statisticsContainer);
content.setSizeFull();
content.setPadding(false);
content.setSpacing(true);
content.addClassName("dashboard-sections");
add(content);
@@ -114,6 +119,7 @@ public class AdminDashboardView extends Main implements HasDynamicTitle {
VerticalLayout mainLayout = new VerticalLayout();
mainLayout.setPadding(false);
mainLayout.setSpacing(true);
mainLayout.addClassName("dashboard-sections");
// System overview section
mainLayout.add(createSystemOverviewSection());
@@ -136,15 +142,13 @@ public class AdminDashboardView extends Main implements HasDynamicTitle {
private VerticalLayout createSystemOverviewSection() {
VerticalLayout section = new VerticalLayout();
section.setPadding(false);
section.addClassName(LumoUtility.Background.CONTRAST_5);
section.getStyle().set("border-radius", "8px").set("padding", "1rem");
section.addClassNames("surface-panel", "dashboard-section");
H3 title = new H3(getTranslation("admindashboard.section.overview"));
title.addClassName(LumoUtility.Margin.Bottom.MEDIUM);
title.addClassNames(LumoUtility.Margin.Bottom.MEDIUM, "dashboard-section-title");
HorizontalLayout cards = new HorizontalLayout();
cards.setWidthFull();
cards.setSpacing(true);
Div cards = new Div();
cards.addClassName("dashboard-grid");
// Total jobs card
long totalJobs = jobRepository.count();
@@ -173,15 +177,13 @@ public class AdminDashboardView extends Main implements HasDynamicTitle {
private VerticalLayout createJobStatisticsSection() {
VerticalLayout section = new VerticalLayout();
section.setPadding(false);
section.addClassName(LumoUtility.Background.CONTRAST_5);
section.getStyle().set("border-radius", "8px").set("padding", "1rem");
section.addClassNames("surface-panel", "dashboard-section");
H3 title = new H3(getTranslation("admindashboard.section.jobs"));
title.addClassName(LumoUtility.Margin.Bottom.MEDIUM);
title.addClassNames(LumoUtility.Margin.Bottom.MEDIUM, "dashboard-section-title");
HorizontalLayout cards = new HorizontalLayout();
cards.setWidthFull();
cards.setSpacing(true);
Div cards = new Div();
cards.addClassName("dashboard-grid");
// Jobs by status
try {
@@ -214,15 +216,13 @@ public class AdminDashboardView extends Main implements HasDynamicTitle {
private VerticalLayout createTaskStatisticsSection() {
VerticalLayout section = new VerticalLayout();
section.setPadding(false);
section.addClassName(LumoUtility.Background.CONTRAST_5);
section.getStyle().set("border-radius", "8px").set("padding", "1rem");
section.addClassNames("surface-panel", "dashboard-section");
H3 title = new H3(getTranslation("admindashboard.section.tasks"));
title.addClassName(LumoUtility.Margin.Bottom.MEDIUM);
title.addClassNames(LumoUtility.Margin.Bottom.MEDIUM, "dashboard-section-title");
HorizontalLayout cards = new HorizontalLayout();
cards.setWidthFull();
cards.setSpacing(true);
Div cards = new Div();
cards.addClassName("dashboard-grid");
// Total tasks
long totalTasks = taskRepository.count();
@@ -251,15 +251,13 @@ public class AdminDashboardView extends Main implements HasDynamicTitle {
private VerticalLayout createUserStatisticsSection() {
VerticalLayout section = new VerticalLayout();
section.setPadding(false);
section.addClassName(LumoUtility.Background.CONTRAST_5);
section.getStyle().set("border-radius", "8px").set("padding", "1rem");
section.addClassNames("surface-panel", "dashboard-section");
H3 title = new H3(getTranslation("admindashboard.section.users"));
title.addClassName(LumoUtility.Margin.Bottom.MEDIUM);
title.addClassNames(LumoUtility.Margin.Bottom.MEDIUM, "dashboard-section-title");
HorizontalLayout cards = new HorizontalLayout();
cards.setWidthFull();
cards.setSpacing(true);
Div cards = new Div();
cards.addClassName("dashboard-grid");
// Content statistics
long totalPhotos = photoRepository.count();
@@ -285,15 +283,13 @@ public class AdminDashboardView extends Main implements HasDynamicTitle {
private VerticalLayout createSystemHealthSection() {
VerticalLayout section = new VerticalLayout();
section.setPadding(false);
section.addClassName(LumoUtility.Background.CONTRAST_5);
section.getStyle().set("border-radius", "8px").set("padding", "1rem");
section.addClassNames("surface-panel", "dashboard-section");
H3 title = new H3(getTranslation("admindashboard.section.health"));
title.addClassName(LumoUtility.Margin.Bottom.MEDIUM);
title.addClassNames(LumoUtility.Margin.Bottom.MEDIUM, "dashboard-section-title");
HorizontalLayout cards = new HorizontalLayout();
cards.setWidthFull();
cards.setSpacing(true);
Div cards = new Div();
cards.addClassName("dashboard-grid");
// Database connection status
try {
@@ -327,31 +323,31 @@ public class AdminDashboardView extends Main implements HasDynamicTitle {
private Div createStatCard(String title, String value, VaadinIcon icon, String color) {
Div card = new Div();
card.addClassName(LumoUtility.Background.BASE);
card.getStyle().set("border-radius", "8px").set("padding", "1rem")
.set("box-shadow", "0 2px 4px rgba(0,0,0,0.1)").set("min-width", "200px")
.set("border-left", "4px solid var(--lumo-" + color + "-color, #007bff)");
card.addClassNames("dashboard-stat-card", "accent-" + color);
HorizontalLayout header = new HorizontalLayout();
header.setAlignItems(FlexComponent.Alignment.CENTER);
header.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
header.addClassName("dashboard-stat-card-header");
Icon cardIcon = icon.create();
cardIcon.getStyle().set("color", "var(--lumo-" + color + "-color, #007bff)");
cardIcon.addClassNames("dashboard-stat-icon", "accent-" + color);
Span titleSpan = new Span(title);
titleSpan.addClassName(LumoUtility.FontSize.SMALL);
titleSpan.getStyle().set("color", "var(--lumo-secondary-text-color)");
titleSpan.addClassName("dashboard-stat-title");
header.add(titleSpan, cardIcon);
Span valueSpan = new Span(value);
valueSpan.addClassName(LumoUtility.FontSize.XLARGE);
valueSpan.addClassName(LumoUtility.FontWeight.BOLD);
valueSpan.addClassName("dashboard-stat-value");
VerticalLayout content = new VerticalLayout(header, valueSpan);
content.setPadding(false);
content.setSpacing(false);
content.addClassName("dashboard-stat-content");
card.add(content);
return card;
@@ -361,4 +357,4 @@ public class AdminDashboardView extends Main implements HasDynamicTitle {
public String getPageTitle() {
return getTranslation("page.title.admin.dashboard");
}
}
}

View File

@@ -1,13 +1,14 @@
package de.assecutor.votianlt.pages.view;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.model.PriceTable;
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
import de.assecutor.votianlt.pages.base.ui.view.AdminLayout;
import de.assecutor.votianlt.repository.PriceTableRepository;
import jakarta.annotation.security.RolesAllowed;
@@ -28,13 +29,13 @@ public class AdminPricetableView extends VerticalLayout implements HasDynamicTit
setPadding(false);
getStyle().set("margin", "14px");
setWidth("90%");
H2 title = new H2(getTranslation("adminpricetable.title"));
add(title);
addClassName("admin-form-view");
add(new ViewToolbar(getTranslation("adminpricetable.title")));
VerticalLayout fieldsLayout = new VerticalLayout();
fieldsLayout.setSpacing(true);
fieldsLayout.setPadding(false);
fieldsLayout.addClassNames("form-shell", "form-card");
monthlyBasePackage = new TextField();
monthlyBasePackage.setLabel(getTranslation("adminpricetable.field.monthly"));
@@ -58,8 +59,12 @@ public class AdminPricetableView extends VerticalLayout implements HasDynamicTit
Button saveButton = new Button(getTranslation("button.savechanges"));
saveButton.getStyle().set("margin-top", "20px");
saveButton.addClickListener(e -> savePriceTable());
saveButton.addThemeVariants(com.vaadin.flow.component.button.ButtonVariant.LUMO_PRIMARY);
add(saveButton);
Div actions = new Div(saveButton);
actions.addClassNames("form-shell", "simple-card");
add(fieldsLayout, actions);
// Load existing data
loadPriceTable();

View File

@@ -3,15 +3,14 @@ package de.assecutor.votianlt.pages.view;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.icon.Icon;
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.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.model.AppUser;
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
import de.assecutor.votianlt.pages.service.AppUserService;
import jakarta.annotation.security.RolesAllowed;
import org.springframework.beans.factory.annotation.Autowired;
@@ -30,26 +29,18 @@ public class AppUserView extends VerticalLayout implements HasDynamicTitle {
setSizeFull();
setPadding(true);
setSpacing(true);
// Header mit Titel und Button
HorizontalLayout header = new HorizontalLayout();
header.setWidthFull();
header.setAlignItems(FlexComponent.Alignment.CENTER);
H2 title = new H2(getTranslation("appuser.title"));
title.getStyle().set("margin", "0");
addClassName("data-view");
Button addButton = new Button(getTranslation("appuser.button.add"), new Icon(VaadinIcon.PLUS));
addButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
addButton.addClickListener(e -> navigateToAddAppUser());
header.add(title, addButton);
header.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
add(header);
add(new ViewToolbar(getTranslation("appuser.title"), addButton));
// Grid für App-Nutzer
appUserGrid = new Grid<>(AppUser.class, false);
appUserGrid.setSizeFull();
appUserGrid.addClassName("data-grid");
// Grid-Spalten konfigurieren
appUserGrid.addColumn(AppUser::getBezeichnung).setHeader(getTranslation("appuser.column.designation"))
@@ -75,7 +66,11 @@ public class AppUserView extends VerticalLayout implements HasDynamicTitle {
}
});
add(appUserGrid);
Div gridPanel = new Div(appUserGrid);
gridPanel.addClassNames("surface-panel", "data-grid-panel");
gridPanel.setSizeFull();
add(gridPanel);
// Daten laden
loadAppUsers();

View File

@@ -31,8 +31,9 @@ public class AuthenticatedStartView extends VerticalLayout implements HasDynamic
setSizeFull();
setPadding(false);
setSpacing(false);
setSpacing(true);
setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH);
addClassName("dashboard-home-view");
// Hero Section for authenticated users
add(createAuthenticatedHeroSection());
@@ -53,10 +54,8 @@ public class AuthenticatedStartView extends VerticalLayout implements HasDynamic
heroSection.setPadding(true);
heroSection.setSpacing(true);
heroSection.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
heroSection.getStyle().set("background",
"linear-gradient(135deg, var(--lumo-primary-color-10pct), var(--lumo-primary-color-50pct))");
heroSection.getStyle().set("min-height", "300px");
heroSection.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
heroSection.addClassNames("page-shell", "hero-panel", "landing-hero");
// Welcome message for authenticated user
User currentUser = securityService.getCurrentDatabaseUser();
@@ -64,14 +63,10 @@ public class AuthenticatedStartView extends VerticalLayout implements HasDynamic
? currentUser.getFirstname() + " " + currentUser.getName()
: securityService.getCurrentUsername();
H1 welcomeTitle = new H1(getTranslation("dashboard.welcome", displayName));
welcomeTitle.getStyle().set("text-align", "center");
welcomeTitle.getStyle().set("color", "var(--lumo-primary-text-color)");
welcomeTitle.getStyle().set("margin-bottom", "var(--lumo-space-l)");
welcomeTitle.addClassName("hero-panel-title");
Paragraph welcomeDescription = new Paragraph(getTranslation("dashboard.description"));
welcomeDescription.getStyle().set("text-align", "center");
welcomeDescription.getStyle().set("max-width", "600px");
welcomeDescription.getStyle().set("font-size", "var(--lumo-font-size-l)");
welcomeDescription.addClassName("hero-panel-text");
heroSection.add(welcomeTitle, welcomeDescription);
return heroSection;
@@ -83,26 +78,18 @@ public class AuthenticatedStartView extends VerticalLayout implements HasDynamic
systemSection.setPadding(true);
systemSection.setSpacing(true);
systemSection.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
systemSection.getStyle().set("background-color", "var(--lumo-base-color)");
systemSection.addClassNames("page-shell", "surface-panel", "section-panel");
// Section Header
H2 systemTitle = new H2(getTranslation("dashboard.system.title"));
systemTitle.getStyle().set("color", "var(--lumo-primary-color)");
systemTitle.getStyle().set("text-align", "center");
systemTitle.addClassName("section-title");
Paragraph systemIntro = new Paragraph(getTranslation("dashboard.system.intro"));
systemIntro.getStyle().set("text-align", "center");
systemIntro.getStyle().set("max-width", "800px");
systemIntro.getStyle().set("margin-bottom", "var(--lumo-space-xl)");
systemIntro.addClassName("section-intro");
// Features Grid - using Div with CSS flexbox for proper centering
Div featuresGrid = new Div();
featuresGrid.getStyle().set("display", "flex");
featuresGrid.getStyle().set("flex-wrap", "wrap");
featuresGrid.getStyle().set("justify-content", "center");
featuresGrid.getStyle().set("align-items", "stretch");
featuresGrid.getStyle().set("gap", "var(--lumo-space-l)");
featuresGrid.getStyle().set("width", "100%");
featuresGrid.addClassName("tile-grid");
// Feature Cards
featuresGrid.add(
@@ -122,32 +109,18 @@ public class AuthenticatedStartView extends VerticalLayout implements HasDynamic
card.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
card.setPadding(true);
card.setSpacing(true);
card.getStyle().set("background-color", "var(--lumo-base-color)");
card.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)");
card.getStyle().set("border-radius", "var(--lumo-border-radius-m)");
card.getStyle().set("box-shadow", "var(--lumo-box-shadow-s)");
card.setWidth("300px");
card.setMinWidth("300px");
card.setMaxWidth("300px");
card.setHeight("100%");
card.getStyle().set("flex-grow", "0");
card.getStyle().set("flex-shrink", "0");
card.setWidthFull();
card.addClassName("feature-card");
Icon icon = iconType.create();
icon.setSize("48px");
icon.getStyle().set("color", "var(--lumo-primary-color)");
icon.getStyle().set("margin-bottom", "var(--lumo-space-m)");
icon.addClassName("feature-card-icon");
H3 cardTitle = new H3(title);
cardTitle.getStyle().set("text-align", "center");
cardTitle.getStyle().set("margin", "0");
cardTitle.getStyle().set("color", "var(--lumo-primary-text-color)");
cardTitle.addClassName("feature-card-title");
Paragraph cardDescription = new Paragraph(description);
cardDescription.getStyle().set("text-align", "center");
cardDescription.getStyle().set("font-size", "var(--lumo-font-size-s)");
cardDescription.getStyle().set("color", "var(--lumo-secondary-text-color)");
cardDescription.getStyle().set("margin", "0");
cardDescription.addClassName("feature-card-description");
card.add(icon, cardTitle, cardDescription);
return card;
@@ -159,15 +132,13 @@ public class AuthenticatedStartView extends VerticalLayout implements HasDynamic
appSection.setPadding(true);
appSection.setSpacing(true);
appSection.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
appSection.getStyle().set("background-color", "var(--lumo-contrast-5pct)");
appSection.addClassNames("page-shell", "surface-panel", "app-overview-panel");
H2 appTitle = new H2(getTranslation("dashboard.app.title"));
appTitle.getStyle().set("color", "var(--lumo-primary-color)");
appTitle.getStyle().set("text-align", "center");
appTitle.addClassName("section-title");
Paragraph appDescription = new Paragraph(getTranslation("dashboard.app.description"));
appDescription.getStyle().set("text-align", "center");
appDescription.getStyle().set("max-width", "600px");
appDescription.addClassName("section-intro");
appSection.add(appTitle, appDescription);
return appSection;
@@ -179,18 +150,14 @@ public class AuthenticatedStartView extends VerticalLayout implements HasDynamic
footer.setPadding(true);
footer.setSpacing(true);
footer.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
footer.getStyle().set("background-color", "var(--lumo-contrast-10pct)");
footer.getStyle().set("border-top", "1px solid var(--lumo-contrast-20pct)");
footer.getStyle().set("margin-top", "auto");
footer.addClassNames("page-shell", "surface-panel", "footer-panel");
HorizontalLayout footerContent = new HorizontalLayout();
footerContent.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
footerContent.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
Paragraph copyright = new Paragraph(getTranslation("dashboard.footer.copyright"));
copyright.getStyle().set("color", "var(--lumo-secondary-text-color)");
copyright.getStyle().set("font-size", "var(--lumo-font-size-s)");
copyright.getStyle().set("margin", "0");
copyright.addClassName("footer-version");
footerContent.add(copyright);
footer.add(footerContent);

View File

@@ -128,6 +128,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
setSizeFull();
setPadding(true);
setSpacing(true);
addClassName("data-view");
}
/**
@@ -216,9 +217,9 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
private Div createJobDetailsSection() {
Div section = new Div();
section.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)")
.set("margin-bottom", "var(--lumo-space-m)").set("width", "100%").set("box-sizing", "border-box");
section.setWidthFull();
section.addClassName("invoice-summary-card");
section.getStyle().set("margin-bottom", "var(--lumo-space-m)");
H3 sectionTitle = new H3(getTranslation("createinvoice.section.job"));
section.add(sectionTitle);
@@ -243,13 +244,11 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
private Div createRouteInfoSection() {
Div section = new Div();
section.getStyle().set("border", "1px solid var(--lumo-primary-color-50pct)")
.set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)")
.set("margin-bottom", "var(--lumo-space-m)").set("width", "100%").set("box-sizing", "border-box")
.set("background-color", "var(--lumo-primary-color-10pct)");
section.setWidthFull();
section.addClassNames("detail-card", "detail-card--accent");
section.getStyle().set("margin-bottom", "var(--lumo-space-m)");
H3 sectionTitle = new H3(getTranslation("createinvoice.section.route"));
sectionTitle.getStyle().set("color", "var(--lumo-primary-text-color)");
section.add(sectionTitle);
VerticalLayout routeInfo = new VerticalLayout();
@@ -275,9 +274,9 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
private Div createServicesSelectionSection() {
servicesSection = new Div();
servicesSection.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)")
.set("margin-bottom", "var(--lumo-space-m)").set("width", "100%").set("box-sizing", "border-box");
servicesSection.setWidthFull();
servicesSection.addClassName("services-panel");
servicesSection.getStyle().set("margin-bottom", "var(--lumo-space-m)");
H3 sectionTitle = new H3(getTranslation("createinvoice.section.services"));
servicesSection.add(sectionTitle);
@@ -286,6 +285,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
servicesGrid = new Grid<>();
servicesGrid.setWidthFull();
servicesGrid.setAllRowsVisible(true);
servicesGrid.addClassName("data-grid");
// Service name column (read-only)
servicesGrid.addColumn(row -> {
@@ -333,9 +333,9 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
private Div createSummarySection() {
Div section = new Div();
section.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)")
.set("margin-bottom", "var(--lumo-space-m)").set("width", "100%").set("box-sizing", "border-box");
section.setWidthFull();
section.addClassName("invoice-section-card");
section.getStyle().set("margin-bottom", "var(--lumo-space-m)");
H3 sectionTitle = new H3(getTranslation("createinvoice.section.summary"));
section.add(sectionTitle);
@@ -347,7 +347,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
BigDecimal totalAmount = netAmount.add(vatAmount);
Div priceTable = new Div();
priceTable.getStyle().set("width", "100%");
priceTable.addClassName("price-table");
priceTable.add(createPriceRow(getTranslation("createinvoice.summary.net") + ":",
netAmount.setScale(2, RoundingMode.HALF_UP) + "", false));
@@ -360,17 +360,16 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
private Div createPriceRow(String label, String value, boolean bold) {
Div row = new Div();
row.getStyle().set("display", "flex").set("justify-content", "space-between").set("padding", "4px 0");
row.addClassName("price-row");
if (bold) {
row.addClassName("price-row--strong");
}
Span labelSpan = new Span(label);
labelSpan.getStyle().set("padding-right", "8px");
labelSpan.addClassName("price-row-label");
Span valueSpan = new Span(value);
valueSpan.getStyle().set("white-space", "nowrap");
if (bold) {
labelSpan.getStyle().set("font-weight", "bold");
valueSpan.getStyle().set("font-weight", "bold");
}
valueSpan.addClassName("price-row-value");
row.add(labelSpan, valueSpan);
return row;
@@ -668,10 +667,10 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
IFrame pdfFrame = new IFrame();
pdfFrame.setWidth("100%");
pdfFrame.setHeight("100%");
pdfFrame.addClassName("invoice-generator-frame");
String base64Pdf = java.util.Base64.getEncoder().encodeToString(pdfBytes);
pdfFrame.getElement().setAttribute("src", "data:application/pdf;base64," + base64Pdf);
pdfFrame.getStyle().set("border", "none");
Button closeButton = new Button(getTranslation("button.close"), e -> pdfDialog.close());
closeButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
@@ -705,10 +704,10 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
IFrame pdfFrame = new IFrame();
pdfFrame.setWidth("100%");
pdfFrame.setHeight("100%");
pdfFrame.addClassName("invoice-generator-frame");
String base64Pdf = java.util.Base64.getEncoder().encodeToString(pdfBytes);
pdfFrame.getElement().setAttribute("src", "data:application/pdf;base64," + base64Pdf);
pdfFrame.getStyle().set("border", "none");
Button downloadButton = new Button("Herunterladen", e -> {
parent.getElement()

View File

@@ -4,7 +4,7 @@ import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Main;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.HasDynamicTitle;
@@ -12,6 +12,7 @@ import com.vaadin.flow.router.Menu;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.theme.lumo.LumoUtility;
import de.assecutor.votianlt.model.Customer;
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
import de.assecutor.votianlt.pages.service.CustomerService;
import java.time.Clock;
@@ -38,15 +39,21 @@ public class CustomersView extends Main implements HasDynamicTitle {
todoGrid.setItems(query -> todoService.list(toSpringPageRequest(query)).stream());
todoGrid.addColumn(Customer::getCompanyName).setHeader(getTranslation("customers.column.company"));
todoGrid.setSizeFull();
todoGrid.addClassName("data-grid");
setSizeFull();
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL);
addClassName("data-view");
H2 title = new H2(getTranslation("customers.title"));
add(title);
ViewToolbar toolbar = new ViewToolbar(getTranslation("customers.title"), createBtn);
createBtn.addClassName("toolbar-primary-action");
add(todoGrid);
Div gridPanel = new Div(todoGrid);
gridPanel.addClassNames("surface-panel", "data-grid-panel");
gridPanel.setSizeFull();
add(toolbar, gridPanel);
}
private void addCustomer() {

View File

@@ -46,6 +46,7 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
this.appUserService = appUserService;
setSizeFull();
setPadding(true);
addClassName("form-page");
setSpacing(true);
// Set field labels via i18n
@@ -67,17 +68,16 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
contentContainer.setMaxWidth("90%");
contentContainer.setSpacing(true);
contentContainer.setPadding(true);
contentContainer.getStyle().set("background", "var(--lumo-contrast-5pct)");
contentContainer.getStyle().set("border-radius", "var(--lumo-border-radius)");
contentContainer.getStyle().set("box-shadow", "var(--lumo-box-shadow-s)");
contentContainer.addClassNames("form-shell", "form-card");
// Header with title and back button
HorizontalLayout header = new HorizontalLayout();
header.setAlignItems(FlexComponent.Alignment.CENTER);
header.setSpacing(true);
header.addClassName("form-header");
H2 title = new H2(getTranslation("editappuser.title"));
title.getStyle().set("margin", "0");
title.addClassName("form-title");
Button backButton = new Button(getTranslation("button.back"), new Icon(VaadinIcon.ARROW_LEFT));
backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);

View File

@@ -50,6 +50,7 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
this.customerService = customerService;
setSizeFull();
setPadding(true);
addClassName("form-page");
setSpacing(true);
// Set field labels via i18n
@@ -76,14 +77,12 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
contentContainer.setMaxWidth("90%");
contentContainer.setSpacing(true);
contentContainer.setPadding(true);
contentContainer.getStyle().set("background", "var(--lumo-contrast-5pct)");
contentContainer.getStyle().set("border-radius", "var(--lumo-border-radius)");
contentContainer.getStyle().set("box-shadow", "var(--lumo-box-shadow-s)");
contentContainer.addClassNames("form-shell", "form-card");
// Header
H2 header = new H2(getTranslation("editcustomer.title"));
header.addClassName("form-title");
header.getStyle().set("text-align", "center");
header.getStyle().set("margin", "0");
contentContainer.add(header);
// Form layout

View File

@@ -96,6 +96,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
setSizeFull();
setPadding(true);
setSpacing(true);
addClassName("form-page");
setJustifyContentMode(JustifyContentMode.CENTER);
// 2% Abstand zwischen den Spalten
setWidthFull();

View File

@@ -34,15 +34,18 @@ public class ForgetPasswordView extends VerticalLayout implements BeforeEnterObs
setSizeFull();
setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
addClassName("form-page");
VerticalLayout container = new VerticalLayout();
container.setWidth("400px");
container.setMaxWidth("100%");
container.setPadding(true);
container.setSpacing(true);
container.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH);
container.addClassNames("form-shell", "narrow", "form-card");
H1 title = new H1("Neues Passwort festlegen");
title.getStyle().set("text-align", "center");
title.addClassName("form-title");
newPassword = new PasswordField("Neues Passwort");
confirmPassword = new PasswordField("Passwort bestätigen");

View File

@@ -24,15 +24,18 @@ public class ForgotPasswordRequestView extends VerticalLayout implements HasDyna
setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
setPadding(true);
addClassName("form-page");
VerticalLayout container = new VerticalLayout();
container.setWidth("400px");
container.setMaxWidth("100%");
container.setPadding(true);
container.setSpacing(true);
container.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH);
container.addClassNames("form-shell", "narrow", "form-card");
H1 title = new H1("Passwort zurücksetzen");
title.getStyle().set("text-align", "center");
title.addClassName("form-title");
EmailField emailField = new EmailField("E-Mail-Adresse");
emailField.setWidthFull();

View File

@@ -15,6 +15,7 @@ public class ImprintView extends VerticalLayout implements HasDynamicTitle {
setPadding(true);
setSpacing(true);
setAlignItems(Alignment.CENTER);
addClassNames("data-view", "form-shell");
try {
// Load HTML content from resources
@@ -23,6 +24,7 @@ public class ImprintView extends VerticalLayout implements HasDynamicTitle {
// Create a Div to hold the HTML content
Div imprintDiv = new Div();
imprintDiv.addClassNames("form-card", "form-shell");
imprintDiv.getElement().setProperty("innerHTML", htmlContent);
add(imprintDiv);
@@ -30,6 +32,7 @@ public class ImprintView extends VerticalLayout implements HasDynamicTitle {
} catch (Exception e) {
// Fallback content in case of error
Div errorDiv = new Div();
errorDiv.addClassNames("form-card", "form-shell");
errorDiv.setText(getTranslation("imprint.error", e.getMessage()));
add(errorDiv);
}

View File

@@ -41,7 +41,7 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi
this.customerInvoiceService = customerInvoiceService;
setId("invoice-generator-view");
getElement().setAttribute("class", "invoice-generator-view");
addClassNames("invoice-generator-view", "admin-form-view");
setSpacing(false);
setPadding(false);
@@ -57,6 +57,7 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi
mainLayout.setHeight("calc(100vh - 60px)"); // Abzug für Action-Buttons
mainLayout.setSpacing(true);
mainLayout.getStyle().set("overflow", "hidden");
mainLayout.addClassName("invoice-generator-main");
// Linke Seite: Templates/Bausteine
VerticalLayout leftPanel = createTemplatesPanel();
@@ -84,7 +85,7 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi
// Aktions-Buttons unter dem Canvas (fixe Höhe)
HorizontalLayout actionLayout = createActionButtons();
actionLayout.setHeight("60px");
actionLayout.getStyle().set("flex-shrink", "0").set("padding", "0 var(--lumo-space-m)");
actionLayout.getStyle().set("flex-shrink", "0");
add(actionLayout);
}
@@ -102,11 +103,11 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi
panel.setPadding(true);
panel.setSpacing(true);
panel.setHeightFull();
panel.getStyle().set("background-color", "var(--lumo-contrast-5pct)")
.set("border-radius", "var(--lumo-border-radius-m)").set("overflow", "auto");
panel.getStyle().set("overflow", "auto");
panel.addClassName("invoice-generator-panel");
Span header = new Span(getTranslation("invoicegenerator.panel.templates"));
header.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-l)");
header.addClassName("invoice-generator-panel-title");
// Draggable Templates
Div textBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.text"), VaadinIcon.TEXT_LABEL,
@@ -137,10 +138,7 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi
private Div createDraggableTemplate(String label, VaadinIcon icon, String type) {
Div template = new Div();
template.setText(label);
template.getStyle().set("padding", "var(--lumo-space-m)").set("margin", "var(--lumo-space-xs) 0")
.set("background-color", "var(--lumo-base-color)").set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border-radius", "var(--lumo-border-radius-m)").set("cursor", "grab").set("display", "flex")
.set("align-items", "center").set("gap", "var(--lumo-space-s)").set("user-select", "none");
template.addClassName("invoice-generator-template");
// Icon hinzufügen
Icon templateIcon = icon.create();
@@ -174,10 +172,7 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi
canvasContainer.setId("invoice-canvas-container");
canvasContainer.setWidth("100%");
canvasContainer.setHeight("100%");
canvasContainer.getStyle().set("background-color", "#e8e8e8")
.set("border", "2px dashed var(--lumo-contrast-30pct)")
.set("border-radius", "var(--lumo-border-radius-m)").set("position", "relative")
.set("overflow", "hidden").set("cursor", "default");
canvasContainer.addClassName("invoice-generator-canvas");
// Drop Zone Event Listener
canvasContainer.getElement()
@@ -206,17 +201,16 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi
panel.setPadding(true);
panel.setSpacing(true);
panel.setHeightFull();
panel.getStyle().set("background-color", "var(--lumo-contrast-5pct)")
.set("border-radius", "var(--lumo-border-radius-m)").set("overflow", "auto");
panel.getStyle().set("overflow", "auto");
panel.addClassName("invoice-generator-panel");
Span header = new Span(getTranslation("invoicegenerator.panel.properties"));
header.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-l)");
header.addClassName("invoice-generator-panel-title");
// Info-Text wenn kein Element ausgewählt
selectedElementInfo = new Div();
selectedElementInfo.setText(getTranslation("invoicegenerator.properties.info"));
selectedElementInfo.getStyle().set("color", "var(--lumo-secondary-text-color)").set("font-size",
"var(--lumo-font-size-s)");
selectedElementInfo.addClassName("invoice-generator-info");
panel.add(header, selectedElementInfo);
@@ -230,6 +224,7 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi
layout.setPadding(false);
layout.setSpacing(true);
layout.setAlignItems(Alignment.CENTER);
layout.addClassName("invoice-generator-actionbar");
Button clearButton = new Button(getTranslation("invoicegenerator.button.clear"), new Icon(VaadinIcon.TRASH));
clearButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
@@ -325,7 +320,7 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi
Div pdfContainer = new Div();
pdfContainer.setWidth("100%");
pdfContainer.setHeight("100%");
pdfContainer.getStyle().set("display", "flex").set("flex-direction", "column").set("overflow", "hidden");
pdfContainer.addClassName("invoice-generator-pdf");
// Use an iframe with data URL for PDF display
String base64Pdf = java.util.Base64.getEncoder().encodeToString(pdfBytes);
@@ -335,7 +330,7 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi
pdfFrame.setWidth("100%");
pdfFrame.setHeight("100%");
pdfFrame.getElement().setAttribute("src", dataUrl);
pdfFrame.getStyle().set("border", "none").set("flex-grow", "1");
pdfFrame.addClassName("invoice-generator-frame");
pdfContainer.add(pdfFrame);
@@ -368,11 +363,11 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi
propertiesPanel.removeAll();
Span header = new Span(getTranslation("invoicegenerator.properties.title"));
header.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-l)");
header.addClassName("invoice-generator-panel-title");
// Element Typ Anzeige
Span typeLabel = new Span(getTranslation("invoicegenerator.properties.type") + ": " + elementType);
typeLabel.getStyle().set("font-size", "var(--lumo-font-size-s)");
typeLabel.addClassName("invoice-generator-info");
propertiesPanel.add(header, typeLabel);

View File

@@ -1,7 +1,7 @@
package de.assecutor.votianlt.pages.view;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
@@ -10,6 +10,7 @@ import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.component.UI;
import de.assecutor.votianlt.model.invoices.CustomerInvoice;
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
import de.assecutor.votianlt.repository.CustomerInvoiceRepository;
import de.assecutor.votianlt.security.SecurityService;
import jakarta.annotation.security.RolesAllowed;
@@ -39,12 +40,13 @@ public class InvoicesView extends VerticalLayout implements HasDynamicTitle {
setSpacing(true);
setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
addClassName("data-view");
H2 title = new H2(getTranslation("invoices.title"));
add(title);
add(new ViewToolbar(getTranslation("invoices.title")));
invoiceGrid = new Grid<>(CustomerInvoice.class, false);
invoiceGrid.setWidthFull();
invoiceGrid.addClassName("data-grid");
invoiceGrid.addColumn(invoice -> firstNonBlank(invoice.getInvoiceNumber(), invoice.getId()))
.setHeader(getTranslation("invoices.column.number")).setAutoWidth(true);
invoiceGrid.addColumn(this::getRecipientLabel).setHeader(getTranslation("invoices.column.customer"))
@@ -66,7 +68,11 @@ public class InvoicesView extends VerticalLayout implements HasDynamicTitle {
});
loadInvoices();
add(invoiceGrid);
Div gridPanel = new Div(invoiceGrid);
gridPanel.addClassNames("surface-panel", "data-grid-panel");
gridPanel.setWidthFull();
add(gridPanel);
}
private void loadInvoices() {

View File

@@ -55,6 +55,7 @@ public class JobHistoryView extends Main implements HasUrlParameter<String>, Has
setSizeFull();
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL);
addClassName("data-view");
add(new ViewToolbar(getTranslation("jobhistory.title")));
@@ -62,6 +63,7 @@ public class JobHistoryView extends Main implements HasUrlParameter<String>, Has
content.setSpacing(true);
content.setPadding(true);
content.setWidthFull();
content.addClassNames("form-shell", "form-card");
add(content);
}
@@ -131,9 +133,8 @@ public class JobHistoryView extends Main implements HasUrlParameter<String>, Has
private Div createJobInfoBox(Job job) {
Div infoBox = new Div();
infoBox.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)")
.set("background-color", "var(--lumo-base-color)").set("margin-bottom", "var(--lumo-space-m)");
infoBox.addClassName("summary-card");
infoBox.getStyle().set("margin-bottom", "var(--lumo-space-m)");
VerticalLayout infoContent = new VerticalLayout();
infoContent.setPadding(false);
@@ -169,11 +170,9 @@ public class JobHistoryView extends Main implements HasUrlParameter<String>, Has
private Div createHistoryEntryCard(JobHistory entry) {
Div card = new Div();
card.getStyle().set("border", "1px solid var(--lumo-contrast-10pct)")
.set("border-left", "4px solid " + getTypeColor(entry.getChangeType()))
.set("border-radius", "var(--lumo-border-radius-s)").set("padding", "var(--lumo-space-m)")
.set("margin-bottom", "var(--lumo-space-s)").set("background-color", "var(--lumo-base-color)")
.set("width", "100%").set("box-sizing", "border-box");
card.addClassName("timeline-entry-card");
card.setWidthFull();
card.getStyle().set("--timeline-accent", getTypeColor(entry.getChangeType()));
// Header row with icon, reason and timestamp
HorizontalLayout headerRow = new HorizontalLayout();
@@ -185,11 +184,10 @@ public class JobHistoryView extends Main implements HasUrlParameter<String>, Has
Span reason = new Span(
entry.getReason() != null ? entry.getReason() : getTranslation("jobhistory.entry.unknown"));
reason.getStyle().set("font-weight", "500");
reason.addClassName("timeline-reason");
Span timestamp = new Span(formatDateTime(entry.getTimestamp()));
timestamp.getStyle().set("color", "var(--lumo-secondary-text-color)").set("font-size",
"var(--lumo-font-size-s)");
timestamp.addClassName("timeline-timestamp");
HorizontalLayout leftSide = new HorizontalLayout(typeIcon, reason);
leftSide.setAlignItems(HorizontalLayout.Alignment.CENTER);
@@ -206,8 +204,7 @@ public class JobHistoryView extends Main implements HasUrlParameter<String>, Has
// Description
if (entry.getDescription() != null && !entry.getDescription().isBlank()) {
Span description = new Span(entry.getDescription());
description.getStyle().set("color", "var(--lumo-body-text-color)").set("margin-top", "var(--lumo-space-xs)")
.set("display", "block");
description.addClassName("timeline-description");
cardContent.add(description);
}
@@ -465,11 +462,8 @@ public class JobHistoryView extends Main implements HasUrlParameter<String>, Has
private Div createBarcodeBox(String barcodeValue) {
Div barcodeBox = new Div();
barcodeBox.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border-radius", "var(--lumo-border-radius-s)").set("padding", "var(--lumo-space-xs)")
.set("background-color", "var(--lumo-contrast-5pct)").set("font-family", "monospace")
.set("font-size", "var(--lumo-font-size-s)").set("margin-bottom", "var(--lumo-space-xs)")
.set("word-break", "break-all");
barcodeBox.addClassNames("detail-card", "detail-card--code");
barcodeBox.getStyle().set("font-size", "var(--lumo-font-size-s)").set("margin-bottom", "var(--lumo-space-xs)");
barcodeBox.add(new Span(barcodeValue));
return barcodeBox;
@@ -580,4 +574,4 @@ public class JobHistoryView extends Main implements HasUrlParameter<String>, Has
log.error("Error showing enlarged signature: {}", e.getMessage());
}
}
}
}

View File

@@ -120,11 +120,13 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
setSizeFull();
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL);
addClassName("data-view");
content = new VerticalLayout();
content.setSpacing(true);
content.setPadding(true);
content.setWidthFull();
content.addClassNames("form-shell", "form-card");
}
@Override
@@ -282,7 +284,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
PriceCalculationResult priceResult = calculatePriceFromServices(job);
Div priceTable = new Div();
priceTable.getStyle().set("width", "100%");
priceTable.addClassName("price-table");
priceTable.add(createPriceRow(getTranslation("jobsummary.info.netto") + ":",
formatPrice(priceResult.netAmount()), false));
@@ -359,9 +361,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
private VerticalLayout borderedBox() {
VerticalLayout box = new VerticalLayout();
box.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)");
box.getStyle().set("border-radius", "var(--lumo-border-radius-m)");
box.getStyle().set("background-color", "var(--lumo-base-color)");
box.addClassName("summary-card");
box.setPadding(true);
box.setSpacing(false);
return box;
@@ -369,10 +369,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
private Div createStationTilesSection(Job job, List<CargoItem> cargoItems, List<BaseTask> tasks) {
Div stationGrid = new Div();
stationGrid.getStyle().set("display", "grid");
stationGrid.getStyle().set("grid-template-columns", "repeat(auto-fit, minmax(220px, 1fr))");
stationGrid.getStyle().set("gap", "var(--lumo-space-m)");
stationGrid.setWidthFull();
stationGrid.addClassName("stations-grid");
stationGrid.add(createPickupSummaryTile(job, cargoItems, tasks));
@@ -683,6 +681,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
dialogContent.setSpacing(true);
dialogContent.setWidthFull();
dialogContent.getStyle().set("min-width", "0");
dialogContent.addClassName("dialog-content-panel");
H4 header = new H4(getTranslation("jobsummary.section.tasks"));
header.getStyle().set("margin", "0");
@@ -775,17 +774,16 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
private Div createPriceRow(String label, String value, boolean bold) {
Div row = new Div();
row.getStyle().set("display", "flex").set("justify-content", "space-between").set("padding", "4px 0");
row.addClassName("price-row");
if (bold) {
row.addClassName("price-row--strong");
}
Span labelSpan = new Span(label);
labelSpan.getStyle().set("padding-right", "8px");
labelSpan.addClassName("price-row-label");
Span valueSpan = new Span(value);
valueSpan.getStyle().set("white-space", "nowrap");
if (bold) {
labelSpan.getStyle().set("font-weight", "bold");
valueSpan.getStyle().set("font-weight", "bold");
}
valueSpan.addClassName("price-row-value");
row.add(labelSpan, valueSpan);
return row;
@@ -860,15 +858,11 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
Div map = new Div();
map.setWidthFull();
map.setHeight("520px");
map.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)");
map.getStyle().set("border-radius", "var(--lumo-border-radius-m)");
map.addClassName("job-summary-map");
Div routeInfo = new Div();
routeInfo.setWidthFull();
routeInfo.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)");
routeInfo.getStyle().set("border-radius", "var(--lumo-border-radius-m)");
routeInfo.getStyle().set("padding", "var(--lumo-space-m)");
routeInfo.getStyle().set("background-color", "var(--lumo-base-color)");
routeInfo.addClassName("route-card");
content.add(map, routeInfo);
@@ -1110,6 +1104,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
VerticalLayout dialogContent = new VerticalLayout();
dialogContent.setPadding(true);
dialogContent.setSpacing(true);
dialogContent.addClassName("dialog-content-panel");
// Header
H4 header = new H4("Aufgaben-Details");
@@ -1238,12 +1233,9 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
if (svgContent != null && !svgContent.isBlank()) {
// Create a div to hold the SVG
Div svgContainer = new Div();
svgContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border-radius", "var(--lumo-border-radius-m)")
.set("padding", "var(--lumo-space-s)").set("background-color", "white")
.set("width", "100%").set("max-width", "450px").set("overflow", "hidden")
.set("display", "flex").set("align-items", "center")
.set("justify-content", "center");
svgContainer.addClassNames("detail-card", "detail-card--asset");
svgContainer.getStyle().set("width", "100%").set("max-width", "450px").set("overflow",
"hidden");
// Process SVG to make it responsive
String responsiveSvg = makeResponsiveSvg(svgContent);
@@ -1276,12 +1268,9 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
if (barcodeValue != null && !barcodeValue.isBlank()) {
// Create a styled container for each barcode
Div barcodeContainer = new Div();
barcodeContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border-radius", "var(--lumo-border-radius-s)")
.set("padding", "var(--lumo-space-s)").set("margin", "var(--lumo-space-xs) 0")
.set("background-color", "var(--lumo-contrast-5pct)")
.set("font-family", "monospace").set("font-size", "var(--lumo-font-size-s)")
.set("word-break", "break-all");
barcodeContainer.addClassNames("detail-card", "detail-card--code");
barcodeContainer.getStyle().set("margin", "var(--lumo-space-xs) 0").set("font-size",
"var(--lumo-font-size-s)");
Span barcodeSpan = new Span((i + 1) + ". " + barcodeValue);
barcodeContainer.add(barcodeSpan);
@@ -1315,10 +1304,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
for (Comment comment : comments) {
Div commentContainer = new Div();
commentContainer.getStyle().set("background-color", "#f5f5f5")
.set("border", "1px solid #ddd").set("border-radius", "4px").set("padding", "8px")
.set("margin", "4px 0").set("font-family", "monospace")
.set("white-space", "pre-wrap");
commentContainer.addClassNames("detail-card", "detail-card--comment");
commentContainer.getStyle().set("margin", "4px 0");
Span commentText = new Span(comment.getCommentText());
commentContainer.add(commentText);
@@ -1338,48 +1325,28 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
private Div createTaskCard(BaseTask task, String displayName) {
Div taskCard = new Div();
// Card styling with fixed width
taskCard.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)")
.set("margin", "var(--lumo-space-xs) 0").set("background-color", "var(--lumo-base-color)")
.set("cursor", "pointer").set("transition", "all 0.2s ease")
.set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)").set("display", "flex").set("align-items", "center")
.set("gap", "var(--lumo-space-m)").set("width", "100%").set("box-sizing", "border-box");
// Hover effects
taskCard.getElement().addEventListener("mouseenter", e -> {
taskCard.getStyle().set("transform", "translateY(-2px)").set("box-shadow", "0 4px 12px rgba(0, 0, 0, 0.15)")
.set("border-color", "var(--lumo-primary-color-50pct)");
});
taskCard.getElement().addEventListener("mouseleave", e -> {
taskCard.getStyle().set("transform", "translateY(0)").set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)")
.set("border-color", "var(--lumo-contrast-20pct)");
});
taskCard.addClassName("job-task-card");
if (task.isCompleted()) {
taskCard.addClassName("completed");
}
// Task icon based on type
Icon taskIcon = getTaskIcon(task);
taskIcon.getStyle().set("color",
task.isCompleted() ? "var(--lumo-success-color)" : "var(--lumo-primary-color)");
taskIcon.addClassName("job-task-icon");
// Task content
VerticalLayout taskContent = new VerticalLayout();
taskContent.setPadding(false);
taskContent.setSpacing(false);
taskContent.getStyle().set("flex-grow", "1");
taskContent.addClassName("job-task-content");
// Task name with order number (display as 1-based instead of 0-based)
String taskNameWithOrder = (task.getTaskOrder() != null ? (task.getTaskOrder() + 1) + ". " : "") + displayName;
Span taskName = new Span(taskNameWithOrder);
taskName.getStyle().set("font-weight", "500").set("font-size", "var(--lumo-font-size-m)").set("color",
task.isCompleted() ? "var(--lumo-success-text-color)" : "var(--lumo-body-text-color)");
taskName.addClassName("job-task-name");
Span statusBadge = new Span(getTaskStatusLabel(task));
statusBadge.getStyle().set("font-size", "var(--lumo-font-size-xs)").set("font-weight", "600")
.set("padding", "0.2rem 0.55rem").set("border-radius", "999px")
.set("background-color", task.isCompleted() ? "rgba(76, 175, 80, 0.15)" : "rgba(244, 67, 54, 0.12)")
.set("color", task.isCompleted() ? "var(--lumo-success-text-color)" : "var(--lumo-error-text-color)");
statusBadge.addClassNames("job-task-status", task.isCompleted() ? "completed" : "open");
HorizontalLayout headerRow = new HorizontalLayout(taskName, statusBadge);
headerRow.setWidthFull();
@@ -1390,24 +1357,18 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
// Task status/description
Span taskDescription = new Span(getTaskDescription(task));
taskDescription.getStyle().set("font-size", "var(--lumo-font-size-s)")
.set("color", "var(--lumo-secondary-text-color)").set("margin-top", "var(--lumo-space-xs)");
taskDescription.addClassName("job-task-description");
taskContent.add(headerRow, taskDescription);
// Status indicator
Div statusIndicator = new Div();
statusIndicator.getStyle().set("width", "8px").set("height", "8px").set("border-radius", "50%")
.set("background-color", task.isCompleted() ? "var(--lumo-success-color)" : "var(--lumo-error-color)");
statusIndicator.addClassName("job-task-indicator");
taskCard.add(taskIcon, taskContent, statusIndicator);
// Click handler with hover state reset
taskCard.addClickListener(event -> {
showTaskDetailsDialog(task);
// Reset hover state after dialog interaction
taskCard.getStyle().set("transform", "translateY(0)").set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)")
.set("border-color", "var(--lumo-contrast-20pct)");
});
return taskCard;
@@ -1480,17 +1441,11 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
private Div createPhotoGallery(List<String> photos) {
Div galleryContainer = new Div();
galleryContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)")
.set("background-color", "white").set("max-width", "600px").set("min-height", "500px")
.set("height", "500px").set("position", "relative").set("display", "flex").set("align-items", "center")
.set("justify-content", "center");
galleryContainer.addClassName("job-photo-gallery");
if (photos.size() == 1) {
// Single photo - no navigation needed
Div photoContainer = createPhotoContainer(photos.get(0));
photoContainer.getStyle().set("flex", "1").set("display", "flex").set("align-items", "center")
.set("justify-content", "center");
galleryContainer.add(photoContainer);
} else {
// Multiple photos - add navigation
@@ -1498,11 +1453,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
// Photo counter
Span photoCounter = new Span((currentIndex[0] + 1) + " / " + photos.size());
photoCounter.getStyle().set("position", "absolute").set("top", "var(--lumo-space-s)")
.set("right", "var(--lumo-space-s)").set("background-color", "rgba(0, 0, 0, 0.6)")
.set("color", "white").set("padding", "var(--lumo-space-xs) var(--lumo-space-s)")
.set("border-radius", "var(--lumo-border-radius-s)").set("font-size", "var(--lumo-font-size-s)")
.set("z-index", "10");
photoCounter.addClassName("job-photo-counter");
// Photo container
Div photoContainer = createPhotoContainer(photos.get(0));
@@ -1513,16 +1464,12 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
// Previous button
Button prevButton = new Button(new Icon(VaadinIcon.CHEVRON_LEFT));
prevButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ICON);
prevButton.getStyle().set("position", "absolute").set("left", "var(--lumo-space-s)").set("top", "50%")
.set("transform", "translateY(-50%)").set("background-color", "rgba(255, 255, 255, 0.8)")
.set("border-radius", "50%").set("z-index", "10");
prevButton.addClassNames("job-photo-nav", "prev");
// Next button
Button nextButton = new Button(new Icon(VaadinIcon.CHEVRON_RIGHT));
nextButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ICON);
nextButton.getStyle().set("position", "absolute").set("right", "var(--lumo-space-s)").set("top", "50%")
.set("transform", "translateY(-50%)").set("background-color", "rgba(255, 255, 255, 0.8)")
.set("border-radius", "50%").set("z-index", "10");
nextButton.addClassNames("job-photo-nav", "next");
// Navigation logic
prevButton.addClickListener(e -> {
@@ -1557,8 +1504,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
private Div createPhotoContainer(String base64Photo) {
Div photoContainer = new Div();
photoContainer.getStyle().set("width", "100%").set("height", "100%").set("display", "flex")
.set("align-items", "center").set("justify-content", "center").set("overflow", "hidden");
photoContainer.addClassName("job-photo-container");
// Create image element
String imgSrc = base64Photo.startsWith("data:") ? base64Photo : "data:image/jpeg;base64," + base64Photo;
@@ -1628,10 +1574,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
}
private void resetAllTaskCardHoverStates() {
for (Div taskCard : taskCards) {
taskCard.getStyle().set("transform", "translateY(0)").set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)")
.set("border-color", "var(--lumo-contrast-20pct)");
}
// Hover states are handled purely in CSS.
}
private String getGoogleMapsApiKey() {

View File

@@ -5,7 +5,10 @@ import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.login.LoginForm;
import com.vaadin.flow.component.login.LoginI18n;
import com.vaadin.flow.component.textfield.TextField;
@@ -96,30 +99,48 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
loginForm.addForgotPasswordListener(e -> UI.getCurrent().navigate(ForgotPasswordRequestView.class));
H1 title = new H1(getTranslation("login.votianlt"));
title.getStyle().set("color", "var(--lumo-primary-color)");
title.addClassName("login-card-title");
Button registerButton = new Button(getTranslation("login.register"), e -> UI.getCurrent().navigate("register"));
registerButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
registerButton.addClassName("login-register-button");
// Version display - will be set in @PostConstruct
versionSpan = new Span("");
versionSpan.getStyle().set("color", "var(--lumo-secondary-text-color)")
.set("font-size", "var(--lumo-font-size-s)").set("margin-top", "var(--lumo-space-m)");
versionSpan.addClassName("login-version");
// Inline flash message box (hidden by default)
flashBox.getStyle().set("background", "var(--lumo-error-color-10pct)")
.set("color", "var(--lumo-error-text-color)").set("border", "1px solid var(--lumo-error-color)")
.set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)")
.set("width", "100%").set("display", "none");
flashBox.addClassName("inline-flash");
flashBox.getStyle().set("display", "none");
VerticalLayout loginLayout = new VerticalLayout();
loginLayout.setAlignItems(FlexComponent.Alignment.CENTER);
loginLayout.addClassName("login-card");
loginLayout.add(flashBox, title, loginForm, twoFaField, verify2faButton, registerButton, versionSpan);
loginLayout.setMaxWidth("400px");
loginLayout.setPadding(true);
add(loginLayout);
Div loginHighlight = new Div();
loginHighlight.addClassName("login-highlight");
Span loginEyebrow = new Span(getTranslation("login.title"));
loginEyebrow.addClassName("eyebrow-chip");
Icon introIcon = VaadinIcon.CUBES.create();
introIcon.addClassName("login-highlight-icon");
H1 introTitle = new H1(getTranslation("login.votianlt"));
introTitle.addClassName("login-highlight-title");
Paragraph introText = new Paragraph(getTranslation("start.hero.description"));
introText.addClassName("login-highlight-text");
loginHighlight.add(loginEyebrow, introIcon, introTitle, introText);
Div loginShell = new Div(loginHighlight, loginLayout);
loginShell.addClassName("login-shell");
add(loginShell);
// Login-Schritt 1: Benutzername/Passwort
loginForm.addLoginListener(e -> handlePasswordLogin(e.getUsername(), e.getPassword()));
}

View File

@@ -98,6 +98,7 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has
this.messageService = messageService;
this.messageBroadcaster = messageBroadcaster;
this.eventPublisher = eventPublisher;
addClassName("message-hub-view");
// Set height to 100% to prevent page from growing beyond viewport
setHeightFull();
@@ -109,6 +110,7 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has
contentLayout.setWidthFull();
contentLayout.setHeightFull();
contentLayout.getStyle().set("overflow", "hidden");
contentLayout.addClassName("message-thread-layout");
add(contentLayout);
}
@@ -172,8 +174,7 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has
messagesContainer.setPadding(false);
messagesContainer.setSpacing(false);
messagesContainer.setWidthFull();
messagesContainer.getStyle().set("background-color", "#f0f0f0").set("border-radius", "8px").set("padding",
"15px");
messagesContainer.addClassName("message-thread");
// Wrap messages container in scroller for vertical scrolling
messagesScroller = new Scroller(messagesContainer);
@@ -274,7 +275,7 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has
upload.setWidthFull();
Span helper = new Span("Unterstützte Formate: PNG, JPG, GIF, WebP (max. 32 MB)");
helper.getStyle().set("font-size", "12px").set("color", "#666666");
helper.addClassName("inline-caption");
Image preview = new Image();
preview.setAlt("Vorschau des ausgewählten Bildes");
@@ -388,14 +389,11 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has
*/
private Div createDateSeparator(LocalDate date) {
Div separator = new Div();
separator.getStyle().set("display", "flex").set("justify-content", "center").set("text-align", "center")
.set("margin", "20px 0");
separator.setWidthFull();
separator.addClassName("message-date-separator");
Span dateLabel = new Span(DateTimeFormatUtil.formatDate(date));
dateLabel.getStyle().set("background-color", "#d0d0d0").set("padding", "4px 10px").set("border-radius", "12px")
.set("font-size", "12px").set("font-weight", "500").set("color", "#333333")
.set("display", "inline-block");
dateLabel.addClassName("message-date-chip");
separator.add(dateLabel);
return separator;
@@ -413,24 +411,20 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has
// Container for the message (aligns left or right)
Div messageWrapper = new Div();
String alignment = isServerMessage ? "right" : "left";
messageWrapper.getStyle().set("display", "flex")
.set("justify-content", isServerMessage ? "flex-end" : "flex-start").set("margin", "5px 0")
.set("width", "100%");
messageWrapper.addClassNames("message-wrapper", isServerMessage ? "server" : "client");
// Message bubble
Div bubble = new Div();
bubble.getStyle().set("background-color", isServerMessage ? "#dcf8c6" : "#ffffff").set("padding", "10px 15px")
.set("border-radius", "18px").set("max-width", "70%").set("box-shadow", "0 1px 2px rgba(0,0,0,0.1)")
.set("word-wrap", "break-word").set("white-space", "pre-wrap").set("text-align", alignment);
bubble.addClassNames("message-bubble", isServerMessage ? "server" : "client");
bubble.getStyle().set("text-align", alignment);
// Message content component (text or media)
Component contentComponent = createContentComponent(message, alignment);
// Timestamp
Span timeSpan = new Span(DateTimeFormatUtil.formatTime(timestamp));
timeSpan.getStyle().set("font-size", "11px").set("color", isServerMessage ? "#666666" : "#999999")
.set("display", "block").set("text-align", alignment);
timeSpan.addClassName("message-bubble-time");
timeSpan.getStyle().set("text-align", alignment);
bubble.add(contentComponent, timeSpan);
messageWrapper.add(bubble);
@@ -450,15 +444,14 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has
Div contentDiv = new Div();
String content = Optional.ofNullable(contentValue).filter(value -> !value.isBlank()).orElse("(kein Inhalt)");
contentDiv.setText(content);
contentDiv.getStyle().set("font-size", "14px").set("color", "#000000").set("margin-bottom", "5px")
.set("text-align", alignment);
contentDiv.addClassName("message-bubble-text");
contentDiv.getStyle().set("text-align", alignment);
return contentDiv;
}
private Component createImageContent(String base64Value, String alignment) {
Div wrapper = new Div();
wrapper.getStyle().set("margin-bottom", "5px").set("display", "flex").set("justify-content",
"right".equals(alignment) ? "flex-end" : "flex-start");
wrapper.addClassNames("message-image-row", "right".equals(alignment) ? "server" : "client");
String trimmed = Optional.ofNullable(base64Value).map(String::trim).orElse("");
if (trimmed.isEmpty()) {
@@ -473,8 +466,7 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has
}
Image image = new Image(dataUrl, "Nachrichtenbild");
image.getStyle().set("max-width", "100%").set("border-radius", "12px").set("display", "block")
.set("max-height", "320px").set("height", "auto");
image.addClassName("message-bubble-image");
image.getElement().setAttribute("loading", "lazy");
wrapper.add(image);
@@ -687,8 +679,7 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has
title.getStyle().set("margin", "0");
Span subtitle = new Span(conversationTitle);
subtitle.getStyle().set("color", "#666666");
subtitle.getStyle().set("font-size", "14px");
subtitle.addClassName("message-subtitle");
titleLayout.add(title, subtitle);
@@ -696,6 +687,7 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has
layout.setWidthFull();
layout.setAlignItems(com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment.CENTER);
layout.setSpacing(true);
layout.addClassName("message-thread-header");
return layout;
}
@@ -732,6 +724,7 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has
layout.setAlignItems(FlexComponent.Alignment.END);
layout.setSpacing(true);
layout.expand(messageInput);
layout.addClassName("message-thread-input");
return layout;
}

View File

@@ -4,12 +4,11 @@ import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.DetachEvent;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.Main;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.data.renderer.ComponentRenderer;
import com.vaadin.flow.router.HasDynamicTitle;
@@ -21,6 +20,7 @@ import de.assecutor.votianlt.model.Message;
import de.assecutor.votianlt.model.MessageContentType;
import de.assecutor.votianlt.model.MessageOrigin;
import de.assecutor.votianlt.pages.service.AppUserService;
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
import de.assecutor.votianlt.service.MessageBroadcaster;
import de.assecutor.votianlt.service.MessageService;
import de.assecutor.votianlt.util.DateTimeFormatUtil;
@@ -60,21 +60,29 @@ public class MessagesView extends Main implements HasDynamicTitle {
this.messageService = messageService;
this.appUserService = appUserService;
this.messageBroadcaster = messageBroadcaster;
addClassNames("data-view", "message-hub-view");
// Create main layout
VerticalLayout layout = new VerticalLayout();
layout.setPadding(true);
layout.setSpacing(true);
layout.setWidthFull();
layout.addClassName("form-shell");
// Add title and action buttons
HorizontalLayout headerLayout = createHeaderLayout();
ViewToolbar headerLayout = new ViewToolbar(getTranslation("messages.title"));
// Create client grid
clientGrid = createClientGrid();
clientGrid.addClassName("data-grid");
Div gridPanel = new Div(clientGrid);
gridPanel.addClassNames("surface-panel", "data-grid-panel");
gridPanel.setWidthFull();
gridPanel.setHeight("680px");
// Add components to layout
layout.add(headerLayout, clientGrid);
layout.add(headerLayout, gridPanel);
// Add layout to main view
add(layout);
@@ -83,18 +91,10 @@ public class MessagesView extends Main implements HasDynamicTitle {
loadClientSummaries();
}
private HorizontalLayout createHeaderLayout() {
H2 title = new H2(getTranslation("messages.title"));
HorizontalLayout layout = new HorizontalLayout(title);
layout.setWidthFull();
layout.setAlignItems(com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment.CENTER);
return layout;
}
private Grid<ClientMessageSummary> createClientGrid() {
Grid<ClientMessageSummary> grid = new Grid<>(ClientMessageSummary.class, false);
grid.setWidthFull();
grid.setHeight("600px");
grid.setHeightFull();
// Add columns
grid.addColumn(new ComponentRenderer<>(summary -> {

View File

@@ -53,6 +53,7 @@ public class MyInvoicesView extends Main implements HasDynamicTitle {
public MyInvoicesView(SystemInvoiceService systemInvoiceService) {
this.systemInvoiceService = systemInvoiceService;
addClassName("data-view");
getStyle().set("max-width", "1100px");
getStyle().set("margin-left", "auto");
getStyle().set("margin-right", "auto");
@@ -74,8 +75,7 @@ public class MyInvoicesView extends Main implements HasDynamicTitle {
private Component createTopCards() {
// Container mit responsiven Spalten
Div container = new Div();
container.getStyle().set("display", "grid").set("grid-template-columns", "repeat(auto-fit, minmax(280px, 1fr))")
.set("gap", "var(--lumo-space-m)");
container.addClassName("invoice-top-grid");
// Karte: Offene Rechnungen
Paragraph hint = new Paragraph(getTranslation("myinvoices.hint.noopen"));
@@ -159,9 +159,7 @@ public class MyInvoicesView extends Main implements HasDynamicTitle {
H4 emptyTitle = new H4(getTranslation("myinvoices.empty.title"));
Paragraph emptyDesc = new Paragraph(getTranslation("myinvoices.empty.desc"));
emptyState.add(emptyTitle, emptyDesc);
emptyState.getStyle().set("text-align", "center").set("color", "var(--lumo-secondary-text-color)")
.set("padding", "var(--lumo-space-m)").set("border", "1px dashed var(--lumo-contrast-20pct)")
.set("border-radius", "var(--lumo-border-radius-m)");
emptyState.addClassName("empty-state-card");
updateEmptyStateVisibility(allRows);
// Suche (einfacher Text-Filter über alle sichtbaren Felder)
@@ -230,7 +228,7 @@ public class MyInvoicesView extends Main implements HasDynamicTitle {
H3 h3 = new H3(title);
h3.getStyle().set("margin", "0");
Div inner = new Div(content);
inner.getStyle().set("padding", "var(--lumo-space-s)");
inner.addClassName("invoice-card-inner");
card.add(h3, inner);
return card;
}
@@ -246,9 +244,7 @@ public class MyInvoicesView extends Main implements HasDynamicTitle {
}
private void styleCard(Div card) {
card.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border-radius", "var(--lumo-border-radius-l)").set("padding", "var(--lumo-space-m)")
.set("background", "var(--lumo-base-color)").set("box-shadow", "0 1px 1px rgba(0,0,0,0.02)");
card.addClassName("invoice-summary-card");
card.setWidthFull();
}

View File

@@ -61,6 +61,7 @@ public class RegisterView extends VerticalLayout implements HasDynamicTitle {
setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
setPadding(true);
setSpacing(false);
addClassName("form-page");
// Hauptcontainer für das Formular
VerticalLayout formContainer = createFormContainer();
@@ -70,28 +71,18 @@ public class RegisterView extends VerticalLayout implements HasDynamicTitle {
private VerticalLayout createFormContainer() {
VerticalLayout container = new VerticalLayout();
container.setWidth("980px");
container.setMaxWidth("100%");
container.setPadding(true);
container.setSpacing(true);
container.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH);
// Styling für den Container
container.getStyle().set("background-color", "var(--lumo-base-color)");
container.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)");
container.getStyle().set("border-radius", "var(--lumo-border-radius-m)");
container.getStyle().set("box-shadow", "var(--lumo-box-shadow-s)");
container.addClassNames("form-shell", "form-card");
// Titel
H1 title = new H1(getTranslation("register.title"));
title.getStyle().set("text-align", "center");
title.getStyle().set("color", "var(--lumo-primary-color)");
title.getStyle().set("margin-top", "0");
title.addClassName("form-title");
H2 subtitle = new H2(getTranslation("register.subtitle"));
subtitle.getStyle().set("text-align", "center");
subtitle.getStyle().set("color", "var(--lumo-secondary-text-color)");
subtitle.getStyle().set("font-size", "var(--lumo-font-size-l)");
subtitle.getStyle().set("font-weight", "normal");
subtitle.getStyle().set("margin-bottom", "var(--lumo-space-l)");
subtitle.addClassName("form-subtitle");
// Formularfelder
emailField = new TextField(getTranslation("register.email"));

View File

@@ -2,14 +2,15 @@ package de.assecutor.votianlt.pages.view;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.model.Customer;
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
import de.assecutor.votianlt.pages.service.CustomerService;
import de.assecutor.votianlt.security.SecurityService;
import jakarta.annotation.security.RolesAllowed;
@@ -30,21 +31,14 @@ public class ShowCustomersView extends VerticalLayout implements HasDynamicTitle
setSizeFull();
setPadding(true);
setSpacing(true);
addClassName("data-view");
// Header with title and add button
HorizontalLayout header = new HorizontalLayout();
header.setWidthFull();
header.add(new H2(getTranslation("customers.title")));
Button addCustomerButton = new Button(getTranslation("customers.button.add"), new Icon(VaadinIcon.PLUS));
header.add(addCustomerButton);
header.setJustifyContentMode(JustifyContentMode.BETWEEN);
header.setAlignItems(Alignment.CENTER);
add(header);
add(new ViewToolbar(getTranslation("customers.title"), addCustomerButton));
// Add hint text
var hintText = new com.vaadin.flow.component.html.Paragraph(getTranslation("customers.hint.click"));
hintText.getStyle().set("color", "var(--lumo-secondary-text-color)");
hintText.getStyle().set("font-size", "var(--lumo-font-size-s)");
Paragraph hintText = new Paragraph(getTranslation("customers.hint.click"));
hintText.addClassName("list-hint");
add(hintText);
// Configure grid columns
@@ -67,6 +61,7 @@ public class ShowCustomersView extends VerticalLayout implements HasDynamicTitle
grid.setMultiSort(true);
grid.setSizeFull();
grid.addClassName("data-grid");
// Make grid rows clickable
grid.setSelectionMode(Grid.SelectionMode.SINGLE);
@@ -80,7 +75,11 @@ public class ShowCustomersView extends VerticalLayout implements HasDynamicTitle
}
});
add(grid);
Div gridPanel = new Div(grid);
gridPanel.addClassNames("surface-panel", "data-grid-panel");
gridPanel.setSizeFull();
add(gridPanel);
// Button action
addCustomerButton.addClickListener(e -> getUI().ifPresent(ui -> ui.navigate("add-customer")));

View File

@@ -7,7 +7,7 @@ import com.vaadin.flow.component.confirmdialog.ConfirmDialog;
import com.vaadin.flow.component.datepicker.DatePicker;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.Anchor;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.notification.Notification;
@@ -21,6 +21,7 @@ import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.model.Job;
import de.assecutor.votianlt.model.JobStatus;
import de.assecutor.votianlt.messaging.MessagingPublisher;
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
import de.assecutor.votianlt.util.DateTimeFormatUtil;
import de.assecutor.votianlt.repository.CustomerInvoiceRepository;
import de.assecutor.votianlt.repository.JobRepository;
@@ -62,38 +63,39 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
setSizeFull();
setPadding(true);
setSpacing(true);
addClassName("data-view");
startDate.setLabel(getTranslation("jobs.filter.startdate"));
endDate.setLabel(getTranslation("jobs.filter.enddate"));
searchField.setLabel(getTranslation("jobs.filter.search"));
statusFilter.setLabel(getTranslation("jobs.filter.status"));
H2 title = new H2(getTranslation("nav.jobs"));
add(title);
Button exportButton = new Button(getTranslation("jobs.button.csvexport"));
ViewToolbar toolbar = new ViewToolbar(getTranslation("nav.jobs"), exportButton);
add(toolbar);
// Configure status filter
statusFilter.setItems(getTranslation("jobs.status.all"), getTranslation("jobs.status.open"),
getTranslation("jobs.status.done"));
statusFilter.setValue(getTranslation("jobs.status.open"));
statusFilter.setWidth("150px");
// Configure search field
searchField.setPlaceholder(getTranslation("jobs.filter.search.placeholder"));
searchField.setClearButtonVisible(true);
searchField.setWidth("200px");
// Filterleiste mit Export-Button am rechten Rand
Button applyFilter = new Button(getTranslation("jobs.filter.apply"));
HorizontalLayout leftFilters = new HorizontalLayout(startDate, endDate, searchField, statusFilter, applyFilter);
leftFilters.setAlignItems(Alignment.END);
leftFilters.getStyle().set("flex-wrap", "wrap");
HorizontalLayout filterBar = new HorizontalLayout();
filterBar.setWidthFull();
filterBar.add(leftFilters);
Button exportButton = new Button(getTranslation("jobs.button.csvexport"));
filterBar.add(exportButton);
filterBar.setJustifyContentMode(JustifyContentMode.BETWEEN);
filterBar.setAlignItems(Alignment.END);
filterBar.getStyle().set("flex-wrap", "wrap");
filterBar.addClassNames("surface-panel", "filter-panel");
add(filterBar);
// Init default period: last 30 days
@@ -186,6 +188,7 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
grid.setMultiSort(true);
grid.setSizeFull();
grid.addClassName("data-grid");
// Make grid rows clickable
grid.setSelectionMode(Grid.SelectionMode.SINGLE);
@@ -199,7 +202,11 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
}
});
add(grid);
Div gridPanel = new Div(grid);
gridPanel.addClassNames("surface-panel", "data-grid-panel");
gridPanel.setSizeFull();
add(gridPanel);
loadData();
}

View File

@@ -33,9 +33,10 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
public StartView(SecurityService securityService, @Value("${app.version:unknown}") String appVersion) {
this.securityService = securityService;
this.appVersion = appVersion;
addClassName("landing-view");
setSizeFull();
setPadding(false);
setSpacing(false);
setSpacing(true);
setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH);
// Header mit Navigation
@@ -69,14 +70,11 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
header.setSpacing(true);
header.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
header.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
header.getStyle().set("background-color", "var(--lumo-contrast-5pct)");
header.getStyle().set("border-bottom", "1px solid var(--lumo-contrast-10pct)");
header.addClassNames("page-shell", "surface-panel", "landing-header");
// Logo
H1 logo = new H1("votian LT");
logo.getStyle().set("color", "var(--lumo-primary-color)");
logo.getStyle().set("margin", "0");
logo.getStyle().set("font-weight", "bold");
logo.addClassName("landing-logo");
// Navigation - abhängig vom Anmeldestatus
Component navigation = securityService.isUserLoggedIn() ? createAuthenticatedNavigation()
@@ -91,12 +89,15 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
HorizontalLayout navButtons = new HorizontalLayout();
navButtons.setSpacing(true);
navButtons.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
navButtons.addClassName("landing-nav");
Button loginBtn = new Button(getTranslation("start.button.login"), event -> login());
loginBtn.addThemeVariants(ButtonVariant.LUMO_CONTRAST);
loginBtn.addClassName("landing-nav-button");
Button registerBtn = new Button(getTranslation("start.button.register"), event -> register());
registerBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
registerBtn.addClassName("landing-nav-button");
// Sprachauswahl Button
@@ -114,6 +115,7 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
Button languageBtn = new Button(flag + " " + currentLang);
languageBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
languageBtn.addClassName("landing-language-button");
ContextMenu languageMenu = new ContextMenu();
languageMenu.setOpenOnClick(true);
@@ -186,10 +188,12 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
HorizontalLayout navLayout = new HorizontalLayout();
navLayout.setSpacing(true);
navLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
navLayout.addClassName("landing-nav");
// Auftragserstellung Button
Button createOrderBtn = new Button("Auftragserstellung", event -> UI.getCurrent().navigate("add_job"));
createOrderBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
createOrderBtn.addClassName("landing-nav-button");
// Verwaltung ComboBox
ComboBox<String> managementCombo = new ComboBox<>();
@@ -254,28 +258,23 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
heroSection.setPadding(true);
heroSection.setSpacing(true);
heroSection.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
heroSection.getStyle().set("background",
"linear-gradient(135deg, var(--lumo-primary-color-10pct), var(--lumo-primary-color-50pct))");
heroSection.getStyle().set("min-height", "400px");
heroSection.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
heroSection.addClassNames("page-shell", "hero-panel", "landing-hero");
// Hero Image Placeholder
Icon heroIcon = VaadinIcon.TRUCK.create();
heroIcon.setSize("120px");
heroIcon.getStyle().set("color", "var(--lumo-primary-color)");
heroIcon.addClassName("hero-panel-icon");
H1 heroTitle = new H1(getTranslation("start.title"));
heroTitle.getStyle().set("text-align", "center");
heroTitle.getStyle().set("color", "var(--lumo-primary-text-color)");
heroTitle.getStyle().set("margin-bottom", "var(--lumo-space-l)");
heroTitle.addClassName("hero-panel-title");
Paragraph heroDescription = new Paragraph(getTranslation("start.hero.description"));
heroDescription.getStyle().set("text-align", "center");
heroDescription.getStyle().set("max-width", "600px");
heroDescription.getStyle().set("font-size", "var(--lumo-font-size-l)");
heroDescription.addClassName("hero-panel-text");
Button ctaButton = new Button(getTranslation("cta.freetest"), event -> register());
ctaButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_LARGE);
ctaButton.addClassName("hero-cta");
heroSection.add(heroIcon, heroTitle, heroDescription, ctaButton);
return heroSection;
@@ -287,24 +286,18 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
systemSection.setPadding(true);
systemSection.setSpacing(true);
systemSection.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
systemSection.getStyle().set("background-color", "var(--lumo-base-color)");
systemSection.addClassNames("page-shell", "surface-panel", "section-panel");
// Section Header
H2 systemTitle = new H2(getTranslation("start.system.title"));
systemTitle.getStyle().set("color", "var(--lumo-primary-color)");
systemTitle.getStyle().set("text-align", "center");
systemTitle.addClassName("section-title");
Paragraph systemIntro = new Paragraph(getTranslation("start.system.intro"));
systemIntro.getStyle().set("text-align", "center");
systemIntro.getStyle().set("max-width", "800px");
systemIntro.getStyle().set("margin-bottom", "var(--lumo-space-xl)");
systemIntro.addClassName("section-intro");
// Features Grid
HorizontalLayout featuresGrid = new HorizontalLayout();
featuresGrid.setWidthFull();
featuresGrid.setSpacing(true);
featuresGrid.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
featuresGrid.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.STRETCH);
Div featuresGrid = new Div();
featuresGrid.addClassName("tile-grid");
// Feature Cards
featuresGrid.add(
@@ -324,24 +317,18 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
card.setSpacing(true);
card.setPadding(true);
card.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
card.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)");
card.getStyle().set("border-radius", "var(--lumo-border-radius-m)");
card.getStyle().set("background-color", "var(--lumo-base-color)");
card.getStyle().set("box-shadow", "var(--lumo-box-shadow-xs)");
card.setWidth("300px");
card.setHeightFull();
card.setWidthFull();
card.addClassName("feature-card");
Icon icon = iconType.create();
icon.setSize("48px");
icon.getStyle().set("color", "var(--lumo-primary-color)");
icon.addClassName("feature-card-icon");
H3 cardTitle = new H3(title);
cardTitle.getStyle().set("text-align", "center");
cardTitle.getStyle().set("margin", "var(--lumo-space-s) 0");
cardTitle.addClassName("feature-card-title");
Paragraph cardDescription = new Paragraph(description);
cardDescription.getStyle().set("text-align", "center");
cardDescription.getStyle().set("font-size", "var(--lumo-font-size-s)");
cardDescription.addClassName("feature-card-description");
card.add(icon, cardTitle, cardDescription);
return card;
@@ -353,19 +340,17 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
appSection.setPadding(true);
appSection.setSpacing(true);
appSection.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
appSection.getStyle().set("background-color", "var(--lumo-contrast-5pct)");
appSection.addClassNames("page-shell", "surface-panel", "app-overview-panel");
H2 appTitle = new H2(getTranslation("start.app.title"));
appTitle.getStyle().set("color", "var(--lumo-primary-color)");
appTitle.getStyle().set("text-align", "center");
appTitle.addClassName("section-title");
Paragraph appDescription = new Paragraph(getTranslation("start.app.description"));
appDescription.getStyle().set("text-align", "center");
appDescription.getStyle().set("max-width", "800px");
appDescription.addClassName("section-intro");
Icon appIcon = VaadinIcon.MOBILE.create();
appIcon.setSize("80px");
appIcon.getStyle().set("color", "var(--lumo-primary-color)");
appIcon.addClassName("feature-card-icon");
appSection.add(appTitle, appDescription, appIcon);
return appSection;
@@ -377,18 +362,17 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
footer.setPadding(true);
footer.setSpacing(true);
footer.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
footer.getStyle().set("background-color", "var(--lumo-contrast-10pct)");
footer.getStyle().set("border-top", "1px solid var(--lumo-contrast-20pct)");
footer.addClassNames("page-shell", "surface-panel", "footer-panel");
// Company Info
H3 companyTitle = new H3(getTranslation("start.imprint.title"));
companyTitle.getStyle().set("color", "var(--lumo-primary-color)");
companyTitle.getStyle().set("text-align", "center");
companyTitle.addClassName("section-title");
VerticalLayout companyInfo = new VerticalLayout();
companyInfo.setSpacing(false);
companyInfo.setPadding(false);
companyInfo.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
companyInfo.addClassName("footer-details");
companyInfo.add(new Paragraph(getTranslation("start.imprint.company")),
new Paragraph(getTranslation("start.imprint.address")),
@@ -397,20 +381,14 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
// Call to Action
Paragraph ctaText = new Paragraph(getTranslation("start.cta.text"));
ctaText.getStyle().set("text-align", "center");
ctaText.getStyle().set("font-weight", "bold");
ctaText.getStyle().set("color", "var(--lumo-primary-color)");
ctaText.getStyle().set("margin-top", "var(--lumo-space-l)");
ctaText.addClassName("footer-cta");
Paragraph slogan = new Paragraph(getTranslation("start.slogan"));
slogan.getStyle().set("text-align", "center");
slogan.getStyle().set("font-style", "italic");
slogan.getStyle().set("color", "var(--lumo-primary-color)");
slogan.addClassName("footer-slogan");
// Versionsnummer
Span versionSpan = new Span("Version " + appVersion);
versionSpan.getStyle().set("color", "var(--lumo-secondary-text-color)")
.set("font-size", "var(--lumo-font-size-s)").set("margin-top", "var(--lumo-space-l)");
versionSpan.addClassName("footer-version");
footer.add(companyTitle, companyInfo, ctaText, slogan, versionSpan);
return footer;

View File

@@ -53,6 +53,7 @@ public class StatisticsView extends VerticalLayout implements HasDynamicTitle {
setSizeFull();
setPadding(false);
setSpacing(false);
addClassName("statistics-chat-view");
// Header
HorizontalLayout header = createHeader();
@@ -64,11 +65,13 @@ public class StatisticsView extends VerticalLayout implements HasDynamicTitle {
chatContainer.setPadding(true);
chatContainer.setSpacing(true);
chatContainer.getStyle().set("padding-bottom", "20px");
chatContainer.addClassName("statistics-chat-container");
Scroller scroller = new Scroller(chatContainer);
scroller.setSizeFull();
scroller.setScrollDirection(Scroller.ScrollDirection.VERTICAL);
scroller.getStyle().set("background", "var(--lumo-contrast-5pct)");
scroller.addClassName("statistics-scroller");
add(scroller);
setFlexGrow(1, scroller);
@@ -83,8 +86,7 @@ public class StatisticsView extends VerticalLayout implements HasDynamicTitle {
header.setWidthFull();
header.setPadding(true);
header.setAlignItems(FlexComponent.Alignment.CENTER);
header.getStyle().set("background", "var(--lumo-base-color)").set("border-bottom",
"1px solid var(--lumo-contrast-10pct)");
header.addClassName("statistics-header");
Icon aiIcon = VaadinIcon.MAGIC.create();
aiIcon.getStyle().set("color", "var(--lumo-primary-color)");
@@ -106,8 +108,7 @@ public class StatisticsView extends VerticalLayout implements HasDynamicTitle {
inputArea.setPadding(true);
inputArea.setSpacing(true);
inputArea.setAlignItems(FlexComponent.Alignment.CENTER);
inputArea.getStyle().set("background", "var(--lumo-base-color)").set("border-top",
"1px solid var(--lumo-contrast-10pct)");
inputArea.addClassName("statistics-input-panel");
Button sendButton = new Button(VaadinIcon.PAPERPLANE.create());
sendButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
@@ -124,6 +125,7 @@ public class StatisticsView extends VerticalLayout implements HasDynamicTitle {
HorizontalLayout quickActions = new HorizontalLayout(jobCountBtn, revenueBtn, trendBtn);
quickActions.setSpacing(true);
quickActions.addClassName("statistics-quick-actions");
VerticalLayout inputWrapper = new VerticalLayout();
inputWrapper.setPadding(false);
@@ -193,17 +195,10 @@ public class StatisticsView extends VerticalLayout implements HasDynamicTitle {
private void addUserMessage(String message) {
Div messageDiv = new Div();
messageDiv.addClassName("chat-message");
messageDiv.addClassName("user-message");
messageDiv.getStyle().set("display", "flex").set("justify-content", "flex-end").set("margin-bottom",
"var(--lumo-space-m)");
messageDiv.addClassNames("chat-message", "user-message");
Div bubble = new Div();
bubble.getStyle().set("background", "var(--lumo-primary-color)")
.set("color", "var(--lumo-primary-contrast-color)")
.set("padding", "var(--lumo-space-s) var(--lumo-space-m)")
.set("border-radius", "var(--lumo-border-radius-l)").set("max-width", "70%")
.set("word-wrap", "break-word");
bubble.addClassNames("chat-bubble", "chat-bubble--user");
Paragraph text = new Paragraph(message);
text.getStyle().set("margin", "0");
@@ -219,16 +214,10 @@ public class StatisticsView extends VerticalLayout implements HasDynamicTitle {
private void addAiResponse(AiStatisticsService.StatisticsResponse response) {
Div messageDiv = new Div();
messageDiv.addClassName("chat-message");
messageDiv.addClassName("ai-message");
messageDiv.getStyle().set("display", "flex").set("justify-content", "flex-start").set("margin-bottom",
"var(--lumo-space-m)");
messageDiv.addClassNames("chat-message", "ai-message");
Div bubble = new Div();
bubble.getStyle().set("background", "var(--lumo-base-color)")
.set("border", "1px solid var(--lumo-contrast-10pct)").set("padding", "var(--lumo-space-m)")
.set("border-radius", "var(--lumo-border-radius-l)").set("max-width", "85%")
.set("box-shadow", "var(--lumo-box-shadow-xs)");
bubble.addClassNames("chat-bubble", "chat-bubble--ai");
// AI Icon
HorizontalLayout header = new HorizontalLayout();
@@ -283,14 +272,10 @@ public class StatisticsView extends VerticalLayout implements HasDynamicTitle {
private void addErrorMessage(String message) {
Div messageDiv = new Div();
messageDiv.getStyle().set("display", "flex").set("justify-content", "flex-start").set("margin-bottom",
"var(--lumo-space-m)");
messageDiv.addClassNames("chat-message", "error-message");
Div bubble = new Div();
bubble.getStyle().set("background", "var(--lumo-error-color-10pct)")
.set("border", "1px solid var(--lumo-error-color)")
.set("padding", "var(--lumo-space-s) var(--lumo-space-m)")
.set("border-radius", "var(--lumo-border-radius-l)").set("max-width", "70%");
bubble.addClassNames("chat-bubble", "chat-bubble--error");
Icon errorIcon = VaadinIcon.EXCLAMATION_CIRCLE.create();
errorIcon.setSize("16px");
@@ -310,14 +295,10 @@ public class StatisticsView extends VerticalLayout implements HasDynamicTitle {
private Div createLoadingMessage() {
Div messageDiv = new Div();
messageDiv.getStyle().set("display", "flex").set("justify-content", "flex-start").set("margin-bottom",
"var(--lumo-space-m)");
messageDiv.addClassNames("chat-message", "loading-message");
Div bubble = new Div();
bubble.getStyle().set("background", "var(--lumo-base-color)")
.set("border", "1px solid var(--lumo-contrast-10pct)")
.set("padding", "var(--lumo-space-s) var(--lumo-space-m)")
.set("border-radius", "var(--lumo-border-radius-l)");
bubble.addClassNames("chat-bubble", "chat-bubble--loading");
Span dots = new Span(getTranslation("statistics.loading"));
dots.getStyle().set("color", "var(--lumo-secondary-text-color)").set("font-style", "italic");

View File

@@ -49,12 +49,14 @@ public class UserMessagesView extends Main implements HasUrlParameter<String>, H
public UserMessagesView(AppUserService appUserService, MessageService messageService) {
this.appUserService = appUserService;
this.messageService = messageService;
addClassName("message-hub-view");
// Create main layout
contentLayout = new VerticalLayout();
contentLayout.setPadding(true);
contentLayout.setSpacing(true);
contentLayout.setWidthFull();
contentLayout.addClassName("form-shell");
add(contentLayout);
}
@@ -104,6 +106,7 @@ public class UserMessagesView extends Main implements HasUrlParameter<String>, H
layout.setWidthFull();
layout.setAlignItems(com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment.CENTER);
layout.setSpacing(true);
layout.addClassName("message-thread-header");
return layout;
}
@@ -112,12 +115,11 @@ public class UserMessagesView extends Main implements HasUrlParameter<String>, H
VerticalLayout section = new VerticalLayout();
section.setPadding(true);
section.setSpacing(true);
section.getStyle().set("border", "1px solid #e0e0e0");
section.getStyle().set("border-radius", "8px");
section.setWidthFull();
section.getStyle().set("margin-right", "20px");
section.addClassName("message-section");
H3 title = new H3(getTranslation("usermessages.general.title"));
title.addClassName("message-section-title");
section.add(title);
List<Message> sortedMessages = new ArrayList<>();
@@ -145,12 +147,11 @@ public class UserMessagesView extends Main implements HasUrlParameter<String>, H
VerticalLayout section = new VerticalLayout();
section.setPadding(true);
section.setSpacing(true);
section.getStyle().set("border", "1px solid #e0e0e0");
section.getStyle().set("border-radius", "8px");
section.setWidthFull();
section.getStyle().set("margin-right", "20px");
section.addClassName("message-section");
H3 title = new H3(getTranslation("usermessages.job.title"));
title.addClassName("message-section-title");
section.add(title);
if (jobMessages == null || jobMessages.isEmpty()) {
@@ -191,40 +192,21 @@ public class UserMessagesView extends Main implements HasUrlParameter<String>, H
int messageCount, int unreadCount, String conversationId) {
Div card = new Div();
card.setWidthFull();
card.getStyle().set("padding", "15px");
card.getStyle().set("border", "1px solid #e0e0e0");
card.getStyle().set("border-radius", "8px");
card.getStyle().set("cursor", "pointer");
card.getStyle().set("background-color", "#ffffff");
card.getStyle().set("margin-bottom", "10px");
card.getStyle().set("max-width", "97.5%");
card.addClassName("message-card");
// Hover effect
card.getElement().addEventListener("mouseenter", e -> {
card.getStyle().set("background-color", "#f5f5f5");
});
card.getElement().addEventListener("mouseleave", e -> {
card.getStyle().set("background-color", "#ffffff");
});
// Title row with unread indicator
HorizontalLayout titleRow = new HorizontalLayout();
titleRow.setWidthFull();
titleRow.setAlignItems(com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment.CENTER);
titleRow.setSpacing(true);
Span titleSpan = new Span(conversationTitle);
titleSpan.getStyle().set("font-weight", "bold");
titleSpan.getStyle().set("font-size", "16px");
titleSpan.addClassName("message-section-title");
if (unreadCount > 0) {
Span unreadBadge = new Span(String.valueOf(unreadCount));
unreadBadge.getStyle().set("background-color", "var(--lumo-primary-color)");
unreadBadge.getStyle().set("color", "white");
unreadBadge.getStyle().set("border-radius", "50%");
unreadBadge.getStyle().set("padding", "2px 8px");
unreadBadge.getStyle().set("font-size", "12px");
unreadBadge.getStyle().set("font-weight", "bold");
unreadBadge.addClassName("message-badge");
titleRow.add(titleSpan, unreadBadge);
} else {
titleRow.add(titleSpan);
@@ -235,20 +217,16 @@ public class UserMessagesView extends Main implements HasUrlParameter<String>, H
// Preview text
Span preview = new Span(Optional.ofNullable(lastMessagePreview).filter(s -> !s.isBlank())
.orElse(getTranslation("usermessages.preview.empty")));
preview.getStyle().set("color", "#666666");
preview.getStyle().set("font-size", "14px");
preview.addClassName("message-preview");
// Metadata row
HorizontalLayout metaRow = new HorizontalLayout();
metaRow.setWidthFull();
metaRow.addClassName("message-meta");
Span timeSpan = new Span(lastMessageTime != null ? DateTimeFormatUtil.formatDateTime(lastMessageTime) : "-");
timeSpan.getStyle().set("color", "#999999");
timeSpan.getStyle().set("font-size", "12px");
Span countSpan = new Span(getTranslation("usermessages.message.count", messageCount));
countSpan.getStyle().set("color", "#999999");
countSpan.getStyle().set("font-size", "12px");
metaRow.add(timeSpan, countSpan);
metaRow.expand(timeSpan);

View File

@@ -21,6 +21,7 @@ public class VerwaltungView extends Main implements HasDynamicTitle {
setSizeFull();
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL);
addClassName("data-view");
add(new ViewToolbar(getTranslation("verwaltung.title")));
@@ -37,6 +38,7 @@ public class VerwaltungView extends Main implements HasDynamicTitle {
content.setDefaultHorizontalComponentAlignment(VerticalLayout.Alignment.CENTER);
content.setJustifyContentMode(VerticalLayout.JustifyContentMode.CENTER);
content.setSizeFull();
content.addClassNames("surface-panel", "section-panel");
add(content);
}