|
|
|
|
@@ -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);
|
|
|
|
|
|
|
|
|
|
// 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");
|
|
|
|
|
// Store reference to messages item for badge updates
|
|
|
|
|
messagesTreeItem = nachrichtenItem;
|
|
|
|
|
|
|
|
|
|
// Create collapsible content with navigation items
|
|
|
|
|
VerticalLayout verwaltungContent = new VerticalLayout();
|
|
|
|
|
verwaltungContent.setPadding(false);
|
|
|
|
|
verwaltungContent.setSpacing(true);
|
|
|
|
|
// Add root items
|
|
|
|
|
treeData.addItem(null, auftragserstellungItem);
|
|
|
|
|
treeData.addItem(null, nachrichtenItem);
|
|
|
|
|
treeData.addItem(null, verwaltungItem);
|
|
|
|
|
treeData.addItem(null, benutzerItem);
|
|
|
|
|
|
|
|
|
|
// 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));
|
|
|
|
|
// 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));
|
|
|
|
|
|
|
|
|
|
verwaltungContent.add(jobs, customers, appUsers, statistics);
|
|
|
|
|
|
|
|
|
|
// Only show invoices menu if billing is enabled for the current user
|
|
|
|
|
// 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);
|
|
|
|
|
// 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 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 Tree
|
|
|
|
|
tree = new TreeGrid<>();
|
|
|
|
|
tree.setDataProvider(new TreeDataProvider<>(treeData));
|
|
|
|
|
tree.addClassNames(Margin.Horizontal.MEDIUM);
|
|
|
|
|
|
|
|
|
|
// Create collapsible content with navigation items
|
|
|
|
|
VerticalLayout userContent = new VerticalLayout();
|
|
|
|
|
userContent.setPadding(false);
|
|
|
|
|
userContent.setSpacing(true);
|
|
|
|
|
// 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();
|
|
|
|
|
|
|
|
|
|
// 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));
|
|
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
userContent.add(profile, myInvoices, imprint);
|
|
|
|
|
userDetails.add(userContent);
|
|
|
|
|
// Label
|
|
|
|
|
Span label = new Span(item.label());
|
|
|
|
|
label.getStyle().set("font-size", "var(--lumo-font-size-s)");
|
|
|
|
|
row.add(label);
|
|
|
|
|
row.setFlexGrow(1, label);
|
|
|
|
|
|
|
|
|
|
// Create a vertical layout to hold both regular menu items and collapsible
|
|
|
|
|
// sections
|
|
|
|
|
// 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<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();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|