Erweiterungen

This commit is contained in:
2026-02-19 20:06:42 +01:00
parent 00811cdc36
commit 049deab12b
6 changed files with 154 additions and 115 deletions

View File

@@ -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<MenuTreeItem> 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);
// Create tree data with hierarchical menu structure
TreeData<MenuTreeItem> treeData = new TreeData<>();
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);
// 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()) {
treeData.addItem(verwaltungItem, new MenuTreeItem(getTranslation("nav.invoices"), "invoices", VaadinIcon.FILE_TEXT));
}
// 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;
});
// 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
if (isBillingEnabledForCurrentUser()) {
SideNavItem invoices = new SideNavItem(getTranslation("nav.invoices"), "invoices", new Icon(VaadinIcon.FILE_TEXT));
verwaltungContent.add(invoices);
// Handle selection/navigation
tree.addSelectionListener(event -> {
event.getFirstSelectedItem().ifPresent(item -> {
if (item.path() != null && !item.path().isEmpty()) {
UI.getCurrent().navigate(item.path());
}
});
});
verwaltungContent.add(statistics);
verwaltungDetails.add(verwaltungContent);
// Expand management and user nodes by default
tree.expand(verwaltungItem, benutzerItem);
// 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
// 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);
// 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();
});
}
} else {
if (messagesBadge != null) {
messagesNavItem.setSuffixComponent(null);
messagesBadge = null;
/**
* 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;
};
}

View File

@@ -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 {

View File

@@ -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;

View File

@@ -43,7 +43,7 @@ public class LanguageService {
locale = Locale.FRENCH;
break;
case ES:
locale = new Locale("es", "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;
};
}

View File

@@ -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

View File

@@ -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