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.applayout.AppLayout;
import com.vaadin.flow.component.avatar.Avatar; import com.vaadin.flow.component.avatar.Avatar;
import com.vaadin.flow.component.avatar.AvatarVariant; 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.Div;
import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.menubar.MenuBar; import com.vaadin.flow.component.menubar.MenuBar;
import com.vaadin.flow.component.menubar.MenuBarVariant; 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.Scroller;
import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.sidenav.SideNav; import com.vaadin.flow.component.treegrid.TreeGrid;
import com.vaadin.flow.component.sidenav.SideNavItem; 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.router.Layout;
import com.vaadin.flow.server.auth.AnonymousAllowed; 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 com.vaadin.flow.shared.Registration;
import de.assecutor.votianlt.model.User; import de.assecutor.votianlt.model.User;
import de.assecutor.votianlt.model.UserInvoiceData; 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.service.UserInvoiceDataService;
import de.assecutor.votianlt.pages.view.EditProfileView; import de.assecutor.votianlt.pages.view.EditProfileView;
import de.assecutor.votianlt.model.Language; import de.assecutor.votianlt.model.Language;
import de.assecutor.votianlt.config.TranslationProvider;
import de.assecutor.votianlt.security.SecurityService; import de.assecutor.votianlt.security.SecurityService;
import de.assecutor.votianlt.service.LanguageService; import de.assecutor.votianlt.service.LanguageService;
import de.assecutor.votianlt.service.MessageBadgeUpdateService; 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.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects;
@AnonymousAllowed @AnonymousAllowed
@Slf4j @Slf4j
@@ -55,9 +55,9 @@ public final class MainLayout extends AppLayout {
private Div headerRef; private Div headerRef;
private Scroller navRef; private Scroller navRef;
private Component userMenuRef; private Component userMenuRef;
private Span messagesBadge; // Reference to the messages badge for dynamic updates private TreeGrid<MenuTreeItem> tree;
private SideNavItem messagesNavItem; // Reference to the messages nav item private MenuTreeItem messagesTreeItem;
private Registration badgeUpdateRegistration; // Track badge update listener registration private Registration badgeUpdateRegistration;
public MainLayout(SecurityService securityService, UserInvoiceDataService userInvoiceDataService, public MainLayout(SecurityService securityService, UserInvoiceDataService userInvoiceDataService,
MessageService messageService, MessageBadgeUpdateService messageBadgeUpdateService, MessageService messageService, MessageBadgeUpdateService messageBadgeUpdateService,
@@ -107,117 +107,157 @@ public final class MainLayout extends AppLayout {
} }
private Component createSideNav() { private Component createSideNav() {
var nav = new SideNav(); // Create tree data with hierarchical menu structure
nav.addClassNames(Margin.Horizontal.MEDIUM); TreeData<MenuTreeItem> treeData = new TreeData<>();
MenuConfiguration.getMenuEntries().forEach(entry -> { // Root nodes
// Skip "Verwaltung" entry as we'll handle it separately with Details component MenuTreeItem auftragserstellungItem = new MenuTreeItem(getTranslation("nav.job.create"), "add_job", VaadinIcon.PLUS_CIRCLE);
if (!"Verwaltung".equals(entry.title())) { MenuTreeItem nachrichtenItem = new MenuTreeItem(getTranslation("nav.messages"), "messages", VaadinIcon.ENVELOPE);
SideNavItem item = createSideNavItem(entry); MenuTreeItem verwaltungItem = new MenuTreeItem(getTranslation("nav.management"), null, VaadinIcon.COG);
nav.addItem(item); 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 // Handle selection/navigation
Details verwaltungDetails = new Details(); tree.addSelectionListener(event -> {
verwaltungDetails.setSummaryText(getTranslation("nav.management")); event.getFirstSelectedItem().ifPresent(item -> {
verwaltungDetails.addClassNames(Margin.Horizontal.MEDIUM, FontSize.MEDIUM, FontWeight.MEDIUM, "#000000"); if (item.path() != null && !item.path().isEmpty()) {
UI.getCurrent().navigate(item.path());
// 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);
} }
});
});
verwaltungContent.add(statistics); // Expand management and user nodes by default
verwaltungDetails.add(verwaltungContent); tree.expand(verwaltungItem, benutzerItem);
// Create Details component for "Verwaltung" with collapsible list // Create container
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
VerticalLayout navContainer = new VerticalLayout(); VerticalLayout navContainer = new VerticalLayout();
navContainer.setPadding(false); navContainer.setPadding(false);
navContainer.setSpacing(false); navContainer.setSpacing(false);
navContainer.add(nav, verwaltungDetails, userDetails); navContainer.add(tree);
return navContainer; 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 * Updates the messages badge with the current unread count
*/ */
private void updateMessagesBadge() { private void updateMessagesBadge() {
if (messagesNavItem == null) { if (tree == null) {
return; return;
} }
long unreadCount = resolveUnreadMessageCount(); long unreadCount = resolveUnreadMessageCount();
if (unreadCount > 0) { // Get current data provider and update the messages item
if (messagesBadge == null) { TreeDataProvider<MenuTreeItem> dataProvider = (TreeDataProvider<MenuTreeItem>) tree.getDataProvider();
messagesBadge = new Span(String.valueOf(unreadCount)); TreeData<MenuTreeItem> treeData = dataProvider.getTreeData();
messagesBadge.getElement().getThemeList().add("badge");
messagesBadge.getStyle().set("background-color", "var(--lumo-primary-color)"); // Find and update the messages item with new badge count
messagesBadge.getStyle().set("color", "#ffffff"); treeData.getChildren(null).stream()
messagesBadge.getStyle().set("border-radius", "12px"); .filter(item -> "messages".equals(item.path()))
messagesBadge.getStyle().set("padding", "2px 8px"); .findFirst()
messagesBadge.getStyle().set("font-size", "12px"); .ifPresent(oldItem -> {
messagesBadge.getStyle().set("font-weight", "bold"); MenuTreeItem newItem = new MenuTreeItem(
messagesBadge.getStyle().set("min-width", "20px"); getTranslation("nav.messages"),
messagesBadge.getStyle().set("text-align", "center"); "messages",
messagesNavItem.setSuffixComponent(messagesBadge); VaadinIcon.ENVELOPE,
} else { unreadCount
messagesBadge.setText(String.valueOf(unreadCount)); );
messagesBadge.setVisible(true); messagesTreeItem = newItem;
// Refresh to show updated badge
dataProvider.refreshAll();
});
} }
} else {
if (messagesBadge != null) { /**
messagesNavItem.setSuffixComponent(null); * Record representing a menu item in the tree
messagesBadge = null; */
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 DE -> Locale.GERMAN;
case EN -> Locale.ENGLISH; case EN -> Locale.ENGLISH;
case FR -> Locale.FRENCH; case FR -> Locale.FRENCH;
case ES -> new Locale("es", "ES"); case ES -> Locale.of("es", "ES");
default -> Locale.GERMAN; 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.IntegerField;
import com.vaadin.flow.component.textfield.NumberField; import com.vaadin.flow.component.textfield.NumberField;
import com.vaadin.flow.data.binder.Binder; import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.router.Menu;
import com.vaadin.flow.component.Component; import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.textfield.TextArea; import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.component.tabs.TabSheet; import com.vaadin.flow.component.tabs.TabSheet;
@@ -69,7 +68,6 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@Route(value = "add_job", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) @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") @RolesAllowed("USER")
@Slf4j @Slf4j
public class AddJobView extends Main implements HasDynamicTitle { public class AddJobView extends Main implements HasDynamicTitle {

View File

@@ -10,7 +10,6 @@ import com.itextpdf.html2pdf.HtmlConverter;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.time.LocalDate; import java.time.LocalDate;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.text.NumberFormat; import java.text.NumberFormat;

View File

@@ -43,7 +43,7 @@ public class LanguageService {
locale = Locale.FRENCH; locale = Locale.FRENCH;
break; break;
case ES: case ES:
locale = new Locale("es", "ES"); locale = Locale.of("es", "ES");
break; break;
default: default:
locale = Locale.GERMAN; locale = Locale.GERMAN;
@@ -77,7 +77,7 @@ public class LanguageService {
case DE -> Locale.GERMAN; case DE -> Locale.GERMAN;
case EN -> Locale.ENGLISH; case EN -> Locale.ENGLISH;
case FR -> Locale.FRENCH; case FR -> Locale.FRENCH;
case ES -> new Locale("es", "ES"); case ES -> Locale.of("es", "ES");
default -> Locale.GERMAN; default -> Locale.GERMAN;
}; };
} }

View File

@@ -1,12 +1,13 @@
# Navigation and Main Layout # Navigation and Main Layout
nav.jobs=Aufträge nav.jobs=Aufträge
nav.job.create=Auftragserstellung
nav.customers=Kunden nav.customers=Kunden
nav.appusers=App-Nutzer nav.appusers=App-Nutzer
nav.statistics=Statistiken nav.statistics=Statistiken
nav.invoices=Rechnungen nav.invoices=Rechnungen
nav.messages=Nachrichten nav.messages=Nachrichten
nav.profile=Mein Profil nav.profile=Mein Profil
nav.myinvoices=Meine Rechnungen nav.myinvoices=Rechnungen
nav.imprint=Impressum nav.imprint=Impressum
nav.management=Verwaltung nav.management=Verwaltung
nav.users=Benutzer nav.users=Benutzer
@@ -239,7 +240,7 @@ page.title.invoices=Rechnungen
page.title.appusers=App-Nutzer page.title.appusers=App-Nutzer
page.title.job.history=Job Historie page.title.job.history=Job Historie
page.title.message.history=Nachrichtenverlauf page.title.message.history=Nachrichtenverlauf
page.title.myinvoices=Meine Rechnungen page.title.myinvoices=Rechnungen
page.title.job.create=Neuen Auftrag anlegen page.title.job.create=Neuen Auftrag anlegen
page.title.job.summary=Zusammenfassung page.title.job.summary=Zusammenfassung
page.title.pricetable=Preis-Tabelle page.title.pricetable=Preis-Tabelle
@@ -646,7 +647,7 @@ invoices.column.amount=Betrag
invoices.column.description=Beschreibung invoices.column.description=Beschreibung
# My Invoices # My Invoices
myinvoices.title=Meine Rechnungen myinvoices.title=Rechnungen
myinvoices.hint.noopen=Sie haben keine offenen Rechnungen. Alle Rechnungen sind beglichen. myinvoices.hint.noopen=Sie haben keine offenen Rechnungen. Alle Rechnungen sind beglichen.
myinvoices.bank.institute=Bank myinvoices.bank.institute=Bank
myinvoices.bank.beneficiary=Empfänger myinvoices.bank.beneficiary=Empfänger

View File

@@ -1,5 +1,6 @@
# Navigation and Main Layout # Navigation and Main Layout
nav.jobs=Jobs nav.jobs=Jobs
nav.job.create=Create New Job
nav.customers=Customers nav.customers=Customers
nav.appusers=App Users nav.appusers=App Users
nav.statistics=Statistics nav.statistics=Statistics