From 049deab12bdb95407f3ae64818217b501f6afe71 Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Thu, 19 Feb 2026 20:06:42 +0100 Subject: [PATCH] Erweiterungen --- .../pages/base/ui/view/MainLayout.java | 250 ++++++++++-------- .../votianlt/pages/view/AddJobView.java | 2 - .../service/CustomerInvoiceService.java | 1 - .../votianlt/service/LanguageService.java | 8 +- src/main/resources/messages.properties | 7 +- src/main/resources/messages_en.properties | 1 + 6 files changed, 154 insertions(+), 115 deletions(-) diff --git a/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java b/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java index a87fc39..242251b 100644 --- a/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java +++ b/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java @@ -7,21 +7,21 @@ 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.details.Details; import com.vaadin.flow.component.html.Div; 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.menubar.MenuBar; import com.vaadin.flow.component.menubar.MenuBarVariant; +import com.vaadin.flow.component.orderedlayout.FlexComponent; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.Scroller; import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.component.sidenav.SideNav; -import com.vaadin.flow.component.sidenav.SideNavItem; +import com.vaadin.flow.component.treegrid.TreeGrid; +import com.vaadin.flow.data.provider.hierarchy.TreeData; +import com.vaadin.flow.data.provider.hierarchy.TreeDataProvider; import com.vaadin.flow.router.Layout; import com.vaadin.flow.server.auth.AnonymousAllowed; -import com.vaadin.flow.server.menu.MenuConfiguration; -import com.vaadin.flow.server.menu.MenuEntry; import com.vaadin.flow.shared.Registration; import de.assecutor.votianlt.model.User; import de.assecutor.votianlt.model.UserInvoiceData; @@ -29,7 +29,6 @@ import de.assecutor.votianlt.pages.service.AppUserService; import de.assecutor.votianlt.pages.service.UserInvoiceDataService; import de.assecutor.votianlt.pages.view.EditProfileView; import de.assecutor.votianlt.model.Language; -import de.assecutor.votianlt.config.TranslationProvider; import de.assecutor.votianlt.security.SecurityService; import de.assecutor.votianlt.service.LanguageService; import de.assecutor.votianlt.service.MessageBadgeUpdateService; @@ -40,6 +39,7 @@ import static com.vaadin.flow.theme.lumo.LumoUtility.*; import java.util.List; import java.util.Locale; +import java.util.Objects; @AnonymousAllowed @Slf4j @@ -55,9 +55,9 @@ public final class MainLayout extends AppLayout { private Div headerRef; private Scroller navRef; private Component userMenuRef; - private Span messagesBadge; // Reference to the messages badge for dynamic updates - private SideNavItem messagesNavItem; // Reference to the messages nav item - private Registration badgeUpdateRegistration; // Track badge update listener registration + private TreeGrid tree; + private MenuTreeItem messagesTreeItem; + private Registration badgeUpdateRegistration; public MainLayout(SecurityService securityService, UserInvoiceDataService userInvoiceDataService, MessageService messageService, MessageBadgeUpdateService messageBadgeUpdateService, @@ -107,117 +107,157 @@ public final class MainLayout extends AppLayout { } private Component createSideNav() { - var nav = new SideNav(); - nav.addClassNames(Margin.Horizontal.MEDIUM); - - MenuConfiguration.getMenuEntries().forEach(entry -> { - // Skip "Verwaltung" entry as we'll handle it separately with Details component - if (!"Verwaltung".equals(entry.title())) { - SideNavItem item = createSideNavItem(entry); - nav.addItem(item); - } - }); - - // Create Details component for "Verwaltung" with collapsible list - Details verwaltungDetails = new Details(); - verwaltungDetails.setSummaryText(getTranslation("nav.management")); - verwaltungDetails.addClassNames(Margin.Horizontal.MEDIUM, FontSize.MEDIUM, FontWeight.MEDIUM, "#000000"); - - // Create collapsible content with navigation items - VerticalLayout verwaltungContent = new VerticalLayout(); - verwaltungContent.setPadding(false); - verwaltungContent.setSpacing(true); - - // Create navigation items for the collapsible list - SideNavItem jobs = new SideNavItem(getTranslation("nav.jobs"), "jobs", new Icon(VaadinIcon.LIST)); - SideNavItem customers = new SideNavItem(getTranslation("nav.customers"), "customers", new Icon(VaadinIcon.USERS)); - SideNavItem appUsers = new SideNavItem(getTranslation("nav.appusers"), "app-user", new Icon(VaadinIcon.USERS)); - SideNavItem statistics = new SideNavItem(getTranslation("nav.statistics"), "statistics", new Icon(VaadinIcon.BAR_CHART)); - - verwaltungContent.add(jobs, customers, appUsers, statistics); - - // Only show invoices menu if billing is enabled for the current user + // Create tree data with hierarchical menu structure + TreeData treeData = new TreeData<>(); + + // Root nodes + MenuTreeItem auftragserstellungItem = new MenuTreeItem(getTranslation("nav.job.create"), "add_job", VaadinIcon.PLUS_CIRCLE); + MenuTreeItem nachrichtenItem = new MenuTreeItem(getTranslation("nav.messages"), "messages", VaadinIcon.ENVELOPE); + 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); + treeData.addItem(null, verwaltungItem); + treeData.addItem(null, benutzerItem); + + // Add children to "Verwaltung" + treeData.addItem(verwaltungItem, new MenuTreeItem(getTranslation("nav.customers"), "customers", VaadinIcon.USERS)); + treeData.addItem(verwaltungItem, new MenuTreeItem(getTranslation("nav.appusers"), "app-user", VaadinIcon.USERS)); + treeData.addItem(verwaltungItem, new MenuTreeItem(getTranslation("nav.statistics"), "statistics", VaadinIcon.BAR_CHART)); + + // Add invoices only if billing is enabled if (isBillingEnabledForCurrentUser()) { - SideNavItem invoices = new SideNavItem(getTranslation("nav.invoices"), "invoices", new Icon(VaadinIcon.FILE_TEXT)); - verwaltungContent.add(invoices); + treeData.addItem(verwaltungItem, new MenuTreeItem(getTranslation("nav.invoices"), "invoices", VaadinIcon.FILE_TEXT)); } - - verwaltungContent.add(statistics); - verwaltungDetails.add(verwaltungContent); - - // Create Details component for "Verwaltung" with collapsible list - Details userDetails = new Details(); - userDetails.setSummaryText(getTranslation("nav.users")); - userDetails.addClassNames(Margin.Horizontal.MEDIUM, FontSize.MEDIUM, FontWeight.MEDIUM, TextColor.BODY); - - // Create collapsible content with navigation items - VerticalLayout userContent = new VerticalLayout(); - userContent.setPadding(false); - userContent.setSpacing(true); - - // Create navigation items for the collapsible list - SideNavItem profile = new SideNavItem(getTranslation("nav.profile"), "edit-profile", new Icon(VaadinIcon.USER)); - SideNavItem myInvoices = new SideNavItem(getTranslation("nav.myinvoices"), "my-invoices", new Icon(VaadinIcon.FILE_TEXT)); - SideNavItem imprint = new SideNavItem(getTranslation("nav.imprint"), "impressum", new Icon(VaadinIcon.INFO_CIRCLE)); - - userContent.add(profile, myInvoices, imprint); - userDetails.add(userContent); - - // Create a vertical layout to hold both regular menu items and collapsible - // sections + + // Add children to "Benutzer" + treeData.addItem(benutzerItem, new MenuTreeItem(getTranslation("nav.profile"), "edit-profile", VaadinIcon.USER)); + treeData.addItem(benutzerItem, new MenuTreeItem(getTranslation("nav.myinvoices"), "my-invoices", VaadinIcon.FILE_TEXT)); + treeData.addItem(benutzerItem, new MenuTreeItem(getTranslation("nav.imprint"), "impressum", VaadinIcon.INFO_CIRCLE)); + + // Create Tree + tree = new TreeGrid<>(); + tree.setDataProvider(new TreeDataProvider<>(treeData)); + tree.addClassNames(Margin.Horizontal.MEDIUM); + + // Custom item renderer to show icon and label with badge + tree.addComponentHierarchyColumn(item -> { + HorizontalLayout row = new HorizontalLayout(); + row.setAlignItems(FlexComponent.Alignment.CENTER); + row.setSpacing(true); + row.setPadding(false); + row.setMargin(false); + row.setWidthFull(); + + // 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)"); + row.add(icon); + } + + // Label + Span label = new Span(item.label()); + label.getStyle().set("font-size", "var(--lumo-font-size-s)"); + row.add(label); + row.setFlexGrow(1, label); + + // Badge for messages + if (item == messagesTreeItem && item.badgeCount() > 0) { + Span badge = new Span(String.valueOf(item.badgeCount())); + 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"); + row.add(badge); + } + + return row; + }); + + // Handle selection/navigation + tree.addSelectionListener(event -> { + event.getFirstSelectedItem().ifPresent(item -> { + if (item.path() != null && !item.path().isEmpty()) { + UI.getCurrent().navigate(item.path()); + } + }); + }); + + // Expand management and user nodes by default + tree.expand(verwaltungItem, benutzerItem); + + // Create container VerticalLayout navContainer = new VerticalLayout(); navContainer.setPadding(false); navContainer.setSpacing(false); - navContainer.add(nav, verwaltungDetails, userDetails); - + navContainer.add(tree); + return navContainer; } - private SideNavItem createSideNavItem(MenuEntry menuEntry) { - SideNavItem item; - if (menuEntry.icon() != null) { - item = new SideNavItem(menuEntry.title(), menuEntry.path(), new Icon(menuEntry.icon())); - } else { - item = new SideNavItem(menuEntry.title(), menuEntry.path()); - } - if ("Nachrichten".equals(menuEntry.title())) { - messagesNavItem = item; - } - return item; - } - /** * Updates the messages badge with the current unread count */ private void updateMessagesBadge() { - if (messagesNavItem == null) { + if (tree == null) { return; } long unreadCount = resolveUnreadMessageCount(); - - if (unreadCount > 0) { - if (messagesBadge == null) { - messagesBadge = new Span(String.valueOf(unreadCount)); - messagesBadge.getElement().getThemeList().add("badge"); - messagesBadge.getStyle().set("background-color", "var(--lumo-primary-color)"); - messagesBadge.getStyle().set("color", "#ffffff"); - messagesBadge.getStyle().set("border-radius", "12px"); - messagesBadge.getStyle().set("padding", "2px 8px"); - messagesBadge.getStyle().set("font-size", "12px"); - messagesBadge.getStyle().set("font-weight", "bold"); - messagesBadge.getStyle().set("min-width", "20px"); - messagesBadge.getStyle().set("text-align", "center"); - messagesNavItem.setSuffixComponent(messagesBadge); - } else { - messagesBadge.setText(String.valueOf(unreadCount)); - messagesBadge.setVisible(true); - } - } else { - if (messagesBadge != null) { - messagesNavItem.setSuffixComponent(null); - messagesBadge = null; - } + + // Get current data provider and update the messages item + TreeDataProvider dataProvider = (TreeDataProvider) tree.getDataProvider(); + TreeData 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(); + }); + } + + /** + * 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); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MenuTreeItem that = (MenuTreeItem) o; + return Objects.equals(label, that.label) && Objects.equals(path, that.path); + } + + @Override + public int hashCode() { + return Objects.hash(label, path); } } @@ -349,7 +389,7 @@ public final class MainLayout extends AppLayout { case DE -> Locale.GERMAN; case EN -> Locale.ENGLISH; case FR -> Locale.FRENCH; - case ES -> new Locale("es", "ES"); + case ES -> Locale.of("es", "ES"); default -> Locale.GERMAN; }; } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java index 1b56726..336541f 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java @@ -23,7 +23,6 @@ import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.textfield.IntegerField; import com.vaadin.flow.component.textfield.NumberField; import com.vaadin.flow.data.binder.Binder; -import com.vaadin.flow.router.Menu; import com.vaadin.flow.component.Component; import com.vaadin.flow.component.textfield.TextArea; import com.vaadin.flow.component.tabs.TabSheet; @@ -69,7 +68,6 @@ import java.util.Objects; import java.util.Optional; @Route(value = "add_job", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) -@Menu(order = 0, icon = "vaadin:clipboard-check", title = "Auftragserstellung") @RolesAllowed("USER") @Slf4j public class AddJobView extends Main implements HasDynamicTitle { diff --git a/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java b/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java index e25c035..8e2d264 100644 --- a/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java +++ b/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java @@ -10,7 +10,6 @@ import com.itextpdf.html2pdf.HtmlConverter; import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.time.LocalDate; import java.math.BigDecimal; import java.text.NumberFormat; diff --git a/src/main/java/de/assecutor/votianlt/service/LanguageService.java b/src/main/java/de/assecutor/votianlt/service/LanguageService.java index 6c09c02..6f5ab50 100644 --- a/src/main/java/de/assecutor/votianlt/service/LanguageService.java +++ b/src/main/java/de/assecutor/votianlt/service/LanguageService.java @@ -42,9 +42,9 @@ public class LanguageService { case FR: locale = Locale.FRENCH; break; - case ES: - locale = new Locale("es", "ES"); - break; + case ES: + locale = Locale.of("es", "ES"); + break; default: locale = Locale.GERMAN; } @@ -77,7 +77,7 @@ public class LanguageService { case DE -> Locale.GERMAN; case EN -> Locale.ENGLISH; case FR -> Locale.FRENCH; - case ES -> new Locale("es", "ES"); + case ES -> Locale.of("es", "ES"); default -> Locale.GERMAN; }; } diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 8f2a343..5c2c7d8 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -1,12 +1,13 @@ # Navigation and Main Layout nav.jobs=Aufträge +nav.job.create=Auftragserstellung nav.customers=Kunden nav.appusers=App-Nutzer nav.statistics=Statistiken nav.invoices=Rechnungen nav.messages=Nachrichten nav.profile=Mein Profil -nav.myinvoices=Meine Rechnungen +nav.myinvoices=Rechnungen nav.imprint=Impressum nav.management=Verwaltung nav.users=Benutzer @@ -239,7 +240,7 @@ page.title.invoices=Rechnungen page.title.appusers=App-Nutzer page.title.job.history=Job Historie page.title.message.history=Nachrichtenverlauf -page.title.myinvoices=Meine Rechnungen +page.title.myinvoices=Rechnungen page.title.job.create=Neuen Auftrag anlegen page.title.job.summary=Zusammenfassung page.title.pricetable=Preis-Tabelle @@ -646,7 +647,7 @@ invoices.column.amount=Betrag invoices.column.description=Beschreibung # My Invoices -myinvoices.title=Meine Rechnungen +myinvoices.title=Rechnungen myinvoices.hint.noopen=Sie haben keine offenen Rechnungen. Alle Rechnungen sind beglichen. myinvoices.bank.institute=Bank myinvoices.bank.beneficiary=Empfänger diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties index 7775be7..a2349b6 100644 --- a/src/main/resources/messages_en.properties +++ b/src/main/resources/messages_en.properties @@ -1,5 +1,6 @@ # Navigation and Main Layout nav.jobs=Jobs +nav.job.create=Create New Job nav.customers=Customers nav.appusers=App Users nav.statistics=Statistics